local addonName, ns = ...

if addonName ~= "NomTools" then
    ns = _G.NomTools
    addonName = ns and ns.ADDON_NAME or "NomTools"
end

if not ns then
    return
end

local CONFIG_KEY = "objectiveTracker"
local FOCUSED_TRACKER_NAME = addonName .. "FocusedObjectiveTracker"
local FOCUSED_TRACKER_HEADER = "Focused Quest"
local ZONE_TRACKER_NAME = addonName .. "ZoneObjectiveTracker"
local ZONE_TRACKER_HEADER = ZONE or "Zone"
local TRACK_ALL_BUTTON_TEXT = QUEST_LOG_TRACK_ALL or "Track All"
-- Frequently-referenced layout constants kept as top-level locals for readability.
local TRACKER_SCROLL_CLIP_LEFT_PADDING = 20
local TRACKER_MODULE_HEADER_RIGHT_EXTENSION = 0
local DEFAULT_STATUSBAR_TEXTURE_PATH = "Interface\\TargetingFrame\\UI-StatusBar"
local CanAccessValue = canaccessvalue
local IsSecretValue = issecretvalue

-- All other constants live in a single table to stay under the 200-local chunk limit.
local K = {
    TRACKER_SCROLL_STEP = 48,
    TRACKER_MAX_LAYOUT_HEIGHT = 10000,
    TRACKER_LAYOUT_INITIAL_BUDGET = 1200,
    TRACKER_LAYOUT_BUDGET_PADDING = 400,
    ZONE_TASK_RETRY_INTERVAL_SECONDS = 0.4,
    ZONE_TASK_RETRY_MAX_ATTEMPTS = 20,
    -- Coalescing window for rapid-fire events (SCENARIO_CRITERIA_UPDATE,
    -- QUEST_POI_UPDATE). Limits MarkDirty to at most once per window.
    MARK_DIRTY_THROTTLE_SECONDS = 0.15,
    TRACKER_BUTTON_WIDTH = 78,
    TRACKER_BUTTON_HEIGHT = 18,
    QUEST_LOG_BUTTON_WIDTH = 78,
    QUEST_LOG_BUTTON_HEIGHT = 20,
    SCROLL_BAR_DEFAULT_WIDTH = 4,
    SCROLL_BAR_MIN_WIDTH = 1,
    SCROLL_BAR_MAX_WIDTH = 24,
    SCROLL_BAR_MIN_THUMB_HEIGHT = 24,
    TRACKER_RIGHT_INSET_PADDING = 10,
    TRACKER_HEADER_BUTTON_RIGHT_INSET = 4,
    TRACKER_VISUAL_LEFT_EXTENSION = 10,
    TRACKER_HEADER_TEXT = "Objective Tracker",
    LEVEL_PREFIX_MODE_ALL = "all",
    LEVEL_PREFIX_MODE_TRIVIAL = "trivial",
    LEVEL_PREFIX_MODE_NONE = "none",
    WARBAND_COMPLETION_TOOLTIP = ACCOUNT_COMPLETED_QUEST_NOTICE or "Your Warband previously completed this quest.",
    WARBAND_COMPLETION_ICON_SIZE = 20,
    WARBAND_COMPLETION_ICON_ATLASES = {
        "warbands-icon",
        "questlog-questtypeicon-account",
    },
    WARBAND_COMPLETION_CHECK_ATLASES = {
        "common-icon-checkmark-yellow",
        "common-icon-checkmark",
    },
    DEFAULT_PROGRESS_BORDER_SIZE = 1,
    MIN_PROGRESS_BORDER_SIZE = -10,
    MAX_PROGRESS_BORDER_SIZE = 10,
    DEFAULT_CHROME_BORDER_SIZE = 1,
    MIN_CHROME_BORDER_SIZE = -10,
    MAX_CHROME_BORDER_SIZE = 10,
    DEFAULT_SCROLL_BAR_COLOR = {
        r = 1,
        g = 0.82,
        b = 0,
        a = 0.9,
    },
    LEGACY_SCROLL_BAR_COLOR = {
        r = 0.88,
        g = 0.88,
        b = 0.88,
        a = 0.9,
    },
    DEFAULT_SCROLL_BAR_BACKGROUND_COLOR = {
        r = 0,
        g = 0,
        b = 0,
        a = 0.8,
    },
    DEFAULT_SCROLL_BAR_BORDER_COLOR = {
        r = 0,
        g = 0,
        b = 0,
        a = 1,
    },
    DEFAULT_UNCOMPLETED_COLOR = {
        r = 1,
        g = 1,
        b = 1,
        a = 1,
    },
    DEFAULT_COMPLETED_COLOR = {
        r = 0.36,
        g = 0.95,
        b = 0.45,
        a = 1,
    },
    DEFAULT_PROGRESS_FILL_COLOR = {
        r = 0.26,
        g = 0.42,
        b = 1,
        a = 1,
    },
    DEFAULT_PROGRESS_LOW_FILL_COLOR = {
        r = 0.90,
        g = 0.18,
        b = 0.18,
        a = 1,
    },
    DEFAULT_PROGRESS_MEDIUM_FILL_COLOR = {
        r = 0.95,
        g = 0.82,
        b = 0.18,
        a = 1,
    },
    DEFAULT_PROGRESS_HIGH_FILL_COLOR = {
        r = 0.28,
        g = 0.82,
        b = 0.32,
        a = 1,
    },
    DEFAULT_PROGRESS_BACKGROUND_COLOR = {
        r = 0.04,
        g = 0.07,
        b = 0.18,
        a = 0.85,
    },
    LEGACY_PROGRESS_BORDER_COLOR = {
        r = 0.96,
        g = 0.82,
        b = 0.28,
        a = 1,
    },
    DEFAULT_PROGRESS_BORDER_COLOR = {
        r = 0,
        g = 0,
        b = 0,
        a = 1,
    },
    DEFAULT_HEADER_BACKGROUND_COLOR = {
        r = 0,
        g = 0,
        b = 0,
        a = 1,
    },
    DEFAULT_HEADER_BORDER_COLOR = {
        r = 0,
        g = 0,
        b = 0,
        a = 1,
    },
    DEFAULT_ZONE_DIVIDER_BG_COLOR = {
        r = 0.05,
        g = 0.05,
        b = 0.05,
        a = 0.6,
    },
    DEFAULT_ZONE_DIVIDER_BORDER_COLOR = {
        r = 0.3,
        g = 0.3,
        b = 0.3,
        a = 1,
    },
    DEFAULT_ZONE_DIVIDER_TEXT_COLOR = {
        r = 0.8,
        g = 0.72,
        b = 0.42,
        a = 1,
    },
    DEFAULT_ZONE_DIVIDER_LINE_COLOR = {
        r = 0.8,
        g = 0.72,
        b = 0.42,
        a = 0.5,
    },
    -- FPS experiment: while true, removes Scenario + UIWidget tracker modules from
    -- ObjectiveTrackerFrame whenever a delve context is active (see
    -- delveScenarioWidgetTest_IsDelveContext in SyncDelveScenarioWidgetTestRemoval). Not in options.
    DEBUG_HIDE_DELVE_SCENARIO_WIDGET_TEST = false,
}
K.GRADIENT_SOLID = CreateColor(1, 1, 1, 1)
K.GRADIENT_TRANSPARENT = CreateColor(1, 1, 1, 0)
K.DEFAULT_QUEST_KIND_TITLE_COLORS = {
    quest = {
        r = 1,
        g = 0.82,
        b = 0,
        a = 1,
    },
    worldQuest = {
        r = 0.46,
        g = 0.84,
        b = 1,
        a = 1,
    },
    bonusObjective = {
        r = 0.18,
        g = 0.88,
        b = 0.76,
        a = 1,
    },
}
K.DEFAULT_SPECIAL_TITLE_COLORS = {
    quest = K.DEFAULT_QUEST_KIND_TITLE_COLORS.quest,
    worldQuest = K.DEFAULT_QUEST_KIND_TITLE_COLORS.worldQuest,
    bonusObjective = K.DEFAULT_QUEST_KIND_TITLE_COLORS.bonusObjective,
    daily = {
        r = 0.32,
        g = 0.63,
        b = 1,
        a = 1,
    },
    weekly = {
        r = 0.32,
        g = 0.63,
        b = 1,
        a = 1,
    },
    meta = {
        r = 0.32,
        g = 0.63,
        b = 1,
        a = 1,
    },
    important = {
        r = 0.52,
        g = 0.38,
        b = 0.74,
        a = 1,
    },
    prey = {
        r = 0.92,
        g = 0.18,
        b = 0.18,
        a = 1,
    },
    campaign = {
        r = 1,
        g = 0.4,
        b = 0.7,
        a = 1,
    },
    trivial = {
        r = 0.58,
        g = 0.58,
        b = 0.58,
        a = 1,
    },
    legendary = {
        r = 1,
        g = 0.5,
        b = 0,
        a = 1,
    },
}
K.LEGACY_ZONE_TITLE_COLOR_KEYS = {
    "quest",
    "worldQuest",
    "bonusObjective",
}
K.UIPANEL_BUTTON_ART_REGION_KEYS = {
    "Left",
    "Middle",
    "Right",
    "LeftDisabled",
    "MiddleDisabled",
    "RightDisabled",
    "LeftHighlight",
    "MiddleHighlight",
    "RightHighlight",
}

-- The "search" key is intentionally NOT in the order list. The search bar
-- is no longer a reorderable section in the module chain; it is docked
-- directly beneath the main tracker header via ApplySearchFramePosition,
-- outside the scroll region. Any legacy saved "search" entry in
-- settings.order is silently ignored by BuildOrderedModules and
-- GetOrderedContainerModules.
K.DEFAULT_ORDER = {
    "scenario",
    "uiWidget",
    "focusedQuest",
    "campaign",
    "zone",
    "quest",
    "adventure",
    "achievement",
    "monthlyActivities",
    "initiativeTasks",
    "professionsRecipe",
    "bonusObjective",
    "worldQuest",
}

K.LEGACY_DEFAULT_ORDER = {
    "scenario",
    "uiWidget",
    "campaign",
    "zone",
    "focusedQuest",
    "quest",
    "adventure",
    "achievement",
    "monthlyActivities",
    "initiativeTasks",
    "professionsRecipe",
    "bonusObjective",
    "worldQuest",
}

local RoundNearestInteger
local NormalizeBorderSize

RoundNearestInteger = function(value)
    value = tonumber(value) or 0
    if value >= 0 then
        return math.floor(value + 0.5)
    end

    return math.ceil(value - 0.5)
end

NormalizeBorderSize = function(value, defaultValue, minimum, maximum)
    local resolvedValue = tonumber(value)
    if resolvedValue == nil then
        resolvedValue = tonumber(defaultValue) or 1
    end

    resolvedValue = RoundNearestInteger(resolvedValue)
    if resolvedValue < minimum then
        return minimum
    end
    if resolvedValue > maximum then
        return maximum
    end

    return resolvedValue
end

local function ResolvePublicNumber(value, fallback)
    local resolvedValue

    if value ~= nil then
        local accessible = true
        if CanAccessValue then
            accessible = CanAccessValue(value)
        end
        if accessible and IsSecretValue then
            accessible = not IsSecretValue(value)
        end
        if accessible then
            resolvedValue = tonumber(value)
        end
    end

    if resolvedValue ~= nil then
        return resolvedValue
    end

    if fallback ~= nil then
        local accessibleFallback = true
        if CanAccessValue then
            accessibleFallback = CanAccessValue(fallback)
        end
        if accessibleFallback and IsSecretValue then
            accessibleFallback = not IsSecretValue(fallback)
        end
        if accessibleFallback then
            return tonumber(fallback)
        end
    end

    return nil
end

K.ORDER_LABELS = {
    focusedQuest = FOCUSED_TRACKER_HEADER,
    scenario = TRACKER_HEADER_SCENARIO or "Scenario",
    uiWidget = "Zone Widgets",
    campaign = TRACKER_HEADER_CAMPAIGN_QUESTS or "Campaign Quests",
    zone = ZONE_TRACKER_HEADER,
    quest = TRACKER_HEADER_QUESTS or "Quests",
    adventure = ADVENTURE_TRACKING_MODULE_HEADER_TEXT or "Adventure",
    achievement = TRACKER_HEADER_ACHIEVEMENTS or "Achievements",
    monthlyActivities = TRACKER_HEADER_MONTHLY_ACTIVITIES or "Monthly Activities",
    initiativeTasks = TRACKER_HEADER_INITIATIVE_TASKS or "Initiative Tasks",
    professionsRecipe = "Professions",
    bonusObjective = TRACKER_HEADER_BONUS_OBJECTIVES or "Bonus Objectives",
    worldQuest = TRACKER_HEADER_WORLD_QUESTS or "World Quests",
    -- NOTE: "search" is intentionally omitted. The search bar is docked
    -- directly below the tracker header and is not a reorderable
    -- category anymore, so it should not appear in the Category Order
    -- options list or in the normalized settings.order. Legacy entries
    -- are silently dropped by GetNormalizedOrder (which requires an
    -- ORDER_LABELS entry for a key to survive normalization).
}

K.ZONE_FILTER_LABELS = {
    regularQuests = "Regular quests",
    campaignQuests = "Campaign quests",
    worldQuests = "World quests",
    bonusObjectives = "Bonus objectives",
}

K.ZONE_FILTER_KEYS = {
    "regularQuests",
    "campaignQuests",
    "worldQuests",
    "bonusObjectives",
}

K.SEARCH_FRAME_HEIGHT = 16
K.SEARCH_DEBOUNCE_INTERVAL = 0.25
K.SEARCH_PLACEHOLDER_TEXT = "Search quests..."

K.FILTER_QUEST_TYPE_KEYS = {
    "regular",
    "campaign",
    "daily",
    "weekly",
    "prey",
    "legendary",
    "important",
    "meta",
    "bonusObjective",
    "worldQuest",
}

K.FILTER_QUEST_TYPE_LABELS = {
    regular = "Regular",
    campaign = TRACKER_HEADER_CAMPAIGN_QUESTS or "Campaign",
    daily = DAILY or "Daily",
    weekly = WEEKLY or "Weekly",
    prey = "Prey",
    legendary = "Legendary",
    important = "Important",
    meta = "Meta",
    bonusObjective = TRACKER_HEADER_BONUS_OBJECTIVES or "Bonus Objectives",
    worldQuest = TRACKER_HEADER_WORLD_QUESTS or "World Quests",
}

K.SORT_MODE_KEYS = {
    "default",
    "name",
}

K.SORT_MODE_LABELS = {
    default = "Default (Blizzard)",
    name = "Name (Alphabetical)",
}

K.FILTER_DROP_WIDTH = 200

K.ZONE_TRACKER_SETTINGS = {
    headerText = ZONE_TRACKER_HEADER,
    events = {
        "PLAYER_ENTERING_WORLD",
        "ZONE_CHANGED_NEW_AREA",
        "ZONE_CHANGED",
        "QUEST_LOG_UPDATE",
        "QUEST_DATA_LOAD_RESULT",
        "QUEST_WATCH_LIST_CHANGED",
        "QUEST_AUTOCOMPLETE",
        "SUPER_TRACKING_CHANGED",
        "QUEST_TURNED_IN",
        "QUEST_POI_UPDATE",
        "CRITERIA_COMPLETE",
        "SCENARIO_BONUS_VISIBILITY_UPDATE",
        "SCENARIO_CRITERIA_UPDATE",
        "SCENARIO_UPDATE",
        "QUEST_ACCEPTED",
        "QUEST_REMOVED",
    },
    lineTemplate = "QuestObjectiveLineTemplate",
    blockTemplate = "ObjectiveTrackerQuestPOIBlockTemplate",
    progressBarTemplate = "BonusTrackerProgressBarTemplate",
    rightEdgeFrameSpacing = 2,
    questItemButtonSettings = {
        template = "QuestObjectiveItemButtonTemplate",
        offsetX = 0,
        offsetY = 0,
    },
    findGroupButtonSettings = {
        template = "QuestObjectiveFindGroupButtonTemplate",
        offsetX = 5,
        offsetY = 2,
    },
    completedSupersededObjectives = {},
}

K.FOCUSED_TRACKER_SETTINGS = {
    headerText = FOCUSED_TRACKER_HEADER,
    events = {
        "SUPER_TRACKING_CHANGED",
        "QUEST_LOG_UPDATE",
        "QUEST_DATA_LOAD_RESULT",
        "QUEST_TURNED_IN",
        "QUEST_REMOVED",
        "QUEST_POI_UPDATE",
        "QUEST_WATCH_LIST_CHANGED",
        "QUEST_AUTOCOMPLETE",
    },
    lineTemplate = K.ZONE_TRACKER_SETTINGS.lineTemplate,
    blockTemplate = K.ZONE_TRACKER_SETTINGS.blockTemplate,
    progressBarTemplate = K.ZONE_TRACKER_SETTINGS.progressBarTemplate,
    rightEdgeFrameSpacing = K.ZONE_TRACKER_SETTINGS.rightEdgeFrameSpacing,
    questItemButtonSettings = K.ZONE_TRACKER_SETTINGS.questItemButtonSettings,
    findGroupButtonSettings = K.ZONE_TRACKER_SETTINGS.findGroupButtonSettings,
    completedSupersededObjectives = K.ZONE_TRACKER_SETTINGS.completedSupersededObjectives,
}

local eventFrame = CreateFrame("Frame")
local combatDeferralFrame = CreateFrame("Frame")
local state = {
    trackerHooksInstalled = false,
    frameHooksInstalled = false,
    managerInitHookInstalled = false,
    worldMapHooksInstalled = false,
    blockHooksInstalled = false,
    moduleLayoutHooksInstalled = false,
    moduleSlideHooksInstalled = false,
    collapseHooksInstalled = false,
    focusedQuestModule = nil,
    zoneModule = nil,
    trackerButton = nil,
    questLogButton = nil,
    scrollClipFrame = nil,
    scrollBar = nil,
    scrollOffset = 0,
    scrollActive = false,
    updatingScrollBar = false,
    currentRightInset = 0,
    layoutRefreshPending = false,
    trackerStyleRefreshPending = false,
    captureCollapseAfterStyleRefresh = false,
    lastTypographySignature = nil,
    lastSortSettingsSignature = nil,
    cachedTrackerStyleSignature = nil,
    cachedTrackerStyles = nil,
    pendingScrollAnchor = nil,
    pendingFullRefreshAfterCombat = false,
    pendingApplyTrackerDimensions = false,
    pendingApplyModuleAnchors = false,
    applyingCollapseStates = false,
    savedCollapseStatesInitialized = false,
    collapsePersistenceReady = false,
    pendingRewardDataQuestIDs = {},
    lastContentHeight = 0,
    originalModuleOrders = {},
    originalQuestShouldDisplayQuest = nil,
    originalCampaignShouldDisplayQuest = nil,
    originalBonusAddQuest = nil,
    originalWorldQuestAddQuest = nil,
    resolvedWarbandCompletionAtlas = nil,
    resolvedWarbandCompletionCheckAtlas = nil,
    moduleRuntimeEnabled = nil,
    zoneSuppressionConfigured = false,
    zoneFeaturesSuppressedInCurrentInstance = nil,
    delveScenarioWidgetsStrippedForTest = false,
    delveQuestMarkDirtyThrottleInstalled = false,
    suppressionWatcherRegistered = false,
    originalTrackerGetAvailableHeight = nil,
    nomToolsEnforcingHeight = false,
    nomToolsEnforcingPosition = false,
    lastTrackerSizeWidth = nil,
    lastTrackerSizeHeight = nil,
    lastApplied_attachToMinimap = nil,
    lastApplied_desiredHeight = nil,
    lastApplied_desiredAttachEdge = nil,
    lastApplied_desiredYOffset = nil,
    lastApplied_desiredMatchMinimapWidth = nil,
    lastApplied_desiredConfiguredWidth = nil,
    lastApplied_desiredStandaloneWidth = nil,
    lastApplied_standaloneLayoutName = nil,
    lastApplied_standalonePoint = nil,
    lastApplied_standaloneX = nil,
    lastApplied_standaloneY = nil,
    needsTrackerDimensionReplay = false,
    scrollBarVisualCache = nil,
    scrollBarBorderColor = {
        r = 0,
        g = 0,
        b = 0,
        a = 1,
    },
    postLayoutStyleDirty = true,
    searchFrame = nil,
    searchEditBox = nil,
    searchText = "",
    searchDebounceTimer = nil,
    filterButton = nil,
    filterDropFrame = nil,
    filterDropCloseListener = nil,
    cachedContainerModulesCount = -1,
    cachedContainerModuleRefs = {},
    cachedContainerModuleOrders = {},
    cachedContainerModulesSearchFrame = nil,
    cachedContainerModulesSearchOrder = nil,
    cachedContainerModulesNeedsSorting = nil,
    moduleMarkDirtyStateByModule = setmetatable({}, { __mode = "k" }),
    -- Set only during RunSortableModulesSortPass: questID -> zone name string.
    -- BuildChainSignature and GetBlockSortData both call GetQuestZoneName; without
    -- deduping, group-by-zone pays duplicate C_Map/C_TaskQuest work every frame.
    questZoneNameSortPassCache = nil,
}

-- =============================================================================
-- Performance diagnostics (toggle via /ntdiag)
--
-- Lightweight counter + timer infrastructure used to localise the per-frame
-- hot path responsible for the Delve FPS drop. When `state.diag.enabled` is
-- false (default) every helper short-circuits in one boolean check, so the
-- runtime cost when disabled is negligible. The data is stored on the `state`
-- table so it does not consume file-level local-variable budget (which is
-- already tight on this chunk — see the 200-local split notes elsewhere in
-- the file).
--
-- Interpreting dumps (Delves / scenario spam):
--   * container.update.posthook ~ Blizzard ObjectiveTrackerFrame:Update rate.
--   * Moving / quest areas: QUEST_POI_UPDATE still spams QuestObjectiveTracker even if
--     scenario/UI widget modules are removed; delveUpdateThrottleSeconds throttles
--     quest MarkDirty globally (see EnsureScenarioStyleHookInstalled).
--   * posthook.sortpass + posthook.applyanchors = NomTools work *before*
--     RunTrackerPostLayout; usually dominates Lua time when RPL short-circuits.
--   * applymoduleanchors.full_rebuild vs applymoduleanchors.shortcircuited:
--     the post-hook nils lastModuleAnchorSignature every Update to undo Blizzard
--     anchor clobber, so full_rebuild stays high; path_shortcircuit only happens
--     on other callers that do not force nil.
--   * applyanchors.path_full_total / reanchor_and_nineslice / path_shortcircuit
--     split work inside ApplyModuleAnchors.
--   * runtrackerpostlayout.total = time inside RunTrackerPostLayout (often small
--     when runtrackerpostlayout.shortcircuited dominates).
-- =============================================================================
state.diag = {
    enabled = false,
    counters = {},          -- [key] = integer count
    timers = {},            -- [key] = { count, totalMs, maxMs }
    eventCounts = {},       -- [event] = integer count
    moduleCounts = {},      -- [bucket][moduleKey] = integer count
    startTime = 0,
    lastDumpTime = 0,
    autoDumpInterval = 0,   -- seconds; 0 disables auto dump
    autoDumpFrame = nil,
}

state.DiagNow = function()
    -- debugprofilestop() returns milliseconds since session start with high
    -- precision and is much cheaper than GetTime() for sub-millisecond timing.
    return debugprofilestop and debugprofilestop() or (GetTime() * 1000)
end

state.DiagInc = function(key, count)
    if not state.diag.enabled then return end
    state.diag.counters[key] = (state.diag.counters[key] or 0) + (count or 1)
end

state.DiagTime = function(key, durationMs)
    if not state.diag.enabled then return end
    local entry = state.diag.timers[key]
    if not entry then
        entry = { count = 0, totalMs = 0, maxMs = 0 }
        state.diag.timers[key] = entry
    end
    entry.count = entry.count + 1
    entry.totalMs = entry.totalMs + durationMs
    if durationMs > entry.maxMs then
        entry.maxMs = durationMs
    end
end

state.DiagEvent = function(event)
    if not state.diag.enabled then return end
    state.diag.eventCounts[event] = (state.diag.eventCounts[event] or 0) + 1
end

state.DiagModule = function(bucket, moduleKey)
    if not state.diag.enabled then return end
    local b = state.diag.moduleCounts[bucket]
    if not b then
        b = {}
        state.diag.moduleCounts[bucket] = b
    end
    moduleKey = moduleKey or "?"
    b[moduleKey] = (b[moduleKey] or 0) + 1
end

state.DiagReset = function()
    state.diag.counters = {}
    state.diag.timers = {}
    state.diag.eventCounts = {}
    state.diag.moduleCounts = {}
    state.diag.startTime = state.DiagNow()
    state.diag.lastDumpTime = state.diag.startTime
end

local function DiagSortPairs(t, valueExtractor)
    local list = {}
    for k, v in pairs(t) do
        list[#list + 1] = { k = k, v = v, sortVal = valueExtractor and valueExtractor(v) or v }
    end
    table.sort(list, function(a, b) return (a.sortVal or 0) > (b.sortVal or 0) end)
    return list
end

state.DiagDump = function()
    local now = state.DiagNow()
    local windowSec = math.max(0.001, (now - state.diag.startTime) / 1000)
    local fmt = string.format

    print(fmt("|cffffd200[NomTools]|r Diag window=%.2fs", windowSec))

    if next(state.diag.eventCounts) then
        print("|cffffd100  Events:|r")
        for _, e in ipairs(DiagSortPairs(state.diag.eventCounts)) do
            print(fmt("    %-42s %6d  (%6.1f/s)", e.k, e.v, e.v / windowSec))
        end
    end

    if next(state.diag.counters) then
        print("|cffffd100  Counters:|r")
        for _, e in ipairs(DiagSortPairs(state.diag.counters)) do
            print(fmt("    %-42s %6d  (%6.1f/s)", e.k, e.v, e.v / windowSec))
        end
    end

    if next(state.diag.timers) then
        print("|cffffd100  Timings (avg / max / total ms, count):|r")
        local rows = DiagSortPairs(state.diag.timers, function(v) return v and v.totalMs or 0 end)
        for _, e in ipairs(rows) do
            local t = e.v
            local avg = t.count > 0 and (t.totalMs / t.count) or 0
            print(fmt("    %-42s avg %7.3f  max %7.3f  tot %8.2f  n=%d",
                e.k, avg, t.maxMs, t.totalMs, t.count))
        end
    end

    if next(state.diag.moduleCounts) then
        print("|cffffd100  Per-module:|r")
        for bucket, modCounts in pairs(state.diag.moduleCounts) do
            local parts = {}
            for _, e in ipairs(DiagSortPairs(modCounts)) do
                parts[#parts + 1] = fmt("%s=%d", e.k, e.v)
            end
            print(fmt("    %-20s %s", bucket, table.concat(parts, " ")))
        end
    end
end

local function DiagAutoDumpOnUpdate(self, elapsed)
    if not state.diag.enabled or state.diag.autoDumpInterval <= 0 then
        return
    end
    self.elapsed = (self.elapsed or 0) + (elapsed or 0)
    if self.elapsed >= state.diag.autoDumpInterval then
        self.elapsed = 0
        state.DiagDump()
        state.DiagReset()
    end
end

state.DiagSetAutoDump = function(seconds)
    state.diag.autoDumpInterval = seconds or 0
    if not state.diag.autoDumpFrame then
        state.diag.autoDumpFrame = CreateFrame("Frame")
        state.diag.autoDumpFrame:SetScript("OnUpdate", DiagAutoDumpOnUpdate)
    end
    state.diag.autoDumpFrame.elapsed = 0
end

state.DiagEnable = function(autoDumpSeconds)
    state.DiagReset()
    state.diag.enabled = true
    state.DiagSetAutoDump(autoDumpSeconds or 0)
    print("|cffffd200[NomTools]|r Diag enabled" ..
        (autoDumpSeconds and autoDumpSeconds > 0
            and (" (auto-dump every " .. autoDumpSeconds .. "s)")
            or ""))
end

state.DiagDisable = function()
    state.diag.enabled = false
    state.DiagSetAutoDump(0)
    print("|cffffd200[NomTools]|r Diag disabled")
end

ns.NomToolsObjectiveTrackerDiag = state

SLASH_NTDIAG1 = "/ntdiag"
SlashCmdList = SlashCmdList or {}
SlashCmdList["NTDIAG"] = function(msg)
    msg = (msg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "")
    local cmd, arg = msg:match("^(%S+)%s*(.-)$")
    cmd = cmd or ""
    if cmd == "" or cmd == "help" then
        print("|cffffd200[NomTools]|r Diag commands:")
        print("  /ntdiag on [seconds]   enable (optionally auto-dump every N seconds)")
        print("  /ntdiag off            disable")
        print("  /ntdiag dump           print current counters")
        print("  /ntdiag reset          reset counters")
        print("  /ntdiag status         show enabled state")
        print("  Timers: posthook.sortpass / posthook.applyanchors = cost before RPL;")
        print("    runtrackerpostlayout.total = RPL; applyanchors.* = inside anchors.")
        return
    elseif cmd == "on" or cmd == "start" then
        local seconds = tonumber(arg) or 0
        state.DiagEnable(seconds)
    elseif cmd == "off" or cmd == "stop" then
        state.DiagDisable()
    elseif cmd == "dump" then
        state.DiagDump()
    elseif cmd == "reset" then
        state.DiagReset()
        print("|cffffd200[NomTools]|r Diag counters reset")
    elseif cmd == "status" then
        print("|cffffd200[NomTools]|r Diag enabled=" .. tostring(state.diag.enabled)
            .. " autoDump=" .. tostring(state.diag.autoDumpInterval) .. "s")
    else
        print("|cffffd200[NomTools]|r Diag unknown command: " .. cmd)
    end
end

local function DrainCombatDeferredObjectiveTrackerMutations()
    if InCombatLockdown() then
        return
    end

    if state.pendingFullRefreshAfterCombat then
        state.pendingFullRefreshAfterCombat = false
        state.pendingApplyTrackerDimensions = false
        state.pendingApplyModuleAnchors = false
        combatDeferralFrame:UnregisterEvent("PLAYER_REGEN_ENABLED")
        if ns.RefreshObjectiveTrackerUI then
            ns.RefreshObjectiveTrackerUI()
        end
        return
    end

    if not state.pendingApplyTrackerDimensions and not state.pendingApplyModuleAnchors then
        combatDeferralFrame:UnregisterEvent("PLAYER_REGEN_ENABLED")
        return
    end

    local needsApplyModuleAnchors = state.pendingApplyModuleAnchors

    state.pendingApplyTrackerDimensions = false
    state.pendingApplyModuleAnchors = false
    combatDeferralFrame:UnregisterEvent("PLAYER_REGEN_ENABLED")

    if ns.RefreshObjectiveTrackerUI then
        local resolvedSettings = ns.GetObjectiveTrackerSettings and ns.GetObjectiveTrackerSettings()
            or (ns.DEFAULTS and ns.DEFAULTS.objectiveTracker)
            or {}
        local enabled = resolvedSettings and resolvedSettings.enabled
        local moduleEnabled = ns.IsModuleRuntimeEnabled and ns.IsModuleRuntimeEnabled(CONFIG_KEY, enabled)
            or enabled ~= false

        if needsApplyModuleAnchors and not moduleEnabled then
            ns.RefreshObjectiveTrackerUI()
        else
            ns.RefreshObjectiveTrackerUI("soft")
        end
    end
end

local function QueueCombatDeferredObjectiveTrackerMutation(needsApplyTrackerDimensions, needsApplyModuleAnchors)
    if needsApplyTrackerDimensions then
        state.pendingApplyTrackerDimensions = true
        state.needsTrackerDimensionReplay = true
    end
    if needsApplyModuleAnchors then
        state.pendingApplyModuleAnchors = true
    end

    combatDeferralFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
end

combatDeferralFrame:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_REGEN_ENABLED" then
        DrainCombatDeferredObjectiveTrackerMutations()
    end
end)

local BuildTrackerStyleData
local ShouldApplyModuleContentStyling
local ApplyBlockStyle
local BuildBlockHeaderColorStyle
local ApplyTrackerStyles
local RequestCustomModuleRefresh
local RequestTrackerLayoutRefresh
local RequestTrackerStyleRefresh
local RefreshObjectiveTrackerDisplay
local styleHelpers = {}

local ClearSortableModuleZoneDividerArtifacts

local function CopyArray(source)
    local copy = {}
    if type(source) ~= "table" then
        return copy
    end

    for index, value in ipairs(source) do
        copy[index] = value
    end
    return copy
end

local function CopyColor(source)
    if type(source) ~= "table" then
        return nil
    end

    return {
        r = source.r,
        g = source.g,
        b = source.b,
        a = source.a,
    }
end

local function DoesOrderMatchTemplate(order, template)
    if type(order) ~= "table" or type(template) ~= "table" or #order ~= #template then
        return false
    end

    for index, key in ipairs(template) do
        if order[index] ~= key then
            return false
        end
    end

    return true
end

local function AreColorsEquivalent(left, right)
    if type(left) ~= "table" or type(right) ~= "table" then
        return false
    end

    local epsilon = 0.001
    local function Resolve(color, key, index)
        return tonumber(color[key] or color[index]) or 0
    end

    return math.abs(Resolve(left, "r", 1) - Resolve(right, "r", 1)) <= epsilon
        and math.abs(Resolve(left, "g", 2) - Resolve(right, "g", 2)) <= epsilon
        and math.abs(Resolve(left, "b", 3) - Resolve(right, "b", 3)) <= epsilon
        and math.abs(Resolve(left, "a", 4) - Resolve(right, "a", 4)) <= epsilon
end

local function NormalizeLevelPrefixMode(mode, legacyShowLevelPrefix)
    if mode == K.LEVEL_PREFIX_MODE_ALL
        or mode == K.LEVEL_PREFIX_MODE_TRIVIAL
        or mode == K.LEVEL_PREFIX_MODE_NONE
    then
        return mode
    end

    if legacyShowLevelPrefix == false then
        return K.LEVEL_PREFIX_MODE_NONE
    end

    return K.LEVEL_PREFIX_MODE_TRIVIAL
end

local function LoadBlizzardAddon(addonKey)
    local loader = (C_AddOns and C_AddOns.LoadAddOn) or UIParentLoadAddOn
    if loader and addonKey then
        pcall(loader, addonKey)
    end
end

local function GetSettings()
    local settings = ns.GetObjectiveTrackerSettings and ns.GetObjectiveTrackerSettings() or (ns.DEFAULTS and ns.DEFAULTS.objectiveTracker) or {}
    settings.focusedQuest = settings.focusedQuest or {}
    settings.typography = settings.typography or {}
    settings.zone = settings.zone or {}
    settings.scrollBar = settings.scrollBar or {}
    settings.progressBar = settings.progressBar or {}
    settings.appearance = settings.appearance or {}
    settings.search = settings.search or {}

    local defaultFocusedQuest = ns.DEFAULTS and ns.DEFAULTS.objectiveTracker and ns.DEFAULTS.objectiveTracker.focusedQuest or {}
    if settings.focusedQuest.enabled == nil then
        settings.focusedQuest.enabled = defaultFocusedQuest.enabled ~= false
    end

    local defaultZone = ns.DEFAULTS and ns.DEFAULTS.objectiveTracker and ns.DEFAULTS.objectiveTracker.zone or {}
    for key, value in pairs(defaultZone) do
        if settings.zone[key] == nil then
            settings.zone[key] = value
        end
    end

    settings.zone.titleColors = settings.zone.titleColors or {}
    local defaultZoneTitleColors = defaultZone.titleColors or K.DEFAULT_QUEST_KIND_TITLE_COLORS
    for key, value in pairs(defaultZoneTitleColors) do
        if type(settings.zone.titleColors[key]) ~= "table" then
            settings.zone.titleColors[key] = {
                r = value.r,
                g = value.g,
                b = value.b,
                a = value.a,
            }
        end
    end

    settings.typography.titleColors = settings.typography.titleColors or {}
    local defaultTypography = ns.DEFAULTS and ns.DEFAULTS.objectiveTracker and ns.DEFAULTS.objectiveTracker.typography or {}
    local defaultSpecialTitleColors = defaultTypography.titleColors or K.DEFAULT_SPECIAL_TITLE_COLORS
    settings.typography.levelPrefixMode = NormalizeLevelPrefixMode(settings.typography.levelPrefixMode, settings.typography.showLevelPrefix)
    if settings.typography.showWarbandCompletedIndicator == nil then
        settings.typography.showWarbandCompletedIndicator = defaultTypography.showWarbandCompletedIndicator ~= false
    end
    if settings.typography.showQuestLogCount == nil then
        settings.typography.showQuestLogCount = defaultTypography.showQuestLogCount ~= false
    end
    for key, value in pairs(defaultSpecialTitleColors) do
        if type(value) == "table" and type(settings.typography.titleColors[key]) ~= "table" then
            settings.typography.titleColors[key] = {
                r = value.r,
                g = value.g,
                b = value.b,
                a = value.a,
            }
        end
    end
    -- Always initialize keys that exist in K.DEFAULT_SPECIAL_TITLE_COLORS but may be absent
    -- from an older DEFAULTS table (e.g. legendary was added later).
    for key, value in pairs(K.DEFAULT_SPECIAL_TITLE_COLORS) do
        if type(settings.typography.titleColors[key]) ~= "table" then
            settings.typography.titleColors[key] = {
                r = value.r,
                g = value.g,
                b = value.b,
                a = value.a,
            }
        end
    end

    for _, key in ipairs(K.LEGACY_ZONE_TITLE_COLOR_KEYS) do
        local legacyColor = settings.zone.titleColors[key]
        local defaultColor = defaultSpecialTitleColors[key]
        if type(legacyColor) == "table"
            and type(defaultColor) == "table"
            and AreColorsEquivalent(settings.typography.titleColors[key], defaultColor)
        then
            settings.typography.titleColors[key] = CopyColor(legacyColor)
        end
    end

    local defaultAppearance = ns.DEFAULTS and ns.DEFAULTS.objectiveTracker and ns.DEFAULTS.objectiveTracker.appearance or {}
    if settings.appearance.preset ~= "nomtools" then
        settings.appearance.preset = defaultAppearance.preset or "blizzard"
    end
    -- Per-section appearance (main header / category headers / buttons).
    -- On first load, migrate from old flat settings so existing users keep their customisation.
    if settings.appearance.mainHeader == nil then
        local tex = (type(settings.appearance.texture) == "string" and settings.appearance.texture ~= "" and settings.appearance.texture)
            or defaultAppearance.texture or "blizzard"
        local op  = Clamp(tonumber(settings.appearance.opacity) or tonumber(defaultAppearance.opacity) or 80, 0, 100)
        local col = type(settings.appearance.color) == "table" and settings.appearance.color
            or defaultAppearance.color or K.DEFAULT_HEADER_BACKGROUND_COLOR
        local bdr = type(settings.appearance.borderColor) == "table" and settings.appearance.borderColor
            or defaultAppearance.borderColor or K.DEFAULT_HEADER_BORDER_COLOR
        local borderSize = NormalizeBorderSize(defaultAppearance.borderSize, K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE)
        local borderTexture = defaultAppearance.borderTexture or tex or "blizzard"
        settings.appearance.mainHeader     = { texture = tex, opacity = op, color = CopyColor(col), borderColor = CopyColor(bdr), borderTexture = borderTexture, borderSize = borderSize }
        settings.appearance.categoryHeader = { texture = tex, opacity = op, color = CopyColor(col), borderColor = CopyColor(bdr), borderTexture = borderTexture, borderSize = borderSize }
        settings.appearance.button         = { texture = tex, opacity = op, color = CopyColor(col), borderColor = CopyColor(bdr), borderTexture = borderTexture, borderSize = borderSize }
    end
    for _, sectionKey in ipairs({ "mainHeader", "categoryHeader", "button" }) do
        local s  = settings.appearance[sectionKey] or {}
        settings.appearance[sectionKey] = s
        local sd = (type(defaultAppearance[sectionKey]) == "table" and defaultAppearance[sectionKey]) or {}
        if type(s.texture) ~= "string" or s.texture == "" then
            s.texture = sd.texture or defaultAppearance.texture or "blizzard"
        end
        s.opacity = Clamp(tonumber(s.opacity) or tonumber(sd.opacity) or tonumber(defaultAppearance.opacity) or 80, 0, 100)
        if type(s.color) ~= "table" then
            s.color = CopyColor(sd.color or defaultAppearance.color or K.DEFAULT_HEADER_BACKGROUND_COLOR)
        end
        if type(s.borderColor) ~= "table" then
            s.borderColor = CopyColor(sd.borderColor or defaultAppearance.borderColor or K.DEFAULT_HEADER_BORDER_COLOR)
        end
        if s.borderEnabled == false and tonumber(s.borderSize or sd.borderSize or defaultAppearance.borderSize or K.DEFAULT_CHROME_BORDER_SIZE) ~= 0 then
            s.borderSize = 0
        end
        if type(s.borderTexture) ~= "string" or s.borderTexture == "" then
            s.borderTexture = sd.borderTexture or defaultAppearance.borderTexture or s.texture or defaultAppearance.texture or "blizzard"
        end
        s.borderSize = NormalizeBorderSize(
            tonumber(s.borderSize) or tonumber(sd.borderSize) or tonumber(defaultAppearance.borderSize) or K.DEFAULT_CHROME_BORDER_SIZE,
            K.DEFAULT_CHROME_BORDER_SIZE,
            K.MIN_CHROME_BORDER_SIZE,
            K.MAX_CHROME_BORDER_SIZE
        )
    end
    -- Tracker background
    if type(settings.appearance.trackerBackground) ~= "table" then
        settings.appearance.trackerBackground = {}
    end
    local tbgDef = (type(defaultAppearance.trackerBackground) == "table" and defaultAppearance.trackerBackground) or {}
    local tbg = settings.appearance.trackerBackground
    if tbg.enabled == nil then
        tbg.enabled = settings.appearance.preset == "nomtools"
    end
    if type(tbg.texture) ~= "string" or tbg.texture == "" then
        tbg.texture = tbgDef.texture or defaultAppearance.texture or ns.GLOBAL_CHOICE_KEY
    end
    tbg.opacity = Clamp(tonumber(tbg.opacity) or tonumber(tbgDef.opacity) or 60, 0, 100)
    if type(tbg.color) ~= "table" then
        tbg.color = CopyColor(type(tbgDef.color) == "table" and tbgDef.color or K.DEFAULT_HEADER_BACKGROUND_COLOR)
    end
    if type(tbg.borderColor) ~= "table" then
        tbg.borderColor = CopyColor(type(tbgDef.borderColor) == "table" and tbgDef.borderColor or K.DEFAULT_HEADER_BORDER_COLOR)
    end
    if tbg.borderEnabled == false and tonumber(tbg.borderSize or tbgDef.borderSize or K.DEFAULT_CHROME_BORDER_SIZE) ~= 0 then
        tbg.borderSize = 0
    end
    if type(tbg.borderTexture) ~= "string" or tbg.borderTexture == "" then
        tbg.borderTexture = tbgDef.borderTexture or defaultAppearance.borderTexture or tbg.texture or ns.GLOBAL_CHOICE_KEY
    end
    tbg.borderSize = NormalizeBorderSize(
        tonumber(tbg.borderSize) or tonumber(tbgDef.borderSize) or K.DEFAULT_CHROME_BORDER_SIZE,
        K.DEFAULT_CHROME_BORDER_SIZE,
        K.MIN_CHROME_BORDER_SIZE,
        K.MAX_CHROME_BORDER_SIZE
    )
    -- Header typography overrides (nil fields inherit from the global typography settings).
    settings.typography.mainHeader = settings.typography.mainHeader or {}
    settings.typography.mainHeader.xOffset = Clamp(tonumber(settings.typography.mainHeader.xOffset) or 0, -200, 200)
    settings.typography.mainHeader.yOffset = Clamp(tonumber(settings.typography.mainHeader.yOffset) or 0, -200, 200)
    if type(settings.typography.mainHeader.textColor) ~= "table" then
        settings.typography.mainHeader.textColor = nil
    end
    settings.typography.categoryHeader = settings.typography.categoryHeader or {}
    if type(settings.typography.categoryHeader.textColor) ~= "table" then
        settings.typography.categoryHeader.textColor = nil
    end

    local defaultProgressBar = ns.DEFAULTS and ns.DEFAULTS.objectiveTracker and ns.DEFAULTS.objectiveTracker.progressBar or {}
    local hadLegacyProgressBarSettings = settings.progressBar.fillMode == nil
        and settings.progressBar.borderSize == nil
    if settings.progressBar.fillMode ~= "static" then
        settings.progressBar.fillMode = defaultProgressBar.fillMode or "progress"
    end
    if type(settings.progressBar.fillColor) ~= "table" then
        settings.progressBar.fillColor = CopyColor(defaultProgressBar.fillColor or K.DEFAULT_PROGRESS_FILL_COLOR)
    end
    if type(settings.progressBar.lowFillColor) ~= "table" then
        settings.progressBar.lowFillColor = CopyColor(defaultProgressBar.lowFillColor or K.DEFAULT_PROGRESS_LOW_FILL_COLOR)
    end
    if type(settings.progressBar.mediumFillColor) ~= "table" then
        settings.progressBar.mediumFillColor = CopyColor(defaultProgressBar.mediumFillColor or K.DEFAULT_PROGRESS_MEDIUM_FILL_COLOR)
    end
    if type(settings.progressBar.highFillColor) ~= "table" then
        settings.progressBar.highFillColor = CopyColor(defaultProgressBar.highFillColor or K.DEFAULT_PROGRESS_HIGH_FILL_COLOR)
    end
    if type(settings.progressBar.backgroundColor) ~= "table" then
        settings.progressBar.backgroundColor = CopyColor(defaultProgressBar.backgroundColor or K.DEFAULT_PROGRESS_BACKGROUND_COLOR)
    end
    if settings.progressBar.borderEnabled == false and tonumber(settings.progressBar.borderSize or defaultProgressBar.borderSize or K.DEFAULT_PROGRESS_BORDER_SIZE) ~= 0 then
        settings.progressBar.borderSize = 0
    end
    settings.progressBar.borderSize = NormalizeBorderSize(
        tonumber(settings.progressBar.borderSize) or tonumber(defaultProgressBar.borderSize) or K.DEFAULT_PROGRESS_BORDER_SIZE,
        K.DEFAULT_PROGRESS_BORDER_SIZE,
        K.MIN_PROGRESS_BORDER_SIZE,
        K.MAX_PROGRESS_BORDER_SIZE
    )
    if type(settings.progressBar.borderColor) ~= "table" then
        settings.progressBar.borderColor = CopyColor(defaultProgressBar.borderColor or K.DEFAULT_PROGRESS_BORDER_COLOR)
    elseif hadLegacyProgressBarSettings and AreColorsEquivalent(settings.progressBar.borderColor, K.LEGACY_PROGRESS_BORDER_COLOR) then
        settings.progressBar.borderColor = CopyColor(defaultProgressBar.borderColor or K.DEFAULT_PROGRESS_BORDER_COLOR)
    end
    if type(settings.progressBar.borderTexture) ~= "string" or settings.progressBar.borderTexture == "" then
        settings.progressBar.borderTexture = defaultProgressBar.borderTexture or settings.progressBar.texture or "blizzard"
    end

    if type(settings.scrollBar.color) ~= "table" then
        settings.scrollBar.color = CopyColor(K.DEFAULT_SCROLL_BAR_COLOR)
    elseif AreColorsEquivalent(settings.scrollBar.color, K.LEGACY_SCROLL_BAR_COLOR) then
        settings.scrollBar.color = CopyColor(K.DEFAULT_SCROLL_BAR_COLOR)
    end

    if type(settings.scrollBar.backgroundColor) ~= "table" then
        settings.scrollBar.backgroundColor = CopyColor(K.DEFAULT_SCROLL_BAR_BACKGROUND_COLOR)
    end

    if type(settings.scrollBar.borderColor) ~= "table" then
        settings.scrollBar.borderColor = CopyColor(K.DEFAULT_SCROLL_BAR_BORDER_COLOR)
    end
    if settings.scrollBar.borderSize == nil then
        settings.scrollBar.borderSize = K.DEFAULT_CHROME_BORDER_SIZE
    end

    if type(settings.order) ~= "table" then
        settings.order = CopyArray(K.DEFAULT_ORDER)
    elseif DoesOrderMatchTemplate(settings.order, K.LEGACY_DEFAULT_ORDER) then
        settings.order = CopyArray(K.DEFAULT_ORDER)
    end

    settings.layout = settings.layout or {}
    local defaultLayout = ns.DEFAULTS and ns.DEFAULTS.objectiveTracker and ns.DEFAULTS.objectiveTracker.layout or {}
    settings.layout.width = Clamp(
        tonumber(settings.layout.width) or tonumber(defaultLayout.width) or 235,
        150, 500
    )
    settings.layout.height = Clamp(
        tonumber(settings.layout.height) or tonumber(defaultLayout.height) or 800,
        200, 1400
    )
    if settings.layout.matchMinimapWidth == nil then
        settings.layout.matchMinimapWidth = defaultLayout.matchMinimapWidth == true
    end
    if settings.layout.attachToMinimap == nil then
        settings.layout.attachToMinimap = defaultLayout.attachToMinimap == true
    end
    settings.layout.minimapYOffset = Clamp(
        tonumber(settings.layout.minimapYOffset) or tonumber(defaultLayout.minimapYOffset) or 0,
        -200, 200
    )
    local attachEdge = settings.layout.minimapAttachEdge
    if attachEdge ~= "top" and attachEdge ~= "bottom" then
        settings.layout.minimapAttachEdge = defaultLayout.minimapAttachEdge or "bottom"
    end

    -- Normalize header settings.
    settings.header = settings.header or {}
    local defaultHeader = ns.DEFAULTS and ns.DEFAULTS.objectiveTracker and ns.DEFAULTS.objectiveTracker.header or {}
    if settings.header.enabled == nil then
        settings.header.enabled = defaultHeader.enabled ~= false
    end
    if settings.header.showBackground == nil then
        settings.header.showBackground = defaultHeader.showBackground ~= false
    end
    if settings.header.showTitle == nil then
        settings.header.showTitle = defaultHeader.showTitle ~= false
    end

    -- Normalize buttons and migrate the legacy shared trackAll toggle.
    settings.buttons = settings.buttons or {}
    local defaultButtons = ns.DEFAULTS and ns.DEFAULTS.objectiveTracker and ns.DEFAULTS.objectiveTracker.buttons or {}
    if settings.buttons.minimize == nil then
        settings.buttons.minimize = defaultButtons.minimize ~= false
    end
    if settings.buttons.trackerTrackAll == nil then
        if settings.buttons.trackAll ~= nil then
            settings.buttons.trackerTrackAll = settings.buttons.trackAll ~= false
        else
            settings.buttons.trackerTrackAll = defaultButtons.trackAll ~= false
        end
    end
    if settings.buttons.questLogTrackAll == nil then
        if settings.buttons.trackAll ~= nil then
            settings.buttons.questLogTrackAll = settings.buttons.trackAll ~= false
        else
            settings.buttons.questLogTrackAll = defaultButtons.trackAll ~= false
        end
    end

    -- Migrate removed "completion" sort mode to "default".
    if settings.filter and settings.filter.sortBy == "completion" then
        settings.filter.sortBy = "default"
    end

    -- Migrate removed "trivial" quest type to separate showTrivial toggle.
    if settings.filter and settings.filter.questTypes and settings.filter.questTypes.trivial then
        settings.filter.questTypes.trivial = nil
        if settings.filter.showTrivial == nil then
            settings.filter.showTrivial = false
        end
    end

    -- Migrate zone divider settings from filter to zoneDivider.
    if settings.filter then
        if not settings.zoneDivider then
            settings.zoneDivider = {}
        end
        if settings.filter.zoneDividerAlign and not settings.zoneDivider.align then
            settings.zoneDivider.align = settings.filter.zoneDividerAlign
        end
        if settings.filter.zoneDividerShowLines ~= nil and settings.zoneDivider.showLines == nil then
            settings.zoneDivider.showLines = settings.filter.zoneDividerShowLines
        end
    end

    local defaultObjectiveTrackerRoot = ns.DEFAULTS and ns.DEFAULTS.objectiveTracker or {}
    if settings.delveUpdateThrottleSeconds == nil then
        settings.delveUpdateThrottleSeconds = tonumber(defaultObjectiveTrackerRoot.delveUpdateThrottleSeconds) or 0.25
    end

    return settings
end

do
    state.GetDelveObjectiveTrackerUpdateThrottleSeconds = function()
        local s = GetSettings()
        local def = (ns.DEFAULTS and ns.DEFAULTS.objectiveTracker and tonumber(ns.DEFAULTS.objectiveTracker.delveUpdateThrottleSeconds)) or 0.25
        local v = tonumber(s and s.delveUpdateThrottleSeconds)
        if v == nil then
            v = def
        end
        if v < 0 then
            v = 0
        end
        if v > 2 then
            v = 2
        end
        return math.floor(v * 20 + 0.5) / 20
    end
end

local function Clamp(value, minimum, maximum)
    if value < minimum then
        return minimum
    end
    if value > maximum then
        return maximum
    end
    return value
end

local function NormalizeColor(color, fallback)
    fallback = fallback or K.DEFAULT_UNCOMPLETED_COLOR
    if type(color) ~= "table" then
        return {
            r = fallback.r,
            g = fallback.g,
            b = fallback.b,
            a = fallback.a,
        }
    end

    return {
        r = tonumber(color.r or color[1]) or fallback.r,
        g = tonumber(color.g or color[2]) or fallback.g,
        b = tonumber(color.b or color[3]) or fallback.b,
        a = tonumber(color.a or color[4]) or fallback.a,
    }
end

local function BrightenColor(color, amount)
    amount = Clamp(tonumber(amount) or 0.16, 0, 1)
    color = NormalizeColor(color, K.DEFAULT_UNCOMPLETED_COLOR)

    return {
        r = color.r + (1 - color.r) * amount,
        g = color.g + (1 - color.g) * amount,
        b = color.b + (1 - color.b) * amount,
        a = color.a,
    }
end

local function DarkenColor(color, amount)
    amount = Clamp(tonumber(amount) or 0.24, 0, 1)
    color = NormalizeColor(color, K.DEFAULT_UNCOMPLETED_COLOR)

    return {
        r = color.r * (1 - amount),
        g = color.g * (1 - amount),
        b = color.b * (1 - amount),
        a = color.a,
    }
end

local function GetColorLuminance(color)
    color = NormalizeColor(color, K.DEFAULT_UNCOMPLETED_COLOR)
    return (color.r * 0.2126) + (color.g * 0.7152) + (color.b * 0.0722)
end

local function GetHoverColor(color)
    local normalized = NormalizeColor(color, K.DEFAULT_UNCOMPLETED_COLOR)

    if GetColorLuminance(normalized) >= 0.60 then
        return DarkenColor(normalized, 0.26)
    end

    return BrightenColor(normalized, 0.22)
end

local GetZoneFilters

local function GetQuestKindTitleColor(questKind, styles, isHighlighted)
    if not questKind then
        return nil
    end

    local color
    if questKind == "quest" then
        color = styles and styles.questTitleColor or nil
    elseif questKind == "worldQuest" then
        color = styles and styles.worldQuestTitleColor or nil
    elseif questKind == "bonusObjective" then
        color = styles and styles.bonusObjectiveTitleColor or nil
    end

    if not color then
        return nil
    end

    if isHighlighted then
        return GetHoverColor(color)
    end

    return color
end

local function GetTypographySettings()
    return GetSettings().typography or {}
end

local function GetLevelPrefixMode(typography)
    local resolved = typography or GetTypographySettings()
    return NormalizeLevelPrefixMode(resolved.levelPrefixMode, resolved.showLevelPrefix)
end

local function IsWarbandIndicatorEnabled(typography)
    local resolved = typography or GetTypographySettings()
    return resolved.showWarbandCompletedIndicator ~= false
end

local function IsQuestLogCountEnabled(typography)
    local resolved = typography or GetTypographySettings()
    return resolved.showQuestLogCount ~= false
end

local function GetFocusedQuestSettings()
    return GetSettings().focusedQuest or {}
end

local function GetScrollBarSettings()
    return GetSettings().scrollBar or {}
end

local function GetProgressBarSettings()
    return GetSettings().progressBar or {}
end

local function GetHeaderAppearanceSettings()
    return GetSettings().appearance or {}
end

local function IsTrackerTrackAllButtonEnabled()
    local buttons = GetSettings().buttons or {}
    return buttons.trackerTrackAll ~= false
end

local function IsQuestLogTrackAllButtonEnabled()
    local buttons = GetSettings().buttons or {}
    return buttons.questLogTrackAll ~= false
end

local function IsMainHeaderEnabled()
    local h = GetSettings().header or {}
    return h.enabled ~= false
end

local function IsHeaderBackgroundShown()
    local h = GetSettings().header or {}
    return h.showBackground ~= false
end

local function IsHeaderTitleShown()
    local h = GetSettings().header or {}
    return h.showTitle ~= false
end

local function IsMinimizeButtonEnabled()
    local b = GetSettings().buttons or {}
    return b.minimize ~= false
end

local function IsFocusedQuestEnabled(settings)
    local focusedQuest = settings and settings.focusedQuest or GetFocusedQuestSettings()
    return focusedQuest.enabled ~= false
end

local function IsSearchEnabled()
    local search = GetSettings().search or {}
    return search.enabled ~= false
end

local function IsScrollEnabled()
    return GetScrollBarSettings().enabled ~= false
end

local function IsScrollBarVisible()
    return GetScrollBarSettings().visible ~= false
end

local function GetScrollBarWidth(settings)
    local resolved = settings or GetScrollBarSettings()
    return Clamp(tonumber(resolved.width) or K.SCROLL_BAR_DEFAULT_WIDTH, K.SCROLL_BAR_MIN_WIDTH, K.SCROLL_BAR_MAX_WIDTH)
end

local function IsModuleEnabled(settings)
    if settings == nil and state.moduleRuntimeEnabled ~= nil then
        return state.moduleRuntimeEnabled == true
    end

    local resolvedSettings = settings
    if resolvedSettings == nil then
        resolvedSettings = ns.GetObjectiveTrackerSettings and ns.GetObjectiveTrackerSettings()
            or (ns.DEFAULTS and ns.DEFAULTS.objectiveTracker)
            or {}
    end
    local enabled = resolvedSettings and resolvedSettings.enabled
    if ns.IsModuleRuntimeEnabled then
        return ns.IsModuleRuntimeEnabled(CONFIG_KEY, enabled)
    end

    return enabled ~= false
end

local function IsZoneSuppressionActive()
    return state.zoneFeaturesSuppressedInCurrentInstance == true
end

GetZoneFilters = function()
    return GetSettings().zone
end

local function GetNormalizedOrder(order)
    local normalized = {}
    local seen = {}

    if type(order) == "table" then
        for _, key in ipairs(order) do
            if K.ORDER_LABELS[key] and not seen[key] then
                normalized[#normalized + 1] = key
                seen[key] = true
            end
        end
    end

    if not seen.focusedQuest and K.ORDER_LABELS.focusedQuest then
        local insertIndex = #normalized + 1
        local passedFocusedQuest = false
        local keysAfterFocusedQuest = {}

        for _, defaultKey in ipairs(K.DEFAULT_ORDER) do
            if defaultKey == "focusedQuest" then
                passedFocusedQuest = true
            elseif passedFocusedQuest then
                keysAfterFocusedQuest[defaultKey] = true
            end
        end

        for index, existingKey in ipairs(normalized) do
            if keysAfterFocusedQuest[existingKey] then
                insertIndex = index
                break
            end
        end

        table.insert(normalized, insertIndex, "focusedQuest")
        seen.focusedQuest = true
    end

    -- The search bar is no longer part of the module ordering. It docks
    -- below the header via ApplySearchFramePosition, so we intentionally
    -- do NOT force-insert a "search" entry into the order list.

    for _, key in ipairs(K.DEFAULT_ORDER) do
        if not seen[key] then
            normalized[#normalized + 1] = key
        end
    end

    return normalized
end

function ns.GetObjectiveTrackerCategoryOrder()
    return CopyArray(GetNormalizedOrder(GetSettings().order))
end

function ns.GetObjectiveTrackerCategoryLabel(key)
    return K.ORDER_LABELS[key] or key
end

function ns.GetObjectiveTrackerZoneFilterKeys()
    return CopyArray(K.ZONE_FILTER_KEYS)
end

function ns.GetObjectiveTrackerZoneFilterLabel(key)
    return K.ZONE_FILTER_LABELS[key] or key
end

function ns.MoveObjectiveTrackerOrderEntry(position, delta)
    local order = GetNormalizedOrder(GetSettings().order)
    local swapIndex = (position or 0) + (delta or 0)
    if swapIndex < 1 or swapIndex > #order then
        return false
    end

    order[position], order[swapIndex] = order[swapIndex], order[position]
    GetSettings().order = order
    return true
end

function ns.ResetObjectiveTrackerOrder()
    GetSettings().order = CopyArray(K.DEFAULT_ORDER)
end

local function GetModuleByKey(key)
    if key == "search" then
        return state.searchFrame
    elseif key == "focusedQuest" then
        return state.focusedQuestModule
    elseif key == "scenario" then
        return ScenarioObjectiveTracker
    elseif key == "uiWidget" then
        return UIWidgetObjectiveTracker
    elseif key == "campaign" then
        return CampaignQuestObjectiveTracker
    elseif key == "zone" then
        return state.zoneModule
    elseif key == "quest" then
        return QuestObjectiveTracker
    elseif key == "adventure" then
        return AdventureObjectiveTracker
    elseif key == "achievement" then
        return AchievementObjectiveTracker
    elseif key == "monthlyActivities" then
        return MonthlyActivitiesObjectiveTracker
    elseif key == "initiativeTasks" then
        return InitiativeTasksObjectiveTracker
    elseif key == "professionsRecipe" then
        return ProfessionsRecipeTracker
    elseif key == "bonusObjective" then
        return BonusObjectiveTracker
    elseif key == "worldQuest" then
        return WorldQuestObjectiveTracker
    end

    return nil
end

local function GetKeyForModule(module)
    if not module then
        return nil
    end

    if module.nomtoolsObjectiveTrackerKey then
        return module.nomtoolsObjectiveTrackerKey
    end

    if module == state.searchFrame then
        return "search"
    elseif module == state.focusedQuestModule then
        return "focusedQuest"
    elseif module == ScenarioObjectiveTracker then
        return "scenario"
    elseif module == UIWidgetObjectiveTracker then
        return "uiWidget"
    elseif module == CampaignQuestObjectiveTracker then
        return "campaign"
    elseif module == state.zoneModule then
        return "zone"
    elseif module == QuestObjectiveTracker then
        return "quest"
    elseif module == AdventureObjectiveTracker then
        return "adventure"
    elseif module == AchievementObjectiveTracker then
        return "achievement"
    elseif module == MonthlyActivitiesObjectiveTracker then
        return "monthlyActivities"
    elseif module == InitiativeTasksObjectiveTracker then
        return "initiativeTasks"
    elseif module == ProfessionsRecipeTracker then
        return "professionsRecipe"
    elseif module == BonusObjectiveTracker then
        return "bonusObjective"
    elseif module == WorldQuestObjectiveTracker then
        return "worldQuest"
    end

    return nil
end

local function GetQuestLogCountText()
    if not C_QuestLog or not C_QuestLog.GetNumQuestLogEntries or not C_QuestLog.GetInfo then
        return nil
    end

    local entryCount = C_QuestLog.GetNumQuestLogEntries()
    if type(entryCount) ~= "number" or entryCount <= 0 then
        return "0"
    end

    local questCount = 0
    for questLogIndex = 1, entryCount do
        local info = C_QuestLog.GetInfo(questLogIndex)
        local questID = C_QuestLog.GetQuestIDForLogIndex and C_QuestLog.GetQuestIDForLogIndex(questLogIndex) or (info and info.questID) or nil
        if info
            and type(questID) == "number"
            and questID > 0
            and not info.isHeader
            and not info.isHidden
            and info.isTask ~= true
            and info.isBounty ~= true
            and (not C_QuestLog.IsQuestTask or not C_QuestLog.IsQuestTask(questID))
            and (not C_QuestLog.IsQuestBounty or not C_QuestLog.IsQuestBounty(questID))
        then
            questCount = questCount + 1
        end
    end

    local maxQuests = C_QuestLog.GetMaxNumQuestsCanAccept and C_QuestLog.GetMaxNumQuestsCanAccept() or nil
    if type(maxQuests) == "number" and maxQuests > 0 then
        return string.format("%d/%d", questCount, maxQuests)
    end

    return tostring(questCount)
end

local function BuildQuestModuleHeaderText(module, fallbackText)
    if GetKeyForModule(module) ~= "quest" then
        return fallbackText
    end

    local baseText = K.ORDER_LABELS.quest or TRACKER_HEADER_QUESTS or "Quests"
    if type(baseText) ~= "string" or baseText == "" then
        baseText = fallbackText
    end

    if not IsQuestLogCountEnabled() then
        return baseText
    end

    local questCountText = GetQuestLogCountText()
    if not questCountText or questCountText == "" then
        return baseText
    end

    return string.format("%s (%s)", baseText, questCountText)
end

local function TagKnownModules()
    for _, key in ipairs(K.DEFAULT_ORDER) do
        local module = GetModuleByKey(key)
        if module then
            module.nomtoolsObjectiveTrackerKey = key
        end
    end
end

local function GetCharacterTrackerState()
    return ns.GetObjectiveTrackerCharacterSettings and ns.GetObjectiveTrackerCharacterSettings() or nil
end

local function GetCollapsedSections()
    local characterState = GetCharacterTrackerState()
    return characterState and characterState.collapsedSections or nil
end

local function GetSavedSectionCollapsed(key)
    local collapsedSections = GetCollapsedSections()
    if not collapsedSections then
        return nil
    end

    local value = collapsedSections[key]
    if value == nil then
        return nil
    end

    return value == true
end

local function SetSavedSectionCollapsed(key, collapsed)
    local collapsedSections = GetCollapsedSections()
    if collapsedSections and key then
        collapsedSections[key] = collapsed == true
    end
end

local function CaptureCurrentSectionCollapseStates()
    local collapsedSections = GetCollapsedSections()
    if not collapsedSections then
        return false
    end

    local changed = false

    for _, key in ipairs(GetNormalizedOrder(GetSettings().order)) do
        local module = GetModuleByKey(key)
        if module and module.parentContainer == ObjectiveTrackerFrame and module.IsCollapsed then
            local collapsed = module:IsCollapsed() == true
            if collapsedSections[key] ~= collapsed then
                collapsedSections[key] = collapsed
                changed = true
            end
        end
    end

    return changed
end

local function ApplySavedSectionCollapsedState(module)
    if not module or module.parentContainer ~= ObjectiveTrackerFrame or not module.SetCollapsed or not module.IsCollapsed then
        return false
    end

    local moduleKey = GetKeyForModule(module)
    local collapsed = moduleKey and GetSavedSectionCollapsed(moduleKey) or nil
    if collapsed == nil or module:IsCollapsed() == collapsed then
        return false
    end

    state.applyingCollapseStates = true
    module:SetCollapsed(collapsed)
    state.applyingCollapseStates = false
    return true
end

local function ApplySavedSectionCollapseStates()
    if not ObjectiveTrackerFrame then
        return false
    end

    if not GetCharacterTrackerState() then
        return false
    end

    local changed = false
    state.applyingCollapseStates = true

    for _, key in ipairs(GetNormalizedOrder(GetSettings().order)) do
        local module = GetModuleByKey(key)
        local collapsed = GetSavedSectionCollapsed(key)
        if module and collapsed ~= nil and module.SetCollapsed and module.IsCollapsed and module:IsCollapsed() ~= collapsed then
            module:SetCollapsed(collapsed)
            changed = true
        end
    end

    state.applyingCollapseStates = false
    state.savedCollapseStatesInitialized = true
    return changed
end

local function EnsureObjectiveTrackerLoaded()
    if ObjectiveTrackerFrame and ObjectiveTrackerManager then
        return true
    end

    LoadBlizzardAddon("Blizzard_ObjectiveTracker")
    return ObjectiveTrackerFrame ~= nil and ObjectiveTrackerManager ~= nil
end

local function HookObjectiveTrackerManagerInit()
    if state.managerInitHookInstalled then
        return
    end

    if ObjectiveTrackerFrame and ObjectiveTrackerManager and type(ObjectiveTrackerManager.Init) == "function" then
        local function DeferredRefreshOT()
            if ns.RefreshObjectiveTrackerUI then
                ns.RefreshObjectiveTrackerUI()
            end
        end
        hooksecurefunc(ObjectiveTrackerManager, "Init", function()
            if ns.RefreshObjectiveTrackerUI then
                C_Timer.After(0, DeferredRefreshOT)
            end
        end)
        state.managerInitHookInstalled = true
        return
    end

    eventFrame:UnregisterEvent("ADDON_LOADED")
    eventFrame:SetScript("OnEvent", function(_, event, addonNameLoaded)
        if event ~= "ADDON_LOADED" or addonNameLoaded ~= "Blizzard_ObjectiveTracker" then
            return
        end

        eventFrame:UnregisterEvent("ADDON_LOADED")
        eventFrame:SetScript("OnEvent", nil)

        HookObjectiveTrackerManagerInit()
        if ns.RefreshObjectiveTrackerUI then
            ns.RefreshObjectiveTrackerUI()
        end
    end)
    eventFrame:RegisterEvent("ADDON_LOADED")
end

local function BuildMapAncestors(mapID)
    local ancestors = {}
    local currentMapID = mapID

    while type(currentMapID) == "number" and currentMapID > 0 and not ancestors[currentMapID] do
        ancestors[currentMapID] = true
        local mapInfo = C_Map and C_Map.GetMapInfo and C_Map.GetMapInfo(currentMapID) or nil
        currentMapID = mapInfo and mapInfo.parentMapID or nil
    end

    return ancestors
end

local function IsQuestInCurrentZone(questID)
    if type(questID) ~= "number" then
        return false
    end

    local currentMapID = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player") or nil
    if not currentMapID then
        return false
    end

    local questMapID = C_TaskQuest and C_TaskQuest.GetQuestZoneID and C_TaskQuest.GetQuestZoneID(questID) or nil
    if not questMapID or questMapID == 0 then
        questMapID = GetQuestUiMapID and GetQuestUiMapID(questID, true) or nil
    end
    if not questMapID or questMapID == 0 then
        return false
    end

    local currentAncestors = BuildMapAncestors(currentMapID)
    if currentAncestors[questMapID] then
        return true
    end

    local questAncestors = BuildMapAncestors(questMapID)
    return questAncestors[currentMapID] == true
end

local function IsTaskQuestReadyForZoneTracker(questID)
    if type(questID) ~= "number" or not GetTaskInfo then
        return false
    end

    local isInArea = GetTaskInfo(questID)
    return isInArea == true
end

local function IsCampaignQuest(questID, quest)
    if quest and quest.GetQuestClassification then
        return quest:GetQuestClassification() == Enum.QuestClassification.Campaign
    end

    if C_QuestInfoSystem and C_QuestInfoSystem.GetQuestClassification then
        return C_QuestInfoSystem.GetQuestClassification(questID) == Enum.QuestClassification.Campaign
    end

    return false
end

local function GetQuestClassification(questID, quest)
    if quest and quest.GetQuestClassification then
        return quest:GetQuestClassification()
    end

    if C_QuestInfoSystem and C_QuestInfoSystem.GetQuestClassification then
        return C_QuestInfoSystem.GetQuestClassification(questID)
    end

    return nil
end

local function IsTaskQuest(questID, quest)
    if quest and (quest.isTask or quest.isBounty) then
        return true
    end

    if C_QuestLog and C_QuestLog.IsQuestTask and C_QuestLog.IsQuestTask(questID) then
        return true
    end

    if C_QuestLog and C_QuestLog.IsQuestBounty and C_QuestLog.IsQuestBounty(questID) then
        return true
    end

    return false
end

local function GetQuestKind(questID, quest)
    if type(questID) ~= "number" or questID <= 0 then
        return nil
    end

    if (quest and quest.IsDisabledForSession and quest:IsDisabledForSession())
        or (C_QuestLog and C_QuestLog.IsQuestDisabledForSession and C_QuestLog.IsQuestDisabledForSession(questID))
    then
        return nil
    end

    if IsTaskQuest(questID, quest) then
        if QuestUtils_IsQuestWorldQuest and QuestUtils_IsQuestWorldQuest(questID) then
            return "worldQuest"
        end

        return "bonusObjective"
    end

    return IsCampaignQuest(questID, quest) and "campaign" or "quest"
end

local function IsDailyQuest(questID)
    if type(QuestIsDaily) == "function" then
        return QuestIsDaily(questID) == true
    end

    if C_QuestLog and C_QuestLog.IsDailyQuest then
        return C_QuestLog.IsDailyQuest(questID) == true
    end

    return false
end

local function IsWeeklyQuest(questID)
    if type(QuestIsWeekly) == "function" then
        return QuestIsWeekly(questID) == true
    end

    if C_QuestLog and C_QuestLog.IsWeeklyQuest then
        return C_QuestLog.IsWeeklyQuest(questID) == true
    end

    return false
end

local function GetRecurringQuestType(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return nil
    end

    local classification = GetQuestClassification(questID)
    if Enum
        and Enum.QuestClassification
        and Enum.QuestClassification.Meta ~= nil
        and classification == Enum.QuestClassification.Meta
    then
        return nil
    end

    if IsWeeklyQuest(questID) then
        return "weekly"
    end

    if IsDailyQuest(questID) then
        return "daily"
    end

    -- Note: frequency is checked via C_QuestLog.GetInfo below

    if C_QuestLog and C_QuestLog.GetLogIndexForQuestID and C_QuestLog.GetInfo then
        local questLogIndex = C_QuestLog.GetLogIndexForQuestID(questID)
        if questLogIndex and questLogIndex > 0 then
            local info = C_QuestLog.GetInfo(questLogIndex)
            if info then
                if info.isWeekly == true then
                    return "weekly"
                end

                if info.isDaily == true then
                    return "daily"
                end

                local frequency = info.frequency or info.questFrequency
                local dailyFrequency = _G.LE_QUEST_FREQUENCY_DAILY or (Enum and Enum.QuestFrequency and Enum.QuestFrequency.Daily) or nil
                local weeklyFrequency = _G.LE_QUEST_FREQUENCY_WEEKLY or (Enum and Enum.QuestFrequency and Enum.QuestFrequency.Weekly) or nil
                if frequency ~= nil then
                    if weeklyFrequency ~= nil and frequency == weeklyFrequency then
                        return "weekly"
                    end

                    if dailyFrequency ~= nil and frequency == dailyFrequency then
                        return "daily"
                    end
                end
            end
        end
    end

    if C_QuestLog and C_QuestLog.IsRepeatableQuest and C_QuestLog.IsRepeatableQuest(questID) then
        -- Repeatable quests can be daily or weekly; without a reliable signal
        -- fall through to the classification check below rather than guessing.
    end

    if Enum
        and Enum.QuestClassification
        and (
            classification == Enum.QuestClassification.Recurring
            or classification == Enum.QuestClassification.Calling
        )
    then
        return "weekly"
    end

    return nil
end

local function IsPreyQuest(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return false
    end

    if C_QuestLog and C_QuestLog.GetActivePreyQuest then
        return C_QuestLog.GetActivePreyQuest() == questID
    end

    return false
end

local function IsTrivialQuest(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return false
    end

    if C_QuestLog and C_QuestLog.IsQuestTrivial then
        return C_QuestLog.IsQuestTrivial(questID) == true
    end

    return false
end

local function GetQuestLogInfoByQuestID(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return nil
    end

    if C_QuestLog and C_QuestLog.GetLogIndexForQuestID and C_QuestLog.GetInfo then
        local questLogIndex = C_QuestLog.GetLogIndexForQuestID(questID)
        if questLogIndex and questLogIndex > 0 then
            return C_QuestLog.GetInfo(questLogIndex)
        end
    end

    return nil
end

local function BuildRuntimeQuestEntry(questID)
    local info = GetQuestLogInfoByQuestID(questID)
    if not info then
        return nil
    end

    local questLogIndex = C_QuestLog and C_QuestLog.GetLogIndexForQuestID and C_QuestLog.GetLogIndexForQuestID(questID) or nil
    if not questLogIndex or questLogIndex <= 0 then
        return nil
    end

    local classification = C_QuestInfoSystem and C_QuestInfoSystem.GetQuestClassification and C_QuestInfoSystem.GetQuestClassification(questID) or nil
    local disabledForSession = C_QuestLog and C_QuestLog.IsQuestDisabledForSession and C_QuestLog.IsQuestDisabledForSession(questID) == true or false

    local quest = {
        questID = questID,
        questLogIndex = questLogIndex,
        title = info.title or (C_QuestLog and C_QuestLog.GetTitleForQuestID and C_QuestLog.GetTitleForQuestID(questID)) or nil,
        requiredMoney = tonumber(info.requiredMoney) or 0,
        isAutoComplete = info.isAutoComplete == true,
        isTask = info.isTask == true,
        isBounty = info.isBounty == true,
        overridesSortOrder = info.overridesSortOrder,
        classification = classification,
        disabledForSession = disabledForSession,
    }

    function quest:GetID()
        return self.questID
    end

    function quest:GetQuestLogIndex()
        return self.questLogIndex
    end

    function quest:IsComplete()
        return C_QuestLog and C_QuestLog.IsComplete and C_QuestLog.IsComplete(self.questID) == true or false
    end

    function quest:IsDisabledForSession()
        return self.disabledForSession == true
    end

    function quest:GetQuestClassification()
        return self.classification
    end

    return quest
end

local function GetQuestHeaderTitle(questID)
    local info = GetQuestLogInfoByQuestID(questID)
    local title = info and info.title or nil
    if type(title) == "string" and title ~= "" then
        return title
    end

    if C_QuestLog and C_QuestLog.GetTitleForQuestID then
        title = C_QuestLog.GetTitleForQuestID(questID)
        if type(title) == "string" and title ~= "" then
            return title
        end
    end

    return nil
end

local function GetQuestDisplayLevel(questID)
    local info = GetQuestLogInfoByQuestID(questID)
    local level = info and (info.level or info.questLevel or info.difficultyLevel) or nil
    if type(level) ~= "number" and type(GetQuestDifficultyLevel) == "function" then
        level = GetQuestDifficultyLevel(questID)
    end

    if type(level) == "number" and level > 0 then
        return level
    end

    return nil
end

local function ShouldShowQuestLevelPrefix(questID)
    local mode = GetLevelPrefixMode()
    if mode == K.LEVEL_PREFIX_MODE_ALL then
        return true
    end

    if mode == K.LEVEL_PREFIX_MODE_TRIVIAL then
        return IsTrivialQuest(questID)
    end

    return false
end

local function IsQuestWarbandCompleted(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return false
    end

    return C_QuestLog
        and C_QuestLog.IsQuestFlaggedCompletedOnAccount
        and C_QuestLog.IsQuestFlaggedCompletedOnAccount(questID) == true
        or false
end

local function GetWarbandCompletionAtlas()
    if state.resolvedWarbandCompletionAtlas ~= nil then
        return state.resolvedWarbandCompletionAtlas or nil
    end

    local atlasInfoGetter = C_Texture and C_Texture.GetAtlasInfo or nil
    for _, atlas in ipairs(K.WARBAND_COMPLETION_ICON_ATLASES) do
        if not atlasInfoGetter or atlasInfoGetter(atlas) then
            state.resolvedWarbandCompletionAtlas = atlas
            return atlas
        end
    end

    state.resolvedWarbandCompletionAtlas = false
    return nil
end

local function GetWarbandCompletionCheckAtlas()
    if state.resolvedWarbandCompletionCheckAtlas ~= nil then
        return state.resolvedWarbandCompletionCheckAtlas or nil
    end

    local atlasInfoGetter = C_Texture and C_Texture.GetAtlasInfo or nil
    for _, atlas in ipairs(K.WARBAND_COMPLETION_CHECK_ATLASES) do
        if not atlasInfoGetter or atlasInfoGetter(atlas) then
            state.resolvedWarbandCompletionCheckAtlas = atlas
            return atlas
        end
    end

    state.resolvedWarbandCompletionCheckAtlas = false
    return nil
end

local function IsQuestLikeModule(module)
    local key = GetKeyForModule(module)
    return key == "focusedQuest"
        or key == "campaign"
        or key == "zone"
        or key == "quest"
        or key == "bonusObjective"
        or key == "worldQuest"
end

local function ShouldShowWarbandCompletedIndicator(questID, module)
    return IsWarbandIndicatorEnabled()
        and IsQuestLikeModule(module)
        and IsQuestWarbandCompleted(questID)
        and GetWarbandCompletionAtlas() ~= nil
end

local function BuildQuestHeaderText(questID, fallbackText)
    if type(questID) ~= "number" or questID <= 0 then
        return fallbackText
    end

    local title = GetQuestHeaderTitle(questID) or fallbackText
    if type(title) ~= "string" or title == "" then
        return fallbackText
    end

    if ShouldShowQuestLevelPrefix(questID) then
        local level = GetQuestDisplayLevel(questID)
        if level then
            title = string.format("[%d] %s", level, title)
        end
    end

    return title
end

local function GetSuperTrackedQuestID()
    local questID = C_SuperTrack and C_SuperTrack.GetSuperTrackedQuestID and C_SuperTrack.GetSuperTrackedQuestID() or 0
    if type(questID) == "number" and questID > 0 then
        return questID
    end

    return nil
end

-- Delves (The War Within+) are often implemented so IsInInstance() is *false*
-- while still being isolated objective-tracker content — the same failure mode
-- users report for "Zone section still shows in Delves". Combine the standard
-- instance check with delve-specific signals Blizzard exposes elsewhere.
local K_DELVE_DIFFICULTY_ID = 208 -- matches NomTools_Data delve difficulty

local function IsPlayerInInstanceLikeAreaForZoneSuppression()
    if IsInInstance and select(1, IsInInstance()) == true then
        return true
    end

    if C_PartyInfo and type(C_PartyInfo.IsDelveInProgress) == "function" then
        local ok, inDelve = pcall(C_PartyInfo.IsDelveInProgress)
        if ok and inDelve == true then
            return true
        end
    end

    if GetInstanceInfo then
        local ok, _, _, difficultyID = pcall(GetInstanceInfo)
        if ok and type(difficultyID) == "number" and difficultyID == K_DELVE_DIFFICULTY_ID then
            return true
        end
    end

    return false
end

-- Delve FPS test: scoped in do/end so these are not three extra chunk-level locals
-- (NomTools_ObjectiveTracker.lua is at Lua 5.1's 200-local limit).
do
    local function delveScenarioWidgetTest_IsDelveContext()
        if C_PartyInfo and type(C_PartyInfo.IsDelveInProgress) == "function" then
            local ok, inDelve = pcall(C_PartyInfo.IsDelveInProgress)
            if ok and inDelve == true then
                return true
            end
        end

        if GetInstanceInfo then
            local ok, _, _, difficultyID = pcall(GetInstanceInfo)
            if ok and type(difficultyID) == "number" and difficultyID == K_DELVE_DIFFICULTY_ID then
                return true
            end
        end

        return false
    end

    state.SyncDelveScenarioWidgetTestRemoval = function()
        if not ObjectiveTrackerFrame or not IsModuleEnabled() then
            return
        end

        local scenario = _G.ScenarioObjectiveTracker
        local uiWidget = _G.UIWidgetObjectiveTracker
        local wantStrip = K.DEBUG_HIDE_DELVE_SCENARIO_WIDGET_TEST == true
            and delveScenarioWidgetTest_IsDelveContext()

        if not wantStrip then
            if state.delveScenarioWidgetsStrippedForTest then
                if InCombatLockdown() then
                    QueueCombatDeferredObjectiveTrackerMutation(false, true)
                    return
                end
                for _, mod in ipairs({ scenario, uiWidget }) do
                    if mod and ObjectiveTrackerFrame.HasModule and not ObjectiveTrackerFrame:HasModule(mod) then
                        pcall(function()
                            ObjectiveTrackerFrame:AddModule(mod)
                        end)
                        if mod.Show then
                            mod:Show()
                        end
                        if mod.MarkDirty then
                            mod:MarkDirty()
                        end
                    end
                end
                state.delveScenarioWidgetsStrippedForTest = false
            end
            return
        end

        if InCombatLockdown() then
            QueueCombatDeferredObjectiveTrackerMutation(false, true)
            return
        end

        for _, mod in ipairs({ scenario, uiWidget }) do
            if mod and ObjectiveTrackerFrame.HasModule and ObjectiveTrackerFrame:HasModule(mod) then
                pcall(function()
                    ObjectiveTrackerFrame:RemoveModule(mod)
                end)
                if mod.Hide then
                    mod:Hide()
                end
                if mod.SetHeight then
                    mod:SetHeight(0)
                end
                state.delveScenarioWidgetsStrippedForTest = true
            end
        end
    end

    state.OnDelveScenarioWidgetTestModuleAdded = function(container, module)
        if container ~= ObjectiveTrackerFrame then
            return
        end
        if K.DEBUG_HIDE_DELVE_SCENARIO_WIDGET_TEST ~= true then
            return
        end
        if not delveScenarioWidgetTest_IsDelveContext() then
            return
        end
        local scenario = _G.ScenarioObjectiveTracker
        local uiWidget = _G.UIWidgetObjectiveTracker
        if module ~= scenario and module ~= uiWidget then
            return
        end
        if InCombatLockdown() then
            QueueCombatDeferredObjectiveTrackerMutation(false, true)
            return
        end
        if module and ObjectiveTrackerFrame.HasModule and ObjectiveTrackerFrame:HasModule(module) then
            pcall(function()
                ObjectiveTrackerFrame:RemoveModule(module)
            end)
            if module.Hide then
                module:Hide()
            end
            if module.SetHeight then
                module:SetHeight(0)
            end
            state.delveScenarioWidgetsStrippedForTest = true
        end
    end

    state.IsDelveTrackerPerfContext = delveScenarioWidgetTest_IsDelveContext
end

local function IsZoneFeaturesSuppressedInCurrentInstance()
    -- Read the option directly from settings every time. Do NOT gate on
    -- state.zoneSuppressionConfigured — that flag is only set inside a *full*
    -- RefreshObjectiveTrackerState pass, which can run *after* PLAYER_ENTERING_WORLD
    -- / ZONE_CHANGED handlers. The suppression watcher and early layout paths
    -- would otherwise see zoneSuppressionConfigured == false and treat the
    -- instance as "never suppress", leaving the Zone section visible in dungeons
    -- until some unrelated full refresh ran.
    local filter = (GetSettings() and GetSettings().filter) or {}
    if filter.disableZoneFeaturesInInstances ~= true then
        state.zoneFeaturesSuppressedInCurrentInstance = false
        return false
    end

    local suppressLocation = IsPlayerInInstanceLikeAreaForZoneSuppression()
    state.zoneFeaturesSuppressedInCurrentInstance = suppressLocation
    return suppressLocation
end

local function IsZoneEligibleQuest(questID, questType)
    if not IsModuleEnabled() or IsZoneFeaturesSuppressedInCurrentInstance() then
        return false
    end

    local zone = GetZoneFilters()
    if questType == "quest" then
        return zone.regularQuests == true and IsQuestInCurrentZone(questID)
    elseif questType == "campaign" then
        return zone.campaignQuests == true and IsQuestInCurrentZone(questID)
    elseif questType == "worldQuest" then
        return zone.worldQuests == true and IsTaskQuestReadyForZoneTracker(questID)
    elseif questType == "trackedWorldQuest" then
        -- Tracked world quests: use map-based zone check instead of isInArea.
        -- Blizzard's WorldQuestObjectiveTracker bypasses isInArea for watched quests
        -- (treatAsInArea = isTrackedWorldQuest or isInArea). We match that by checking
        -- map geography only, so tracked quests appear immediately on zone entry
        -- without waiting for the server isInArea flag.
        return zone.worldQuests == true and IsQuestInCurrentZone(questID)
    elseif questType == "bonusObjective" then
        return zone.bonusObjectives == true and IsTaskQuestReadyForZoneTracker(questID)
    end

    return false
end

local function GetZoneQuestType(quest)
    if not quest or not quest.GetID then
        return nil
    end

    local questID = quest:GetID()
    if quest.isTask or quest.isBounty then
        return nil
    end

    if quest.IsDisabledForSession and quest:IsDisabledForSession() then
        return nil
    end

    local questType = GetQuestKind(questID, quest)
    if (questType == "quest" or questType == "campaign") and IsZoneEligibleQuest(questID, questType) then
        return questType
    end

    return nil
end

local function BuildZoneQuestLists()
    local campaignQuests = {}
    local regularQuests = {}

    if not C_QuestLog or not C_QuestLog.GetNumQuestWatches then
        return campaignQuests, regularQuests
    end

    for index = 1, C_QuestLog.GetNumQuestWatches() do
        local questID = C_QuestLog.GetQuestIDForQuestWatchIndex(index)
        if questID then
            local quest = BuildRuntimeQuestEntry(questID)
            local questType = GetZoneQuestType(quest)
            if questType == "campaign" then
                campaignQuests[#campaignQuests + 1] = quest
            elseif questType == "quest" then
                regularQuests[#regularQuests + 1] = quest
            end
        end
    end

    return campaignQuests, regularQuests
end

local function CompareTrackedWorldQuestIDs(leftQuestID, rightQuestID)
    local leftInArea, leftOnMap = GetTaskInfo(leftQuestID)
    local rightInArea, rightOnMap = GetTaskInfo(rightQuestID)

    if leftInArea ~= rightInArea then
        return leftInArea
    end

    if leftOnMap ~= rightOnMap then
        return leftOnMap
    end

    return leftQuestID < rightQuestID
end

local function GetSortedTrackedWorldQuestIDs()
    local trackedWorldQuestIDs = {}

    if not C_QuestLog
        or not C_QuestLog.GetNumWorldQuestWatches
        or not C_QuestLog.GetQuestIDForWorldQuestWatchIndex
    then
        return trackedWorldQuestIDs
    end

    for index = 1, C_QuestLog.GetNumWorldQuestWatches() do
        local questID = C_QuestLog.GetQuestIDForWorldQuestWatchIndex(index)
        if type(questID) == "number" and questID > 0 then
            trackedWorldQuestIDs[#trackedWorldQuestIDs + 1] = questID
        end
    end

    table.sort(trackedWorldQuestIDs, CompareTrackedWorldQuestIDs)
    return trackedWorldQuestIDs
end

local function TrackAllQuestLogQuests()
    if not C_QuestLog or not C_QuestLog.GetNumQuestLogEntries then
        return
    end

    local maxQuestWatches = Constants and Constants.QuestWatchConsts and Constants.QuestWatchConsts.MAX_QUEST_WATCHES or math.huge
    local suppressWatchUpdates = QuestMapFrame ~= nil

    if suppressWatchUpdates then
        QuestMapFrame.ignoreQuestWatchListChanged = true
    end

    local shownWatchLimitError = false
    for questLogIndex = 1, C_QuestLog.GetNumQuestLogEntries() do
        local info = C_QuestLog.GetInfo(questLogIndex)
        if info and not info.isHeader and info.questID and not C_QuestLog.IsQuestDisabledForSession(info.questID) then
            if not QuestUtils_IsQuestWatched(info.questID) then
                if C_QuestLog.GetNumQuestWatches() >= maxQuestWatches then
                    if not shownWatchLimitError and UIErrorsFrame and OBJECTIVES_WATCH_TOO_MANY then
                        UIErrorsFrame:AddMessage(OBJECTIVES_WATCH_TOO_MANY, 1.0, 0.1, 0.1, 1.0)
                    end
                    shownWatchLimitError = true
                    break
                end

                C_QuestLog.AddQuestWatch(info.questID)
            end
        end
    end

    if suppressWatchUpdates then
        QuestMapFrame.ignoreQuestWatchListChanged = false
        if QuestMapFrame_UpdateAll then
            QuestMapFrame_UpdateAll()
        end
    end

    if ObjectiveTrackerManager and ObjectiveTrackerManager.UpdateAll then
        ObjectiveTrackerManager:UpdateAll()
    elseif ObjectiveTrackerFrame and ObjectiveTrackerFrame.Update then
        ObjectiveTrackerFrame:Update()
    end
end

local NomToolsZoneObjectiveTrackerMixin = {}
for key, value in pairs(K.ZONE_TRACKER_SETTINGS) do
    NomToolsZoneObjectiveTrackerMixin[key] = value
end

function NomToolsZoneObjectiveTrackerMixin:InitModule()
    if QuestObjectiveTrackerMixin and QuestObjectiveTrackerMixin.InitModule then
        QuestObjectiveTrackerMixin.InitModule(self)
        return
    end

    self:AddTag("quest")
    self:WatchMoney(false)
end

local function StopModuleTicker(module)
    if not module then
        return
    end

    if module.ticker then
        module.ticker:Cancel()
        module.ticker = nil
    end

    module.nomtoolsTickerSeconds = 0
end

local function StopZoneTaskRetry(module)
    if not module then
        return
    end

    if module.nomtoolsZoneTaskRetryTicker then
        module.nomtoolsZoneTaskRetryTicker:Cancel()
        module.nomtoolsZoneTaskRetryTicker = nil
    end

    module.nomtoolsZoneTaskRetryAttemptsRemaining = 0
end

local function ShouldRunZoneTaskRetry(module)
    if module ~= state.zoneModule or not IsModuleEnabled() then
        return false
    end

    if IsZoneFeaturesSuppressedInCurrentInstance() then
        return false
    end

    local zone = GetZoneFilters()
    return zone and (zone.worldQuests == true or zone.bonusObjectives == true) or false
end

local function StartZoneTaskRetry(module)
    if not ShouldRunZoneTaskRetry(module) or not C_Timer or not C_Timer.NewTicker then
        StopZoneTaskRetry(module)
        return
    end

    StopZoneTaskRetry(module)
    module.nomtoolsZoneTaskRetryAttemptsRemaining = K.ZONE_TASK_RETRY_MAX_ATTEMPTS
    module.nomtoolsZoneTaskRetryTicker = C_Timer.NewTicker(K.ZONE_TASK_RETRY_INTERVAL_SECONDS, function()
        local remaining = (module.nomtoolsZoneTaskRetryAttemptsRemaining or 0) - 1
        module.nomtoolsZoneTaskRetryAttemptsRemaining = remaining
        RequestCustomModuleRefresh(module)
        if remaining <= 0 then
            StopZoneTaskRetry(module)
        end
    end)
end

local function UpdateModuleTicker(module)
    if not module then
        return
    end

    local desiredTickerSeconds = tonumber(module.tickerSeconds) or 0
    if desiredTickerSeconds <= 0 or not C_Timer or not C_Timer.NewTicker then
        StopModuleTicker(module)
        return
    end

    if module.ticker and module.nomtoolsTickerSeconds == desiredTickerSeconds then
        return
    end

    StopModuleTicker(module)
    module.nomtoolsTickerSeconds = desiredTickerSeconds
    module.ticker = C_Timer.NewTicker(desiredTickerSeconds, function()
        RequestCustomModuleRefresh(module)
    end)
end

local function FinalizeModuleLayout(module)
    UpdateModuleTicker(module)
end

local function WithTaskTrackerTemplates(module, questKind, callback)
    if not module or type(callback) ~= "function" then
        return false
    end

    local previousShowWorldQuests = module.showWorldQuests
    local previousBlockTemplate = module.blockTemplate
    local previousLineTemplate = module.lineTemplate
    local previousProgressBarTemplate = module.progressBarTemplate
    local previousHeaderText = module.headerText

    module.showWorldQuests = questKind == "worldQuest"
    module.blockTemplate = "BonusObjectiveTrackerBlockTemplate"
    module.lineTemplate = "ObjectiveTrackerAnimLineTemplate"
    module.progressBarTemplate = "BonusTrackerProgressBarTemplate"

    local ok, result = pcall(callback)

    module.showWorldQuests = previousShowWorldQuests
    module.blockTemplate = previousBlockTemplate
    module.lineTemplate = previousLineTemplate
    module.progressBarTemplate = previousProgressBarTemplate
    module.headerText = previousHeaderText

    if not ok then
        error(result)
    end

    return result
end

local function EnsureQuestRewardData(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return true
    end

    if type(HaveQuestRewardData) ~= "function"
        or not C_TaskQuest
        or not C_TaskQuest.RequestPreloadRewardData
    then
        return true
    end

    if HaveQuestRewardData(questID) then
        state.pendingRewardDataQuestIDs[questID] = nil
        return true
    end

    if not state.pendingRewardDataQuestIDs[questID] then
        state.pendingRewardDataQuestIDs[questID] = true
        C_TaskQuest.RequestPreloadRewardData(questID)
    end

    return false
end

state.GetOrCreateModuleRefreshState = function(module)
    if not module then
        return nil
    end

    local moduleState = state.moduleMarkDirtyStateByModule[module]
    if not moduleState then
        moduleState = {
            lastMarkDirtyTime = 0,
            pendingMarkDirty = false,
            needsSortRefresh = false,
            needsStyleRefresh = false,
        }
        state.moduleMarkDirtyStateByModule[module] = moduleState
    end

    return moduleState
end

state.MarkModuleRefreshPending = function(module, sortDirty, styleDirty)
    -- Fast path: if the requested dirty states are already set on this
    -- module, short-circuit BEFORE doing diag work. HandleTrackedBlockReset
    -- fires once per block during Blizzard's LayoutContents pass (one pass
    -- can reset ~10-15 blocks for quest + scenario + bonus). Every one of
    -- those calls goes through here with sortDirty=true, styleDirty=true,
    -- and when diagnostics are enabled the per-call overhead (DiagInc +
    -- DiagModule string ops) is measurable on hot paths. The semantic
    -- result of N redundant calls after the first one is identical — both
    -- needsSortRefresh and needsStyleRefresh are already true, so there is
    -- no state change. Skipping them entirely is a pure win.
    if module then
        local existing = state.moduleMarkDirtyStateByModule[module]
        if existing then
            local sortAlreadyPending = sortDirty == false or existing.needsSortRefresh == true
            local styleAlreadyPending = styleDirty ~= true or existing.needsStyleRefresh == true
            if sortAlreadyPending and styleAlreadyPending then
                if state.diag.enabled then
                    state.DiagInc("mmrp.fastpath_deduped")
                end
                return existing
            end
        end
    end

    if state.diag.enabled then
        state.DiagInc("mmrp.calls")
        if styleDirty == true then state.DiagInc("mmrp.styleDirty") end
        if sortDirty ~= false then state.DiagInc("mmrp.sortDirty") end
        if not module then
            state.DiagInc("mmrp.module_nil")
        else
            state.DiagModule("mmrp.byModule", GetKeyForModule(module) or "?")
        end
    end

    if not module then
        if styleDirty then
            state.postLayoutStyleDirty = true
        end
        return nil
    end

    local moduleState = state.GetOrCreateModuleRefreshState(module)
    if sortDirty ~= false then
        moduleState.needsSortRefresh = true
        state.pendingModuleSortRefresh = true
    end
    if styleDirty == true then
        moduleState.needsStyleRefresh = true
        state.pendingModuleStyleRefresh = true
    end

    return moduleState
end

state.HasPendingModuleSortRefresh = function()
    if not state.pendingModuleSortRefresh then
        return false
    end

    for module, moduleState in pairs(state.moduleMarkDirtyStateByModule) do
        if module
            and module.parentContainer == ObjectiveTrackerFrame
            and moduleState
            and moduleState.needsSortRefresh
        then
            return true
        end
    end

    state.pendingModuleSortRefresh = false
    return false
end

state.HasPendingModuleStyleRefresh = function()
    if not state.pendingModuleStyleRefresh then
        return false
    end

    for module, moduleState in pairs(state.moduleMarkDirtyStateByModule) do
        if module
            and module.parentContainer == ObjectiveTrackerFrame
            and moduleState
            and moduleState.needsStyleRefresh
        then
            return true
        end
    end

    state.pendingModuleStyleRefresh = false
    return false
end

state.ClearPendingModuleRefresh = function(module, clearSort, clearStyle)
    local moduleState = module and state.moduleMarkDirtyStateByModule[module] or nil
    if not moduleState then
        return
    end

    if clearSort then
        moduleState.needsSortRefresh = false
    end
    if clearStyle then
        moduleState.needsStyleRefresh = false
    end
end

state.GetSortSettingsSignature = function()
    local filterSettings = (GetSettings().filter or {})
    return table.concat({
        tostring(filterSettings.sortBy or "default"),
        tostring(filterSettings.sortDirection or "asc"),
        tostring(filterSettings.groupByZone == true),
        tostring(filterSettings.zoneSortDirection or "asc"),
    }, "|")
end

RequestCustomModuleRefresh = function(module, styleDirtyOverrideOrOptions)
    state.DiagInc("requestcustommodulerefresh.calls")
    local styleDirty
    local overrideType = type(styleDirtyOverrideOrOptions)
    if overrideType == "boolean" then
        styleDirty = styleDirtyOverrideOrOptions
    elseif overrideType == "table" then
        styleDirty = styleDirtyOverrideOrOptions.styleDirty ~= false
    else
        styleDirty = true
    end
    local moduleState = state.MarkModuleRefreshPending(module, true, styleDirty == true)

    if not (module and module.MarkDirty) then
        return
    end

    local now = GetTime()
    local lastDirty = moduleState.lastMarkDirtyTime or 0
    local throttle = K.MARK_DIRTY_THROTTLE_SECONDS

    if (now - lastDirty) >= throttle then
        moduleState.lastMarkDirtyTime = now
        moduleState.pendingMarkDirty = false
        state.DiagInc("requestcustommodulerefresh.markdirty_immediate")
        module:MarkDirty()
    elseif not moduleState.pendingMarkDirty then
        -- Within the throttle window; schedule a single deferred dirty at the
        -- end of the window so the final state still gets applied.
        -- High-frequency events (SCENARIO_CRITERIA_UPDATE every kill,
        -- QUEST_POI_UPDATE every frame while moving) are coalesced here so
        -- the full layout+style cascade runs at most ~7 times/sec instead of
        -- every frame, with no perceptible difference to the user.
        moduleState.pendingMarkDirty = true
        local delay = throttle - (now - lastDirty)
        C_Timer.After(delay, function()
            local deferredModuleState = state.moduleMarkDirtyStateByModule[module]
            if not deferredModuleState then
                deferredModuleState = {
                    lastMarkDirtyTime = 0,
                    pendingMarkDirty = false,
                }
                state.moduleMarkDirtyStateByModule[module] = deferredModuleState
            end
            deferredModuleState.pendingMarkDirty = false
            if module.MarkDirty then
                deferredModuleState.lastMarkDirtyTime = GetTime()
                module:MarkDirty()
            end
        end)
    end
    -- Do NOT call RequestTrackerLayoutRefresh() here. That calls
    -- ObjectiveTrackerManager.UpdateAll() which forces every tracker module
    -- (including Blizzard's QuestObjectiveTracker) to re-layout, resetting
    -- all block HeaderTexts to plain titles. On high-frequency events like
    -- QUEST_POI_UPDATE or SCENARIO_CRITERIA_UPDATE this creates a
    -- continuous every-frame flicker when regularQuests are shown in the
    -- normal quest tracker (regularQuests zone filter disabled). Just
    -- marking the module dirty is enough: Blizzard's container OnUpdate will
    -- call ObjectiveTrackerFrame:Update() on the next frame, which only
    -- re-layouts dirty modules and triggers RunTrackerPostLayout so
    -- ApplyTrackerStyles re-applies level prefixes and other styling.
end

function NomToolsZoneObjectiveTrackerMixin:OnHide()
    StopZoneTaskRetry(self)
    StopModuleTicker(self)

    if ObjectiveTrackerModuleMixin and ObjectiveTrackerModuleMixin.OnHide then
        ObjectiveTrackerModuleMixin.OnHide(self)
    end
end

function NomToolsZoneObjectiveTrackerMixin:OnEvent(event, ...)
    state.DiagEvent("zone:" .. event)
    local questID = ...
    if (event == "QUEST_DATA_LOAD_RESULT" or event == "QUEST_TURNED_IN" or event == "QUEST_REMOVED")
        and type(questID) == "number"
        and questID > 0
    then
        state.pendingRewardDataQuestIDs[questID] = nil
    end

    if self == state.zoneModule then
        if event == "ZONE_CHANGED" or event == "ZONE_CHANGED_NEW_AREA" or event == "PLAYER_ENTERING_WORLD" then
            local previousSuppressionState = state.zoneFeaturesSuppressedInCurrentInstance
            state.zoneFeaturesSuppressedInCurrentInstance = nil
            local suppressZoneFeatures = IsZoneFeaturesSuppressedInCurrentInstance()
            local suppressionStateChanged = previousSuppressionState ~= suppressZoneFeatures
            state.zoneFeaturesSuppressedInCurrentInstance = suppressZoneFeatures

            if (suppressZoneFeatures or suppressionStateChanged) and ClearSortableModuleZoneDividerArtifacts then
                ClearSortableModuleZoneDividerArtifacts()
            end

            if (event == "PLAYER_ENTERING_WORLD" or suppressionStateChanged) and ns.RefreshObjectiveTrackerUI then
                ns.RefreshObjectiveTrackerUI()
            end

            if suppressZoneFeatures then
                StopZoneTaskRetry(self)
            else
                StartZoneTaskRetry(self)
            end
        end
    end

    local isMovementOrScenarioEvent = event == "QUEST_POI_UPDATE"
        or event == "SCENARIO_CRITERIA_UPDATE"
        or event == "SCENARIO_UPDATE"
        or event == "SCENARIO_BONUS_VISIBILITY_UPDATE"

    if isMovementOrScenarioEvent then
        RequestCustomModuleRefresh(self, false)
    else
        RequestCustomModuleRefresh(self)
    end
end

function NomToolsZoneObjectiveTrackerMixin:WatchMoney(watch)
    if QuestObjectiveTrackerMixin and QuestObjectiveTrackerMixin.WatchMoney then
        QuestObjectiveTrackerMixin.WatchMoney(self, watch)
    end
end

function NomToolsZoneObjectiveTrackerMixin:DoQuestObjectives(block, questCompleted, questSequenced, isExistingBlock, useFullHeight)
    if QuestObjectiveTrackerMixin and QuestObjectiveTrackerMixin.DoQuestObjectives then
        return QuestObjectiveTrackerMixin.DoQuestObjectives(self, block, questCompleted, questSequenced, isExistingBlock, useFullHeight)
    end

    return false
end

function NomToolsZoneObjectiveTrackerMixin:TryAddingExpirationWarningLine(block, questID)
    if WorldQuestObjectiveTrackerMixin and WorldQuestObjectiveTrackerMixin.TryAddingExpirationWarningLine then
        WorldQuestObjectiveTrackerMixin.TryAddingExpirationWarningLine(self, block, questID)
    end
end

function NomToolsZoneObjectiveTrackerMixin:SetUpQuestBlock(block, forceShowCompleted)
    if BonusObjectiveTrackerMixin and BonusObjectiveTrackerMixin.SetUpQuestBlock then
        BonusObjectiveTrackerMixin.SetUpQuestBlock(self, block, forceShowCompleted)
    end
end

function NomToolsZoneObjectiveTrackerMixin:ShouldDisplayQuest(quest)
    return GetZoneQuestType(quest) ~= nil
end

function NomToolsZoneObjectiveTrackerMixin:AddWatchedQuest(quest)
    local didLayout = QuestObjectiveTrackerMixin and QuestObjectiveTrackerMixin.UpdateSingle and QuestObjectiveTrackerMixin.UpdateSingle(self, quest)
    if didLayout ~= false and quest and quest.GetID then
        local block = self:GetExistingBlock(quest:GetID())
        if block then
            block.nomtoolsQuestKind = GetZoneQuestType(quest)
        end
    end

    return didLayout ~= false
end

function NomToolsZoneObjectiveTrackerMixin:AddTaskQuest(questID, questKind, isTrackedWorldQuest)
    local didLayout = false

    WithTaskTrackerTemplates(self, questKind, function()
        if questKind == "worldQuest" then
            EnsureQuestRewardData(questID)
        end

        if BonusObjectiveTrackerMixin and BonusObjectiveTrackerMixin.AddQuest then
            didLayout = BonusObjectiveTrackerMixin.AddQuest(self, questID, isTrackedWorldQuest) ~= false
        end
    end)

    if didLayout then
        local block = self:GetExistingBlock(questID, "BonusObjectiveTrackerBlockTemplate")
        if block then
            block.nomtoolsQuestKind = questKind
        end
    end

    return didLayout
end

function NomToolsZoneObjectiveTrackerMixin:AddZoneTask(questID, questKind, forceShowInArea)
    local isTrackedWorldQuest = questKind == "worldQuest" and forceShowInArea == true or false
    return self:AddTaskQuest(questID, questKind, isTrackedWorldQuest)
end

local zoneTrackerLayoutDeps = {
    buildZoneQuestLists = BuildZoneQuestLists,
    finalizeModuleLayout = FinalizeModuleLayout,
    getSortedTrackedWorldQuestIDs = GetSortedTrackedWorldQuestIDs,
    getZoneFilters = GetZoneFilters,
    isModuleEnabled = IsModuleEnabled,
    isZoneEligibleQuest = IsZoneEligibleQuest,
    zoneTrackerHeader = ZONE_TRACKER_HEADER,
}

function NomToolsZoneObjectiveTrackerMixin:LayoutContents()
    local deps = zoneTrackerLayoutDeps

    self.tickerSeconds = 0

    if not deps.isModuleEnabled() then
        return deps.finalizeModuleLayout(self)
    end

    self:SetHeader(deps.zoneTrackerHeader)
    self:AddAutoQuestObjectives()
    if self:HasSkippedBlocks() then
        return deps.finalizeModuleLayout(self)
    end

    self:WatchMoney(false)

    local campaignQuests, regularQuests = deps.buildZoneQuestLists()
    for _, quest in ipairs(campaignQuests) do
        if not ns.ShouldFilterQuest(quest:GetID(), quest) then
            if not self:AddWatchedQuest(quest) then
                return deps.finalizeModuleLayout(self)
            end
        end
    end

    for _, quest in ipairs(regularQuests) do
        if not ns.ShouldFilterQuest(quest:GetID(), quest) then
            if not self:AddWatchedQuest(quest) then
                return deps.finalizeModuleLayout(self)
            end
        end
    end

    local zone = deps.getZoneFilters()
    if zone.worldQuests then
        local tasksTable = GetTasksTable and GetTasksTable() or nil
        if tasksTable then
            for index = 1, #tasksTable do
                local questID = tasksTable[index]
                if questID
                    and QuestUtils_IsQuestWorldQuest(questID)
                    and not QuestUtils_IsQuestWatched(questID)
                    and deps.isZoneEligibleQuest(questID, "worldQuest")
                    and not ns.ShouldFilterQuest(questID)
                then
                    if not self:AddZoneTask(questID, "worldQuest", false) then
                        return deps.finalizeModuleLayout(self)
                    end
                end
            end
        end
        

        for _, questID in ipairs(deps.getSortedTrackedWorldQuestIDs()) do
            if deps.isZoneEligibleQuest(questID, "trackedWorldQuest") and not ns.ShouldFilterQuest(questID) then
                if not self:AddZoneTask(questID, "worldQuest", true) then
                    return deps.finalizeModuleLayout(self)
                end
            end
        end
    end

    if zone.bonusObjectives and GetTasksTable then
        local tasksTable = GetTasksTable()
        if tasksTable then
            for index = 1, #tasksTable do
                local questID = tasksTable[index]
                if questID
                    and not QuestUtils_IsQuestWorldQuest(questID)
                    and not QuestUtils_IsQuestWatched(questID)
                    and deps.isZoneEligibleQuest(questID, "bonusObjective")
                    and not ns.ShouldFilterQuest(questID)
                then
                    if not self:AddZoneTask(questID, "bonusObjective") then
                        return deps.finalizeModuleLayout(self)
                    end
                end
            end
        end
    end

    return deps.finalizeModuleLayout(self)
end

function NomToolsZoneObjectiveTrackerMixin:OnBlockHeaderClick(block, mouseButton)
    if block and (block.nomtoolsQuestKind == "worldQuest" or block.nomtoolsQuestKind == "bonusObjective") then
        local previousShowWorldQuests = self.showWorldQuests
        self.showWorldQuests = block.nomtoolsQuestKind == "worldQuest"
        if BonusObjectiveTrackerMixin and BonusObjectiveTrackerMixin.OnBlockHeaderClick then
            BonusObjectiveTrackerMixin.OnBlockHeaderClick(self, block, mouseButton)
        end
        self.showWorldQuests = previousShowWorldQuests
        return
    end

    if QuestObjectiveTrackerMixin and QuestObjectiveTrackerMixin.OnBlockHeaderClick then
        QuestObjectiveTrackerMixin.OnBlockHeaderClick(self, block, mouseButton)
    end
end

function NomToolsZoneObjectiveTrackerMixin:OnBlockHeaderEnter(block)
    if block and (block.nomtoolsQuestKind == "worldQuest" or block.nomtoolsQuestKind == "bonusObjective") then
        local previousShowWorldQuests = self.showWorldQuests
        self.showWorldQuests = block.nomtoolsQuestKind == "worldQuest"
        if BonusObjectiveTrackerMixin and BonusObjectiveTrackerMixin.OnBlockHeaderEnter then
            BonusObjectiveTrackerMixin.OnBlockHeaderEnter(self, block)
        end
        self.showWorldQuests = previousShowWorldQuests
        return
    end

    if QuestObjectiveTrackerMixin and QuestObjectiveTrackerMixin.OnBlockHeaderEnter then
        QuestObjectiveTrackerMixin.OnBlockHeaderEnter(self, block)
    end
end

function NomToolsZoneObjectiveTrackerMixin:OnBlockHeaderLeave(block)
    if block and (block.nomtoolsQuestKind == "worldQuest" or block.nomtoolsQuestKind == "bonusObjective") then
        if BonusObjectiveTrackerMixin and BonusObjectiveTrackerMixin.OnBlockHeaderLeave then
            BonusObjectiveTrackerMixin.OnBlockHeaderLeave(self, block)
        elseif GameTooltip then
            GameTooltip:Hide()
        end
        return
    end

    if QuestObjectiveTrackerMixin and QuestObjectiveTrackerMixin.OnBlockHeaderLeave then
        QuestObjectiveTrackerMixin.OnBlockHeaderLeave(self, block)
    elseif GameTooltip then
        GameTooltip:Hide()
    end
end

function NomToolsZoneObjectiveTrackerMixin:OnFreeBlock(block)
    block.ItemButton = nil
    block.nomtoolsQuestKind = nil
    block.numObjectives = nil
    block.taskName = nil
    block.nomtoolsSortData = nil
end

local NomToolsFocusedQuestTrackerMixin = {}
for key, value in pairs(K.FOCUSED_TRACKER_SETTINGS) do
    NomToolsFocusedQuestTrackerMixin[key] = value
end

NomToolsFocusedQuestTrackerMixin.InitModule = NomToolsZoneObjectiveTrackerMixin.InitModule
NomToolsFocusedQuestTrackerMixin.OnHide = NomToolsZoneObjectiveTrackerMixin.OnHide
NomToolsFocusedQuestTrackerMixin.OnEvent = NomToolsZoneObjectiveTrackerMixin.OnEvent
NomToolsFocusedQuestTrackerMixin.WatchMoney = NomToolsZoneObjectiveTrackerMixin.WatchMoney
NomToolsFocusedQuestTrackerMixin.DoQuestObjectives = NomToolsZoneObjectiveTrackerMixin.DoQuestObjectives
NomToolsFocusedQuestTrackerMixin.TryAddingExpirationWarningLine = NomToolsZoneObjectiveTrackerMixin.TryAddingExpirationWarningLine
NomToolsFocusedQuestTrackerMixin.SetUpQuestBlock = NomToolsZoneObjectiveTrackerMixin.SetUpQuestBlock
NomToolsFocusedQuestTrackerMixin.AddWatchedQuest = NomToolsZoneObjectiveTrackerMixin.AddWatchedQuest
NomToolsFocusedQuestTrackerMixin.AddTaskQuest = NomToolsZoneObjectiveTrackerMixin.AddTaskQuest
NomToolsFocusedQuestTrackerMixin.AddZoneTask = NomToolsZoneObjectiveTrackerMixin.AddZoneTask
NomToolsFocusedQuestTrackerMixin.OnBlockHeaderClick = NomToolsZoneObjectiveTrackerMixin.OnBlockHeaderClick
NomToolsFocusedQuestTrackerMixin.OnBlockHeaderEnter = NomToolsZoneObjectiveTrackerMixin.OnBlockHeaderEnter
NomToolsFocusedQuestTrackerMixin.OnBlockHeaderLeave = NomToolsZoneObjectiveTrackerMixin.OnBlockHeaderLeave
NomToolsFocusedQuestTrackerMixin.OnFreeBlock = NomToolsZoneObjectiveTrackerMixin.OnFreeBlock

function NomToolsFocusedQuestTrackerMixin:OnEvent(event, ...)
    if not IsModuleEnabled() or not IsFocusedQuestEnabled() then
        return
    end

    local questID = ...
    if (event == "QUEST_DATA_LOAD_RESULT" or event == "QUEST_TURNED_IN" or event == "QUEST_REMOVED")
        and type(questID) == "number"
        and questID > 0
    then
        state.pendingRewardDataQuestIDs[questID] = nil
    end

    RequestCustomModuleRefresh(self)
end

local function RegisterCustomModuleEvents(module)
    if not module or module.nomtoolsEventsRegistered or type(module.events) ~= "table" then
        return
    end

    for _, event in ipairs(module.events) do
        module:RegisterEvent(event)
    end

    module.nomtoolsEventsRegistered = true
end

local function UnregisterCustomModuleEvents(module)
    if not module or not module.nomtoolsEventsRegistered or type(module.events) ~= "table" then
        return
    end

    for _, event in ipairs(module.events) do
        module:UnregisterEvent(event)
    end

    module.nomtoolsEventsRegistered = false
end

function NomToolsFocusedQuestTrackerMixin:ShouldDisplayQuest(quest)
    if not IsFocusedQuestEnabled() then
        return false
    end

    local focusedQuestID = GetSuperTrackedQuestID()
    return quest and quest.GetID and focusedQuestID and quest:GetID() == focusedQuestID or false
end

function NomToolsFocusedQuestTrackerMixin:LayoutContents()
    self.tickerSeconds = 0

    if not IsModuleEnabled() or not IsFocusedQuestEnabled() then
        return FinalizeModuleLayout(self)
    end

    self:SetHeader(FOCUSED_TRACKER_HEADER)
    self:WatchMoney(false)

    local focusedQuestID = GetSuperTrackedQuestID()
    if not focusedQuestID then
        return FinalizeModuleLayout(self)
    end

    if ns.ShouldFilterQuest(focusedQuestID) then
        return FinalizeModuleLayout(self)
    end

    local quest = BuildRuntimeQuestEntry(focusedQuestID)
    local questKind = GetQuestKind(focusedQuestID, quest)
    if questKind == "quest" or questKind == "campaign" then
        if not quest or not self:AddWatchedQuest(quest) then
            return FinalizeModuleLayout(self)
        end
    elseif questKind == "worldQuest" or questKind == "bonusObjective" then
        if not self:AddZoneTask(focusedQuestID, questKind, true) then
            return FinalizeModuleLayout(self)
        end
    else
        return FinalizeModuleLayout(self)
    end

    return FinalizeModuleLayout(self)
end

local function EnsureFocusedQuestModule()
    if state.focusedQuestModule or not ObjectiveTrackerFrame then
        return state.focusedQuestModule
    end

    local focusedQuestModule = CreateFrame("Frame", FOCUSED_TRACKER_NAME, ObjectiveTrackerFrame, "ObjectiveTrackerModuleTemplate")
    if QuestObjectiveTrackerMixin then
        Mixin(focusedQuestModule, QuestObjectiveTrackerMixin)
    end
    Mixin(focusedQuestModule, NomToolsFocusedQuestTrackerMixin)
    focusedQuestModule:SetScript("OnEvent", focusedQuestModule.OnEvent)
    if not focusedQuestModule.usedBlocks and ObjectiveTrackerModuleMixin and ObjectiveTrackerModuleMixin.OnLoad then
        ObjectiveTrackerModuleMixin.OnLoad(focusedQuestModule)
    end
    focusedQuestModule:SetHeader(FOCUSED_TRACKER_HEADER)

    state.focusedQuestModule = focusedQuestModule
    return focusedQuestModule
end

local function EnsureZoneModule()
    if state.zoneModule or not ObjectiveTrackerFrame then
        return state.zoneModule
    end

    local zoneModule = CreateFrame("Frame", ZONE_TRACKER_NAME, ObjectiveTrackerFrame, "ObjectiveTrackerModuleTemplate")
    if QuestObjectiveTrackerMixin then
        Mixin(zoneModule, QuestObjectiveTrackerMixin)
    end
    Mixin(zoneModule, NomToolsZoneObjectiveTrackerMixin)
    zoneModule:SetScript("OnEvent", zoneModule.OnEvent)
    if not zoneModule.usedBlocks and ObjectiveTrackerModuleMixin and ObjectiveTrackerModuleMixin.OnLoad then
        ObjectiveTrackerModuleMixin.OnLoad(zoneModule)
    end
    zoneModule:SetHeader(ZONE_TRACKER_HEADER)

    -- Blizzard's ObjectiveTracker pipeline calls Show() on modules whenever the
    -- container updates. If we only Hide() from the suppression watcher (e.g.
    -- while RefreshObjectiveTrackerState is deferred for combat), the next
    -- tracker Update re-shows the Zone module while it is still attached.
    -- Re-hide on every Show when the option + location context demand it.
    zoneModule:HookScript("OnShow", function(self)
        if IsZoneFeaturesSuppressedInCurrentInstance() then
            if self.SetHeight then
                self:SetHeight(0)
            end
            self:Hide()
        end
    end)

    state.zoneModule = zoneModule
    return zoneModule
end

do -- EnsureSearchFrame (scoped to avoid the 200-local-per-chunk limit)

---@return table|nil searchFrame
local function _CreateSearchFrame()
    if not ObjectiveTrackerFrame then
        return nil
    end
    if InCombatLockdown() then return nil end

    local searchFrame = CreateFrame("Frame", addonName .. "ObjectiveTrackerSearchFrame", ObjectiveTrackerFrame)
    searchFrame:SetHeight(K.SEARCH_FRAME_HEIGHT)
    searchFrame.nomtoolsSearchFrame = true
    searchFrame.nomtoolsObjectiveTrackerKey = "search"

    local bg = searchFrame:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(searchFrame)
    bg:SetColorTexture(0, 0, 0, 0.1)
    searchFrame.bg = bg

    local borderFrame = CreateFrame("Frame", nil, searchFrame, BackdropTemplateMixin and "BackdropTemplate" or nil)
    borderFrame:SetAllPoints(searchFrame)
    borderFrame:SetFrameLevel((searchFrame:GetFrameLevel() or 0) + 1)
    borderFrame:EnableMouse(false)
    searchFrame.borderFrame = borderFrame

    local editBox = CreateFrame("EditBox", nil, searchFrame, "InputBoxTemplate")
    -- Chrome visibility is managed dynamically by ApplySearchBarSettings based on preset
    editBox:SetAutoFocus(false)
    editBox:ClearAllPoints()
    editBox:SetPoint("TOPLEFT", searchFrame, "TOPLEFT", 8, 0)
    editBox:SetPoint("BOTTOMRIGHT", searchFrame, "BOTTOMRIGHT", -8, 0)
    editBox:SetFontObject(GameFontHighlightSmall)

    local placeholder = editBox:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
    placeholder:SetPoint("LEFT", editBox, "LEFT", 0, 0)
    placeholder:SetText(K.SEARCH_PLACEHOLDER_TEXT)
    editBox.placeholder = placeholder
    searchFrame.editBox = editBox
    searchFrame.placeholder = placeholder

    editBox:SetScript("OnTextChanged", function(self)
        local text = strtrim(self:GetText() or ""):lower()
        state.searchText = text
        if text == "" then
            placeholder:Show()
        else
            placeholder:Hide()
        end
        if state.searchDebounceTimer then
            state.searchDebounceTimer:Cancel()
            state.searchDebounceTimer = nil
        end
        if text == "" then
            RequestTrackerLayoutRefresh()
            state.postLayoutStyleDirty = true
        else
            state.searchDebounceTimer = C_Timer.NewTimer(K.SEARCH_DEBOUNCE_INTERVAL, function()
                state.searchDebounceTimer = nil
                RequestTrackerLayoutRefresh()
                state.postLayoutStyleDirty = true
            end)
        end
    end)

    editBox:SetScript("OnEscapePressed", function(self)
        self:SetText("")
        self:ClearFocus()
    end)

    state.searchFrame = searchFrame
    state.searchEditBox = editBox
    return searchFrame
end

ns.EnsureSearchFrame = function()
    if state.searchFrame then
        return state.searchFrame
    end
    return _CreateSearchFrame()
end

end -- do: EnsureSearchFrame

do -- EnsureFilterButton / EnsureFilterDropdown (scoped to avoid the 200-local-per-chunk limit)

local function GetFilterSettings()
    local settings = GetSettings()
    settings.filter = settings.filter or {}
    return settings.filter
end

local function IsFilterButtonEnabled()
    local b = GetSettings().buttons or {}
    return b.filterButton ~= false
end

-- Tables to hold dropdown row buttons for checkmark updates.
local typeButtons = {}
local sortButtons = {}
local directionButtons = {}
local zoneDirectionButtons = {}
local groupByZoneButton = nil
local showTrivialButton = nil

local function UpdateTypeChecks()
    local filterSettings = GetFilterSettings()
    local questTypes = filterSettings.questTypes or {}
    for _, tb in ipairs(typeButtons) do
        tb.checkmark:SetShown(not questTypes[tb.typeKey])
    end
end

local function UpdateSortChecks()
    local filterSettings = GetFilterSettings()
    for _, sb in ipairs(sortButtons) do
        sb.checkmark:SetShown(filterSettings.sortBy == sb.sortKey)
    end
end

local function UpdateDirectionChecks()
    local filterSettings = GetFilterSettings()
    for _, db in ipairs(directionButtons) do
        db.checkmark:SetShown(filterSettings.sortDirection == db.directionKey)
    end
end

local function UpdateZoneDirectionChecks()
    local filterSettings = GetFilterSettings()
    local zoneSortDir = filterSettings.zoneSortDirection or "asc"
    for _, db in ipairs(zoneDirectionButtons) do
        db.checkmark:SetShown(zoneSortDir == db.directionKey)
    end
end

local function UpdateGroupByZoneCheck()
    if not groupByZoneButton then return end
    local filterSettings = GetFilterSettings()
    groupByZoneButton.checkmark:SetShown(filterSettings.groupByZone == true)
end

local function UpdateShowTrivialCheck()
    if not showTrivialButton then return end
    local filterSettings = GetFilterSettings()
    showTrivialButton.checkmark:SetShown(filterSettings.showTrivial ~= false)
end

local function UpdateDirectionButtonsEnabled()
    local fs = GetFilterSettings()
    -- Quest sort direction: only relevant when sorting by name (not default)
    local questEnabled = fs.sortBy ~= "default"
    for _, db in ipairs(directionButtons) do
        if questEnabled then
            db:Enable()
            db.label:SetTextColor(1, 1, 1, 1)
        else
            db:Disable()
            db.label:SetTextColor(0.45, 0.45, 0.45, 1)
        end
    end
    -- Zone sort direction: only relevant when zone grouping is on
    local zoneEnabled = fs.groupByZone == true
    for _, db in ipairs(zoneDirectionButtons) do
        if zoneEnabled then
            db:Enable()
            db.label:SetTextColor(1, 1, 1, 1)
        else
            db:Disable()
            db.label:SetTextColor(0.45, 0.45, 0.45, 1)
        end
    end

    if groupByZoneButton and groupByZoneButton.label then
        groupByZoneButton:Enable()
        groupByZoneButton.label:SetTextColor(1, 1, 1, 1)
    end
end

local function IsFilterDefault()
    local fs = GetFilterSettings()
    if fs.questTypes and next(fs.questTypes) then return false end
    if fs.sortBy and fs.sortBy ~= "default" then return false end
    if fs.sortDirection and fs.sortDirection ~= "asc" then return false end
    if fs.groupByZone then return false end
    if fs.zoneSortDirection and fs.zoneSortDirection ~= "asc" then return false end
    if fs.showTrivial == false then return false end
    return true
end

local function UpdateFilterResetState()
    -- No-op: reset is now inside the dropdown menu
end

local function ResetFiltersToDefault()
    local fs = GetFilterSettings()
    wipe(fs)
    UpdateTypeChecks()
    UpdateSortChecks()
    UpdateDirectionChecks()
    UpdateZoneDirectionChecks()
    UpdateDirectionButtonsEnabled()
    UpdateGroupByZoneCheck()
    UpdateShowTrivialCheck()
    UpdateFilterResetState()
    RequestTrackerLayoutRefresh()
end

---@param yPos number
---@param text string
---@param parent table
---@return table header
local function AddDropHeader(yPos, text, parent)
    local lbl = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    lbl:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, yPos)
    lbl:SetText(text)
    lbl:SetTextColor(0.8, 0.72, 0.42)
    return lbl
end

---@param anchor table
---@param yOff number
---@param labelText string
---@param onClick function
---@param parent table
---@return table btn
local function AddDropButton(anchor, yOff, labelText, onClick, parent)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetHeight(18)
    btn:SetPoint("TOP",   anchor, "BOTTOM",  0, yOff)
    btn:SetPoint("LEFT",  parent, "LEFT",    4, 0)
    btn:SetPoint("RIGHT", parent, "RIGHT",  -4, 0)

    local check = btn:CreateTexture(nil, "ARTWORK")
    check:SetSize(14, 14)
    check:SetPoint("LEFT", btn, "LEFT", 10, 0)
    check:SetAtlas("checkmark-minimal")
    check:Hide()
    btn.checkmark = check

    local lbl = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    lbl:SetPoint("LEFT", check, "RIGHT", 4, 0)
    lbl:SetPoint("RIGHT", btn, "RIGHT", -8, 0)
    lbl:SetJustifyH("LEFT")
    lbl:SetText(labelText)
    btn.label = lbl

    btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestLogTitleHighlight", "ADD")
    btn:SetScript("OnClick", onClick)
    return btn
end

---@param parent table
---@return table divider
local function AddDivider(parent, curY)
    local div = parent:CreateTexture(nil, "ARTWORK")
    div:SetHeight(1)
    div:SetPoint("TOPLEFT",  parent, "TOPLEFT",  8, curY - 4)
    div:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -8, curY - 4)
    div:SetColorTexture(0.3, 0.3, 0.3, 0.8)
    return div
end

ns.EnsureFilterDropdown = function()
    if InCombatLockdown() then return nil end
    if state.filterDropFrame then
        return state.filterDropFrame
    end
    if not ObjectiveTrackerFrame or not ObjectiveTrackerFrame.Header then
        return nil
    end

    local header = ObjectiveTrackerFrame.Header
    local dropFrame = CreateFrame("Frame", addonName .. "ObjectiveTrackerFilterDrop",
                                  ObjectiveTrackerFrame, "BackdropTemplate")
    dropFrame:SetBackdrop({
        bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile = true, tileSize = 32, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    dropFrame:SetWidth(K.FILTER_DROP_WIDTH)
    dropFrame:SetPoint("TOPRIGHT", header, "BOTTOMRIGHT", 0, -2)
    dropFrame:SetFrameStrata("DIALOG")
    dropFrame:SetFrameLevel(100)
    dropFrame:Hide()
    dropFrame:EnableMouse(true)

    -- Close listener — fullscreen invisible frame behind dropdown
    local dropCloseListener = CreateFrame("Frame", nil, UIParent)
    dropCloseListener:EnableMouse(true)
    dropCloseListener:SetAllPoints(UIParent)
    dropCloseListener:SetFrameStrata("DIALOG")
    dropCloseListener:SetFrameLevel(50)
    dropCloseListener:Hide()
    dropCloseListener:SetScript("OnMouseDown", function()
        dropFrame:Hide()
        dropCloseListener:Hide()
    end)

    local filterSettings = GetFilterSettings()
    local curY = -8

    -- --- Section 1: Quest Types (multi-select, exclusion-set) ---
    local typeHeader = AddDropHeader(curY, "Quest Types", dropFrame)
    curY = curY - 18

    local lastTypeAnchor = typeHeader
    for _, key in ipairs(K.FILTER_QUEST_TYPE_KEYS) do
        local lbl = K.FILTER_QUEST_TYPE_LABELS[key] or key
        local tb = AddDropButton(lastTypeAnchor, -2, lbl, function()
            local fs = GetFilterSettings()
            fs.questTypes = fs.questTypes or {}
            if fs.questTypes[key] then
                fs.questTypes[key] = nil
            else
                fs.questTypes[key] = true
            end
            UpdateTypeChecks()
            UpdateFilterResetState()
            RequestTrackerLayoutRefresh()
        end, dropFrame)
        tb.typeKey = key
        typeButtons[#typeButtons + 1] = tb
        lastTypeAnchor = tb
        curY = curY - 20
    end

    AddDivider(dropFrame, curY)
    curY = curY - 12

    -- --- Section 2: Other Filters ---
    local otherHeader = AddDropHeader(curY, "Other Filters", dropFrame)
    curY = curY - 18

    showTrivialButton = AddDropButton(otherHeader, -2, "Show Trivial Quests", function()
        local fs = GetFilterSettings()
        fs.showTrivial = fs.showTrivial == false
        UpdateShowTrivialCheck()
        UpdateFilterResetState()
        RequestTrackerLayoutRefresh()
    end, dropFrame)
    curY = curY - 20

    AddDivider(dropFrame, curY)
    curY = curY - 12

    -- --- Section 3: Sort By (radio) ---
    local sortHeader = AddDropHeader(curY, "Sort By", dropFrame)
    curY = curY - 18

    local lastSortAnchor = sortHeader
    for _, key in ipairs(K.SORT_MODE_KEYS) do
        local lbl = K.SORT_MODE_LABELS[key] or key
        local sb = AddDropButton(lastSortAnchor, -2, lbl, function()
            local fs = GetFilterSettings()
            fs.sortBy = key
            UpdateSortChecks()
            UpdateDirectionButtonsEnabled()
            UpdateFilterResetState()
            RequestTrackerLayoutRefresh()
        end, dropFrame)
        sb.sortKey = key
        sortButtons[#sortButtons + 1] = sb
        lastSortAnchor = sb
        curY = curY - 20
    end

    AddDivider(dropFrame, curY)
    curY = curY - 12

    -- --- Section 4: Quest Sort Direction (radio) ---
    local dirHeader = AddDropHeader(curY, "Quest Sort Direction", dropFrame)
    curY = curY - 18

    local DIRECTION_DEFS = {
        { key = "asc",  label = "Ascending" },
        { key = "desc", label = "Descending" },
    }

    local lastDirAnchor = dirHeader
    for _, def in ipairs(DIRECTION_DEFS) do
        local db = AddDropButton(lastDirAnchor, -2, def.label, function()
            local fs = GetFilterSettings()
            fs.sortDirection = def.key
            UpdateDirectionChecks()
            UpdateFilterResetState()
            RequestTrackerLayoutRefresh()
        end, dropFrame)
        db.directionKey = def.key
        directionButtons[#directionButtons + 1] = db
        lastDirAnchor = db
        curY = curY - 20
    end

    AddDivider(dropFrame, curY)
    curY = curY - 12

    -- --- Section 5: Group By Zone (single checkbox) ---
    local groupHeader = AddDropHeader(curY, "Group By Zone", dropFrame)
    curY = curY - 18

    groupByZoneButton = AddDropButton(groupHeader, -2, "Separate by Zone", function()
        local fs = GetFilterSettings()
        fs.groupByZone = not fs.groupByZone
        UpdateGroupByZoneCheck()
        UpdateDirectionButtonsEnabled()
        UpdateFilterResetState()
        RequestTrackerLayoutRefresh()
    end, dropFrame)
    curY = curY - 20

    AddDivider(dropFrame, curY)
    curY = curY - 12

    -- --- Section 6: Zone Sort Direction (radio) ---
    local zoneDirHeader = AddDropHeader(curY, "Zone Sort Direction", dropFrame)
    curY = curY - 18

    local lastZoneDirAnchor = zoneDirHeader
    for _, def in ipairs(DIRECTION_DEFS) do
        local zdb = AddDropButton(lastZoneDirAnchor, -2, def.label, function()
            local fs = GetFilterSettings()
            fs.zoneSortDirection = def.key
            UpdateZoneDirectionChecks()
            UpdateFilterResetState()
            RequestTrackerLayoutRefresh()
        end, dropFrame)
        zdb.directionKey = def.key
        zoneDirectionButtons[#zoneDirectionButtons + 1] = zdb
        lastZoneDirAnchor = zdb
        curY = curY - 20
    end

    -- --- Reset Filters button at bottom ---
    AddDivider(dropFrame, curY)
    curY = curY - 12

    local resetBtn = CreateFrame("Button", nil, dropFrame)
    resetBtn:SetHeight(20)
    resetBtn:SetPoint("TOPLEFT", dropFrame, "TOPLEFT", 8, curY)
    resetBtn:SetPoint("TOPRIGHT", dropFrame, "TOPRIGHT", -8, curY)
    local resetLabel = resetBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    resetLabel:SetPoint("CENTER")
    resetLabel:SetText(RESET)
    resetLabel:SetTextColor(1, 0.82, 0)
    resetBtn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestLogTitleHighlight", "ADD")
    resetBtn:SetScript("OnClick", function()
        ResetFiltersToDefault()
        PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)
    end)
    resetBtn:SetScript("OnEnter", function(self)
        if IsFilterDefault() then
            resetLabel:SetTextColor(0.5, 0.5, 0.5)
        else
            resetLabel:SetTextColor(1, 1, 1)
        end
    end)
    resetBtn:SetScript("OnLeave", function()
        resetLabel:SetTextColor(1, 0.82, 0)
    end)
    state.filterResetButton = resetBtn
    curY = curY - 22

    dropFrame:SetHeight(math.abs(curY) + 12)

    state.filterDropFrame = dropFrame
    state.filterDropCloseListener = dropCloseListener

    dropFrame:SetScript("OnHide", function()
        state.filterDropCloseListener:Hide()
    end)

    return dropFrame
end

ns.EnsureFilterButton = function()
    if state.filterButton then
        return state.filterButton
    end
    if not ObjectiveTrackerFrame or not ObjectiveTrackerFrame.Header then
        return nil
    end
    if InCombatLockdown() then return nil end

    local header = ObjectiveTrackerFrame.Header
    local button = CreateFrame("Button", nil, header)
    button:SetSize(18, 19)

    button:SetPoint("RIGHT", header.MinimizeButton or header, "LEFT", -4, 0)

    local icon = button:CreateTexture(nil, "ARTWORK")
    icon:SetSize(18, 19)
    icon:SetPoint("CENTER")
    icon:SetAtlas("ui-questtrackerbutton-filter", false)
    button.icon = icon

    button:SetScript("OnMouseDown", function(self)
        if self.icon then
            self.icon:SetAtlas("ui-questtrackerbutton-filter-pressed", true)
        end
    end)
    button:SetScript("OnMouseUp", function(self)
        if self.icon then
            self.icon:SetAtlas("ui-questtrackerbutton-filter", true)
        end
    end)
    button:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText("Quest Filter", 1, 1, 1)
        GameTooltip:Show()
    end)
    button:HookScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    button:SetScript("OnClick", function()
        local dropFrame = ns.EnsureFilterDropdown()
        if not dropFrame then return end
        if dropFrame:IsShown() then
            dropFrame:Hide()
            if state.filterDropCloseListener then
                state.filterDropCloseListener:Hide()
            end
        else
            UpdateTypeChecks()
            UpdateSortChecks()
            UpdateDirectionChecks()
            UpdateZoneDirectionChecks()
            UpdateDirectionButtonsEnabled()
            UpdateGroupByZoneCheck()
            UpdateShowTrivialCheck()
            dropFrame:Show()
            if state.filterDropCloseListener then
                state.filterDropCloseListener:Show()
            end
        end
    end)


    state.filterButton = button
    return button
end

ns.RefreshFilterButton = function()
    local button = ns.EnsureFilterButton()
    if not button then
        return
    end

    local shouldShow = IsFilterButtonEnabled()
        and IsModuleEnabled()
        and ObjectiveTrackerFrame
        and ObjectiveTrackerFrame.Header
        and ObjectiveTrackerFrame.Header:IsShown()
    if InCombatLockdown() then return end
    button:SetShown(shouldShow)
    UpdateFilterResetState()

    -- Re-anchor in case the tracker button visibility changed.
    button:ClearAllPoints()
    local anchorTo = ObjectiveTrackerFrame.Header.MinimizeButton or ObjectiveTrackerFrame.Header
    button:SetPoint("RIGHT", anchorTo, "LEFT", -4, 0)
end

end -- do: EnsureFilterButton / EnsureFilterDropdown

do -- ShouldFilterQuest (scoped to stay under 200-local chunk limit)

---@param questID number
---@param quest table|nil
---@return boolean shouldHide true if the quest should be filtered out
ns.ShouldFilterQuest = function(questID, quest)
    if not IsModuleEnabled() then
        return false
    end

    local settings = GetSettings()
    settings.filter = settings.filter or {}
    local filterSettings = settings.filter
    local questTypes = filterSettings.questTypes or {}
    local searchText = state.searchText

    -- Classify quest into a type key (priority matches the title color system).
    local typeKey
    local isTrivial = IsTrivialQuest(questID)
    if IsPreyQuest(questID) then
        typeKey = "prey"
    else
        local recurring = GetRecurringQuestType(questID)
        if recurring then
            typeKey = recurring -- "daily" or "weekly"
        else
            local classification = GetQuestClassification(questID, quest)
            if classification and Enum and Enum.QuestClassification then
                if classification == Enum.QuestClassification.Meta then
                    typeKey = "meta"
                elseif classification == Enum.QuestClassification.Legendary then
                    typeKey = "legendary"
                elseif classification == Enum.QuestClassification.Campaign then
                    typeKey = "campaign"
                elseif classification == Enum.QuestClassification.Important then
                    typeKey = "important"
                end
            end
            if not typeKey then
                local kind = GetQuestKind(questID, quest)
                if kind == "campaign" then
                    typeKey = "campaign"
                elseif kind == "worldQuest" then
                    typeKey = "worldQuest"
                elseif kind == "bonusObjective" then
                    typeKey = "bonusObjective"
                else
                    typeKey = "regular"
                end
            end
        end
    end

    -- (a) Quest type exclusion filter.
    if typeKey and questTypes[typeKey] then
        return true
    end

    -- (a2) Trivial filter (independent of type classification).
    if isTrivial and filterSettings.showTrivial == false then
        return true
    end

    -- (b) Search text filter.
    if searchText and searchText ~= "" then
        local title = GetQuestHeaderTitle(questID)
        if title and title:lower():find(searchText, 1, true) then
            return false
        end

        local label = K.FILTER_QUEST_TYPE_LABELS[typeKey]
        if label and label:lower():find(searchText, 1, true) then
            return false
        end

        if isTrivial and ("trivial"):find(searchText, 1, true) then
            return false
        end

        if GetQuestObjectiveInfo then
            for i = 1, 10 do
                local text = GetQuestObjectiveInfo(questID, i, false)
                if not text then break end
                if text:lower():find(searchText, 1, true) then
                    return false
                end
            end
        end

        -- Zone name match
        do
            local mapID = C_TaskQuest and C_TaskQuest.GetQuestZoneID and C_TaskQuest.GetQuestZoneID(questID) or nil
            if not mapID or mapID == 0 then
                mapID = GetQuestUiMapID and GetQuestUiMapID(questID, true) or nil
            end
            if mapID and mapID ~= 0 then
                local mapInfo = C_Map and C_Map.GetMapInfo and C_Map.GetMapInfo(mapID) or nil
                local zoneName = mapInfo and mapInfo.name or nil
                if zoneName and zoneName:lower():find(searchText, 1, true) then
                    return false
                end
            end
        end

        return true
    end

    return false
end

end -- do: ShouldFilterQuest

-- Events Blizzard routes to ScenarioObjectiveTrackerMixin:OnEvent that can
-- actually affect NomTools styling (fonts, colors, level/kind prefixes,
-- warband indicator visibility). The high-frequency events listed below in
-- SCENARIO_HIGH_FREQ_EVENTS are intentionally excluded: in Delves they fire
-- essentially per-frame while moving (proximity criteria), and any work tied
-- to them is throttled separately.
local SCENARIO_STYLE_RELEVANT_EVENTS = {
    SCENARIO_UPDATE = true,
    SCENARIO_COMPLETED = true,
    SCENARIO_CRITERIA_SHOW_STATE_UPDATE = true,
}

-- Blizzard scenario events that cascade to ScenarioObjectiveTracker:MarkDirty()
-- which in turn forces the parent ObjectiveTrackerFrame to run a full
-- container Update + LayoutContents pass. In Delves these fire at display
-- rate (60-300+ Hz depending on framerate), and Blizzard's own LayoutContents
-- walk over scenario blocks/widgets is the dominant Delve FPS cost — confirmed
-- by the Blizzard EU Tech Support thread on the ScenarioObjectiveTracker
-- delve performance bug, which reproduces with all addons disabled.
--
-- We coalesce these events with a leading-edge throttle so the original
-- handler (and therefore Blizzard's MarkDirty -> Update cascade) runs at most
-- once per delveUpdateThrottleSeconds window (0 = disabled). The first event in a quiet
-- window fires immediately so completion / criterion changes feel responsive;
-- subsequent events inside the window are coalesced into a single deferred
-- call at the end of the window so we never lose the trailing state.
local SCENARIO_HIGH_FREQ_EVENTS = {
    SCENARIO_CRITERIA_UPDATE = true,
    SCENARIO_SPELL_UPDATE = true,
}

-- Interval from settings (delveUpdateThrottleSeconds; 0 = no throttle). Applied in
-- ScenarioObjectiveTracker:MarkDirty, ScenarioObjectiveTrackerMixin.OnEvent (high-freq),
-- and QuestObjectiveTracker:MarkDirty (everywhere, not only Delves).

-- Wrap ScenarioObjectiveTracker:MarkDirty with a leading-edge throttle.
-- This is a belt-and-suspenders measure for paths that bypass OnEvent — most
-- importantly the widget container OnSizeChanged callbacks (Delve timer
-- widgets, criteria progress widgets) which call MarkDirty directly at
-- display rate as the widget animates. Without this, those paths still drive
-- per-frame Blizzard LayoutContents passes even with the OnEvent throttle.
local function InstallScenarioMarkDirtyThrottle()
    if state.scenarioMarkDirtyThrottleInstalled then return end
    local module = _G.ScenarioObjectiveTracker
    if type(module) ~= "table" or type(module.MarkDirty) ~= "function" then
        return
    end

    state.scenarioMarkDirtyThrottleInstalled = true

    local originalMarkDirty = module.MarkDirty
    local lastMarkTime = 0
    local pending = false

    module.MarkDirty = function(self)
        local throttleSec = state.GetDelveObjectiveTrackerUpdateThrottleSeconds and state.GetDelveObjectiveTrackerUpdateThrottleSeconds() or 0
        if throttleSec <= 0 then
            return originalMarkDirty(self)
        end
        state.DiagInc("scenario.markdirty.called")
        local now = GetTime()
        if (now - lastMarkTime) >= throttleSec then
            lastMarkTime = now
            pending = false
            state.DiagInc("scenario.markdirty.fired_immediate")
            return originalMarkDirty(self)
        end
        if not pending then
            pending = true
            state.DiagInc("scenario.markdirty.deferred")
            local delay = throttleSec - (now - lastMarkTime)
            if delay < 0 then delay = 0 end
            C_Timer.After(delay, function()
                pending = false
                lastMarkTime = GetTime()
                state.DiagInc("scenario.markdirty.fired_deferred")
                originalMarkDirty(self)
            end)
        else
            state.DiagInc("scenario.markdirty.coalesced")
        end
    end
end

local function EnsureScenarioStyleHookInstalled()
    InstallScenarioMarkDirtyThrottle()

    -- QUEST_POI_UPDATE at display rate while moving; quest MarkDirty drives container
    -- Update -> NomTools post-hook. Same throttle as scenario (delveUpdateThrottleSeconds).
    if not state.delveQuestMarkDirtyThrottleInstalled then
        local questMod = _G.QuestObjectiveTracker
        if type(questMod) == "table" and type(questMod.MarkDirty) == "function" then
            state.delveQuestMarkDirtyThrottleInstalled = true
            local originalQuestMarkDirty = questMod.MarkDirty
            local lastQuestMarkTime = 0
            local questMarkPending = false
            questMod.MarkDirty = function(self)
                local throttleSec = state.GetDelveObjectiveTrackerUpdateThrottleSeconds and state.GetDelveObjectiveTrackerUpdateThrottleSeconds() or 0
                if throttleSec <= 0 then
                    return originalQuestMarkDirty(self)
                end
                state.DiagInc("delvequest.markdirty.called")
                local now = GetTime()
                if (now - lastQuestMarkTime) >= throttleSec then
                    lastQuestMarkTime = now
                    questMarkPending = false
                    state.DiagInc("delvequest.markdirty.fired_immediate")
                    return originalQuestMarkDirty(self)
                end
                if not questMarkPending then
                    questMarkPending = true
                    state.DiagInc("delvequest.markdirty.deferred")
                    local delay = throttleSec - (now - lastQuestMarkTime)
                    if delay < 0 then
                        delay = 0
                    end
                    C_Timer.After(delay, function()
                        questMarkPending = false
                        lastQuestMarkTime = GetTime()
                        state.DiagInc("delvequest.markdirty.fired_deferred")
                        originalQuestMarkDirty(self)
                    end)
                else
                    state.DiagInc("delvequest.markdirty.coalesced")
                end
            end
        end
    end

    if state.scenarioEventHookInstalled then
        return true
    end

    if type(ScenarioObjectiveTrackerMixin) ~= "table" then
        return false
    end

    local originalOnEvent = ScenarioObjectiveTrackerMixin.OnEvent
    if type(originalOnEvent) ~= "function" then
        return false
    end

    state.scenarioEventHookInstalled = true

    local lastFireTime = {}
    local pending = {}

    local function EmitNomToolsStyleSignal(self, event)
        if not self or self.parentContainer ~= ObjectiveTrackerFrame then
            return
        end
        if not IsModuleEnabled() then
            return
        end
        if SCENARIO_STYLE_RELEVANT_EVENTS[event] then
            state.MarkModuleRefreshPending(self, false, true)
        end
    end

    -- Replace (not hooksecurefunc) the mixin OnEvent so we can actually
    -- suppress Blizzard's MarkDirty cascade for high-frequency events.
    -- ScenarioObjectiveTracker is unprotected UI, so this carries no taint
    -- risk. Other addons that hooksecurefunc OnEvent will still see every
    -- raw event because their post-hook fires after our wrapper returns.
    ScenarioObjectiveTrackerMixin.OnEvent = function(self, event, ...)
        state.DiagEvent("scenario:" .. event)
        if SCENARIO_HIGH_FREQ_EVENTS[event] then
            local throttleSec = state.GetDelveObjectiveTrackerUpdateThrottleSeconds and state.GetDelveObjectiveTrackerUpdateThrottleSeconds() or 0
            if throttleSec <= 0 then
                state.DiagInc("scenario.onevent.passthrough_nothrottle")
                local t0 = state.DiagNow()
                originalOnEvent(self, event, ...)
                state.DiagTime("scenario.onevent.original", state.DiagNow() - t0)
                EmitNomToolsStyleSignal(self, event)
                return
            end
            local now = GetTime()
            local last = lastFireTime[event] or 0
            if (now - last) >= throttleSec then
                lastFireTime[event] = now
                pending[event] = nil
                state.DiagInc("scenario.onevent.fired_immediate")
                local t0 = state.DiagNow()
                originalOnEvent(self, event, ...)
                state.DiagTime("scenario.onevent.original", state.DiagNow() - t0)
                EmitNomToolsStyleSignal(self, event)
                return
            end

            if not pending[event] then
                pending[event] = true
                state.DiagInc("scenario.onevent.deferred")
                local arg1, arg2, arg3, arg4 = ...
                local delay = throttleSec - (now - last)
                if delay < 0 then delay = 0 end
                C_Timer.After(delay, function()
                    pending[event] = nil
                    lastFireTime[event] = GetTime()
                    state.DiagInc("scenario.onevent.fired_deferred")
                    local t0 = state.DiagNow()
                    originalOnEvent(self, event, arg1, arg2, arg3, arg4)
                    state.DiagTime("scenario.onevent.original", state.DiagNow() - t0)
                    EmitNomToolsStyleSignal(self, event)
                end)
            else
                state.DiagInc("scenario.onevent.coalesced")
            end
            return
        end

        state.DiagInc("scenario.onevent.passthrough")
        local t0 = state.DiagNow()
        originalOnEvent(self, event, ...)
        state.DiagTime("scenario.onevent.original", state.DiagNow() - t0)
        EmitNomToolsStyleSignal(self, event)
    end

    return true
end

local function InstallTrackerHooks()
    if state.trackerHooksInstalled or not QuestObjectiveTrackerMixin or not CampaignQuestObjectiveTrackerMixin or not WorldQuestObjectiveTrackerMixin or not BonusObjectiveTrackerMixin then

        return
    end

    state.trackerHooksInstalled = true

    state.originalQuestShouldDisplayQuest = QuestObjectiveTrackerMixin.ShouldDisplayQuest
    QuestObjectiveTrackerMixin.ShouldDisplayQuest = function(self, quest)
        local shouldDisplay = state.originalQuestShouldDisplayQuest and state.originalQuestShouldDisplayQuest(self, quest)
        if not shouldDisplay then
            return false
        end

        if not IsModuleEnabled() then
            return true
        end

        local questID = quest:GetID()
        if not IsZoneSuppressionActive() and IsZoneEligibleQuest(questID, "quest") then return false end
        if ns.ShouldFilterQuest(questID, quest) then return false end
        return true
    end
    if QuestObjectiveTracker then
        QuestObjectiveTracker.ShouldDisplayQuest = QuestObjectiveTrackerMixin.ShouldDisplayQuest
    end

    state.originalCampaignShouldDisplayQuest = CampaignQuestObjectiveTrackerMixin.ShouldDisplayQuest
    CampaignQuestObjectiveTrackerMixin.ShouldDisplayQuest = function(self, quest)
        local shouldDisplay = state.originalCampaignShouldDisplayQuest and state.originalCampaignShouldDisplayQuest(self, quest)
        if not shouldDisplay then
            return false
        end

        if not IsModuleEnabled() then
            return true
        end

        local questID = quest:GetID()
        if not IsZoneSuppressionActive() and IsZoneEligibleQuest(questID, "campaign") then return false end
        if ns.ShouldFilterQuest(questID, quest) then return false end
        return true
    end
    if CampaignQuestObjectiveTracker then
        CampaignQuestObjectiveTracker.ShouldDisplayQuest = CampaignQuestObjectiveTrackerMixin.ShouldDisplayQuest
    end

    state.originalBonusAddQuest = BonusObjectiveTrackerMixin.AddQuest
    BonusObjectiveTrackerMixin.AddQuest = function(self, questID, isTrackedWorldQuest)
        if self == BonusObjectiveTracker then
            if not IsModuleEnabled() then
                return state.originalBonusAddQuest(self, questID, isTrackedWorldQuest)
            end
            if not IsZoneSuppressionActive() and IsZoneEligibleQuest(questID, "bonusObjective") then return true end
            if ns.ShouldFilterQuest(questID) then return true end
        end

        return state.originalBonusAddQuest(self, questID, isTrackedWorldQuest)
    end
    if BonusObjectiveTracker then
        BonusObjectiveTracker.AddQuest = BonusObjectiveTrackerMixin.AddQuest
    end

    -- WorldQuestObjectiveTrackerMixin is built with CreateFromMixins which copies
    -- AddQuest at mixin time, so overriding BonusObjectiveTrackerMixin.AddQuest above
    -- has no effect on it. Hook it directly so zone-eligible world quests are filtered
    -- out of the stock world-quest section and shown only in NomTools's zone section.
    state.originalWorldQuestAddQuest = WorldQuestObjectiveTrackerMixin.AddQuest
    WorldQuestObjectiveTrackerMixin.AddQuest = function(self, questID, isTrackedWorldQuest)
        if IsModuleEnabled() then
            if not IsZoneSuppressionActive() then
                -- For tracked world quests use the map-based eligibility check so that
                -- zone-eligible quests are suppressed from Blizzard's section at the same
                -- moment the NomTools zone section would display them (immediately on zone
                -- entry, without waiting for the server isInArea flag to propagate).
                local eligibleType = isTrackedWorldQuest and "trackedWorldQuest" or "worldQuest"
                if IsZoneEligibleQuest(questID, eligibleType) then
                    return true
                end
            end
            if ns.ShouldFilterQuest(questID) then
                return true
            end
        end
        return state.originalWorldQuestAddQuest(self, questID, isTrackedWorldQuest)
    end
    if WorldQuestObjectiveTracker then
        WorldQuestObjectiveTracker.AddQuest = WorldQuestObjectiveTrackerMixin.AddQuest
    end

    EnsureScenarioStyleHookInstalled()
end

local function moduleOrderComparator(left, right)
    return (left.uiOrder or math.huge) < (right.uiOrder or math.huge)
end

local cachedContainerModules = {}

local function GetOrderedContainerModules()
    local modules = cachedContainerModules
    if not ObjectiveTrackerFrame or type(ObjectiveTrackerFrame.modules) ~= "table" then
        for k in pairs(modules) do modules[k] = nil end
        state.cachedContainerModulesCount = -1
        for k in pairs(state.cachedContainerModuleRefs) do state.cachedContainerModuleRefs[k] = nil end
        for k in pairs(state.cachedContainerModuleOrders) do state.cachedContainerModuleOrders[k] = nil end
        state.cachedContainerModulesSearchFrame = nil
        state.cachedContainerModulesSearchOrder = nil
        state.cachedContainerModulesNeedsSorting = nil
        return modules
    end

    local containerModules = ObjectiveTrackerFrame.modules
    local moduleCount = #containerModules
    local needsSorting = ObjectiveTrackerFrame.needsSorting == true
    local cachedModuleRefs = state.cachedContainerModuleRefs
    local cachedModuleOrders = state.cachedContainerModuleOrders
    local cacheMatchesModules = true

    if state.cachedContainerModulesCount == moduleCount then
        for index = 1, moduleCount do
            local module = containerModules[index]
            if cachedModuleRefs[index] ~= module or cachedModuleOrders[index] ~= (module and module.uiOrder or nil) then
                cacheMatchesModules = false
                break
            end
        end
    else
        cacheMatchesModules = false
    end

    if cacheMatchesModules
        and state.cachedContainerModulesNeedsSorting == needsSorting
    then
        return cachedContainerModules
    end

    for k in pairs(modules) do modules[k] = nil end
    for k in pairs(cachedModuleRefs) do cachedModuleRefs[k] = nil end
    for k in pairs(cachedModuleOrders) do cachedModuleOrders[k] = nil end

    for index, module in ipairs(containerModules) do
        modules[#modules + 1] = module
        cachedModuleRefs[index] = module
        cachedModuleOrders[index] = module and module.uiOrder or nil
    end

    -- The search frame is intentionally NOT part of the ordered module
    -- chain anymore. It is docked directly below the tracker header via
    -- ApplySearchFramePosition and sits outside the scroll region, so
    -- it cannot contribute to module sorting, spacing, or layout.
    table.sort(modules, moduleOrderComparator)

    state.cachedContainerModulesCount = moduleCount
    state.cachedContainerModulesNeedsSorting = needsSorting

    return modules
end

local function BuildOrderedModules(includeCustomModules)
    local modules = {}
    local seen = {}
    local order = GetNormalizedOrder(GetSettings().order)
    local suppressZoneFeatures = IsZoneFeaturesSuppressedInCurrentInstance()

    -- The search bar is NOT included in the ordered module list: it's
    -- docked below the header outside the scroll region. Any legacy
    -- "search" entry in settings.order is silently skipped.
    for _, key in ipairs(order) do
        local includeKey = key ~= "search"
            and ((key ~= "zone" and key ~= "focusedQuest") or includeCustomModules)
            and (key ~= "zone" or not suppressZoneFeatures)
            and (key ~= "focusedQuest" or IsFocusedQuestEnabled())
        if includeKey then
            local module = GetModuleByKey(key)
            if module and not seen[module] then
                modules[#modules + 1] = module
                seen[module] = true
            end
        end
    end

    for _, key in ipairs(K.DEFAULT_ORDER) do
        local includeKey = key ~= "search"
            and ((key ~= "zone" and key ~= "focusedQuest") or includeCustomModules)
            and (key ~= "zone" or not suppressZoneFeatures)
            and (key ~= "focusedQuest" or IsFocusedQuestEnabled())
        if includeKey then
            local module = GetModuleByKey(key)
            if module and not seen[module] then
                modules[#modules + 1] = module
                seen[module] = true
            end
        end
    end

    return modules
end

local function ApplyCategoryOrder()
    if not ObjectiveTrackerFrame or not ObjectiveTrackerManager then
        return
    end

    if next(state.originalModuleOrders) == nil and type(ObjectiveTrackerFrame.modules) == "table" then
        for _, module in ipairs(ObjectiveTrackerFrame.modules) do
            state.originalModuleOrders[module] = module and module.uiOrder or nil
        end
    end

    local orderedModules = BuildOrderedModules(IsModuleEnabled())
    if #orderedModules == 0 then
        return
    end

    ObjectiveTrackerManager:AssignModulesOrder(orderedModules)
    ObjectiveTrackerFrame.needsSorting = true
end

local function RestoreOriginalModuleOrder()
    if not ObjectiveTrackerFrame or next(state.originalModuleOrders) == nil then
        return
    end

    for module, uiOrder in pairs(state.originalModuleOrders) do
        if module then
            module.uiOrder = uiOrder
        end
    end

    ObjectiveTrackerFrame.needsSorting = true
end

local function RefreshLayoutCallback()
    state.layoutRefreshPending = false
    if ObjectiveTrackerManager and ObjectiveTrackerManager.UpdateAll then
        ObjectiveTrackerManager:UpdateAll()
    elseif ObjectiveTrackerFrame and ObjectiveTrackerFrame.Update then
        ObjectiveTrackerFrame:Update()
    end
end

RequestTrackerLayoutRefresh = function()
    state.DiagInc("requesttrackerlayoutrefresh.calls")
    if state.layoutRefreshPending then
        state.DiagInc("requesttrackerlayoutrefresh.coalesced")
        return
    end

    state.DiagInc("requesttrackerlayoutrefresh.scheduled")
    state.layoutRefreshPending = true

    if C_Timer and C_Timer.After then
        C_Timer.After(0, RefreshLayoutCallback)
    else
        RefreshLayoutCallback()
    end
end

local function GetDesiredRightInset(shouldScroll)
    return 0
end

local function UpdateLayoutHeightBudget(visibleHeight, contentHeight)
    if visibleHeight and contentHeight then
        return false
    end

    return false
end

local function GetTrackerFrameWidth()
    if not ObjectiveTrackerFrame then
        return 0
    end

    -- Do not call GetLeft()/GetRight() here.  When OTF is briefly positioned by
    -- UIParentRightManagedFrameContainer (Blizzard's secure edit-mode managed
    -- container), those methods return "secret" tainted values.  Subtracting them
    -- propagates taint into targetWidth, which then contaminates every module that
    -- receives SetWidth(targetWidth) — including UIWidgetObjectiveTracker whose
    -- UIWidget-managed frames share frame state with the GameTooltip
    -- EmbeddedItemTooltip hierarchy.  A tainted width on those frames causes
    -- "attempt to perform arithmetic on a secret number" failures in Blizzard's
    -- tooltip sizing code when hovering map task POIs.
    -- GetWidth() is safe: NomTools always calls SetWidth() on OTF with a plain
    -- non-secret value (ApplyTrackerDimensions).  In the dual-Minimap-anchor case
    -- GetWidth() returns the anchor-computed rendered width, which is also non-secret.
    local width = ObjectiveTrackerFrame.GetWidth and ResolvePublicNumber(ObjectiveTrackerFrame:GetWidth()) or nil
    if not width or width <= 0 then
        return 0
    end

    return width
end

local function GetTargetModuleWidth(module)
    if not module then
        return 0
    end

    local trackerWidth = GetTrackerFrameWidth()
    if trackerWidth <= 0 then
        return 0
    end

    local leftMargin = ResolvePublicNumber(module.leftMargin, 0) or 0
    local rightInset = ResolvePublicNumber(state.currentRightInset, 0) or 0
    return math.max(trackerWidth - leftMargin - rightInset, 1)
end

local function PrepareModuleForLayout(module)
    if not module or module.parentContainer ~= ObjectiveTrackerFrame then
        return
    end

    -- UIWidgetObjectiveTracker contains UIWidget-managed frames whose dimensions
    -- feed into Blizzard's EmbeddedItemTooltip/tooltip layout hierarchy.  Setting
    -- SetWidth on those frames with a potentially tainted targetWidth taints the
    -- tooltip chain and causes "secret number" arithmetic failures when map POI
    -- tooltips are shown.  Blizzard sizes this module itself; skip it here.
    if module == UIWidgetObjectiveTracker then
        return
    end

    -- WorldQuestObjectiveTracker embeds world-quest reward UI (item/currency/money)
    -- that shares the same GameTooltip / EmbeddedItemTooltip hierarchy as map POI
    -- tooltips.  Forcing width from NomTools can propagate taint into that chain and
    -- trigger MoneyFrame arithmetic failures on quest rewards (same class of bug as
    -- UIWidgetObjectiveTracker above).  Let Blizzard lay out this module.
    if WorldQuestObjectiveTracker and module == WorldQuestObjectiveTracker then
        return
    end

    local targetWidth = GetTargetModuleWidth(module)
    if targetWidth <= 0 then
        return
    end

    if module.SetWidth and type(targetWidth) == "number" then
        module:SetWidth(targetWidth)
    end

    local contentsFrame = module.ContentsFrame
    if contentsFrame and contentsFrame.SetWidth then
        local pointCount = contentsFrame.GetNumPoints and contentsFrame:GetNumPoints() or 0
        if pointCount <= 1 then
            contentsFrame:SetWidth(targetWidth)
        end
    end
end

local function PrepareModulesForLayout(container)
    if container ~= ObjectiveTrackerFrame then
        return
    end

    for _, module in ipairs(GetOrderedContainerModules()) do
        PrepareModuleForLayout(module)
    end
end

local function GetModuleEffectiveHeight(module)
    if not module then
        return 0
    end

    if module.IsShown and not module:IsShown() then
        return 0
    end

    local frameHeight = module.GetHeight and module:GetHeight() or 0
    local contentsHeight = module.GetContentsHeight and module:GetContentsHeight() or 0
    local headerHeight = module.headerHeight or 0

    if module.Header and module.Header.IsShown and module.Header:IsShown() and module.Header.GetHeight then
        headerHeight = math.max(headerHeight, module.Header:GetHeight() or 0)
    end

    return math.max(frameHeight or 0, contentsHeight or 0, headerHeight or 0)
end

local function GetEffectiveSearchFrameHeight(searchModule)
    if not searchModule then
        return K.SEARCH_FRAME_HEIGHT or 16
    end

    local frameHeight = searchModule.GetHeight and searchModule:GetHeight() or nil
    if type(frameHeight) == "number" and frameHeight > 0 then
        return frameHeight
    end

    return K.SEARCH_FRAME_HEIGHT or 16
end

local function GetEffectiveTopPadding()
    local DEFAULT_TOP_MODULE_PADDING = 38
    local settings = GetSettings()
    if settings and settings.header and settings.header.enabled == false then
        state.lastKnownTopModulePadding = nil
        return 0
    end

    local resolvedTopPadding = nil

    local topModulePadding = ObjectiveTrackerFrame and ObjectiveTrackerFrame.topModulePadding or nil
    if type(topModulePadding) == "number" then
        resolvedTopPadding = math.max(0, topModulePadding)
        state.lastKnownTopModulePadding = resolvedTopPadding
    else
        local cachedTopModulePadding = state.lastKnownTopModulePadding
        if type(cachedTopModulePadding) == "number" then
            resolvedTopPadding = math.max(0, cachedTopModulePadding)
        else
            resolvedTopPadding = DEFAULT_TOP_MODULE_PADDING
        end
    end

    return resolvedTopPadding
end

local layoutScratch = {}
local layoutEntryPool = {}
local layoutEntryPoolUsed = 0

local function BuildVisibleModuleLayout()
    local layout = layoutScratch
    for k in pairs(layout) do layout[k] = nil end
    layoutEntryPoolUsed = 0
    if not ObjectiveTrackerFrame then
        return layout, 0
    end

    local topPadding = GetEffectiveTopPadding()
    local bottomPadding = ObjectiveTrackerFrame.bottomModulePadding or 0
    local moduleSpacing = ObjectiveTrackerFrame.moduleSpacing or 0
    local contentHeight = topPadding
    local hasVisibleModules = false

    -- Search bar is NOT in GetOrderedContainerModules anymore — it sits
    -- docked below the header via ApplySearchFramePosition, outside the
    -- scroll region — so no isSearch special-case is needed here.
    for _, module in ipairs(GetOrderedContainerModules()) do
        local moduleHeight = GetModuleEffectiveHeight(module)
        if moduleHeight > 0 then
            if hasVisibleModules then
                -- Keep uniform module spacing; pairwise no-gap compaction is intentionally disabled.
                contentHeight = contentHeight + moduleSpacing
            end

            local top = contentHeight
            local bottom = top + moduleHeight
            layoutEntryPoolUsed = layoutEntryPoolUsed + 1
            local entry = layoutEntryPool[layoutEntryPoolUsed]
            if not entry then
                entry = {}
                layoutEntryPool[layoutEntryPoolUsed] = entry
            end
            entry.module = module
            entry.top = top
            entry.bottom = bottom
            entry.height = moduleHeight
            layout[#layout + 1] = entry

            contentHeight = bottom
            hasVisibleModules = true
        end
    end

    if hasVisibleModules then
        contentHeight = contentHeight + bottomPadding
    end

    return layout, contentHeight
end

local function ClampScrollAnchorOffset(moduleHeight, offset)
    local normalizedHeight = math.max(0, math.floor((tonumber(moduleHeight) or 0) + 0.5))
    local maxOffset = 0
    if normalizedHeight > 0 then
        maxOffset = normalizedHeight - 1
    end

    local normalizedOffset = math.floor((tonumber(offset) or 0) + 0.5)
    if normalizedOffset < 0 then
        return 0
    end

    if normalizedOffset > maxOffset then
        return maxOffset
    end

    return normalizedOffset
end

local function RestorePendingScrollAnchor()
    local anchor = state.pendingScrollAnchor
    if not anchor then
        return
    end

    state.pendingScrollAnchor = nil

    if not anchor.module then
        return
    end

    local layout = BuildVisibleModuleLayout()
    for _, entry in ipairs(layout) do
        if entry.module == anchor.module then
            local offset = ClampScrollAnchorOffset(entry.height, anchor.offset)

            state.scrollOffset = math.max(0, math.floor((entry.top + offset) + 0.5))
            return
        end
    end
end

local function EnsureScrollClipFrame()
    if state.scrollClipFrame or not ObjectiveTrackerFrame then
        return state.scrollClipFrame
    end

    local clipFrame = CreateFrame("Frame", addonName .. "ObjectiveTrackerScrollClipFrame", ObjectiveTrackerFrame)
    clipFrame:SetClipsChildren(true)
    state.scrollClipFrame = clipFrame
    return clipFrame
end

-- Gap between the tracker header and the docked search bar, and also
-- between the docked search bar and the first content module. Using
-- the same value both above and below makes the search bar feel
-- visually balanced against the header.
local SEARCH_FRAME_HEADER_GAP = 3

-- Whether the docked search bar should currently be visible.
-- Single source of truth for both ApplySearchFramePosition (decides
-- whether to Show/Hide + anchor) and RefreshScrollClipFrame (decides
-- whether to reserve space above the clip region).
local function IsSearchFrameVisible()
    if not IsModuleEnabled() or not IsSearchEnabled() then
        return false
    end
    if ObjectiveTrackerFrame and ObjectiveTrackerFrame.IsCollapsed and ObjectiveTrackerFrame:IsCollapsed() then
        return false
    end
    return true
end

-- Vertical space (px) to push the scroll clip down so its top sits just
-- below the docked search bar. Returns 0 when the bar is hidden.
local function GetReservedSearchSpace()
    if not IsSearchFrameVisible() then
        return 0
    end

    -- Prefer saved settings height so reserve matches ApplySearchBarSettings
    -- and does not follow transient :GetHeight() spikes (seen in combat when
    -- layout has not settled), which inflated the clip offset.
    local height = K.SEARCH_FRAME_HEIGHT or 16
    local searchOpts = GetSettings() and GetSettings().search
    if type(searchOpts) == "table" and type(searchOpts.height) == "number" and searchOpts.height > 0 then
        height = searchOpts.height
    else
        local searchFrame = state.searchFrame
        if searchFrame and searchFrame.GetHeight then
            local h = searchFrame:GetHeight()
            if type(h) == "number" and h > 0 then
                height = math.min(h, 96)
            end
        end
    end

    -- One SEARCH_FRAME_HEADER_GAP only: padding between the bottom of the
    -- search bar and the top of the scrolling clip. The gap *above* the bar
    -- (header → search) is exclusively ApplySearchFramePosition's anchor
    -- offset from Header.BOTTOM. Adding a second gap here doubled the blank
    -- strip and looked like a "massive" hole—especially when :GetHeight()
    -- disagreed with the real bar for a frame.
    return height + SEARCH_FRAME_HEADER_GAP
end

-- Dock the search bar directly below the tracker header, outside the
-- scroll clip region and outside the module chain. Because the bar
-- lives in "header space" (a sibling of Header, not a module), its
-- position is never recomputed by Blizzard's container update and its
-- height never feeds back into module anchoring — which is what the
-- previous architecture kept re-introducing whenever the first content
-- module's height changed.
state.ApplySearchFramePosition = function()
    local searchFrame = state.searchFrame
    if not searchFrame or not ObjectiveTrackerFrame or not ObjectiveTrackerFrame.Header then
        return
    end

    if not IsSearchFrameVisible() then
        searchFrame:Hide()
        return
    end

    -- We own this frame and it is unprotected, so combat-lockdown does
    -- not forbid moving it. Guard only against the theoretical case
    -- where something else marked it protected.
    if searchFrame.IsProtected and searchFrame:IsProtected() then
        return
    end

    -- Re-parent to the outer ObjectiveTrackerFrame so the bar is never
    -- placed inside the scroll clip region (which would scroll it away)
    -- and never in container.modules (which Blizzard re-anchors on
    -- every Update pass).
    if searchFrame:GetParent() ~= ObjectiveTrackerFrame then
        searchFrame:SetParent(ObjectiveTrackerFrame)
    end

    local header = ObjectiveTrackerFrame.Header
    local baseLevel = (ObjectiveTrackerFrame.GetFrameLevel and ObjectiveTrackerFrame:GetFrameLevel()) or 0
    local headerLevel = (header.GetFrameLevel and header:GetFrameLevel()) or baseLevel
    local desiredLevel = math.max(headerLevel, baseLevel + 2)
    searchFrame:SetFrameStrata(ObjectiveTrackerFrame:GetFrameStrata())
    searchFrame:SetFrameLevel(desiredLevel)

    searchFrame:ClearAllPoints()
    searchFrame:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -SEARCH_FRAME_HEADER_GAP)
    searchFrame:SetPoint("TOPRIGHT", header, "BOTTOMRIGHT", 0, -SEARCH_FRAME_HEADER_GAP)
    -- Height is owned exclusively by ApplySearchBarSettings (which reads
    -- the user's `settings.search.height` value). Forcing a constant
    -- here would create a 1-frame oscillation between the user's
    -- configured height and K.SEARCH_FRAME_HEIGHT every time
    -- ApplyModuleAnchors and ApplySearchBarSettings ran in alternating
    -- order, which manifested as the docked bar visibly pulsing AND
    -- altered the value of GetReservedSearchSpace each pass — driving
    -- clipFrame's TOP up and down by the height delta.
    searchFrame:Show()
end

local function RefreshScrollClipFrame()
    local clipFrame = EnsureScrollClipFrame()
    if not clipFrame or not ObjectiveTrackerFrame then
        return nil
    end

    local trackerFrameLevel = ObjectiveTrackerFrame:GetFrameLevel() or 0
    local nineSliceFrameLevel = ObjectiveTrackerFrame.NineSlice and ObjectiveTrackerFrame.NineSlice:GetFrameLevel() or trackerFrameLevel
    local contentFrameLevel = math.max(trackerFrameLevel, nineSliceFrameLevel) + 1
    local topPadding = GetEffectiveTopPadding()
    -- Reserve vertical space for the docked search bar so content
    -- modules start *below* it, never overlap it, and never fight it
    -- for position. When search is hidden this collapses to 0 and the
    -- clip region snaps back to its default Blizzard offset.
    local searchReserve = GetReservedSearchSpace()
    clipFrame:SetFrameStrata(ObjectiveTrackerFrame:GetFrameStrata())
    clipFrame:SetFrameLevel(contentFrameLevel)
    clipFrame:ClearAllPoints()
    clipFrame:SetPoint("TOPLEFT", ObjectiveTrackerFrame, "TOPLEFT", -TRACKER_SCROLL_CLIP_LEFT_PADDING, -(topPadding + searchReserve))
    clipFrame:SetPoint("BOTTOMRIGHT", ObjectiveTrackerFrame, "BOTTOMRIGHT", TRACKER_MODULE_HEADER_RIGHT_EXTENSION, 0)
    clipFrame:SetShown(IsModuleEnabled())
    clipFrame:SetClipsChildren(IsModuleEnabled())
    return clipFrame
end

local function GetTrackerVisibleScrollHeight()
    local clipFrame = state.scrollClipFrame or RefreshScrollClipFrame()
    local visibleHeight = clipFrame and clipFrame.GetHeight and clipFrame:GetHeight() or nil
    if type(visibleHeight) == "number" and visibleHeight > 0 then
        return visibleHeight
    end

    local frameHeight = ObjectiveTrackerFrame and ObjectiveTrackerFrame.GetHeight and ObjectiveTrackerFrame:GetHeight() or 0
    local topPadding = GetEffectiveTopPadding()
    return math.max(0, (frameHeight or 0) - (topPadding or 0))
end

local function RestoreBlizzardModuleAnchors(clearPoints)
    if not ObjectiveTrackerFrame or type(ObjectiveTrackerFrame.modules) ~= "table" then
        return
    end

    if InCombatLockdown() then
        QueueCombatDeferredObjectiveTrackerMutation(false, true)
        return
    end

    for _, module in ipairs(ObjectiveTrackerFrame.modules) do
        if module and module.parentContainer == ObjectiveTrackerFrame then
            if module:GetParent() ~= ObjectiveTrackerFrame then
                module:SetParent(ObjectiveTrackerFrame)
            end
            if clearPoints and module.ClearAllPoints then
                module:ClearAllPoints()
            end
        end
    end
end

local function ApplyModuleAnchors()
    if not ObjectiveTrackerFrame then
        return
    end

    if not IsModuleEnabled() then
        return
    end

    local tApplyAnchors0 = state.diag.enabled and state.DiagNow() or nil

    -- Search position before RefreshScrollClipFrame: GetReservedSearchSpace must
    -- match the real bar height/points.
    if state.ApplySearchFramePosition then
        state.ApplySearchFramePosition()
    end

    local clipFrame = RefreshScrollClipFrame()
    if not clipFrame then
        return
    end

    -- Full module re-anchor must run in combat too. Blizzard's container
    -- Update (before our post-hook) re-chains modules onto ObjectiveTrackerFrame
    -- with TOP/topModulePadding and no search reserve. If we only refreshed the
    -- clip + search here and returned, the clip would reserve space under the
    -- header while modules stayed on Blizzard's chain — a large empty band between
    -- the search bar and the first module (e.g. Delves). ObjectiveTracker modules
    -- are not secure; SetParent/SetPoint on them is allowed under lockdown.

    local previousModule
    local bottomModulePadding = ObjectiveTrackerFrame.bottomModulePadding or 0
    local moduleSpacing = ObjectiveTrackerFrame.moduleSpacing or 0
    local moduleFrameLevel = (clipFrame:GetFrameLevel() or 0) + 1

    -- Topology-signature short-circuit. ApplyModuleAnchors is called on every
    -- RPL pass (plus scroll changes, sort applies, etc.). In steady state
    -- (the Delve-spam scenario) none of its inputs actually change between
    -- calls, so we compute a cheap signature over the inputs that drive
    -- anchor placement and skip the ClearAllPoints/SetPoint chain entirely
    -- when it matches the last applied one.
    --
    -- The search bar is deliberately excluded from this signature because
    -- it is no longer part of the module chain — its position is governed
    -- exclusively by ApplySearchFramePosition (docked below the header).
    local sigParts = state.moduleAnchorSignatureParts
    if not sigParts then
        sigParts = {}
        state.moduleAnchorSignatureParts = sigParts
    end
    for i = 1, #sigParts do sigParts[i] = nil end
    sigParts[1] = tostring(state.scrollOffset or 0)
    sigParts[2] = tostring(state.currentRightInset or 0)
    sigParts[3] = tostring(moduleSpacing)
    sigParts[4] = tostring(bottomModulePadding)
    sigParts[5] = tostring(state.scrollActive and 1 or 0)
    sigParts[6] = tostring(clipFrame)
    sigParts[7] = tostring(moduleFrameLevel)
    local sigIdx = 7
    for _, module in ipairs(GetOrderedContainerModules()) do
        local mh = GetModuleEffectiveHeight(module)
        sigIdx = sigIdx + 1
        sigParts[sigIdx] = (GetKeyForModule(module) or tostring(module))
            .. ":" .. tostring(math.floor((mh or 0) + 0.5))
            .. ":" .. tostring(module.leftMargin or 0)
    end
    local sig = table.concat(sigParts, "|")
    if state.lastModuleAnchorSignature == sig then
        if state.diag.enabled then
            state.DiagInc("applymoduleanchors.shortcircuited")
            if tApplyAnchors0 then
                state.DiagTime("applyanchors.path_shortcircuit", state.DiagNow() - tApplyAnchors0)
            end
        end
        return
    end
    if state.diag.enabled then
        state.DiagInc("applymoduleanchors.full_rebuild")
    end
    state.lastModuleAnchorSignature = sig

    local tReanchor0 = state.diag.enabled and state.DiagNow() or nil
    for _, module in ipairs(GetOrderedContainerModules()) do
        local moduleHeight = GetModuleEffectiveHeight(module)
        local shouldInclude = moduleHeight > 0

        if shouldInclude then
            PrepareModuleForLayout(module)

            if module:GetParent() ~= clipFrame then
                module:SetParent(clipFrame)
            end
            if module.SetFrameStrata and module:GetFrameStrata() ~= clipFrame:GetFrameStrata() then
                module:SetFrameStrata(clipFrame:GetFrameStrata())
            end
            if module.SetFrameLevel and (module:GetFrameLevel() or 0) < moduleFrameLevel then
                module:SetFrameLevel(moduleFrameLevel)
            end

            module:ClearAllPoints()
            local leftMargin = module.leftMargin or 0
            local rightInset = state.currentRightInset or 0
            if previousModule then
                local previousLeftMargin = previousModule.leftMargin or 0
                local spacingY = moduleSpacing
                module:SetPoint("TOPLEFT", previousModule, "BOTTOMLEFT", leftMargin - previousLeftMargin, -spacingY)
                module:SetPoint("TOPRIGHT", previousModule, "BOTTOMRIGHT", 0, -spacingY)
            else
                local topOffset = state.scrollOffset or 0
                module:SetPoint("TOPLEFT", clipFrame, "TOPLEFT", TRACKER_SCROLL_CLIP_LEFT_PADDING + leftMargin, topOffset)
                module:SetPoint("TOPRIGHT", clipFrame, "TOPRIGHT", -rightInset - TRACKER_MODULE_HEADER_RIGHT_EXTENSION, topOffset)
            end

            previousModule = module
        end
    end

    if ObjectiveTrackerFrame.NineSlice then
        if state.scrollActive and clipFrame then
            ObjectiveTrackerFrame.NineSlice:SetPoint("BOTTOM", clipFrame, "BOTTOM", 0, 0)
        elseif previousModule then
            ObjectiveTrackerFrame.NineSlice:SetPoint("BOTTOM", previousModule, "BOTTOM", 0, -bottomModulePadding)
        else
            ObjectiveTrackerFrame.NineSlice:SetPoint("BOTTOM", ObjectiveTrackerFrame, "BOTTOM", 0, 0)
        end
    end
    if tReanchor0 and state.diag.enabled then
        state.DiagTime("applyanchors.reanchor_and_nineslice", state.DiagNow() - tReanchor0)
    end
    if tApplyAnchors0 and state.diag.enabled then
        state.DiagTime("applyanchors.path_full_total", state.DiagNow() - tApplyAnchors0)
    end
end

do -- SortModuleBlocks (scoped to avoid the 200-local-per-chunk limit)

local SORTABLE_MODULE_KEYS = {
    quest = true,
    campaign = true,
    zone = true,
    focusedQuest = true,
    bonusObjective = true,
    worldQuest = true,
}

local sortScratch = {}
local sortPassID = 0

local function GetZoneDividerSettings()
    local settings = GetSettings()
    return settings and settings.zoneDivider or {}
end

local function GetSearchBarSettings()
    local settings = GetSettings()
    return settings and settings.search or {}
end

---@param questID number
---@return string
local function GetQuestZoneName(questID)
    if type(questID) ~= "number" then
        return ""
    end

    local passCache = state.questZoneNameSortPassCache
    if passCache then
        local cached = passCache[questID]
        if cached ~= nil then
            return cached
        end
    end

    local mapID = C_TaskQuest and C_TaskQuest.GetQuestZoneID and C_TaskQuest.GetQuestZoneID(questID) or nil
    if not mapID or mapID == 0 then
        mapID = GetQuestUiMapID and GetQuestUiMapID(questID, true) or nil
    end
    if not mapID or mapID == 0 then
        if passCache then
            passCache[questID] = ""
        end
        return ""
    end
    local info = C_Map and C_Map.GetMapInfo and C_Map.GetMapInfo(mapID) or nil
    local name = info and info.name or ""
    if passCache then
        passCache[questID] = name
    end
    return name
end

---@param block table
---@return table
local function GetBlockSortData(block)
    local cached = block.nomtoolsSortData
    if cached and cached.passID == sortPassID then
        return cached
    end
    if not cached then
        cached = {}
        block.nomtoolsSortData = cached
    end
    cached.passID = sortPassID
    cached.block = block
    local questID = block.id or block.questID or block.poiQuestID or 0
    cached.questID = questID
    cached.title = (block.HeaderText and block.HeaderText.GetText and block.HeaderText:GetText()) or GetQuestHeaderTitle(questID) or ""
    cached.zoneName = GetQuestZoneName(questID)
    return cached
end

-- No-op Reset so Blizzard's LayoutBlock / any frame-reset call site treats the
-- divider as a fully-formed block. Dividers carry no per-use state that needs
-- resetting, so this is intentionally empty.
local function ZoneDividerResetNoop() end

local function AcquireZoneDivider(module, index)
    local pool = module.nomtoolsZoneDividers
    if not pool then
        pool = {}
        module.nomtoolsZoneDividers = pool
    end
    local divider = pool[index]
    if divider then
        -- parentModule may have changed if the divider pool was shared across
        -- module reparents during combat edit mode; re-stamp defensively.
        divider.parentModule = module
        divider:Show()
        return divider
    end
    local parent = module.ContentsFrame or module
    divider = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)

    local label = divider:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    divider.label = label

    local lineLeft = divider:CreateTexture(nil, "ARTWORK")
    divider.lineLeft = lineLeft

    local lineRight = divider:CreateTexture(nil, "ARTWORK")
    divider.lineRight = lineRight

    -- Pre-allocate reusable backdrop tables (zero allocation on subsequent calls)
    divider.nomtoolsBackdrop = { bgFile = nil }
    divider.nomtoolsBackdropInsets = { left = 0, right = 0, top = 0, bottom = 0 }

    -- Block-shape surface so Blizzard's ObjectiveTrackerModuleMixin:LayoutBlock
    -- can treat the divider as a regular block. The full contract (see
    -- Blizzard_ObjectiveTrackerModule.lua) requires all of these fields:
    --   * parentModule           — owning module (set/refreshed each pass)
    --   * nextBlock              — rebuilt by LayoutBlock each pass
    --   * offsetX                — LEFT anchor offset (0 = flush to module left)
    --   * fixedWidth=false       — lets Blizzard set both TOPLEFT + TOPRIGHT
    --   * height                 — CRITICAL: LayoutBlock reads block.height as a
    --                              field (NOT :GetHeight()). Missing this causes
    --                              `block:SetHeight(nil)` + `block.height - offsetY`
    --                              to throw, which pcall silently swallows, making
    --                              the entire sort pass fail whenever groupByZone
    --                              is active.
    --   * fixedHeight=true       — prevents Blizzard's Block:Reset from zeroing
    --                              block.height (defensive; our Reset is a no-op
    --                              anyway, but this matches Blizzard's contract).
    --   * cached=false           — lets LayoutBlock's `not block.cached` check
    --                              fall through; CheckCachedBlocks(divider) early-
    --                              exits when cachedOrderList is nil (normal case).
    --   * id                     — stable unique identifier (not colliding with
    --                              any quest ID) so Blizzard's usedBlocks/cache
    --                              bookkeeping doesn't confuse dividers with
    --                              quests if it ever walks past our dividers.
    --   * used=false             — LayoutBlock sets this to true when added;
    --                              initialize defensively to avoid stale state.
    divider.parentModule = module
    divider.nextBlock = nil
    divider.offsetX = 0
    divider.fixedWidth = false
    divider.fixedHeight = true
    divider.cached = false
    divider.used = false
    divider.isDirty = false
    divider.height = 20
    divider.id = -1000 - index
    divider.Reset = ZoneDividerResetNoop
    divider.nomtoolsIsZoneDividerBlock = true

    pool[index] = divider
    return divider
end

---@param divider table
local function ApplyZoneDividerSettings(divider)
    local zdSettings = GetZoneDividerSettings()

    -- Height — keep both the frame height and the `block.height` field in sync.
    -- Blizzard's ObjectiveTrackerModuleMixin:LayoutBlock reads `block.height` as
    -- a table field (line `block:SetHeight(block.height)` and
    -- `InternalAddBlock`'s `contentsHeight + block.height - offsetY`). If we
    -- only call :SetHeight() without also updating the field, Blizzard's layout
    -- crashes on nil arithmetic and pcall silently kills the entire sort pass.
    local configuredHeight = zdSettings.height or 20
    divider.height = configuredHeight
    divider:SetHeight(configuredHeight)

    -- Background
    if divider.SetBackdrop then
        if zdSettings.showBackground == true then
            local bgTextureKey = zdSettings.backgroundTexture
            local bgTexture = ns.GetStatusBarTexturePath and ns.GetStatusBarTexturePath(bgTextureKey) or "Interface\\Buttons\\WHITE8x8"
            local borderTextureKey = zdSettings.borderTexture
            local borderSize = zdSettings.borderSize or 1

            local backdrop = divider.nomtoolsBackdrop
            backdrop.bgFile = bgTexture
            backdrop.edgeFile = nil
            backdrop.edgeSize = nil
            backdrop.insets = nil

            if borderTextureKey and borderTextureKey ~= "none" then
                local borderPath = ns.GetBorderTexturePath and ns.GetBorderTexturePath(borderTextureKey) or borderTextureKey
                backdrop.edgeFile = borderPath
                backdrop.edgeSize = math.max(borderSize, 1)
                local insets = divider.nomtoolsBackdropInsets
                insets.left = borderSize
                insets.right = borderSize
                insets.top = borderSize
                insets.bottom = borderSize
                backdrop.insets = insets
            end
            divider:SetBackdrop(nil)
            divider:SetBackdrop(backdrop)

            local bgColor = zdSettings.backgroundColor or K.DEFAULT_ZONE_DIVIDER_BG_COLOR
            divider:SetBackdropColor(bgColor.r or 0.05, bgColor.g or 0.05, bgColor.b or 0.05, bgColor.a or 0.6)

            if backdrop.edgeFile then
                local borderColor = zdSettings.borderColor or K.DEFAULT_ZONE_DIVIDER_BORDER_COLOR
                divider:SetBackdropBorderColor(borderColor.r or 0.3, borderColor.g or 0.3, borderColor.b or 0.3, borderColor.a or 1)
            end
        else
            divider:SetBackdrop(nil)
        end
    end

    -- Label font
    local label = divider.label
    if label then
        local fontPath = ns.GetFontPath and ns.GetFontPath(zdSettings.font) or STANDARD_TEXT_FONT
        local fontSize = zdSettings.fontSize or 12
        local fontOutline = ns.GetFontOutlineFlags and ns.GetFontOutlineFlags(zdSettings.fontOutline) or ""

        if fontPath then
            label:SetFont(fontPath, fontSize, fontOutline)
        else
            local existingFont, _, existingFlags = label:GetFont()
            if existingFont then
                label:SetFont(existingFont, fontSize, fontOutline ~= "" and fontOutline or existingFlags or "")
            end
        end

        local textColor = zdSettings.textColor or K.DEFAULT_ZONE_DIVIDER_TEXT_COLOR
        label:SetTextColor(textColor.r or 0.8, textColor.g or 0.72, textColor.b or 0.42, textColor.a or 1)
    end

    -- Line styling
    local lineColor = zdSettings.lineColor or K.DEFAULT_ZONE_DIVIDER_LINE_COLOR
    local lineThickness = zdSettings.lineThickness or 1

    if divider.lineLeft then
        divider.lineLeft:SetHeight(lineThickness)
        divider.lineLeft:SetColorTexture(lineColor.r or 0.8, lineColor.g or 0.72, lineColor.b or 0.42, lineColor.a or 0.5)
    end
    if divider.lineRight then
        divider.lineRight:SetHeight(lineThickness)
        divider.lineRight:SetColorTexture(lineColor.r or 0.8, lineColor.g or 0.72, lineColor.b or 0.42, lineColor.a or 0.5)
    end
end

local function ApplyDividerStyle(divider, align, showLines, fade)
    local label = divider.label
    local ll = divider.lineLeft
    local lr = divider.lineRight
    local pad = 4
    local gap = 8

    label:ClearAllPoints()
    ll:ClearAllPoints()
    lr:ClearAllPoints()

    if align == "left" then
        label:SetPoint("LEFT", divider, "LEFT", pad, 0)
        ll:Hide()
        if showLines then
            lr:SetPoint("LEFT", label, "RIGHT", gap, 0)
            lr:SetPoint("RIGHT", divider, "RIGHT", -pad, 0)
            lr:Show()
            if fade then
                lr:SetGradient("HORIZONTAL", K.GRADIENT_SOLID, K.GRADIENT_TRANSPARENT)
            else
                lr:SetGradient("HORIZONTAL", K.GRADIENT_SOLID, K.GRADIENT_SOLID)
            end
        else
            lr:Hide()
        end
    elseif align == "right" then
        label:SetPoint("RIGHT", divider, "RIGHT", -pad, 0)
        lr:Hide()
        if showLines then
            ll:SetPoint("LEFT", divider, "LEFT", pad, 0)
            ll:SetPoint("RIGHT", label, "LEFT", -gap, 0)
            ll:Show()
            if fade then
                ll:SetGradient("HORIZONTAL", K.GRADIENT_TRANSPARENT, K.GRADIENT_SOLID)
            else
                ll:SetGradient("HORIZONTAL", K.GRADIENT_SOLID, K.GRADIENT_SOLID)
            end
        else
            ll:Hide()
        end
    else -- "center"
        label:SetPoint("CENTER", divider, "CENTER", 0, 0)
        if showLines then
            ll:SetPoint("LEFT", divider, "LEFT", pad, 0)
            ll:SetPoint("RIGHT", label, "LEFT", -gap, 0)
            ll:Show()
            lr:SetPoint("LEFT", label, "RIGHT", gap, 0)
            lr:SetPoint("RIGHT", divider, "RIGHT", -pad, 0)
            lr:Show()
            if fade then
                ll:SetGradient("HORIZONTAL", K.GRADIENT_TRANSPARENT, K.GRADIENT_SOLID)
                lr:SetGradient("HORIZONTAL", K.GRADIENT_SOLID, K.GRADIENT_TRANSPARENT)
            else
                ll:SetGradient("HORIZONTAL", K.GRADIENT_SOLID, K.GRADIENT_SOLID)
                lr:SetGradient("HORIZONTAL", K.GRADIENT_SOLID, K.GRADIENT_SOLID)
            end
        else
            ll:Hide()
            lr:Hide()
        end
    end
end

local function ApplySearchBarSettings(searchFrame)
    if not searchFrame then return end
    local settings = GetSearchBarSettings()
    local trackerSettings = GetSettings()
    local useCustom = trackerSettings and trackerSettings.appearance and trackerSettings.appearance.preset == "nomtools"

    -- Height
    searchFrame:SetHeight(settings.height or 16)

    local editBox = searchFrame.editBox or state.searchEditBox
    local placeholder = searchFrame.placeholder or (editBox and editBox.placeholder)
    local bg = searchFrame.bg
    local borderFrame = searchFrame.borderFrame

    if not useCustom then
        -- Native Blizzard look: show InputBoxTemplate chrome, hide custom bg/border
        if editBox then
            if editBox.Left   then editBox.Left:Show()   end
            if editBox.Right  then editBox.Right:Show()  end
            if editBox.Middle then editBox.Middle:Show() end
            editBox:ClearAllPoints()
            editBox:SetPoint("TOPLEFT",     searchFrame, "TOPLEFT",     12, 0)
            editBox:SetPoint("BOTTOMRIGHT", searchFrame, "BOTTOMRIGHT", -5, 0)
        end
        if bg then bg:Hide() end
        if borderFrame then borderFrame:Hide() end

        -- Reset to standard Blizzard font/color
        if editBox then
            editBox:SetFontObject(GameFontHighlightSmall)
            editBox:SetTextColor(1, 1, 1, 1)
        end
        if placeholder then
            placeholder:SetTextColor(0.5, 0.5, 0.5, 1)
        end
    else
        -- Custom preset: hide InputBoxTemplate chrome, use custom bg/border/font
        if editBox then
            if editBox.Left   then editBox.Left:Hide()   end
            if editBox.Right  then editBox.Right:Hide()  end
            if editBox.Middle then editBox.Middle:Hide() end
            editBox:ClearAllPoints()
            editBox:SetPoint("TOPLEFT",     searchFrame, "TOPLEFT",     8, 0)
            editBox:SetPoint("BOTTOMRIGHT", searchFrame, "BOTTOMRIGHT", -8, 0)
        end

        -- Background texture
        if bg then
            if settings.showBackground ~= false then
                local bgTextureKey = settings.backgroundTexture
                if ns.GetStatusBarTexturePath then
                    local texturePath = ns.GetStatusBarTexturePath(bgTextureKey)
                    if texturePath then
                        styleHelpers.ApplyTexturePath(bg, texturePath)
                    else
                        bg:SetColorTexture(0, 0, 0, 1)
                    end
                else
                    bg:SetColorTexture(0, 0, 0, 1)
                end
                local bgColor = settings.backgroundColor or { r = 0, g = 0, b = 0, a = 0.1 }
                styleHelpers.ApplyRegionColor(bg, bgColor)
                bg:ClearAllPoints()
                bg:SetPoint("TOPLEFT", searchFrame, "TOPLEFT", -K.TRACKER_VISUAL_LEFT_EXTENSION, 0)
                bg:SetPoint("BOTTOMRIGHT", searchFrame, "BOTTOMRIGHT", 0, 0)
                bg:Show()
            else
                bg:Hide()
            end
        end

        -- Border via canonical helper
        if borderFrame then
            local borderColor = settings.borderColor or { r = 0.3, g = 0.3, b = 0.3, a = 1 }
            local _, borderThickness = styleHelpers.ApplyBackdropBorder(
                borderFrame,
                searchFrame,
                settings.borderSize,
                settings.borderTexture,
                borderColor,
                -K.TRACKER_VISUAL_LEFT_EXTENSION, 0
            )
            if borderThickness == 0 then
                borderFrame:Hide()
            end
        end

        -- Font for edit box
        if editBox and editBox.SetFont then
            local fontPath = ns.GetFontPath and ns.GetFontPath(settings.font) or nil
            local fontSize = settings.fontSize or 11
            local fontOutline = ns.GetFontOutlineFlags and ns.GetFontOutlineFlags(settings.fontOutline) or ""
            if fontPath then
                editBox:SetFont(fontPath, fontSize, fontOutline)
            end

            local textColor = settings.textColor or { r = 1, g = 1, b = 1, a = 1 }
            editBox:SetTextColor(textColor.r or 1, textColor.g or 1, textColor.b or 1, textColor.a or 1)
        end

        -- Placeholder font
        if placeholder then
            local fontPath = ns.GetFontPath and ns.GetFontPath(settings.font) or nil
            local fontSize = settings.fontSize or 11
            local fontOutline = ns.GetFontOutlineFlags and ns.GetFontOutlineFlags(settings.fontOutline) or ""
            if fontPath then
                placeholder:SetFont(fontPath, fontSize, fontOutline)
            end

            local phColor = settings.placeholderColor or { r = 0.5, g = 0.5, b = 0.5, a = 1 }
            placeholder:SetTextColor(phColor.r or 0.5, phColor.g or 0.5, phColor.b or 0.5, phColor.a or 1)
        end
    end
end

local function HideAllZoneDividers(module)
    if not module then
        return false
    end

    local pool = module.nomtoolsZoneDividers
    if not pool then
        module.nomtoolsGroupedAnchorsApplied = nil
        return false
    end

    local hadArtifacts = false
    for i = 1, #pool do
        local divider = pool[i]
        if divider then
            divider:Hide()
            divider:ClearAllPoints()
            -- Detach from the module.firstBlock chain so Blizzard's layout
            -- and our post-hook relayout never see a stale pooled divider.
            divider.nextBlock = nil
            if divider.label then
                divider.label:SetText("")
            end
            hadArtifacts = true
        end
    end

    module.nomtoolsGroupedAnchorsApplied = nil
    return hadArtifacts
end

ClearSortableModuleZoneDividerArtifacts = function(targetModule)
    if targetModule then
        return HideAllZoneDividers(targetModule)
    end

    local hadArtifacts = false
    for _, module in ipairs(GetOrderedContainerModules()) do
        local moduleKey = GetKeyForModule(module)
        if moduleKey and SORTABLE_MODULE_KEYS[moduleKey] then
            hadArtifacts = HideAllZoneDividers(module) or hadArtifacts
        end
    end

    return hadArtifacts
end

local compareSortBy, compareGroupByZone, compareQuestDescMul, compareZoneDescMul
local orderedBlockVisitedScratch = {}

local function CollectSortableBlocks(module, scratch)
    local count = 0
    local firstBlock = module and module.firstBlock or nil

    if firstBlock then
        state.blockCollectionPassID = (state.blockCollectionPassID or 0) + 1
        local collectionPassID = state.blockCollectionPassID
        for i = 1, #orderedBlockVisitedScratch do
            orderedBlockVisitedScratch[i] = nil
        end

        local chainIndex = 0
        local block = firstBlock
        while block and block.nomtoolsBlockCollectionPass ~= collectionPassID do
            chainIndex = chainIndex + 1
            block.nomtoolsBlockCollectionPass = collectionPassID
            orderedBlockVisitedScratch[chainIndex] = block
            block = block.nextBlock
        end

        if chainIndex > 0 then
            for i = 1, chainIndex do
                local chainedBlock = orderedBlockVisitedScratch[i]
                -- Skip synthetic zone-divider blocks: they are woven into the
                -- linked list by a previous SortModuleBlocks pass to drive
                -- Blizzard's native anchoring, but they are NOT sortable quest
                -- content and must not be fed into the comparator or the
                -- chain-order signature (which keys off questID).
                if chainedBlock
                    and not chainedBlock.nomtoolsIsZoneDividerBlock
                    and (not chainedBlock.IsShown or chainedBlock:IsShown())
                    and chainedBlock.GetHeight
                    and chainedBlock:GetHeight() > 0
                then
                    count = count + 1
                    scratch[count] = chainedBlock
                end
            end

            for i = count + 1, #scratch do
                scratch[i] = nil
            end

            return count
        end
    end

    if module and module.EnumerateActiveBlocks then
        module:EnumerateActiveBlocks(function(block)
            if block
                and not block.nomtoolsIsZoneDividerBlock
                and (not block.IsShown or block:IsShown())
                and block.GetHeight and block:GetHeight() > 0
            then
                count = count + 1
                scratch[count] = block
            end
        end)
    elseif module and type(module.usedBlocks) == "table" then
        for _, blocksByID in pairs(module.usedBlocks) do
            if type(blocksByID) == "table" then
                for _, block in pairs(blocksByID) do
                    if block
                        and not block.nomtoolsIsZoneDividerBlock
                        and (not block.IsShown or block:IsShown())
                        and block.GetHeight and block:GetHeight() > 0
                    then
                        count = count + 1
                        scratch[count] = block
                    end
                end
            end
        end
    end

    for i = count + 1, #scratch do
        scratch[i] = nil
    end

    return count
end

local function blockComparator(a, b)
    local da = GetBlockSortData(a)
    local db = GetBlockSortData(b)

    if compareGroupByZone then
        local za, zb = da.zoneName, db.zoneName
        if za ~= zb then
            if za < zb then return compareZoneDescMul == 1 end
            return compareZoneDescMul ~= 1
        end
    end

    if compareSortBy == "name" then
        local ta, tb = da.title or "", db.title or ""
        if ta ~= tb then
            if ta < tb then return compareQuestDescMul == 1 end
            return compareQuestDescMul ~= 1
        end
        return da.origIndex < db.origIndex
    end

    return da.origIndex < db.origIndex
end

-- =============================================================================
-- SortModuleBlocks — linked-list-only sort driven through Blizzard's LayoutBlock.
--
-- Previous approach re-anchored every block with a custom SetPoint chain,
-- NomTools-owned block spacing (nomtoolsBlockSpacingY, firstBlockOffsetY), and
-- manually positioned divider frames as siblings. Every Blizzard LayoutContents
-- pass wiped those custom anchors via ObjectiveTrackerBlockMixin:Reset, forcing
-- an immediate re-anchor — the oscillation the user sees in Delves is a frame
-- of Blizzard's default layout leaking through between the wipe and the redo.
--
-- The new approach:
--   1. Collect blocks and compute the same signature as before.
--   2. Sort the scratch list with the existing comparator.
--   3. Weave pooled zone-divider frames into the sorted sequence as first-class
--      "block" entries — they will be anchored by Blizzard's own LayoutBlock
--      using Blizzard's own spacing (blockOffsetY / fromHeaderOffsetY).
--   4. Reset module.firstBlock/lastBlock/contentsHeight and invoke
--      module:LayoutBlock(item) for each entry in order. Blizzard's LayoutBlock
--      performs the ClearAllPoints + SetPoint chain and accumulates
--      contentsHeight using the module's own constants, so NomTools owns zero
--      custom spacing values.
--   5. Set module height from Blizzard's contentsHeight + header height.
--
-- Because all ClearAllPoints / SetPoint / SetHeight calls on blocks come from
-- Blizzard's own LayoutBlock (invoked from inside a post-hook of
-- LayoutContents, before the next screen render), there is no interval where a
-- NomTools-only intermediate state is rendered. Dividers are part of the
-- module.firstBlock -> nextBlock chain, same as quest blocks.
-- =============================================================================

---@param divider table
---@param module table
local function ConfigureDividerAsBlock(divider, module)
    -- Most block-shape fields are stamped at creation in AcquireZoneDivider;
    -- this refreshes the mutable fields each time the divider is reused in a
    -- sort pass (parentModule can shift, nextBlock is rewritten by LayoutBlock).
    divider.parentModule = module
    divider.nextBlock = nil
    if not divider:IsShown() then
        divider:Show()
    end
end

---@param module table
---@return boolean heightChanged  true if the module's height was updated
ns.SortModuleBlocks = function(module)
    if not module then
        return false
    end

    local moduleKey = GetKeyForModule(module)
    if not moduleKey or not SORTABLE_MODULE_KEYS[moduleKey] then
        return false
    end

    local filterSettings = GetSettings().filter or {}
    local sortBy = filterSettings.sortBy or "default"
    local sortDirection = filterSettings.sortDirection or "asc"
    local groupByZoneGlobal = filterSettings.groupByZone == true

    -- Zone dividers belong ONLY in the main Quests section. Other sortable
    -- modules (campaign, the custom Zone module, focused-quest, bonus-objective,
    -- world-quest) must never get "Zone: X" headers spliced into their chain:
    --   * The custom Zone module already scopes itself to a single zone, so
    --     inserting zone dividers inside it is nonsensical and the dividers
    --     would flicker every time the player crosses a zone border while
    --     flying (the "zone section bugs out" report).
    --   * The Focused Quest module is a single-quest module by definition.
    --   * Campaign / world-quest / bonus-objective have their own grouping
    --     semantics and should stay as Blizzard laid them out.
    local groupByZone = groupByZoneGlobal and (moduleKey == "quest")
    local customSortActive = groupByZone or sortBy ~= "default"

    -- Any module that is NOT the Quests section must not retain divider
    -- artifacts: if groupByZone was ever toggled while those modules were
    -- sortable, the pool could still contain live divider frames that
    -- `AcquireZoneDivider` parented to them. Purge unconditionally for
    -- non-quest modules, and when groupByZone is off for the quest module.
    if ClearSortableModuleZoneDividerArtifacts and not groupByZone then
        ClearSortableModuleZoneDividerArtifacts(module)
    end

    -- When neither a custom sort nor grouping is active, leave Blizzard's
    -- layout entirely alone — no intervention, no re-anchoring, no chain
    -- mutation. This is the "works with Blizzard" default.
    if not customSortActive then
        module.nomtoolsLastSortSignature = nil
        module.nomtoolsGroupedAnchorsApplied = nil
        return false
    end

    -- Collect blocks in Blizzard's actual visual chain order. EnumerateActiveBlocks
    -- walks usedBlocks via pairs(), which is not a stable display-order source.
    local scratch = sortScratch
    local count = CollectSortableBlocks(module, scratch)

    local sortSignatureParts = module.nomtoolsSortSignatureParts
    if not sortSignatureParts then
        sortSignatureParts = {}
        module.nomtoolsSortSignatureParts = sortSignatureParts
    end

    -- Build a chain-order-sensitive signature from scratch. Called twice:
    -- once before sort (to compare against the stored post-sort signature)
    -- and once after sort (to store the new post-sort signature).
    local function BuildChainSignature()
        for i = 1, #sortSignatureParts do
            sortSignatureParts[i] = nil
        end
        sortSignatureParts[1] = tostring(sortBy)
        sortSignatureParts[2] = tostring(sortDirection)
        sortSignatureParts[3] = tostring(groupByZone == true)
        sortSignatureParts[4] = tostring(filterSettings.zoneSortDirection or "asc")
        sortSignatureParts[5] = tostring(count)
        local sigIdx = 5
        for i = 1, count do
            local block = scratch[i]
            local questID = block and (block.id or block.questID or block.poiQuestID) or 0
            sigIdx = sigIdx + 1
            sortSignatureParts[sigIdx] = tostring(questID or 0)
            if groupByZone then
                sigIdx = sigIdx + 1
                sortSignatureParts[sigIdx] = GetQuestZoneName(questID)
            end
            if sortBy == "name" then
                local title = (block and block.HeaderText and block.HeaderText.GetText and block.HeaderText:GetText())
                    or GetQuestHeaderTitle(questID)
                    or ""
                sigIdx = sigIdx + 1
                sortSignatureParts[sigIdx] = title
            end
        end
        return table.concat(sortSignatureParts, "\031")
    end

    -- Pre-sort signature: reflects the CURRENT chain order. If this matches
    -- the POST-sort signature stored on the last successful pass, it means
    -- the chain is already in our sorted order — nothing to do. If Blizzard
    -- rebuilt the chain (the common "fighting" case), pre-sort reflects
    -- Blizzard's data-source order, which differs from our stored post-sort
    -- signature, so we correctly fall through to re-sort.
    local preSortSignature = BuildChainSignature()
    if module.nomtoolsLastSortSignature == preSortSignature
        and module.nomtoolsGroupedAnchorsApplied == groupByZone
    then
        if state.diag.enabled then
            state.DiagInc("sortmoduleblocks.shortcircuited")
        end
        return false
    end

    -- Use Blizzard's canonical module.headerHeight (from ObjectiveTrackerModule
    -- settings). Blizzard's own BeginLayout reads this exact field to seed
    -- contentsHeight, so mirroring it here keeps our accounting identical to
    -- what a fresh Blizzard BeginLayout would produce.
    local headerHeightForContents = module.headerHeight or 0
    local oldHeight = (module.GetHeight and module:GetHeight()) or 0

    if count == 0 then
        HideAllZoneDividers(module)
        -- Empty module: mirror Blizzard's BeginLayout + UpdateHeight pipeline
        -- exactly. BeginLayout sets contentsHeight = headerHeight, then
        -- UpdateHeight sets module height to contentsHeight + bottomSpacing +
        -- heightModifier. Using UpdateHeight here guarantees the same height
        -- Blizzard would produce for an empty module, including bottomSpacing
        -- and any active slide heightModifier — no drift vs. Blizzard's value.
        module.firstBlock = nil
        module.lastBlock = nil
        module.contentsHeight = headerHeightForContents
        if module.UpdateHeight then
            module:UpdateHeight()
        elseif module.SetHeight then
            module:SetHeight(headerHeightForContents)
        end
        module.nomtoolsGroupedAnchorsApplied = groupByZone
        -- Empty module: signature is stable (no blocks). Pre == post.
        module.nomtoolsLastSortSignature = preSortSignature
        local newModuleHeight = (module.GetHeight and module:GetHeight()) or headerHeightForContents
        return math.abs(newModuleHeight - oldHeight) > 0.5
    end

    -- Sort blocks
    sortPassID = sortPassID + 1
    for i = 1, count do
        local data = GetBlockSortData(scratch[i])
        data.origIndex = i
    end

    compareSortBy = sortBy
    compareGroupByZone = groupByZone
    compareQuestDescMul = (sortDirection == "desc") and -1 or 1
    local zoneSortDir = filterSettings.zoneSortDirection or "asc"
    compareZoneDescMul = (zoneSortDir == "desc") and -1 or 1

    table.sort(scratch, blockComparator)

    -- Hide the divider pool up front; we reacquire in order below and any
    -- leftover pooled entries remain hidden (and are NOT included in the chain).
    HideAllZoneDividers(module)

    -- Build the ordered sequence of items (dividers + blocks) to hand off to
    -- Blizzard's LayoutBlock. `sortScratch` is reused after the sort completes,
    -- so we use a second scratch table stored on `state` (doesn't count toward
    -- the 200-local-per-chunk limit).
    local sequence = state.relayoutScratchSequence
    if not sequence then
        sequence = {}
        state.relayoutScratchSequence = sequence
    end
    for i = 1, #sequence do sequence[i] = nil end

    local zdSettings = GetZoneDividerSettings()
    local dividerIndex = 0
    local seqLen = 0
    local prevZone = nil

    for i = 1, count do
        local block = scratch[i]
        local data = GetBlockSortData(block)

        if groupByZone and data.zoneName ~= "" then
            if data.zoneName ~= prevZone then
                dividerIndex = dividerIndex + 1
                local divider = AcquireZoneDivider(module, dividerIndex)
                if divider.label then
                    divider.label:SetText(data.zoneName)
                end
                ApplyZoneDividerSettings(divider)
                ApplyDividerStyle(divider, zdSettings.align or "center", zdSettings.showLines ~= false, zdSettings.lineFadeOut ~= false)
                ConfigureDividerAsBlock(divider, module)
                seqLen = seqLen + 1
                sequence[seqLen] = divider
                prevZone = data.zoneName
            end
        else
            prevZone = nil
        end

        seqLen = seqLen + 1
        sequence[seqLen] = block
    end

    -- Drive Blizzard's native anchoring: reset chain state exactly like
    -- Blizzard's own BeginLayout (firstBlock/lastBlock nil, contentsHeight
    -- seeded at headerHeight). Then call Blizzard's LayoutBlock for each
    -- entry in our sorted order — it performs the ClearAllPoints + SetPoint
    -- chain with blockOffsetX / fromHeaderOffsetY / fromBlockOffsetY and
    -- accumulates contentsHeight identically to a fresh Blizzard pass.
    --
    -- NomTools performs ZERO SetPoint calls on blocks or dividers.
    module.firstBlock = nil
    module.lastBlock = nil
    module.contentsHeight = headerHeightForContents

    local layoutBlockFn = module.LayoutBlock
    if type(layoutBlockFn) == "function" then
        for i = 1, seqLen do
            layoutBlockFn(module, sequence[i])
        end
    end

    -- Apply module height via Blizzard's own UpdateHeight, which uses the
    -- canonical formula: SetHeight(contentsHeight + bottomSpacing + heightModifier).
    -- This is identical to what Blizzard's own EndLayout would produce and
    -- preserves bottomSpacing and any in-flight slide heightModifier, preventing
    -- the subtle module-height drift that drove ApplyModuleAnchors signature
    -- mismatches and the resulting per-frame tracker shifting.
    --
    -- We deliberately do NOT set ContentsFrame's height: it is XML-anchored to
    -- the module by all four sides (TOPLEFT/TOPRIGHT/BOTTOMLEFT/BOTTOMRIGHT) so
    -- its height follows the module automatically. Touching it would fight
    -- Blizzard's implicit sizing and could introduce geometry drift.
    if module.UpdateHeight then
        module:UpdateHeight()
    elseif module.SetHeight then
        module:SetHeight((module.contentsHeight or 0))
    end
    local newModuleHeight = (module.GetHeight and module:GetHeight()) or 0

    module.nomtoolsGroupedAnchorsApplied = groupByZone
    -- Store the POST-sort signature (scratch is now in our sorted order).
    -- Next pass's pre-sort signature of a chain still in our order will match
    -- this and short-circuit. A Blizzard-rebuild of the chain yields a
    -- different pre-sort signature and correctly re-triggers sort.
    module.nomtoolsLastSortSignature = BuildChainSignature()

    return math.abs(newModuleHeight - oldHeight) > 0.5
end

ns.RefreshZoneDividerAndSearchStyles = function()
    local zdSettings = GetZoneDividerSettings()
    for _, module in ipairs(GetOrderedContainerModules()) do
        local pool = module.nomtoolsZoneDividers
        if pool then
            for i = 1, #pool do
                local divider = pool[i]
                if divider and divider:IsShown() then
                    ApplyZoneDividerSettings(divider)
                    ApplyDividerStyle(divider, zdSettings.align or "center", zdSettings.showLines ~= false, zdSettings.lineFadeOut ~= false)
                end
            end
        end
    end
    ApplySearchBarSettings(state.searchFrame)
end

end -- do: SortModuleBlocks

local function EnsureScrollBar()
    if state.scrollBar or not ObjectiveTrackerFrame then
        return state.scrollBar
    end

    local scrollBar = CreateFrame("Slider", addonName .. "ObjectiveTrackerScrollBar", ObjectiveTrackerFrame)
    scrollBar:SetOrientation("VERTICAL")
    scrollBar:SetObeyStepOnDrag(true)
    scrollBar:SetValueStep(1)
    scrollBar:SetMinMaxValues(0, 0)
    scrollBar:SetValue(0)
    scrollBar:Hide()

    local background = scrollBar:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints()
    scrollBar.background = background

    local thumb = scrollBar:CreateTexture(nil, "ARTWORK")
    thumb:SetTexture(DEFAULT_STATUSBAR_TEXTURE_PATH)
    thumb:SetSize(K.SCROLL_BAR_DEFAULT_WIDTH, K.SCROLL_BAR_MIN_THUMB_HEIGHT)
    scrollBar:SetThumbTexture(thumb)
    scrollBar.thumb = thumb

    local borderFrame = CreateFrame("Frame", nil, scrollBar, BackdropTemplateMixin and "BackdropTemplate" or nil)
    borderFrame:SetAllPoints(scrollBar)
    borderFrame:SetFrameLevel((scrollBar:GetFrameLevel() or 0) + 1)
    borderFrame:EnableMouse(false)
    scrollBar.borderFrame = borderFrame

    scrollBar:SetScript("OnValueChanged", function(_, value)
        if state.updatingScrollBar then
            return
        end

        state.scrollOffset = math.floor((value or 0) + 0.5)
        ApplyModuleAnchors()
    end)

    state.scrollBar = scrollBar
    return scrollBar
end

local function RefreshTrackerHeader()
    if not ObjectiveTrackerFrame or not ObjectiveTrackerFrame.Header then
        return
    end

    if not IsModuleEnabled() then
        return
    end

    local header = ObjectiveTrackerFrame.Header
    local shouldShowHeader = ObjectiveTrackerFrame.ShouldShowHeader and ObjectiveTrackerFrame:ShouldShowHeader() or header:IsShown()

    if header.NomToolsTitle then
        header.NomToolsTitle:Hide()
    end

    local title = header.Text or header.Title or header.NomToolsTitle

    ObjectiveTrackerFrame.headerText = K.TRACKER_HEADER_TEXT

    if title then
        title:SetText(K.TRACKER_HEADER_TEXT)
        if title.SetTextColor then
            title:SetTextColor(1, 1, 1, 1)
        end
        -- Hide the title text when the header background is suppressed.
        title:SetShown(shouldShowHeader and IsHeaderTitleShown())
    end

    local clipFrame = state.scrollClipFrame
    if clipFrame then
        local minHeaderLevel = clipFrame:GetFrameLevel() + 2
        header:SetFrameLevel(math.max(header:GetFrameLevel(), minHeaderLevel))
    end

    -- Enforce the header-enabled setting; the hook on Header.Show handles re-shows from
    -- Blizzard, but we also need to act here for both directions of the toggle.
    if IsMainHeaderEnabled() then
        header:SetShown(shouldShowHeader)
    else
        header:Hide()
    end
end

local function RefreshScrollState()
    if not ObjectiveTrackerFrame then
        return
    end

    local scrollBar = EnsureScrollBar()
    if not scrollBar then
        return
    end

    if not IsModuleEnabled() or not IsScrollEnabled() then
        state.scrollOffset = 0
        state.scrollActive = false
        state.currentRightInset = 0
        state.lastContentHeight = 0
        if state.scrollClipFrame then
            state.scrollClipFrame:SetShown(false)
            state.scrollClipFrame:SetClipsChildren(false)
        end
        scrollBar:Hide()
        scrollBar:EnableMouse(false)
        scrollBar:GetScript("OnValueChanged")(scrollBar, 0)
        return
    end

    local visibleHeight = GetTrackerVisibleScrollHeight()
    if visibleHeight <= 0 then
        state.scrollActive = false
        scrollBar:Hide()
        scrollBar:EnableMouse(false)
        return
    end

    local layout, contentHeight = BuildVisibleModuleLayout()
    state.lastContentHeight = contentHeight
    local hasVisibleModules = #layout > 0
    UpdateLayoutHeightBudget(visibleHeight, contentHeight)

    local maxOffset = math.max(0, math.floor(contentHeight - visibleHeight + 0.5))
    if state.scrollOffset > maxOffset then
        state.scrollOffset = maxOffset
    end

    local shouldScroll = maxOffset > 0
    state.scrollActive = shouldScroll
    local desiredRightInset = GetDesiredRightInset(shouldScroll)
    if state.currentRightInset ~= desiredRightInset then
        state.currentRightInset = desiredRightInset
        RequestTrackerLayoutRefresh()
    end

    if state.scrollClipFrame then
        state.scrollClipFrame:SetShown(true)
        state.scrollClipFrame:SetClipsChildren(true)
    end

    if not shouldScroll then
        state.scrollOffset = 0
        scrollBar:Hide()
        scrollBar:EnableMouse(false)
        state.updatingScrollBar = true
        scrollBar:SetMinMaxValues(0, 0)
        scrollBar:SetValue(0)
        state.updatingScrollBar = false
        scrollBar:GetScript("OnValueChanged")(scrollBar, 0)
        return
    end

    local scrollSettings = GetScrollBarSettings()
    local scrollBarWidth = GetScrollBarWidth(scrollSettings)
    local scrollBarTexture = ns.GetStatusBarTexturePath and ns.GetStatusBarTexturePath(scrollSettings.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH
    local rawColor = scrollSettings.color
    local fallbackColor = K.DEFAULT_SCROLL_BAR_COLOR
    local scrollBarR, scrollBarG, scrollBarB, scrollBarA
    if type(rawColor) == "table" then
        scrollBarR = tonumber(rawColor.r or rawColor[1]) or fallbackColor.r
        scrollBarG = tonumber(rawColor.g or rawColor[2]) or fallbackColor.g
        scrollBarB = tonumber(rawColor.b or rawColor[3]) or fallbackColor.b
        scrollBarA = tonumber(rawColor.a or rawColor[4]) or fallbackColor.a
    else
        scrollBarR = fallbackColor.r
        scrollBarG = fallbackColor.g
        scrollBarB = fallbackColor.b
        scrollBarA = fallbackColor.a
    end
    local contentH = state.lastContentHeight or 0
    local effectiveH = (contentH > 0) and math.min(contentH, visibleHeight) or visibleHeight
    local rawBgColor = scrollSettings.backgroundColor
    local bgR, bgG, bgB, bgA
    if type(rawBgColor) == "table" then
        bgR = tonumber(rawBgColor.r or rawBgColor[1]) or (scrollBarR * 0.22)
        bgG = tonumber(rawBgColor.g or rawBgColor[2]) or (scrollBarG * 0.22)
        bgB = tonumber(rawBgColor.b or rawBgColor[3]) or (scrollBarB * 0.22)
        bgA = tonumber(rawBgColor.a or rawBgColor[4]) or math.min(scrollBarA, 0.35)
    else
        bgR = scrollBarR * 0.22
        bgG = scrollBarG * 0.22
        bgB = scrollBarB * 0.22
        bgA = math.min(scrollBarA, 0.35)
    end

    local ratio = visibleHeight / math.max(contentHeight, visibleHeight)
    local thumbHeight = math.max(K.SCROLL_BAR_MIN_THUMB_HEIGHT, math.floor(effectiveH * ratio))
    local fallbackBorderColor = K.DEFAULT_SCROLL_BAR_BORDER_COLOR
    local rawBorderColor = scrollSettings.borderColor
    local borderR, borderG, borderB, borderA
    if type(rawBorderColor) == "table" then
        borderR = tonumber(rawBorderColor.r or rawBorderColor[1]) or fallbackBorderColor.r
        borderG = tonumber(rawBorderColor.g or rawBorderColor[2]) or fallbackBorderColor.g
        borderB = tonumber(rawBorderColor.b or rawBorderColor[3]) or fallbackBorderColor.b
        borderA = tonumber(rawBorderColor.a or rawBorderColor[4]) or fallbackBorderColor.a
    else
        borderR = fallbackBorderColor.r
        borderG = fallbackBorderColor.g
        borderB = fallbackBorderColor.b
        borderA = fallbackBorderColor.a
    end
    local borderTexturePath = ns.GetBorderTexturePath and ns.GetBorderTexturePath(scrollSettings.borderTexture) or nil

    local clipFrame = state.scrollClipFrame
    local desiredStrata = clipFrame and clipFrame:GetFrameStrata() or scrollBar:GetFrameStrata()
    local desiredLevel = clipFrame and ((clipFrame:GetFrameLevel() or 0) + 3) or (scrollBar:GetFrameLevel() or 0)
    local wantsVisibleBar = IsScrollBarVisible()
    local visualCache = state.scrollBarVisualCache
    if not visualCache then
        visualCache = {}
        state.scrollBarVisualCache = visualCache
    end

    -- Track spans the full tracker chrome (main header + docked search + clip), not
    -- only the clip viewport — same vertical extent as RefreshScrollClipFrame's
    -- top offset plus visibleHeight, matching ObjectiveTrackerFrame height when
    -- Blizzard's layout is consistent.
    local barTrackHeight = visibleHeight + GetEffectiveTopPadding() + GetReservedSearchSpace()
    if ObjectiveTrackerFrame and ObjectiveTrackerFrame.GetHeight then
        local oh = ResolvePublicNumber(ObjectiveTrackerFrame:GetHeight())
        if type(oh) == "number" and oh > barTrackHeight then
            barTrackHeight = oh
        end
    end

    local needsVisualRefresh = visualCache.width ~= scrollBarWidth
        or visualCache.height ~= barTrackHeight
        or visualCache.texture ~= scrollBarTexture
        or visualCache.thumbHeight ~= thumbHeight
        or visualCache.scrollR ~= scrollBarR
        or visualCache.scrollG ~= scrollBarG
        or visualCache.scrollB ~= scrollBarB
        or visualCache.scrollA ~= scrollBarA
        or visualCache.bgR ~= bgR
        or visualCache.bgG ~= bgG
        or visualCache.bgB ~= bgB
        or visualCache.bgA ~= bgA
        or visualCache.borderR ~= borderR
        or visualCache.borderG ~= borderG
        or visualCache.borderB ~= borderB
        or visualCache.borderA ~= borderA
        or visualCache.borderSize ~= scrollSettings.borderSize
        or visualCache.borderTexture ~= borderTexturePath
        or visualCache.strata ~= desiredStrata
        or visualCache.level ~= desiredLevel

    if needsVisualRefresh then
        scrollBar:SetFrameStrata(desiredStrata)
        scrollBar:SetFrameLevel(desiredLevel)
        scrollBar:ClearAllPoints()
        -- Pin the track to the outer ObjectiveTrackerFrame so it runs from the
        -- tracker top (including the main header) through the docked search area
        -- down to the bottom edge; the clip region only holds scrollable modules.
        if clipFrame and ObjectiveTrackerFrame then
            scrollBar:SetPoint("TOPLEFT", ObjectiveTrackerFrame, "TOPRIGHT", 0, 0)
            scrollBar:SetPoint("BOTTOMLEFT", ObjectiveTrackerFrame, "BOTTOMRIGHT", 0, 0)
        else
            scrollBar:SetPoint("TOPLEFT", ObjectiveTrackerFrame, "TOPRIGHT", 0, 0)
            scrollBar:SetHeight(effectiveH)
        end
        scrollBar:SetWidth(scrollBarWidth)

        scrollBar.background:SetTexture(scrollBarTexture)
        scrollBar.background:SetVertexColor(bgR, bgG, bgB, bgA)
        scrollBar.thumb:SetTexture(scrollBarTexture)
        scrollBar.thumb:SetVertexColor(scrollBarR, scrollBarG, scrollBarB, scrollBarA)
        scrollBar.thumb:SetSize(scrollBarWidth, thumbHeight)

        if scrollBar.borderFrame then
            local borderColor = state.scrollBarBorderColor
            borderColor.r = borderR
            borderColor.g = borderG
            borderColor.b = borderB
            borderColor.a = borderA
            styleHelpers.ApplyBackdropBorder(
                scrollBar.borderFrame,
                scrollBar,
                scrollSettings.borderSize,
                borderTexturePath,
                borderColor,
                0, 0
            )
        end

        visualCache.width = scrollBarWidth
        visualCache.height = barTrackHeight
        visualCache.texture = scrollBarTexture
        visualCache.thumbHeight = thumbHeight
        visualCache.scrollR = scrollBarR
        visualCache.scrollG = scrollBarG
        visualCache.scrollB = scrollBarB
        visualCache.scrollA = scrollBarA
        visualCache.bgR = bgR
        visualCache.bgG = bgG
        visualCache.bgB = bgB
        visualCache.bgA = bgA
        visualCache.borderR = borderR
        visualCache.borderG = borderG
        visualCache.borderB = borderB
        visualCache.borderA = borderA
        visualCache.borderSize = scrollSettings.borderSize
        visualCache.borderTexture = borderTexturePath
        visualCache.strata = desiredStrata
        visualCache.level = desiredLevel
    end

    state.updatingScrollBar = true
    scrollBar:SetMinMaxValues(0, maxOffset)
    scrollBar:SetValue(state.scrollOffset)
    state.updatingScrollBar = false
    scrollBar:GetScript("OnValueChanged")(scrollBar, state.scrollOffset)

    if wantsVisibleBar then
        scrollBar:EnableMouse(true)
        scrollBar:Show()
    else
        scrollBar:EnableMouse(false)
        scrollBar:Hide()
    end
end

local function OnTrackerMouseWheel(_, delta)
    if not state.scrollBar or not IsModuleEnabled() or not IsScrollEnabled() then
        return
    end

    local currentValue = state.scrollOffset or 0
    local minValue, maxValue = state.scrollBar:GetMinMaxValues()
    if (maxValue or 0) <= (minValue or 0) then
        return
    end

    local newValue = math.max(minValue or 0, math.min(maxValue or 0, currentValue - ((delta or 0) * K.TRACKER_SCROLL_STEP)))

    state.updatingScrollBar = true
    state.scrollBar:SetValue(newValue)
    state.updatingScrollBar = false
    state.scrollOffset = newValue
    state.scrollBar:GetScript("OnValueChanged")(state.scrollBar, newValue)
end

local function EnsureTrackerButton()
    if state.trackerButton or not ObjectiveTrackerFrame or not ObjectiveTrackerFrame.Header then
        return state.trackerButton
    end

    local button = CreateFrame("Button", addonName .. "ObjectiveTrackerTrackAllButton", ObjectiveTrackerFrame.Header)
    button:SetSize(18, 19)

    local anchor = state.filterButton or ObjectiveTrackerFrame.Header.MinimizeButton or ObjectiveTrackerFrame.Header
    button:SetPoint("RIGHT", anchor, "LEFT", -4, 0)

    local trackIcon = button:CreateTexture(nil, "ARTWORK")
    trackIcon:SetSize(18, 19)
    trackIcon:SetPoint("CENTER")
    trackIcon:SetAtlas("Waypoint-MapPin-Tracked", false)
    button.icon = trackIcon

    button:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
    button:GetHighlightTexture():SetAllPoints()

    button:SetScript("OnClick", TrackAllQuestLogQuests)
    button:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText(TRACK_ALL_BUTTON_TEXT, 1, 1, 1)
        GameTooltip:Show()
    end)
    button:HookScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    state.trackerButton = button
    return button
end

local function RefreshTrackerButton()
    local button = EnsureTrackerButton()
    if not button then
        return
    end

    local shouldShow = IsTrackerTrackAllButtonEnabled()
        and IsModuleEnabled()
        and ObjectiveTrackerFrame
        and ObjectiveTrackerFrame.Header
        and ObjectiveTrackerFrame.Header:IsShown()
    button:SetShown(shouldShow)

    button:ClearAllPoints()
    local anchor = state.filterButton or ObjectiveTrackerFrame.Header.MinimizeButton or ObjectiveTrackerFrame.Header
    button:SetPoint("RIGHT", anchor, "LEFT", -4, 0)

    RefreshTrackerHeader()
end

local function GetQuestLogFrame()
    if WorldMapFrame and WorldMapFrame.QuestLog and WorldMapFrame.QuestLog.QuestsFrame then
        return WorldMapFrame.QuestLog.QuestsFrame
    end

    if QuestMapFrame and QuestMapFrame.QuestsFrame then
        return QuestMapFrame.QuestsFrame
    end

    return nil
end

local function GetQuestLogToolbarAnchor(questLogFrame)
    if not questLogFrame then
        return nil
    end

    return questLogFrame.SearchBox
        or questLogFrame.SettingsDropdown
        or (questLogFrame.ScrollFrame and questLogFrame.ScrollFrame.SearchBox)
        or (questLogFrame.ScrollFrame and questLogFrame.ScrollFrame.SettingsDropdown)
end

local function GetQuestLogSearchBox(questLogFrame)
    if not questLogFrame then
        return nil
    end

    return questLogFrame.SearchBox or (questLogFrame.ScrollFrame and questLogFrame.ScrollFrame.SearchBox)
end

local function GetQuestLogSettingsDropdown(questLogFrame)
    if not questLogFrame then
        return nil
    end

    return questLogFrame.SettingsDropdown or (questLogFrame.ScrollFrame and questLogFrame.ScrollFrame.SettingsDropdown)
end

local function GetQuestLogButtonParent()
    return GetQuestLogFrame()
end

local function LayoutQuestLogButton(button)
    if not button then
        return
    end

    local questLogFrame = GetQuestLogFrame()
    if not questLogFrame then
        return
    end

    if button:GetParent() ~= questLogFrame then
        button:SetParent(questLogFrame)
    end

    local toolbarAnchor = GetQuestLogToolbarAnchor(questLogFrame)
    local searchBox = GetQuestLogSearchBox(questLogFrame)
    local settingsDropdown = GetQuestLogSettingsDropdown(questLogFrame)

    button:ClearAllPoints()
    if settingsDropdown then
        button:SetPoint("RIGHT", settingsDropdown, "LEFT", -6, 0)
        button:SetPoint("TOP", searchBox or settingsDropdown, "TOP", 0, 0)
    elseif searchBox then
        button:SetPoint("LEFT", searchBox, "RIGHT", 6, 0)
        button:SetPoint("TOP", searchBox, "TOP", 0, 0)
    else
        button:SetPoint("TOPRIGHT", questLogFrame, "TOPRIGHT", -30, -6)
    end

    local layerAnchor = settingsDropdown or searchBox or toolbarAnchor or questLogFrame
    if layerAnchor then
        button:SetFrameStrata(layerAnchor:GetFrameStrata())
        button:SetFrameLevel((layerAnchor:GetFrameLevel() or 0) + 2)
    end
end

local function EnsureQuestLogButton()
    local parent = GetQuestLogButtonParent()
    if state.questLogButton or not QuestMapFrame or not parent then
        return state.questLogButton
    end

    local button = CreateFrame("Button", addonName .. "QuestLogTrackAllButton", parent)
    button:SetSize(18, 19)

    local trackIcon = button:CreateTexture(nil, "ARTWORK")
    trackIcon:SetSize(18, 19)
    trackIcon:SetPoint("CENTER")
    trackIcon:SetAtlas("Waypoint-MapPin-Tracked", false)
    button.icon = trackIcon

    button:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
    button:GetHighlightTexture():SetAllPoints()

    button:SetScript("OnClick", TrackAllQuestLogQuests)
    button:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText(TRACK_ALL_BUTTON_TEXT, 1, 0.82, 0)
        GameTooltip:Show()
    end)
    button:HookScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    LayoutQuestLogButton(button)

    state.questLogButton = button

    if not state.worldMapHooksInstalled then
        state.worldMapHooksInstalled = true
        QuestMapFrame:HookScript("OnShow", function()
            if ns.RefreshObjectiveTrackerUI then
                ns.RefreshObjectiveTrackerUI("soft")
            end
        end)
        QuestMapFrame:HookScript("OnHide", function()
            if state.questLogButton then
                state.questLogButton:Hide()
            end
        end)
        if QuestMapFrame_UpdateAll then
            hooksecurefunc("QuestMapFrame_UpdateAll", function()
                if ns.RefreshObjectiveTrackerUI then
                    ns.RefreshObjectiveTrackerUI("soft")
                end
            end)
        end
    end

    return button
end

local function RefreshQuestLogButton()
    local button = EnsureQuestLogButton()
    if not button then
        return
    end

    LayoutQuestLogButton(button)

    local shouldShow = IsQuestLogTrackAllButtonEnabled()
        and IsModuleEnabled()
        and QuestMapFrame
        and QuestMapFrame:IsShown()
        and QuestMapFrame.QuestsFrame
        and QuestMapFrame.QuestsFrame.ScrollFrame
        and QuestMapFrame.QuestsFrame.ScrollFrame:IsShown()

    button:SetShown(shouldShow)
end

local function InstallBlockHooks()
    if state.blockHooksInstalled or not ObjectiveTrackerBlockMixin then
        return
    end

    state.blockHooksInstalled = true

    local function HandleTrackedBlockReset(block)
        -- With the linked-list-only sort we no longer maintain custom block
        -- spacing or grouped-anchor state that Blizzard's Reset path can
        -- invalidate. The post-sort signature in SortModuleBlocks is the
        -- sole source of truth for "is the chain in our order": a Blizzard
        -- rebuild changes the chain's first-block sequence, which produces
        -- a mismatching pre-sort signature and naturally re-triggers the
        -- sort without needing pending flags to be set from this hook.
        --
        -- We keep the per-block style-refresh flag (it marks the block's
        -- visual state as needing re-skinning after Blizzard repools it)
        -- and the right-edge-offset adjustment, both of which are
        -- independent of the layout/sort pipeline.
        if IsModuleEnabled() and block then
            block.nomtoolsNeedsStyleRefresh = true
            local module = block.parentModule
            if not (module and module.parentContainer == ObjectiveTrackerFrame) then
                state.postLayoutStyleDirty = true
            end
        end

        if block and block.AdjustRightEdgeOffset and (state.currentRightInset or 0) > 0 then
            block:AdjustRightEdgeOffset(-(state.currentRightInset or 0))
        end
    end

    state.resetHookedMixins = state.resetHookedMixins or {}
    for _, mixin in ipairs({
        ObjectiveTrackerBlockMixin,
        ObjectiveTrackerAnimBlockMixin,
        ObjectiveTrackerQuestPOIBlockMixin,
        BonusObjectiveBlockMixin,
        AutoQuestPopupBlockMixin,
    }) do
        if type(mixin) == "table"
            and type(mixin.Reset) == "function"
            and not state.resetHookedMixins[mixin]
        then
            hooksecurefunc(mixin, "Reset", HandleTrackedBlockReset)
            state.resetHookedMixins[mixin] = true
        end
    end

    -- Diagnostic: count per-module MarkDirty calls to pinpoint which Blizzard
    -- module is driving the per-frame container Update cascade. The wrapper
    -- only emits diag when enabled, so runtime cost is one boolean check.
    if not state.moduleMarkDirtyDiagInstalled and ObjectiveTrackerModuleMixin and ObjectiveTrackerModuleMixin.MarkDirty then
        state.moduleMarkDirtyDiagInstalled = true
        hooksecurefunc(ObjectiveTrackerModuleMixin, "MarkDirty", function(module)
            if not state.diag.enabled then return end
            if not module or module.parentContainer ~= ObjectiveTrackerFrame then return end
            state.DiagModule("module.markdirty", GetKeyForModule(module) or "?")
        end)
    end

    if not state.containerMarkDirtyDiagInstalled and ObjectiveTrackerContainerMixin and ObjectiveTrackerContainerMixin.MarkDirty then
        state.containerMarkDirtyDiagInstalled = true
        hooksecurefunc(ObjectiveTrackerContainerMixin, "MarkDirty", function(container)
            if not state.diag.enabled then return end
            if container ~= ObjectiveTrackerFrame then return end
            state.DiagInc("container.markdirty.calls")
        end)
    end

    if not state.moduleLayoutHooksInstalled and ObjectiveTrackerModuleMixin and ObjectiveTrackerModuleMixin.EndLayout then
        state.moduleLayoutHooksInstalled = true
        hooksecurefunc(ObjectiveTrackerModuleMixin, "EndLayout", function(module)
            if not module or module.parentContainer ~= ObjectiveTrackerFrame or not IsModuleEnabled() then
                return
            end
            -- Pending-sort flag is no longer set from here. The
            -- post-sort-signature check inside SortModuleBlocks handles
            -- Blizzard chain rebuilds automatically: when the chain differs
            -- from our last applied order, pre-sort signature mismatches
            -- post-sort and the sort re-runs. Setting a pending flag here
            -- forced an additional unconditional sort per LayoutContents
            -- pass (60+ Hz in Delves), which was a major driver of the
            -- layout oscillation.
            state.DiagModule("module.endlayout", GetKeyForModule(module))
        end)
    end

    if not state.moduleSlideHooksInstalled and ObjectiveTrackerModuleMixin and ObjectiveTrackerModuleMixin.AdjustSlideAnchor then
        state.moduleSlideHooksInstalled = true
        hooksecurefunc(ObjectiveTrackerModuleMixin, "AdjustSlideAnchor", function(module)
            if not module or module.parentContainer ~= ObjectiveTrackerFrame or not IsModuleEnabled() then
                return
            end

            local filterSettings = GetSettings().filter or {}
            if filterSettings.groupByZone ~= true then
                return
            end

            local moduleKey = GetKeyForModule(module)
            local sortableModule = moduleKey == "quest"
                or moduleKey == "campaign"
                or moduleKey == "zone"
                or moduleKey == "focusedQuest"
                or moduleKey == "bonusObjective"
                or moduleKey == "worldQuest"
            if not sortableModule or state.sortingInProgress then
                return
            end

            local heightChanged = ns.SortModuleBlocks and ns.SortModuleBlocks(module)
            if heightChanged then
                ApplyModuleAnchors()
            end
        end)
    end

    -- Blizzard's DoQuestObjectives calls block:AddProgressBar(questID) and immediately
    -- calls progressBar:SetPercent(...) on the result during edit-mode transitions
    -- triggered by AccWideUILayoutSelection + BetterCooldownManager (via secureexecuterange).
    -- When scroll mode inflates GetAvailableHeight aggressively (historically a flat 10k),
    -- Blizzard may process many quests in one pass, exhausting the progress bar pool and
    -- allocating fresh frames that can be missing SetPercent if the pool is created before
    -- full mixin initialization.
    --
    -- Root cause of the hook miss: quest blocks use ObjectiveTrackerQuestPOIBlockTemplate
    -- whose mixin is ObjectiveTrackerQuestPOIBlockMixin. That mixin is built via:
    --   ObjectiveTrackerBlockMixin
    --     -> CreateFromMixins -> ObjectiveTrackerAnimBlockMixin  (flat copy)
    --       -> CreateFromMixins -> ObjectiveTrackerQuestPOIBlockMixin  (flat copy)
    -- Each CreateFromMixins call copies AddProgressBar into a NEW table entry that is
    -- independent of ObjectiveTrackerBlockMixin.AddProgressBar. Hooking only
    -- ObjectiveTrackerBlockMixin therefore never intercepts quest block calls.
    -- We must hook every mixin table that holds its own copy of AddProgressBar.
    if not state.progressBarSetPercentPatched then
        state.progressBarSetPercentPatched = true
        local function PatchProgressBarSetPercent(block)
            local progressBar = block.lastRegion
            if progressBar and progressBar.SetPercent == nil then
                progressBar.SetPercent = function(self, percent)
                    local bar = self and self.Bar
                    if bar and bar.SetValue then
                        bar:SetValue(percent)
                    end

                    local label = bar and bar.Label
                    if label and label.SetFormattedText then
                        label:SetFormattedText(PERCENTAGE_STRING, percent)
                    end
                end
            end
        end
        for _, mixin in ipairs({
            ObjectiveTrackerBlockMixin,
            ObjectiveTrackerAnimBlockMixin,
            ObjectiveTrackerQuestPOIBlockMixin,
        }) do
            if mixin and mixin.AddProgressBar then
                hooksecurefunc(mixin, "AddProgressBar", PatchProgressBarSetPercent)
            end
        end
    end
end

local function InstallCollapseHooks()
    if state.collapseHooksInstalled then
        return
    end

    if ObjectiveTrackerModuleMixin then
        hooksecurefunc(ObjectiveTrackerModuleMixin, "SetCollapsed", function(module, collapsed)
            if not module or module.parentContainer ~= ObjectiveTrackerFrame or state.applyingCollapseStates or not state.collapsePersistenceReady then
                return
            end

            local moduleKey = GetKeyForModule(module)
            if moduleKey then
                local isCollapsed = module.IsCollapsed and module:IsCollapsed()
                if isCollapsed == nil then
                    isCollapsed = collapsed == true
                end
                SetSavedSectionCollapsed(moduleKey, isCollapsed)
                state.postLayoutStyleDirty = true
                if InCombatLockdown() then
                    QueueCombatDeferredObjectiveTrackerMutation(false, true)
                else
                    -- Layout runs on the next frame; styling before Update/LayoutContents repools
                    -- blocks applies header colors to the wrong quest frames. RunTrackerPostLayout
                    -- schedules RequestTrackerStyleRefresh once content matches the layout.
                    RequestTrackerLayoutRefresh()
                end
            end

            -- Re-apply minimize button chrome so +/- reflects the new state immediately.
            local moduleHeader = module.Header
            if moduleHeader and moduleHeader.MinimizeButton then
                if InCombatLockdown() then
                    QueueCombatDeferredObjectiveTrackerMutation(false, true)
                    return
                end
                local styles = BuildTrackerStyleData and BuildTrackerStyleData() or nil
                if styles then
                    local isNowCollapsed = module.IsCollapsed and module:IsCollapsed() == true or false
                    styleHelpers.ApplyMinimizeButtonChrome(moduleHeader.MinimizeButton, styles.buttonAppearance, isNowCollapsed)
                end
            end
        end)
        state.collapseHooksInstalled = true
    end

    if ObjectiveTrackerContainerMixin then
        hooksecurefunc(ObjectiveTrackerContainerMixin, "AddModule", function(container, module)
            if container ~= ObjectiveTrackerFrame or state.applyingCollapseStates then
                return
            end

            ApplySavedSectionCollapsedState(module)
            state.OnDelveScenarioWidgetTestModuleAdded(container, module)
        end)
        state.collapseHooksInstalled = true

        if ObjectiveTrackerContainerMixin.SetCollapsed then
            hooksecurefunc(ObjectiveTrackerContainerMixin, "SetCollapsed", function(container)
                if container ~= ObjectiveTrackerFrame then
                    return
                end

                local header = ObjectiveTrackerFrame.Header
                if header and header.MinimizeButton then
                    if InCombatLockdown() then
                        QueueCombatDeferredObjectiveTrackerMutation(false, true)
                        return
                    end
                    local styles = BuildTrackerStyleData and BuildTrackerStyleData() or nil
                    if styles then
                        local isCollapsed = ObjectiveTrackerFrame.IsCollapsed and ObjectiveTrackerFrame:IsCollapsed() == true or false
                        styleHelpers.ApplyMinimizeButtonChrome(header.MinimizeButton, styles.buttonAppearance, isCollapsed)
                    end
                end
            end)
        end
    end

end

-- Shared scratch table for style signature builders. Declared above the
-- styleHelpers `do` blocks so both part 1 (GetTrackerStyleSignature) and
-- part 2 (ApplyTrackerStyles' typography signature) can see it after the
-- 200-local split.
local styleSignatureParts = {}

do -- styleHelpers (part 1 — helpers through SyncLiveBlockHeaderText; split for 200-local limit)

function styleHelpers.SetFontAppearance(fontString, fontPath, fontSize, fontOutline)
    if not fontString or not fontString.SetFont then
        return
    end

    if fontString:SetFont(fontPath or STANDARD_TEXT_FONT, fontSize or 13, fontOutline or "") then
        return
    end

    fontString:SetFont(STANDARD_TEXT_FONT, fontSize or 13, fontOutline or "")
end

function styleHelpers.ApplyRegionColor(region, color)
    if not region or not color then
        return
    end

    if region.SetTextColor then
        region:SetTextColor(color.r, color.g, color.b, color.a)
        return
    end

    if region.SetVertexColor then
        region:SetVertexColor(color.r, color.g, color.b, color.a)
    end
end

function styleHelpers.ApplyTexturePath(textureRegion, texturePath)
    if not textureRegion or not texturePath or not textureRegion.SetTexture or textureRegion.SetTextColor then
        return
    end

    if textureRegion.GetAtlas and textureRegion:GetAtlas() then
        return
    end

    textureRegion:SetTexture(texturePath)
end

function styleHelpers.ApplyEdgeTexture(edges, texturePath)
    if not edges then
        return
    end

    for _, edge in ipairs(edges) do
        styleHelpers.ApplyTexturePath(edge, texturePath)
    end
end

local BORDER_TEXTURE_FALLBACK_PATH = "Interface\\Buttons\\WHITE8x8"

local function CopyBackdropInsets(insets, fallback)
    fallback = Clamp(tonumber(fallback) or 0, 0, 128)
    if type(insets) ~= "table" then
        return {
            left = fallback,
            right = fallback,
            top = fallback,
            bottom = fallback,
        }
    end

    return {
        left = Clamp(tonumber(insets.left) or fallback, 0, 128),
        right = Clamp(tonumber(insets.right) or fallback, 0, 128),
        top = Clamp(tonumber(insets.top) or fallback, 0, 128),
        bottom = Clamp(tonumber(insets.bottom) or fallback, 0, 128),
    }
end

local function ResolveBackdropBorderStyle(texturePath, thickness)
    local borderDefinition = ns.GetBorderTextureDefinition and ns.GetBorderTextureDefinition(texturePath) or nil
    local baseEdgeSize = Clamp(tonumber(borderDefinition and borderDefinition.edgeSize) or 1, 1, 128)
    local scaleStep = Clamp(tonumber(borderDefinition and borderDefinition.scaleStep) or 1, 0, 32)
    local renderedEdgeSize = baseEdgeSize
    if thickness > 0 and (not borderDefinition or borderDefinition.supportsVariableThickness ~= false) then
        renderedEdgeSize = renderedEdgeSize + (math.max(thickness - 1, 0) * scaleStep)
    end

    renderedEdgeSize = Clamp(renderedEdgeSize, 1, 128)
    return {
        edgeFile = (borderDefinition and borderDefinition.path) or texturePath or BORDER_TEXTURE_FALLBACK_PATH,
        tile = borderDefinition and borderDefinition.tile ~= false or true,
        tileSize = Clamp(tonumber(borderDefinition and borderDefinition.tileSize) or 8, 1, 128),
        baseEdgeSize = baseEdgeSize,
        edgeSize = renderedEdgeSize,
        preserveColor = borderDefinition and borderDefinition.preserveColor == true or false,
        insets = CopyBackdropInsets(borderDefinition and borderDefinition.insets, math.max(1, math.floor(renderedEdgeSize / 4))),
    }
end

function styleHelpers.ApplyBackdropBorderLayout(borderFrame, target, borderSize, edgeSize, baseEdgeSize, leftOffset, rightOffset)
    if not borderFrame or not target then
        return 0, 0, 0
    end

    local signedSize = NormalizeBorderSize(borderSize, K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE)
    local thickness = math.abs(signedSize)
    local renderedEdgeSize = Clamp(tonumber(edgeSize) or thickness, 0, 128)
    local nativeEdgeSize = Clamp(tonumber(baseEdgeSize) or renderedEdgeSize, 0, 128)
    if thickness == 0 or renderedEdgeSize == 0 then
        return signedSize, thickness, 0
    end

    local left = tonumber(leftOffset) or 0
    local right = tonumber(rightOffset) or 0
    local layoutPadding = math.max(renderedEdgeSize - nativeEdgeSize, 0)

    borderFrame:ClearAllPoints()
    if signedSize > 0 then
        borderFrame:SetPoint("TOPLEFT", target, "TOPLEFT", left - layoutPadding, layoutPadding)
        borderFrame:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", right + layoutPadding, -layoutPadding)
    elseif signedSize < 0 then
        borderFrame:SetPoint("TOPLEFT", target, "TOPLEFT", left + layoutPadding, -layoutPadding)
        borderFrame:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", right - layoutPadding, layoutPadding)
    else
        borderFrame:SetPoint("TOPLEFT", target, "TOPLEFT", left, 0)
        borderFrame:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", right, 0)
    end

    return signedSize, thickness, renderedEdgeSize
end

function styleHelpers.ApplyBackdropBorder(borderFrame, target, borderSize, texturePath, color, leftOffset, rightOffset)
    if not borderFrame or not target then
        return 0, 0
    end

    local signedSize = NormalizeBorderSize(borderSize, K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE)
    local thickness = math.abs(signedSize)
    if thickness == 0 then
        borderFrame:Hide()
        return signedSize, thickness
    end

    local borderStyle = ResolveBackdropBorderStyle(texturePath, thickness)
    local _, _, renderedEdgeSize = styleHelpers.ApplyBackdropBorderLayout(
        borderFrame,
        target,
        signedSize,
        borderStyle.edgeSize,
        borderStyle.baseEdgeSize,
        leftOffset,
        rightOffset
    )
    if renderedEdgeSize == 0 then
        borderFrame:Hide()
        return signedSize, 0
    end

    local previousBackdropInfo = borderFrame.nomtoolsBackdropInfo
    local backdropInfo = {
        tile = false,
        insets = {},
    }
    backdropInfo.bgFile = nil
    backdropInfo.edgeFile = borderStyle.edgeFile
    backdropInfo.tile = borderStyle.tile
    backdropInfo.tileSize = borderStyle.tileSize
    backdropInfo.edgeSize = renderedEdgeSize
    backdropInfo.insets.left = borderStyle.insets.left
    backdropInfo.insets.right = borderStyle.insets.right
    backdropInfo.insets.top = borderStyle.insets.top
    backdropInfo.insets.bottom = borderStyle.insets.bottom
    borderFrame.nomtoolsBackdropInfo = backdropInfo

    if previousBackdropInfo and previousBackdropInfo.edgeFile ~= backdropInfo.edgeFile then
        borderFrame:SetBackdrop(nil)
    end

    borderFrame:SetBackdrop(backdropInfo)
    borderFrame:SetBackdropBorderColor(color.r, color.g, color.b, color.a)
    borderFrame:Show()
    return signedSize, renderedEdgeSize
end

function styleHelpers.ApplyBorderLayout(container, target, borderSize, leftOffset, rightOffset)
    if not container or not target then
        return 0, 0
    end

    local signedSize = NormalizeBorderSize(borderSize, K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE)
    local thickness = math.abs(signedSize)
    if thickness == 0 then
        return signedSize, thickness
    end

    local left = tonumber(leftOffset) or 0
    local right = tonumber(rightOffset) or 0

    if signedSize > 0 then
        container.top:ClearAllPoints()
        container.top:SetPoint("TOPLEFT", target, "TOPLEFT", left - thickness, thickness)
        container.top:SetPoint("TOPRIGHT", target, "TOPRIGHT", right + thickness, thickness)
        container.top:SetHeight(thickness)

        container.bottom:ClearAllPoints()
        container.bottom:SetPoint("BOTTOMLEFT", target, "BOTTOMLEFT", left - thickness, -thickness)
        container.bottom:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", right + thickness, -thickness)
        container.bottom:SetHeight(thickness)

        container.left:ClearAllPoints()
        container.left:SetPoint("TOPLEFT", target, "TOPLEFT", left - thickness, thickness)
        container.left:SetPoint("BOTTOMLEFT", target, "BOTTOMLEFT", left - thickness, -thickness)
        container.left:SetWidth(thickness)

        container.right:ClearAllPoints()
        container.right:SetPoint("TOPRIGHT", target, "TOPRIGHT", right + thickness, thickness)
        container.right:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", right + thickness, -thickness)
        container.right:SetWidth(thickness)
    else
        container.top:ClearAllPoints()
        container.top:SetPoint("TOPLEFT", target, "TOPLEFT", left, 0)
        container.top:SetPoint("TOPRIGHT", target, "TOPRIGHT", right, 0)
        container.top:SetHeight(thickness)

        container.bottom:ClearAllPoints()
        container.bottom:SetPoint("BOTTOMLEFT", target, "BOTTOMLEFT", left, 0)
        container.bottom:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", right, 0)
        container.bottom:SetHeight(thickness)

        container.left:ClearAllPoints()
        container.left:SetPoint("TOPLEFT", target, "TOPLEFT", left, 0)
        container.left:SetPoint("BOTTOMLEFT", target, "BOTTOMLEFT", left, 0)
        container.left:SetWidth(thickness)

        container.right:ClearAllPoints()
        container.right:SetPoint("TOPRIGHT", target, "TOPRIGHT", right, 0)
        container.right:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", right, 0)
        container.right:SetWidth(thickness)
    end

    return signedSize, thickness
end

function styleHelpers.GetStatusBarAnonymousRegions(statusBar, excludedRegion, drawLayer, subLevel)
    local regions = {}
    if not statusBar or not statusBar.GetRegions then
        return regions
    end

    for _, region in ipairs({ statusBar:GetRegions() }) do
        if region
            and region ~= excludedRegion
            and region.GetObjectType
            and region:GetObjectType() == "Texture"
        then
            local regionDrawLayer, regionSubLevel = region:GetDrawLayer()
            if (not drawLayer or regionDrawLayer == drawLayer)
                and (subLevel == nil or regionSubLevel == subLevel)
            then
                regions[#regions + 1] = region
            end
        end
    end

    return regions
end

function styleHelpers.BuildTrackerColorStyle(color, highlightColor, isComplete)
    local baseColor = NormalizeColor(color, K.DEFAULT_UNCOMPLETED_COLOR)
    local hoverColor = NormalizeColor(highlightColor, baseColor)
    local style = {
        r = baseColor.r,
        g = baseColor.g,
        b = baseColor.b,
        nomtoolsIsComplete = isComplete == true,
    }

    if hoverColor.r == style.r and hoverColor.g == style.g and hoverColor.b == style.b then
        style.reverse = style
        return style
    end

    local reverseStyle = {
        r = hoverColor.r,
        g = hoverColor.g,
        b = hoverColor.b,
        nomtoolsIsComplete = isComplete == true,
    }
    style.reverse = reverseStyle
    reverseStyle.reverse = style
    return style
end

function styleHelpers.ApplyFontStringColorStyle(fontString, colorStyle, useHighlight)
    if not fontString or not colorStyle then
        return
    end

    local appliedColorStyle = colorStyle
    if useHighlight and colorStyle.reverse then
        appliedColorStyle = colorStyle.reverse
    end

    fontString.nomtoolsColorStyle = colorStyle
    fontString:SetTextColor(appliedColorStyle.r, appliedColorStyle.g, appliedColorStyle.b)
end

local function AppendColorSignature(parts, color, fallback)
    fallback = fallback or K.DEFAULT_UNCOMPLETED_COLOR
    local r, g, b, a
    if type(color) == "table" then
        r = tonumber(color.r or color[1]) or fallback.r
        g = tonumber(color.g or color[2]) or fallback.g
        b = tonumber(color.b or color[3]) or fallback.b
        a = tonumber(color.a or color[4]) or fallback.a
    else
        r, g, b, a = fallback.r, fallback.g, fallback.b, fallback.a
    end
    parts[#parts + 1] = string.format("%.3f,%.3f,%.3f,%.3f", r, g, b, a)
end

local function GetTrackerStyleSignature()
    local typography = GetTypographySettings()
    local progressBar = GetProgressBarSettings()
    local appearance = GetHeaderAppearanceSettings()
    local titleColors = typography.titleColors or {}
    local parts = styleSignatureParts
    for k in pairs(parts) do parts[k] = nil end

    parts[1] = tostring(ns.GetFontPath and ns.GetFontPath(typography.font) or STANDARD_TEXT_FONT)
    parts[2] = tostring(Clamp(tonumber(typography.fontSize) or 13, 8, 24))
    parts[3] = tostring(ns.GetFontOutlineFlags and ns.GetFontOutlineFlags(typography.fontOutline) or "OUTLINE")
    parts[4] = tostring(GetLevelPrefixMode(typography))
    parts[5] = tostring(IsWarbandIndicatorEnabled(typography) and true or false)
    parts[6] = tostring(appearance.preset == "nomtools" and true or false)
    parts[7] = tostring(ns.GetStatusBarTexturePath and ns.GetStatusBarTexturePath((appearance.mainHeader or {}).texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)
    parts[8] = tostring(Clamp(tonumber((appearance.mainHeader or {}).opacity) or 80, 0, 100))
    parts[9] = tostring(ns.GetStatusBarTexturePath and ns.GetStatusBarTexturePath((appearance.categoryHeader or {}).texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)
    parts[10] = tostring(Clamp(tonumber((appearance.categoryHeader or {}).opacity) or 80, 0, 100))
    parts[11] = tostring(ns.GetStatusBarTexturePath and ns.GetStatusBarTexturePath((appearance.button or {}).texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)
    parts[12] = tostring(Clamp(tonumber((appearance.button or {}).opacity) or 80, 0, 100))
    -- Main header typography overrides
    parts[13] = tostring((typography.mainHeader or {}).font or "")
    parts[14] = tostring((typography.mainHeader or {}).fontSize or "")
    parts[15] = tostring((typography.mainHeader or {}).fontOutline or "")
    parts[16] = tostring((typography.mainHeader or {}).overrideTypography and true or false)
    parts[17] = tostring(Clamp(tonumber((typography.mainHeader or {}).xOffset) or 0, -200, 200))
    parts[18] = tostring(Clamp(tonumber((typography.mainHeader or {}).yOffset) or 0, -200, 200))
    -- Category header typography overrides
    parts[19] = tostring((typography.categoryHeader or {}).font or "")
    parts[20] = tostring((typography.categoryHeader or {}).fontSize or "")
    parts[21] = tostring((typography.categoryHeader or {}).fontOutline or "")
    parts[22] = tostring((typography.categoryHeader or {}).overrideTypography and true or false)
    parts[23] = tostring(Clamp(tonumber((typography.categoryHeader or {}).xOffset) or 0, -200, 200))
    parts[24] = tostring(Clamp(tonumber((typography.categoryHeader or {}).yOffset) or 0, -200, 200))
    parts[25] = tostring(progressBar.fillMode == "static" and "static" or "progress")
    parts[26] = tostring(ns.GetStatusBarTexturePath and ns.GetStatusBarTexturePath(progressBar.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)
    parts[27] = tostring(NormalizeBorderSize(progressBar.borderSize, K.DEFAULT_PROGRESS_BORDER_SIZE, K.MIN_PROGRESS_BORDER_SIZE, K.MAX_PROGRESS_BORDER_SIZE))
    parts[28] = tostring(ns.GetBorderTexturePath and ns.GetBorderTexturePath(progressBar.borderTexture or progressBar.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)
    parts[29] = tostring(progressBar.hideRewardIcon == true)
    parts[30] = tostring(NormalizeBorderSize((appearance.mainHeader or {}).borderSize, K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE))
    parts[31] = tostring(NormalizeBorderSize((appearance.categoryHeader or {}).borderSize, K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE))
    parts[32] = tostring(NormalizeBorderSize((appearance.button or {}).borderSize, K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE))
    parts[33] = tostring(NormalizeBorderSize((appearance.trackerBackground or {}).borderSize, K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE))
    parts[34] = tostring(ns.GetBorderTexturePath and ns.GetBorderTexturePath((appearance.mainHeader or {}).borderTexture or (appearance.mainHeader or {}).texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)
    parts[35] = tostring(ns.GetBorderTexturePath and ns.GetBorderTexturePath((appearance.categoryHeader or {}).borderTexture or (appearance.categoryHeader or {}).texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)
    parts[36] = tostring(ns.GetBorderTexturePath and ns.GetBorderTexturePath((appearance.button or {}).borderTexture or (appearance.button or {}).texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)
    parts[37] = tostring(ns.GetBorderTexturePath and ns.GetBorderTexturePath((appearance.trackerBackground or {}).borderTexture or (appearance.trackerBackground or {}).texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)

    AppendColorSignature(parts, typography.uncompletedColor, K.DEFAULT_UNCOMPLETED_COLOR)
    AppendColorSignature(parts, typography.completedColor, K.DEFAULT_COMPLETED_COLOR)
    AppendColorSignature(parts, titleColors.quest, K.DEFAULT_SPECIAL_TITLE_COLORS.quest)
    AppendColorSignature(parts, titleColors.worldQuest, K.DEFAULT_SPECIAL_TITLE_COLORS.worldQuest)
    AppendColorSignature(parts, titleColors.bonusObjective, K.DEFAULT_SPECIAL_TITLE_COLORS.bonusObjective)
    AppendColorSignature(parts, titleColors.daily, K.DEFAULT_SPECIAL_TITLE_COLORS.daily)
    AppendColorSignature(parts, titleColors.weekly, K.DEFAULT_SPECIAL_TITLE_COLORS.weekly)
    AppendColorSignature(parts, titleColors.meta, K.DEFAULT_SPECIAL_TITLE_COLORS.meta)
    AppendColorSignature(parts, titleColors.important, K.DEFAULT_SPECIAL_TITLE_COLORS.important)
    AppendColorSignature(parts, titleColors.prey, K.DEFAULT_SPECIAL_TITLE_COLORS.prey)
    AppendColorSignature(parts, titleColors.campaign, K.DEFAULT_SPECIAL_TITLE_COLORS.campaign)
    AppendColorSignature(parts, titleColors.legendary, K.DEFAULT_SPECIAL_TITLE_COLORS.legendary)
    parts[#parts + 1] = tostring(titleColors.useTrivialColor ~= false)
    AppendColorSignature(parts, titleColors.trivial, K.DEFAULT_SPECIAL_TITLE_COLORS.trivial)
    AppendColorSignature(parts, progressBar.fillColor, K.DEFAULT_PROGRESS_FILL_COLOR)
    AppendColorSignature(parts, progressBar.lowFillColor, K.DEFAULT_PROGRESS_LOW_FILL_COLOR)
    AppendColorSignature(parts, progressBar.mediumFillColor, K.DEFAULT_PROGRESS_MEDIUM_FILL_COLOR)
    AppendColorSignature(parts, progressBar.highFillColor, K.DEFAULT_PROGRESS_HIGH_FILL_COLOR)
    AppendColorSignature(parts, progressBar.backgroundColor, K.DEFAULT_PROGRESS_BACKGROUND_COLOR)
    AppendColorSignature(parts, progressBar.borderColor, K.DEFAULT_PROGRESS_BORDER_COLOR)
    AppendColorSignature(parts, (appearance.mainHeader or {}).color, K.DEFAULT_HEADER_BACKGROUND_COLOR)
    AppendColorSignature(parts, (appearance.mainHeader or {}).borderColor, K.DEFAULT_HEADER_BORDER_COLOR)
    AppendColorSignature(parts, (appearance.categoryHeader or {}).color, K.DEFAULT_HEADER_BACKGROUND_COLOR)
    AppendColorSignature(parts, (appearance.categoryHeader or {}).borderColor, K.DEFAULT_HEADER_BORDER_COLOR)
    AppendColorSignature(parts, (appearance.button or {}).color, K.DEFAULT_HEADER_BACKGROUND_COLOR)
    AppendColorSignature(parts, (appearance.button or {}).borderColor, K.DEFAULT_HEADER_BORDER_COLOR)
    AppendColorSignature(parts, (typography.mainHeader or {}).textColor, K.DEFAULT_HEADER_BACKGROUND_COLOR)
    AppendColorSignature(parts, (typography.categoryHeader or {}).textColor, K.DEFAULT_HEADER_BACKGROUND_COLOR)
    local tbg = appearance.trackerBackground or {}
    parts[#parts + 1] = tostring(tbg.enabled and true or false)
    parts[#parts + 1] = tostring(ns.GetStatusBarTexturePath and ns.GetStatusBarTexturePath(tbg.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)
    parts[#parts + 1] = tostring(Clamp(tonumber(tbg.opacity) or 60, 0, 100))
    AppendColorSignature(parts, tbg.color, K.DEFAULT_HEADER_BACKGROUND_COLOR)
    AppendColorSignature(parts, tbg.borderColor, K.DEFAULT_HEADER_BORDER_COLOR)
    parts[#parts + 1] = tostring(ns.GetBorderTexturePath and ns.GetBorderTexturePath(tbg.borderTexture or tbg.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH)

    local settings = GetSettings()
    local filterSettings = settings.filter or {}
    parts[#parts + 1] = tostring(filterSettings.sortBy or "default")
    parts[#parts + 1] = tostring(filterSettings.sortDirection or "asc")
    parts[#parts + 1] = tostring(filterSettings.groupByZone or false)
    parts[#parts + 1] = tostring(settings.search and settings.search.enabled ~= false)
    parts[#parts + 1] = tostring(settings.buttons and settings.buttons.filterButton ~= false)

    return table.concat(parts, "|")
end

BuildTrackerStyleData = function()
    local styleSignature = GetTrackerStyleSignature()
    if state.cachedTrackerStyleSignature == styleSignature and state.cachedTrackerStyles then
        return state.cachedTrackerStyles
    end

    local typography = GetTypographySettings()
    local progressBar = GetProgressBarSettings()
    local appearance = GetHeaderAppearanceSettings()
    local fontPath = ns.GetFontPath and ns.GetFontPath(typography.font) or STANDARD_TEXT_FONT
    local fontSize = Clamp(tonumber(typography.fontSize) or 13, 8, 24)
    local fontOutline = ns.GetFontOutlineFlags and ns.GetFontOutlineFlags(typography.fontOutline) or "OUTLINE"
    local uncompletedColor = NormalizeColor(typography.uncompletedColor, K.DEFAULT_UNCOMPLETED_COLOR)
    local completedColor = NormalizeColor(typography.completedColor, K.DEFAULT_COMPLETED_COLOR)
    local titleColors = typography.titleColors or {}
    local headerUseNomTools = appearance.preset == "nomtools"
    local progressUseNomTools = appearance.preset == "nomtools"
    local progressFillMode = progressBar.fillMode == "static" and "static" or "progress"

    -- Build one appearance block per visual section from its sub-table.
    local function BuildSectionAppearance(section)
        local s = type(section) == "table" and section or {}
        local opacity = Clamp(tonumber(s.opacity) or 80, 0, 100) / 100
        local bgColor = NormalizeColor(s.color, K.DEFAULT_HEADER_BACKGROUND_COLOR)
        bgColor.a = opacity
        return {
            useNomTools = headerUseNomTools,
            texture = ns.GetStatusBarTexturePath and ns.GetStatusBarTexturePath(s.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH,
            backgroundColor = bgColor,
            borderColor = NormalizeColor(s.borderColor, K.DEFAULT_HEADER_BORDER_COLOR),
            borderTexture = ns.GetBorderTexturePath and ns.GetBorderTexturePath(s.borderTexture or s.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH,
            borderSize = NormalizeBorderSize(s.borderSize, K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE),
        }
    end

    -- Resolve header-specific font overrides, falling back to the global typography.
    local function ResolveHeaderFont(override, base)
        local o = type(override) == "table" and override or {}
        local useOverride = o.overrideTypography == true
        return {
            fontPath = (useOverride and o.font and ns.GetFontPath and ns.GetFontPath(o.font)) or base.fontPath,
            fontSize = (useOverride and o.fontSize and Clamp(tonumber(o.fontSize), 8, 24)) or base.fontSize,
            fontOutline = (useOverride and o.fontOutline and ns.GetFontOutlineFlags and ns.GetFontOutlineFlags(o.fontOutline)) or base.fontOutline,
            textColor = type(o.textColor) == "table" and NormalizeColor(o.textColor) or nil,
        }
    end

    local baseFont = { fontPath = fontPath, fontSize = fontSize, fontOutline = fontOutline }
    local defaultHeaderColor = NormalizeColor(OBJECTIVE_TRACKER_COLOR and OBJECTIVE_TRACKER_COLOR.Header or nil, {
        r = 1,
        g = 0.82,
        b = 0,
        a = 1,
    })

    local styles = {
        fontPath = fontPath or STANDARD_TEXT_FONT,
        fontSize = fontSize,
        fontOutline = fontOutline,
        levelPrefixMode = GetLevelPrefixMode(typography),
        showWarbandCompletedIndicator = IsWarbandIndicatorEnabled(typography),
        uncompletedColor = uncompletedColor,
        completedColor = completedColor,
        questTitleColor = NormalizeColor(titleColors.quest, K.DEFAULT_SPECIAL_TITLE_COLORS.quest),
        worldQuestTitleColor = NormalizeColor(titleColors.worldQuest, K.DEFAULT_SPECIAL_TITLE_COLORS.worldQuest),
        bonusObjectiveTitleColor = NormalizeColor(titleColors.bonusObjective, K.DEFAULT_SPECIAL_TITLE_COLORS.bonusObjective),
        dailyTitleColor = NormalizeColor(titleColors.daily, K.DEFAULT_SPECIAL_TITLE_COLORS.daily),
        weeklyTitleColor = NormalizeColor(titleColors.weekly, K.DEFAULT_SPECIAL_TITLE_COLORS.weekly),
        metaTitleColor = NormalizeColor(titleColors.meta, K.DEFAULT_SPECIAL_TITLE_COLORS.meta),
        importantTitleColor = NormalizeColor(titleColors.important, K.DEFAULT_SPECIAL_TITLE_COLORS.important),
        preyTitleColor = NormalizeColor(titleColors.prey, K.DEFAULT_SPECIAL_TITLE_COLORS.prey),
        campaignTitleColor = NormalizeColor(titleColors.campaign, K.DEFAULT_SPECIAL_TITLE_COLORS.campaign),
        legendaryTitleColor = NormalizeColor(titleColors.legendary, K.DEFAULT_SPECIAL_TITLE_COLORS.legendary),
        trivialTitleColor = NormalizeColor(titleColors.trivial, K.DEFAULT_SPECIAL_TITLE_COLORS.trivial),
        useTrivialTitleColor = titleColors.useTrivialColor ~= false,
        uncompletedStyle = styleHelpers.BuildTrackerColorStyle(uncompletedColor, uncompletedColor, false),
        completedStyle = styleHelpers.BuildTrackerColorStyle(completedColor, completedColor, true),
        progressUseNomTools = progressUseNomTools,
        progressTexture = ns.GetStatusBarTexturePath and ns.GetStatusBarTexturePath(progressBar.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH,
        progressFillMode = progressFillMode,
        progressFillColor = NormalizeColor(progressBar.fillColor, K.DEFAULT_PROGRESS_FILL_COLOR),
        progressLowFillColor = NormalizeColor(progressBar.lowFillColor, K.DEFAULT_PROGRESS_LOW_FILL_COLOR),
        progressMediumFillColor = NormalizeColor(progressBar.mediumFillColor, K.DEFAULT_PROGRESS_MEDIUM_FILL_COLOR),
        progressHighFillColor = NormalizeColor(progressBar.highFillColor, K.DEFAULT_PROGRESS_HIGH_FILL_COLOR),
        progressBackgroundColor = NormalizeColor(progressBar.backgroundColor, K.DEFAULT_PROGRESS_BACKGROUND_COLOR),
        progressBorderTexture = ns.GetBorderTexturePath and ns.GetBorderTexturePath(progressBar.borderTexture or progressBar.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH,
        progressBorderSize = NormalizeBorderSize(progressBar.borderSize, K.DEFAULT_PROGRESS_BORDER_SIZE, K.MIN_PROGRESS_BORDER_SIZE, K.MAX_PROGRESS_BORDER_SIZE),
        progressBorderColor = NormalizeColor(progressBar.borderColor, K.DEFAULT_PROGRESS_BORDER_COLOR),
        progressHideRewardIcon = progressBar.hideRewardIcon == true,
        -- Per-section header chrome appearances
        mainHeaderAppearance = BuildSectionAppearance(appearance.mainHeader),
        categoryHeaderAppearance = BuildSectionAppearance(appearance.categoryHeader),
        buttonAppearance = BuildSectionAppearance(appearance.button),
        -- Per-section header text fonts (resolved from overrides or global typography)
        mainHeaderFont = ResolveHeaderFont(typography.mainHeader, baseFont),
        categoryHeaderFont = ResolveHeaderFont(typography.categoryHeader, baseFont),
        mainHeaderXOffset = Clamp(tonumber((typography.mainHeader or {}).xOffset) or 0, -200, 200),
        mainHeaderYOffset = Clamp(tonumber((typography.mainHeader or {}).yOffset) or 0, -200, 200),
        categoryHeaderXOffset = Clamp(tonumber((typography.categoryHeader or {}).xOffset) or 0, -200, 200),
        categoryHeaderYOffset = Clamp(tonumber((typography.categoryHeader or {}).yOffset) or 0, -200, 200),
        trackerBackground = (function()
            local tbg = appearance.trackerBackground or {}
            if tbg.enabled ~= true then return { enabled = false } end
            local opacity = Clamp(tonumber(tbg.opacity) or 60, 0, 100) / 100
            local bgColor = NormalizeColor(tbg.color, K.DEFAULT_HEADER_BACKGROUND_COLOR)
            bgColor.a = opacity
            return {
                enabled = true,
                texture = ns.GetStatusBarTexturePath and ns.GetStatusBarTexturePath(tbg.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH,
                backgroundColor = bgColor,
                borderColor = NormalizeColor(tbg.borderColor, K.DEFAULT_HEADER_BORDER_COLOR),
                borderTexture = ns.GetBorderTexturePath and ns.GetBorderTexturePath(tbg.borderTexture or tbg.texture) or DEFAULT_STATUSBAR_TEXTURE_PATH,
                borderSize = NormalizeBorderSize(tbg.borderSize, K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE),
            }
        end)(),
    }

    styles.defaultHeaderStyle = styleHelpers.BuildTrackerColorStyle(defaultHeaderColor, GetHoverColor(defaultHeaderColor))
    styles.questHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.questTitleColor, GetHoverColor(styles.questTitleColor))
    styles.worldQuestHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.worldQuestTitleColor, GetHoverColor(styles.worldQuestTitleColor))
    styles.bonusObjectiveHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.bonusObjectiveTitleColor, GetHoverColor(styles.bonusObjectiveTitleColor))
    styles.dailyHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.dailyTitleColor, GetHoverColor(styles.dailyTitleColor))
    styles.weeklyHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.weeklyTitleColor, GetHoverColor(styles.weeklyTitleColor))
    styles.metaHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.metaTitleColor, GetHoverColor(styles.metaTitleColor))
    styles.importantHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.importantTitleColor, GetHoverColor(styles.importantTitleColor))
    styles.preyHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.preyTitleColor, GetHoverColor(styles.preyTitleColor))
    styles.campaignHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.campaignTitleColor, GetHoverColor(styles.campaignTitleColor))
    styles.legendaryHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.legendaryTitleColor, GetHoverColor(styles.legendaryTitleColor))
    styles.trivialHeaderStyle = styleHelpers.BuildTrackerColorStyle(styles.trivialTitleColor, GetHoverColor(styles.trivialTitleColor))

    state.cachedTrackerStyleSignature = styleSignature
    state.cachedTrackerStyles = styles
    return styles
end

function styleHelpers.SetRegionShown(region, shown)
    if not region then
        return
    end

    if region.SetShown then
        region:SetShown(shown)
    elseif shown then
        region:Show()
    else
        region:Hide()
    end
end

function styleHelpers.ApplyModuleHeaderLayout(frame)
    -- Blizzard initialises module-header frames with an explicit SetWidth equal
    -- to the tracker size at the time of creation.  When the user adjusts the
    -- tracker width those headers are never updated, so their chrome textures
    -- (anchored to frame.BOTTOMRIGHT) stay at the original size.  Force the
    -- header to span its parent module so chrome extents stay correct.
    --
    -- Must use ResolvePublicNumber on :GetWidth() before SetWidth. Raw widths
    -- can be secret/tainted (managed anchors, prior taint). Feeding them into
    -- header frames under modules that share EmbeddedItemTooltip / map POI
    -- tooltip layout propagates taint and triggers "secret number" compare
    -- failures in GameTooltip_InsertFrame (SharedTooltipTemplates).
    -- Same modules as PrepareModuleForLayout: let Blizzard size them.
    local ownerModule = frame.GetParent and frame:GetParent() or nil
    if not ownerModule then return end
    if UIWidgetObjectiveTracker and ownerModule == UIWidgetObjectiveTracker then
        return
    end
    if WorldQuestObjectiveTracker and ownerModule == WorldQuestObjectiveTracker then
        return
    end
    local rawWidth = ownerModule.GetWidth and ownerModule:GetWidth() or nil
    local moduleWidth = ResolvePublicNumber(rawWidth, 0) or 0
    if moduleWidth > 0 then
        frame:SetWidth(moduleWidth)
    end
end

function styleHelpers.GetHeaderVisualExtensions(frame)
    if not frame or not ObjectiveTrackerFrame then
        return 0, 0
    end

    if frame == ObjectiveTrackerFrame.Header then
        return K.TRACKER_VISUAL_LEFT_EXTENSION, 0
    end

    local ownerModule = frame.GetParent and frame:GetParent() or nil
    if ownerModule and ownerModule.parentContainer == ObjectiveTrackerFrame then
        return K.TRACKER_VISUAL_LEFT_EXTENSION, TRACKER_MODULE_HEADER_RIGHT_EXTENSION
    end

    return 0, 0
end

function styleHelpers.ApplyHeaderVisualExtents(frame, region)
    if not frame or not region then
        return
    end

    local leftExtension, rightExtension = styleHelpers.GetHeaderVisualExtensions(frame)
    region:ClearAllPoints()
    region:SetPoint("TOPLEFT", frame, "TOPLEFT", -leftExtension, 0)
    region:SetPoint("TOPRIGHT", frame, "TOPRIGHT", rightExtension, 0)
    local visualHeight = styleHelpers.GetHeaderVisualHeight(frame)
    if visualHeight > 0 then
        region:SetHeight(visualHeight)
    end
end

function styleHelpers.ApplyHeaderButtonInset(frame)
    if not frame or not frame.MinimizeButton then
        return
    end

    local button = frame.MinimizeButton
    if not button.nomtoolsOriginalAnchor then
        local point, relativeTo, relativePoint, xOfs, yOfs = button:GetPoint(1)
        if not point then
            return
        end

        button.nomtoolsOriginalAnchor = {
            point = point,
            relativeTo = relativeTo,
            relativePoint = relativePoint,
            xOfs = xOfs or 0,
            yOfs = yOfs or 0,
        }
    end

    local anchor = button.nomtoolsOriginalAnchor
    button:ClearAllPoints()
    button:SetPoint(
        anchor.point,
        anchor.relativeTo,
        anchor.relativePoint,
        (anchor.xOfs or 0) - K.TRACKER_HEADER_BUTTON_RIGHT_INSET,
        anchor.yOfs or 0
    )
end

function styleHelpers.ApplyHeaderChromeExtents(frame, chrome)
    if not frame or not chrome then
        return
    end

    local leftExtension, rightExtension = styleHelpers.GetHeaderVisualExtensions(frame)

    chrome.background:ClearAllPoints()
    chrome.background:SetPoint("TOPLEFT", frame, "TOPLEFT", -leftExtension, 0)
    chrome.background:SetPoint("TOPRIGHT", frame, "TOPRIGHT", rightExtension, 0)
    local visualHeight = styleHelpers.GetHeaderVisualHeight(frame)
    if visualHeight > 0 then
        chrome.background:SetHeight(visualHeight)
    end
end

function styleHelpers.GetHeaderVisualHeight(frame)
    if not frame then
        return 0
    end

    local resolvedHeight = (frame.GetHeight and frame:GetHeight()) or 0
    if frame == (ObjectiveTrackerFrame and ObjectiveTrackerFrame.Header or nil) then
        local topModulePadding = ObjectiveTrackerFrame and ObjectiveTrackerFrame.topModulePadding or 0
        if (topModulePadding or 0) > 0 then
            resolvedHeight = math.max(resolvedHeight or 0, topModulePadding or 0)
        end
    end

    if (resolvedHeight or 0) <= 0 and frame.Background and frame.Background.GetHeight then
        resolvedHeight = frame.Background:GetHeight() or 0
    end

    if (resolvedHeight or 0) <= 0 and frame.GetTop and frame.GetBottom then
        local top = frame:GetTop()
        local bottom = frame:GetBottom()
        if top and bottom then
            resolvedHeight = math.max(0, top - bottom)
        end
    end

    return resolvedHeight or 0
end

function styleHelpers.EnsureFrameChrome(frame)
    if not frame then
        return nil
    end

    if frame.NomToolsChrome then
        return frame.NomToolsChrome
    end

    local chrome = {}
    local background = frame:CreateTexture(nil, "BACKGROUND", nil, -8)
    background:SetAllPoints()
    chrome.background = background

    local borderHost = CreateFrame("Frame", nil, frame)
    borderHost:SetAllPoints(frame)
    borderHost:SetFrameStrata(frame:GetFrameStrata())
    borderHost:SetFrameLevel((frame:GetFrameLevel() or 0) + 30)
    borderHost:EnableMouse(false)
    chrome.borderHost = borderHost

    local borderFrame = CreateFrame("Frame", nil, borderHost, BackdropTemplateMixin and "BackdropTemplate" or nil)
    borderFrame:SetAllPoints(frame)
    borderFrame:EnableMouse(false)
    chrome.borderFrame = borderFrame
    chrome.edges = {}

    frame.NomToolsChrome = chrome
    return chrome
end

function styleHelpers.ApplyChromeEdgeThickness(chrome, thickness)
    if not chrome then
        return
    end

    local resolvedThickness = Clamp(tonumber(thickness) or K.DEFAULT_CHROME_BORDER_SIZE, K.MIN_CHROME_BORDER_SIZE, K.MAX_CHROME_BORDER_SIZE)
    if chrome.top then
        chrome.top:SetHeight(resolvedThickness)
    end
    if chrome.bottom then
        chrome.bottom:SetHeight(resolvedThickness)
    end
    if chrome.left then
        chrome.left:SetWidth(resolvedThickness)
    end
    if chrome.right then
        chrome.right:SetWidth(resolvedThickness)
    end
end

function styleHelpers.ApplyFrameChrome(frame, appearance)
    local chrome = styleHelpers.EnsureFrameChrome(frame)
    if not chrome then
        return
    end

    local useNomTools = appearance and appearance.useNomTools == true
    if not useNomTools then
        styleHelpers.SetRegionShown(chrome.background, false)
        if chrome.borderFrame then
            chrome.borderFrame:Hide()
        end
        return
    end

    styleHelpers.ApplyTexturePath(chrome.background, appearance.texture)
    styleHelpers.ApplyRegionColor(chrome.background, appearance.backgroundColor)
    local _, borderThickness = styleHelpers.ApplyBackdropBorder(
        chrome.borderFrame,
        frame,
        appearance.borderSize,
        appearance.borderTexture or appearance.texture,
        appearance.borderColor,
        0,
        0
    )
    chrome.background:Show()

    if borderThickness == 0 and chrome.borderFrame then
        chrome.borderFrame:Hide()
    end
end

function styleHelpers.EnsureTrackerBackground()
    if state.trackerBackgroundChrome then
        return state.trackerBackgroundChrome
    end
    if not ObjectiveTrackerFrame then
        return nil
    end

    local chrome = {}
    local bg = ObjectiveTrackerFrame:CreateTexture(nil, "BACKGROUND", nil, -8)
    bg:SetTexture("Interface\\Buttons\\WHITE8x8")
    bg:ClearAllPoints()
    bg:SetPoint("TOPLEFT", ObjectiveTrackerFrame, "TOPLEFT", -K.TRACKER_VISUAL_LEFT_EXTENSION, 0)
    bg:SetPoint("BOTTOMRIGHT", ObjectiveTrackerFrame, "BOTTOMRIGHT", 0, 0)
    chrome.background = bg

    local borderHost = CreateFrame("Frame", nil, ObjectiveTrackerFrame)
    borderHost:SetAllPoints(ObjectiveTrackerFrame)
    borderHost:SetFrameStrata(ObjectiveTrackerFrame:GetFrameStrata())
    borderHost:SetFrameLevel((ObjectiveTrackerFrame:GetFrameLevel() or 0) + 60)
    borderHost:EnableMouse(false)
    chrome.borderHost = borderHost

    local borderFrame = CreateFrame("Frame", nil, borderHost, BackdropTemplateMixin and "BackdropTemplate" or nil)
    borderFrame:SetAllPoints(ObjectiveTrackerFrame)
    borderFrame:EnableMouse(false)
    chrome.borderFrame = borderFrame
    chrome.edges = {}
    state.trackerBackgroundChrome = chrome
    return chrome
end

function styleHelpers.ApplyTrackerBackground(tbgStyle)
    local chrome = styleHelpers.EnsureTrackerBackground()
    if not chrome then
        return
    end

    if not tbgStyle or not tbgStyle.enabled then
        chrome.background:Hide()
        if chrome.borderFrame then
            chrome.borderFrame:Hide()
        end
        return
    end

    -- Compute effective height: cap at actual content height so the background
    -- does not extend into the empty space below the last quest/section.
    local frameH = (ObjectiveTrackerFrame.GetHeight and ObjectiveTrackerFrame:GetHeight()) or 0
    local contentH = state.lastContentHeight or 0
    local effectiveH = (contentH > 0) and math.min(contentH, frameH) or frameH

    -- Re-anchor background to the effective height every time so it updates
    -- dynamically when content is added/removed or sections are collapsed.
    chrome.background:ClearAllPoints()
    chrome.background:SetPoint("TOPLEFT",  ObjectiveTrackerFrame, "TOPLEFT",  -K.TRACKER_VISUAL_LEFT_EXTENSION, 0)
    chrome.background:SetPoint("TOPRIGHT", ObjectiveTrackerFrame, "TOPRIGHT", 0, 0)
    chrome.background:SetHeight(effectiveH)

    styleHelpers.ApplyTexturePath(chrome.background, tbgStyle.texture)
    styleHelpers.ApplyRegionColor(chrome.background, tbgStyle.backgroundColor)
    local _, borderThickness = styleHelpers.ApplyBackdropBorder(
        chrome.borderFrame,
        chrome.background,
        tbgStyle.borderSize,
        tbgStyle.borderTexture or tbgStyle.texture,
        tbgStyle.borderColor,
        0,
        0
    )
    chrome.background:Show()

    if borderThickness == 0 and chrome.borderFrame then
        chrome.borderFrame:Hide()
    end
end

function styleHelpers.ApplyBlizzardHeaderBackground(frame)
    if not frame or not frame.Background then
        return
    end
    local background = frame.Background
    -- Cache the canonical atlas on first non-nil read.  Blizzard's UpdateAll (and
    -- related refresh paths) can clear the atlas reference on the background region,
    -- so subsequent calls would otherwise find GetAtlas() == nil and leave the
    -- texture blank until the next full style refresh.
    if background.GetAtlas and not frame.nomtoolsBlizzardAtlasCache then
        local seenAtlas = background:GetAtlas()
        if seenAtlas then
            frame.nomtoolsBlizzardAtlasCache = seenAtlas
        end
    end
    local atlas = frame.nomtoolsBlizzardAtlasCache
        or (background.GetAtlas and background:GetAtlas())
    if atlas and background.SetAtlas then
        background:SetAtlas(atlas, true)
        -- Blizzard templates anchor header backgrounds at CENTER and size via atlas.
        -- Preserve that geometry in Blizzard preset to avoid offset and hidden textures.
        -- Only apply geometry if we have a valid atlas to avoid forcing show/reanchor
        -- when no atlas can be resolved.
        background:ClearAllPoints()
        background:SetPoint("CENTER", frame, "CENTER")
        background:Show()
    end
end

function styleHelpers.ApplyHeaderChrome(frame, appearance)
    local chrome = styleHelpers.EnsureFrameChrome(frame)
    if not chrome then
        return
    end

    local useNomTools = appearance and appearance.useNomTools == true
    if not useNomTools then
        styleHelpers.SetRegionShown(chrome.background, false)
        if chrome.borderFrame then
            chrome.borderFrame:Hide()
        end
        return
    end

    if frame.Background then
        styleHelpers.SetRegionShown(frame.Background, false)
    end

    styleHelpers.ApplyHeaderChromeExtents(frame, chrome)
    styleHelpers.ApplyTexturePath(chrome.background, appearance.texture)
    styleHelpers.ApplyRegionColor(chrome.background, appearance.backgroundColor)
    local _, borderThickness = styleHelpers.ApplyBackdropBorder(
        chrome.borderFrame,
        chrome.background,
        appearance.borderSize,
        appearance.borderTexture or appearance.texture,
        appearance.borderColor,
        0,
        0
    )
    chrome.background:Show()

    if borderThickness == 0 and chrome.borderFrame then
        chrome.borderFrame:Hide()
    end
end

function styleHelpers.SetUIPanelButtonArtVisible(button, visible)
    if not button then
        return
    end

    local alpha = visible and 1 or 0
    for _, key in ipairs(K.UIPANEL_BUTTON_ART_REGION_KEYS) do
        local region = button[key]
        if region and region.SetAlpha then
            region:SetAlpha(alpha)
        end
    end

    for _, texture in ipairs({
        button.GetNormalTexture and button:GetNormalTexture() or nil,
        button.GetPushedTexture and button:GetPushedTexture() or nil,
        button.GetHighlightTexture and button:GetHighlightTexture() or nil,
        button.GetDisabledTexture and button:GetDisabledTexture() or nil,
    }) do
        if texture and texture.SetAlpha then
            texture:SetAlpha(alpha)
        end
    end
end

-- Like SetUIPanelButtonArtVisible but preserves the NormalTexture/PushedTexture icon.
-- Used for MinimizeButtons where the -/+ icon must remain visible.
function styleHelpers.SetMinimizeButtonBorderVisible(button, visible)
    if not button then
        return
    end

    local alpha = visible and 1 or 0
    for _, key in ipairs(K.UIPANEL_BUTTON_ART_REGION_KEYS) do
        local region = button[key]
        if region and region.SetAlpha then
            region:SetAlpha(alpha)
        end
    end
end

-- Creates an OVERLAY-layer chrome + HIGHLIGHT-layer icon label on a minimize button.
-- This is necessary because the standard EnsureFrameChrome background sits at BACKGROUND -8
-- and border edges at BORDER — both below ARTWORK where the button's NormalTexture lives.
-- The overlay approach draws above the NormalTexture and fully controls the visual.
function styleHelpers.EnsureMinimizeButtonOverlay(button)
    if not button then
        return nil
    end
    if button.NomToolsMinimizeOverlay then
        return button.NomToolsMinimizeOverlay
    end

    local overlay = {}

    local bg = button:CreateTexture(nil, "OVERLAY", nil, -2)
    bg:SetAllPoints()
    overlay.background = bg

    local borderFrame = CreateFrame("Frame", nil, button, BackdropTemplateMixin and "BackdropTemplate" or nil)
    borderFrame:SetAllPoints(button)
    borderFrame:SetFrameStrata(button:GetFrameStrata())
    borderFrame:SetFrameLevel((button:GetFrameLevel() or 0) + 2)
    borderFrame:EnableMouse(false)
    overlay.borderFrame = borderFrame
    overlay.edges = {}

    local icon = button:CreateFontString(nil, "OVERLAY", nil)
    icon:SetFont(STANDARD_TEXT_FONT, 14, "OUTLINE")
    icon:SetAllPoints()
    icon:SetJustifyH("CENTER")
    icon:SetJustifyV("MIDDLE")
    overlay.icon = icon

    button.NomToolsMinimizeOverlay = overlay
    return overlay
end

function styleHelpers.ApplyMinimizeButtonChrome(button, appearance, isCollapsed)
    if not button then
        return
    end

    local overlay = styleHelpers.EnsureMinimizeButtonOverlay(button)
    if not overlay then
        return
    end

    local useNomTools = appearance and appearance.useNomTools == true

    local normalTex   = button.GetNormalTexture   and button:GetNormalTexture()   or nil
    local pushedTex   = button.GetPushedTexture   and button:GetPushedTexture()   or nil
    local highlightTex = button.GetHighlightTexture and button:GetHighlightTexture() or nil

    if not useNomTools then
        if normalTex   then normalTex:SetAlpha(1)   end
        if pushedTex   then pushedTex:SetAlpha(1)   end
        if highlightTex then highlightTex:SetAlpha(1) end
        overlay.background:Hide()
        if overlay.borderFrame then overlay.borderFrame:Hide() end
        overlay.icon:Hide()
        return
    end

    if normalTex   then normalTex:SetAlpha(0)   end
    if pushedTex   then pushedTex:SetAlpha(0)   end
    if highlightTex then highlightTex:SetAlpha(0) end

    styleHelpers.ApplyTexturePath(overlay.background, appearance.texture)
    styleHelpers.ApplyRegionColor(overlay.background, appearance.backgroundColor)
    local _, borderThickness = styleHelpers.ApplyBackdropBorder(
        overlay.borderFrame,
        button,
        appearance.borderSize,
        appearance.borderTexture or appearance.texture,
        appearance.borderColor,
        0,
        0
    )
    overlay.background:Show()

    if borderThickness == 0 and overlay.borderFrame then
        overlay.borderFrame:Hide()
    end

    overlay.icon:SetTextColor(1, 1, 1, 1)
    overlay.icon:SetText(isCollapsed and "+" or "-")
    overlay.icon:Show()
end

function styleHelpers.IsCompleteColorStyle(colorStyle)
    if not colorStyle then
        return false
    end

    if colorStyle.nomtoolsIsComplete ~= nil then
        return colorStyle.nomtoolsIsComplete == true
    end

    local completeStyle = OBJECTIVE_TRACKER_COLOR and OBJECTIVE_TRACKER_COLOR.Complete or nil
    if not completeStyle then
        return false
    end

    return colorStyle == completeStyle
        or colorStyle == completeStyle.reverse
        or completeStyle == colorStyle.reverse
end

-- Two sequential scopes: keeps WoW Lua under the 200-local limit for the
-- main chunk. Each do...end frees its locals before the next one starts,
-- so the Lua 5.1 VM never sees more than the per-scope peak simultaneously.
do -- block / line iteration helpers
    function styleHelpers.ForEachBlockLine(block, callback)
        if not block or type(callback) ~= "function" then
            return
        end

        if block.ForEachUsedLine then
            block:ForEachUsedLine(callback)
            return
        end

        if type(block.usedLines) == "table" then
            for line in pairs(block.usedLines) do
                callback(line)
            end
        end
    end

    local forEachBlockSeen = {}
    local currentBlockCallback = nil

    local function VisitBlockShared(block)
        if block and not forEachBlockSeen[block] and (not block.IsShown or block:IsShown()) then
            forEachBlockSeen[block] = true
            currentBlockCallback(block)
        end
    end

    function styleHelpers.ForEachModuleBlock(module, callback)
        if not module or type(callback) ~= "function" then
            return
        end

        local seen = forEachBlockSeen
        for k in pairs(seen) do seen[k] = nil end
        currentBlockCallback = callback

        if module.EnumerateActiveBlocks then
            module:EnumerateActiveBlocks(VisitBlockShared)
        elseif type(module.usedBlocks) == "table" then
            for _, blocksByID in pairs(module.usedBlocks) do
                if type(blocksByID) == "table" then
                    for _, block in pairs(blocksByID) do
                        VisitBlockShared(block)
                    end
                end
            end
        end

        if type(module.FixedBlocks) == "table" then
            for _, block in ipairs(module.FixedBlocks) do
                VisitBlockShared(block)
            end
        end
    end
end -- do: block / line iteration helpers
-- forEachBlockSeen, currentBlockCallback, VisitBlockShared freed here.

do -- progress-bar iteration helpers
    local collectProgressBarsScratch = {}
    local collectProgressBarsSeen = {}

    local function AddProgressBarShared(progressBar)
        if progressBar and not collectProgressBarsSeen[progressBar] then
            collectProgressBarsSeen[progressBar] = true
            collectProgressBarsScratch[#collectProgressBarsScratch + 1] = progressBar
        end
    end

    local function CollectLineProgressBarsCallback(line)
        AddProgressBarShared(line and line.progressBar or nil)
        AddProgressBarShared(line and line.statusBar or nil)
        AddProgressBarShared(line and line.ProgressBar or nil)
        AddProgressBarShared(line and line.StatusBar or nil)
        AddProgressBarShared(line and line.Bar or nil)
    end

    function styleHelpers.CollectBlockProgressBars(block)
        local progressBars = collectProgressBarsScratch
        for k in pairs(progressBars) do progressBars[k] = nil end
        local seen = collectProgressBarsSeen
        for k in pairs(seen) do seen[k] = nil end

        if not block then
            return progressBars
        end

        AddProgressBarShared(block.progressBar)
        AddProgressBarShared(block.statusBar)
        AddProgressBarShared(block.ProgressBar)
        AddProgressBarShared(block.StatusBar)
        AddProgressBarShared(block.Bar)

        styleHelpers.ForEachBlockLine(block, CollectLineProgressBarsCallback)

        return progressBars
    end

    local forEachProgressBarSeen = {}
    local currentProgressBarCallback = nil

    local function VisitProgressBarShared(progressBar)
        if progressBar and not forEachProgressBarSeen[progressBar] and (not progressBar.IsShown or progressBar:IsShown()) then
            forEachProgressBarSeen[progressBar] = true
            currentProgressBarCallback(progressBar)
        end
    end

    local function ForEachProgressBarBlockCallback(block)
        for _, progressBar in ipairs(styleHelpers.CollectBlockProgressBars(block)) do
            VisitProgressBarShared(progressBar)
        end
    end

    function styleHelpers.ForEachModuleProgressBar(module, callback)
        if not module or type(callback) ~= "function" then
            return
        end

        local seen = forEachProgressBarSeen
        for k in pairs(seen) do seen[k] = nil end
        currentProgressBarCallback = callback

        styleHelpers.ForEachModuleBlock(module, ForEachProgressBarBlockCallback)

        if type(module.usedProgressBars) == "table" then
            for _, progressBar in pairs(module.usedProgressBars) do
                VisitProgressBarShared(progressBar)
            end
        end
    end
end -- do: progress-bar iteration helpers

function styleHelpers.GetBlockQuestID(block)
    local questID = block and (block.id or block.questID or block.poiQuestID) or nil
    if type(questID) == "number" and questID > 0 then
        return questID
    end

    return nil
end

function styleHelpers.GetQuestBackedBlockQuestID(block, module)
    if not block then
        return nil
    end

    local resolvedModule = module or block.parentModule
    if block.nomtoolsQuestKind == nil and not IsQuestLikeModule(resolvedModule) then
        return nil
    end

    return styleHelpers.GetBlockQuestID(block)
end

function styleHelpers.GetLineCompletionFromQuestData(line, block)
    if not line then
        return nil
    end

    local questID = styleHelpers.GetQuestBackedBlockQuestID(block)
    local objectiveIndex = tonumber(line.objectiveKey)
    if not questID or not objectiveIndex or objectiveIndex < 1 then
        return nil
    end

    if GetQuestObjectiveInfo then
        local _, _, finished = GetQuestObjectiveInfo(questID, objectiveIndex, false)
        if finished ~= nil then
            return finished == true
        end
    end

    if C_QuestLog and C_QuestLog.GetQuestObjectives then
        local objectives = C_QuestLog.GetQuestObjectives(questID)
        local objective = objectives and objectives[objectiveIndex]
        if objective and objective.finished ~= nil then
            return objective.finished == true
        end
    end

    return nil
end

function styleHelpers.IsLineCompleted(line, block)
    if not line then
        return false
    end

    if line.objectiveKey == "QuestComplete" or line.objectiveKey == "ClickComplete" then
        return true
    end

    -- Scenario blocks use pooled objective lines heavily; never trust pooled
    -- line.finished/line.state here or completion color can leak between runs.
    -- Use live scenario criteria as the single source of truth.
    local blockModule = block and (block.module or block.parentModule) or nil
    if blockModule == ScenarioObjectiveTracker then
        local criteriaIndex = tonumber(line.objectiveKey)
        if criteriaIndex then
            -- WoW 12.x: C_Scenario.GetCriteriaInfo was removed; the modern call is
            -- C_ScenarioInfo.GetCriteriaInfo(criteriaIndex) (step is implicit).
            -- Older clients still expose C_Scenario.GetCriteriaInfoByStep, so try both.
            local criteriaInfo
            if C_ScenarioInfo and C_ScenarioInfo.GetCriteriaInfo then
                criteriaInfo = C_ScenarioInfo.GetCriteriaInfo(criteriaIndex)
            elseif C_Scenario and C_Scenario.GetCriteriaInfoByStep then
                local stepIndex = C_Scenario.GetStepInfo and select(2, C_Scenario.GetStepInfo()) or nil
                if type(stepIndex) == "number" then
                    criteriaInfo = C_Scenario.GetCriteriaInfoByStep(stepIndex, criteriaIndex)
                end
            end
            if criteriaInfo then
                return criteriaInfo.completed == true
            end
        end

        return false
    end

    local objectiveCompleted = styleHelpers.GetLineCompletionFromQuestData(line, block)
    if objectiveCompleted ~= nil then
        return objectiveCompleted
    end

    if line.finished ~= nil then
        return line.finished == true
    end

    if ObjectiveTrackerAnimLineState and line.state then
        if line.state == ObjectiveTrackerAnimLineState.Completed
            or line.state == ObjectiveTrackerAnimLineState.Completing
            or line.state == ObjectiveTrackerAnimLineState.Fading
        then
            return true
        end

        if line.state == ObjectiveTrackerAnimLineState.Present
            or line.state == ObjectiveTrackerAnimLineState.Adding
            or line.state == ObjectiveTrackerAnimLineState.Faded
        then
            return false
        end
    end

    return false
end

function styleHelpers.IsProgressBarCompleted(progressBar)
    local statusBar = progressBar and (progressBar.Bar or progressBar) or nil
    if not statusBar or not statusBar.GetMinMaxValues or not statusBar.GetValue then
        return false
    end

    local minValue, maxValue = statusBar:GetMinMaxValues()
    local resolvedMinValue = tonumber(minValue) or 0
    local resolvedMaxValue = tonumber(maxValue)
    if not resolvedMaxValue or resolvedMaxValue <= resolvedMinValue then
        return false
    end

    return (tonumber(statusBar:GetValue()) or resolvedMinValue) >= resolvedMaxValue
end

function styleHelpers.GetProgressBarPercent(progressBar)
    local statusBar = progressBar and (progressBar.Bar or progressBar) or nil
    if not statusBar or not statusBar.GetMinMaxValues or not statusBar.GetValue then
        return nil
    end

    local minValue, maxValue = statusBar:GetMinMaxValues()
    local resolvedMinValue = tonumber(minValue) or 0
    local resolvedMaxValue = tonumber(maxValue)
    if not resolvedMaxValue or resolvedMaxValue <= resolvedMinValue then
        return nil
    end

    local value = tonumber(statusBar:GetValue()) or resolvedMinValue
    return Clamp((value - resolvedMinValue) / (resolvedMaxValue - resolvedMinValue), 0, 1)
end

function styleHelpers.GetProgressBarFillColor(progressBar, styles)
    if not styles then
        return K.DEFAULT_PROGRESS_FILL_COLOR
    end

    if styles.progressFillMode ~= "progress" then
        return styles.progressFillColor
    end

    local progressPercent = styleHelpers.GetProgressBarPercent(progressBar)
    if not progressPercent then
        return styles.progressFillColor
    end

    if progressPercent < 0.33 then
        return styles.progressLowFillColor
    end

    if progressPercent < 0.66 then
        return styles.progressMediumFillColor
    end

    return styles.progressHighFillColor
end

function styleHelpers.EnsureCustomProgressBarBorder(progressBar, statusBar)
    if not progressBar or not statusBar then
        return nil
    end

    local parent = progressBar ~= statusBar and progressBar or statusBar
    if parent.NomToolsCustomBorder then
        return parent.NomToolsCustomBorder
    end

    local border = CreateFrame("Frame", nil, parent)
    border:SetFrameStrata(parent:GetFrameStrata())
    border:SetFrameLevel(math.max(parent:GetFrameLevel() or 0, statusBar:GetFrameLevel() or 0) + 30)
    parent.NomToolsCustomBorder = border
    if border.SetBackdrop == nil and BackdropTemplateMixin then
        Mixin(border, BackdropTemplateMixin)
    end

    return border
end

function styleHelpers.SetCustomProgressBarBorderThickness(border, statusBar, thickness)
    if not border or not statusBar then
        return
    end

    local signedSize = NormalizeBorderSize(thickness, K.DEFAULT_PROGRESS_BORDER_SIZE, K.MIN_PROGRESS_BORDER_SIZE, K.MAX_PROGRESS_BORDER_SIZE)
    local magnitude = math.abs(signedSize)
    local borderStyle = ResolveBackdropBorderStyle(border.texturePath or BORDER_TEXTURE_FALLBACK_PATH, magnitude)
    local _, _, renderedEdgeSize = styleHelpers.ApplyBackdropBorderLayout(
        border,
        statusBar,
        signedSize,
        borderStyle.edgeSize,
        borderStyle.baseEdgeSize,
        0,
        0
    )
    border.thickness = renderedEdgeSize
    border.signedThickness = signedSize
end

function styleHelpers.HideCustomProgressBarBorder(progressBar, statusBar)
    local parent = progressBar ~= statusBar and progressBar or statusBar
    local border = parent and parent.NomToolsCustomBorder or nil
    if border then
        border:Hide()
    end

    return border
end

function styleHelpers.ApplyCustomProgressBarBorder(progressBar, statusBar, color, texturePath, thickness)
    local signedSize = NormalizeBorderSize(thickness, K.DEFAULT_PROGRESS_BORDER_SIZE, K.MIN_PROGRESS_BORDER_SIZE, K.MAX_PROGRESS_BORDER_SIZE)
    if signedSize == 0 then
        styleHelpers.HideCustomProgressBarBorder(progressBar, statusBar)
        return
    end

    local border = styleHelpers.EnsureCustomProgressBarBorder(progressBar, statusBar)
    if not border then
        return
    end

    border.texturePath = texturePath or BORDER_TEXTURE_FALLBACK_PATH
    styleHelpers.ApplyBackdropBorder(border, statusBar, signedSize, border.texturePath, color, 0, 0)
    border:Show()
end

function styleHelpers.CaptureTextureRegionState(region)
    if not region then
        return nil
    end

    local state = {
        atlas = region.GetAtlas and region:GetAtlas() or nil,
        texture = region.GetTexture and region:GetTexture() or nil,
        shown = region.IsShown and region:IsShown() or nil,
    }

    if region.GetVertexColor then
        local r, g, b, a = region:GetVertexColor()
        state.color = { r = r, g = g, b = b, a = a }
    end

    return state
end

function styleHelpers.RestoreTextureRegionState(region, state)
    if not region or not state then
        return
    end

    if state.atlas and region.SetAtlas then
        region:SetAtlas(state.atlas, false)
    elseif region.SetTexture then
        region:SetTexture(state.texture)
    end

    if state.color and region.SetVertexColor then
        region:SetVertexColor(state.color.r, state.color.g, state.color.b, state.color.a)
    end

    if state.shown ~= nil and region.SetShown then
        region:SetShown(state.shown)
    end
end

function styleHelpers.CaptureFontStringState(fontString)
    if not fontString then
        return nil
    end

    local fontPath, fontSize, fontOutline
    if fontString.GetFont then
        fontPath, fontSize, fontOutline = fontString:GetFont()
    end

    local r, g, b, a = 1, 1, 1, 1
    if fontString.GetTextColor then
        r, g, b, a = fontString:GetTextColor()
    end

    return {
        fontPath = fontPath,
        fontSize = fontSize,
        fontOutline = fontOutline,
        color = { r = r, g = g, b = b, a = a },
        shown = fontString.IsShown and fontString:IsShown() or nil,
    }
end

function styleHelpers.RestoreFontStringState(fontString, state)
    if not fontString or not state then
        return
    end

    if state.fontPath and fontString.SetFont then
        fontString:SetFont(state.fontPath, state.fontSize or 13, state.fontOutline or "")
    end

    if state.color and fontString.SetTextColor then
        fontString:SetTextColor(state.color.r, state.color.g, state.color.b, state.color.a)
    end

    if state.shown ~= nil and fontString.SetShown then
        fontString:SetShown(state.shown)
    end
end

function styleHelpers.CaptureProgressBarStyleState(progressBar, statusBar)
    if not progressBar or not statusBar then
        return nil
    end

    if progressBar.nomtoolsOriginalStyleState then
        return progressBar.nomtoolsOriginalStyleState
    end

    local statusBarTexture = statusBar.GetStatusBarTexture and statusBar:GetStatusBarTexture() or nil
    local rewardIconExclusion = statusBar.Icon
    local backgroundRegions = {
        statusBar.BarBG,
        statusBar.Background,
        statusBar.BG,
        progressBar.BarBG,
        progressBar.Background,
        progressBar.BG,
    }
    for _, region in ipairs(styleHelpers.GetStatusBarAnonymousRegions(statusBar, statusBarTexture, "BACKGROUND", -1)) do
        if region ~= rewardIconExclusion then
            backgroundRegions[#backgroundRegions + 1] = region
        end
    end

    local borderRegions = {
        statusBar.BarFrame,
        statusBar.BarFrame2,
        statusBar.BarFrame3,
        statusBar.BorderLeft,
        statusBar.BorderMid,
        statusBar.BorderRight,
        statusBar.LeftBorder,
        statusBar.MiddleBorder,
        statusBar.RightBorder,
    }

    local glowRegions = {
        statusBar.Sheen,
        statusBar.Starburst,
    }

    local function CaptureRegionList(regions)
        local captured = {}
        local seen = {}
        for _, region in ipairs(regions) do
            if region and not seen[region] then
                seen[region] = true
                captured[#captured + 1] = {
                    region = region,
                    state = styleHelpers.CaptureTextureRegionState(region),
                }
            end
        end
        return captured
    end

    local state = {
        statusBarTexturePath = statusBarTexture and statusBarTexture.GetTexture and statusBarTexture:GetTexture() or nil,
        statusBarTextureState = styleHelpers.CaptureTextureRegionState(statusBarTexture),
        backgroundRegions = CaptureRegionList(backgroundRegions),
        borderRegions = CaptureRegionList(borderRegions),
        glowRegions = CaptureRegionList(glowRegions),
        label = (function()
            local label = statusBar.Label or statusBar.TimeLeft or progressBar.Label or progressBar.TimeLeft
            if not label then
                return nil
            end

            return {
                fontString = label,
                state = styleHelpers.CaptureFontStringState(label),
            }
        end)(),
        rewardIcon = (function()
            if not statusBar.Icon then
                return nil
            end

            return {
                onEnter = statusBar.Icon.GetScript and statusBar.Icon:GetScript("OnEnter") or nil,
                onLeave = statusBar.Icon.GetScript and statusBar.Icon:GetScript("OnLeave") or nil,
                mouseEnabled = statusBar.Icon.IsMouseEnabled and statusBar.Icon:IsMouseEnabled() or nil,
            }
        end)(),
    }

    if statusBar.GetStatusBarColor then
        local r, g, b, a = statusBar:GetStatusBarColor()
        state.statusBarColor = { r = r, g = g, b = b, a = a }
    end

    progressBar.nomtoolsOriginalStyleState = state
    return state
end

function styleHelpers.RestoreProgressBarStyle(progressBar, keepRewardTooltip)
    if not progressBar then
        return
    end

    local statusBar = progressBar.Bar or progressBar
    local state = progressBar.nomtoolsOriginalStyleState
    if not statusBar or not state then
        return
    end

    if state.statusBarTexturePath and statusBar.SetStatusBarTexture then
        statusBar:SetStatusBarTexture(state.statusBarTexturePath)
    end

    if state.statusBarColor and statusBar.SetStatusBarColor then
        statusBar:SetStatusBarColor(
            state.statusBarColor.r,
            state.statusBarColor.g,
            state.statusBarColor.b,
            state.statusBarColor.a
        )
    end

    local currentStatusBarTexture = statusBar.GetStatusBarTexture and statusBar:GetStatusBarTexture() or nil
    styleHelpers.RestoreTextureRegionState(currentStatusBarTexture, state.statusBarTextureState)

    for _, entry in ipairs(state.backgroundRegions or {}) do
        styleHelpers.RestoreTextureRegionState(entry.region, entry.state)
    end
    for _, entry in ipairs(state.borderRegions or {}) do
        styleHelpers.RestoreTextureRegionState(entry.region, entry.state)
    end
    for _, entry in ipairs(state.glowRegions or {}) do
        styleHelpers.RestoreTextureRegionState(entry.region, entry.state)
    end

    styleHelpers.HideCustomProgressBarBorder(progressBar, statusBar)

    if progressBar.UpdateReward then
        progressBar:UpdateReward()
    elseif statusBar.Icon then
        statusBar.Icon:Show()
        if statusBar.IconBG then
            statusBar.IconBG:Show()
        end
    end

    progressBar.nomtoolsCachedRewardTexture = nil
    progressBar.nomtoolsCachedRewardQuestID = nil

    if state.rewardIcon and statusBar.Icon then
        if statusBar.Icon.SetScript then
            statusBar.Icon:SetScript("OnEnter", state.rewardIcon.onEnter)
            statusBar.Icon:SetScript("OnLeave", state.rewardIcon.onLeave)
        end
        if state.rewardIcon.mouseEnabled ~= nil and statusBar.Icon.EnableMouse then
            statusBar.Icon:EnableMouse(state.rewardIcon.mouseEnabled)
        end
    end
    progressBar.nomtoolsIconHookInstalled = false

    if keepRewardTooltip
        and statusBar.Icon
        and state.rewardIcon
        and not state.rewardIcon.onEnter
        and not state.rewardIcon.onLeave
    then
        styleHelpers.InstallProgressBarRewardTooltip(progressBar, statusBar)
    end

    if state.label then
        styleHelpers.RestoreFontStringState(state.label.fontString, state.label.state)
    end

    progressBar.nomtoolsCustomStyleApplied = false
end

function styleHelpers.InstallProgressBarRewardTooltip(progressBar, statusBar)
    if not progressBar or not statusBar or not statusBar.Icon or progressBar.nomtoolsIconHookInstalled then
        return false
    end

    statusBar.Icon:EnableMouse(true)
    statusBar.Icon:SetScript("OnEnter", function()
        local qid = progressBar.questID
        if not qid then return end
        local block = progressBar.parentLine and progressBar.parentLine.parentBlock
        local isWorldQuest = block and block.parentModule and block.parentModule.showWorldQuests
        GameTooltip:ClearAllPoints()
        GameTooltip:SetPoint("TOPRIGHT", statusBar.Icon, "TOPLEFT", 0, 0)
        GameTooltip:SetOwner(progressBar, "ANCHOR_PRESERVE")
        if HaveQuestRewardData(qid) then
            if isWorldQuest then
                QuestUtils_AddQuestTypeToTooltip(GameTooltip, qid, NORMAL_FONT_COLOR)
                GameTooltip:AddLine(REWARDS, NORMAL_FONT_COLOR:GetRGB())
            else
                GameTooltip:SetText(REWARDS, NORMAL_FONT_COLOR:GetRGB())
            end
            GameTooltip:AddLine(isWorldQuest and WORLD_QUEST_TOOLTIP_DESCRIPTION or BONUS_OBJECTIVE_TOOLTIP_DESCRIPTION, 1, 1, 1, 1)
            GameTooltip:AddLine(" ")
            local rewardsOk = pcall(
                GameTooltip_AddQuestRewardsToTooltip,
                GameTooltip,
                qid,
                TOOLTIP_QUEST_REWARDS_STYLE_NONE)
            if rewardsOk then
                GameTooltip_SetTooltipWaitingForData(GameTooltip, false)
            else
                GameTooltip:AddLine(RETRIEVING_DATA, RED_FONT_COLOR.r, RED_FONT_COLOR.g, RED_FONT_COLOR.b)
                GameTooltip_SetTooltipWaitingForData(GameTooltip, true)
            end
        else
            GameTooltip:AddLine(RETRIEVING_DATA, RED_FONT_COLOR.r, RED_FONT_COLOR.g, RED_FONT_COLOR.b)
            GameTooltip_SetTooltipWaitingForData(GameTooltip, true)
        end
        GameTooltip:Show()
    end)
    statusBar.Icon:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    progressBar.nomtoolsIconHookInstalled = true
    return true
end

function styleHelpers.IsBlockCompleted(block)
    if not block then
        return false
    end

    local questID = styleHelpers.GetQuestBackedBlockQuestID(block)
    if questID and C_QuestLog and C_QuestLog.IsComplete then
        local isComplete = C_QuestLog.IsComplete(questID)
        if isComplete ~= nil then
            return isComplete == true
        end
    end

    local hasLines = false
    local allLinesComplete = true
    styleHelpers.ForEachBlockLine(block, function(line)
        if line and (not line.IsShown or line:IsShown()) then
            hasLines = true
            if not styleHelpers.IsLineCompleted(line, block) then
                allLinesComplete = false
            end
        end
    end)
    if hasLines then
        return allLinesComplete
    end

    for _, progressBar in ipairs(styleHelpers.CollectBlockProgressBars(block)) do
        if styleHelpers.IsProgressBarCompleted(progressBar) then
            return true
        end
    end

    return false
end

function styleHelpers.IsQuestReadyForTurnIn(questID)
    if type(questID) ~= "number" or questID <= 0 then
        return false
    end

    if C_QuestLog and C_QuestLog.ReadyForTurnIn then
        local readyForTurnIn = C_QuestLog.ReadyForTurnIn(questID)
        if readyForTurnIn ~= nil then
            return readyForTurnIn == true
        end
    end

    if C_QuestLog and C_QuestLog.IsComplete then
        local isComplete = C_QuestLog.IsComplete(questID)
        if isComplete ~= nil then
            return isComplete == true
        end
    end

    return false
end

function styleHelpers.RefreshQuestBlockPOI(block, module)
    if not block or not block.SetPOIInfo then
        return
    end

    local questID = styleHelpers.GetQuestBackedBlockQuestID(block, module)
    if not questID then
        return
    end

    local questKind = block.nomtoolsQuestKind or GetQuestKind(questID)
    if questKind ~= "quest" and questKind ~= "campaign" and questKind ~= "worldQuest" then
        return
    end

    local isSuperTracked = C_SuperTrack and C_SuperTrack.GetSuperTrackedQuestID and questID == C_SuperTrack.GetSuperTrackedQuestID() or false
    block:SetPOIInfo(questID, styleHelpers.IsQuestReadyForTurnIn(questID), isSuperTracked, questKind == "worldQuest")

    if questKind == "worldQuest" and block.poiButton and block.poiButton.SetMapPinInfo and block.poiButton.SetStyle then
        block.poiButton:SetMapPinInfo(nil)
        block.poiButton:SetStyle(POIButtonUtil.Style.WorldQuest)
        block.poiButton:UpdateButtonStyle()
    end
end

function styleHelpers.GetQuestTitleColorOverride(questID, styles, isHighlighted)
    if not questID or not styles then
        return nil
    end

    local color
    if IsPreyQuest(questID) then
        color = styles.preyTitleColor
    elseif IsTrivialQuest(questID) and styles.useTrivialTitleColor and not IsCampaignQuest(questID) then
        color = styles.trivialTitleColor
    else
        local recurringType = GetRecurringQuestType(questID)
        if recurringType == "weekly" then
            color = styles.weeklyTitleColor
        elseif recurringType == "daily" then
            color = styles.dailyTitleColor
        else
            local classification = GetQuestClassification(questID)
            if Enum
                and Enum.QuestClassification
                and Enum.QuestClassification.Meta ~= nil
                and classification == Enum.QuestClassification.Meta
            then
                color = styles.metaTitleColor
            elseif classification == Enum.QuestClassification.Legendary then
                color = styles.legendaryTitleColor
            elseif classification == Enum.QuestClassification.Campaign then
                color = styles.campaignTitleColor
            elseif Enum
                and Enum.QuestClassification
                and Enum.QuestClassification.Important ~= nil
                and classification == Enum.QuestClassification.Important
            then
                color = styles.importantTitleColor
            end
        end
    end

    if not color then
        return nil
    end

    if isHighlighted then
        return GetHoverColor(color)
    end

    return color
end

function styleHelpers.GetQuestKindTitleColorOverride(questKind, styles, isHighlighted)
    local color = GetQuestKindTitleColor(questKind, styles, false)
    if not color then
        return nil
    end

    if isHighlighted then
        return GetQuestKindTitleColor(questKind, styles, true) or GetHoverColor(color)
    end

    return color
end

local function HideWarbandIndicatorTooltip(block, indicator)
    if not GameTooltip or not GameTooltip.GetOwner then
        return
    end

    local owner = GameTooltip:GetOwner()
    if owner == indicator or owner == block then
        GameTooltip_Hide()
    end
end

function styleHelpers.EnsureWarbandIndicator(block)
    if not block then
        return nil
    end

    if block.NomToolsWarbandIndicator then
        return block.NomToolsWarbandIndicator
    end

    local indicator = CreateFrame("Frame", nil, block)
    indicator:SetSize(K.WARBAND_COMPLETION_ICON_SIZE, K.WARBAND_COMPLETION_ICON_SIZE)
    indicator:SetFrameStrata(block:GetFrameStrata())
    indicator:SetFrameLevel((block:GetFrameLevel() or 0) + 8)
    indicator:EnableMouse(true)
    indicator:SetHitRectInsets(-2, -2, -2, -2)

    local icon = indicator:CreateTexture(nil, "ARTWORK")
    icon:SetAllPoints()
    indicator.Icon = icon

    local check = indicator:CreateTexture(nil, "OVERLAY")
    check:SetSize(12, 12)
    check:SetPoint("BOTTOMRIGHT", indicator, "BOTTOMRIGHT", 2, -1)
    indicator.Check = check

    indicator:SetScript("OnEnter", function(self)
        if not self.questID or not GameTooltip then
            return
        end

        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip_AddHighlightLine(GameTooltip, K.WARBAND_COMPLETION_TOOLTIP)
        GameTooltip:Show()
    end)
    indicator:SetScript("OnLeave", function(self)
        HideWarbandIndicatorTooltip(block, self)
    end)
    indicator:SetScript("OnHide", function(self)
        HideWarbandIndicatorTooltip(block, self)
    end)
    indicator:Hide()

    block.NomToolsWarbandIndicator = indicator
    return indicator
end

function styleHelpers.UpdateWarbandIndicator(block, questID, module)
        if not block or not block.HeaderText then
        return
    end

    local shouldShow = ShouldShowWarbandCompletedIndicator(questID, module)
        local indicator = block.NomToolsWarbandIndicator
        if not shouldShow then
            if indicator then
                HideWarbandIndicatorTooltip(block, indicator)
                indicator.questID = nil
                indicator:Hide()
            end
            return
        end

        indicator = indicator or styleHelpers.EnsureWarbandIndicator(block)
        if not indicator then
            return
        end

    if shouldShow and block.HeaderText:IsShown() then
        local iconAtlas = GetWarbandCompletionAtlas()
        local checkAtlas = GetWarbandCompletionCheckAtlas()
        indicator.questID = questID
        indicator:ClearAllPoints()
        indicator:SetPoint("RIGHT", block.HeaderText, "RIGHT", 0, 0)
        indicator.Icon:SetAtlas(iconAtlas, false)
        indicator.Icon:SetSize(K.WARBAND_COMPLETION_ICON_SIZE, K.WARBAND_COMPLETION_ICON_SIZE)
        if checkAtlas then
            indicator.Check:SetAtlas(checkAtlas, false)
            indicator.Check:Show()
        else
            indicator.Check:Hide()
        end
        indicator:Show()
    else
        HideWarbandIndicatorTooltip(block, indicator)
        indicator.questID = nil
        indicator:Hide()
    end
end

function styleHelpers.ApplyProgressBarStyle(progressBar, styles)
    if not progressBar or not styles then
        return
    end

    local statusBar = progressBar.Bar or progressBar
    if not statusBar then
        return
    end

    if styles.progressUseNomTools ~= true then
        if progressBar.nomtoolsCustomStyleApplied then
            styleHelpers.RestoreProgressBarStyle(progressBar, true)
        elseif statusBar.Icon and not progressBar.nomtoolsIconHookInstalled then
            local rewardState = progressBar.nomtoolsOriginalStyleState and progressBar.nomtoolsOriginalStyleState.rewardIcon or nil
            if not rewardState or (not rewardState.onEnter and not rewardState.onLeave) then
                styleHelpers.InstallProgressBarRewardTooltip(progressBar, statusBar)
            end
        end
        return
    end

    styleHelpers.CaptureProgressBarStyleState(progressBar, statusBar)
    progressBar.nomtoolsCustomStyleApplied = true

    local statusBarTexture = statusBar.GetStatusBarTexture and statusBar:GetStatusBarTexture() or nil
    local fillColor = styleHelpers.GetProgressBarFillColor(progressBar, styles)

    if statusBar.SetStatusBarTexture then
        statusBar:SetStatusBarTexture(styles.progressTexture)
    end
    if statusBar.SetStatusBarColor then
        statusBar:SetStatusBarColor(
            fillColor.r,
            fillColor.g,
            fillColor.b,
            fillColor.a
        )
    end
    styleHelpers.ApplyTexturePath(statusBarTexture, styles.progressTexture)
    styleHelpers.ApplyRegionColor(statusBarTexture, fillColor)

    -- Exclude the reward icon from the anonymous background scan; it lives at
    -- BACKGROUND sublevel -1 alongside BarBG and would be overwritten otherwise.
    local rewardIconExclusion = statusBar.Icon
    local backgroundRegions = {
        statusBar.BarBG,
        statusBar.Background,
        statusBar.BG,
        progressBar.BarBG,
        progressBar.Background,
        progressBar.BG,
    }
    for _, region in ipairs(styleHelpers.GetStatusBarAnonymousRegions(statusBar, statusBarTexture, "BACKGROUND", -1)) do
        if region ~= rewardIconExclusion then
            backgroundRegions[#backgroundRegions + 1] = region
        end
    end
    for _, region in ipairs(backgroundRegions) do
        styleHelpers.ApplyTexturePath(region, styles.progressTexture)
        styleHelpers.ApplyRegionColor(region, styles.progressBackgroundColor)
    end

    -- Re-apply after background pass so Icon texture and visibility aren't clobbered.
    if progressBar.UpdateReward then
        progressBar:UpdateReward()
        -- Cache the reward texture when data is available, and restore it on subsequent
        -- refreshes where HaveQuestRewardData momentarily returns false (prevents flicker).
        if statusBar.Icon then
            local currentQuestID = progressBar.questID
            if statusBar.Icon:IsShown() then
                progressBar.nomtoolsCachedRewardTexture = statusBar.Icon:GetTexture()
                progressBar.nomtoolsCachedRewardQuestID = currentQuestID
            elseif progressBar.nomtoolsCachedRewardTexture
                   and progressBar.nomtoolsCachedRewardQuestID == currentQuestID then
                statusBar.Icon:SetTexture(progressBar.nomtoolsCachedRewardTexture)
                statusBar.Icon:Show()
                if statusBar.IconBG then statusBar.IconBG:Show() end
            end
        end

        -- Honour the "Hide Reward Icon" setting.
        if styles.progressHideRewardIcon and statusBar.Icon then
            statusBar.Icon:Hide()
            if statusBar.IconBG then statusBar.IconBG:Hide() end
            progressBar.nomtoolsCachedRewardTexture = nil
            progressBar.nomtoolsCachedRewardQuestID = nil
        end
    end

    -- Install reward icon hover tooltip once per pooled progress bar frame.
    if statusBar.Icon and not progressBar.nomtoolsIconHookInstalled and not styles.progressHideRewardIcon then
        styleHelpers.InstallProgressBarRewardTooltip(progressBar, statusBar)
    end

    local borderRegions = {
        statusBar.BarFrame,
        statusBar.BarFrame2,
        statusBar.BarFrame3,
        statusBar.BorderLeft,
        statusBar.BorderMid,
        statusBar.BorderRight,
        statusBar.LeftBorder,
        statusBar.MiddleBorder,
        statusBar.RightBorder,
    }
    local hiddenBorderColor = {
        r = styles.progressBorderColor.r,
        g = styles.progressBorderColor.g,
        b = styles.progressBorderColor.b,
        a = 0,
    }
    for _, region in ipairs(borderRegions) do
        styleHelpers.ApplyRegionColor(region, hiddenBorderColor)
    end

    local glowRegions = {
        statusBar.Sheen,
        statusBar.Starburst,
    }
    for _, region in ipairs(glowRegions) do
        styleHelpers.ApplyRegionColor(region, hiddenBorderColor)
    end

    styleHelpers.ApplyCustomProgressBarBorder(
        progressBar,
        statusBar,
        styles.progressBorderColor,
        styles.progressBorderTexture,
        styles.progressBorderSize
    )

    local label = statusBar.Label or statusBar.TimeLeft or progressBar.Label or progressBar.TimeLeft
    if label then
        styleHelpers.SetFontAppearance(label, styles.fontPath, styles.fontSize, styles.fontOutline)
        styleHelpers.ApplyRegionColor(label, styleHelpers.IsProgressBarCompleted(progressBar) and styles.completedColor or styles.uncompletedColor)
    end
end

ShouldApplyModuleContentStyling = function(module)
    return module ~= UIWidgetObjectiveTracker
end

function styleHelpers.ApplyLineStyle(line, styles, block)
    if not line or not styles then
        return
    end

    if line and line.Text then
        line.Text.nomtoolsColorStyle = nil
    end

    local isCompleted = styleHelpers.IsLineCompleted(line, block)
    local colorStyle = isCompleted and styles.completedStyle or styles.uncompletedStyle
    local color = (block and block.isHighlighted and colorStyle.reverse) or colorStyle

    if line.Text then
        styleHelpers.SetFontAppearance(line.Text, styles.fontPath, styles.fontSize, styles.fontOutline)
        styleHelpers.ApplyFontStringColorStyle(line.Text, colorStyle, block and block.isHighlighted)
    end

    styleHelpers.ApplyRegionColor(line.Dash, color)
end

BuildBlockHeaderColorStyle = function(block, styles, module, questID)
    local headerStyle = styles and styles.defaultHeaderStyle or nil
    local isQuestModule = IsQuestLikeModule(module) or (block and block.nomtoolsQuestKind ~= nil)
    local zoneQuestKind = block and block.nomtoolsQuestKind or nil
    local questKind = isQuestModule and zoneQuestKind or nil

    if isQuestModule and not questKind and questID then
        local derivedQuestKind = GetQuestKind(questID)
        if derivedQuestKind == "quest" or derivedQuestKind == "worldQuest" or derivedQuestKind == "bonusObjective" then
            questKind = derivedQuestKind
        end
    end

    if isQuestModule and questID then
        if IsPreyQuest(questID) then
            headerStyle = styles.preyHeaderStyle or headerStyle
        elseif IsTrivialQuest(questID) and styles.useTrivialTitleColor and not IsCampaignQuest(questID) then
            headerStyle = styles.trivialHeaderStyle or headerStyle
        else
            local recurringType = GetRecurringQuestType(questID)
            if recurringType == "weekly" then
                headerStyle = styles.weeklyHeaderStyle or headerStyle
            elseif recurringType == "daily" then
                headerStyle = styles.dailyHeaderStyle or headerStyle
            else
                local classification = GetQuestClassification(questID)
                if Enum
                    and Enum.QuestClassification
                    and Enum.QuestClassification.Meta ~= nil
                    and classification == Enum.QuestClassification.Meta
                then
                    headerStyle = styles.metaHeaderStyle or headerStyle
                elseif classification == Enum.QuestClassification.Legendary then
                    headerStyle = styles.legendaryHeaderStyle or headerStyle
                elseif classification == Enum.QuestClassification.Campaign then
                    headerStyle = styles.campaignHeaderStyle or headerStyle
                elseif Enum
                    and Enum.QuestClassification
                    and Enum.QuestClassification.Important ~= nil
                    and classification == Enum.QuestClassification.Important
                then
                    headerStyle = styles.importantHeaderStyle or headerStyle
                end
            end
        end
    end

    if headerStyle == styles.defaultHeaderStyle and isQuestModule then
        if questKind == "quest" then
            headerStyle = styles.questHeaderStyle or headerStyle
        elseif questKind == "worldQuest" then
            headerStyle = styles.worldQuestHeaderStyle or headerStyle
        elseif questKind == "bonusObjective" then
            headerStyle = styles.bonusObjectiveHeaderStyle or headerStyle
        end
    end

    return headerStyle
end

function styleHelpers.ReapplyLiveBlockStyle(block)
    if not block or not IsModuleEnabled() then
        return
    end

    local module = block.parentModule
    if not module or module.parentContainer ~= ObjectiveTrackerFrame then
        return
    end

    if not ShouldApplyModuleContentStyling or not ShouldApplyModuleContentStyling(module) then
        return
    end

    local styles = BuildTrackerStyleData and BuildTrackerStyleData() or nil
    if styles and ApplyBlockStyle then
        ApplyBlockStyle(block, styles, module)
    end
end

function styleHelpers.SyncLiveBlockHeaderText(block)
    if not block or not block.HeaderText or not IsModuleEnabled() then
        return
    end

    local module = block.parentModule
    if not module or module.parentContainer ~= ObjectiveTrackerFrame then
        return
    end

    local questID = styleHelpers.GetQuestBackedBlockQuestID(block, module)
    if not questID then
        return
    end

    local currentText = block.HeaderText:GetText()
    local desiredText = BuildQuestHeaderText(questID, currentText)
    if not desiredText or desiredText == currentText then
        return
    end

    block.NomToolsHeaderTextSyncing = true
    block.HeaderText:SetText(desiredText)
    block.NomToolsHeaderTextSyncing = nil
end

end -- do: styleHelpers (part 1)

do -- styleHelpers (part 2 — live hooks, ApplyTrackerStyles, RequestTrackerStyleRefresh; split for 200-local limit)

local pendingRestyleBlocks = {}
local pendingRestyleScheduled = false

local function RunPendingBlockRestyles()
    pendingRestyleScheduled = false
    for block in pairs(pendingRestyleBlocks) do
        pendingRestyleBlocks[block] = nil
        if block then
            block.NomToolsLiveRestylePending = nil
        end
        if block and block.IsShown and block:IsShown() then
            styleHelpers.ReapplyLiveBlockStyle(block)
        end
    end
end

function styleHelpers.QueueLiveBlockRestyle(block)
    if not block or not C_Timer or not C_Timer.After or block.NomToolsLiveRestylePending then
        return
    end

    block.NomToolsLiveRestylePending = true
    pendingRestyleBlocks[block] = true

    if not pendingRestyleScheduled then
        pendingRestyleScheduled = true
        C_Timer.After(0, RunPendingBlockRestyles)
    end
end

function styleHelpers.StartHoverStyleUpdater(block)
    return
end

function styleHelpers.StopHoverStyleUpdater(block)
    return
end

local function ResolveTrackedQuestBlockFromFrame(frame)
    if not frame then
        return nil
    end
    local p = frame
    local depth = 0
    while p and depth < 24 do
        if p.parentModule and p.HeaderText and p.parentModule.parentContainer == ObjectiveTrackerFrame then
            return p
        end
        p = p.GetParent and p:GetParent() or nil
        depth = depth + 1
    end
    return nil
end

function styleHelpers.EnsureObjectiveLineTextRestyleHooks(block)
    if not block then
        return
    end

    styleHelpers.ForEachBlockLine(block, function(line)
        local fs = line and line.Text
        if not fs or fs.nomtoolsLineRestyleHooked then
            return
        end
        fs.nomtoolsLineRestyleHooked = true

        if type(fs.SetText) == "function" then
            hooksecurefunc(fs, "SetText", function()
                if fs.nomtoolsLineTextSyncing then
                    return
                end
                local parentBlock = ResolveTrackedQuestBlockFromFrame(line)
                if parentBlock then
                    styleHelpers.QueueLiveBlockRestyle(parentBlock)
                end
            end)
        end

        if type(fs.SetFormattedText) == "function" then
            hooksecurefunc(fs, "SetFormattedText", function()
                if fs.nomtoolsLineTextSyncing then
                    return
                end
                local parentBlock = ResolveTrackedQuestBlockFromFrame(line)
                if parentBlock then
                    styleHelpers.QueueLiveBlockRestyle(parentBlock)
                end
            end)
        end
    end)
end

function styleHelpers.EnsureLiveBlockHooks(block)
    if not block or block.NomToolsLiveHooksInstalled then
        return
    end

    block.NomToolsLiveHooksInstalled = true

    if type(block.UpdateHighlight) == "function" then
        block.NomToolsOriginalUpdateHighlight = block.UpdateHighlight
        block.UpdateHighlight = function(self, ...)
            local result = self.NomToolsOriginalUpdateHighlight(self, ...)
            styleHelpers.ReapplyLiveBlockStyle(self)
            return result
        end
    end

    if block.HeaderText and not block.NomToolsHeaderTextHooksInstalled then
        block.NomToolsHeaderTextHooksInstalled = true

        if type(block.HeaderText.SetText) == "function" then
            hooksecurefunc(block.HeaderText, "SetText", function()
                if block.NomToolsHeaderTextSyncing then
                    return
                end

                styleHelpers.SyncLiveBlockHeaderText(block)
                -- Blizzard often assigns text, then colors, across several calls; defer a full
                -- ApplyBlockStyle so questID / completion state match the visible strings.
                styleHelpers.QueueLiveBlockRestyle(block)
            end)
        end

        if type(block.HeaderText.SetFormattedText) == "function" then
            hooksecurefunc(block.HeaderText, "SetFormattedText", function()
                if block.NomToolsHeaderTextSyncing then
                    return
                end

                styleHelpers.SyncLiveBlockHeaderText(block)
                styleHelpers.QueueLiveBlockRestyle(block)
            end)
        end
    end

    if type(block.OnHeaderEnter) == "function" then
        block.NomToolsOriginalOnHeaderEnter = block.OnHeaderEnter
        block.OnHeaderEnter = function(self, ...)
            local result = self.NomToolsOriginalOnHeaderEnter(self, ...)
            styleHelpers.ReapplyLiveBlockStyle(self)
            styleHelpers.QueueLiveBlockRestyle(self)
            styleHelpers.StartHoverStyleUpdater(self)
            return result
        end
    end

    if type(block.OnHeaderLeave) == "function" then
        block.NomToolsOriginalOnHeaderLeave = block.OnHeaderLeave
        block.OnHeaderLeave = function(self, ...)
            local result = self.NomToolsOriginalOnHeaderLeave(self, ...)
            styleHelpers.StopHoverStyleUpdater(self)
            styleHelpers.ReapplyLiveBlockStyle(self)
            styleHelpers.QueueLiveBlockRestyle(self)
            return result
        end
    end
end

do -- Style callback scope to stay under the 200-local chunk limit

do

-- Hoisted callback for ApplyBlockStyle to avoid per-block closure allocations.
local currentLineStyles = nil
local currentLineBlock = nil

local function ApplyLineStyleCallback(line)
    styleHelpers.ApplyLineStyle(line, currentLineStyles, currentLineBlock)
end

ApplyBlockStyle = function(block, styles, module)
    if not block or not styles then
        return
    end

    if block.HeaderText then
        block.HeaderText.nomtoolsColorStyle = nil
    end

    styleHelpers.EnsureLiveBlockHooks(block)
    styleHelpers.EnsureObjectiveLineTextRestyleHooks(block)

    local questID = styleHelpers.GetQuestBackedBlockQuestID(block, module)
    if questID then
        styleHelpers.RefreshQuestBlockPOI(block, module)
    end

    if block.HeaderText then
        if questID then
            local headerText = BuildQuestHeaderText(questID, block.HeaderText:GetText())
            if headerText and block.HeaderText:GetText() ~= headerText then
                block.HeaderText:SetText(headerText)
            end
        end

        local headerColorStyle = BuildBlockHeaderColorStyle(block, styles, module, questID)

        styleHelpers.SetFontAppearance(block.HeaderText, styles.fontPath, styles.fontSize, styles.fontOutline)
        styleHelpers.ApplyFontStringColorStyle(block.HeaderText, headerColorStyle, block.isHighlighted)
    end

    styleHelpers.UpdateWarbandIndicator(block, questID, module)

    currentLineStyles = styles
    currentLineBlock = block
    styleHelpers.ForEachBlockLine(block, ApplyLineStyleCallback)
    currentLineStyles = nil
    currentLineBlock = nil
end

end

-- Hoisted callbacks for ApplyTrackerStyles to avoid per-module closure allocations.
local currentApplyStyles = nil
local currentApplyModule = nil

local function ApplyBlockStyleCallback(block)
    if not state.currentApplyForceBlockStyle and block and not block.nomtoolsNeedsStyleRefresh then
        return
    end
    ApplyBlockStyle(block, currentApplyStyles, currentApplyModule)
    if block then
        block.nomtoolsNeedsStyleRefresh = nil
    end
end

styleHelpers.ApplyProgressBarStyleCallback = styleHelpers.ApplyProgressBarStyleCallback or function(progressBar)
    styleHelpers.ApplyProgressBarStyle(progressBar, currentApplyStyles)
end

ApplyTrackerStyles = function()
    if not ObjectiveTrackerFrame or not IsModuleEnabled() then
        return
    end

    state.DiagInc("applytrackerstyles.calls")
    local _diag_t0 = state.DiagNow()

    local styles = BuildTrackerStyleData()
    local hasPendingModuleStyleRefresh = state.HasPendingModuleStyleRefresh()
    styleHelpers.ApplyTrackerBackground(styles.trackerBackground)
    local typoSigParts = styleSignatureParts
    for k in pairs(typoSigParts) do typoSigParts[k] = nil end
    typoSigParts[1] = tostring(styles.fontPath or "")
    typoSigParts[2] = tostring(styles.fontSize or 13)
    typoSigParts[3] = tostring(styles.fontOutline or "")
    typoSigParts[4] = tostring(styles.levelPrefixMode or "")
    typoSigParts[5] = tostring(styles.showWarbandCompletedIndicator and true or false)
    typoSigParts[6] = tostring(styles.mainHeaderFont and styles.mainHeaderFont.fontSize or "")
    typoSigParts[7] = tostring(styles.categoryHeaderFont and styles.categoryHeaderFont.fontSize or "")
    typoSigParts[8] = tostring(styles.mainHeaderXOffset or 0)
    typoSigParts[9] = tostring(styles.mainHeaderYOffset or 0)
    typoSigParts[10] = tostring(styles.categoryHeaderXOffset or 0)
    typoSigParts[11] = tostring(styles.categoryHeaderYOffset or 0)
    local typographySignature = table.concat(typoSigParts, "|")
    local needsRelayout = state.lastTypographySignature ~= typographySignature
    state.lastTypographySignature = typographySignature
    local mainHdrAppearance    = styles.mainHeaderAppearance
    local catHdrAppearance     = styles.categoryHeaderAppearance
    local btnAppearance        = styles.buttonAppearance
    local useMainNomTools         = mainHdrAppearance and mainHdrAppearance.useNomTools == true
    local useCategoryNomTools     = catHdrAppearance  and catHdrAppearance.useNomTools  == true
    local useButtonNomTools       = btnAppearance     and btnAppearance.useNomTools     == true
    local fullStyleRefresh = state.postLayoutStyleDirty == true or needsRelayout or not hasPendingModuleStyleRefresh

    if fullStyleRefresh and ObjectiveTrackerFrame.Header then
        -- Blizzard anchors the header's right edge with a built-in inset that reserves
        -- space for its native scroll indicator.  The inset persists whether the
        -- indicator shows or not, and SetWidth() is a no-op on dual-anchored frames.
        -- Force the TOPRIGHT anchor directly so the header always fills the full OTF width.
        local header = ObjectiveTrackerFrame.Header
        local numPoints = header.GetNumPoints and header:GetNumPoints() or 0
        if numPoints >= 2 then
            -- Preserve the TOPLEFT anchor; replace only the right-side anchor.
            header:SetPoint("TOPRIGHT", ObjectiveTrackerFrame, "TOPRIGHT", 0, 0)
        else
            local otfWidth = ObjectiveTrackerFrame.GetWidth
                and ResolvePublicNumber(ObjectiveTrackerFrame:GetWidth())
                or nil
            if otfWidth and otfWidth > 0 then
                header:SetWidth(otfWidth)
            end
        end
        local showBg = IsHeaderBackgroundShown()
        styleHelpers.ApplyHeaderChrome(ObjectiveTrackerFrame.Header, mainHdrAppearance)
        -- If the player disabled the header background, suppress the NomTools chrome too.
        if not showBg then
            local chrome = styleHelpers.EnsureFrameChrome(header)
            if chrome then
                styleHelpers.SetRegionShown(chrome.background, false)
                if chrome.borderFrame then
                    chrome.borderFrame:Hide()
                end
            end
        end
        styleHelpers.SetUIPanelButtonArtVisible(ObjectiveTrackerFrame.Header, not useMainNomTools)
        if ObjectiveTrackerFrame.Header.Background then
            if useMainNomTools or not showBg then
                styleHelpers.SetRegionShown(ObjectiveTrackerFrame.Header.Background, false)
            else
                styleHelpers.ApplyBlizzardHeaderBackground(ObjectiveTrackerFrame.Header)
            end
        end
        if ObjectiveTrackerFrame.Header.MinimizeButton then
            styleHelpers.ApplyHeaderButtonInset(ObjectiveTrackerFrame.Header)
            local trackerCollapsed = ObjectiveTrackerFrame.IsCollapsed and ObjectiveTrackerFrame:IsCollapsed() == true or false
            styleHelpers.ApplyMinimizeButtonChrome(ObjectiveTrackerFrame.Header.MinimizeButton, btnAppearance, trackerCollapsed)
            ObjectiveTrackerFrame.Header.MinimizeButton:SetShown(IsMinimizeButtonEnabled())
        end
    elseif not fullStyleRefresh
        and not useMainNomTools
        and ObjectiveTrackerFrame.Header
        and ObjectiveTrackerFrame.Header.Background
        and IsHeaderBackgroundShown() then
        -- Blizzard preset: reassert main header background on partial refreshes so
        -- that combat-entry layout passes (where fullStyleRefresh is false) do not
        -- leave the background hidden after Blizzard resets it.
        styleHelpers.ApplyBlizzardHeaderBackground(ObjectiveTrackerFrame.Header)
    end

    -- Blizzard preset: reassert module header backgrounds on partial refreshes when
    -- module style pass is skipped, ensuring Blizzard preset modules maintain their
    -- background texture after layout passes skip them.
    if not fullStyleRefresh and not useCategoryNomTools then
        for _, module in ipairs(GetOrderedContainerModules()) do
            local moduleHeader = module.Header
            if moduleHeader and moduleHeader.Background then
                styleHelpers.ApplyBlizzardHeaderBackground(moduleHeader)
            end
        end
    end

    if fullStyleRefresh and state.trackerButton then
        styleHelpers.ApplyFrameChrome(state.trackerButton, btnAppearance)
        styleHelpers.SetUIPanelButtonArtVisible(state.trackerButton, not useButtonNomTools)
    end

    local trackerHeaderText = ObjectiveTrackerFrame.Header and (ObjectiveTrackerFrame.Header.Text or ObjectiveTrackerFrame.Header.Title or ObjectiveTrackerFrame.Header.NomToolsTitle) or nil
    if fullStyleRefresh and trackerHeaderText then
        local mhFont = styles.mainHeaderFont or {}
        styleHelpers.SetFontAppearance(trackerHeaderText, mhFont.fontPath, mhFont.fontSize, mhFont.fontOutline)
        if mhFont.textColor then
            trackerHeaderText:SetTextColor(mhFont.textColor.r, mhFont.textColor.g, mhFont.textColor.b, mhFont.textColor.a or 1)
        end
        -- Apply per-user x/y text offset anchored to the original Blizzard position.
        local xOff = styles.mainHeaderXOffset or 0
        local yOff = styles.mainHeaderYOffset or 0
        if not trackerHeaderText.nomtoolsOriginalAnchor then
            local pt, rel, relPt, x, y = trackerHeaderText:GetPoint(1)
            if pt then
                trackerHeaderText.nomtoolsOriginalAnchor = { pt, rel, relPt, x or 0, y or 0 }
            end
        end
        if trackerHeaderText.nomtoolsOriginalAnchor then
            local a = trackerHeaderText.nomtoolsOriginalAnchor
            local finalX = a[4] + xOff
            local finalY = a[5] + yOff
            local anchorSig = tostring(a[1]) .. "|" .. tostring(a[2]) .. "|" .. tostring(a[3])
                .. "|" .. tostring(finalX) .. "|" .. tostring(finalY)
            if trackerHeaderText.nomtoolsLastAnchorSig ~= anchorSig then
                trackerHeaderText:ClearAllPoints()
                trackerHeaderText:SetPoint(a[1], a[2], a[3], finalX, finalY)
                trackerHeaderText.nomtoolsLastAnchorSig = anchorSig
            end
        end
    end

    for _, module in ipairs(GetOrderedContainerModules()) do
        local moduleState = state.moduleMarkDirtyStateByModule[module]
        local shouldStyleModule = fullStyleRefresh or (moduleState and moduleState.needsStyleRefresh)
        if shouldStyleModule then
            local moduleHeader = module.Header
            if moduleHeader then
                styleHelpers.ApplyModuleHeaderLayout(moduleHeader)
                styleHelpers.ApplyHeaderChrome(moduleHeader, catHdrAppearance)
                styleHelpers.SetUIPanelButtonArtVisible(moduleHeader, not useCategoryNomTools)
                if moduleHeader.Background then
                    if useCategoryNomTools then
                        styleHelpers.SetRegionShown(moduleHeader.Background, false)
                    else
                        styleHelpers.ApplyBlizzardHeaderBackground(moduleHeader)
                    end
                end
                if moduleHeader.MinimizeButton then
                    styleHelpers.ApplyHeaderButtonInset(moduleHeader)
                    local isCollapsed = module.IsCollapsed and module:IsCollapsed() == true or false
                    styleHelpers.ApplyMinimizeButtonChrome(moduleHeader.MinimizeButton, btnAppearance, isCollapsed)
                end
            end

            local moduleHeaderText = moduleHeader and (moduleHeader.Text or moduleHeader.Title) or nil
            if moduleHeaderText then
                local headerText = BuildQuestModuleHeaderText(module, moduleHeaderText:GetText())
                if headerText and moduleHeaderText:GetText() ~= headerText then
                    moduleHeaderText:SetText(headerText)
                end
                local chFont = styles.categoryHeaderFont or {}
                styleHelpers.SetFontAppearance(moduleHeaderText, chFont.fontPath, chFont.fontSize, chFont.fontOutline)
                if chFont.textColor then
                    moduleHeaderText:SetTextColor(chFont.textColor.r, chFont.textColor.g, chFont.textColor.b, chFont.textColor.a or 1)
                end
                local cxOff = styles.categoryHeaderXOffset or 0
                local cyOff = styles.categoryHeaderYOffset or 0
                if not moduleHeaderText.nomtoolsOriginalAnchor then
                    local pt, rel, relPt, x, y = moduleHeaderText:GetPoint(1)
                    if pt then
                        moduleHeaderText.nomtoolsOriginalAnchor = { pt, rel, relPt, x or 0, y or 0 }
                    end
                end
                if moduleHeaderText.nomtoolsOriginalAnchor then
                    local a = moduleHeaderText.nomtoolsOriginalAnchor
                    local finalX = a[4] + cxOff
                    local finalY = a[5] + cyOff
                    local anchorSig = tostring(a[1]) .. "|" .. tostring(a[2]) .. "|" .. tostring(a[3])
                        .. "|" .. tostring(finalX) .. "|" .. tostring(finalY)
                    if moduleHeaderText.nomtoolsLastAnchorSig ~= anchorSig then
                        moduleHeaderText:ClearAllPoints()
                        moduleHeaderText:SetPoint(a[1], a[2], a[3], finalX, finalY)
                        moduleHeaderText.nomtoolsLastAnchorSig = anchorSig
                    end
                end
            end

            if ShouldApplyModuleContentStyling(module) then
                currentApplyStyles = styles
                currentApplyModule = module
                -- When pendingModuleStyleRefresh is set, fullStyleRefresh is false; without
                -- forcing, only blocks with nomtoolsNeedsStyleRefresh update—unsafe after repooling.
                state.currentApplyForceBlockStyle = fullStyleRefresh
                    or (moduleState and moduleState.needsStyleRefresh == true)
                styleHelpers.ForEachModuleBlock(module, ApplyBlockStyleCallback)
                styleHelpers.ForEachModuleProgressBar(module, styleHelpers.ApplyProgressBarStyleCallback)
                state.currentApplyForceBlockStyle = nil
                currentApplyStyles = nil
                currentApplyModule = nil
            end

            state.ClearPendingModuleRefresh(module, false, true)
        end
    end

    state.pendingModuleStyleRefresh = false

    if needsRelayout then
        RequestTrackerLayoutRefresh()
    end

    state.DiagTime("applytrackerstyles.total", state.DiagNow() - _diag_t0)
end

end

end -- do: styleHelpers (part 2)

local function StyleRefreshCallback()
    local shouldCaptureCollapseStates = state.captureCollapseAfterStyleRefresh
    state.trackerStyleRefreshPending = false
    state.captureCollapseAfterStyleRefresh = false
    ApplyTrackerStyles()
    -- Clear after ApplyTrackerStyles so fullStyleRefresh sees postLayoutStyleDirty (9287).
    state.postLayoutStyleDirty = false

    if shouldCaptureCollapseStates and state.collapsePersistenceReady and not state.applyingCollapseStates then
        CaptureCurrentSectionCollapseStates()
    end
end

RequestTrackerStyleRefresh = function(captureCollapseStates, immediate)
    if captureCollapseStates then
        state.captureCollapseAfterStyleRefresh = true
    end

    if immediate then
        StyleRefreshCallback()
        return
    end

    if state.trackerStyleRefreshPending then
        return
    end

    state.trackerStyleRefreshPending = true

    if C_Timer and C_Timer.After then
        -- Schedule for next frame. Keeping this in lock-step with Blizzard's
        -- container Update pass is important to avoid visible anchor/spacing
        -- jitter: ApplyTrackerStyles re-applies header anchors and module
        -- header layout, and Blizzard's LayoutContents resets them each
        -- layout pass. If our style refresh ran at a different cadence than
        -- Blizzard's layout passes, users would see the tracker oscillate
        -- between Blizzard-default and NomTools anchoring at the delta
        -- frequency. Do NOT throttle this below 1 frame — any separation
        -- from Blizzard's Update cadence produces visible layout jitter.
        C_Timer.After(0, StyleRefreshCallback)
    else
        StyleRefreshCallback()
    end
end

-- Cheap signature describing the current tracker layout. When this is
-- unchanged between successive ObjectiveTrackerContainerMixin:Update hook
-- calls and no NomTools-driven work is pending, RunTrackerPostLayout can
-- short-circuit its anchor / scroll / style pipeline entirely. In Delves,
-- Blizzard's SCENARIO_CRITERIA_UPDATE proximity spam drives the container
-- Update path at display rate; running the full pipeline per frame is the
-- dominant cost.
local postLayoutSignatureParts = {}
local function ComputePostLayoutLayoutSignature()
    if not ObjectiveTrackerFrame then
        return nil
    end

    local containerWidth = ObjectiveTrackerFrame.GetWidth and ResolvePublicNumber(ObjectiveTrackerFrame:GetWidth()) or 0
    local containerHeight = ObjectiveTrackerFrame.GetHeight and ResolvePublicNumber(ObjectiveTrackerFrame:GetHeight()) or 0

    local parts = postLayoutSignatureParts
    for k in pairs(parts) do parts[k] = nil end
    parts[1] = tostring(math.floor((containerWidth or 0) * 10 + 0.5))
    parts[2] = tostring(math.floor((containerHeight or 0) * 10 + 0.5))
    parts[3] = tostring(state.scrollOffset or 0)
    parts[4] = state.scrollActive and "1" or "0"
    parts[5] = tostring(state.currentRightInset or 0)

    local partIndex = 5
    local modules = ObjectiveTrackerFrame.modules
    if type(modules) == "table" then
        for i = 1, #modules do
            local module = modules[i]
            if module and module.IsShown and module:IsShown() then
                local moduleHeight = module.GetHeight and ResolvePublicNumber(module:GetHeight()) or 0
                partIndex = partIndex + 1
                parts[partIndex] = tostring(math.floor((moduleHeight or 0) * 10 + 0.5))
            end
        end
    end
    partIndex = partIndex + 1
    parts[partIndex] = tostring(InCombatLockdown() and 1 or 0)

    return table.concat(parts, "|")
end

local function HasPendingPostLayoutWork(sortSettingsChanged)
    if state.postLayoutStyleDirty == true then
        state.DiagInc("hppl.postLayoutStyleDirty"); return true
    end
    if state.captureCollapseAfterStyleRefresh == true then
        state.DiagInc("hppl.captureCollapseAfterStyleRefresh"); return true
    end
    if state.pendingModuleSortRefresh == true then
        state.DiagInc("hppl.pendingModuleSortRefresh"); return true
    end
    if state.pendingModuleStyleRefresh == true then
        state.DiagInc("hppl.pendingModuleStyleRefresh"); return true
    end
    if state.pendingFullRefreshAfterCombat == true then
        state.DiagInc("hppl.pendingFullRefreshAfterCombat"); return true
    end
    if state.pendingScrollAnchor ~= nil then
        state.DiagInc("hppl.pendingScrollAnchor"); return true
    end
    if not state.savedCollapseStatesInitialized then
        state.DiagInc("hppl.savedCollapseStatesNotInit"); return true
    end
    if not state.collapsePersistenceReady then
        state.DiagInc("hppl.collapsePersistenceNotReady"); return true
    end
    if sortSettingsChanged == true then
        state.DiagInc("hppl.sortSettingsChanged"); return true
    end
    state.DiagInc("hppl.no_work")
    return false
end

-- Run NomTools' per-module sort pass for every sortable module.
--
-- This is intentionally callable from the container Update post-hook BEFORE
-- RunTrackerPostLayout's outer short-circuit, because Blizzard's
-- ObjectiveTrackerContainerMixin:LayoutContents rebuilds each module's
-- block chain (firstBlock/lastBlock/nextBlock) from `usedBlocks` on every
-- layout pass — which strips out anything we spliced in (zone dividers)
-- and reverts our custom block order back to Blizzard's default. The
-- RunTrackerPostLayout outer short-circuit (ComputePostLayoutLayoutSignature)
-- only inspects module *heights*; when Blizzard fires Update spam during
-- Delve/scenario movement and module heights happen to be unchanged, the
-- short-circuit returns before the inner sort loop ever runs, leaving the
-- Quests section visibly snapping back to default order (zone headers
-- vanishing, then reappearing the next time something invalidates the
-- height signature). That is the source of the "Quests section freaks
-- out when group by zone is enabled" jitter pattern.
--
-- Each call into ns.SortModuleBlocks short-circuits cheaply via the
-- per-module nomtoolsLastSortSignature compare when the chain is already
-- in our order, so this function is effectively a no-op when nothing
-- changed — the cost is one BuildChainSignature per sortable module.
local function RunSortableModulesSortPass()
    if not IsModuleEnabled() or state.sortingInProgress then
        return false
    end

    local sortingApplied = false
    state.sortingInProgress = true

    -- One table per pass, wiped here: GetQuestZoneName is invoked from both
    -- BuildChainSignature (pre-sort) and GetBlockSortData (sort) for the same
    -- quest IDs when group-by-zone or zone-aware signatures run — without a
    -- per-pass cache that duplicates the heaviest API work ~2x per block.
    local qzCache = state.questZoneNameSortPassCache
    if not qzCache then
        qzCache = {}
        state.questZoneNameSortPassCache = qzCache
    elseif wipe then
        wipe(qzCache)
    else
        for k in pairs(qzCache) do
            qzCache[k] = nil
        end
    end

    local sortOk, sortErr = pcall(function()
        for _, module in ipairs(GetOrderedContainerModules()) do
            if module and not module.nomtoolsSearchFrame then
                if ns.SortModuleBlocks(module) then
                    sortingApplied = true
                end
                state.ClearPendingModuleRefresh(module, true, false)
            end
        end
    end)

    state.questZoneNameSortPassCache = nil
    state.sortingInProgress = false
    state.pendingModuleSortRefresh = false
    if state.GetSortSettingsSignature then
        state.lastSortSettingsSignature = state.GetSortSettingsSignature()
    end

    if not sortOk and ns.IsDebugEnabled and ns.IsDebugEnabled() then
        ns.DebugPrint("Sort error: " .. tostring(sortErr))
    end

    return sortingApplied
end

local function RunTrackerPostLayout(container)
    if container ~= ObjectiveTrackerFrame then
        return
    end

    if not state.savedCollapseStatesInitialized then
        local changed = ApplySavedSectionCollapseStates()
        if changed then
            RequestTrackerLayoutRefresh()
        else
            state.collapsePersistenceReady = true
        end
    elseif not state.collapsePersistenceReady then
        state.collapsePersistenceReady = true
    end

    if IsModuleEnabled() then
        RefreshTrackerHeader()
    end

    RestorePendingScrollAnchor()

    if not IsModuleEnabled() then
        RefreshScrollState()
        ns.RefreshFilterButton()
        RefreshTrackerButton()
        RefreshQuestLogButton()
        return
    end

    EnsureScenarioStyleHookInstalled()

    do
        local sortSettingsSignature = state.GetSortSettingsSignature()
        local sortSettingsChanged = state.lastSortSettingsSignature ~= sortSettingsSignature
        if state.sortingInProgress then
            state.DiagInc("rpl.skip_sortingInProgress")
        end

        if not state.sortingInProgress then
            local layoutSignature = ComputePostLayoutLayoutSignature()
            local signatureUnchanged = layoutSignature ~= nil
                and state.lastPostLayoutLayoutSignature == layoutSignature

            -- Pending-work classification:
            --   "structural" work *requires* a full RunTrackerPostLayout pass
            --   (sort order, anchors, scroll, collapse persistence, combat
            --   deferred refresh, sort settings change).
            --   "style-only" work only needs ApplyTrackerStyles, which runs
            --   asynchronously via RequestTrackerStyleRefresh.
            -- When the layout signature is unchanged we can safely short-
            -- circuit the full RPL pass even with style-only work pending,
            -- as long as we make sure the style refresh is scheduled.
            local hasStructuralWork =
                state.captureCollapseAfterStyleRefresh == true
                or state.pendingModuleSortRefresh == true
                or state.pendingFullRefreshAfterCombat == true
                or state.pendingScrollAnchor ~= nil
                or not state.savedCollapseStatesInitialized
                or not state.collapsePersistenceReady
                or sortSettingsChanged == true

            if signatureUnchanged then
                if not hasStructuralWork then
                    -- Schedule async style refresh if any per-block style
                    -- work is pending; safe no-op if already scheduled.
                    if (state.postLayoutStyleDirty == true
                        or state.pendingModuleStyleRefresh == true)
                        and RequestTrackerStyleRefresh then
                        state.DiagInc("rpl.shortcircuit_scheduled_style")
                        RequestTrackerStyleRefresh(false, false)
                    end
                    state.DiagInc("runtrackerpostlayout.shortcircuited")
                    return
                end
                state.DiagInc("rpl.skip_structural_work")
            else
                state.DiagInc("rpl.signature_changed")
                state.lastPostLayoutLayoutSignature = layoutSignature
            end
        end

        -- Touch HPPL counters even when not used for short-circuit so we
        -- still see which flags are true on full passes.
        HasPendingPostLayoutWork(sortSettingsChanged)
    end

    state.DiagInc("runtrackerpostlayout.full")

    local pendingModuleStyleRefresh = state.HasPendingModuleStyleRefresh()
    -- Run the per-module sort pass. The container Update post-hook also
    -- calls this BEFORE entering RunTrackerPostLayout so that Blizzard's
    -- chain clobber is repaired even when the outer signature short-
    -- circuit returns early. Calling it again here is cheap (per-module
    -- signature short-circuit) and keeps the contract for any non-hook
    -- caller of RunTrackerPostLayout: the tracker exits this function in
    -- a fully sorted, anchored state.
    RunSortableModulesSortPass()
    ApplyModuleAnchors()
    RefreshScrollState()

    ns.RefreshFilterButton()
    RefreshTrackerButton()
    RefreshQuestLogButton()
    RefreshTrackerHeader()

    if state.postLayoutStyleDirty or state.captureCollapseAfterStyleRefresh or pendingModuleStyleRefresh then
        if InCombatLockdown() then
            QueueCombatDeferredObjectiveTrackerMutation(false, true)
            return
        end

        -- Apply styles only when tracker content actually changed.
        -- Blizzard's container can update for reasons other than fresh block
        -- content, and restyling the entire tracker on every one of those idle
        -- updates creates large transient allocation churn.

        -- Re-apply zone divider and search bar settings when styles are dirty
        if ns.RefreshZoneDividerAndSearchStyles then
            ns.RefreshZoneDividerAndSearchStyles()
        end

        RequestTrackerStyleRefresh(true)
    end

    -- DO NOT recompute and store the signature here at RPL exit. The
    -- signature computed now reflects module heights AFTER our sort /
    -- anchor / style pipeline has modified them, whereas the next
    -- RunTrackerPostLayout invocation will measure heights AFTER
    -- Blizzard's next LayoutContents pass — which re-anchors modules
    -- from the *content* state and therefore produces a different
    -- signature than our post-work one. Storing the exit signature
    -- here made the entry-side short-circuit at line ~9266 fail on
    -- every single subsequent pass, defeating the whole point of the
    -- short-circuit (observed as `rpl.signature_changed` firing at
    -- display rate). The signature IS already stored at entry on the
    -- "signature changed" branch, which is the correct baseline for
    -- the next comparison.
end

RefreshObjectiveTrackerDisplay = function(immediateStyleRefresh)
    state.DiagInc("refreshobjectivetrackerdisplay.calls")
    if IsModuleEnabled() then
        RefreshTrackerHeader()
    end

    RefreshScrollClipFrame()
    RefreshScrollState()
    ns.RefreshFilterButton()
    RefreshTrackerButton()
    RefreshQuestLogButton()

    if not IsModuleEnabled() then
        return
    end

    RefreshTrackerHeader()

    if immediateStyleRefresh and ns.RefreshZoneDividerAndSearchStyles then
        ns.RefreshZoneDividerAndSearchStyles()
    end

    RequestTrackerStyleRefresh(false, immediateStyleRefresh == true)
end

local function HandleTrackerFrameSizeChanged(self, width, height)
    if not self then
        return
    end

    state.DiagInc("trackerframe.sizechanged.calls")

    local resolvedWidth = ResolvePublicNumber(width)
    if resolvedWidth == nil and self.GetWidth then
        resolvedWidth = ResolvePublicNumber(self:GetWidth())
    end

    local resolvedHeight = ResolvePublicNumber(height)
    if resolvedHeight == nil and self.GetHeight then
        resolvedHeight = ResolvePublicNumber(self:GetHeight())
    end

    resolvedWidth = resolvedWidth or 0
    resolvedHeight = resolvedHeight or 0
    local lastWidth = state.lastTrackerSizeWidth
    local lastHeight = state.lastTrackerSizeHeight
    local sizeEpsilon = 0.5

    state.lastTrackerSizeWidth = resolvedWidth
    state.lastTrackerSizeHeight = resolvedHeight

    if state.nomToolsEnforcingHeight then
        return
    end

    if lastWidth ~= nil
        and math.abs(resolvedWidth - lastWidth) <= sizeEpsilon
        and lastHeight ~= nil
        and math.abs(resolvedHeight - lastHeight) <= sizeEpsilon
        and not state.postLayoutStyleDirty
        and not state.trackerStyleRefreshPending
    then
        state.DiagInc("trackerframe.sizechanged.skipped")
        return
    end

    state.DiagInc("trackerframe.sizechanged.refreshed")
    RefreshObjectiveTrackerDisplay(true)
end

local function GetMinimapEffectiveWidth()
    local mm = Minimap
    if not mm then return nil end
    local w = mm.GetWidth and mm:GetWidth() or nil
    if not w or w <= 0 then return nil end
    -- Convert Minimap logical width to OTF coordinate space accounting for any scale differences
    -- (e.g. another addon making the minimap square or changing its scale).
    local mmScale  = mm:GetEffectiveScale()
    local otfScale = (ObjectiveTrackerFrame and ObjectiveTrackerFrame:GetEffectiveScale()) or UIParent:GetEffectiveScale()
    if mmScale and mmScale > 0 and otfScale and otfScale > 0 then
        w = w * mmScale / otfScale
    end
    return w
end

do -- OT position helpers (scoped to avoid the 200-local-per-chunk limit)

local OT_POSITION_CONFIG_KEY = "objectiveTracker"

-- Returns the position config table {point, x, y} from NomTools edit mode config.
local function GetOTPositionConfig(layoutName)
    local defaults = ns.DEFAULTS and ns.DEFAULTS.editMode and ns.DEFAULTS.editMode.objectiveTracker
        or { point = "RIGHT", x = -5, y = 0 }
    if not ns.GetEditModeConfig then return defaults end
    return ns.GetEditModeConfig(OT_POSITION_CONFIG_KEY, defaults, layoutName)
end

-- Apply the saved position from edit mode config to ObjectiveTrackerFrame.
-- Skips when minimap-attached (minimap mode owns position) or during combat.
-- Sets the reentrancy guard so the SetPoint hook does not recurse.
local function ApplyTrackerPosition(layoutName)
    if not ObjectiveTrackerFrame then return end
    if InCombatLockdown() then return end
    local settings = GetSettings()
    local layout = settings.layout or {}
    if layout.attachToMinimap == true then return end
    local config = GetOTPositionConfig(layoutName)
    local pt = config.point or "RIGHT"
    local px = config.x or -5
    local py = config.y or 0
    state.nomToolsEnforcingPosition = true
    ObjectiveTrackerFrame:ClearAllPoints()
    ObjectiveTrackerFrame:SetPoint(pt, UIParent, pt, px, py)
    state.nomToolsEnforcingPosition = false
end

ns.GetObjectiveTrackerPositionConfig = GetOTPositionConfig
ns.ApplyObjectiveTrackerPosition     = ApplyTrackerPosition

end -- do: OT position helpers

local function ApplyTrackerDimensions(positionLayoutName)
    if not ObjectiveTrackerFrame then return end
    if InCombatLockdown() then
        QueueCombatDeferredObjectiveTrackerMutation(true, false)
        return
    end

    local sizeEpsilon = 0.5
    local forceReplay = state.needsTrackerDimensionReplay == true
    local settings = GetSettings()
    local layout = settings.layout or {}
    local attachToMinimap = layout.attachToMinimap == true
    local desiredHeight = layout.height or 800
    local desiredAttachEdge = layout.minimapAttachEdge
    local desiredYOffset = tonumber(layout.minimapYOffset) or 0
    local desiredConfiguredWidth = layout.width or 235
    local desiredMatchMinimapWidth = layout.matchMinimapWidth == true

    local desiredStandaloneWidth
    local desiredStandaloneLayoutName
    local desiredStandalonePoint
    local desiredStandaloneX
    local desiredStandaloneY
    if not attachToMinimap then
        if desiredMatchMinimapWidth and Minimap then
            local mmW = GetMinimapEffectiveWidth()
            if mmW then
                desiredStandaloneWidth = math.floor(mmW - K.TRACKER_VISUAL_LEFT_EXTENSION - TRACKER_MODULE_HEADER_RIGHT_EXTENSION + 0.5)
                if desiredStandaloneWidth < 60 then
                    desiredStandaloneWidth = nil
                end
            end
        end
        desiredStandaloneWidth = desiredStandaloneWidth or desiredConfiguredWidth

        desiredStandaloneLayoutName = ns.GetEditModeLayoutName and ns.GetEditModeLayoutName(positionLayoutName) or positionLayoutName
        if desiredStandaloneLayoutName == nil then
            desiredStandaloneLayoutName = ""
        end

        local positionConfig = ns.GetObjectiveTrackerPositionConfig and ns.GetObjectiveTrackerPositionConfig(desiredStandaloneLayoutName)
            or { point = "RIGHT", x = -5, y = 0 }
        desiredStandalonePoint = positionConfig.point or "RIGHT"
        desiredStandaloneX = tonumber(positionConfig.x) or -5
        desiredStandaloneY = tonumber(positionConfig.y) or 0
    end

    local signatureChanged = (attachToMinimap ~= state.lastApplied_attachToMinimap)
        or (desiredHeight ~= state.lastApplied_desiredHeight)
        or (desiredAttachEdge ~= state.lastApplied_desiredAttachEdge)
        or (desiredYOffset ~= state.lastApplied_desiredYOffset)
        or (desiredMatchMinimapWidth ~= state.lastApplied_desiredMatchMinimapWidth)
        or (desiredConfiguredWidth ~= state.lastApplied_desiredConfiguredWidth)
        or (desiredStandaloneWidth ~= state.lastApplied_desiredStandaloneWidth)
        or (desiredStandaloneLayoutName ~= state.lastApplied_standaloneLayoutName)
        or (desiredStandalonePoint ~= state.lastApplied_standalonePoint)
        or (desiredStandaloneX ~= state.lastApplied_standaloneX)
        or (desiredStandaloneY ~= state.lastApplied_standaloneY)
    local shouldApplyLayout = forceReplay or signatureChanged

    local currentHeight = ObjectiveTrackerFrame.GetHeight and ResolvePublicNumber(ObjectiveTrackerFrame:GetHeight()) or nil
    if forceReplay or currentHeight == nil or math.abs(currentHeight - desiredHeight) > sizeEpsilon then
        state.nomToolsEnforcingHeight = true
        ObjectiveTrackerFrame:SetHeight(desiredHeight)
        state.nomToolsEnforcingHeight = false
    end

    -- While edit mode is active, LibEditMode owns OTF's position.
    if ns.isEditMode then return end

    -- OTF inherits EditModeObjectiveTrackerSystemTemplate.  Blizzard's
    -- ApplySystemAnchor can reparent OTF into UIParentRightManagedFrameContainer
    -- and let the container's Layout() manage its position.  Break out of that
    -- so NomTools controls placement exclusively.
    ObjectiveTrackerFrame.ignoreFramePositionManager = true
    if shouldApplyLayout then
        if UIParentRightManagedFrameContainer
            and UIParentRightManagedFrameContainer.RemoveManagedFrame then
            UIParentRightManagedFrameContainer:RemoveManagedFrame(ObjectiveTrackerFrame)
        end
    end
    if forceReplay or ObjectiveTrackerFrame:GetParent() ~= UIParent then
        state.nomToolsEnforcingPosition = true
        ObjectiveTrackerFrame:SetParent(UIParent)
        state.nomToolsEnforcingPosition = false
    end

    if attachToMinimap and Minimap then
        -- Minimap-attached mode: NomTools owns position entirely.
        if shouldApplyLayout then
            state.nomToolsEnforcingPosition = true
            ObjectiveTrackerFrame:ClearAllPoints()
            local yOff = -desiredYOffset
            if desiredMatchMinimapWidth then
                if desiredAttachEdge == "top" then
                    ObjectiveTrackerFrame:SetPoint("BOTTOMLEFT",  Minimap, "TOPLEFT",  K.TRACKER_VISUAL_LEFT_EXTENSION,       -yOff)
                    ObjectiveTrackerFrame:SetPoint("BOTTOMRIGHT", Minimap, "TOPRIGHT", -TRACKER_MODULE_HEADER_RIGHT_EXTENSION, -yOff)
                else
                    ObjectiveTrackerFrame:SetPoint("TOPLEFT",  Minimap, "BOTTOMLEFT",  K.TRACKER_VISUAL_LEFT_EXTENSION,       yOff)
                    ObjectiveTrackerFrame:SetPoint("TOPRIGHT", Minimap, "BOTTOMRIGHT", -TRACKER_MODULE_HEADER_RIGHT_EXTENSION, yOff)
                end
            else
                local currentWidth = ObjectiveTrackerFrame.GetWidth and ResolvePublicNumber(ObjectiveTrackerFrame:GetWidth()) or nil
                if forceReplay or currentWidth == nil or math.abs(currentWidth - desiredConfiguredWidth) > sizeEpsilon then
                    ObjectiveTrackerFrame:SetWidth(desiredConfiguredWidth)
                end
                if desiredAttachEdge == "top" then
                    ObjectiveTrackerFrame:SetPoint("BOTTOMRIGHT", Minimap, "TOPRIGHT", -TRACKER_MODULE_HEADER_RIGHT_EXTENSION, -yOff)
                else
                    ObjectiveTrackerFrame:SetPoint("TOPRIGHT", Minimap, "BOTTOMRIGHT", -TRACKER_MODULE_HEADER_RIGHT_EXTENSION, yOff)
                end
            end
            state.nomToolsEnforcingPosition = false
        end
    else
        local widthToApply = desiredStandaloneWidth or desiredConfiguredWidth
        local currentWidth = ObjectiveTrackerFrame.GetWidth and ResolvePublicNumber(ObjectiveTrackerFrame:GetWidth()) or nil
        if forceReplay or currentWidth == nil or math.abs(currentWidth - widthToApply) > sizeEpsilon then
            ObjectiveTrackerFrame:SetWidth(widthToApply)
        end

        local currentPoint, _, currentRelativePoint, currentX, currentY = ObjectiveTrackerFrame:GetPoint(1)
        local positionMismatch = currentPoint ~= desiredStandalonePoint
            or currentRelativePoint ~= desiredStandalonePoint
            or math.abs((tonumber(currentX) or 0) - desiredStandaloneX) > sizeEpsilon
            or math.abs((tonumber(currentY) or 0) - desiredStandaloneY) > sizeEpsilon
        if positionMismatch then
            shouldApplyLayout = true
        end

        -- Apply position from NomTools edit mode config.
        if shouldApplyLayout then
            ns.ApplyObjectiveTrackerPosition(desiredStandaloneLayoutName)
        end
    end

    state.lastApplied_attachToMinimap = attachToMinimap
    state.lastApplied_desiredHeight = desiredHeight
    state.lastApplied_desiredAttachEdge = desiredAttachEdge
    state.lastApplied_desiredYOffset = desiredYOffset
    state.lastApplied_desiredMatchMinimapWidth = desiredMatchMinimapWidth
    state.lastApplied_desiredConfiguredWidth = desiredConfiguredWidth
    state.lastApplied_desiredStandaloneWidth = desiredStandaloneWidth
    state.lastApplied_standaloneLayoutName = desiredStandaloneLayoutName
    state.lastApplied_standalonePoint = desiredStandalonePoint
    state.lastApplied_standaloneX = desiredStandaloneX
    state.lastApplied_standaloneY = desiredStandaloneY
    state.needsTrackerDimensionReplay = false
end

-- Scroll mode needs a height budget large enough that Blizzard lays out full module
-- content (for scroll metrics), but a flat 10k minimum is very expensive. We use the
-- clip viewport + slack, the previous pass content height from BuildVisibleModuleLayout,
-- and Blizzard's own value, capped at TRACKER_MAX_LAYOUT_HEIGHT.
local function ComputeNomToolsScrollAvailableHeightBudget(originalAvailable)
    local visible = GetTrackerVisibleScrollHeight()
    local lastContent = state.lastContentHeight or 0
    local slack = K.TRACKER_LAYOUT_INITIAL_BUDGET + K.TRACKER_LAYOUT_BUDGET_PADDING

    local floorBudget = visible + slack
    if lastContent > 0 then
        floorBudget = math.max(floorBudget, lastContent + K.TRACKER_LAYOUT_BUDGET_PADDING)
    end
    if type(originalAvailable) == "number" and originalAvailable > 0 then
        floorBudget = math.max(floorBudget, originalAvailable)
    end

    return math.min(math.max(floorBudget, 0), K.TRACKER_MAX_LAYOUT_HEIGHT or 10000)
end

local function InstallFrameHooks()
    if state.frameHooksInstalled or not ObjectiveTrackerFrame then
        return
    end

    state.frameHooksInstalled = true

    if ObjectiveTrackerFrame.GetAvailableHeight and not state.originalTrackerGetAvailableHeight then
        state.originalTrackerGetAvailableHeight = ObjectiveTrackerFrame.GetAvailableHeight
        ObjectiveTrackerFrame.GetAvailableHeight = function(self)
            local original = state.originalTrackerGetAvailableHeight
            local availableHeight = original and original(self) or 0
            if not IsModuleEnabled() or not IsScrollEnabled() then
                return availableHeight
            end

            return ComputeNomToolsScrollAvailableHeightBudget(availableHeight)
        end
    end

    if ObjectiveTrackerFrame.Update then
        hooksecurefunc(ObjectiveTrackerFrame, "Update", function(self)
            PrepareModulesForLayout(self)
        end)
    end

    -- After every SetHeight call (including Blizzard's edit-mode layout apply which collapses
    -- the frame to content height), re-enforce NomTools's desired height.  The reentrancy guard
    -- prevents our own corrective call from looping.
    hooksecurefunc(ObjectiveTrackerFrame, "SetHeight", function(self, newHeight)
        if state.nomToolsEnforcingHeight or not IsModuleEnabled() then return end
        local desiredHeight = GetSettings().layout.height or 800
        local resolvedHeight = ResolvePublicNumber(newHeight)
        if resolvedHeight == nil and self and self.GetHeight then
            resolvedHeight = ResolvePublicNumber(self:GetHeight())
        end
        if resolvedHeight == nil or math.abs(resolvedHeight - desiredHeight) > 0.5 then
            state.nomToolsEnforcingHeight = true
            self:SetHeight(desiredHeight)
            state.nomToolsEnforcingHeight = false
        end
    end)

    -- Blizzard's edit-mode system (EditModeObjectiveTrackerSystemTemplate) repositions OTF
    -- asynchronously on reload and during layout updates, overriding NomTools.  This hook
    -- re-asserts NomTools position after any external SetPoint call.  Skipped when LEM
    -- owns the position (edit mode active), when NomTools module is disabled, or when
    -- NomTools itself is setting the position (reentrancy guard).
    hooksecurefunc(ObjectiveTrackerFrame, "SetPoint", function(self)
        if state.nomToolsEnforcingPosition or ns.isEditMode or not IsModuleEnabled() then return end
        if InCombatLockdown() then return end
        local settings = GetSettings()
        local layout = settings.layout or {}
        if layout.attachToMinimap == true then return end
        state.nomToolsEnforcingPosition = true
        ns.ApplyObjectiveTrackerPosition()
        state.nomToolsEnforcingPosition = false
    end)

    -- Hook Blizzard's ApplySystemAnchor (fired via EDIT_MODE_LAYOUTS_UPDATED on
    -- login/reload and whenever the active edit-mode layout changes).  Path A of
    -- that method reparents OTF into UIParentRightManagedFrameContainer and clears
    -- ignoreFramePositionManager; Path B calls SetPoint directly.  Either way, we
    -- must re-break OTF from the container and re-apply NomTools position.
    if ObjectiveTrackerFrame.ApplySystemAnchor then
        hooksecurefunc(ObjectiveTrackerFrame, "ApplySystemAnchor", function(self)
            if ns.isEditMode or not IsModuleEnabled() then return end
            if InCombatLockdown() then return end
            self.ignoreFramePositionManager = true
            if UIParentRightManagedFrameContainer
                and UIParentRightManagedFrameContainer.RemoveManagedFrame then
                UIParentRightManagedFrameContainer:RemoveManagedFrame(self)
            end
            if self:GetParent() ~= UIParent then
                state.nomToolsEnforcingPosition = true
                self:SetParent(UIParent)
                state.nomToolsEnforcingPosition = false
            end
            ApplyTrackerDimensions()
        end)
    end

    ObjectiveTrackerFrame:EnableMouseWheel(true)
    ObjectiveTrackerFrame:HookScript("OnMouseWheel", OnTrackerMouseWheel)
    ObjectiveTrackerFrame:HookScript("OnSizeChanged", function(self, width, height)
        HandleTrackerFrameSizeChanged(self, width, height)
    end)

    hooksecurefunc(ObjectiveTrackerContainerMixin, "Update", function(container)
        if container ~= ObjectiveTrackerFrame then
            return
        end

        state.DiagInc("container.update.posthook")

        if state.inPostLayout then
            state.DiagInc("container.update.reentrant_skip")
            return
        end

        state.SyncDelveScenarioWidgetTestRemoval()

        -- Blizzard's ObjectiveTrackerContainerMixin:Update runs ClearAllPoints
        -- on every module in container.modules and rebuilds the anchor chain
        -- as TOP -> previousBlizzardModule:BOTTOM, anchored from
        -- ObjectiveTrackerFrame.TOP at -topModulePadding. That bypasses:
        --   * the search reserve we add to clipFrame's TOP offset
        --   * frames we splice into the visual order (zone dividers,
        --     focused-quest module) which are NOT in container.modules
        --   * our moduleSpacing / leftMargin handling
        --
        -- We must therefore unconditionally re-apply our anchors on every
        -- Blizzard Update, BEFORE entering RunTrackerPostLayout — because
        -- RunTrackerPostLayout has its own layout-signature short-circuit
        -- (ComputePostLayoutLayoutSignature only inspects module *heights*,
        -- not anchor positions) and will happily skip the entire pass when
        -- heights are unchanged. If we relied on RunTrackerPostLayout to
        -- re-anchor for us, the common case of "Blizzard fires Update due
        -- to scenario/POI spam during movement, no module heights change"
        -- would leave modules sitting at Blizzard's TOP-topModulePadding
        -- positions until something else invalidated the height signature
        -- — producing the visible jitter pattern of content snapping up
        -- (Blizzard's positions, overlapping/touching the search bar)
        -- and down (our positions, with the search reserve below it)
        -- repeatedly while moving.
        --
        -- ApplyModuleAnchors itself has a cheap topology-signature short-
        -- circuit, and we just nil'd the cached signature, so it will do
        -- the full ClearAllPoints+SetPoint chain this pass and then any
        -- subsequent ApplyModuleAnchors call inside RunTrackerPostLayout
        -- will short-circuit harmlessly.
        --
        -- The sort pass MUST run before ApplyModuleAnchors: Blizzard's
        -- LayoutContents rebuilt each module's block chain from
        -- `usedBlocks`, which strips out our spliced-in zone divider
        -- blocks and reverts the per-block order. ApplyModuleAnchors
        -- positions modules but does NOT touch intra-module block order,
        -- so without re-running the sort first we'd anchor the modules
        -- correctly but their internal contents would still be in
        -- Blizzard's default order with no dividers — producing the
        -- "Quests section freaks out / zone headers disappear" jitter.
        -- RunSortableModulesSortPass is cheap (per-module signature
        -- short-circuit) when the chain is already in our order, so the
        -- common idle case is essentially free.
        local tSort0 = state.diag.enabled and state.DiagNow() or nil
        RunSortableModulesSortPass()
        if tSort0 and state.diag.enabled then
            state.DiagTime("posthook.sortpass", state.DiagNow() - tSort0)
        end
        state.lastModuleAnchorSignature = nil
        local tAnchor0 = state.diag.enabled and state.DiagNow() or nil
        ApplyModuleAnchors()
        if tAnchor0 and state.diag.enabled then
            state.DiagTime("posthook.applyanchors", state.DiagNow() - tAnchor0)
        end

        state.inPostLayout = true
        local t0 = state.DiagNow()
        local ok, err = pcall(RunTrackerPostLayout, container)
        state.DiagTime("runtrackerpostlayout.total", state.DiagNow() - t0)
        state.inPostLayout = false
        if not ok and ns.IsDebugEnabled and ns.IsDebugEnabled() then
            ns.DebugPrint("PostLayout error: " .. tostring(err))
        end
    end)

    -- Hook Header.Show so Blizzard re-shows can be immediately suppressed when the
    -- player has disabled the header or minimize button in NomTools options.
    if ObjectiveTrackerFrame.Header then
        hooksecurefunc(ObjectiveTrackerFrame.Header, "Show", function(self)
            if not IsModuleEnabled() then
                return
            end

            if not IsMainHeaderEnabled() then
                self:Hide()
            end
        end)
        if ObjectiveTrackerFrame.Header.MinimizeButton then
            hooksecurefunc(ObjectiveTrackerFrame.Header.MinimizeButton, "Show", function(self)
                if not IsModuleEnabled() then
                    return
                end

                if not IsMinimizeButtonEnabled() then
                    self:Hide()
                end
            end)
        end
    end
end

do -- OT edit mode registration (scoped to avoid the 200-local-per-chunk limit)

local registeredWithLEM = false

local function RegisterWithEditMode()
    if registeredWithLEM or not ObjectiveTrackerFrame or not ns.RegisterEditModeFrame then
        return
    end

    local defaults = ns.DEFAULTS and ns.DEFAULTS.editMode and ns.DEFAULTS.editMode.objectiveTracker
        or { point = "RIGHT", x = -5, y = 0 }

    registeredWithLEM = ns.RegisterEditModeFrame(ObjectiveTrackerFrame, {
        label = "Objective Tracker",
        defaults = {
            point = defaults.point,
            x = defaults.x,
            y = defaults.y,
        },
        applyLayout = function(layoutName)
            ApplyTrackerDimensions(layoutName)
        end,
        onPositionChanged = function(layoutName, point, x, y)
            local config = ns.GetObjectiveTrackerPositionConfig(layoutName)
            config.point = point
            config.x = x
            config.y = y
            if ns.RefreshObjectiveTrackerOptionsSliders then
                ns.RefreshObjectiveTrackerOptionsSliders()
            end
        end,
    }) == true
end

ns.RegisterObjectiveTrackerEditMode = RegisterWithEditMode

end -- do: OT edit mode registration

RefreshObjectiveTrackerState = function(refreshMode)
    if not EnsureObjectiveTrackerLoaded() then
        return
    end

    InstallTrackerHooks()
    InstallBlockHooks()
    InstallCollapseHooks()
    InstallFrameHooks()
    ApplyTrackerDimensions()
    if ns.RegisterObjectiveTrackerEditMode then
        ns.RegisterObjectiveTrackerEditMode()
    end
    RefreshScrollClipFrame()

    if refreshMode == "soft" then
        state.postLayoutStyleDirty = true
        RefreshObjectiveTrackerDisplay(true)
        RequestTrackerLayoutRefresh()
        return
    end

    if InCombatLockdown() then
        state.pendingFullRefreshAfterCombat = true
        QueueCombatDeferredObjectiveTrackerMutation(false, false)
        return
    end

    local focusedQuestModule = EnsureFocusedQuestModule()
    local zoneModule = EnsureZoneModule()
    local runtimeSettings = GetSettings()
    local suppressionSettings = runtimeSettings.filter or {}
    state.moduleRuntimeEnabled = IsModuleEnabled(runtimeSettings)
    state.zoneSuppressionConfigured = suppressionSettings.disableZoneFeaturesInInstances == true
    local previousSuppressionState = state.zoneFeaturesSuppressedInCurrentInstance
    state.zoneFeaturesSuppressedInCurrentInstance = nil
    local suppressZoneFeatures = IsZoneFeaturesSuppressedInCurrentInstance()
    local suppressionStateChanged = previousSuppressionState ~= suppressZoneFeatures
    state.zoneFeaturesSuppressedInCurrentInstance = suppressZoneFeatures

    if suppressZoneFeatures and zoneModule then
        StopZoneTaskRetry(zoneModule)
    end

    if (suppressZoneFeatures or suppressionStateChanged) and ClearSortableModuleZoneDividerArtifacts then
        ClearSortableModuleZoneDividerArtifacts()
    end

    if IsModuleEnabled() and IsSearchEnabled() then
        ns.EnsureSearchFrame()
    end

    if not state.suppressionWatcherRegistered then
        state.suppressionWatcherRegistered = true
        state.suppressionWatcherFrame = state.suppressionWatcherFrame or CreateFrame("Frame")
        state.suppressionWatcherFrame:SetScript("OnEvent", function()
            state.SyncDelveScenarioWidgetTestRemoval()
            local previousSuppressionState = state.zoneFeaturesSuppressedInCurrentInstance
            state.zoneFeaturesSuppressedInCurrentInstance = nil
            local suppressZoneFeatures = IsZoneFeaturesSuppressedInCurrentInstance()
            local suppressionStateChanged = previousSuppressionState ~= suppressZoneFeatures
            state.zoneFeaturesSuppressedInCurrentInstance = suppressZoneFeatures

            -- When suppression flips ON (e.g. user enters a delve / dungeon /
            -- raid / scenario), hide the Zone module *immediately*, even
            -- during combat lockdown. This matters because:
            --   * Entering an instance very often coincides with combat (the
            --     player loads in mid-combat or aggros immediately).
            --   * RefreshObjectiveTrackerState bails early when
            --     InCombatLockdown() is true, deferring the full refresh
            --     (including SetCustomModuleAttached(zoneModule, false))
            --     until PLAYER_REGEN_ENABLED. Without an immediate hide the
            --     Zone section stays visibly attached for the entire combat
            --     duration — that is the "Zone section is enabled inside
            --     instances despite the toggle" report.
            -- Hide() is unprotected on this frame (we created it via
            -- CreateFrame("Frame") with no inherit template), so it is safe
            -- in combat. The eventual deferred RefreshObjectiveTrackerState
            -- will perform the full RemoveModule / event unregistration once
            -- combat ends.
            if suppressZoneFeatures and state.zoneModule then
                if state.zoneModule.SetHeight then
                    state.zoneModule:SetHeight(0)
                end
                if state.zoneModule.Hide then
                    state.zoneModule:Hide()
                end
            end

            if suppressionStateChanged and ns.RefreshObjectiveTrackerUI then
                ns.RefreshObjectiveTrackerUI("full")
            end
        end)
        state.suppressionWatcherFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
        state.suppressionWatcherFrame:RegisterEvent("ZONE_CHANGED")
        state.suppressionWatcherFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
        -- Delves: IsInInstance is often false; delve state can change without a
        -- classic zone transition. These fire when delve participation updates.
        pcall(function()
            state.suppressionWatcherFrame:RegisterEvent("ACTIVE_DELVE_DATA_UPDATE")
        end)
        pcall(function()
            state.suppressionWatcherFrame:RegisterEvent("WALK_IN_DATA_UPDATE")
        end)
    end

    local function SetCustomModuleAttached(module, shouldAttach)
        if not module then
            return
        end

        local hasModule = ObjectiveTrackerFrame.HasModule and ObjectiveTrackerFrame:HasModule(module)
        if shouldAttach then
            RegisterCustomModuleEvents(module)
            if not hasModule then
                if ObjectiveTrackerManager and ObjectiveTrackerManager.SetModuleContainer then
                    ObjectiveTrackerManager:SetModuleContainer(module, ObjectiveTrackerFrame)
                end
                hasModule = ObjectiveTrackerFrame:HasModule(module)
                if not hasModule then
                    ObjectiveTrackerFrame:AddModule(module)
                end
            end
            if module.Show then
                module:Show()
            end
            if module.MarkDirty then
                module:MarkDirty()
            end
            return
        end

        UnregisterCustomModuleEvents(module)

        if hasModule and ObjectiveTrackerFrame.RemoveModule then
            ObjectiveTrackerFrame:RemoveModule(module)
        end

        StopZoneTaskRetry(module)
        StopModuleTicker(module)
        module.tickerSeconds = 0

        if module.Hide then
            module:Hide()
        end
        if module.SetHeight then
            module:SetHeight(0)
        end
    end

    if IsModuleEnabled() then
        SetCustomModuleAttached(focusedQuestModule, IsFocusedQuestEnabled())
        SetCustomModuleAttached(zoneModule, not suppressZoneFeatures)
        TagKnownModules()
        ApplyCategoryOrder()
        RefreshTrackerHeader()
    else
        SetCustomModuleAttached(focusedQuestModule, false)
        SetCustomModuleAttached(zoneModule, false)
        RestoreOriginalModuleOrder()
        RestoreBlizzardModuleAnchors(true)
        state.searchText = ""
        if state.searchEditBox then
            state.searchEditBox:SetText("")
        end
        if state.searchFrame then
            state.searchFrame:Hide()
        end
        if state.filterDropFrame then state.filterDropFrame:Hide() end
        if state.filterDropCloseListener then state.filterDropCloseListener:Hide() end
        if state.filterButton then state.filterButton:Hide() end
    end

    state.postLayoutStyleDirty = true

    if ObjectiveTrackerManager and ObjectiveTrackerManager.UpdateAll then
        ObjectiveTrackerManager:UpdateAll()
    elseif ObjectiveTrackerFrame and ObjectiveTrackerFrame.Update then
        ObjectiveTrackerFrame:Update()
    end

    RefreshObjectiveTrackerDisplay(true)
end

function ns.RefreshObjectiveTrackerUI(refreshMode)
    HookObjectiveTrackerManagerInit()
    RefreshObjectiveTrackerState(refreshMode)
end

function ns.RunRequestedObjectiveTrackerRefresh()
    state.requestedRefreshPending = false
    local refreshMode = state.requestedRefreshMode
    state.requestedRefreshMode = nil
    ns.RefreshObjectiveTrackerUI(refreshMode)
end

function ns.RequestObjectiveTrackerRefresh(refreshMode)
    if refreshMode == "full" or state.requestedRefreshMode == nil then
        state.requestedRefreshMode = refreshMode
    end

    if state.requestedRefreshPending then
        return
    end

    state.requestedRefreshPending = true

    if C_Timer and C_Timer.After then
        C_Timer.After(0, ns.RunRequestedObjectiveTrackerRefresh)
    else
        ns.RunRequestedObjectiveTrackerRefresh()
    end
end

function ns.CaptureObjectiveTrackerUIState()
    if not state.collapsePersistenceReady or state.applyingCollapseStates then
        return false
    end

    return CaptureCurrentSectionCollapseStates()
end

ns.InitializeObjectiveTrackerUI = ns.RefreshObjectiveTrackerUI
