-- ============================================================
-- FILE: PartyFrames.lua
-- PURPOSE: Creates and manages 1-4 custom party member frames
--   with health/power bars, portraits, auras, dispel tracking
--   overlay, and debuff-type visualisation overlay (tinted
--   edge strip + icon row).  Suppresses Blizzard party frames.
-- HOW IT WORKS:
--   PartyFrameManager drives a state machine per unit.
--   CreateSinglePartyFrame builds each frame lazily on group
--   join.  UNIT_AURA drives UpdatePartyUnit which calls
--   CapturePartyHazardsFromBlizzFrame (Blizzard hook cache) +
--   direct C_UnitAuras scan + ConditionBorder hints to classify
--   debuff type.  BuildPartyDebuffOverlay creates a tint strip
--   with animated fade-in/out.  A dispel tracking overlay
--   (EnsurePartyDispelTrackingOverlay) shows a row of aura icons
--   for debuffs the local player can dispel.
--   Bleed detection uses CHAT_MSG_ADDON cross-frame communication
--   when DebuffTracker data is unavailable.
-- DEPENDS ON:
--   MidnightUI_Core, MidnightUI_MoverRegistry, MidnightUI_Diagnostics,
--   MidnightUI_ConditionBorder, MidnightUI_StyleOverlay,
--   MidnightUI_AttachOverlaySettings
-- EXPORTS:
--   MidnightUI_ApplyPartyFramesLayout(), MidnightUI_SetPartyStyle(),
--   MidnightUI_SetPartyFramesLocked(), MidnightUI_RefreshPartyFrames(),
--   MidnightUI_RefreshPartyDispelTrackingOverlay(),
--   MidnightUI_SetPartyDispelTrackingLocked(),
--   MidnightUI_GetPartyBlizzAuraCache(unit),
--   UpdatePartyUnit(frame), SuppressBlizzardPartyFrame(frame),
--   UnsuppressBlizzardPartyFrame(frame), RestoreBlizzardPartyFrames()
-- OWNED SAVESVARIABLES: none (reads MidnightUISettings.PartyFrames,
--   .Combat, .General)
-- EVENTS REGISTERED:
--   ADDON_LOADED, PLAYER_ENTERING_WORLD, GROUP_ROSTER_UPDATE,
--   PLAYER_REGEN_ENABLED/DISABLED, UNIT_HEALTH/POWER_UPDATE/
--   MAXHEALTH/MODEL_CHANGED/AURA/PHASE/ABSORB_AMOUNT_CHANGED,
--   PLAYER_TARGET_CHANGED, READY_CHECK/CONFIRM/FINISHED,
--   CHAT_MSG_ADDON (bleed comm)
-- FRAMES CREATED: 4 × SecureUnitButtonTemplate party frames
-- GOTCHAS:
--   issecretvalue() + pcall wraps permeate debuff type detection.
--   hooksecurefunc on CompactUnitFrame_UpdateAuras provides Blizzard
--   aura cache; hook is optional (degrades gracefully if missing).
--   PARTY_HAZARD_STICKY_SEC = 0.45s debounce to reduce flicker.
--   Bleed detection via CHAT_MSG_ADDON requires CHAT_MSG_ADDON
--   channel registration which may fail inside instances.
-- ============================================================

local ADDON_NAME = "MidnightUI"
local PartyFrameManager = CreateFrame("Frame")

local SuppressBlizzardPartyFrame
local UnsuppressBlizzardPartyFrame
local ForEachBlizzardPartyFrame
local RestoreBlizzardPartyFrames


local POWER_COLORS = {
    ["MANA"] = {0.00, 0.50, 1.00}, ["RAGE"] = {0.90, 0.10, 0.10}, ["FOCUS"] = {1.00, 0.50, 0.25},
    ["ENERGY"] = {1.00, 1.00, 0.35}, ["COMBO_POINTS"] = {1.00, 0.96, 0.41}, ["RUNES"] = {0.50, 0.50, 0.50},
    ["RUNIC_POWER"] = {0.00, 0.82, 1.00}, ["SOUL_SHARDS"] = {0.50, 0.32, 0.55}, ["LUNAR_POWER"] = {0.30, 0.52, 0.90},
    ["HOLY_POWER"] = {0.95, 0.90, 0.60}, ["MAELSTROM"] = {0.00, 0.50, 1.00}, ["CHI"] = {0.71, 1.00, 0.92},
    ["INSANITY"] = {0.40, 0.00, 0.80}, ["ARCANE_CHARGES"] = {0.10, 0.10, 0.98}, ["FURY"] = {0.79, 0.26, 0.99},
    ["PAIN"] = {1.00, 0.61, 0.00}, ["ESSENCE"] = {0.20, 0.88, 0.66},
}
local DEFAULT_POWER_COLOR = {0.0, 0.5, 1.0}
local DEAD_STATUS_ATLAS = "icons_64x64_deadly"

local function EnsureDeadIconTexture(icon)
    if not icon then return end
    if C_Texture and C_Texture.GetAtlasInfo and C_Texture.GetAtlasInfo(DEAD_STATUS_ATLAS) then
        icon:SetAtlas(DEAD_STATUS_ATLAS, false)
    else
        icon:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame-Skull")
    end
end

local function ApplyDeadIconVisualStyle(icon)
    if not icon then return end
    EnsureDeadIconTexture(icon)
    if icon.SetDesaturated then icon:SetDesaturated(false) end
    icon:SetVertexColor(1, 0.2, 0.2, 1)
    icon:SetAlpha(1)
end

local DEFAULT_CLASS_COLOR = {r=0.5, g=0.5, b=0.5}
local CLASS_COLOR_OVERRIDES = {
    DEATHKNIGHT = { r = 0.77, g = 0.12, b = 0.23 },
    DEMONHUNTER = { r = 0.64, g = 0.19, b = 0.79 },
    DRUID = { r = 1.00, g = 0.49, b = 0.04 },
    EVOKER = { r = 0.20, g = 0.58, b = 0.50 },
    HUNTER = { r = 0.67, g = 0.83, b = 0.45 },
    MAGE = { r = 0.25, g = 0.78, b = 0.92 },
    MONK = { r = 0.00, g = 1.00, b = 0.60 },
    PALADIN = { r = 0.96, g = 0.55, b = 0.73 },
    PRIEST = { r = 1.00, g = 1.00, b = 1.00 },
    ROGUE = { r = 1.00, g = 0.96, b = 0.41 },
    SHAMAN = { r = 0.00, g = 0.44, b = 0.87 },
    WARLOCK = { r = 0.53, g = 0.53, b = 0.93 },
    WARRIOR = { r = 0.78, g = 0.61, b = 0.43 },
}

local DEBUFF_COLORS = {
    ["Magic"]   = { r = 0.20, g = 0.60, b = 1.00 },
    ["Curse"]   = { r = 0.60, g = 0.00, b = 1.00 },
    ["Disease"] = { r = 0.60, g = 0.40, b = 0.00 },
    ["Poison"]  = { r = 0.00, g = 0.60, b = 0.00 },
    ["none"]    = { r = 0.80, g = 0.00, b = 0.00 },
}

local PARTY_HAZARD_ENUM = {
    None = 0,
    Magic = 1,
    Curse = 2,
    Disease = 3,
    Poison = 4,
    Unknown = 99,
}

local PARTY_HAZARD_COLORS = {
    [PARTY_HAZARD_ENUM.Magic]   = { 0.15, 0.55, 0.95 },
    [PARTY_HAZARD_ENUM.Curse]   = { 0.70, 0.20, 0.95 },
    [PARTY_HAZARD_ENUM.Disease] = { 0.80, 0.50, 0.10 },
    [PARTY_HAZARD_ENUM.Poison]  = { 0.15, 0.65, 0.20 },
    [PARTY_HAZARD_ENUM.Unknown] = { 0.25, 0.25, 0.25 },
}
local PARTY_UNKNOWN_SWEEP_COLOR = { 1.00, 1.00, 1.00 }

local PARTY_HAZARD_LABELS = {
    [PARTY_HAZARD_ENUM.Magic] = "MAGIC",
    [PARTY_HAZARD_ENUM.Curse] = "CURSE",
    [PARTY_HAZARD_ENUM.Disease] = "DISEASE",
    [PARTY_HAZARD_ENUM.Poison] = "POISON",
    [PARTY_HAZARD_ENUM.Unknown] = "UNKNOWN",
}
local PARTY_HAZARD_PRIORITY = {
    PARTY_HAZARD_ENUM.Magic,
    PARTY_HAZARD_ENUM.Curse,
    PARTY_HAZARD_ENUM.Disease,
    PARTY_HAZARD_ENUM.Poison,
}
local PARTY_HAZARD_STICKY_SEC = 0.45
local PARTY_HAZARD_CURVE_EPSILON = 0.02
local PARTY_SECRET_TINT_BASE_MUL = 0.42
local PARTY_RENDERED_TINT_MUL = 0.86
local PARTY_SIMPLE_TINT_MUL = 1.16
local _partyHazardProbeCurves = {}
local _partyHazardProbeMatchColors = {}
local _partyHazardZeroProbeCurve
local _partyBlizzAuraCache = {}
local _partyBlizzHookRegistered = false

local _partySpellTypeCache = {}

local _partyAuraEventCache = {}

-- ============================================================
-- SECTION: Aura Event Processing + Constants
-- ProcessPartyAuraEvent handles the batched UNIT_AURA payload
-- (C_UnitAuras updateInfo) for party1-4.  Extracts added/
-- removed/updated aura instance IDs and updates the module's
-- aura event cache used by the hazard classification pass.
-- Also houses constant tables: config defaults, PartyFrames
-- array, dispel tracker state, bleed CHAT_MSG_ADDON channel.
--
-- READS:  updateInfo.addedAuras[], .removedAuraInstanceIDs[],
--         .updatedAuraInstanceIDs[]
-- WRITES: _partyAuraEventCache[unit], _partyBlizzAuraCache,
--         _partySpellTypeCache
--
-- FUNCTIONS:
--   ProcessPartyAuraEvent(unit,updateInfo) — batch UNIT_AURA
--     processor; extracts iids and triggers hazard refresh
-- ============================================================
local function ProcessPartyAuraEvent(unit, updateInfo)
    if not unit or not updateInfo then return end
    if unit ~= "party1" and unit ~= "party2" and unit ~= "party3" and unit ~= "party4" then
        return
    end

    if updateInfo.isFullUpdate then
        _partyAuraEventCache[unit] = {}
    end

    if not _partyAuraEventCache[unit] then
        _partyAuraEventCache[unit] = {}
    end
    local cache = _partyAuraEventCache[unit]

    if updateInfo.removedAuraInstanceIDs then
        for _, iid in ipairs(updateInfo.removedAuraInstanceIDs) do
            cache[iid] = nil
        end
    end

    if updateInfo.addedAuras then
        local FilterCheck = C_UnitAuras and C_UnitAuras.IsAuraFilteredOutByInstanceID
        for _, aura in ipairs(updateInfo.addedAuras) do
            local iid = aura.auraInstanceID
            if iid and FilterCheck then
                local okF, filtered = pcall(FilterCheck, unit, iid, "HARMFUL")
                if okF and filtered == false then
                    cache[iid] = aura
                    local dn = aura.dispelName
                    if dn and aura.spellId then
                        local safeId = nil
                        pcall(function()
                            local _t = {[aura.spellId] = true}
                            safeId = aura.spellId
                        end)
                        if safeId and not issecretvalue(dn) then
                            _partySpellTypeCache[safeId] = dn
                        end
                    end
                end
            end
        end
    end

    if updateInfo.updatedAuraInstanceIDs and C_UnitAuras and C_UnitAuras.GetAuraDataByAuraInstanceID then
        for _, iid in ipairs(updateInfo.updatedAuraInstanceIDs) do
            if cache[iid] then
                local ok, aura = pcall(C_UnitAuras.GetAuraDataByAuraInstanceID, unit, iid)
                if ok and aura then
                    cache[iid] = aura
                else
                    cache[iid] = nil
                end
            end
        end
    end
end

local _partyAuraFrame1 = CreateFrame("Frame")
local _partyAuraFrame2 = CreateFrame("Frame")
local function _OnPartyAuraEvent(_, _, unit, updateInfo)
    if unit and updateInfo then
        ProcessPartyAuraEvent(unit, updateInfo)
        for i = 1, 4 do
            local f = PartyFrames and PartyFrames[i]
            if f and f:IsShown() then
                local fUnit = f:GetAttribute("unit")
                if fUnit and fUnit == unit then
                    _partyDirtyUnits[i] = true
                    if _partyBatchFrame then _partyBatchFrame:Show() end
                    break
                end
            end
        end
    end
end
_partyAuraFrame1:SetScript("OnEvent", _OnPartyAuraEvent)
_partyAuraFrame2:SetScript("OnEvent", _OnPartyAuraEvent)
_partyAuraFrame1:RegisterUnitEvent("UNIT_AURA", "party1", "party2")
_partyAuraFrame2:RegisterUnitEvent("UNIT_AURA", "party3", "party4")

local BLEED_COMM_PREFIX = "MUI_BLEED"
local PARTY_COMM_BLEED_COLOR = { 0.65, 0.10, 0.10 }
local _partyCommBleed = {}

pcall(function()
    if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then
        C_ChatInfo.RegisterAddonMessagePrefix(BLEED_COMM_PREFIX)
    end
end)

local function ResolvePartyUnitFromSender(sender)
    if not sender then return nil end
    local shortName = sender:match("^([^%-]+)")
    for i = 1, 4 do
        local unit = "party" .. i
        if UnitExists(unit) then
            local name = UnitName(unit)
            if name and name == shortName then
                return unit, i
            end
        end
    end
    return nil, nil
end

local _bleedCommFrame = CreateFrame("Frame")
_bleedCommFrame:RegisterEvent("CHAT_MSG_ADDON")
_bleedCommFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
_bleedCommFrame:SetScript("OnEvent", function(_, event, ...)
    if event == "GROUP_ROSTER_UPDATE" then
        wipe(_partyCommBleed)
        return
    end
    local prefix, msg, _, sender = ...
    if prefix ~= BLEED_COMM_PREFIX then return end
    local unit, idx = ResolvePartyUnitFromSender(sender)
    if not unit then return end
    if msg == "ON" then
        _partyCommBleed[unit] = true
    elseif msg == "OFF" then
        _partyCommBleed[unit] = nil
    end
    if idx and PartyFrames and PartyFrames[idx] then
        _partyDirtyUnits[idx] = true
        if _partyBatchFrame then _partyBatchFrame:Show() end
    end
end)

local _partyHazardTintCurve
local GetPartyHazardTintCurve


local config = {
    width = 240, height = 58, healthHeight = 44, powerHeight = 14, spacing = 0,
    startPos = {"TOPLEFT", UIParent, "TOPLEFT", 0, 0},
    verticalOffset = -55,
    auraSize = 20,
    auraSpacing = 4,
    showAuras = false,
    layout = "Vertical",
    spacingX = 8,
    spacingY = 8,
    style = "Rendered",
    hide2DPortrait = false,
}

local PartyFrames = {}

local _partyDirtyUnits = {}
local _partyDirtyAll = false
local _partyBatchFrame = CreateFrame("Frame")
_partyBatchFrame:Hide()
local PartyRangeTicker
local PartyAnchor
local PartyPreviewBoxes = {}
local InitPartyFrames
local ApplyRenderedBarStyle
local MakePartySecretColor
local RefreshPartyDispelTrackingOverlays
local UpdatePartyDispelTrackingOverlay
local NormalizePartyHazardEnum
local OVERLAY_PAD = {
    left = 20,
    right = 20,
    top = 20,
    bottom = 90,
}
local PARTY_DISPEL_TRACKING = {
    BASE_ICON_SIZE = 20,
    ICON_SCALE_MIN = 50,
    ICON_SCALE_MAX = 200,
    OFFSET_X = 8,
    OFFSET_Y = 0,
    PLACEHOLDER_ICON = 134400,
    DRAG_SIZE = 56,
    MAX_SHOWN_LIMIT = 40,
    DEFAULT_PER_ROW = 4,
}
local PartyDispelTrackingState = {
    dragOverlay = nil,
    locked = true,
}

local hiddenParent = CreateFrame("Frame")
hiddenParent:Hide()
local pendingPartyVisibility = false


-- ============================================================
-- SECTION: Utility Functions + Health Bar Styling
-- Drop shadow, font setter, portrait model handling, and the
-- three-layer polished / flat gradient system for party health
-- bars.  Also houses the safe health-percent helpers and
-- PARTY_HAZARD_* secret-value parsing utilities.
--
-- READS:  MidnightUISettings.General.unitFrameBarStyle
-- WRITES: healthBar gradient textures, portrait model objects
--
-- FUNCTIONS:
--   CreateDropShadow / ShowShadows / HideShadows
--   SetFontSafe(fs,path,size,flags)
--   ApplyPolishedGradientToBar / ApplyFlatGradientToBar
--   ApplyPartyHealthBarStyle(bar) — polished or flat dispatch
--   AllowSecretHealthPercent() — setting gate
--   GetDisplayHealthPercent(unit) — UnitHealthPercent + fallback
--   IsPartySecretToken / IsPartySecretValue — issecretvalue wrappers
--   ParseHazardLabelText / ParseHazardRGBText — parse enum/color
--     strings from diagnostic labels safely
-- ============================================================
local function CreateDropShadow(frame, intensity)
    intensity = intensity or 6
    local shadows = {}
    for i = 1, intensity do
        local shadowLayer = CreateFrame("Frame", nil, frame)
        shadowLayer:SetFrameLevel(math.max(0, frame:GetFrameLevel() - 1))
        local offset = i * 1.0
        local alpha = (0.25 - (i * 0.035)) * (intensity / 6)
        shadowLayer:SetPoint("TOPLEFT", frame, "TOPLEFT", -offset, offset)
        shadowLayer:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", offset, -offset)
        local shadowTex = shadowLayer:CreateTexture(nil, "BACKGROUND")
        shadowTex:SetAllPoints()
        shadowTex:SetColorTexture(0, 0, 0, alpha)
        shadowLayer.tex = shadowTex
        table.insert(shadows, shadowLayer)
    end
    return shadows
end

local function SetFontSafe(fs, fontPath, size, flags)
    local resolved = (_G.MidnightUI_ResolveFontPath and _G.MidnightUI_ResolveFontPath(fontPath)) or fontPath
    local ok = fs:SetFont(resolved, size, flags)
    if not ok then
        local fallback = (_G.MidnightUI_ResolveFontPath and _G.MidnightUI_ResolveFontPath()) or (GameFontNormal and GameFontNormal:GetFont())
        if fallback then fs:SetFont(fallback, size or 12, flags) end
    end
    return ok
end

local function UpdatePortraitModel(portrait, unit)
    if not portrait or not unit then return end
    if SetPortraitTexture then
        SetPortraitTexture(portrait, unit)
    end
    if portrait.SetTexCoord then
        portrait:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    end
    portrait:SetAlpha(1)
end

local function ApplyHealthBarStyle(healthBar)
    if not healthBar then return end
    local function ApplyPolishedGradient(tex, topDarkA, centerLightA, bottomDarkA)
        local anchor = tex or healthBar
        local baseR, baseG, baseB = healthBar:GetStatusBarColor()
        if type(baseR) ~= "number" then
            baseR, baseG, baseB = 0.12, 0.85, 0.12
        end
        local function Scale(v, f)
            local n = (v or 0) * f
            if n < 0 then return 0 end
            if n > 1 then return 1 end
            return n
        end
        local edgeR, edgeG, edgeB = Scale(baseR, 0.54), Scale(baseG, 0.54), Scale(baseB, 0.54)
        local midR, midG, midB = Scale(baseR, 1.15), Scale(baseG, 1.15), Scale(baseB, 1.15)
        if not healthBar._muiTopHighlight then
            healthBar._muiTopHighlight = healthBar:CreateTexture(nil, "ARTWORK", nil, 2)
        end
        healthBar._muiTopHighlight:SetTexture("Interface\\Buttons\\WHITE8X8")
        healthBar._muiTopHighlight:SetBlendMode("DISABLE")
        if not healthBar._muiBottomShade then
            healthBar._muiBottomShade = healthBar:CreateTexture(nil, "ARTWORK", nil, 1)
        end
        healthBar._muiBottomShade:SetTexture("Interface\\Buttons\\WHITE8X8")
        healthBar._muiBottomShade:SetBlendMode("DISABLE")
        if not healthBar._muiSpecular then
            healthBar._muiSpecular = healthBar:CreateTexture(nil, "ARTWORK", nil, 3)
        end
        healthBar._muiSpecular:SetTexture("Interface\\Buttons\\WHITE8X8")
        healthBar._muiSpecular:SetBlendMode("ADD")
        local rawH = (healthBar.GetHeight and healthBar:GetHeight()) or 2
        local h = tonumber(tostring(rawH)) or 2
        local topH = math.max(1, h * 0.43)
        local botH = math.max(1, h * 0.57)
        local specH = math.max(1, h * 0.35)
        healthBar._muiTopHighlight:ClearAllPoints()
        healthBar._muiTopHighlight:SetPoint("TOPLEFT", anchor, "TOPLEFT", 0, 0)
        healthBar._muiTopHighlight:SetPoint("TOPRIGHT", anchor, "TOPRIGHT", 0, 0)
        healthBar._muiTopHighlight:SetHeight(topH)
        healthBar._muiTopHighlight:SetGradient("VERTICAL",
            CreateColor(edgeR, edgeG, edgeB, 1),
            CreateColor(midR, midG, midB, 1))

        healthBar._muiBottomShade:ClearAllPoints()
        healthBar._muiBottomShade:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", 0, 0)
        healthBar._muiBottomShade:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMRIGHT", 0, 0)
        healthBar._muiBottomShade:SetHeight(botH)
        healthBar._muiBottomShade:SetGradient("VERTICAL",
            CreateColor(midR, midG, midB, 1),
            CreateColor(edgeR, edgeG, edgeB, 1))

        healthBar._muiSpecular:ClearAllPoints()
        healthBar._muiSpecular:SetPoint("TOPLEFT", anchor, "TOPLEFT", 0, 0)
        healthBar._muiSpecular:SetPoint("TOPRIGHT", anchor, "TOPRIGHT", 0, 0)
        healthBar._muiSpecular:SetHeight(specH)
        healthBar._muiSpecular:SetGradient("VERTICAL",
            CreateColor(1, 1, 1, 0),
            CreateColor(1, 1, 1, 0.06))
        healthBar._muiTopHighlight:Show()
        healthBar._muiBottomShade:Show()
        healthBar._muiSpecular:Show()
    end

    local style = (MidnightUISettings and MidnightUISettings.General and MidnightUISettings.General.unitFrameBarStyle) or "Gradient"
    if style == "Balanced" then style = "Gradient" end
    if style == "Gradient" then
        healthBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
        local tex = healthBar:GetStatusBarTexture()
        if tex then
            tex:SetHorizTile(false)
            tex:SetVertTile(false)
        end
        ApplyPolishedGradient(tex, 0.28, 0.035, 0.32)
        if healthBar._muiFlatGradient then healthBar._muiFlatGradient:Hide() end
        return
    end
    healthBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
    local tex = healthBar:GetStatusBarTexture()
    if tex then
        tex:SetHorizTile(false)
        tex:SetVertTile(false)
    end
    if not healthBar._muiFlatGradient then
        healthBar._muiFlatGradient = healthBar:CreateTexture(nil, "ARTWORK")
        healthBar._muiFlatGradient:SetAllPoints(tex or healthBar)
        healthBar._muiFlatGradient:SetBlendMode("DISABLE")
    end
    healthBar._muiFlatGradient:SetGradient("VERTICAL", CreateColor(1, 1, 1, 0.06), CreateColor(0, 0, 0, 0.12))
    healthBar._muiFlatGradient:Show()
    if healthBar._muiTopHighlight then healthBar._muiTopHighlight:Hide() end
    if healthBar._muiBottomShade then healthBar._muiBottomShade:Hide() end
    if healthBar._muiSpecular then healthBar._muiSpecular:Hide() end
end

local function GetPartyDebuffTintBaseMultiplier()
    local style = tostring(config and config.style or "Rendered")
    local mul = PARTY_SECRET_TINT_BASE_MUL
    if style == "Rendered" then
        mul = mul * PARTY_RENDERED_TINT_MUL
    elseif style == "Simple" then
        mul = mul * PARTY_SIMPLE_TINT_MUL
    end
    if mul < 0 then mul = 0 end
    if mul > 1 then mul = 1 end
    return mul
end

local function GetPartyDebuffBackgroundProfile()
    local style = tostring(config and config.style or "Rendered")
    if style == "Simple" then
        return 0.18, 0.14, 0.52, 0.40
    end
    return 0.25, 0.20, 0.62, 0.48
end

local function ApplyPartyDebuffBackgroundTint(frame, r, g, b)
    if not frame then
        return
    end
    local healthMul, powerMul, healthAlpha, powerAlpha = GetPartyDebuffBackgroundProfile()
    if frame.healthBg and frame.healthBg.SetColorTexture then
        frame.healthBg:SetColorTexture((r or 0) * healthMul, (g or 0) * healthMul, (b or 0) * healthMul, healthAlpha)
    end
    if frame.powerBg and frame.powerBg.SetColorTexture then
        frame.powerBg:SetColorTexture((r or 0) * powerMul, (g or 0) * powerMul, (b or 0) * powerMul, powerAlpha)
    end
end

local function Clamp01(v)
    if v < 0 then return 0 end
    if v > 1 then return 1 end
    return v
end

local function ApplyStatusBarGradient(bar, r, g, b, a, topAlpha, bottomAlpha)
    if not bar then return end
    bar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
    local tex = bar:GetStatusBarTexture()
    if not tex then return end
    tex:SetHorizTile(false)
    tex:SetVertTile(false)
    bar:SetStatusBarColor(r, g, b, a or 1)
    if not bar._muiTopHighlight then
        bar._muiTopHighlight = bar:CreateTexture(nil, "OVERLAY", nil, 2)
    end
    bar._muiTopHighlight:SetTexture("Interface\\Buttons\\WHITE8X8")
    bar._muiTopHighlight:SetBlendMode("DISABLE")
    if not bar._muiBottomShade then
        bar._muiBottomShade = bar:CreateTexture(nil, "OVERLAY", nil, 1)
    end
    bar._muiBottomShade:SetTexture("Interface\\Buttons\\WHITE8X8")
    bar._muiBottomShade:SetBlendMode("DISABLE")
    if not bar._muiSpecular then
        bar._muiSpecular = bar:CreateTexture(nil, "OVERLAY", nil, 3)
    end
    bar._muiSpecular:SetTexture("Interface\\Buttons\\WHITE8X8")
    bar._muiSpecular:SetBlendMode("ADD")
    local function Scale(v, f)
        local n = (v or 0) * f
        if n < 0 then return 0 end
        if n > 1 then return 1 end
        return n
    end
    local edgeR, edgeG, edgeB = Scale(r, 0.54), Scale(g, 0.54), Scale(b, 0.54)
    local midR, midG, midB = Scale(r, 1.15), Scale(g, 1.15), Scale(b, 1.15)
    local rawH = (bar.GetHeight and bar:GetHeight()) or 2
    local h = tonumber(tostring(rawH)) or 2
    local topH = math.max(1, h * 0.43)
    local botH = math.max(1, h * 0.57)
    local specH = math.max(1, h * 0.35)

    bar._muiTopHighlight:ClearAllPoints()
    bar._muiTopHighlight:SetPoint("TOPLEFT", tex, "TOPLEFT", 0, 0)
    bar._muiTopHighlight:SetPoint("TOPRIGHT", tex, "TOPRIGHT", 0, 0)
    bar._muiTopHighlight:SetHeight(topH)
    bar._muiTopHighlight:SetGradient("VERTICAL",
        CreateColor(edgeR, edgeG, edgeB, 1),
        CreateColor(midR, midG, midB, 1))

    bar._muiBottomShade:ClearAllPoints()
    bar._muiBottomShade:SetPoint("BOTTOMLEFT", tex, "BOTTOMLEFT", 0, 0)
    bar._muiBottomShade:SetPoint("BOTTOMRIGHT", tex, "BOTTOMRIGHT", 0, 0)
    bar._muiBottomShade:SetHeight(botH)
    bar._muiBottomShade:SetGradient("VERTICAL",
        CreateColor(midR, midG, midB, 1),
        CreateColor(edgeR, edgeG, edgeB, 1))

    bar._muiSpecular:ClearAllPoints()
    bar._muiSpecular:SetPoint("TOPLEFT", tex, "TOPLEFT", 0, 0)
    bar._muiSpecular:SetPoint("TOPRIGHT", tex, "TOPRIGHT", 0, 0)
    bar._muiSpecular:SetHeight(specH)
    bar._muiSpecular:SetGradient("VERTICAL",
        CreateColor(1, 1, 1, 0),
        CreateColor(1, 1, 1, 0.06))

    bar._muiTopHighlight:Show()
    bar._muiBottomShade:Show()
    bar._muiSpecular:Show()
    if bar._muiGradient then bar._muiGradient:Hide() end
    if bar._muiRenderedTop then bar._muiRenderedTop:Hide() end
    if bar._muiRenderedBottom then bar._muiRenderedBottom:Hide() end
    if bar._muiRenderedGloss then bar._muiRenderedGloss:Hide() end
end

local function ApplyPartyFlatStatusBarTint(bar, r, g, b, a)
    if not bar then return end
    bar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
    local tex = bar:GetStatusBarTexture()
    if tex then
        tex:SetHorizTile(false)
        tex:SetVertTile(false)
    end
    bar:SetStatusBarColor(r or 0, g or 0, b or 0, a or 1)
    if tex and tex.SetVertexColor then
        tex:SetVertexColor(r or 0, g or 0, b or 0, a or 1)
    end
    if bar._muiGradient then bar._muiGradient:Hide() end
    if bar._muiFlatGradient then bar._muiFlatGradient:Hide() end
    if bar._muiTopHighlight then bar._muiTopHighlight:Hide() end
    if bar._muiBottomShade then bar._muiBottomShade:Hide() end
    if bar._muiSpecular then bar._muiSpecular:Hide() end
    if bar._muiRenderedTop then bar._muiRenderedTop:Hide() end
    if bar._muiRenderedBottom then bar._muiRenderedBottom:Hide() end
    if bar._muiRenderedGloss then bar._muiRenderedGloss:Hide() end
end

local function ClearBarOverlays(bar)
    if not bar then return end
    if bar._muiGradient then bar._muiGradient:Hide() end
    if bar._muiFlatGradient then bar._muiFlatGradient:Hide() end
    if bar._muiTopHighlight then bar._muiTopHighlight:Hide() end
    if bar._muiBottomShade then bar._muiBottomShade:Hide() end
    if bar._muiSpecular then bar._muiSpecular:Hide() end
    if bar._muiRenderedTop then bar._muiRenderedTop:Hide() end
    if bar._muiRenderedBottom then bar._muiRenderedBottom:Hide() end
    if bar._muiRenderedGloss then bar._muiRenderedGloss:Hide() end
end

local function IsPartySecretToken(value)
    if type(issecretvalue) ~= "function" then
        return false
    end
    local ok, secret = pcall(issecretvalue, value)
    return ok and secret == true
end


NormalizePartyHazardEnum = function(value)
    local t = type(value)
    if t == "number" then
        if type(issecretvalue) == "function" then
            local ok, secret = pcall(issecretvalue, value)
            if ok and secret == true then
                return nil
            end
        end
        if value == PARTY_HAZARD_ENUM.Magic
            or value == PARTY_HAZARD_ENUM.Curse
            or value == PARTY_HAZARD_ENUM.Disease
            or value == PARTY_HAZARD_ENUM.Poison
            or value == PARTY_HAZARD_ENUM.Unknown then
            return value
        end
        return nil
    end
    if t ~= "string" then
        return nil
    end
    if type(issecretvalue) == "function" then
        local okSecret, secret = pcall(issecretvalue, value)
        if okSecret and secret == true then
            return nil
        end
    end
    local okLower, lower = pcall(string.lower, value)
    if not okLower or type(lower) ~= "string" then
        return nil
    end
    if lower == "magic" then return PARTY_HAZARD_ENUM.Magic end
    if lower == "curse" then return PARTY_HAZARD_ENUM.Curse end
    if lower == "disease" then return PARTY_HAZARD_ENUM.Disease end
    if lower == "poison" then return PARTY_HAZARD_ENUM.Poison end
    if lower == "unknown" then return PARTY_HAZARD_ENUM.Unknown end
    return nil
end

local function EnsurePartyCombatSettingsTable()
    if not MidnightUISettings then
        MidnightUISettings = {}
    end
    if not MidnightUISettings.Combat then
        MidnightUISettings.Combat = {}
    end
    local combat = MidnightUISettings.Combat
    if combat.debuffOverlayGlobalEnabled == nil then
        combat.debuffOverlayGlobalEnabled = true
    end
    if combat.debuffOverlayPartyEnabled == nil then
        combat.debuffOverlayPartyEnabled = true
    end
    if combat.partyDispelTrackingEnabled == nil then
        combat.partyDispelTrackingEnabled = true
    end
    if combat.partyDispelTrackingIconScale == nil then
        combat.partyDispelTrackingIconScale = 100
    end
    if combat.partyDispelTrackingAlpha == nil then
        combat.partyDispelTrackingAlpha = 1.0
    end
    if combat.partyDispelTrackingMaxShown == nil then
        combat.partyDispelTrackingMaxShown = 4
    end
    if combat.partyDispelTrackingPerRow == nil then
        combat.partyDispelTrackingPerRow = PARTY_DISPEL_TRACKING.DEFAULT_PER_ROW
    end
    if combat.partyDispelTrackingAutoLayoutMigrated ~= true then
        if combat.partyDispelTrackingOrientation ~= nil then
            combat.partyDispelTrackingOrientation = nil
            combat.partyDispelTrackingPosition = nil
        end
        combat.partyDispelTrackingAutoLayoutMigrated = true
    end
    return combat
end

local function IsPartyDebuffOverlayEnabled()
    local combat = EnsurePartyCombatSettingsTable()
    if combat.debuffOverlayGlobalEnabled == false then
        return false
    end
    if combat.debuffOverlayPartyEnabled == false then
        return false
    end
    return true
end

local function ClampPartyDispelTrackingIconScale(value)
    local num = tonumber(value) or 100
    num = math.floor(num + 0.5)
    if num < PARTY_DISPEL_TRACKING.ICON_SCALE_MIN then
        num = PARTY_DISPEL_TRACKING.ICON_SCALE_MIN
    elseif num > PARTY_DISPEL_TRACKING.ICON_SCALE_MAX then
        num = PARTY_DISPEL_TRACKING.ICON_SCALE_MAX
    end
    return num
end

local function ClampPartyDispelTrackingAlpha(value)
    local num = tonumber(value) or 1.0
    if num < 0.1 then
        num = 0.1
    elseif num > 1.0 then
        num = 1.0
    end
    return num
end

local function GetPartyDispelTrackingIconSize(iconScale)
    local clamped = ClampPartyDispelTrackingIconScale(iconScale)
    return math.max(12, math.floor((PARTY_DISPEL_TRACKING.BASE_ICON_SIZE * clamped / 100) + 0.5))
end

local function IsPartyDispelMovementModeUnlocked()
    return MidnightUISettings
        and MidnightUISettings.Messenger
        and MidnightUISettings.Messenger.locked == false
end

local function NormalizePartyTrackerTypeCode(dt)
    local tokenType = type(dt)
    if tokenType == "number" then
        if dt == 1 then return PARTY_HAZARD_ENUM.Magic end
        if dt == 2 then return PARTY_HAZARD_ENUM.Curse end
        if dt == 3 then return PARTY_HAZARD_ENUM.Disease end
        if dt == 4 then return PARTY_HAZARD_ENUM.Poison end
    elseif tokenType == "string" then
        if type(issecretvalue) == "function" then
            local okSecret, secret = pcall(issecretvalue, dt)
            if okSecret and secret == true then
                return nil
            end
        end
        local okLower, lower = pcall(string.lower, dt)
        if not okLower or type(lower) ~= "string" then
            return nil
        end
        if lower == "magic" or lower == "type_magic" then return PARTY_HAZARD_ENUM.Magic end
        if lower == "curse" or lower == "type_curse" then return PARTY_HAZARD_ENUM.Curse end
        if lower == "disease" or lower == "type_disease" then return PARTY_HAZARD_ENUM.Disease end
        if lower == "poison" or lower == "type_poison" then return PARTY_HAZARD_ENUM.Poison end
    end
    return NormalizePartyHazardEnum(dt)
end

local function ParsePartyHazardLabelText(text)
    if type(text) ~= "string" then
        return nil
    end
    local okUpper, upper = pcall(string.upper, text)
    if not okUpper or type(upper) ~= "string" then
        return nil
    end
    if upper:find("MAGIC", 1, true) then return PARTY_HAZARD_ENUM.Magic end
    if upper:find("CURSE", 1, true) then return PARTY_HAZARD_ENUM.Curse end
    if upper:find("DISEASE", 1, true) then return PARTY_HAZARD_ENUM.Disease end
    if upper:find("POISON", 1, true) then return PARTY_HAZARD_ENUM.Poison end
    if upper:find("UNKNOWN", 1, true) then return PARTY_HAZARD_ENUM.Unknown end
    return nil
end

local function ParsePartyRGBText(text)
    if type(text) ~= "string" then
        return nil, nil, nil
    end
    local rText, gText, bText = text:match("([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)")
    if not rText then
        return nil, nil, nil
    end
    local r, g, b = tonumber(rText), tonumber(gText), tonumber(bText)
    if not r or not g or not b then
        return nil, nil, nil
    end
    local function Clamp01(v)
        if v < 0 then return 0 end
        if v > 1 then return 1 end
        return v
    end
    return Clamp01(r), Clamp01(g), Clamp01(b)
end

-- ============================================================
-- SECTION: Blizzard Frame Hook System + Aura Cache
-- Hooks CompactUnitFrame_UpdateAuras / UpdateDebuffs to capture
-- debuff data (iid, debuffType, spellId, icon) into
-- _partyBlizzAuraCache per unit.  Cache is primed by
-- PrimePartyBlizzAuraCache on group join.  Provides the fastest
-- classification path before C_UnitAuras fields become secret.
--
-- READS:  CompactUnitFrame debuff sub-frame fields
-- WRITES: _partyBlizzAuraCache[unit][iid] = {type,spellId,icon}
--
-- FUNCTIONS:
--   IsPartyBlizzTrackedUnit(unit) — true for party1-4
--   ReadDebuffFrameFields(df) — safe read of debuffType, iid,
--     spellId, icon from compact debuff sub-frame
--   CapturePartyHazardsFromBlizzFrame(frame,unit) — reads all
--     dispelDebuffFrames / debuffFrames and populates cache
--   EnsurePartyBlizzAuraHook() — registers hooksecurefunc once
--   PrimePartyBlizzAuraCache(unit) — snapshot existing Blizzard
--     compact frame state into cache on group join
--   ResetPartyBlizzAuraCache(unit) — clear on roster change
-- ============================================================
local function IsPartyBlizzTrackedUnit(unit)
    if type(unit) ~= "string" then
        return false
    end
    if unit == "player" then
        return true
    end
    return string.match(unit, "^party%d$") ~= nil
end

local function ResolvePartyHazardFromBlizzFrameDebuff(debuffFrame)
    if not debuffFrame then
        return nil
    end

    local dt = NormalizePartyHazardEnum(debuffFrame.debuffType)
    if not dt then
        dt = NormalizePartyHazardEnum(debuffFrame.dispelName)
    end
    if not dt then
        dt = NormalizePartyHazardEnum(debuffFrame.dispelType)
    end
    if not dt then
        dt = NormalizePartyHazardEnum(debuffFrame.type)
    end
    return dt
end

local function CountPartyBlizzAuraSet(tbl)
    if type(tbl) ~= "table" then
        return 0
    end
    local count = 0
    for _ in pairs(tbl) do
        count = count + 1
    end
    return count
end

local _partySpellIDFields = { "spellID", "spellId", "auraSpellID", "auraSpellId" }

local function ReadTextureToken(texObj)
    if not texObj then
        return nil
    end
    if texObj.GetTextureFileID then
        local okID, texID = pcall(texObj.GetTextureFileID, texObj)
        if okID and texID ~= nil then
            return texID
        end
    end
    if texObj.GetTexture then
        local okTex, tex = pcall(texObj.GetTexture, texObj)
        if okTex and tex ~= nil then
            return tex
        end
    end
    return nil
end

local function ReadDebuffFrameIcon(df)
    if not df then
        return nil
    end
    local icon = ReadTextureToken(df.icon)
    if not icon then
        icon = ReadTextureToken(df.Icon)
    end
    if not icon then
        icon = ReadTextureToken(df.iconTexture)
    end
    if (not icon) and (type(df.icon) == "number" or type(df.icon) == "string") then
        icon = df.icon
    end
    return icon
end

local function ReadDebuffFrameStack(df)
    if not df then
        return nil
    end
    local stack = tonumber(df.applications or df.stackCount)
    if (not stack) and df.count then
        if type(df.count) == "number" then
            stack = tonumber(df.count)
        elseif df.count.GetText then
            local okText, text = pcall(df.count.GetText, df.count)
            if okText and text ~= nil then
                if not IsPartySecretToken(text) then
                    local okNum, parsed = pcall(tonumber, text)
                    if okNum and type(parsed) == "number" then
                        stack = parsed
                    end
                end
            end
        end
    end
    if stack and stack > 1 then
        return math.floor(stack + 0.5)
    end
    return nil
end

local function ReadDebuffFrameSpellID(df)
    if not df then
        return nil
    end
    for _, field in ipairs(_partySpellIDFields) do
        local candidate = df[field]
        if candidate then
            local okSpellID, spellID = pcall(tonumber, candidate)
            if okSpellID and spellID and (not IsPartySecretValue(spellID)) and spellID > 0 then
                return spellID
            end
        end
    end
    return nil
end

local function StorePartyAura(entry, iid, isDispellable, dt, icon, stackCount, spellID)
    if not iid then
        return
    end
    local safeIID = nil
    local okKey = pcall(function() local _t = {[iid] = true}; safeIID = iid end)
    if not okKey or not safeIID then
        return
    end
    entry.debuffs[safeIID] = true
    if isDispellable then
        entry.dispellable[safeIID] = true
    end
    if dt then
        entry.types[safeIID] = dt
    end
    if icon then
        entry.icons[safeIID] = icon
    end
    if stackCount and stackCount > 1 then
        entry.stacks[safeIID] = stackCount
    end
    local okSpellID, numericSpellID = pcall(tonumber, spellID)
    if okSpellID and numericSpellID and (not IsPartySecretValue(numericSpellID)) and numericSpellID > 0 then
        entry.spellIDs[safeIID] = numericSpellID
    end
end

local function CapturePartyHazardsFromBlizzFrame(blizzFrame, isFromHook)
    if not blizzFrame then
        return nil, nil
    end

    local unit = blizzFrame.unit
    if not IsPartyBlizzTrackedUnit(unit) then
        local frameName
        local okName, name = pcall(function() return blizzFrame:GetName() end)
        if okName then frameName = name end
        if type(frameName) == "string" then
            local idx = frameName:match("CompactPartyFrameMember(%d)")
                     or frameName:match("CompactRaidFrame(%d)")
            if idx then
                unit = "party" .. idx
            end
        end
        if not IsPartyBlizzTrackedUnit(unit) then
            return nil, nil
        end
    end
    local okExists, unitExists = pcall(UnitExists, unit)
    if okExists and not unitExists then
        return nil, nil
    end

    local staging = { debuffs = {}, dispellable = {}, types = {}, icons = {}, stacks = {}, spellIDs = {} }

    local IsAuraFilteredOut = C_UnitAuras and C_UnitAuras.IsAuraFilteredOutByInstanceID
    local dispelFilter = "HARMFUL|RAID_PLAYER_DISPELLABLE"

    local function CaptureDebuffFrameList(frameList, isDispellable)
        if type(frameList) ~= "table" then return end
        for _, df in pairs(frameList) do
            local shown = (not df) and false or (not df.IsShown) or df:IsShown()
            if df and shown then
                local iid = df.auraInstanceID
                if iid then
                    local dt = ResolvePartyHazardFromBlizzFrameDebuff(df)
                    local dispellable = isDispellable
                    if not dispellable and IsAuraFilteredOut then
                        local okFilter, filtered = pcall(IsAuraFilteredOut, unit, iid, dispelFilter)
                        if okFilter and filtered == false then
                            dispellable = true
                        end
                    end
                    StorePartyAura(staging, iid, dispellable, dt,
                        ReadDebuffFrameIcon(df), ReadDebuffFrameStack(df), ReadDebuffFrameSpellID(df))
                end
            end
        end
    end

    CaptureDebuffFrameList(blizzFrame.debuffFrames, false)
    CaptureDebuffFrameList(blizzFrame.dispelDebuffFrames, true)

    if CountPartyBlizzAuraSet(staging.debuffs) == 0 then
        local debuffContainer = blizzFrame.debuffs
        if debuffContainer and type(debuffContainer.Iterate) == "function" then
            pcall(function()
                debuffContainer:Iterate(function(auraInstanceID)
                    if auraInstanceID then
                        local dispellable = false
                        if IsAuraFilteredOut then
                            local okF, filtered = pcall(IsAuraFilteredOut, unit, auraInstanceID, dispelFilter)
                            if okF and filtered == false then
                                dispellable = true
                            end
                        end
                        StorePartyAura(staging, auraInstanceID, dispellable, nil, nil, nil, nil)
                    end
                    return false
                end)
            end)
        end
    end

    staging.tintColors = {}
    local tintCurve = GetPartyHazardTintCurve()
    local tintCount = 0
    if tintCurve and C_UnitAuras and C_UnitAuras.GetAuraDispelTypeColor then
        for iid in pairs(staging.debuffs) do
            local okColor, color = pcall(C_UnitAuras.GetAuraDispelTypeColor, unit, iid, tintCurve)
            if okColor and color then
                staging.tintColors[iid] = color
                tintCount = tintCount + 1
            end
        end
    end

    local debuffCount = CountPartyBlizzAuraSet(staging.debuffs)
    local now = GetTime()
    if debuffCount > 0 then
        staging._lastPopulated = now
        _partyBlizzAuraCache[unit] = staging
    elseif isFromHook then
        staging._lastPopulated = now
        _partyBlizzAuraCache[unit] = staging
    elseif not _partyBlizzAuraCache[unit] then
        _partyBlizzAuraCache[unit] = staging
    end
    local entry = _partyBlizzAuraCache[unit]

    return unit, entry
end

local function RefreshPartyFrameForUnitFromBlizzHook(unit)
    if not unit then
        return
    end
    if type(UpdatePartyUnit) ~= "function" then
        return
    end
    for i = 1, 4 do
        local frame = PartyFrames[i]
        if frame and frame:IsShown() and frame.GetAttribute then
            local frameUnit = frame:GetAttribute("unit")
            if frameUnit == unit then
                pcall(UpdatePartyUnit, frame)
            end
        end
    end
end

local function PrimePartyBlizzAuraCache()
    local compactPlayerFrame = _G.CompactPlayerFrame
    if compactPlayerFrame and compactPlayerFrame.unit == "player" then
        CapturePartyHazardsFromBlizzFrame(compactPlayerFrame)
    end
    for i = 1, 5 do
        local compactParty = _G["CompactPartyFrameMember" .. tostring(i)]
        if compactParty and compactParty.unit then
            CapturePartyHazardsFromBlizzFrame(compactParty)
        end
    end
    local pf = _G.PartyFrame
    if pf then
        for i = 1, 5 do
            local member = pf["MemberFrame" .. i]
            if member and member.unit then
                CapturePartyHazardsFromBlizzFrame(member)
            end
        end
    end
    for i = 1, 5 do
        local raidFrame = _G["CompactRaidFrame" .. i]
        if raidFrame and raidFrame.unit then
            local u = raidFrame.unit
            if type(u) == "string" and u:match("^party%d$") then
                CapturePartyHazardsFromBlizzFrame(raidFrame)
            end
        end
    end
end

local function ResetPartyAuraEventCache()
    wipe(_partyAuraEventCache)
end

local function ResetPartyBlizzAuraCache()
    ResetPartyAuraEventCache()
    for unit, entry in pairs(_partyBlizzAuraCache) do
        if IsPartyBlizzTrackedUnit(unit) and type(entry) == "table" then
            wipe(entry.debuffs)
            wipe(entry.dispellable)
            wipe(entry.types)
            if type(entry.icons) == "table" then
                wipe(entry.icons)
            end
            if type(entry.stacks) == "table" then
                wipe(entry.stacks)
            end
            if type(entry.spellIDs) == "table" then
                wipe(entry.spellIDs)
            end
            if type(entry.tintColors) == "table" then
                wipe(entry.tintColors)
            end
        end
    end
end

local function EnsurePartyBlizzAuraHook()
    if _partyBlizzHookRegistered then
        return
    end
    if type(hooksecurefunc) ~= "function" then
        return
    end

    local function OnBlizzAuraUpdate(blizzFrame)
        local unit = CapturePartyHazardsFromBlizzFrame(blizzFrame, true)
        if unit then
            RefreshPartyFrameForUnitFromBlizzHook(unit)
        end
    end

    if type(CompactUnitFrame_UpdateAuras) == "function" then
        hooksecurefunc("CompactUnitFrame_UpdateAuras", OnBlizzAuraUpdate)
        _partyBlizzHookRegistered = true
    end
    if type(CompactUnitFrame_UpdateDebuffs) == "function" then
        hooksecurefunc("CompactUnitFrame_UpdateDebuffs", OnBlizzAuraUpdate)
        _partyBlizzHookRegistered = true
    end
    if not _partyBlizzHookRegistered then
        local mixin = _G.CompactUnitFrame_Mixin or (_G.CompactUnitFrameMixin)
        if mixin and type(mixin.UpdateAuras) == "function" then
            hooksecurefunc(mixin, "UpdateAuras", OnBlizzAuraUpdate)
            _partyBlizzHookRegistered = true
        end
    end

    if _partyBlizzHookRegistered then
        PrimePartyBlizzAuraCache()
    end
end

local function IsPartySecretValue(value)
    return IsPartySecretToken(value)
end

local function ClampPartyUnitColor(value)
    if value < 0 then return 0 end
    if value > 1 then return 1 end
    return value
end

MakePartySecretColor = function(r, g, b, source)
    if r == nil or g == nil or b == nil then
        return nil
    end
    return { r, g, b, secret = true, source = source }
end

-- ============================================================
-- SECTION: Hazard Color Curve Detection + Debuff Overlay Build
-- IsPartyCurveColorMatch compares ColorMixin values via GetRGBA
-- with pcall.  GetPartyHazardProbeCurve builds a step curve
-- for probing dispel type via GetAuraDispelTypeColor.
-- BuildPartyDebuffOverlay creates the animated tint edge strip +
-- icon row on each party frame, with fade-in/out AnimationGroups.
--
-- READS:  C_CurveUtil.CreateColorCurve, party frame dimensions
-- WRITES: frame._muiPartyDebuffOverlay (widget set),
--         frame._muiPartyDebuffIconRow[] (icon buttons)
--
-- FUNCTIONS:
--   IsPartyCurveColorMatch / IsPartyCurveDifferent — RGBA compare
--   GetPartyHazardProbeCurve(enum) — builds probe curve for one type
--   ResolvePartyHazardEnumByCurve(unit,iid) — probe all enums;
--     returns first match; unreliable in restricted combat
--   CollectPartyHazardsForUnit(unit) — master classifier: checks
--     blizzard cache → direct C_UnitAuras scan → curve probe →
--     ConditionBorder hint → tracker query
--   BuildPartyDebuffOverlay(frame) — constructs overlay widgets
-- ============================================================
local function IsPartyCurveColorMatch(color, expected)
    if not color or not expected then
        return false
    end
    if color.IsEqualTo then
        local okEq, matched = pcall(color.IsEqualTo, color, expected)
        if okEq and matched == true then
            return true
        end
    end
    if type(color.GetRGBA) ~= "function" or type(expected.GetRGBA) ~= "function" then
        return false
    end
    local okA, ar, ag, ab, aa = pcall(color.GetRGBA, color)
    if not okA then
        return false
    end
    local okB, er, eg, eb, ea = pcall(expected.GetRGBA, expected)
    if not okB then
        return false
    end
    local vals = { ar, ag, ab, aa, er, eg, eb, ea }
    for i = 1, #vals do
        if type(issecretvalue) == "function" then
            local okSecret, secret = pcall(issecretvalue, vals[i])
            if okSecret and secret == true then
                return false
            end
        end
        if type(vals[i]) ~= "number" then
            return false
        end
    end
    if math.abs(ar - er) > PARTY_HAZARD_CURVE_EPSILON then return false end
    if math.abs(ag - eg) > PARTY_HAZARD_CURVE_EPSILON then return false end
    if math.abs(ab - eb) > PARTY_HAZARD_CURVE_EPSILON then return false end
    if math.abs(aa - ea) > PARTY_HAZARD_CURVE_EPSILON then return false end
    return true
end

local function IsPartyCurveColorDifferent(a, b)
    if not a or not b then
        return false
    end
    if a.IsEqualTo then
        local okEq, same = pcall(a.IsEqualTo, a, b)
        if okEq then
            return same ~= true
        end
    end
    if type(a.GetRGBA) ~= "function" or type(b.GetRGBA) ~= "function" then
        return false
    end
    local okA, ar, ag, ab, aa = pcall(a.GetRGBA, a)
    if not okA then
        return false
    end
    local okB, br, bg, bb, ba = pcall(b.GetRGBA, b)
    if not okB then
        return false
    end
    local vals = { ar, ag, ab, aa, br, bg, bb, ba }
    for i = 1, #vals do
        if type(issecretvalue) == "function" then
            local okSecret, secret = pcall(issecretvalue, vals[i])
            if okSecret and secret == true then
                return false
            end
        end
        if type(vals[i]) ~= "number" then
            return false
        end
    end
    if math.abs(ar - br) > PARTY_HAZARD_CURVE_EPSILON then return true end
    if math.abs(ag - bg) > PARTY_HAZARD_CURVE_EPSILON then return true end
    if math.abs(ab - bb) > PARTY_HAZARD_CURVE_EPSILON then return true end
    if math.abs(aa - ba) > PARTY_HAZARD_CURVE_EPSILON then return true end
    return false
end

GetPartyHazardTintCurve = function()
    if _partyHazardTintCurve then
        return _partyHazardTintCurve
    end
    if not C_CurveUtil or not C_CurveUtil.CreateColorCurve then
        return nil
    end
    if not Enum or not Enum.LuaCurveType then
        return nil
    end
    local curve = C_CurveUtil.CreateColorCurve()
    curve:SetType(Enum.LuaCurveType.Step)
    curve:AddPoint(PARTY_HAZARD_ENUM.None, CreateColor(0, 0, 0, 0))
    for _, enum in ipairs(PARTY_HAZARD_PRIORITY) do
        local c = PARTY_HAZARD_COLORS[enum]
        if c then
            curve:AddPoint(enum, CreateColor(c[1], c[2], c[3], 1))
        end
    end
    _partyHazardTintCurve = curve
    return curve
end

local function GetPartyHazardProbeCurve(targetEnum)
    local normalized = NormalizePartyHazardEnum(targetEnum)
    if not normalized then
        return nil
    end
    if _partyHazardProbeCurves[normalized] then
        return _partyHazardProbeCurves[normalized]
    end
    if not C_CurveUtil or not C_CurveUtil.CreateColorCurve then
        return nil
    end
    if not Enum or not Enum.LuaCurveType then
        return nil
    end

    local curve = C_CurveUtil.CreateColorCurve()
    curve:SetType(Enum.LuaCurveType.Step)

    local function Add(enumValue)
        local pointEnum = NormalizePartyHazardEnum(enumValue)
        local alpha = (pointEnum == normalized) and 1 or 0
        curve:AddPoint(enumValue, CreateColor(1, 1, 1, alpha))
    end

    Add(PARTY_HAZARD_ENUM.None)
    Add(PARTY_HAZARD_ENUM.Magic)
    Add(PARTY_HAZARD_ENUM.Curse)
    Add(PARTY_HAZARD_ENUM.Disease)
    Add(PARTY_HAZARD_ENUM.Poison)

    _partyHazardProbeCurves[normalized] = curve
    _partyHazardProbeMatchColors[normalized] = CreateColor(1, 1, 1, 1)
    return curve
end

local function GetPartyHazardZeroProbeCurve()
    if _partyHazardZeroProbeCurve then
        return _partyHazardZeroProbeCurve
    end
    if not C_CurveUtil or not C_CurveUtil.CreateColorCurve then
        return nil
    end
    if not Enum or not Enum.LuaCurveType then
        return nil
    end

    local curve = C_CurveUtil.CreateColorCurve()
    curve:SetType(Enum.LuaCurveType.Step)
    curve:AddPoint(PARTY_HAZARD_ENUM.None, CreateColor(1, 1, 1, 0))
    curve:AddPoint(PARTY_HAZARD_ENUM.Magic, CreateColor(1, 1, 1, 0))
    curve:AddPoint(PARTY_HAZARD_ENUM.Curse, CreateColor(1, 1, 1, 0))
    curve:AddPoint(PARTY_HAZARD_ENUM.Disease, CreateColor(1, 1, 1, 0))
    curve:AddPoint(PARTY_HAZARD_ENUM.Poison, CreateColor(1, 1, 1, 0))
    _partyHazardZeroProbeCurve = curve
    return curve
end

local function ResolvePartyHazardEnumByCurve(unit, auraInstanceID)
    if not unit or not auraInstanceID then
        return nil
    end
    if not C_UnitAuras or type(C_UnitAuras.GetAuraDispelTypeColor) ~= "function" then
        return nil
    end

    local zeroCurve = GetPartyHazardZeroProbeCurve()
    for _, enum in ipairs(PARTY_HAZARD_PRIORITY) do
        local curve = GetPartyHazardProbeCurve(enum)
        if curve then
            local okColor, color = pcall(C_UnitAuras.GetAuraDispelTypeColor, unit, auraInstanceID, curve)
            if okColor and color then
                if zeroCurve then
                    local okZero, zeroColor = pcall(C_UnitAuras.GetAuraDispelTypeColor, unit, auraInstanceID, zeroCurve)
                    if okZero and IsPartyCurveColorDifferent(color, zeroColor) then
                        return enum
                    end
                end
                local expected = _partyHazardProbeMatchColors[NormalizePartyHazardEnum(enum)]
                if expected and IsPartyCurveColorMatch(color, expected) then
                    return enum
                end
            end
        end
    end
    return nil
end

local function ResolvePartyHazardFromAuraInstanceID(unit, auraInstanceID, typeHint)
    local dt = NormalizePartyHazardEnum(typeHint)
    if dt then
        return dt, "blizz:type"
    end
    dt = ResolvePartyHazardEnumByCurve(unit, auraInstanceID)
    if dt then
        return dt, "blizz:curve"
    end
    return nil, nil
end

local function ResolvePartyHazardFromAura(unit, aura)
    if not aura then
        return nil, nil
    end

    local safeSpellId = nil
    pcall(function()
        local id = aura.spellId
        if id ~= nil and type(id) == "number" then
            local _test = {[id] = true}
            safeSpellId = id
        end
    end)

    local dt = NormalizePartyHazardEnum(aura.dispelType)
    if dt then
        if safeSpellId then _partySpellTypeCache[safeSpellId] = aura.dispelType end
        return dt, "field:dispelType"
    end
    dt = NormalizePartyHazardEnum(aura.dispelName)
    if dt then
        if safeSpellId then _partySpellTypeCache[safeSpellId] = aura.dispelName end
        return dt, "field:dispelName"
    end
    dt = NormalizePartyHazardEnum(aura.debuffType)
    if dt then
        if safeSpellId then _partySpellTypeCache[safeSpellId] = aura.debuffType end
        return dt, "field:debuffType"
    end
    dt = NormalizePartyHazardEnum(aura.type)
    if dt then
        if safeSpellId then _partySpellTypeCache[safeSpellId] = aura.type end
        return dt, "field:type"
    end
    if safeSpellId and _partySpellTypeCache[safeSpellId] then
        dt = NormalizePartyHazardEnum(_partySpellTypeCache[safeSpellId])
        if dt then
            return dt, "field:spellCache"
        end
    end
    pcall(function()
        if aura.auraInstanceID then
            dt = ResolvePartyHazardEnumByCurve(unit, aura.auraInstanceID)
        end
    end)
    if dt then
        return dt, "curve"
    end
    return nil, nil
end

local function CollectPartyHazardsForUnit(unit)
    local diag = {
        unit = tostring(unit or "nil"),
        inRestricted = (InCombatLockdown and InCombatLockdown()) and "YES" or "NO",
        scanned = 0, fieldHits = 0, curveHits = 0, blizzHits = 0,
        blizzDebuffs = 0, blizzDispellable = 0, trackerHits = 0, condHits = 0,
        blizzAvail = "NO", trackerAvail = "NO", trackerMethod = "none",
        trackerTypes = "none", condSource = "none", condActive = "NO",
        condSecondary = "none", condSecondaryRGB = "none", condRGB = "none",
        condOverride = "NO", condSecret = "NO",
        customPrimaryColor = nil, customSecondaryColor = nil,
        curveColorObj = nil,
    }

    local primaryEnum, secondaryEnum = nil, nil
    local tintCurve = GetPartyHazardTintCurve()

    local cache = _partyAuraEventCache[unit]
    if cache then
        for iid, aura in pairs(cache) do
            if primaryEnum and secondaryEnum then break end
            diag.scanned = diag.scanned + 1

            local enum = nil
            local sid = aura and aura.spellId
            local safeSid = nil
            if sid then
                pcall(function()
                    local _t = {[sid] = true}
                    safeSid = sid
                end)
            end
            if safeSid and _partySpellTypeCache[safeSid] then
                enum = NormalizePartyHazardEnum(_partySpellTypeCache[safeSid])
                if enum then diag.fieldHits = diag.fieldHits + 1 end
            end

            if not enum and aura then
                local dn = aura.dispelName
                if dn and not issecretvalue(dn) then
                    enum = NormalizePartyHazardEnum(dn)
                    if enum then
                        diag.fieldHits = diag.fieldHits + 1
                        if safeSid then _partySpellTypeCache[safeSid] = dn end
                    end
                end
            end

            if not enum and tintCurve and aura and aura.dispelName ~= nil and C_UnitAuras.GetAuraDispelTypeColor then
                local okC, color = pcall(C_UnitAuras.GetAuraDispelTypeColor, unit, iid, tintCurve)
                if okC and color then
                    if not primaryEnum then
                        primaryEnum = PARTY_HAZARD_ENUM.Unknown
                        diag.curveColorObj = color
                        diag.curveHits = diag.curveHits + 1
                    elseif not secondaryEnum then
                        secondaryEnum = PARTY_HAZARD_ENUM.Unknown
                        diag.customSecondaryColor = color
                        diag.curveHits = diag.curveHits + 1
                    end
                end
            end

            if enum then
                if not primaryEnum then
                    primaryEnum = enum
                    diag.customPrimaryColor = PARTY_HAZARD_COLORS[enum]
                        and { PARTY_HAZARD_COLORS[enum][1], PARTY_HAZARD_COLORS[enum][2], PARTY_HAZARD_COLORS[enum][3] }
                        or nil
                elseif not secondaryEnum and enum ~= primaryEnum then
                    secondaryEnum = enum
                    diag.customSecondaryColor = PARTY_HAZARD_COLORS[enum]
                        and { PARTY_HAZARD_COLORS[enum][1], PARTY_HAZARD_COLORS[enum][2], PARTY_HAZARD_COLORS[enum][3] }
                        or nil
                end
            end
        end
    end

    if not primaryEnum and C_UnitAuras and C_UnitAuras.GetAuraDataByIndex then
        for i = 1, 40 do
            local okAura, aura = pcall(C_UnitAuras.GetAuraDataByIndex, unit, i, "HARMFUL")
            if not okAura or not aura then break end
            diag.scanned = diag.scanned + 1
            local dn = aura.dispelName
            if dn and not issecretvalue(dn) then
                local enum = NormalizePartyHazardEnum(dn)
                if enum then
                    diag.fieldHits = diag.fieldHits + 1
                    if not primaryEnum then
                        primaryEnum = enum
                        diag.customPrimaryColor = PARTY_HAZARD_COLORS[enum]
                            and { PARTY_HAZARD_COLORS[enum][1], PARTY_HAZARD_COLORS[enum][2], PARTY_HAZARD_COLORS[enum][3] }
                            or nil
                    elseif not secondaryEnum and enum ~= primaryEnum then
                        secondaryEnum = enum
                        break
                    end
                end
            end
            if primaryEnum and secondaryEnum then break end
        end
    end

    diag.primary = PARTY_HAZARD_LABELS[primaryEnum] or "NONE"
    diag.secondary = PARTY_HAZARD_LABELS[secondaryEnum] or "NONE"
    diag.reason = primaryEnum and "resolved" or (diag.scanned == 0 and "no-harmful-auras" or "unresolved")

    return primaryEnum, secondaryEnum, diag
end

local function BuildPartyDebuffOverlay(frame)
    if not frame or frame._muiDebuffOverlay then
        return frame and frame._muiDebuffOverlay
    end

    local overlay = CreateFrame("Frame", nil, frame)
    overlay:SetAllPoints(frame)
    overlay:SetFrameLevel(frame:GetFrameLevel() + 3)
    overlay:SetAlpha(1)
    overlay:Show()
    overlay._muiOwnerFrame = frame
    overlay._muiActive = false

    local tintTex = overlay:CreateTexture(nil, "ARTWORK")
    tintTex:SetTexture("Interface\\Buttons\\WHITE8X8")
    tintTex:SetBlendMode("BLEND")
    tintTex:SetAlpha(0)
    tintTex:Hide()
    overlay.tintTex = tintTex

    local tintFadeIn = tintTex:CreateAnimationGroup()
    local tintInAnim = tintFadeIn:CreateAnimation("Alpha")
    tintInAnim:SetFromAlpha(0)
    tintInAnim:SetToAlpha(0.92)
    tintInAnim:SetDuration(0.30)
    tintInAnim:SetSmoothing("OUT")
    tintFadeIn:SetScript("OnPlay", function()
        tintTex:SetAlpha(0)
        tintTex:Show()
    end)
    tintFadeIn:SetScript("OnFinished", function(_, requested)
        if not requested then tintTex:SetAlpha(0.92) end
    end)
    overlay.tintFadeIn = tintFadeIn

    local tintFadeOut = tintTex:CreateAnimationGroup()
    local tintOutAnim = tintFadeOut:CreateAnimation("Alpha")
    tintOutAnim:SetFromAlpha(0.92)
    tintOutAnim:SetToAlpha(0)
    tintOutAnim:SetDuration(0.40)
    tintOutAnim:SetSmoothing("IN_OUT")
    tintFadeOut:SetScript("OnFinished", function(_, requested)
        if not requested then
            tintTex:SetAlpha(0)
            tintTex:Hide()
        end
    end)
    overlay.tintFadeOut = tintFadeOut

    local splitClip = CreateFrame("Frame", nil, overlay)
    splitClip:SetClipsChildren(true)
    splitClip:Hide()
    overlay.splitClip = splitClip

    local SPLIT_ROTATION = -0.12

    local splitTex = splitClip:CreateTexture(nil, "ARTWORK")
    splitTex:SetTexture("Interface\\Buttons\\WHITE8X8")
    splitTex:SetBlendMode("BLEND")
    overlay.splitTex = splitTex

    local splitEdge = splitClip:CreateTexture(nil, "ARTWORK", nil, 1)
    splitEdge:SetTexture("Interface\\Buttons\\WHITE8X8")
    splitEdge:SetBlendMode("BLEND")
    overlay.splitEdge = splitEdge
    overlay.splitSep = nil
    overlay._muiSplitRotation = SPLIT_ROTATION

    local splitSlideIn = splitClip:CreateAnimationGroup()
    local slideInAnim = splitSlideIn:CreateAnimation("Translation")
    slideInAnim:SetOffset(0, 0)
    slideInAnim:SetDuration(0.30)
    slideInAnim:SetSmoothing("OUT")
    splitSlideIn:SetScript("OnPlay", function()
        splitClip:Show()
    end)
    splitSlideIn:SetScript("OnFinished", function(_, requested)
        if requested then return end
        local owner = overlay._muiOwnerFrame
        local hc = owner and owner.healthContainer
        if not hc then return end
        local barH = math.max(1, tonumber(tostring(hc:GetHeight())) or 40)
        local barW = math.max(1, tonumber(tostring(hc:GetWidth())) or 120)
        local halfW = math.max(1, math.floor(barW * 0.5 + 0.5))
        local edgeW = math.max(10, math.floor(halfW * 0.35 + 0.5))
        local edgeShift = math.floor(edgeW * 0.5 + 0.5)
        local fillW = halfW - edgeW + edgeShift
        if overlay.splitEdge then
            overlay.splitEdge:ClearAllPoints()
            overlay.splitEdge:SetPoint("TOPLEFT", hc, "TOP", -edgeShift, 0)
            overlay.splitEdge:SetSize(edgeW, barH)
        end
        overlay.splitTex:ClearAllPoints()
        overlay.splitTex:SetPoint("TOPLEFT", hc, "TOP", edgeW - edgeShift, 0)
        overlay.splitTex:SetSize(fillW, barH)
    end)
    overlay.splitSlideIn = splitSlideIn
    overlay.splitSlideInAnim = slideInAnim

    local splitSlideOut = splitClip:CreateAnimationGroup()
    local slideOutAnim = splitSlideOut:CreateAnimation("Translation")
    slideOutAnim:SetOffset(0, 0)
    slideOutAnim:SetDuration(0.25)
    slideOutAnim:SetSmoothing("IN")
    splitSlideOut:SetScript("OnFinished", function(_, requested)
        if not requested then splitClip:Hide() end
    end)
    overlay.splitSlideOut = splitSlideOut
    overlay.splitSlideOutAnim = slideOutAnim

    local hpBar = CreateFrame("StatusBar", nil, overlay)
    hpBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
    hpBar:SetStatusBarColor(0.2, 0.9, 0.2, 1)
    hpBar:SetMinMaxValues(0, 1)
    hpBar:SetValue(1)
    hpBar:SetHeight(6)
    hpBar:SetFrameLevel(overlay:GetFrameLevel() + 2)
    hpBar:Hide()
    local hpBg = hpBar:CreateTexture(nil, "BACKGROUND")
    hpBg:SetAllPoints()
    hpBg:SetColorTexture(0, 0, 0, 0.6)
    overlay.hpBar = hpBar

    overlay.fadeIn = tintFadeIn
    overlay.fadeOut = tintFadeOut

    frame._muiDebuffOverlay = overlay
    return overlay
end

local function CoerceNumber(val)
    if val == nil then return nil end
    return tonumber(tostring(val))
end

local function GetPartyDebuffOverlayDisplaySize(frame)
    local width = (frame and CoerceNumber(frame:GetWidth())) or 0
    local height = (frame and CoerceNumber(frame:GetHeight())) or 0
    if width <= 1 then width = tonumber(config.width) or 240 end
    if height <= 1 then height = tonumber(config.height) or 58 end

    return width, height
end

-- ============================================================
-- SECTION: Debuff Overlay Rendering + Dispel Tracking Overlay
-- LayoutPartyDebuffOverlay positions the tint strip and icon row.
-- UpdatePartyDebuffOverlay applies collected hazard state:
-- tint color to strip, icons to row, handles secret values via
-- SetVertexColor.  EnsurePartyDispelTrackingOverlay builds and
-- positions the separate dispel-able debuff icon row shown above
-- each party frame; driven by CollectPartyDispelTrackingAuraEntry.
--
-- READS:  hazard state from CollectPartyHazardsForUnit,
--         C_UnitAuras HARMFUL slots for dispel tracking
-- WRITES: overlay._muiTintStrip vertex color,
--         overlay._muiIcons[] textures, dispelOverlay icon row
--
-- FUNCTIONS:
--   LayoutPartyDebuffOverlay(overlay,frame) — SetPoint for strip
--     and icon row relative to frame dimensions
--   UpdatePartyDebuffOverlay(frame,unit) — classify → apply;
--     handles PARTY_HAZARD_STICKY_SEC debounce
--   CollectPartyDispelTrackingAuraEntry(unit,inRestricted) —
--     returns array of dispellable aura entries for the unit
--   EnsurePartyDispelTrackingOverlay(frame,unit) — builds or
--     refreshes the dispel icon row; respects filter settings
--   MidnightUI_RefreshPartyDispelTrackingOverlay(forceHide)
--     — public refresh called after settings change
-- ============================================================
local function LayoutPartyDebuffOverlay(overlay, frame)
    if not overlay or not frame then
        return
    end

    local overlayWidth = GetPartyDebuffOverlayDisplaySize(frame)

    overlay:ClearAllPoints()
    overlay:SetAllPoints(frame)

    if overlay.tintTex then
        local hb = frame.healthBar
        local hbTex = hb and hb.GetStatusBarTexture and hb:GetStatusBarTexture()
        if hbTex then
            overlay.tintTex:ClearAllPoints()
            overlay.tintTex:SetAllPoints(hbTex)
        elseif hb then
            overlay.tintTex:ClearAllPoints()
            overlay.tintTex:SetAllPoints(hb)
        end
    end

    if overlay.splitClip then
        local hc = frame.healthContainer
        if hc then
            overlay.splitClip:ClearAllPoints()
            overlay.splitClip:SetAllPoints(hc)
        end
    end

    if overlay.hpBar then
        local hc = frame.healthContainer
        if hc then
            overlay.hpBar:ClearAllPoints()
            overlay.hpBar:SetPoint("BOTTOMLEFT", hc, "BOTTOMLEFT", 0, 0)
            overlay.hpBar:SetPoint("BOTTOMRIGHT", hc, "BOTTOMRIGHT", 0, 0)
        end
        if not overlay._muiSplitShown then
            overlay.hpBar:Hide()
        end
    end
end

local function LayoutPartyDebuffSweep(overlay, frame) end

local function GetPartyUnknownFallbackColor(frame)
    local classColor = frame and frame._muiClassColor
    local r, g, b = nil, nil, nil
    if type(classColor) == "table" then
        r = tonumber(classColor.r or classColor[1])
        g = tonumber(classColor.g or classColor[2])
        b = tonumber(classColor.b or classColor[3])
    end
    if r == nil or g == nil or b == nil then
        r = DEFAULT_CLASS_COLOR.r or 0.5
        g = DEFAULT_CLASS_COLOR.g or 0.5
        b = DEFAULT_CLASS_COLOR.b or 0.5
    end
    return { ClampPartyUnitColor(r), ClampPartyUnitColor(g), ClampPartyUnitColor(b) }
end

local function ClonePartyColor(color)
    if type(color) ~= "table" then
        return nil
    end
    if color.secret == true then
        if color[1] == nil or color[2] == nil or color[3] == nil then
            return nil
        end
        return { color[1], color[2], color[3], secret = true, source = color.source }
    end
    local r = tonumber(color[1] or color.r)
    local g = tonumber(color[2] or color.g)
    local b = tonumber(color[3] or color.b)
    if not r or not g or not b then
        return nil
    end
    return { ClampPartyUnitColor(r), ClampPartyUnitColor(g), ClampPartyUnitColor(b) }
end

local function IsPartySecretColor(color)
    return type(color) == "table" and color.secret == true
end

local function IsPartyUnknownHazardSource(source)
    return type(source) == "string" and string.find(source, "unknown", 1, true) ~= nil
end

local function SetPartyRenderedSecretPolish(bar, suppressed)
    if not bar then
        return
    end
    if suppressed ~= true then
        bar._muiPartySecretRenderedPolish = false
        return
    end
    if bar._muiRenderedTop then
        pcall(bar._muiRenderedTop.SetBlendMode, bar._muiRenderedTop, "ADD")
        bar._muiRenderedTop:SetGradient("VERTICAL",
            CreateColor(0.09, 0.09, 0.09, 1),
            CreateColor(0.00, 0.00, 0.00, 1))
        bar._muiRenderedTop:Show()
    end
    if bar._muiRenderedBottom then
        pcall(bar._muiRenderedBottom.SetBlendMode, bar._muiRenderedBottom, "ADD")
        bar._muiRenderedBottom:SetGradient("VERTICAL",
            CreateColor(0.00, 0.00, 0.00, 1),
            CreateColor(0.09, 0.09, 0.09, 1))
        bar._muiRenderedBottom:Show()
    end
    if bar._muiRenderedGloss then
        bar._muiRenderedGloss:Hide()
    end
    bar._muiPartySecretRenderedPolish = true
end

local function SetPartyDebuffPrimaryColor(overlay, frame, enum, customColor)
    local color = customColor
    if not color then
        if enum == PARTY_HAZARD_ENUM.Unknown then
            color = GetPartyUnknownFallbackColor(frame)
        else
            color = PARTY_HAZARD_COLORS[enum]
        end
    end
    if IsPartySecretColor(color) then
        if overlay.tintTex then
            overlay.tintTex:SetVertexColor(color[1], color[2], color[3], 1)
        end
        return
    end
    if not color then return end
    if overlay.tintTex then
        overlay.tintTex:SetVertexColor(color[1], color[2], color[3], 1)
    end
end

local function SetPartyDebuffSecondaryColor(overlay, enum, customColor)
    if not overlay or not overlay.splitTex then return end
    local color = customColor
    if IsPartySecretColor(color) then
        color = { color[1], color[2], color[3] }
    end
    if not color then
        if enum == PARTY_HAZARD_ENUM.Unknown then
            color = PARTY_UNKNOWN_SWEEP_COLOR
        else
            color = PARTY_HAZARD_COLORS[enum]
        end
    end
    if not color then return end
    local cr, cg, cb
    if color.GetRGBA then
        cr, cg, cb = color:GetRGBA()
    else
        cr, cg, cb = color[1], color[2], color[3]
    end
    if not (cr and cg and cb) then return end
    overlay.splitTex:SetVertexColor(cr, cg, cb, 0.92)
    if overlay.splitEdge and overlay.splitEdge.SetGradient and CreateColor then
        local okMin, minC = pcall(CreateColor, cr, cg, cb, 0)
        local okMax, maxC = pcall(CreateColor, cr, cg, cb, 0.92)
        if okMin and okMax and minC and maxC then
            pcall(overlay.splitEdge.SetGradient, overlay.splitEdge, "HORIZONTAL", minC, maxC)
        end
    end
end


local function SetPartyDebuffSweepVisible(overlay, isVisible)
    if not overlay or not overlay.splitClip then return end

    if isVisible then
        if overlay._muiSplitShown then

            return
        end
        overlay._muiSplitShown = true


        local owner = overlay._muiOwnerFrame
        local hc = owner and owner.healthContainer
        if not hc then return end

        local barW = math.max(1, CoerceNumber(hc:GetWidth()) or 120)
        local barH = math.max(1, CoerceNumber(hc:GetHeight()) or 40)
        local halfW = math.max(1, math.floor(barW * 0.5 + 0.5))

        if overlay.splitSlideOut and overlay.splitSlideOut:IsPlaying() then
            overlay.splitSlideOut:Stop()
        end

        overlay.splitTex:ClearAllPoints()
        overlay.splitTex:SetPoint("TOPLEFT", hc, "TOP", 0, 0)
        overlay.splitTex:SetPoint("BOTTOMRIGHT", hc, "BOTTOMRIGHT", 0, 0)

        if overlay.splitSep then
            overlay.splitSep:ClearAllPoints()
            overlay.splitSep:SetPoint("TOP", overlay.splitTex, "TOPLEFT", 0, 2)
            overlay.splitSep:SetPoint("BOTTOM", overlay.splitTex, "BOTTOMLEFT", 0, -2)
        end

        if overlay.splitSlideInAnim then
            overlay.splitSlideInAnim:SetOffset(-halfW, 0)
        end
        local edgeW = math.max(10, math.floor(halfW * 0.35 + 0.5))
        local edgeShift = math.floor(edgeW * 0.5 + 0.5)
        local fillW = halfW - edgeW + edgeShift

        if overlay.splitEdge then
            overlay.splitEdge:ClearAllPoints()
            overlay.splitEdge:SetPoint("TOPLEFT", hc, "TOPRIGHT", -edgeShift, 0)
            overlay.splitEdge:SetSize(edgeW, barH)
        end
        overlay.splitTex:ClearAllPoints()
        overlay.splitTex:SetPoint("TOPLEFT", hc, "TOPRIGHT", edgeW - edgeShift, 0)
        overlay.splitTex:SetSize(fillW, barH)

        overlay.splitClip:Show()
        if overlay.splitSlideIn and not overlay.splitSlideIn:IsPlaying() then
            overlay.splitSlideIn:Play()
        end
        if overlay.hpBar then
            local hb = owner and owner.healthBar
            if hb then
                local _, maxVal = hb:GetMinMaxValues()
                local curVal = hb:GetValue()
                if maxVal and maxVal > 0 then
                    overlay.hpBar:SetMinMaxValues(0, maxVal)
                    overlay.hpBar:SetValue(curVal)
                end
            end
            overlay.hpBar:Show()
        end
    else
        if not overlay._muiSplitShown then

            return
        end
        overlay._muiSplitShown = false


        local owner = overlay._muiOwnerFrame
        local hc = owner and owner.healthContainer
        local halfW = hc and math.max(1, math.floor((CoerceNumber(hc:GetWidth()) or 120) * 0.5 + 0.5)) or 60

        if overlay.splitSlideIn and overlay.splitSlideIn:IsPlaying() then
            overlay.splitSlideIn:Stop()
        end
        if overlay.splitSlideOutAnim then
            overlay.splitSlideOutAnim:SetOffset(halfW, 0)
        end
        if overlay.splitClip:IsShown() then
            if overlay.splitSlideOut and not overlay.splitSlideOut:IsPlaying() then
                overlay.splitSlideOut:Play()
            end
        end
        if overlay.hpBar then overlay.hpBar:Hide() end
    end
end

local function ApplyPartyCurveColorToBar(frame, curveColor)
    if not frame or not curveColor then return end
    local unit = frame.GetAttribute and frame:GetAttribute("unit") or "?"
    local baseMul = GetPartyDebuffTintBaseMultiplier()

    frame._muiDebuffBarTintActive = true
    frame._muiDebuffBarTintEnum = PARTY_HAZARD_ENUM.Unknown
    frame._muiDebuffBarTintRGB = "curve"
    frame._muiDebuffBarTintSecret = true

    local cR, cG, cB, cA
    local okExtract = pcall(function() cR, cG, cB, cA = curveColor:GetRGBA() end)

    if _G.MidnightUI_Diagnostics and _G.MidnightUI_Diagnostics.LogDebugSource then
        local inRestricted = (InCombatLockdown and InCombatLockdown()) and "YES" or "NO"
        local cRnil = (cR == nil) and "nil" or "num"
        local cRsecret = (cR ~= nil and issecretvalue and issecretvalue(cR)) and "YES" or "NO"
        local hasHealth = (frame.healthBar and frame.healthBar:IsShown()) and "YES" or "NO"
        _G.MidnightUI_Diagnostics.LogDebugSource("PartyCurve",
            string.format("unit=%s inRestricted=%s extractOK=%s cR=%s cRsecret=%s hasHealth=%s",
                tostring(unit), inRestricted, tostring(okExtract), cRnil, cRsecret, hasHealth))
    end

    local function ApplyCurveToBar(bar)
        if not bar or not bar:IsShown() or not cR then return end
        local tex = bar.GetStatusBarTexture and bar:GetStatusBarTexture()
        if tex and tex.SetVertexColor then
            tex:SetVertexColor(cR, cG, cB, cA or 1)
        else
            bar:SetStatusBarColor(cR, cG, cB, cA or 1)
        end
        for _, key in ipairs({"_muiRenderedTop", "_muiRenderedBottom", "_muiRenderedGloss",
                              "_muiTopHighlight", "_muiBottomShade", "_muiSpecular",
                              "_muiGradient", "_muiFlatGradient"}) do
            local overlay = bar[key]
            if overlay and overlay.Hide then overlay:Hide() end
        end
    end
    ApplyCurveToBar(frame.healthBar)
    ApplyCurveToBar(frame.powerBar)

end

local function ApplyPartyDebuffBarTint(frame, primaryEnum, isActive, customColor)
    if not frame then
        return
    end

    if not isActive then
        frame._muiDebuffBarTintActive = false
        frame._muiDebuffBarTintEnum = nil
        frame._muiDebuffBarTintRGB = nil
        frame._muiDebuffBarTintSecret = false
        SetPartyRenderedSecretPolish(frame.healthBar, false)
        SetPartyRenderedSecretPolish(frame.powerBar, false)
        if config.style == "Simple" then
            ClearBarOverlays(frame.healthBar)
            ClearBarOverlays(frame.powerBar)
        end
        return
    end

    local color = customColor
    if not color then
        if primaryEnum == PARTY_HAZARD_ENUM.Unknown then
            color = GetPartyUnknownFallbackColor(frame)
        else
            color = PARTY_HAZARD_COLORS[primaryEnum]
        end
    end

    if IsPartySecretColor(color) then
        local fallback = GetPartyUnknownFallbackColor(frame)
        if primaryEnum and primaryEnum ~= PARTY_HAZARD_ENUM.Unknown and PARTY_HAZARD_COLORS[primaryEnum] then
            fallback = PARTY_HAZARD_COLORS[primaryEnum]
        end
        local fr, fg, fb = fallback[1], fallback[2], fallback[3]
        local baseMul = GetPartyDebuffTintBaseMultiplier()

        frame._muiDebuffBarTintActive = true
        frame._muiDebuffBarTintEnum = primaryEnum
        frame._muiDebuffBarTintRGB = "secret"
        frame._muiDebuffBarTintSecret = true

        if frame.healthBar and frame.healthBar:IsShown() then
            if config.style == "Rendered" then
                ApplyRenderedBarStyle(frame.healthBar, baseMul, baseMul, baseMul, 1.0, 0.20, 0.34)
                SetPartyRenderedSecretPolish(frame.healthBar, true)
            elseif config.style == "Simple" then
                ApplyPartyFlatStatusBarTint(frame.healthBar, baseMul, baseMul, baseMul, 1.0)
                SetPartyRenderedSecretPolish(frame.healthBar, false)
            else
                ApplyStatusBarGradient(frame.healthBar, baseMul, baseMul, baseMul, 1.0, 0.20, 0.34)
            end
            local hTex = frame.healthBar.GetStatusBarTexture and frame.healthBar:GetStatusBarTexture()
            if hTex and hTex.SetVertexColor then
                hTex:SetVertexColor(color[1], color[2], color[3], 1.0)
            end
        end

        if frame.powerBar and frame.powerBar:IsShown() then
            if config.style == "Rendered" then
                ApplyRenderedBarStyle(frame.powerBar, baseMul, baseMul, baseMul, 1.0, 0.20, 0.32)
                SetPartyRenderedSecretPolish(frame.powerBar, true)
            elseif config.style == "Simple" then
                ApplyPartyFlatStatusBarTint(frame.powerBar, baseMul, baseMul, baseMul, 1.0)
                SetPartyRenderedSecretPolish(frame.powerBar, false)
            else
                ApplyStatusBarGradient(frame.powerBar, baseMul, baseMul, baseMul, 1.0, 0.16, 0.30)
            end
            local pTex = frame.powerBar.GetStatusBarTexture and frame.powerBar:GetStatusBarTexture()
            if pTex and pTex.SetVertexColor then
                pTex:SetVertexColor(color[1], color[2], color[3], 1.0)
            end
        end

        ApplyPartyDebuffBackgroundTint(frame, fr, fg, fb)
        return
    end

    if not color then
        return
    end

    local r, g, b = color[1], color[2], color[3]
    local darkMul = GetPartyDebuffTintBaseMultiplier()
    local dr, dg, db = r * darkMul, g * darkMul, b * darkMul
    frame._muiDebuffBarTintActive = true
    frame._muiDebuffBarTintEnum = primaryEnum
    frame._muiDebuffBarTintRGB = string.format("%.3f,%.3f,%.3f", r, g, b)
    frame._muiDebuffBarTintSecret = false

    if frame.healthBar and frame.healthBar:IsShown() then
        if config.style == "Rendered" then
            ApplyRenderedBarStyle(frame.healthBar, dr, dg, db, 1.0, 0.20, 0.34)
            SetPartyRenderedSecretPolish(frame.healthBar, false)
        elseif config.style == "Simple" then
            ApplyPartyFlatStatusBarTint(frame.healthBar, dr, dg, db, 1.0)
            SetPartyRenderedSecretPolish(frame.healthBar, false)
        else
            ApplyStatusBarGradient(frame.healthBar, dr, dg, db, 1.0, 0.20, 0.34)
        end
        local hTex = frame.healthBar.GetStatusBarTexture and frame.healthBar:GetStatusBarTexture()
        if hTex and hTex.SetVertexColor then
            hTex:SetVertexColor(dr, dg, db, 1.0)
        end
    end

    if frame.powerBar and frame.powerBar:IsShown() then
        if config.style == "Rendered" then
            ApplyRenderedBarStyle(frame.powerBar, dr, dg, db, 1.0, 0.20, 0.32)
            SetPartyRenderedSecretPolish(frame.powerBar, false)
        elseif config.style == "Simple" then
            ApplyPartyFlatStatusBarTint(frame.powerBar, dr, dg, db, 1.0)
            SetPartyRenderedSecretPolish(frame.powerBar, false)
        else
            ApplyStatusBarGradient(frame.powerBar, dr, dg, db, 1.0, 0.16, 0.30)
        end
        local pTex = frame.powerBar.GetStatusBarTexture and frame.powerBar:GetStatusBarTexture()
        if pTex and pTex.SetVertexColor then
            pTex:SetVertexColor(dr, dg, db, 1.0)
        end
    end

    ApplyPartyDebuffBackgroundTint(frame, r, g, b)

end

local function GetPartyDebuffNow()
    if type(GetTime) == "function" then
        local okNow, now = pcall(GetTime)
        if okNow and type(now) == "number" then
            return now
        end
    end
    return 0
end

local function ApplyPartyStickyHazardFallback(frame, primaryEnum, secondaryEnum, primaryColor, collectDiag)
    local normalizedPrimary = NormalizePartyHazardEnum(primaryEnum)
    local normalizedSecondary = NormalizePartyHazardEnum(secondaryEnum)
    if normalizedSecondary == normalizedPrimary then
        normalizedSecondary = nil
    end

    local resolvedPrimaryColor = ClonePartyColor(primaryColor)
    local inRestricted = (collectDiag and collectDiag.inRestricted == "YES")
    local condActive = (collectDiag and collectDiag.condActive == "YES")
    local condUnknown = IsPartyUnknownHazardSource(collectDiag and collectDiag.condSource)
    local holdStickyForUnknown = condActive and condUnknown
    local now = GetPartyDebuffNow()

    if normalizedPrimary and normalizedPrimary ~= PARTY_HAZARD_ENUM.Unknown then
        frame._muiDebuffStickyPrimary = normalizedPrimary
        frame._muiDebuffStickySecondary = (normalizedSecondary and normalizedSecondary ~= PARTY_HAZARD_ENUM.Unknown) and normalizedSecondary or nil
        frame._muiDebuffStickyAt = now
        if not resolvedPrimaryColor then
            resolvedPrimaryColor = ClonePartyColor(PARTY_HAZARD_COLORS[normalizedPrimary])
        end
        frame._muiDebuffStickyPrimaryColor = ClonePartyColor(resolvedPrimaryColor)
        return normalizedPrimary, normalizedSecondary, resolvedPrimaryColor, false
    end

    local stickyPrimary = NormalizePartyHazardEnum(frame._muiDebuffStickyPrimary)
    local stickySecondary = NormalizePartyHazardEnum(frame._muiDebuffStickySecondary)
    local stickyColor = ClonePartyColor(frame._muiDebuffStickyPrimaryColor)
    local stickyAt = tonumber(frame._muiDebuffStickyAt) or 0
    local stickyFresh = stickyPrimary
        and now > 0
        and stickyAt > 0
        and (now - stickyAt) <= PARTY_HAZARD_STICKY_SEC

    if stickyPrimary and (stickyFresh or holdStickyForUnknown) then
        if not normalizedPrimary or normalizedPrimary == PARTY_HAZARD_ENUM.Unknown then
            normalizedPrimary = stickyPrimary
            resolvedPrimaryColor = stickyColor or resolvedPrimaryColor
        end
        if (not normalizedSecondary or normalizedSecondary == PARTY_HAZARD_ENUM.Unknown or normalizedSecondary == normalizedPrimary)
            and stickySecondary
            and stickySecondary ~= normalizedPrimary then
            normalizedSecondary = stickySecondary
        end
        return normalizedPrimary, normalizedSecondary, resolvedPrimaryColor, true
    end

    if (not inRestricted) and (not condActive) and (not normalizedPrimary or normalizedPrimary == PARTY_HAZARD_ENUM.Unknown) then
        frame._muiDebuffStickyPrimary = nil
        frame._muiDebuffStickySecondary = nil
        frame._muiDebuffStickyAt = nil
        frame._muiDebuffStickyPrimaryColor = nil
    end

    return normalizedPrimary, normalizedSecondary, resolvedPrimaryColor, false
end

local function LogPartyDebuffFX(overlay, tag, extra)
    if not _G.MidnightUI_Diagnostics or not _G.MidnightUI_Diagnostics.LogDebugSource then
        return
    end
    local owner = overlay and overlay._muiOwnerFrame
    local unit = "?"
    if owner and owner.GetAttribute then
        local ok, u = pcall(owner.GetAttribute, owner, "unit")
        if ok and u then unit = tostring(u) end
    end
    local inCombat = (InCombatLockdown and InCombatLockdown()) and "YES" or "NO"
    local tintAlpha = (overlay and overlay.tintTex) and string.format("%.2f", overlay.tintTex:GetAlpha() or 0) or "N/A"
    local splitShown = (overlay and overlay.splitClip and overlay.splitClip:IsShown()) and "YES" or "NO"
    local shown = (overlay and overlay:IsShown()) and "YES" or "NO"
    local msg = string.format("%s unit=%s combat=%s shown=%s tint=%s split=%s%s",
        tag, unit, inCombat, shown, tintAlpha, splitShown,
        extra and (" " .. extra) or "")
    pcall(_G.MidnightUI_Diagnostics.LogDebugSource, "PartyDebuffFX", msg)
end

local function SetPartyDebuffOverlayActive(overlay, isActive)
    if not overlay then return end
    if isActive then
        local wasActive = overlay._muiActive == true
        if overlay.tintFadeOut and overlay.tintFadeOut:IsPlaying() then
            overlay.tintFadeOut:Stop()
        end
        if overlay.tintTex then
            overlay.tintTex:Show()
            if not wasActive and overlay.tintFadeIn and not overlay.tintFadeIn:IsPlaying() then
                overlay.tintFadeIn:Play()
            else
                overlay.tintTex:SetAlpha(0.92)
            end
        end
        overlay._muiActive = true
        local owner = overlay._muiOwnerFrame
        if owner then owner._muiDebuffBarTintActive = true end
    else
        local wasActive = overlay._muiActive == true
        if overlay.tintFadeIn and overlay.tintFadeIn:IsPlaying() then
            overlay.tintFadeIn:Stop()
        end
        SetPartyDebuffSweepVisible(overlay, false)
        if overlay.hpBar then overlay.hpBar:Hide() end
        if overlay.tintTex and overlay.tintTex:IsShown() then
            if overlay.tintFadeOut and not overlay.tintFadeOut:IsPlaying() then
                overlay.tintFadeOut:Play()

            end
        elseif overlay.tintTex then
            overlay.tintTex:SetAlpha(0)
            overlay.tintTex:Hide()

        end
        overlay._muiActive = false
        local owner = overlay._muiOwnerFrame
        if owner then owner._muiDebuffBarTintActive = false end
    end
end

local function UpdatePartyDebuffOverlay(frame, unit)
    if not frame or not unit then
        return
    end
    local overlay = BuildPartyDebuffOverlay(frame)
    if not overlay then
        return
    end

    LayoutPartyDebuffOverlay(overlay, frame)
    LayoutPartyDebuffSweep(overlay, frame)
    local primaryEnum, secondaryEnum, collectDiag = CollectPartyHazardsForUnit(unit)

    local primaryColorOverride = collectDiag and collectDiag.customPrimaryColor or nil
    local secondaryColorOverride = collectDiag and collectDiag.customSecondaryColor or nil
    local stickyApplied = false
    primaryEnum, secondaryEnum, primaryColorOverride, stickyApplied = ApplyPartyStickyHazardFallback(frame, primaryEnum, secondaryEnum, primaryColorOverride, collectDiag)

    if _partyCommBleed[unit] then
        if not primaryEnum then
            primaryEnum = PARTY_HAZARD_ENUM.Unknown
            primaryColorOverride = PARTY_COMM_BLEED_COLOR
        else
            if not secondaryEnum then
                secondaryEnum = PARTY_HAZARD_ENUM.Unknown
            end
            secondaryColorOverride = PARTY_COMM_BLEED_COLOR
        end
    end

    if collectDiag then
        frame._muiDebuffCollectReason = collectDiag.reason
        frame._muiDebuffCollectPrimary = collectDiag.primary
        frame._muiDebuffCollectTracker = collectDiag.trackerTypes
        frame._muiDebuffCollectCond = collectDiag.condSource
        frame._muiDebuffCollectCondActive = collectDiag.condActive
        frame._muiDebuffCollectCondSecondary = collectDiag.condSecondary
        frame._muiDebuffCollectCondSecondaryRGB = collectDiag.condSecondaryRGB
        frame._muiDebuffCollectCondRGB = collectDiag.condRGB
        frame._muiDebuffCollectOverride = collectDiag.condOverride
        frame._muiDebuffCollectCondSecret = collectDiag.condSecret
        frame._muiDebuffCollectBlizzHits = collectDiag.blizzHits
        frame._muiDebuffCollectBlizzDebuffs = collectDiag.blizzDebuffs
        frame._muiDebuffCollectBlizzDispellable = collectDiag.blizzDispellable
        frame._muiDebuffCollectBlizzAvail = collectDiag.blizzAvail
        frame._muiDebuffCollectSticky = stickyApplied and "YES" or "NO"
    end
    local hasPrimary = primaryEnum ~= nil
    local hasSecondary = (secondaryEnum ~= nil and secondaryEnum ~= primaryEnum)
        or (type(secondaryColorOverride) == "table")
    local primaryRenderColor = primaryColorOverride
    if hasPrimary and IsPartySecretColor(primaryColorOverride) then
        local overlayShown = (overlay.IsShown and overlay:IsShown()) and true or false
        if not overlayShown then
            if primaryEnum and primaryEnum ~= PARTY_HAZARD_ENUM.Unknown and PARTY_HAZARD_COLORS[primaryEnum] then
                local canonical = PARTY_HAZARD_COLORS[primaryEnum]
                primaryRenderColor = { canonical[1], canonical[2], canonical[3] }
            else
                primaryRenderColor = GetPartyUnknownFallbackColor(frame)
            end
        end
    end

    if hasPrimary then
        local curveColor = collectDiag and collectDiag.curveColorObj or nil
        if curveColor and primaryEnum == PARTY_HAZARD_ENUM.Unknown and not primaryRenderColor then
            if overlay.tintTex then
                local cr, cg, cb, ca = curveColor:GetRGBA()
                overlay.tintTex:SetVertexColor(cr, cg, cb, 1)
            end
        else
            SetPartyDebuffPrimaryColor(overlay, frame, primaryEnum, primaryRenderColor)
        end
        SetPartyDebuffOverlayActive(overlay, true)
    else
        SetPartyDebuffOverlayActive(overlay, false)
    end

    if hasPrimary and hasSecondary then
        SetPartyDebuffSecondaryColor(overlay, secondaryEnum, secondaryColorOverride)
        SetPartyDebuffSweepVisible(overlay, true)
    else
        SetPartyDebuffSweepVisible(overlay, false)
    end

    UpdatePartyDispelTrackingOverlay(
        frame,
        unit,
        primaryEnum,
        primaryRenderColor,
        collectDiag and collectDiag.inRestricted == "YES",
        false
    )

    local secondaryLabel = PARTY_HAZARD_LABELS[secondaryEnum]
        or ((type(secondaryColorOverride) == "table") and "SECRET")
        or "NONE"
    local signature = (PARTY_HAZARD_LABELS[primaryEnum] or "NONE")
        .. ">" .. secondaryLabel
    if frame._muiDebuffOverlaySig ~= signature then
        frame._muiDebuffOverlaySig = signature
    end
end

local function DarkenColor(r, g, b, factor)
    local f = factor or 0.4
    return (r or 0) * f, (g or 0) * f, (b or 0) * f
end

local function SafeSetBlendMode(tex, mode, label)
    if not tex or not tex.SetBlendMode then return end
    local targetMode = mode or "DISABLE"
    local ok = pcall(tex.SetBlendMode, tex, targetMode)
    if not ok then
        pcall(tex.SetBlendMode, tex, "DISABLE")
    end
end

local function EnsureRenderedOverlays(bar, tex)
    if not bar or not tex then return end
    if not bar._muiRenderedTop then
        bar._muiRenderedTop = bar:CreateTexture(nil, "OVERLAY", nil, 2)
        bar._muiRenderedTop:SetTexture("Interface\\Buttons\\WHITE8X8")
        bar._muiRenderedTop:SetAllPoints(tex)
        SafeSetBlendMode(bar._muiRenderedTop, "DISABLE", "top")
    end
    if not bar._muiRenderedBottom then
        bar._muiRenderedBottom = bar:CreateTexture(nil, "OVERLAY", nil, 1)
        bar._muiRenderedBottom:SetTexture("Interface\\Buttons\\WHITE8X8")
        bar._muiRenderedBottom:SetAllPoints(tex)
        SafeSetBlendMode(bar._muiRenderedBottom, "DISABLE", "bottom")
    end
    if not bar._muiRenderedGloss then
        bar._muiRenderedGloss = bar:CreateTexture(nil, "OVERLAY", nil, 0)
        bar._muiRenderedGloss:SetTexture("Interface\\Buttons\\WHITE8X8")
        bar._muiRenderedGloss:SetAllPoints(tex)
        SafeSetBlendMode(bar._muiRenderedGloss, "DISABLE", "gloss")
    end
end

ApplyRenderedBarStyle = function(bar, r, g, b, a, topAlpha, bottomAlpha)
    if not bar then return end
    bar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
    local tex = bar:GetStatusBarTexture()
    if not tex then return end
    tex:SetHorizTile(false)
    tex:SetVertTile(false)
    bar:SetStatusBarColor(r, g, b, a or 1)
    EnsureRenderedOverlays(bar, tex)
    SafeSetBlendMode(bar._muiRenderedTop, "DISABLE", "top-reset")
    SafeSetBlendMode(bar._muiRenderedBottom, "DISABLE", "bottom-reset")
    SafeSetBlendMode(bar._muiRenderedGloss, "DISABLE", "gloss-reset")
    local function Scale(v, f)
        local n = (v or 0) * f
        if n < 0 then return 0 end
        if n > 1 then return 1 end
        return n
    end
    local edgeR, edgeG, edgeB = Scale(r, 0.54), Scale(g, 0.54), Scale(b, 0.54)
    local midR, midG, midB = Scale(r, 1.15), Scale(g, 1.15), Scale(b, 1.15)
    local rawH = (bar.GetHeight and bar:GetHeight()) or 2
    local h = tonumber(tostring(rawH)) or 2
    local topH = math.max(1, h * 0.43)
    local botH = math.max(1, h * 0.57)

    bar._muiRenderedTop:ClearAllPoints()
    bar._muiRenderedTop:SetPoint("TOPLEFT", tex, "TOPLEFT", 0, 0)
    bar._muiRenderedTop:SetPoint("TOPRIGHT", tex, "TOPRIGHT", 0, 0)
    bar._muiRenderedTop:SetHeight(topH)
    bar._muiRenderedTop:SetGradient("VERTICAL",
        CreateColor(edgeR, edgeG, edgeB, 1),
        CreateColor(midR, midG, midB, 1))

    bar._muiRenderedBottom:ClearAllPoints()
    bar._muiRenderedBottom:SetPoint("BOTTOMLEFT", tex, "BOTTOMLEFT", 0, 0)
    bar._muiRenderedBottom:SetPoint("BOTTOMRIGHT", tex, "BOTTOMRIGHT", 0, 0)
    bar._muiRenderedBottom:SetHeight(botH)
    bar._muiRenderedBottom:SetGradient("VERTICAL",
        CreateColor(midR, midG, midB, 1),
        CreateColor(edgeR, edgeG, edgeB, 1))
    bar._muiRenderedTop:Show()
    bar._muiRenderedBottom:Show()
    if bar._muiRenderedGloss then bar._muiRenderedGloss:Hide() end
    if bar._muiGradient then bar._muiGradient:Hide() end
    if bar._muiFlatGradient then bar._muiFlatGradient:Hide() end
    if bar._muiTopHighlight then bar._muiTopHighlight:Hide() end
    if bar._muiBottomShade then bar._muiBottomShade:Hide() end
    if bar._muiSpecular then bar._muiSpecular:Hide() end
end

local function CreateAuraButton(parent)
    local btn = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    btn:SetSize(config.auraSize, config.auraSize)
    
    btn.icon = btn:CreateTexture(nil, "ARTWORK")
    btn.icon:SetAllPoints()
    btn.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    
    btn.cooldown = CreateFrame("Cooldown", nil, btn, "CooldownFrameTemplate")
    btn.cooldown:SetAllPoints()
    btn.cooldown:SetHideCountdownNumbers(false)
    
    btn.border = CreateFrame("Frame", nil, btn, "BackdropTemplate")
    btn.border:SetAllPoints()
    btn.border:SetBackdrop({edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1})
    btn.border:SetBackdropBorderColor(0, 0, 0, 1)
    
    return btn
end

local function NormalizePartyAuraStackCount(value)
    if value == nil or IsPartySecretValue(value) then
        return ""
    end
    local n = tonumber(value)
    if not n or IsPartySecretValue(n) then
        return ""
    end
    n = math.floor(n + 0.5)
    if n <= 1 then
        return ""
    end
    return tostring(n)
end

local function SetPartyDispelTrackingIconBorderColor(btn, r, g, b, a)
    if not btn or not btn.SetBackdropBorderColor then
        return
    end
    local alpha = a or 1
    local ok = pcall(btn.SetBackdropBorderColor, btn, r, g, b, alpha)
    if ok then
        return
    end
    btn:SetBackdropBorderColor(0.70, 0.70, 0.70, alpha)
end

local function CollectPartyDispelTrackingAuraEntry(unit, inRestricted)
    if not unit then
        return nil
    end

    local blizzEntry = _partyBlizzAuraCache[unit]
    local function CanApplyIconToken(iconToken)
        if iconToken == nil then
            return false
        end
        if IsPartySecretValue(iconToken) then
            return true
        end
        local tokenType = type(iconToken)
        if tokenType == "number" then
            if iconToken == PARTY_DISPEL_TRACKING.PLACEHOLDER_ICON then
                return false
            end
            return iconToken > 0
        end
        if tokenType == "string" then
            if iconToken == "" then
                return false
            end
            local okLower, lower = pcall(string.lower, iconToken)
            if okLower and type(lower) == "string" then
                if lower:find("questionmark", 1, true) or lower:find("inv_misc_questionmark", 1, true) then
                    return false
                end
            end
            return true
        end
        return false
    end
    local function HazardRank(enum)
        local normalized = NormalizePartyHazardEnum(enum)
        if not normalized or normalized == PARTY_HAZARD_ENUM.Unknown then
            return 999
        end
        for i, candidate in ipairs(PARTY_HAZARD_PRIORITY) do
            if normalized == candidate then
                return i
            end
        end
        return 999
    end

    local bestEntry = nil
    local bestScore = math.huge
    local function ResolveIconFromSpellID(spellID)
        local okSpellID, numericSpellID = pcall(tonumber, spellID)
        if (not okSpellID) or (not numericSpellID) then
            return nil, nil
        end
        if IsPartySecretValue(numericSpellID) then
            return nil, nil
        end
        if numericSpellID <= 0 then
            return nil, nil
        end
        local icon = nil
        if C_Spell and type(C_Spell.GetSpellTexture) == "function" then
            local okTex, tex = pcall(C_Spell.GetSpellTexture, numericSpellID)
            if okTex and tex and (not IsPartySecretValue(tex)) then
                icon = tex
            end
        end
        if (not icon) and type(GetSpellTexture) == "function" then
            local okLegacyTex, legacyTex = pcall(GetSpellTexture, numericSpellID)
            if okLegacyTex and legacyTex and (not IsPartySecretValue(legacyTex)) then
                icon = legacyTex
            end
        end
        return icon, numericSpellID
    end
    local function ConsiderEntry(icon, stackCount, auraInstanceID, auraIndex, enum, sourceScore, sourceLabel, spellID)
        local normalized = NormalizePartyHazardEnum(enum)
        local rank = HazardRank(normalized)
        if rank >= 999 then
            return
        end
        local okSpellID, resolvedSpellID = pcall(tonumber, spellID)
        if not okSpellID then
            resolvedSpellID = nil
        end
        if resolvedSpellID and IsPartySecretValue(resolvedSpellID) then
            resolvedSpellID = nil
        end
        if resolvedSpellID and resolvedSpellID <= 0 then
            resolvedSpellID = nil
        end
        if not CanApplyIconToken(icon) then
            icon = nil
        end
        if (not icon) and blizzEntry and type(blizzEntry.icons) == "table" and auraInstanceID then
            local cachedIcon = blizzEntry.icons[auraInstanceID]
            if CanApplyIconToken(cachedIcon) then
                icon = cachedIcon
            end
        end
        if (not resolvedSpellID) and blizzEntry and type(blizzEntry.spellIDs) == "table" and auraInstanceID then
            local okBlizzSpellID, blizzSpellID = pcall(tonumber, blizzEntry.spellIDs[auraInstanceID])
            if okBlizzSpellID and blizzSpellID and (not IsPartySecretValue(blizzSpellID)) and blizzSpellID > 0 then
                resolvedSpellID = blizzSpellID
            end
        end
        if (not CanApplyIconToken(icon)) and resolvedSpellID then
            local spellIcon, normalizedSpellID = ResolveIconFromSpellID(resolvedSpellID)
            if CanApplyIconToken(spellIcon) then
                icon = spellIcon
                resolvedSpellID = normalizedSpellID
            end
        end
        local usingPlaceholder = false
        if not CanApplyIconToken(icon) then
            icon = PARTY_DISPEL_TRACKING.PLACEHOLDER_ICON
            usingPlaceholder = true
        end
        local score = (rank * 100) + ((usingPlaceholder and 1 or 0) * 10) + (tonumber(sourceScore) or 0)
        if score >= bestScore then
            return
        end
        bestScore = score
        bestEntry = {
            icon = icon,
            stackCount = stackCount,
            auraInstanceID = auraInstanceID,
            auraIndex = auraIndex,
            enum = normalized,
            source = sourceLabel or "UNKNOWN",
            sourceScore = tonumber(sourceScore) or 0,
            spellID = resolvedSpellID,
        }
    end

    local function ConsiderBlizzAuraSetByIID(auraSet, sourceScore)
        if type(auraSet) ~= "table" then
            return
        end
        for iid in pairs(auraSet) do
            local hint = blizzEntry and blizzEntry.types and blizzEntry.types[iid] or nil
            local enum = NormalizePartyHazardEnum(hint)
            if (not enum or enum == PARTY_HAZARD_ENUM.Unknown) then
                enum = select(1, ResolvePartyHazardFromAuraInstanceID(unit, iid, hint))
            end
            if enum and enum ~= PARTY_HAZARD_ENUM.Unknown then
                local icon = blizzEntry and blizzEntry.icons and blizzEntry.icons[iid] or nil
                local stacks = blizzEntry and blizzEntry.stacks and blizzEntry.stacks[iid] or nil
                local spellID = blizzEntry and blizzEntry.spellIDs and blizzEntry.spellIDs[iid] or nil
                ConsiderEntry(icon, stacks, iid, nil, enum, sourceScore,
                    sourceScore == 0 and "BLIZZ_IID_DISPELLABLE" or "BLIZZ_IID_DEBUFF",
                    spellID)
            end
        end
    end

    if inRestricted and type(blizzEntry) == "table" then
        ConsiderBlizzAuraSetByIID(blizzEntry.dispellable, 0)
        ConsiderBlizzAuraSetByIID(blizzEntry.debuffs, 1)
        if not bestEntry then
            local function BuildIconOnlyEntry(auraSet, sourceLabel, sourceScore)
                if type(auraSet) ~= "table" then
                    return false
                end
                for iid in pairs(auraSet) do
                    local icon = blizzEntry and blizzEntry.icons and blizzEntry.icons[iid] or nil
                    local stacks = blizzEntry and blizzEntry.stacks and blizzEntry.stacks[iid] or nil
                    local spellID = blizzEntry and blizzEntry.spellIDs and blizzEntry.spellIDs[iid] or nil
                    local hint = blizzEntry and blizzEntry.types and blizzEntry.types[iid] or nil
                    local enum = NormalizePartyHazardEnum(hint)
                    if (not enum or enum == PARTY_HAZARD_ENUM.Unknown) then
                        enum = select(1, ResolvePartyHazardFromAuraInstanceID(unit, iid, hint))
                    end
                    local iconToken = CanApplyIconToken(icon) and icon or PARTY_DISPEL_TRACKING.PLACEHOLDER_ICON
                    bestEntry = {
                        icon = iconToken,
                        stackCount = stacks,
                        auraInstanceID = iid,
                        auraIndex = nil,
                        enum = enum,
                        source = sourceLabel,
                        sourceScore = sourceScore or 9,
                        spellID = spellID,
                    }
                    return true
                end
                return false
            end
        if not BuildIconOnlyEntry(blizzEntry.dispellable, "BLIZZ_ICON_ONLY_DISPELLABLE", 8) then
            BuildIconOnlyEntry(blizzEntry.debuffs, "BLIZZ_ICON_ONLY_DEBUFF", 9)
        end
    end
    if bestEntry and CanApplyIconToken(bestEntry.icon) then
        return bestEntry
    end
    end

    if C_UnitAuras and C_UnitAuras.GetAuraDataByIndex then
        local fallbackIIDs = nil
        if type(blizzEntry) == "table" then
            fallbackIIDs = {}
            if type(blizzEntry.dispellable) == "table" then
                for iid in pairs(blizzEntry.dispellable) do
                    fallbackIIDs[iid] = true
                end
            end
            if next(fallbackIIDs) == nil and type(blizzEntry.debuffs) == "table" then
                for iid in pairs(blizzEntry.debuffs) do
                    fallbackIIDs[iid] = true
                end
            end
            if next(fallbackIIDs) == nil then
                fallbackIIDs = nil
            end
        end

        for i = 1, 40 do
            local aura
            if inRestricted then
                local ok, result = pcall(C_UnitAuras.GetAuraDataByIndex, unit, i, "HARMFUL")
                if not ok then
                    break
                end
                aura = result
            else
                aura = C_UnitAuras.GetAuraDataByIndex(unit, i, "HARMFUL")
            end
            if not aura then
                break
            end
            local enum = ResolvePartyHazardFromAura(unit, aura)
            local auraIID = aura.auraInstanceID
            local hint = blizzEntry and blizzEntry.types and auraIID and blizzEntry.types[auraIID] or nil
            if (not enum or enum == PARTY_HAZARD_ENUM.Unknown) and auraIID then
                enum = NormalizePartyHazardEnum(hint)
            end
            if (not enum or enum == PARTY_HAZARD_ENUM.Unknown) and auraIID then
                enum = select(1, ResolvePartyHazardFromAuraInstanceID(unit, auraIID, hint))
            end

            if enum and enum ~= PARTY_HAZARD_ENUM.Unknown then
                local sourceScore = 2
                if blizzEntry and blizzEntry.dispellable and auraIID and blizzEntry.dispellable[auraIID] then
                    sourceScore = 0
                end
                ConsiderEntry(
                    aura.icon or aura.iconFileID,
                    aura.applications or aura.stackCount or aura.charges,
                    auraIID,
                    i,
                    enum,
                    sourceScore,
                    "AURA_SCAN_TYPED",
                    aura.spellId or aura.spellID
                )
            elseif fallbackIIDs and auraIID and fallbackIIDs[auraIID] then
                local fallbackEnum = select(1, ResolvePartyHazardFromAuraInstanceID(unit, auraIID, hint))
                if fallbackEnum and fallbackEnum ~= PARTY_HAZARD_ENUM.Unknown then
                    ConsiderEntry(
                        aura.icon or aura.iconFileID,
                        aura.applications or aura.stackCount or aura.charges,
                        auraIID,
                        i,
                        fallbackEnum,
                        1,
                        "AURA_SCAN_FALLBACK_IID",
                        aura.spellId or aura.spellID
                    )
                end
            end

            if fallbackIIDs and auraIID then
                fallbackIIDs[auraIID] = nil
            end
        end

        if fallbackIIDs and C_UnitAuras.GetAuraDataByAuraInstanceID then
            for fallbackIID in pairs(fallbackIIDs) do
                local fallbackAura = nil
                if inRestricted then
                    local okAura, auraData = pcall(C_UnitAuras.GetAuraDataByAuraInstanceID, unit, fallbackIID)
                    if okAura then
                        fallbackAura = auraData
                    end
                else
                    fallbackAura = C_UnitAuras.GetAuraDataByAuraInstanceID(unit, fallbackIID)
                end

                local hint = blizzEntry and blizzEntry.types and blizzEntry.types[fallbackIID] or nil
                local enum = NormalizePartyHazardEnum(hint)
                if (not enum or enum == PARTY_HAZARD_ENUM.Unknown) then
                    enum = select(1, ResolvePartyHazardFromAuraInstanceID(unit, fallbackIID, hint))
                end
                if enum and enum ~= PARTY_HAZARD_ENUM.Unknown then
                    local icon = nil
                    local stacks = nil
                    local spellID = nil
                    if fallbackAura then
                        icon = fallbackAura.icon or fallbackAura.iconFileID
                        stacks = fallbackAura.applications or fallbackAura.stackCount or fallbackAura.charges
                        spellID = fallbackAura.spellId or fallbackAura.spellID
                    end
                    if (not icon) and blizzEntry and blizzEntry.icons then
                        icon = blizzEntry.icons[fallbackIID]
                    end
                    if (not stacks) and blizzEntry and blizzEntry.stacks then
                        stacks = blizzEntry.stacks[fallbackIID]
                    end
                    if (not spellID) and blizzEntry and blizzEntry.spellIDs then
                        spellID = blizzEntry.spellIDs[fallbackIID]
                    end
                    ConsiderEntry(icon, stacks, fallbackIID, nil, enum, 1, "AURA_IID_FALLBACK", spellID)
                end
            end
        end

        if bestEntry then
            return bestEntry
        end
        return nil
    end

    if type(UnitDebuff) ~= "function" then
        if bestEntry then
            return bestEntry
        end
        return nil
    end
    for i = 1, 40 do
        local name, icon, count, debuffType = UnitDebuff(unit, i)
        if not name then
            break
        end
        local enum = NormalizePartyHazardEnum(debuffType)
        if enum and enum ~= PARTY_HAZARD_ENUM.Unknown then
            ConsiderEntry(icon, count, nil, i, enum, 3, "LEGACY_UNITDEBUFF", nil)
        end
    end
    return bestEntry
end

local function EnsurePartyDispelTrackingOverlay(frame)
    if not frame then
        return nil
    end
    if frame._muiPartyDispelTrackingOverlay then
        return frame._muiPartyDispelTrackingOverlay
    end

    local overlay = CreateFrame("Frame", nil, frame)
    overlay:SetSize(PARTY_DISPEL_TRACKING.BASE_ICON_SIZE + 2, PARTY_DISPEL_TRACKING.BASE_ICON_SIZE + 2)
    overlay:SetPoint("LEFT", frame, "RIGHT", PARTY_DISPEL_TRACKING.OFFSET_X, PARTY_DISPEL_TRACKING.OFFSET_Y)
    overlay:SetFrameStrata("MEDIUM")
    overlay:SetFrameLevel(frame:GetFrameLevel() + 45)
    overlay:EnableMouse(false)
    overlay:Hide()

    local function CreateIconButton()
        local btn = CreateFrame("Frame", nil, overlay, "BackdropTemplate")
        btn:SetSize(PARTY_DISPEL_TRACKING.BASE_ICON_SIZE, PARTY_DISPEL_TRACKING.BASE_ICON_SIZE)
        btn:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            edgeSize = 1,
            insets = { left = 0, right = 0, top = 0, bottom = 0 },
        })
        btn:SetBackdropColor(0, 0, 0, 0.75)
        btn:SetBackdropBorderColor(0, 0, 0, 1)

        local icon = btn:CreateTexture(nil, "ARTWORK")
        icon:SetPoint("TOPLEFT", 1, -1)
        icon:SetPoint("BOTTOMRIGHT", -1, 1)
        icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        btn.icon = icon

        local count = btn:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall")
        count:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 1)
        count:SetJustifyH("RIGHT")
        count:SetTextColor(1, 1, 1, 1)
        btn.countText = count

        btn:EnableMouse(true)
        btn:SetScript("OnEnter", function(self)
            if not GameTooltip then
                return
            end
            local unitToken = self._muiUnit
            if type(unitToken) ~= "string" or (UnitExists and not UnitExists(unitToken)) then
                return
            end
            if not self.auraInstanceID and not self.auraIndex then
                return
            end

            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            if GameTooltip.ClearLines then
                GameTooltip:ClearLines()
            end
            local shown = false
            local function TooltipHasLines()
                if not GameTooltip or type(GameTooltip.NumLines) ~= "function" then
                    return false
                end
                local okLines, lines = pcall(GameTooltip.NumLines, GameTooltip)
                return okLines and type(lines) == "number" and lines > 0
            end
            local function MarkShown(okCall, result)
                if not okCall then
                    return false
                end
                if result ~= nil then
                    return result and true or false
                end
                return TooltipHasLines()
            end
            if self.auraInstanceID and GameTooltip.SetUnitDebuffByAuraInstanceID then
                local okAura, result = pcall(GameTooltip.SetUnitDebuffByAuraInstanceID, GameTooltip, unitToken, self.auraInstanceID)
                shown = MarkShown(okAura, result)
            end
            if (not shown) and self.auraInstanceID and GameTooltip.SetUnitAuraByAuraInstanceID then
                local okAura, result = pcall(GameTooltip.SetUnitAuraByAuraInstanceID, GameTooltip, unitToken, self.auraInstanceID)
                shown = MarkShown(okAura, result)
            end
            if (not shown) and self.auraIndex and GameTooltip.SetUnitAura then
                local okAura, result = pcall(GameTooltip.SetUnitAura, GameTooltip, unitToken, self.auraIndex, "HARMFUL")
                shown = MarkShown(okAura, result)
            end
            if (not shown) and self.auraIndex and GameTooltip.SetUnitDebuff then
                local okLegacy, result = pcall(GameTooltip.SetUnitDebuff, GameTooltip, unitToken, self.auraIndex)
                shown = MarkShown(okLegacy, result)
            end
            if shown then
                GameTooltip:Show()
            else
                GameTooltip:Hide()
            end
        end)
        btn:SetScript("OnLeave", function()
            if GameTooltip then
                GameTooltip:Hide()
            end
        end)
        return btn
    end

    local btn = CreateIconButton()
    btn:SetPoint("CENTER")
    overlay.iconButton = btn
    overlay.iconButtons = { btn }
    overlay._muiCreateIconButton = CreateIconButton
    frame._muiPartyDispelTrackingOverlay = overlay
    return overlay
end

local function GetPartyDispelTrackingAnchorOwner()
    if PartyAnchor and PartyAnchor.dragOverlay and PartyAnchor.dragOverlay.IsShown and PartyAnchor.dragOverlay:IsShown() then
        local slot1 = PartyPreviewBoxes[1]
        if slot1 and slot1.IsShown and slot1:IsShown() then
            return slot1
        end
    end
    for i = 1, 4 do
        local frame = PartyFrames[i]
        if frame and frame.IsShown and frame:IsShown() then
            return frame
        end
    end
    if PartyAnchor and PartyAnchor.IsShown and PartyAnchor:IsShown() then
        return PartyAnchor
    end
    if PartyFrames[1] then
        return PartyFrames[1]
    end
    return PartyAnchor
end

local function GetPartyDispelTrackingDefaultOffset(ownerFrame, iconSize)
    local ownerW = (ownerFrame and ownerFrame.GetWidth and CoerceNumber(ownerFrame:GetWidth())) or tonumber(config.width) or 240
    local ownerH = (ownerFrame and ownerFrame.GetHeight and CoerceNumber(ownerFrame:GetHeight())) or tonumber(config.height) or 58

    local horizontalLayout = (config.layout == "Horizontal")
    if horizontalLayout then
        local y = math.floor((ownerH / 2) + (iconSize / 2) + 4 + 0.5)
        return 0, y
    end

    local x = math.floor((ownerW / 2) + (iconSize / 2) + PARTY_DISPEL_TRACKING.OFFSET_X + 0.5)
    return x, PARTY_DISPEL_TRACKING.OFFSET_Y
end

local function ClampPartyDispelTrackingOffset(ownerFrame, iconSize, x, y)
    local ownerW = (ownerFrame and ownerFrame.GetWidth and CoerceNumber(ownerFrame:GetWidth())) or tonumber(config.width) or 240
    local ownerH = (ownerFrame and ownerFrame.GetHeight and CoerceNumber(ownerFrame:GetHeight())) or tonumber(config.height) or 58
    local halfW = math.max(1, ownerW * 0.5)
    local halfH = math.max(1, ownerH * 0.5)
    local minX, maxX, minY, maxY
    if config.layout == "Horizontal" then
        local pitchX = ownerW + (tonumber(config.spacingX) or 0)
        local primaryLimitX = math.max(18, math.floor((pitchX * 0.75) + 0.5))
        minX = -primaryLimitX
        maxX = primaryLimitX
        minY = -math.floor((halfH + iconSize + 6) + 0.5)
        maxY = math.floor((halfH + iconSize + 12) + 0.5)
    else
        local pitchY = ownerH + (tonumber(config.spacingY) or 0)
        local primaryLimitY = math.max(14, math.floor((pitchY * 0.75) + 0.5))
        minX = -math.floor((halfW + iconSize + PARTY_DISPEL_TRACKING.OFFSET_X + 24) + 0.5)
        maxX = math.floor((halfW + iconSize + PARTY_DISPEL_TRACKING.OFFSET_X + 24) + 0.5)
        minY = -primaryLimitY
        maxY = primaryLimitY
    end

    if x < minX then
        x = minX
    elseif x > maxX then
        x = maxX
    end
    if y < minY then
        y = minY
    elseif y > maxY then
        y = maxY
    end
    return x, y
end

local function GetPartyDispelTrackingRelativeOffset(ownerFrame, iconSize)
    local combat = EnsurePartyCombatSettingsTable()
    local pos = combat.partyDispelTrackingPosition
    -- New 4-element format {point, relPoint, x, y} is owned by the drag-stop
    -- handler and consumed directly by UpdatePartyDispelTrackingDragOverlay.
    -- Do NOT interpret or overwrite it here, or we'd clobber a freshly-saved drag.
    local isNewFormat = type(pos) == "table" and type(pos[1]) == "string"
    local x, y
    if (not isNewFormat) and type(pos) == "table" and #pos >= 2 then
        x = tonumber(pos[1])
        y = tonumber(pos[2])
    end
    if not x or not y then
        x, y = GetPartyDispelTrackingDefaultOffset(ownerFrame, iconSize)
    end

    x, y = ClampPartyDispelTrackingOffset(ownerFrame, iconSize, x, y)

    if (not isNewFormat) and type(pos) == "table" and (tonumber(pos[1]) ~= x or tonumber(pos[2]) ~= y) then
        combat.partyDispelTrackingPosition = { x, y }
    end
    return x, y
end

local function SavePartyDispelTrackingRelativePosition(overlay, ownerFrame)
    if not overlay or not ownerFrame or not overlay.GetCenter or not ownerFrame.GetCenter then
        return
    end
    local fx, fy = overlay:GetCenter()
    local ox, oy = ownerFrame:GetCenter()
    if not fx or not fy or not ox or not oy then
        return
    end
    local combat = EnsurePartyCombatSettingsTable()
    combat.partyDispelTrackingPosition = { fx - ox, fy - oy }
    local iconSize = GetPartyDispelTrackingIconSize(combat.partyDispelTrackingIconScale)
    local clampedX, clampedY = GetPartyDispelTrackingRelativeOffset(ownerFrame, iconSize)
    combat.partyDispelTrackingPosition = { clampedX, clampedY }
end

local function EnsurePartyDispelTrackingDragOverlay()
    if PartyDispelTrackingState.dragOverlay then
        return PartyDispelTrackingState.dragOverlay
    end

    local overlay = CreateFrame("Frame", "MidnightUI_PartyDispelTrackingDragOverlay", UIParent, "BackdropTemplate")
    overlay:SetSize(PARTY_DISPEL_TRACKING.DRAG_SIZE, PARTY_DISPEL_TRACKING.DRAG_SIZE)
    overlay:SetFrameStrata("HIGH")
    overlay:SetFrameLevel(70)
    overlay:SetMovable(true)
    overlay:SetClampedToScreen(true)
    if _G.MidnightUI_StyleOverlay then
        _G.MidnightUI_StyleOverlay(overlay, "PARTY DISPEL ICONS", nil, "auras")
    else
        overlay:SetBackdrop({
            bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true,
            tileSize = 16,
            edgeSize = 16,
            insets = { left = 4, right = 4, top = 4, bottom = 4 },
        })
        overlay:SetBackdropColor(0.05, 0.08, 0.11, 0.30)
        overlay:SetBackdropBorderColor(0.30, 0.46, 0.58, 0.78)
        local label = overlay:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        label:SetPoint("BOTTOM", overlay, "TOP", 0, 2)
        label:SetText("PARTY DISPEL ICONS")
        label:SetTextColor(1, 1, 1)
    end

    local preview = CreateFrame("Frame", nil, overlay, "BackdropTemplate")
    preview:SetPoint("CENTER")
    preview:SetSize(PARTY_DISPEL_TRACKING.BASE_ICON_SIZE, PARTY_DISPEL_TRACKING.BASE_ICON_SIZE)
    preview:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 },
    })
    preview:SetBackdropColor(0, 0, 0, 0.75)
    preview:SetBackdropBorderColor(PARTY_HAZARD_COLORS[PARTY_HAZARD_ENUM.Magic][1], PARTY_HAZARD_COLORS[PARTY_HAZARD_ENUM.Magic][2], PARTY_HAZARD_COLORS[PARTY_HAZARD_ENUM.Magic][3], 1)

    local icon = preview:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("TOPLEFT", 1, -1)
    icon:SetPoint("BOTTOMRIGHT", -1, 1)
    icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    icon:SetTexture(PARTY_DISPEL_TRACKING.PLACEHOLDER_ICON)
    overlay.preview = preview
    overlay.previewIcon = icon
    overlay.previewIcons = {}

    overlay:EnableMouse(true)
    overlay:RegisterForDrag("LeftButton")
    overlay:SetScript("OnDragStart", function(self)
        if InCombatLockdown and InCombatLockdown() then return end
        self:StartMoving()
    end)
    overlay:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local combat = EnsurePartyCombatSettingsTable()
        local point, _, relPoint, xOfs, yOfs = self:GetPoint()
        if point and relPoint and xOfs and yOfs then
            combat.partyDispelTrackingPosition = { point, relPoint, xOfs, yOfs }
        end
        RefreshPartyDispelTrackingOverlays(false)
    end)
    if _G.MidnightUI_AttachOverlaySettings then
        _G.MidnightUI_AttachOverlaySettings(overlay, "PartyDispelTracking")
    end
    overlay:Hide()
    PartyDispelTrackingState.dragOverlay = overlay
    return overlay
end

local function UpdatePartyDispelTrackingDragOverlay()
    local overlay = EnsurePartyDispelTrackingDragOverlay()
    if not overlay then
        return
    end

    local combat = EnsurePartyCombatSettingsTable()
    local enabled = IsPartyDebuffOverlayEnabled() and (combat.partyDispelTrackingEnabled ~= false)
    local movementUnlocked = IsPartyDispelMovementModeUnlocked()
    if (not enabled) or (not movementUnlocked) or PartyDispelTrackingState.locked then
        overlay._muiDragging = false
        overlay._muiDragOwner = nil
        overlay._muiDragIconSize = nil
        overlay:SetScript("OnUpdate", nil)
        overlay:Hide()
        return
    end

    local iconSize = GetPartyDispelTrackingIconSize(combat.partyDispelTrackingIconScale)
    local owner = GetPartyDispelTrackingAnchorOwner()
    if not owner then
        overlay:Hide()
        return
    end

    overlay:ClearAllPoints()
    local pos = combat.partyDispelTrackingPosition
    if type(pos) == "table" and #pos >= 4 and type(pos[1]) == "string" and type(pos[2]) == "string" then
        overlay:SetPoint(pos[1], UIParent, pos[2], pos[3], pos[4])
    else
        local offsetX, offsetY = GetPartyDispelTrackingRelativeOffset(owner, iconSize)
        overlay:SetPoint("CENTER", owner, "CENTER", offsetX, offsetY)
    end
    overlay:SetAlpha(ClampPartyDispelTrackingAlpha(combat.partyDispelTrackingAlpha))
    local usingPreviewSlots = false
    if PartyAnchor and PartyAnchor.dragOverlay and PartyAnchor.dragOverlay.IsShown and PartyAnchor.dragOverlay:IsShown() then
        for i = 1, 4 do
            local slot = PartyPreviewBoxes[i]
            if slot and slot.IsShown and slot:IsShown() then
                usingPreviewSlots = true
                break
            end
        end
    end

    local maxShown = math.max(1, math.floor((tonumber(combat.partyDispelTrackingMaxShown) or 4) + 0.5))
    if maxShown > PARTY_DISPEL_TRACKING.MAX_SHOWN_LIMIT then maxShown = PARTY_DISPEL_TRACKING.MAX_SHOWN_LIMIT end
    local perRow = math.max(1, combat.partyDispelTrackingPerRow or PARTY_DISPEL_TRACKING.DEFAULT_PER_ROW)
    local hSpacing = iconSize + 2
    local vSpacing = iconSize + 2
    local cols = math.min(perRow, maxShown)
    local rows = math.ceil(maxShown / perRow)
    local gridW = math.max(iconSize + 2, cols * hSpacing)
    local gridH = math.max(iconSize + 2, rows * vSpacing)
    overlay:SetSize(gridW, gridH)

    if overlay.preview then overlay.preview:Hide() end

    if not overlay._muiGridIcons then overlay._muiGridIcons = {} end
    local gridIcons = overlay._muiGridIcons
    for i = 1, maxShown do
        if not gridIcons[i] then
            local holder = CreateFrame("Frame", nil, overlay, "BackdropTemplate")
            holder:SetBackdrop({
                bgFile = "Interface\\Buttons\\WHITE8X8",
                edgeFile = "Interface\\Buttons\\WHITE8X8",
                edgeSize = 1,
                insets = { left = 0, right = 0, top = 0, bottom = 0 },
            })
            holder:SetBackdropColor(0, 0, 0, 0.75)
            holder:SetBackdropBorderColor(PARTY_HAZARD_COLORS[PARTY_HAZARD_ENUM.Magic][1], PARTY_HAZARD_COLORS[PARTY_HAZARD_ENUM.Magic][2], PARTY_HAZARD_COLORS[PARTY_HAZARD_ENUM.Magic][3], 1)
            local ico = holder:CreateTexture(nil, "ARTWORK")
            ico:SetPoint("TOPLEFT", 1, -1)
            ico:SetPoint("BOTTOMRIGHT", -1, 1)
            ico:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            ico:SetTexture(PARTY_DISPEL_TRACKING.PLACEHOLDER_ICON)
            holder.icon = ico
            gridIcons[i] = holder
        end
        local holder = gridIcons[i]
        holder:SetSize(iconSize, iconSize)
        holder:ClearAllPoints()
        local row = math.floor((i - 1) / perRow)
        local col = (i - 1) % perRow
        holder:SetPoint("TOPLEFT", overlay, "TOPLEFT", col * hSpacing, -(row * vSpacing))
        if usingPreviewSlots then
            holder:Hide()
        else
            holder:Show()
        end
    end
    for i = maxShown + 1, #gridIcons do
        if gridIcons[i] then gridIcons[i]:Hide() end
    end

    local previewIcons = overlay.previewIcons
    if type(previewIcons) ~= "table" then
        previewIcons = {}
        overlay.previewIcons = previewIcons
    end

    local ownerCenterX, ownerCenterY = owner:GetCenter()
    if (not ownerCenterX or not ownerCenterY) and PartyAnchor and PartyAnchor.GetCenter then
        ownerCenterX, ownerCenterY = PartyAnchor:GetCenter()
    end
    if not overlay._muiGhostGrids then overlay._muiGhostGrids = {} end
    local ghostGrids = overlay._muiGhostGrids
    for i = 1, 4 do
        if not ghostGrids[i] then
            ghostGrids[i] = { container = nil, icons = {} }
        end
        local gg = ghostGrids[i]
        if not gg.container then
            gg.container = CreateFrame("Frame", nil, overlay)
            gg.container:SetSize(gridW, gridH)
        end

        local target = nil
        if usingPreviewSlots then
            local slot = PartyPreviewBoxes[i]
            if slot and slot.IsShown and slot:IsShown() and slot.GetCenter then
                target = slot
            end
        else
            local frame = PartyFrames[i]
            if frame and frame ~= owner and frame.IsShown and frame:IsShown() and frame.GetCenter then
                target = frame
            end
        end

        if target and ownerCenterX and ownerCenterY then
            local fx, fy = target:GetCenter()
            if fx and fy then
                gg.container:SetSize(gridW, gridH)
                gg.container:ClearAllPoints()
                gg.container:SetPoint("CENTER", overlay, "CENTER", fx - ownerCenterX, fy - ownerCenterY)
                for j = 1, maxShown do
                    if not gg.icons[j] then
                        local holder = CreateFrame("Frame", nil, gg.container, "BackdropTemplate")
                        holder:SetBackdrop({
                            bgFile = "Interface\\Buttons\\WHITE8X8",
                            edgeFile = "Interface\\Buttons\\WHITE8X8",
                            edgeSize = 1,
                            insets = { left = 0, right = 0, top = 0, bottom = 0 },
                        })
                        holder:SetBackdropColor(0, 0, 0, 0.75)
                        holder:SetBackdropBorderColor(PARTY_HAZARD_COLORS[PARTY_HAZARD_ENUM.Magic][1], PARTY_HAZARD_COLORS[PARTY_HAZARD_ENUM.Magic][2], PARTY_HAZARD_COLORS[PARTY_HAZARD_ENUM.Magic][3], 1)
                        local ico = holder:CreateTexture(nil, "ARTWORK")
                        ico:SetPoint("TOPLEFT", 1, -1)
                        ico:SetPoint("BOTTOMRIGHT", -1, 1)
                        ico:SetTexCoord(0.08, 0.92, 0.08, 0.92)
                        ico:SetTexture(PARTY_DISPEL_TRACKING.PLACEHOLDER_ICON)
                        holder.icon = ico
                        gg.icons[j] = holder
                    end
                    local holder = gg.icons[j]
                    holder:SetSize(iconSize, iconSize)
                    holder:ClearAllPoints()
                    local row = math.floor((j - 1) / perRow)
                    local col = (j - 1) % perRow
                    holder:SetPoint("TOPLEFT", gg.container, "TOPLEFT", col * hSpacing, -(row * vSpacing))
                    holder:Show()
                end
                for j = maxShown + 1, #gg.icons do
                    if gg.icons[j] then gg.icons[j]:Hide() end
                end
                gg.container:Show()
            else
                gg.container:Hide()
            end
        else
            if gg.container then gg.container:Hide() end
        end
    end
    for i = 1, #previewIcons do
        if previewIcons[i] then previewIcons[i]:Hide() end
    end
    overlay:Show()
end

UpdatePartyDispelTrackingOverlay = function(frame, unit, primaryEnum, primaryColorOverride, inRestricted, forceHide)
    local overlay = EnsurePartyDispelTrackingOverlay(frame)
    if not overlay then
        return
    end

    local combat = EnsurePartyCombatSettingsTable()
    local movementUnlocked = IsPartyDispelMovementModeUnlocked()
    local enabled = (forceHide ~= true) and IsPartyDebuffOverlayEnabled() and (combat.partyDispelTrackingEnabled ~= false)
    local ownerShown = frame and frame.IsShown and (frame:IsShown() == true) or false
    if not enabled then
        overlay:Hide()
        return
    end
    if (not ownerShown) and (not movementUnlocked) then
        overlay:Hide()
        return
    end

    local iconScale = ClampPartyDispelTrackingIconScale(combat.partyDispelTrackingIconScale)
    local overlayAlpha = ClampPartyDispelTrackingAlpha(combat.partyDispelTrackingAlpha)
    local iconSize = GetPartyDispelTrackingIconSize(iconScale)
    local maxShown = math.floor((tonumber(combat.partyDispelTrackingMaxShown) or 4) + 0.5)
    if maxShown < 1 then
        maxShown = 1
    elseif maxShown > PARTY_DISPEL_TRACKING.MAX_SHOWN_LIMIT then
        maxShown = PARTY_DISPEL_TRACKING.MAX_SHOWN_LIMIT
    end
    combat.partyDispelTrackingIconScale = iconScale
    combat.partyDispelTrackingAlpha = overlayAlpha
    combat.partyDispelTrackingMaxShown = maxShown

    local offsetX, offsetY = GetPartyDispelTrackingRelativeOffset(frame, iconSize)
    overlay:SetScale(1)
    overlay:SetAlpha(overlayAlpha)
    overlay:ClearAllPoints()
    overlay:SetPoint("CENTER", frame, "CENTER", offsetX, offsetY)
    overlay:SetFrameLevel(((frame and frame.GetFrameLevel and frame:GetFrameLevel()) or 1) + 45)
    overlay:SetFrameStrata("MEDIUM")
    overlay:SetSize((iconSize + 2) * maxShown, iconSize + 2)
    overlay:EnableMouse(false)

    local baseBtn = overlay.iconButton
    if not baseBtn then
        overlay:Hide()
        return
    end

    local iconButtons = overlay.iconButtons
    if type(iconButtons) ~= "table" then
        iconButtons = { baseBtn }
        overlay.iconButtons = iconButtons
    elseif iconButtons[1] ~= baseBtn then
        iconButtons[1] = baseBtn
    end

    local function EnsureIconButton(index)
        local btn = iconButtons[index]
        if btn then
            return btn
        end
        if type(overlay._muiCreateIconButton) == "function" then
            btn = overlay._muiCreateIconButton()
            iconButtons[index] = btn
            return btn
        end
        return nil
    end

    for i = 1, maxShown do
        local btn = EnsureIconButton(i)
        if btn then
            btn:EnableMouse(movementUnlocked ~= true)
            btn:SetSize(iconSize, iconSize)
            btn._muiUnit = unit
        end
    end
    for i = maxShown + 1, #iconButtons do
        local btn = iconButtons[i]
        if btn then
            btn.auraInstanceID = nil
            btn.auraIndex = nil
            if btn.countText then
                btn.countText:SetText("")
            end
            btn:Hide()
        end
    end

    local normalizedPrimary = NormalizePartyHazardEnum(primaryEnum)
    local function CanApplyIconToken(iconToken)
        if iconToken == nil then
            return false
        end
        if IsPartySecretValue(iconToken) then
            return true
        end
        local tokenType = type(iconToken)
        if tokenType == "number" then
            if iconToken == PARTY_DISPEL_TRACKING.PLACEHOLDER_ICON then
                return false
            end
            return iconToken > 0
        end
        if tokenType == "string" then
            if iconToken == "" then
                return false
            end
            local okLower, lower = pcall(string.lower, iconToken)
            if okLower and type(lower) == "string" then
                if lower:find("questionmark", 1, true) or lower:find("inv_misc_questionmark", 1, true) then
                    return false
                end
            end
            return true
        end
        return false
    end
    local function SetIconTexCoord(targetBtn, usingAtlas)
        if not targetBtn or not targetBtn.icon or not targetBtn.icon.SetTexCoord then
            return
        end
        if usingAtlas then
            targetBtn.icon:SetTexCoord(0, 1, 0, 1)
        else
            targetBtn.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        end
    end
    local function GetHazardIconAtlas(enum)
        if enum == PARTY_HAZARD_ENUM.Magic then
            return "RaidFrame-Icon-DebuffMagic"
        elseif enum == PARTY_HAZARD_ENUM.Curse then
            return "RaidFrame-Icon-DebuffCurse"
        elseif enum == PARTY_HAZARD_ENUM.Disease then
            return "RaidFrame-Icon-DebuffDisease"
        elseif enum == PARTY_HAZARD_ENUM.Poison then
            return "RaidFrame-Icon-DebuffPoison"
        end
        return nil
    end
    local function ResolveIconFromSpellID(spellID)
        local okSpellID, numericSpellID = pcall(tonumber, spellID)
        if (not okSpellID) or (not numericSpellID) then
            return nil
        end
        if IsPartySecretValue(numericSpellID) or numericSpellID <= 0 then
            return nil
        end
        local spellTexture = nil
        if C_Spell and type(C_Spell.GetSpellTexture) == "function" then
            local okTex, tex = pcall(C_Spell.GetSpellTexture, numericSpellID)
            if okTex and tex then
                spellTexture = tex
            end
        end
        if (not spellTexture) and type(GetSpellTexture) == "function" then
            local okLegacyTex, legacyTex = pcall(GetSpellTexture, numericSpellID)
            if okLegacyTex and legacyTex then
                spellTexture = legacyTex
            end
        end
        if CanApplyIconToken(spellTexture) then
            return spellTexture
        end
        return nil
    end
    local function ApplyEntryToButton(targetBtn, entry)
        if not targetBtn or not entry then
            return false, "HIDE_NO_ENTRY"
        end
        local iconToken = entry.icon
        local iconSource = tostring(entry.source or "entry")
        local blizzEntry = _partyBlizzAuraCache[unit]
        if (not CanApplyIconToken(iconToken)) and blizzEntry and type(blizzEntry.icons) == "table" and entry.auraInstanceID then
            local cachedIcon = blizzEntry.icons[entry.auraInstanceID]
            if CanApplyIconToken(cachedIcon) then
                iconToken = cachedIcon
                iconSource = iconSource .. "+blizz-cache"
            end
        end
        local iconSpellID = entry.spellID
        if (not iconSpellID) and blizzEntry and type(blizzEntry.spellIDs) == "table" and entry.auraInstanceID then
            iconSpellID = blizzEntry.spellIDs[entry.auraInstanceID]
        end
        if (not CanApplyIconToken(iconToken)) and iconSpellID then
            local spellTexture = ResolveIconFromSpellID(iconSpellID)
            if CanApplyIconToken(spellTexture) then
                iconToken = spellTexture
                iconSource = iconSource .. "+spellID"
            end
        end

        local iconAppliedFromAtlas = false
        local atlasEnum = NormalizePartyHazardEnum(entry.enum) or normalizedPrimary
        if not CanApplyIconToken(iconToken) then
            local atlasName = GetHazardIconAtlas(atlasEnum)
            if atlasName and targetBtn.icon and targetBtn.icon.SetAtlas then
                local okAtlas = pcall(targetBtn.icon.SetAtlas, targetBtn.icon, atlasName, false)
                if okAtlas then
                    iconAppliedFromAtlas = true
                    iconSource = iconSource .. "+atlas"
                end
            end
            if not iconAppliedFromAtlas then
                -- Always hide when there's no real icon. Previously, Edit Mode
                -- showed a "?" placeholder per party member as a preview, which
                -- duplicated the moveable overlay's preview grid.
                targetBtn.auraInstanceID = entry.auraInstanceID
                targetBtn.auraIndex = entry.auraIndex
                if targetBtn.countText then
                    targetBtn.countText:SetText("")
                end
                targetBtn:Hide()
                return false, "HIDE_NO_USABLE_ICON"
            end
        end

        local setTextureState = "SKIP"
        if iconAppliedFromAtlas then
            SetIconTexCoord(targetBtn, true)
            setTextureState = "OK_ATLAS:" .. iconSource
        else
            SetIconTexCoord(targetBtn, false)
            local okIcon = pcall(targetBtn.icon.SetTexture, targetBtn.icon, iconToken)
            if not okIcon then
                targetBtn.icon:SetTexture(PARTY_DISPEL_TRACKING.PLACEHOLDER_ICON)
                SetIconTexCoord(targetBtn, false)
                setTextureState = "FALLBACK_PLACEHOLDER:" .. iconSource
            else
                setTextureState = "OK:" .. iconSource
            end
        end

        if targetBtn.countText then
            targetBtn.countText:SetText(NormalizePartyAuraStackCount(entry.stackCount))
        end
        targetBtn.auraInstanceID = entry.auraInstanceID
        targetBtn.auraIndex = entry.auraIndex
        return true, setTextureState
    end

    local entries = {}
    local primaryEntry = nil
    if enabled and unit and type(unit) == "string" then
        primaryEntry = CollectPartyDispelTrackingAuraEntry(unit, inRestricted)
    end
    if primaryEntry then
        entries[#entries + 1] = primaryEntry
    end

    if (#entries < maxShown) and unit and type(unit) == "string" then
        local blizzEntry = _partyBlizzAuraCache[unit]
        local usedIIDs = {}
        if primaryEntry and primaryEntry.auraInstanceID then
            usedIIDs[primaryEntry.auraInstanceID] = true
        end

        local function BuildSortedIIDList(auraSet)
            local list = {}
            if type(auraSet) ~= "table" then
                return list
            end
            for iid in pairs(auraSet) do
                list[#list + 1] = iid
            end
            table.sort(list, function(a, b)
                local na = tonumber(a) or 0
                local nb = tonumber(b) or 0
                return na < nb
            end)
            return list
        end

        local function AddFromAuraSet(auraSet, sourceLabel, sourceScore)
            if (#entries >= maxShown) or type(auraSet) ~= "table" then
                return
            end
            local orderedIIDs = BuildSortedIIDList(auraSet)
            for _, iid in ipairs(orderedIIDs) do
                if #entries >= maxShown then
                    break
                end
                if not usedIIDs[iid] then
                    local icon = blizzEntry and blizzEntry.icons and blizzEntry.icons[iid] or nil
                    local stacks = blizzEntry and blizzEntry.stacks and blizzEntry.stacks[iid] or nil
                    local spellID = blizzEntry and blizzEntry.spellIDs and blizzEntry.spellIDs[iid] or nil
                    local hint = blizzEntry and blizzEntry.types and blizzEntry.types[iid] or nil
                    local enum = NormalizePartyHazardEnum(hint)
                    if (not enum or enum == PARTY_HAZARD_ENUM.Unknown) then
                        enum = select(1, ResolvePartyHazardFromAuraInstanceID(unit, iid, hint))
                    end
                    local hasTrackedEnum = enum and enum ~= PARTY_HAZARD_ENUM.Unknown
                    local hasIcon = CanApplyIconToken(icon)
                    if hasTrackedEnum or (inRestricted and hasIcon) then
                        entries[#entries + 1] = {
                            icon = icon,
                            stackCount = stacks,
                            auraInstanceID = iid,
                            auraIndex = nil,
                            enum = enum,
                            source = sourceLabel,
                            sourceScore = sourceScore,
                            spellID = spellID,
                        }
                        usedIIDs[iid] = true
                    end
                end
            end
        end

        AddFromAuraSet(blizzEntry and blizzEntry.dispellable, "BLIZZ_EXTRA_DISPELLABLE", 8)
        AddFromAuraSet(blizzEntry and blizzEntry.debuffs, "BLIZZ_EXTRA_DEBUFF", 9)
    end

    local borderR, borderG, borderB = 0.70, 0.70, 0.70
    local hasBaseBorderColor = false
    if type(primaryColorOverride) == "table" and primaryColorOverride.secret ~= true then
        local r = tonumber(primaryColorOverride[1] or primaryColorOverride.r)
        local g = tonumber(primaryColorOverride[2] or primaryColorOverride.g)
        local b = tonumber(primaryColorOverride[3] or primaryColorOverride.b)
        if r and g and b then
            borderR, borderG, borderB = r, g, b
            hasBaseBorderColor = true
        end
    end
    if (not hasBaseBorderColor) and normalizedPrimary and PARTY_HAZARD_COLORS[normalizedPrimary] then
        local c = PARTY_HAZARD_COLORS[normalizedPrimary]
        borderR, borderG, borderB = c[1], c[2], c[3]
        hasBaseBorderColor = true
    end
    if (not hasBaseBorderColor) and frame and type(frame._muiClassColor) == "table" then
        local cr = tonumber(frame._muiClassColor.r or frame._muiClassColor[1])
        local cg = tonumber(frame._muiClassColor.g or frame._muiClassColor[2])
        local cb = tonumber(frame._muiClassColor.b or frame._muiClassColor[3])
        if cr and cg and cb then
            borderR, borderG, borderB = cr, cg, cb
            hasBaseBorderColor = true
        end
    end

    local activeButtons = {}
    local activeEntries = {}
    local setTextureState = "SKIP"
    local logEntry = nil

    if #entries > 0 then
        for i = 1, maxShown do
            local btn = EnsureIconButton(i)
            local entry = entries[i]
            if btn and entry then
                local shown, state = ApplyEntryToButton(btn, entry)
                if (not logEntry) and entry then
                    logEntry = entry
                    setTextureState = state
                end
                if shown then
                    activeButtons[#activeButtons + 1] = btn
                    activeEntries[#activeEntries + 1] = entry
                else
                    btn.auraInstanceID = nil
                    btn.auraIndex = nil
                    if btn.countText then
                        btn.countText:SetText("")
                    end
                    btn:Hide()
                end
            elseif btn then
                btn.auraInstanceID = nil
                btn.auraIndex = nil
                if btn.countText then
                    btn.countText:SetText("")
                end
                btn:Hide()
            end
        end
    elseif normalizedPrimary and normalizedPrimary ~= PARTY_HAZARD_ENUM.Unknown then
        -- Show only real hazard atlases. Edit-Mode placeholder previews here
        -- duplicated the moveable overlay's preview grid and have been removed.
        local btn = EnsureIconButton(1)
        if btn then
            local appliedPrimaryAtlas = false
            local primaryAtlas = GetHazardIconAtlas(normalizedPrimary)
            if primaryAtlas and btn.icon and btn.icon.SetAtlas then
                local okAtlas = pcall(btn.icon.SetAtlas, btn.icon, primaryAtlas, false)
                if okAtlas then
                    SetIconTexCoord(btn, true)
                    appliedPrimaryAtlas = true
                end
            end
            if btn.countText then
                btn.countText:SetText("")
            end
            btn.auraInstanceID = nil
            btn.auraIndex = nil
            if appliedPrimaryAtlas then
                activeButtons[1] = btn
                activeEntries[1] = nil
                setTextureState = "PRIMARY_ATLAS"
            end
        end
    else
        for i = 1, #iconButtons do
            local btn = iconButtons[i]
            if btn then
                btn.auraInstanceID = nil
                btn.auraIndex = nil
                if btn.countText then
                    btn.countText:SetText("")
                end
                btn:Hide()
            end
        end
        overlay:Hide()
        return
    end

    if #activeButtons == 0 then
        for i = 1, #iconButtons do
            local btn = iconButtons[i]
            if btn then
                btn:Hide()
            end
        end
        overlay:Hide()
        return
    end

    if #activeButtons > 1 then
        setTextureState = setTextureState .. "+multi:" .. tostring(#activeButtons)
    end

    local combat2 = EnsurePartyCombatSettingsTable()
    local perRow = math.max(1, combat2.partyDispelTrackingPerRow or PARTY_DISPEL_TRACKING.DEFAULT_PER_ROW)
    local hSpacing = iconSize + 2
    local vSpacing = iconSize + 2
    local shownCount = #activeButtons
    local cols = math.min(perRow, shownCount)
    local rows = math.ceil(shownCount / perRow)
    local stripWidth = math.max(iconSize + 2, cols * hSpacing)
    local stripHeight = math.max(iconSize + 2, rows * vSpacing)
    overlay:SetSize(stripWidth, stripHeight)
    for i, btn in ipairs(activeButtons) do
        local entry = activeEntries[i]
        local r, g, b = borderR, borderG, borderB
        if entry and entry.enum and PARTY_HAZARD_COLORS[entry.enum] then
            local c = PARTY_HAZARD_COLORS[entry.enum]
            r, g, b = c[1], c[2], c[3]
        end
        SetPartyDispelTrackingIconBorderColor(btn, r, g, b, 1)
        btn:ClearAllPoints()
        local row = math.floor((i - 1) / perRow)
        local col = (i - 1) % perRow
        btn:SetPoint("TOPLEFT", overlay, "TOPLEFT", col * hSpacing, -(row * vSpacing))
        btn:Show()
    end
    for i = shownCount + 1, #iconButtons do
        local btn = iconButtons[i]
        if btn then
            btn.auraInstanceID = nil
            btn.auraIndex = nil
            if btn.countText then
                btn.countText:SetText("")
            end
            btn:Hide()
        end
    end
    overlay:Show()
end

RefreshPartyDispelTrackingOverlays = function(forceHide)
    local hide = forceHide == true
    for i = 1, 4 do
        local frame = PartyFrames[i]
        if frame then
            local unit = frame.GetAttribute and frame:GetAttribute("unit")
            local primaryEnum, secondaryEnum, collectDiag = nil, nil, nil
            local primaryColorOverride = nil
            local restricted = (InCombatLockdown and InCombatLockdown()) and true or false
            if (not hide) and unit and type(unit) == "string" and UnitExists and UnitExists(unit) then
                primaryEnum, secondaryEnum, collectDiag = CollectPartyHazardsForUnit(unit)
                primaryColorOverride = collectDiag and collectDiag.customPrimaryColor or nil
                primaryEnum, secondaryEnum, primaryColorOverride = ApplyPartyStickyHazardFallback(frame, primaryEnum, secondaryEnum, primaryColorOverride, collectDiag)
                if collectDiag and collectDiag.inRestricted == "YES" then
                    restricted = true
                end
            end
            UpdatePartyDispelTrackingOverlay(frame, unit, primaryEnum, primaryColorOverride, restricted, hide)
        end
    end
    if hide then
        if PartyDispelTrackingState.dragOverlay then
            PartyDispelTrackingState.dragOverlay:Hide()
        end
    else
        UpdatePartyDispelTrackingDragOverlay()
    end
end

local function ApplyFrameTextStyle(frame)
    if not frame or not frame.nameText then return end

    local sharedText = _G.MidnightUI_ApplySharedUnitTextStyle
    if type(sharedText) == "function" then
        sharedText(frame, {
            nameFont = "Fonts\\FRIZQT__.TTF",
            nameSize = 12,
            healthFont = "Fonts\\FRIZQT__.TTF",
            healthSize = 11,
            nameShadowAlpha = 0.9,
            healthShadowAlpha = 0.9,
        })
        return
    end

    SetFontSafe(frame.nameText, "Fonts\\FRIZQT__.TTF", 12, "OUTLINE")
    SetFontSafe(frame.healthText, "Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
    frame.nameText:SetShadowOffset(1, -1); frame.nameText:SetShadowColor(0, 0, 0, 0.9)
    frame.healthText:SetShadowOffset(1, -1); frame.healthText:SetShadowColor(0, 0, 0, 0.9)
end

local function ApplyCircleRingTexture(tex, color)
    if not tex then return end
    tex:SetTexture("Interface\\Buttons\\WHITE8X8")
    tex:SetBlendMode("DISABLE")
    if color then
        tex:SetVertexColor(color.r or 1, color.g or 1, color.b or 1, color.a or 1)
    end
end

-- ============================================================
-- SECTION: Health Display + Range + Frame Anchoring
-- SafeUnitValue wraps unit API calls in pcall to handle secret
-- values.  GetDisplayHealthPercent prefers UnitHealthPercent()
-- with ScaleTo100 curve; falls back to manual division.
-- Range ticker periodically calls UnitInRange to fade out-of-
-- range party frames.  EnsurePartyAnchor creates the invisible
-- anchor frame that LayoutPartyFrames positions against.
--
-- READS:  UnitHealth/Max, UnitHealthPercent, UnitInRange,
--         MidnightUISettings.PartyFrames layout settings
-- WRITES: frame alpha (range), anchor position
--
-- FUNCTIONS:
--   SafeUnitValue(fn,unit) — pcall wrapper
--   AllowSecretHealthPercent() — setting gate
--   GetDisplayHealthPercent(unit) — health % with fallback
--   UpdatePartyRangeForFrame(frame) — update alpha for range
--   EnsurePartyRangeTicker() — create 1s C_Timer.NewTicker
--   EnsurePartyAnchor() — create/return anchor frame
--   LayoutPartyFrames() — position all 4 frames in H/V grid
-- ============================================================
local function SafeUnitValue(fn, unit)
    if type(fn) ~= "function" then return nil end
    local ok, val = pcall(fn, unit)
    if not ok then return nil end
    if type(issecretvalue) == "function" and issecretvalue(val) then return nil end
    return val
end

local function GetPartyUnitHealthValues(unit)
    local hp = SafeUnitValue(UnitHealth, unit)
    local maxHp = SafeUnitValue(UnitHealthMax, unit)
    if hp ~= nil and maxHp ~= nil and maxHp > 0 then
        return hp, maxHp, true
    end
    local isGroupUnit = false
    if UnitIsUnit and UnitIsUnit(unit, "player") then
        isGroupUnit = true
    elseif UnitInParty and UnitInParty(unit) then
        isGroupUnit = true
    elseif UnitInRaid and UnitInRaid(unit) then
        isGroupUnit = true
    end
    if not isGroupUnit then
        if UnitExists and UnitExists(unit) then
            local okHp, rawHp = pcall(UnitHealth, unit)
            local okMax, rawMax = pcall(UnitHealthMax, unit)
            if okHp and okMax then
                if type(issecretvalue) == "function" and (issecretvalue(rawHp) or issecretvalue(rawMax)) then
                    return nil, nil, false
                end
                local hpNum = CoerceNumber(rawHp)
                local maxNum = CoerceNumber(rawMax)
                if hpNum ~= nil and maxNum ~= nil and maxNum > 0 then
                    return hpNum, maxNum, false
                end
            end
        end
        return nil, nil, false
    end
    local okHp, rawHp = pcall(UnitHealth, unit)
    local okMax, rawMax = pcall(UnitHealthMax, unit)
    if okHp and okMax then
        local hpNum = CoerceNumber(rawHp)
        local maxNum = CoerceNumber(rawMax)
        if hpNum ~= nil and maxNum ~= nil and maxNum > 0 then
            return hpNum, maxNum, false
        end
    end
    return nil, nil, false
end

local function AllowSecretHealthPercent()
    if _G.MidnightUI_ForceHideHealthPct then return false end
    return MidnightUISettings and MidnightUISettings.General and MidnightUISettings.General.allowSecretHealthPercent == true
end

local lastAllowSecretState = nil

local function IsSecretValue(val)
    if type(issecretvalue) ~= "function" then return false end
    local ok, res = pcall(issecretvalue, val)
    return ok and res == true
end

local function IsUnitActuallyDead(unit)
    if not unit or not UnitExists(unit) then return false end

    local okDead, dead = pcall(UnitIsDead, unit)
    if okDead and type(dead) == "boolean" and dead then
        return true
    end

    local okGhost, ghost = pcall(UnitIsGhost, unit)
    if okGhost and type(ghost) == "boolean" and ghost then
        return true
    end

    local okHp, hp = pcall(UnitHealth, unit)
    if okHp and type(hp) == "number" then
        local okCmp, isZeroOrLess = pcall(function()
            return hp <= 0
        end)
        if okCmp and isZeroOrLess then
            return true
        end
    end

    return false
end

local function GetDisplayHealthPercent(unit)
    if not unit then return nil end
    local allowSecret = AllowSecretHealthPercent()
    if not allowSecret then
        return nil
    end
    if UnitHealthPercent then
        local scaleTo100 = CurveConstants and CurveConstants.ScaleTo100 or nil
        local ok, pct = pcall(UnitHealthPercent, unit, true, scaleTo100)
        if ok and pct ~= nil then
            return pct
        end
    end
    return nil
end

local function GetDisplayHealthPercentWithText(unit)
    local pct = GetDisplayHealthPercent(unit)
    if pct == nil then
        return nil, nil
    end
    local ok, text = pcall(string.format, "%.0f%%", pct)
    if not ok or text == nil then
        return nil, nil
    end
    return pct, text
end


local function UpdatePartyRangeForFrame(frame)
    local unit = frame:GetAttribute("unit")
    if UnitExists(unit) then
        local inRange = true
        local ok, val = pcall(UnitInRange, unit)
        if ok and not IsSecretValue(val) and (val == true or val == false) then
            inRange = val
        end
        if not inRange and UnitIsUnit(unit, "player") then
            inRange = true
        end
        frame:SetAlpha(inRange and 1.0 or 0.35)
    else
        frame:SetAlpha(1.0)
    end
end

local function UpdatePartyRanges()
    for i = 1, 4 do
        local frame = PartyFrames[i]
        if frame and frame:IsShown() then
            UpdatePartyRangeForFrame(frame)
        end
    end
end

local function EnsurePartyRangeTicker()
    if PartyRangeTicker then return end
    PartyRangeTicker = C_Timer.NewTicker(0.2, UpdatePartyRanges)
end

local function ShowUnitTooltip(owner, unit)
    if not owner or not unit then return end
    if not UnitExists or not UnitExists(unit) then return end
    GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
    GameTooltip:SetUnit(unit)
    GameTooltip:Show()
end

local function ShowDummyTooltip(owner, name, class, role)
    if not owner then return end
    GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
    GameTooltip:ClearLines()
    local displayName = name or "Party Member"
    local classText = class or "Adventurer"
    local roleText = role or "DAMAGER"
    GameTooltip:AddLine(displayName, 1, 0.82, 0)
    local roleAtlas = nil
    if roleText == "TANK" then
        roleAtlas = "UI-LFG-RoleIcon-Tank"
    elseif roleText == "HEALER" then
        roleAtlas = "UI-LFG-RoleIcon-Healer"
    else
        roleAtlas = "UI-LFG-RoleIcon-DPS"
    end
    local roleIconMarkup = roleAtlas and ("|A:" .. roleAtlas .. ":16:16|a") or roleText
    GameTooltip:AddDoubleLine("Level 70 " .. classText, roleIconMarkup, 1, 1, 1, 1, 1, 1)
    GameTooltip:AddLine(" ", 1, 1, 1)
    GameTooltip:AddDoubleLine("Health", "128,430 / 146,200", 0.7, 1, 0.7, 0.9, 1, 0.9)
    GameTooltip:AddDoubleLine("Mana", "92,110 / 138,560", 0.6, 0.8, 1, 0.8, 0.9, 1)
    GameTooltip:Show()
end

local function EnsurePartyAnchor()
    if PartyAnchor then return end
    PartyAnchor = CreateFrame("Frame", "MidnightUI_PartyAnchor", UIParent)
    PartyAnchor:SetSize(config.width, config.height)
    PartyAnchor:SetMovable(true)
    PartyAnchor:SetClampedToScreen(true)
    local pos = MidnightUISettings and MidnightUISettings.PartyFrames and MidnightUISettings.PartyFrames.position
    if pos then
        if pos[2] == "UIParent" and pos[3] then
            PartyAnchor:SetPoint(pos[1], UIParent, pos[3], pos[4], pos[5])
        else
            PartyAnchor:SetPoint(pos[1], UIParent, pos[2], pos[3], pos[4])
        end
    else
        PartyAnchor:SetPoint(unpack(config.startPos))
    end
end

local function ApplyPartyConfigFromSettings() end

local function UpdatePartyOverlayPreview()
    if not PartyAnchor or not PartyAnchor.dragOverlay then return end
    if not PartyAnchor.dragOverlay:IsShown() then return end
    local w = config.width
    local h = config.height
    local spacingX = config.spacingX or 8
    local spacingY = config.spacingY or 8
    local total = 4

    local previewWidth
    local previewHeight
    if config.layout == "Horizontal" then
        previewWidth = (w * total) + (spacingX * (total - 1))
        previewHeight = h
    else
        previewWidth = w
        previewHeight = (h * total) + (spacingY * (total - 1))
    end

    for i = 1, total do
        local box = PartyPreviewBoxes[i]
        if not box then
            box = CreateFrame("Frame", nil, PartyAnchor.dragOverlay, "BackdropTemplate")
            box:SetBackdrop({
                bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
                edgeFile = "Interface\\Buttons\\WHITE8X8",
                edgeSize = 1,
                insets = { left = 0, right = 0, top = 0, bottom = 0 }
            })
            box:SetBackdropColor(0.15, 0.18, 0.24, 0.55)
            box:SetBackdropBorderColor(0.35, 0.75, 1.0, 0.95)
            local n = box:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            n:SetPoint("CENTER")
            n:SetText(tostring(i))
            n:SetTextColor(1, 0.9, 0.1)
            box.label = n
            PartyPreviewBoxes[i] = box
        end
        box:SetSize(w, h)
        box:ClearAllPoints()
        if config.layout == "Horizontal" then
            box:SetPoint("TOPLEFT", PartyAnchor, "TOPLEFT", (i - 1) * (w + spacingX), 0)
        else
            box:SetPoint("TOPLEFT", PartyAnchor, "TOPLEFT", 0, -((i - 1) * (h + spacingY)))
        end
        if box.label then box.label:Show() end
        box:SetBackdropColor(0.15, 0.18, 0.24, 0.55)
        box:SetBackdropBorderColor(0.35, 0.75, 1.0, 0.95)
        box:Show()
    end

    for i = total + 1, #PartyPreviewBoxes do
        if PartyPreviewBoxes[i] then PartyPreviewBoxes[i]:Hide() end
    end
    if UpdatePartyDispelTrackingDragOverlay then
        UpdatePartyDispelTrackingDragOverlay()
    end
end

local pendingPartyLayout = false

local function LayoutPartyFrames()
    ApplyPartyConfigFromSettings()
    EnsurePartyAnchor()
    if not PartyAnchor then return end
    if InCombatLockdown and InCombatLockdown() then
        pendingPartyLayout = true
        return
    end
    pendingPartyLayout = false
    local w = config.width
    local h = config.height
    local hide2DPortrait = (config.hide2DPortrait == true)
    local spacingX = config.spacingX or 8
    local spacingY = config.spacingY or 8
    for i = 1, 4 do
        local frame = PartyFrames[i]
        if frame then
            frame:SetSize(w, h)
            frame:ClearAllPoints()
            if config.layout == "Horizontal" then
                frame:SetPoint("TOPLEFT", PartyAnchor, "TOPLEFT", (i - 1) * (w + spacingX), 0)
            else
                frame:SetPoint("TOPLEFT", PartyAnchor, "TOPLEFT", 0, -((i - 1) * (h + spacingY)))
            end
            if frame.portrait then
                local portraitSize = hide2DPortrait and 0 or (h - 2)
                frame.portrait:SetSize(portraitSize, portraitSize)
            end
            if frame.healthContainer then
                frame.healthContainer:ClearAllPoints()
                if hide2DPortrait then
                    frame.healthContainer:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
                else
                    frame.healthContainer:SetPoint("TOPLEFT", frame.portrait, "TOPRIGHT", 0, 0)
                end
                frame.healthContainer:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
                frame.healthContainer:SetHeight(h * 0.75)
            end
            if frame.powerContainer then
                frame.powerContainer:SetHeight(math.max(8, h - (h * 0.75) - 2))
            end
            if frame.roleIcon and frame.healthContainer then
                frame.roleIcon:ClearAllPoints()
                if hide2DPortrait then
                    frame.roleIcon:SetPoint("BOTTOMLEFT", frame.healthContainer, "BOTTOMLEFT", -2, 2)
                else
                    frame.roleIcon:SetPoint("BOTTOMRIGHT", frame.portrait, "BOTTOMRIGHT", 2, -2)
                end
            end
            if frame.leaderIcon and frame.healthContainer then
                frame.leaderIcon:ClearAllPoints()
                if hide2DPortrait then
                    frame.leaderIcon:SetPoint("TOPLEFT", frame.healthContainer, "TOPLEFT", -2, 2)
                else
                    frame.leaderIcon:SetPoint("TOPLEFT", frame.portrait, "TOPLEFT", -2, 2)
                end
            end
            if frame.phaseIcon and frame.healthContainer then
                frame.phaseIcon:ClearAllPoints()
                if hide2DPortrait then
                    frame.phaseIcon:SetPoint("CENTER", frame.healthContainer, "CENTER", 0, 0)
                else
                    frame.phaseIcon:SetPoint("CENTER", frame.portrait, "CENTER", 0, 0)
                end
            end
            if frame.auras then
                frame.auras:SetSize(w, config.auraSize or 20)
            end
        end
    end
    if config.layout == "Horizontal" then
        PartyAnchor:SetSize((w * 4) + (spacingX * 3), h)
    else
        PartyAnchor:SetSize(w, (h * 4) + (spacingY * 3))
    end
    UpdatePartyOverlayPreview()
end

-- ============================================================
-- SECTION: Public Layout API + Frame Creation
-- MidnightUI_ApplyPartyFramesLayout / SetPartyStyle /
-- SetPartyFramesLocked are the entry points called from
-- Settings_UI.  CreateSinglePartyFrame builds one full party
-- frame: health bar, power bar, portrait (2D/3D), name text,
-- health/power texts, aura container, debuff overlay, dispel
-- tracking overlay.
--
-- READS:  config (from ApplyPartyConfigFromSettings), index
-- WRITES: PartyFrames[index] (module-local array),
--         RegisterUnitWatch per frame
--
-- FUNCTIONS:
--   MidnightUI_ApplyPartyFramesLayout() — reads settings +
--     calls LayoutPartyFrames
--   MidnightUI_SetPartyStyle(style) — switches bar gradient
--   MidnightUI_SetPartyFramesLocked(locked) — drag overlay toggle
--   CreateSinglePartyFrame(index) — full frame constructor;
--     called lazily on group join or once on PLAYER_ENTERING_WORLD
-- ============================================================
function MidnightUI_ApplyPartyFramesLayout()
    ApplyPartyConfigFromSettings()
    LayoutPartyFrames()
    if _G.MidnightUI_Debug then
    end
    for i = 1, 4 do
        local f = PartyFrames[i]
        if f then UpdatePartyUnit(f) end
    end
end

function MidnightUI_SetPartyStyle(style)
    if not MidnightUISettings then MidnightUISettings = {} end
    if not MidnightUISettings.PartyFrames then MidnightUISettings.PartyFrames = {} end
    MidnightUISettings.PartyFrames.style = style
    config.style = style
    MidnightUI_ApplyPartyFramesLayout()
end

do
    local MR = _G.MidnightUI_MoverRegistry
    if MR and MR.DefineMover then
        MR.DefineMover("PartyFrames", {
            resolveFrame = function()
                EnsurePartyAnchor()
                return PartyAnchor
            end,
            defaultPosition = "TOPLEFT,UIParent,TOPLEFT,31.67,-31.77",
            category = "unit", label = "Party Frames",
            externalOverlay = true,
            onLock = function()
                MidnightUI_SetPartyFramesLocked(true)
            end,
            onUnlock = function()
                MidnightUI_SetPartyFramesLocked(false)
            end,
        })
    end
end

function MidnightUI_SetPartyFramesLocked(locked)
    EnsurePartyAnchor()
    if not PartyAnchor then return end
    PartyDispelTrackingState.locked = (locked ~= false)
    if locked then
        if PartyAnchor.dragOverlay then PartyAnchor.dragOverlay:Hide() end
        if PartyDispelTrackingState.dragOverlay then
            PartyDispelTrackingState.dragOverlay:Hide()
        end
    else
        if not PartyAnchor.dragOverlay then
            local overlay = CreateFrame("Frame", nil, PartyAnchor, "BackdropTemplate")
            overlay:ClearAllPoints()
            overlay:SetPoint("TOPLEFT", PartyAnchor, "TOPLEFT", 0, 0)
            overlay:SetPoint("BOTTOMRIGHT", PartyAnchor, "BOTTOMRIGHT", 0, -26)
            overlay:SetFrameStrata("HIGH")
            overlay:SetBackdrop({
                bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
                edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
                tile = true,
                tileSize = 16,
                edgeSize = 16,
                insets = { left = 4, right = 4, top = 4, bottom = 4 }
            })
            overlay:SetBackdropColor(0.05, 0.08, 0.11, 0.30)
            overlay:SetBackdropBorderColor(0.30, 0.46, 0.58, 0.78)
            if _G.MidnightUI_StyleOverlay then _G.MidnightUI_StyleOverlay(overlay, nil, nil, "unit") end
            local label = overlay:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
            label:SetPoint("BOTTOM", overlay, "BOTTOM", 0, 8)
            label:SetText("PARTY FRAMES")
            label:SetTextColor(1, 1, 1)
            overlay:EnableMouse(true)
            overlay:RegisterForDrag("LeftButton")
            overlay:SetScript("OnDragStart", function() PartyAnchor:StartMoving() end)
            overlay:SetScript("OnDragStop", function()
                PartyAnchor:StopMovingOrSizing()
                local point, relativeTo, relativePoint, xOfs, yOfs = PartyAnchor:GetPoint()
                if MidnightUISettings and MidnightUISettings.PartyFrames then
                    MidnightUISettings.PartyFrames.position = { point, relativePoint, xOfs, yOfs }
                end
                local MR = _G.MidnightUI_MoverRegistry
                if MR and MR.SavePosition then MR.SavePosition("PartyFrames") end
            end)
            if _G.MidnightUI_AttachOverlaySettings then
                _G.MidnightUI_AttachOverlaySettings(overlay, "PartyFrames")
            end
            PartyAnchor.dragOverlay = overlay
        end
        PartyAnchor.dragOverlay:Show()
        UpdatePartyOverlayPreview()
        UpdatePartyDispelTrackingDragOverlay()
    end
    if RefreshPartyDispelTrackingOverlays then
        RefreshPartyDispelTrackingOverlays(false)
    end
end

local function SetPartyDispelTrackingLocked(locked)
    PartyDispelTrackingState.locked = (locked ~= false)
    if locked and PartyDispelTrackingState.dragOverlay then
        PartyDispelTrackingState.dragOverlay:Hide()
    end
    if RefreshPartyDispelTrackingOverlays then
        RefreshPartyDispelTrackingOverlays(false)
    end
end


local function CreateSinglePartyFrame(index)
    local unit = "party"..index
    local frameName = "MidnightUI_PartyFrame"..index
    
    EnsurePartyAnchor()
    local frame = CreateFrame("Button", frameName, PartyAnchor, "SecureUnitButtonTemplate, BackdropTemplate")
    frame:SetSize(config.width, config.height)
    frame:SetAttribute("unit", unit)
    frame:SetAttribute("type1", "target")
    frame:SetAttribute("*type2", "togglemenu")
    frame:RegisterForClicks("AnyUp")
    
    frame:ClearAllPoints()

    RegisterUnitWatch(frame)

    frame:SetScript("OnEnter", function(self)
        if MidnightUISettings and MidnightUISettings.PartyFrames and MidnightUISettings.PartyFrames.showTooltip == false then
            return
        end
        local u = self:GetAttribute("unit")
        ShowUnitTooltip(self, u)
    end)
    frame:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    frame:SetFrameStrata("MEDIUM"); frame:SetFrameLevel(10); frame:EnableMouse(true)
    
    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    frame._muiBackdropOrig = frame:GetBackdrop()
    frame:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
    frame:SetBackdropBorderColor(0, 0, 0, 1)

    frame.shadows = CreateDropShadow(frame, 3)

    local portraitSize = config.height - 2
    local portrait = frame:CreateTexture(nil, "ARTWORK")
    portrait:SetSize(portraitSize, portraitSize)
    portrait:SetPoint("LEFT", frame, "LEFT", 1, 0)
    portrait:SetAlpha(1)
    
    local portraitSep = frame:CreateTexture(nil, "OVERLAY")
    portraitSep:SetWidth(1)
    portraitSep:SetPoint("TOPRIGHT", portrait, "TOPRIGHT", 0, 0)
    portraitSep:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", 0, 0)
    portraitSep:SetColorTexture(0, 0, 0, 1)
    frame.portraitSep = portraitSep
    
    local portraitBg = frame:CreateTexture(nil, "BACKGROUND")
    portraitBg:SetAllPoints(portrait)
    portraitBg:SetColorTexture(0.1, 0.1, 0.1, 1)
    frame.portraitBg = portraitBg
    frame.portrait = portrait

    local healthContainer = CreateFrame("Frame", nil, frame)
    healthContainer:SetPoint("TOPLEFT", portrait, "TOPRIGHT", 0, 0)
    healthContainer:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
    healthContainer:SetHeight(config.height * 0.75)
    frame.healthContainer = healthContainer
    
    local healthBg = healthContainer:CreateTexture(nil, "BACKGROUND")
    healthBg:SetAllPoints()
    healthBg:SetColorTexture(0.1, 0.1, 0.1, 1)
    frame.healthBg = healthBg

    local healthBar = CreateFrame("StatusBar", nil, healthContainer)
    healthBar:SetAllPoints()
    ApplyHealthBarStyle(healthBar)
    healthBar:SetStatusBarColor(0.5, 0.5, 0.5, 0.92); healthBar:SetMinMaxValues(0, 1); healthBar:SetValue(1)
    frame.healthBar = healthBar
    
    local absorbBar = CreateFrame("StatusBar", nil, healthContainer)
    absorbBar:SetAllPoints()
    absorbBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
    absorbBar:SetStatusBarColor(0.6, 0.9, 1, 0.35)
    absorbBar:SetMinMaxValues(0, 1); absorbBar:SetValue(0)
    frame.absorbBar = absorbBar

    local textFrame = CreateFrame("Frame", nil, healthContainer)
    textFrame:SetAllPoints()
    textFrame:SetFrameLevel(healthContainer:GetFrameLevel() + 5)

    local powerContainer = CreateFrame("Frame", nil, frame)
    powerContainer:SetPoint("TOPLEFT", healthContainer, "BOTTOMLEFT", 0, -1)
    powerContainer:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1)
    frame.powerContainer = powerContainer
    
    local pSep = powerContainer:CreateTexture(nil, "OVERLAY", nil, 7)
    pSep:SetHeight(1); pSep:SetPoint("TOPLEFT"); pSep:SetPoint("TOPRIGHT")
    pSep:SetColorTexture(0, 0, 0, 1)
    
    local powerBg = powerContainer:CreateTexture(nil, "BACKGROUND")
    powerBg:SetAllPoints()
    powerBg:SetColorTexture(0.02, 0.02, 0.02, 1)
    frame.powerBg = powerBg

    local powerBar = CreateFrame("StatusBar", nil, powerContainer)
    powerBar:SetAllPoints()
    powerBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
    powerBar:GetStatusBarTexture():SetHorizTile(false)
    powerBar:SetStatusBarColor(0, 0.5, 1, 0.95); powerBar:SetMinMaxValues(0, 1); powerBar:SetValue(1)
    frame.powerBar = powerBar

    local nameText = textFrame:CreateFontString(nil, "OVERLAY")
    nameText:SetPoint("LEFT", healthContainer, "LEFT", 6, 1)
    SetFontSafe(nameText, "Fonts\\FRIZQT__.TTF", 12, "OUTLINE")
    nameText:SetTextColor(1, 1, 1, 1); nameText:SetShadowOffset(1, -1); nameText:SetShadowColor(0, 0, 0, 0.9)
    frame.nameText = nameText

    local healthText = textFrame:CreateFontString(nil, "OVERLAY")
    healthText:SetPoint("RIGHT", healthContainer, "RIGHT", -6, 1)
    SetFontSafe(healthText, "Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
    healthText:SetTextColor(1, 1, 1, 1); healthText:SetShadowOffset(1, -1); healthText:SetShadowColor(0, 0, 0, 0.9)
    frame.healthText = healthText

    local simpleTextFrame = CreateFrame("Frame", nil, healthContainer)
    simpleTextFrame:SetAllPoints(healthContainer)
    simpleTextFrame:SetFrameLevel(healthContainer:GetFrameLevel() + 5)
    simpleTextFrame:Hide()
    frame.simpleTextFrame = simpleTextFrame

    local simpleNameText = simpleTextFrame:CreateFontString(nil, "OVERLAY")
    simpleNameText:SetPoint("LEFT", simpleTextFrame, "LEFT", 6, 0)
    simpleNameText:SetPoint("CENTER", simpleTextFrame, "CENTER", 0, 0)
    SetFontSafe(simpleNameText, "Fonts\\FRIZQT__.TTF", 12, "OUTLINE")
    simpleNameText:SetTextColor(1, 1, 1, 1)
    simpleNameText:SetShadowOffset(1, -1)
    simpleNameText:SetShadowColor(0, 0, 0, 0.9)
    frame.simpleNameText = simpleNameText

    local simpleHealthText = simpleTextFrame:CreateFontString(nil, "OVERLAY")
    simpleHealthText:SetPoint("RIGHT", simpleTextFrame, "RIGHT", -6, 0)
    simpleHealthText:SetPoint("CENTER", simpleTextFrame, "CENTER", 0, 0)
    SetFontSafe(simpleHealthText, "Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
    simpleHealthText:SetTextColor(1, 1, 1, 1)
    simpleHealthText:SetShadowOffset(1, -1)
    simpleHealthText:SetShadowColor(0, 0, 0, 0.9)
    frame.simpleHealthText = simpleHealthText

    local resIcon = textFrame:CreateTexture(nil, "OVERLAY")
    resIcon:SetSize(28, 28)
    resIcon:SetPoint("CENTER", healthContainer, "CENTER", 0, 0)
    resIcon:SetTexture("Interface\\RaidFrame\\Raid-Icon-Rez")
    resIcon:SetVertexColor(1, 1, 1, 1)
    resIcon:Hide()
    frame.resIcon = resIcon

    local deadIcon = textFrame:CreateTexture(nil, "OVERLAY")
    deadIcon:SetSize(14, 14)
    deadIcon:SetPoint("CENTER", healthText, "CENTER", 0, 0)
    EnsureDeadIconTexture(deadIcon)
    deadIcon:SetVertexColor(1, 1, 1, 1)
    deadIcon:SetDrawLayer("OVERLAY", 7)
    deadIcon:SetAlpha(1)
    deadIcon:Hide()
    frame.deadIcon = deadIcon
    
    local resGlow = textFrame:CreateTexture(nil, "ARTWORK")
    resGlow:SetSize(50, 50)
    resGlow:SetPoint("CENTER", resIcon, "CENTER")
    resGlow:SetTexture("Interface\\Buttons\\WHITE8X8")
    resGlow:SetVertexColor(1, 0.85, 0.3, 0)
    resGlow:SetBlendMode("ADD")
    resGlow:Hide()
    frame.resGlow = resGlow

    local roleIcon = textFrame:CreateTexture(nil, "OVERLAY")
    roleIcon:SetSize(26, 26)
    roleIcon:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", 2, -2)
    frame.roleIcon = roleIcon
    roleIcon:SetVertexColor(1, 1, 1, 1)

    local leaderIcon = textFrame:CreateTexture(nil, "OVERLAY")
    leaderIcon:SetSize(14, 14)
    leaderIcon:SetPoint("TOPLEFT", portrait, "TOPLEFT", -2, 2)
    frame.leaderIcon = leaderIcon

    local targetHl = frame:CreateTexture(nil, "OVERLAY")
    targetHl:SetAllPoints()
    targetHl:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2")
    targetHl:SetVertexColor(1, 1, 1, 0.2)
    targetHl:SetBlendMode("ADD")
    targetHl:Hide()
    frame.targetHighlight = targetHl

    local rcOverlay = CreateFrame("Frame", nil, frame)
    rcOverlay:SetAllPoints(frame)
    rcOverlay:SetFrameStrata(frame:GetFrameStrata())
    rcOverlay:SetFrameLevel((frame:GetFrameLevel() or 10) + 50)
    frame.readyCheckOverlay = rcOverlay
    local rcIcon = rcOverlay:CreateTexture(nil, "OVERLAY", nil, 7)
    rcIcon:SetSize(portraitSize - 4, portraitSize - 4)
    rcIcon:SetPoint("CENTER", portrait, "CENTER", 0, 0)
    rcIcon:Hide()
    frame.readyCheckIcon = rcIcon

    local phaseIcon = frame:CreateTexture(nil, "OVERLAY")
    phaseIcon:SetSize(24, 24)
    phaseIcon:SetPoint("CENTER", portrait, "CENTER", 0, 0)
    phaseIcon:SetTexture("Interface\\TargetingFrame\\UI-PhasingIcon")
    phaseIcon:Hide()
    frame.phaseIcon = phaseIcon

    frame.auras = CreateFrame("Frame", nil, frame); frame.auras:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", 0, -4); frame.auras:SetSize(config.width, config.auraSize); frame.auraButtons = {}

    local dispelGlow = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    dispelGlow:SetPoint("TOPLEFT", -2, 2)
    dispelGlow:SetPoint("BOTTOMRIGHT", 2, -2)
    dispelGlow:SetBackdrop({
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 2,
    })
    dispelGlow:SetBackdropBorderColor(0, 0, 0, 0)
    dispelGlow:SetFrameLevel(frame:GetFrameLevel() + 20)
    dispelGlow:EnableMouse(false)
    dispelGlow:Hide()
    frame.dispelGlow = dispelGlow

    EnsurePartyDispelTrackingOverlay(frame)

    return frame
end


local function BuildPartyOverlaySettings(content)
    if not _G.MidnightUI_CreateOverlayBuilder then return end
    if not MidnightUISettings.PartyFrames then MidnightUISettings.PartyFrames = {} end
    local s = MidnightUISettings.PartyFrames
    local b = _G.MidnightUI_CreateOverlayBuilder(content, { startY = -6 })
    b:Header("Layout")
    b:Dropdown("Party Layout", {"Vertical", "Horizontal"}, s.layout or "Vertical", function(v)
        s.layout = v
        if _G.MidnightUI_ApplyPartyFramesLayout then _G.MidnightUI_ApplyPartyFramesLayout() end
        if _G.MidnightUI_ShowOverlaySettings then _G.MidnightUI_ShowOverlaySettings("PartyFrames") end
    end)
    b:Dropdown("Party Styling", {"Rendered", "Simple"}, s.style or "Rendered", function(v)
        if _G.MidnightUI_SetPartyStyle then
            _G.MidnightUI_SetPartyStyle(v)
        else
            s.style = v
            if _G.MidnightUI_ApplyPartyFramesLayout then _G.MidnightUI_ApplyPartyFramesLayout() end
        end
        if _G.MidnightUI_ShowOverlaySettings then _G.MidnightUI_ShowOverlaySettings("PartyFrames") end
    end)

    b:Slider("Width", 120, 420, 2, s.width or 240, function(v)
        s.width = math.floor(v)
        if _G.MidnightUI_ApplyPartyFramesLayout then _G.MidnightUI_ApplyPartyFramesLayout() end
    end)
    b:Slider("Height", 24, 120, 2, s.height or 58, function(v)
        s.height = math.floor(v)
        if _G.MidnightUI_ApplyPartyFramesLayout then _G.MidnightUI_ApplyPartyFramesLayout() end
    end)

    if (s.layout or "Vertical") == "Horizontal" then
        b:Slider("Horizontal Spacing", 0, 200, 1, s.spacingX or 8, function(v)
            s.spacingX = math.floor(v)
            if _G.MidnightUI_ApplyPartyFramesLayout then _G.MidnightUI_ApplyPartyFramesLayout() end
        end)
    else
        b:Slider("Vertical Spacing", 0, 200, 1, s.spacingY or 8, function(v)
            s.spacingY = math.floor(v)
            if _G.MidnightUI_ApplyPartyFramesLayout then _G.MidnightUI_ApplyPartyFramesLayout() end
        end)
    end

    b:Checkbox("Hide 2D Portrait", s.hide2DPortrait == true, function(v)
        s.hide2DPortrait = v and true or false
        if _G.MidnightUI_ApplyPartyFramesLayout then _G.MidnightUI_ApplyPartyFramesLayout() end
    end)

    b:Checkbox("Show Tooltip", s.showTooltip ~= false, function(v)
        s.showTooltip = v and true or false
    end)

    b:Checkbox("Hide In Raid", s.hideInRaid == true, function(v)
        s.hideInRaid = v and true or false
        if _G.MidnightUI_UpdatePartyVisibility then _G.MidnightUI_UpdatePartyVisibility() end
    end)
    b:Checkbox("Party Debuff Overlay", IsPartyDebuffOverlayEnabled(), function(v)
        local combat = EnsurePartyCombatSettingsTable()
        combat.debuffOverlayPartyEnabled = v and true or false
        if RefreshPartyDispelTrackingOverlays then
            RefreshPartyDispelTrackingOverlays(false)
        end
    end)

    return b:Height()
end

local function BuildPartyDispelTrackingOverlaySettings(content)
    if not _G.MidnightUI_CreateOverlayBuilder then
        return
    end
    local combat = EnsurePartyCombatSettingsTable()
    local b = _G.MidnightUI_CreateOverlayBuilder(content, { startY = -6 })
    b:Header("Party Dispel Icons")
    b:Checkbox("Party Debuff Overlay", IsPartyDebuffOverlayEnabled(), function(v)
        local combatSettings = EnsurePartyCombatSettingsTable()
        combatSettings.debuffOverlayPartyEnabled = v and true or false
        if RefreshPartyDispelTrackingOverlays then
            RefreshPartyDispelTrackingOverlays(false)
        end
    end)
    b:Checkbox("Enable Party Dispel Icons", combat.partyDispelTrackingEnabled ~= false, function(v)
        combat.partyDispelTrackingEnabled = v and true or false
        if RefreshPartyDispelTrackingOverlays then
            RefreshPartyDispelTrackingOverlays(false)
        end
    end)
    b:Slider("Icon Size %", PARTY_DISPEL_TRACKING.ICON_SCALE_MIN, PARTY_DISPEL_TRACKING.ICON_SCALE_MAX, 5, ClampPartyDispelTrackingIconScale(combat.partyDispelTrackingIconScale), function(v)
        combat.partyDispelTrackingIconScale = ClampPartyDispelTrackingIconScale(v)
        if RefreshPartyDispelTrackingOverlays then
            RefreshPartyDispelTrackingOverlays(false)
        end
    end)
    b:Slider("Opacity", 0.1, 1.0, 0.05, ClampPartyDispelTrackingAlpha(combat.partyDispelTrackingAlpha), function(v)
        combat.partyDispelTrackingAlpha = ClampPartyDispelTrackingAlpha(v)
        if RefreshPartyDispelTrackingOverlays then
            RefreshPartyDispelTrackingOverlays(false)
        end
    end)
    local maxShown = math.floor((tonumber(combat.partyDispelTrackingMaxShown) or 4) + 0.5)
    if maxShown < 1 then
        maxShown = 1
    elseif maxShown > PARTY_DISPEL_TRACKING.MAX_SHOWN_LIMIT then
        maxShown = PARTY_DISPEL_TRACKING.MAX_SHOWN_LIMIT
    end
    combat.partyDispelTrackingMaxShown = maxShown
    b:Slider("Max Icons", 1, PARTY_DISPEL_TRACKING.MAX_SHOWN_LIMIT, 1, maxShown, function(v)
        local n = math.floor((tonumber(v) or 4) + 0.5)
        if n < 1 then
            n = 1
        elseif n > PARTY_DISPEL_TRACKING.MAX_SHOWN_LIMIT then
            n = PARTY_DISPEL_TRACKING.MAX_SHOWN_LIMIT
        end
        combat.partyDispelTrackingMaxShown = n
        if RefreshPartyDispelTrackingOverlays then
            RefreshPartyDispelTrackingOverlays(false)
        end
    end)
    local perRow = math.max(1, combat.partyDispelTrackingPerRow or PARTY_DISPEL_TRACKING.DEFAULT_PER_ROW)
    b:Slider("Icons Per Row", 1, PARTY_DISPEL_TRACKING.MAX_SHOWN_LIMIT, 1, perRow, function(v)
        local n = math.floor((tonumber(v) or PARTY_DISPEL_TRACKING.DEFAULT_PER_ROW) + 0.5)
        if n < 1 then n = 1 end
        combat.partyDispelTrackingPerRow = n
        if RefreshPartyDispelTrackingOverlays then
            RefreshPartyDispelTrackingOverlays(false)
        end
    end)
    return b:Height()
end

if _G.MidnightUI_RegisterOverlaySettings then
    _G.MidnightUI_RegisterOverlaySettings("PartyFrames", { title = "Party Frames", build = BuildPartyOverlaySettings })
    _G.MidnightUI_RegisterOverlaySettings("PartyDispelTracking", { title = "Party Dispel Icons", build = BuildPartyDispelTrackingOverlaySettings })
end

-- ============================================================
-- SECTION: Frame Styling + Main Unit Update Engine
-- ApplyPartyFramesBarStyle applies gradient/flat style to all
-- frames.  UpdateAuras refreshes the aura container on each
-- frame.  UpdatePartyUnit is the central dispatch: calls health,
-- power, auras, portrait, ready-check, range, and debuff overlay
-- update functions.  SuppressBlizzardPartyFrame + siblings handle
-- hiding Blizzard frames.
--
-- READS:  unit API (UnitHealth, UnitExists, UnitIsConnected...),
--         hazard classification results, portrait state
-- WRITES: frame.healthBar SetValue/color, frame nameText,
--         portrait model, aura icons, debuff overlay state
--
-- FUNCTIONS:
--   ApplyPartyFramesBarStyle() — gradient dispatch for all frames
--   UpdateAuras(frame) — refresh aura icon row
--   UpdatePartyUnit(frame) — master update dispatcher
--   SuppressBlizzardPartyFrame(frame) — hooks SetAlpha + OnShow
--   UnsuppressBlizzardPartyFrame(frame) — removes hooks
--   ForEachBlizzardPartyFrame(fn) — iterates all Blizzard frames
--   RestoreBlizzardPartyFrames() — unsuppress all on disable
--   MidnightUI_RefreshPartyFrames() — public refresh entry
--   InitPartyFrames() — create all 4 frames, apply settings
--   StartPartyDebuffCombatRefresh() — 0.2s ticker during combat
--   StopPartyDebuffCombatRefresh() — cancel ticker
-- ============================================================
local function ApplyPartyFramesBarStyle()
    for i = 1, 4 do
        local frame = PartyFrames[i]
        if frame and frame.healthBar then
            if (config.style or "Rendered") ~= "Rendered" then
                ApplyHealthBarStyle(frame.healthBar)
            else
                ClearBarOverlays(frame.healthBar)
            end
        end
        if frame and frame.powerBar then
            if (config.style or "Rendered") ~= "Rendered" then
                frame.powerBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
                local tex = frame.powerBar:GetStatusBarTexture()
                if tex then
                    tex:SetHorizTile(false)
                    tex:SetVertTile(false)
                end
            else
                ClearBarOverlays(frame.powerBar)
            end
        end
        ApplyFrameTextStyle(frame)
    end
end
_G.MidnightUI_ApplyPartyFramesBarStyle = ApplyPartyFramesBarStyle


local function UpdatePartyVisibility()
    EnsurePartyAnchor()
    if not PartyAnchor then return end
    if InCombatLockdown and InCombatLockdown() then
        pendingPartyVisibility = true
        return
    end
    pendingPartyVisibility = false
    if MidnightUISettings and MidnightUISettings.PartyFrames and MidnightUISettings.PartyFrames.enabled == false then
        for i = 1, 4 do
            local f = PartyFrames[i]
            if f then f:Hide() end
        end
        PartyAnchor:Hide()
        RestoreBlizzardPartyFrames()
        return
    else
        ForEachBlizzardPartyFrame(SuppressBlizzardPartyFrame)
    end
    local hideInRaid = MidnightUISettings
        and MidnightUISettings.PartyFrames
        and MidnightUISettings.PartyFrames.hideInRaid == true
    if hideInRaid then
        if RegisterStateDriver then
            RegisterStateDriver(PartyAnchor, "visibility", "[group:raid] hide; show")
        else
            if IsInRaid and IsInRaid() then PartyAnchor:Hide() else PartyAnchor:Show() end
        end
    else
        if RegisterStateDriver then
            pcall(UnregisterStateDriver, PartyAnchor, "visibility")
        end
        if PartyAnchor.SetAttribute then PartyAnchor:SetAttribute("state-visibility", "show") end
        PartyAnchor:Show()
        for i = 1, 4 do
            local f = PartyFrames[i]
            if f then
                if UnitExists and UnitExists("party" .. i) then
                    f:Show()
                else
                    f:Hide()
                end
            end
        end
        if _G.MidnightUI_RefreshPartyFrames then _G.MidnightUI_RefreshPartyFrames() end
    end
end

_G.MidnightUI_UpdatePartyVisibility = UpdatePartyVisibility


local function UpdateTargetGlow(frame)
    if not frame then return end
    if frame.targetHighlight then frame.targetHighlight:Hide() end
    if frame.shadows and frame.shadows[1] and frame.shadows[1].tex then
        frame.shadows[1].tex:SetColorTexture(0, 0, 0, 0.28)
    end
end

local READY_CHECK_ATLAS_READY    = "UI-LFG-ReadyMark"
local READY_CHECK_ATLAS_NOTREADY = "UI-LFG-DeclineMark"
local READY_CHECK_ATLAS_WAITING  = "UI-LFG-PendingMark"

local READY_CHECK_FALLBACK_READY    = "Interface\\RaidFrame\\ReadyCheck-Ready"
local READY_CHECK_FALLBACK_NOTREADY = "Interface\\RaidFrame\\ReadyCheck-NotReady"
local READY_CHECK_FALLBACK_WAITING  = "Interface\\RaidFrame\\ReadyCheck-Waiting"

local function ApplyReadyCheckTexture(icon, atlasName, fallbackPath)
    if icon.SetAtlas then
        icon:SetAtlas(atlasName)
        local got = icon:GetTexture()
        if got and got ~= "" then return end
    end
    icon:SetTexture(fallbackPath)
end

local PARTY_RC_DECAY_SECS = 6
local _partyRcDecayUntil = 0
local _partyRcTicker = nil

local function UpdateReadyCheck(frame)
    if not frame then return end
    if not frame.readyCheckIcon then return end
    if GetTime() < _partyRcDecayUntil then return end
    local unit = frame:GetAttribute("unit")
    local status = (unit and GetReadyCheckStatus) and GetReadyCheckStatus(unit) or nil
    if status == "ready" then
        ApplyReadyCheckTexture(frame.readyCheckIcon, READY_CHECK_ATLAS_READY, READY_CHECK_FALLBACK_READY)
        frame.readyCheckIcon:Show()
    elseif status == "notready" then
        ApplyReadyCheckTexture(frame.readyCheckIcon, READY_CHECK_ATLAS_NOTREADY, READY_CHECK_FALLBACK_NOTREADY)
        frame.readyCheckIcon:Show()
    elseif status == "waiting" then
        ApplyReadyCheckTexture(frame.readyCheckIcon, READY_CHECK_ATLAS_WAITING, READY_CHECK_FALLBACK_WAITING)
        frame.readyCheckIcon:Show()
    else
        frame.readyCheckIcon:Hide()
    end
end

local function StopPartyReadyCheckTicker()
    if _partyRcTicker then
        _partyRcTicker:Cancel()
        _partyRcTicker = nil
    end
end

local function StartPartyReadyCheckTicker()
    StopPartyReadyCheckTicker()
    _partyRcTicker = C_Timer.NewTicker(0.4, function()
        _partyDirtyAll = true
        if _partyBatchFrame then _partyBatchFrame:Show() end
    end)
end

local function FinalizePartyReadyCheck()
    StopPartyReadyCheckTicker()
    _partyRcDecayUntil = GetTime() + PARTY_RC_DECAY_SECS
    for i = 1, 4 do
        local f = PartyFrames[i]
        if f and f.readyCheckIcon then
            local unit = f:GetAttribute("unit")
            local exists = unit and UnitExists(unit)
            if exists then
                local status = GetReadyCheckStatus and GetReadyCheckStatus(unit) or nil
                if status == "ready" then
                    ApplyReadyCheckTexture(f.readyCheckIcon, READY_CHECK_ATLAS_READY, READY_CHECK_FALLBACK_READY)
                else
                    ApplyReadyCheckTexture(f.readyCheckIcon, READY_CHECK_ATLAS_NOTREADY, READY_CHECK_FALLBACK_NOTREADY)
                end
                f.readyCheckIcon:Show()
            else
                f.readyCheckIcon:Hide()
            end
        end
    end
    C_Timer.After(PARTY_RC_DECAY_SECS + 0.1, function()
        if GetTime() >= _partyRcDecayUntil then
            for i = 1, 4 do
                local f = PartyFrames[i]
                if f and f.readyCheckIcon then f.readyCheckIcon:Hide() end
            end
            _partyRcDecayUntil = 0
        end
    end)
end

local function UpdatePhaseStatus(frame)
    local unit = frame:GetAttribute("unit")
    local reason = UnitPhaseReason(unit)
    if reason then
        frame.phaseIcon:Show()
    else
        frame.phaseIcon:Hide()
    end
end

local function UpdateAuras(frame)
    if not config.showAuras then
        if frame.auras then frame.auras:Hide() end
        for i = 1, #frame.auraButtons do frame.auraButtons[i]:Hide() end
        return
    end
    local unit = frame:GetAttribute("unit")
    local btnIdx = 1

    local function ApplyAuraCooldown(btn, aura)
        local show = false
        local start, duration

        if aura and aura.duration and aura.expirationTime then
            local ok = pcall(function()
                if aura.duration > 0 then
                    duration = aura.duration
                    start = aura.expirationTime - aura.duration
                    show = true
                end
            end)
            if not ok then
                show = false
            end
        end

        if show then
            btn.cooldown:SetCooldown(start, duration)
            btn.cooldown:Show()
        else
            btn.cooldown:Hide()
        end
    end

    local function GetSafeDispelKeyFromAura(aura)
        if not aura then return "none" end
        local ok, dispelName = pcall(function() return aura.dispelName end)
        if not ok or type(dispelName) ~= "string" then
            return "none"
        end
        if dispelName == "Magic" or dispelName == "Curse" or dispelName == "Disease" or dispelName == "Poison" then
            return dispelName
        end
        return "none"
    end

    for i = 1, 40 do
        local aura = C_UnitAuras.GetAuraDataByIndex(unit, i, "HARMFUL")
        if not aura then break end
        if btnIdx > 6 then break end
        
        local btn = frame.auraButtons[btnIdx] or CreateAuraButton(frame.auras)
        frame.auraButtons[btnIdx] = btn
        btn:SetPoint("LEFT", (btnIdx-1)*(config.auraSize+config.auraSpacing), 0)
        
        btn.icon:SetTexture(aura.icon)
        ApplyAuraCooldown(btn, aura)
        local dispelKey = GetSafeDispelKeyFromAura(aura)
        local color = DEBUFF_COLORS[dispelKey] or DEBUFF_COLORS["none"]
        btn.border:SetBackdropBorderColor(color.r, color.g, color.b, 1)
        btn:Show(); btnIdx = btnIdx + 1
    end
    
    for i = 1, 40 do
        local aura = C_UnitAuras.GetAuraDataByIndex(unit, i, "HELPFUL")
        if not aura then break end
        if btnIdx > 6 then break end
        local btn = frame.auraButtons[btnIdx] or CreateAuraButton(frame.auras); frame.auraButtons[btnIdx] = btn; btn:SetPoint("LEFT", (btnIdx-1)*(config.auraSize+config.auraSpacing), 0)
        btn.icon:SetTexture(aura.icon); ApplyAuraCooldown(btn, aura)
        btn.border:SetBackdropBorderColor(0, 0, 0, 1); btn:Show(); btnIdx = btnIdx + 1
    end
    for i = btnIdx, #frame.auraButtons do frame.auraButtons[i]:Hide() end
end

function UpdatePartyUnit(frame)
    local unit = frame:GetAttribute("unit")
    if not unit then return end
    local allowSecret = AllowSecretHealthPercent()
    if lastAllowSecretState == nil or lastAllowSecretState ~= allowSecret then
        lastAllowSecretState = allowSecret
    end

    ApplyPartyConfigFromSettings()

    local _, class = UnitClass(unit)

    local c = DEFAULT_CLASS_COLOR
    if class and CLASS_COLOR_OVERRIDES[class] then
        c = CLASS_COLOR_OVERRIDES[class]
    elseif _G.MidnightUI_Core and _G.MidnightUI_Core.ClassColorsExact and class and _G.MidnightUI_Core.ClassColorsExact[class] then
        c = _G.MidnightUI_Core.ClassColorsExact[class]
    elseif class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[class] then
        c = RAID_CLASS_COLORS[class]
    end

    local classR, classG, classB = c.r, c.g, c.b
    frame._muiClassColor = c
    local isSimple = (config.style == "Simple")
    local hide2DPortrait = (config.hide2DPortrait == true)
    local isRendered = not isSimple
    if isSimple then
        if frame.roleIcon then frame.roleIcon:Hide() end
        if frame.leaderIcon then frame.leaderIcon:Hide() end
        if frame._muiBackdropOrig then
            frame:SetBackdrop(frame._muiBackdropOrig)
        end
        if frame.healthContainer then frame.healthContainer:Show() end
        if frame.powerContainer then frame.powerContainer:Show() end
        if frame.powerBg then frame.powerBg:Show() end
        if frame.powerBar then frame.powerBar:Show() end
        if frame.portrait then
            if hide2DPortrait then
                frame.portrait:Hide()
            else
                frame.portrait:Show()
                UpdatePortraitModel(frame.portrait, unit)
                if _G.MidnightUI_Debug and not frame._muiPortraitDebugged then
                    frame._muiPortraitDebugged = true
                end
            end
        end
        if frame.portraitBg then
            if hide2DPortrait then frame.portraitBg:Hide() else frame.portraitBg:Show() end
        end
        if frame.portraitSep then
            if hide2DPortrait then frame.portraitSep:Hide() else frame.portraitSep:Show() end
        end
        if frame.shadows then
            for _, s in ipairs(frame.shadows) do s:Hide() end
        end
        if frame.healthBg then frame.healthBg:Show() end
        if frame.healthBar then frame.healthBar:Show() end
        if frame.absorbBar then frame.absorbBar:Hide() end
        if frame.healthBar then ClearBarOverlays(frame.healthBar) end
        if frame.powerBar then ClearBarOverlays(frame.powerBar) end
        frame:SetBackdropColor(0.06, 0.06, 0.06, 0.5)
        frame:SetBackdropBorderColor(0, 0, 0, 0.8)
        if frame.nameText then frame.nameText:SetText("") end
        if frame.healthText then frame.healthText:SetText("") end
        if frame.simpleTextFrame then frame.simpleTextFrame:Show() end
        if frame.simpleNameText then
            frame.simpleNameText:SetTextColor(1, 1, 1, 1)
            frame.simpleNameText:SetText(UnitName(unit) or "")
        end
        local isConnectedSimple = UnitIsConnected(unit)
        local hasIncomingResSimple = UnitHasIncomingResurrection(unit)
        if frame.simpleHealthText then
            if not isConnectedSimple then
                frame.simpleHealthText:SetText("OFFLINE")
                frame.simpleHealthText:SetTextColor(0.6, 0.6, 0.6, 1)
            elseif IsUnitActuallyDead(unit) and not hasIncomingResSimple then
                frame.simpleHealthText:SetText("")
                frame.simpleHealthText:SetTextColor(1, 1, 1, 1)
            elseif not AllowSecretHealthPercent() then
                frame.simpleHealthText:SetText("")
                frame.simpleHealthText:SetTextColor(1, 1, 1, 1)
            else
                local _, pctText = GetDisplayHealthPercentWithText(unit)
                if pctText then
                    frame.simpleHealthText:SetText(pctText)
                else
                    frame.simpleHealthText:SetText("")
                end
                frame.simpleHealthText:SetTextColor(1, 1, 1, 1)
            end
        end
        if frame.deadIcon then
            local isDeadSimpleIcon = IsUnitActuallyDead(unit) and not hasIncomingResSimple and isConnectedSimple
            if isDeadSimpleIcon then
                frame.deadIcon:SetSize(32, 32)
                frame.deadIcon:ClearAllPoints()
                frame.deadIcon:SetPoint("RIGHT", frame.healthContainer, "RIGHT", -28, 0)
                ApplyDeadIconVisualStyle(frame.deadIcon)
                frame.deadIcon:Show()
            else
                frame.deadIcon:Hide()
            end
        end
        if frame.healthBar then
            frame.healthBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
            local tex = frame.healthBar:GetStatusBarTexture()
            if tex then
                tex:SetHorizTile(false)
                tex:SetVertTile(false)
            end
            if not isConnectedSimple then
                frame.healthBar:SetStatusBarColor(0.1, 0.1, 0.1, 0.5)
            elseif not frame._muiDebuffBarTintActive then
                frame.healthBar:SetStatusBarColor(classR, classG, classB, 0.9)
            end
            local hp, maxHp = GetPartyUnitHealthValues(unit)
            local hasSafeHpSimple = (hp ~= nil and maxHp ~= nil and maxHp > 0)
            if not isConnectedSimple then
                frame.healthBar:SetMinMaxValues(0, 1)
                frame.healthBar:SetValue(0)
            elseif hasSafeHpSimple then
                frame.healthBar:SetMinMaxValues(0, maxHp)
                frame.healthBar:SetValue(hp)
            else
                local pctValue = GetDisplayHealthPercent(unit)
                if type(pctValue) == "number" then
                    pcall(function()
                        frame.healthBar:SetMinMaxValues(0, 100)
                        frame.healthBar:SetValue(pctValue)
                    end)
                else
                    pcall(function()
                        frame.healthBar:SetMinMaxValues(0, 1)
                        frame.healthBar:SetValue(0)
                    end)
                end
            end
        end
        local isDeadSimple = IsUnitActuallyDead(unit)
        if frame.powerBar then
            if not isDeadSimple and isConnectedSimple then
                local pp = UnitPower(unit)
                local maxPp = UnitPowerMax(unit)
                local _, pToken = UnitPowerType(unit)
                local pColor = POWER_COLORS[pToken] or DEFAULT_POWER_COLOR
                frame.powerBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
                local tex = frame.powerBar:GetStatusBarTexture()
                if tex then
                    tex:SetHorizTile(false)
                    tex:SetVertTile(false)
                end
                frame.powerBar:SetStatusBarColor(pColor[1], pColor[2], pColor[3], 1)
                frame.powerBar:SetMinMaxValues(0, maxPp)
                frame.powerBar:SetValue(pp)
            else
                frame.powerBar:SetStatusBarColor(0.2, 0.2, 0.2, 0.5)
                frame.powerBar:SetValue(0)
            end
        end
        UpdatePartyDebuffOverlay(frame, unit)
        return
    else
        if frame.simpleTextFrame then frame.simpleTextFrame:Hide() end
        if frame.simpleNameText then frame.simpleNameText:SetText("") end
        if frame.simpleHealthText then frame.simpleHealthText:SetText("") end
        if frame.roleIcon then
            if hide2DPortrait then frame.roleIcon:Hide() else frame.roleIcon:Show() end
        end
        if frame._muiBackdropOrig then
            frame:SetBackdrop(frame._muiBackdropOrig)
        end
        if frame.healthContainer then frame.healthContainer:Show() end
        if frame.powerContainer then frame.powerContainer:Show() end
        if frame.portrait then
            if hide2DPortrait then frame.portrait:Hide() else frame.portrait:Show() end
        end
        if frame.portraitBg then
            if hide2DPortrait then frame.portraitBg:Hide() else frame.portraitBg:Show() end
        end
        if frame.portraitSep then
            if hide2DPortrait then frame.portraitSep:Hide() else frame.portraitSep:Show() end
        end
        if frame.shadows then
            for _, s in ipairs(frame.shadows) do s:Show() end
        end
        if frame.healthBg then frame.healthBg:Show() end
        if frame.healthBar then frame.healthBar:Show() end
        if frame.absorbBar then frame.absorbBar:Show() end
        if frame.powerBg then frame.powerBg:Show() end
        if frame.powerBar then frame.powerBar:Show() end
        if frame.healthBar then
            frame.healthBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
            local tex = frame.healthBar:GetStatusBarTexture()
            if tex then tex:SetHorizTile(false); tex:SetVertTile(false) end
        end
        if frame.powerBar then
            frame.powerBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
            local tex = frame.powerBar:GetStatusBarTexture()
            if tex then tex:SetHorizTile(false); tex:SetVertTile(false) end
        end
        frame:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
        frame:SetBackdropBorderColor(0, 0, 0, 1)
        if frame.nameText and frame.healthContainer then
            frame.nameText:ClearAllPoints()
            frame.nameText:SetPoint("LEFT", frame.healthContainer, "LEFT", 6, 1)
            frame.nameText:SetTextColor(1, 1, 1, 1)
        end
        if frame.healthText and frame.healthContainer then
            frame.healthText:ClearAllPoints()
            frame.healthText:SetPoint("RIGHT", frame.healthContainer, "RIGHT", -6, 1)
        end
    end
    
    if frame.portrait and not hide2DPortrait then
        UpdatePortraitModel(frame.portrait, unit)
          if _G.MidnightUI_Debug and not frame._muiPortraitDebugged then
              frame._muiPortraitDebugged = true
          end
      elseif _G.MidnightUI_Debug and frame.portrait and not frame._muiPortraitDebugged then
          frame._muiPortraitDebugged = true
    end

    local isDead = IsUnitActuallyDead(unit)
    local isConnected = UnitIsConnected(unit)
    local hasIncomingRes = UnitHasIncomingResurrection(unit)
    
    local bgR, bgG, bgB, bgAlpha = classR * 0.15, classG * 0.15, classB * 0.15, 0.6
    local textR, textG, textB = 1, 1, 1
    local debugState = "HEALTHY"
    local statusText = nil
    local showResIcon = false
    local showDeadIcon = isConnected and isDead and not hasIncomingRes

    local hp, maxHp = GetPartyUnitHealthValues(unit)
    local hasSafeHp = (hp ~= nil and maxHp ~= nil and maxHp > 0)
    if not hasSafeHp and allowSecret then
        local okHp, vHp = pcall(UnitHealth, unit)
        local okMax, vMax = pcall(UnitHealthMax, unit)
        if okHp and okMax and type(vHp) == "number" and type(vMax) == "number"
            and not IsSecretValue(vHp) and not IsSecretValue(vMax)
            and vMax > 0 then
            hp, maxHp = vHp, vMax
            hasSafeHp = true
        end
    end

    if not isConnected then
        statusText = "OFFLINE"
        textR, textG, textB = 0.5, 0.5, 0.5
        bgR, bgG, bgB, bgAlpha = 0.1, 0.1, 0.1, 0.8
        frame.healthBar:SetStatusBarColor(0.2, 0.2, 0.2, 0.5)
    else
    if hasIncomingRes then
        bgR, bgG, bgB, bgAlpha = 0.3, 0.25, 0.05, 0.6
        textR, textG, textB = 1.0, 0.95, 0.5
        showResIcon = true
        debugState = "RESURRECTION PENDING"
        
    elseif isDead then
        bgR, bgG, bgB, bgAlpha = 0.1, 0.0, 0.0, 0.9
        textR, textG, textB = 1.0, 0.2, 0.2
        statusText = nil
        showDeadIcon = true
        debugState = "DEAD"
        
    end
    end

    
    local hbR, hbG, hbB, hbA = classR, classG, classB, 0.92
    local hbTopA, hbBotA = 0.20, 0.34
    if hasIncomingRes then
        hbR, hbG, hbB, hbA = classR * 1.2, classG * 1.2, classB * 1.2, 0.95
        hbTopA, hbBotA = 0.24, 0.36
    elseif isDead or not isConnected then
        hbR, hbG, hbB, hbA = classR * 0.25, classG * 0.25, classB * 0.25, 0.7
        hbTopA, hbBotA = 0.12, 0.24
    end
    if not isConnected then
        hbR, hbG, hbB, hbA = 0.1, 0.1, 0.1, 0.5
        hbTopA, hbBotA = 0.08, 0.18
    end
    if not frame._muiDebuffBarTintActive then
        if isRendered then
            ApplyRenderedBarStyle(frame.healthBar, hbR, hbG, hbB, hbA, hbTopA, hbBotA)
        else
            ApplyStatusBarGradient(frame.healthBar, hbR, hbG, hbB, hbA, hbTopA, hbBotA)
        end
    end
    
    frame.healthBg:SetColorTexture(bgR, bgG, bgB, bgAlpha)
    
    if frame.resIcon then
        if showResIcon then
            frame.resIcon:Show()
            if frame.resGlow then
                frame.resGlow:Show()
                frame.resGlow:SetAlpha(0.6)
            end
        else
            frame.resIcon:Hide()
            if frame.resGlow then frame.resGlow:Hide() end
        end
    end
    if frame.deadIcon then
        if showDeadIcon then
            frame.deadIcon:SetSize(32, 32)
            frame.deadIcon:ClearAllPoints()
            frame.deadIcon:SetPoint("RIGHT", frame.healthContainer, "RIGHT", -28, 0)
            ApplyDeadIconVisualStyle(frame.deadIcon)
            frame.deadIcon:Show()
        else
            frame.deadIcon:Hide()
        end
    end
    
    frame.nameText:SetText(UnitName(unit)) 
    frame.nameText:SetTextColor(textR, textG, textB)

    if hasSafeHp then
        pcall(function()
            frame.healthBar:SetMinMaxValues(0, maxHp)
            if hasIncomingRes then
                frame.healthBar:SetValue(hp)
            else
                frame.healthBar:SetValue(isDead and 0 or hp)
            end
        end)
    else
        local pctValue = GetDisplayHealthPercent(unit)
        if type(pctValue) == "number" then
            pcall(function()
                frame.healthBar:SetMinMaxValues(0, 100)
                frame.healthBar:SetValue(pctValue)
            end)
        else
            pcall(function()
                frame.healthBar:SetMinMaxValues(0, 1)
                frame.healthBar:SetValue(0)
            end)
        end
    end
    
    local absorbs = SafeUnitValue(UnitGetTotalAbsorbs, unit) or 0
    if hasSafeHp then
        frame.absorbBar:SetMinMaxValues(0, maxHp)
        frame.absorbBar:SetValue(absorbs)
    else
        frame.absorbBar:SetMinMaxValues(0, 1)
        frame.absorbBar:SetValue(0)
    end

    local allowSecret = AllowSecretHealthPercent()
    local textSuccess, textString
    if showResIcon then
        textString = ""
    elseif showDeadIcon then
        textString = ""
    elseif statusText then
        textString = statusText
    elseif not allowSecret then
        textString = ""
    elseif not hasSafeHp then
        textString = ""
    else
        local pct = GetDisplayHealthPercent(unit)
        local fmt = _G.MidnightUI_FormatHealthText
        local okC, rawC = pcall(UnitHealth, unit)
        local okM, rawM = pcall(UnitHealthMax, unit)
        local cur = okC and rawC or 0
        local mx  = okM and rawM or 1
        local text = fmt and fmt(cur, mx, pct) or nil
        if not text and pct then
            local ok, t = pcall(string.format, "%.0f%%", pct)
            if ok then text = t end
        end
        textString = text or ""
    end
    
    frame.healthText:SetText(textString)
    frame.healthText:SetTextColor(textR, textG, textB)

    if not isDead then
        local pp = UnitPower(unit)
        local maxPp = UnitPowerMax(unit)
        local _, pToken = UnitPowerType(unit)
        local pColor = POWER_COLORS[pToken] or DEFAULT_POWER_COLOR

        frame.powerBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
        local tex = frame.powerBar:GetStatusBarTexture()
        if tex then
            tex:SetHorizTile(false)
            tex:SetVertTile(false)
        end
        
        local skipPowerUpdate = false
        if not isConnected and isRendered then
            ApplyRenderedBarStyle(frame.powerBar, 0.2, 0.2, 0.2, 0.5, 0.12, 0.22)
            frame.powerBar:SetMinMaxValues(0, 1)
            frame.powerBar:SetValue(0)
            skipPowerUpdate = true
        end
         
        if not skipPowerUpdate and hasIncomingRes then
            if isRendered then
                ApplyRenderedBarStyle(frame.powerBar, pColor[1] * 1.15, pColor[2] * 1.15, pColor[3] * 1.15, 0.85, 0.22, 0.34)
            else
                ApplyStatusBarGradient(frame.powerBar, pColor[1] * 1.15, pColor[2] * 1.15, pColor[3] * 1.15, 0.85, 0.20, 0.32)
            end
        elseif not skipPowerUpdate then
            if isRendered then
                ApplyRenderedBarStyle(frame.powerBar, pColor[1], pColor[2], pColor[3], 1, 0.20, 0.32)
            else
                ApplyStatusBarGradient(frame.powerBar, pColor[1], pColor[2], pColor[3], 1, 0.16, 0.30)
            end
        end
        if not skipPowerUpdate then
            frame.powerBar:SetMinMaxValues(0, maxPp)
            frame.powerBar:SetValue(pp)
        end
    elseif isDead then
        if isRendered then
            ApplyRenderedBarStyle(frame.powerBar, 0.2, 0.2, 0.2, 0.5, 0.12, 0.22)
        else
            ApplyStatusBarGradient(frame.powerBar, 0.2, 0.2, 0.2, 0.5, 0.10, 0.22)
        end
        frame.powerBar:SetValue(0)
    end

    local role = UnitGroupRolesAssigned(unit)
    if hide2DPortrait then
        frame.roleIcon:Hide()
    elseif role == "TANK" then
        frame.roleIcon:SetTexture("Interface\\AddOns\\MidnightUI\\Media\\TankIcon")
        if not frame.roleIcon:GetTexture() then frame.roleIcon:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-PORTRAITROLES"); frame.roleIcon:SetTexCoord(0, 19/64, 22/64, 41/64) end
        frame.roleIcon:SetVertexColor(1, 1, 1, 1)
        frame.roleIcon:SetAlpha(1)
        frame.roleIcon:Show()
    elseif role == "HEALER" then
        frame.roleIcon:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-PORTRAITROLES"); frame.roleIcon:SetTexCoord(20/64, 39/64, 1/64, 20/64)
        frame.roleIcon:SetVertexColor(1, 1, 1, 1)
        frame.roleIcon:SetAlpha(1)
        frame.roleIcon:Show()
    elseif role == "DAMAGER" then
        frame.roleIcon:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-PORTRAITROLES"); frame.roleIcon:SetTexCoord(20/64, 39/64, 22/64, 41/64)
        frame.roleIcon:SetVertexColor(1, 1, 1, 1)
        frame.roleIcon:SetAlpha(1)
        frame.roleIcon:Show()
    else
        frame.roleIcon:Hide()
    end

    if hide2DPortrait then
        frame.leaderIcon:Hide()
    elseif UnitIsGroupLeader(unit) then
        frame.leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon")
        frame.leaderIcon:Show()
    else
        frame.leaderIcon:Hide()
    end

    UpdateAuras(frame)
    UpdateReadyCheck(frame)
    UpdateTargetGlow(frame)
    UpdatePhaseStatus(frame)
    UpdatePartyDebuffOverlay(frame, unit)
end

_partyBatchFrame:SetScript("OnUpdate", function(self)
    if _partyDirtyAll then
        _partyDirtyAll = false
        for i = 1, 4 do
            local f = PartyFrames[i]
            if f and f:IsShown() then UpdatePartyUnit(f) end
        end
    else
        for i in pairs(_partyDirtyUnits) do
            local f = PartyFrames[i]
            if f and f:IsShown() then UpdatePartyUnit(f) end
        end
    end
    wipe(_partyDirtyUnits)
    self:Hide()
end)


function SuppressBlizzardPartyFrame(frame)
    if not frame or frame.MidnightUISuppressed then return end
    frame.MidnightUISuppressed = true
    if frame.SetAlpha then frame:SetAlpha(0) end
    if frame.SetScale then frame:SetScale(0.001) end
    if frame.EnableMouse then frame:EnableMouse(false) end
end

function UnsuppressBlizzardPartyFrame(frame)
    if not frame or not frame.MidnightUISuppressed then return end
    frame.MidnightUISuppressed = nil
    if frame.SetAlpha then frame:SetAlpha(1) end
    if frame.SetScale then frame:SetScale(1) end
    if frame.EnableMouse then frame:EnableMouse(true) end
end

function ForEachBlizzardPartyFrame(fn)
    if _G.PartyFrame then
        fn(_G.PartyFrame)
        for i = 1, 5 do
            fn(_G.PartyFrame["MemberFrame"..i])
        end
    end
    for i = 1, 4 do
        fn(_G["PartyMemberFrame"..i])
    end
    if _G.CompactPartyFrame then
        fn(_G.CompactPartyFrame)
    end
end

local function HideBlizzardFrames()
    if MidnightUISettings and MidnightUISettings.PartyFrames and MidnightUISettings.PartyFrames.useDefaultFrames then
        return
    end
    if MidnightUISettings and MidnightUISettings.PartyFrames and MidnightUISettings.PartyFrames.enabled == false then
        ForEachBlizzardPartyFrame(UnsuppressBlizzardPartyFrame)
        return
    end
    ForEachBlizzardPartyFrame(SuppressBlizzardPartyFrame)
end

function RestoreBlizzardPartyFrames()
    ForEachBlizzardPartyFrame(UnsuppressBlizzardPartyFrame)
end

function MidnightUI_RefreshPartyFrames()
    for i = 1, 4 do
        local f = PartyFrames[i]
        if f and f:IsShown() then
            UpdatePartyUnit(f)
        end
    end
end

_G.MidnightUI_RefreshPartyDispelTrackingOverlay = function(forceHide)
    if RefreshPartyDispelTrackingOverlays then
        RefreshPartyDispelTrackingOverlays(forceHide == true)
    end
end

_G.MidnightUI_SetPartyDispelTrackingLocked = function(locked)
    SetPartyDispelTrackingLocked(locked)
end

_G.MidnightUI_GetPartyBlizzAuraCache = function(unit)
    return _partyBlizzAuraCache[unit]
end

InitPartyFrames = function()
    if MidnightUISettings and MidnightUISettings.PartyFrames and MidnightUISettings.PartyFrames.useDefaultFrames then
        for i = 1, 4 do
            if PartyFrames[i] then PartyFrames[i]:Hide() end
        end
        if PartyAnchor then PartyAnchor:Hide() end
        return
    end
    EnsurePartyCombatSettingsTable()
    if MidnightUISettings and MidnightUISettings.Messenger and MidnightUISettings.Messenger.locked ~= nil then
        PartyDispelTrackingState.locked = (MidnightUISettings.Messenger.locked ~= false)
    end
    EnsurePartyBlizzAuraHook()
    HideBlizzardFrames()
    if PartyFrames[1] then return end
    if not IsInGroup() then return end
    EnsurePartyAnchor()
    for i = 1, 4 do
        PartyFrames[i] = CreateSinglePartyFrame(i)
    end
    LayoutPartyFrames()
    UpdatePartyVisibility()
    EnsurePartyRangeTicker()
    for i = 1, 4 do
        local f = PartyFrames[i]
        if f and f.dispelGlow then
            f.dispelGlow:Hide()
        end
    end
    if RefreshPartyDispelTrackingOverlays then
        RefreshPartyDispelTrackingOverlays(false)
    end
end


local function PrimePartySpellTypeCache()
    if not C_UnitAuras or not C_UnitAuras.GetAuraDataByIndex then return end
    local added = 0
    local scanned = 0
    for i = 1, 4 do
        local unit = "party" .. i
        if UnitExists(unit) then
            for idx = 1, 40 do
                local ok, aura = pcall(C_UnitAuras.GetAuraDataByIndex, unit, idx, "HARMFUL")
                if not ok or not aura then break end
                scanned = scanned + 1
                if aura.spellId then
                    local dt = aura.dispelType or aura.dispelName or aura.debuffType
                    if dt and type(dt) == "string" then
                        local okWrite = pcall(function() _partySpellTypeCache[aura.spellId] = dt end)
                        if okWrite then added = added + 1 end
                    end
                end
            end
        end
    end
    if UnitExists("player") then
        for idx = 1, 40 do
            local ok, aura = pcall(C_UnitAuras.GetAuraDataByIndex, "player", idx, "HARMFUL")
            if not ok or not aura then break end
            scanned = scanned + 1
            if aura.spellId then
                local dt = aura.dispelType or aura.dispelName or aura.debuffType
                if dt and type(dt) == "string" then
                    local okWrite = pcall(function() _partySpellTypeCache[aura.spellId] = dt end)
                    if okWrite then added = added + 1 end
                end
            end
        end
    end
end


local _partyDebuffCombatTicker = nil

local function StartPartyDebuffCombatRefresh()
    if _partyDebuffCombatTicker then return end
    if not C_Timer or not C_Timer.NewTicker then return end
    _partyDebuffCombatTicker = C_Timer.NewTicker(0.15, function()
        for i = 1, 4 do
            local frame = PartyFrames[i]
            if frame and frame:IsShown() then
                local unit = frame:GetAttribute("unit") or ("party" .. i)
                local primaryEnum, secondaryEnum, diag = CollectPartyHazardsForUnit(unit)
                local overlay = frame._muiDebuffOverlay
                if overlay then
                    local primaryColorOverride = diag and diag.customPrimaryColor or nil
                    local secondaryColorOverride = diag and diag.customSecondaryColor or nil
                    local stickyApplied
                    primaryEnum, secondaryEnum, primaryColorOverride, stickyApplied = ApplyPartyStickyHazardFallback(frame, primaryEnum, secondaryEnum, primaryColorOverride, diag)
                    local hasPrimary = primaryEnum ~= nil
                    local hasSecondary = (secondaryEnum ~= nil and secondaryEnum ~= primaryEnum)
                        or (type(secondaryColorOverride) == "table")
                    local primaryRenderColor = primaryColorOverride
                    if hasPrimary and IsPartySecretColor and IsPartySecretColor(primaryColorOverride) then
                        local overlayShown = (overlay.IsShown and overlay:IsShown()) and true or false
                        if not overlayShown then
                            if primaryEnum and primaryEnum ~= PARTY_HAZARD_ENUM.Unknown and PARTY_HAZARD_COLORS[primaryEnum] then
                                local canonical = PARTY_HAZARD_COLORS[primaryEnum]
                                primaryRenderColor = { canonical[1], canonical[2], canonical[3] }
                            end
                        end
                    end

                    local curveColor = diag and diag.curveColorObj or nil

                    if hasPrimary then
                        if curveColor and primaryEnum == PARTY_HAZARD_ENUM.Unknown and not primaryRenderColor then
                            if overlay.tintTex then
                                local cr, cg, cb, ca = curveColor:GetRGBA()
                                overlay.tintTex:SetVertexColor(cr, cg, cb, 1)
                            end
                        else
                            SetPartyDebuffPrimaryColor(overlay, frame, primaryEnum, primaryRenderColor)
                        end
                        SetPartyDebuffOverlayActive(overlay, true)
                    else
                        SetPartyDebuffOverlayActive(overlay, false)
                    end
                    if hasPrimary and hasSecondary then
                        SetPartyDebuffSecondaryColor(overlay, secondaryEnum, secondaryColorOverride)
                        SetPartyDebuffSweepVisible(overlay, true)
                    else
                        SetPartyDebuffSweepVisible(overlay, false)
                    end
                end
            end
        end
    end)
end

local function StopPartyDebuffCombatRefresh()
    if _partyDebuffCombatTicker then
        _partyDebuffCombatTicker:Cancel()
        _partyDebuffCombatTicker = nil
    end
    for i = 1, 4 do
        local frame = PartyFrames[i]
        if frame and frame:IsShown() then
            local unit = frame:GetAttribute("unit") or ("party" .. i)
            UpdatePartyDebuffOverlay(frame, unit)
        end
    end
end

-- ============================================================
-- SECTION: Event Registration + Event Handler
-- Registers all unit and global events.  OnEvent routes to
-- the appropriate update functions; PLAYER_REGEN_ENABLED flushes
-- pending layout changes; GROUP_ROSTER_UPDATE re-creates/hides
-- frames; CHAT_MSG_ADDON handles cross-frame bleed comm.
--
-- FUNCTIONS (event handler):
--   ADDON_LOADED — suppress Blizzard party frames
--   PLAYER_ENTERING_WORLD — init frames, apply settings
--   GROUP_ROSTER_UPDATE — create/show/hide per-member frames
--   PLAYER_REGEN_DISABLED/ENABLED — combat ticker start/stop
--   UNIT_HEALTH / UNIT_MAXHEALTH — UpdatePartyUnit (health)
--   UNIT_POWER_UPDATE / UNIT_AURA — UpdatePartyUnit
--   UNIT_MODEL_CHANGED — portrait model refresh
--   UNIT_ABSORB_AMOUNT_CHANGED — absorb bar update
--   READY_CHECK / READY_CHECK_CONFIRM / READY_CHECK_FINISHED —
--     ready check icon show/hide
--   CHAT_MSG_ADDON — bleed type communication from other frames
-- ============================================================
PartyFrameManager:RegisterEvent("ADDON_LOADED")
PartyFrameManager:RegisterEvent("PLAYER_ENTERING_WORLD")
PartyFrameManager:RegisterEvent("GROUP_ROSTER_UPDATE")
PartyFrameManager:RegisterEvent("PLAYER_REGEN_ENABLED")
PartyFrameManager:RegisterEvent("PLAYER_REGEN_DISABLED")
PartyFrameManager:RegisterEvent("UNIT_HEALTH")
PartyFrameManager:RegisterEvent("UNIT_POWER_UPDATE")
PartyFrameManager:RegisterEvent("UNIT_MAXHEALTH")
PartyFrameManager:RegisterEvent("UNIT_MODEL_CHANGED")
PartyFrameManager:RegisterEvent("UNIT_AURA")
PartyFrameManager:RegisterEvent("PLAYER_TARGET_CHANGED")
PartyFrameManager:RegisterEvent("READY_CHECK")
PartyFrameManager:RegisterEvent("READY_CHECK_CONFIRM")
PartyFrameManager:RegisterEvent("READY_CHECK_FINISHED")
PartyFrameManager:RegisterEvent("UNIT_PHASE")
PartyFrameManager:RegisterEvent("UNIT_ABSORB_AMOUNT_CHANGED")

PartyFrameManager:SetScript("OnEvent", function(self, event, ...)
    local arg1, arg2 = ...
    if event == "ADDON_LOADED" and arg1 == ADDON_NAME then
        InitPartyFrames()
    elseif event == "PLAYER_ENTERING_WORLD" then
        InitPartyFrames()
        ResetPartyBlizzAuraCache()
        PrimePartyBlizzAuraCache()
        UpdatePartyVisibility()
        for i = 1, 4 do if PartyFrames[i] then UpdatePartyUnit(PartyFrames[i]) end end
    elseif event == "GROUP_ROSTER_UPDATE" then
        InitPartyFrames()
        ResetPartyBlizzAuraCache()
        PrimePartyBlizzAuraCache()
        UpdatePartyVisibility()
        for i = 1, 4 do if PartyFrames[i] then UpdatePartyUnit(PartyFrames[i]) end end
    elseif event == "PLAYER_REGEN_ENABLED" then
        StopPartyDebuffCombatRefresh()
        if pendingPartyLayout then
            LayoutPartyFrames()
        end
        if pendingPartyVisibility then
            UpdatePartyVisibility()
        end
    elseif event == "PLAYER_REGEN_DISABLED" then
        PrimePartySpellTypeCache()
        StartPartyDebuffCombatRefresh()
    elseif event == "PLAYER_TARGET_CHANGED" or event == "READY_CHECK" or event == "READY_CHECK_FINISHED" then
        if event == "READY_CHECK" then
            _partyRcDecayUntil = 0
            StartPartyReadyCheckTicker()
        elseif event == "READY_CHECK_FINISHED" then
            FinalizePartyReadyCheck()
        end
        _partyDirtyAll = true
        _partyBatchFrame:Show()
    elseif event == "UNIT_HEALTH" or event == "UNIT_POWER_UPDATE" or event == "UNIT_MAXHEALTH" or event == "UNIT_MODEL_CHANGED" or event == "UNIT_AURA" or event == "READY_CHECK_CONFIRM" or event == "UNIT_PHASE" or event == "UNIT_ABSORB_AMOUNT_CHANGED" then
        if event == "UNIT_AURA" and arg2 and type(arg1) == "string" then
            ProcessPartyAuraEvent(arg1, arg2)
        end

        local argIsSafeUnit = type(arg1) == "string"
        if argIsSafeUnit and type(issecretvalue) == "function" then
            local okSecret, isSecret = pcall(issecretvalue, arg1)
            if okSecret and isSecret == true then
                argIsSafeUnit = false
            end
        end

        for i = 1, 4 do
            local f = PartyFrames[i]
            if f and f:IsShown() and argIsSafeUnit then
                local unit = f:GetAttribute("unit")
                if unit and arg1 == unit then
                    _partyDirtyUnits[i] = true
                    _partyBatchFrame:Show()
                end
            end
        end
    end
end)
ApplyPartyConfigFromSettings = function()
    if not MidnightUISettings or not MidnightUISettings.PartyFrames then return end
    EnsurePartyCombatSettingsTable()
    local s = MidnightUISettings.PartyFrames
    if s.width then config.width = s.width end
    if s.height then config.height = s.height end
    if s.layout then config.layout = s.layout end
    if s.spacingX then config.spacingX = s.spacingX end
    if s.spacingY then config.spacingY = s.spacingY end
    if s.style then
        if s.style == "Circular" or s.style == "Square" then
            config.style = "Rendered"
            s.style = "Rendered"
        else
            config.style = s.style
        end
    end
    config.hide2DPortrait = (s.hide2DPortrait == true)
end
