  local addon, ns = ...
  local oUF = ns.oUF or oUF

  local cfg
  local db = ns.db
  local func = ns.func
  local unit = ns.unit

  local floor, sin, pi = floor, math.sin, math.pi
  local tinsert = tinsert
  local format = string.format



  -- strip taint (works for non-secret numbers like max health)
  local function clean(v)
    if v == nil then return 0 end
    local ok, r = pcall(format, "%.10g", v)
    if ok then return tonumber(r) or 0 end
    return 0
  end

  ---------------------------------------------
  -- FUNCTIONS
  ---------------------------------------------

  -- ---------------------------------------------------------------
  -- applyIconDragFunctionality
  -- Shift+LClick = Move, Alt+LClick = Scale, Tooltip.
  -- Sperre nur bei rOrbsDB.orbDragLocked (wie Orb-Drag in lib.lua).
  -- Grün/rot Overlay wie Orb-Dragframe (framesLocked = nur mit Shift/Alt sichtbar).
  -- Hinweis: ns.cfg.framesLocked ist oft true (Standard in config.lua) und blockiert
  -- bei den Orbs das Ziehen NICHT — daher hier nie für Drag/Tooltip verwenden.
  -- ---------------------------------------------------------------
  local function applyIconDragFunctionality(f, iconType, sizeKey, offXKey, offYKey, updateLayoutFn)
    f:EnableMouse(true)
    f:SetMovable(true)
    f:SetClampedToScreen(true)

    -- Modifier-Tint wie df.texture in core/lib.lua (updateModifierOverlay)
    local overlayTex = f:CreateTexture(nil, "OVERLAY", nil, 6)
    overlayTex:SetAllPoints(f)
    overlayTex:SetColorTexture(1, 1, 1)
    overlayTex:SetAlpha(0.2)
    overlayTex:Hide()
    f._iconModifierOverlay = overlayTex

    local _isDragging   = false
    local _isScaling    = false
    local _dragStartX   = 0
    local _dragStartY   = 0
    local _dragStartOX  = 0
    local _dragStartOY  = 0
    local _scaleStartY  = 0
    local _scaleStartSz = 0

    local function IsIconDragLocked()
      return rOrbsDB and rOrbsDB.orbDragLocked == true
    end

    local function updateIconModifierOverlay()
      if not overlayTex then return end
      local cfg = ns.cfg
      if not cfg or not cfg.framesUserplaced then
        overlayTex:Hide()
        return
      end
      if IsIconDragLocked() then
        overlayTex:Hide()
        return
      end
      local locked = cfg.framesLocked == true
      local shift, al = IsShiftKeyDown(), IsAltKeyDown()
      if locked then
        if al then
          overlayTex:SetVertexColor(0.95, 0.2, 0.2)
          overlayTex:SetAlpha(0.36)
          overlayTex:Show()
        elseif shift then
          overlayTex:SetVertexColor(0, 1, 0)
          overlayTex:SetAlpha(0.24)
          overlayTex:Show()
        else
          overlayTex:Hide()
        end
      else
        overlayTex:Show()
        if al then
          overlayTex:SetVertexColor(0.95, 0.2, 0.2)
          overlayTex:SetAlpha(0.3)
        else
          overlayTex:SetVertexColor(0, 1, 0)
          overlayTex:SetAlpha(0.2)
        end
      end
    end

    f._iconHover = false

    f:SetScript("OnEnter", function(s)
      if IsIconDragLocked() then return end

      s._iconHover = true
      GameTooltip:SetOwner(s, "ANCHOR_TOP")
      if iconType == "resting" then
        GameTooltip:AddLine("Resting Icon", 0, 1, 0.5, 1, 1, 1)
      elseif iconType == "leader" then
        GameTooltip:AddLine("Leader Icon", 0, 1, 0.5, 1, 1, 1)
      else
        GameTooltip:AddLine("Combat Icon", 0, 1, 0.5, 1, 1, 1)
      end
      GameTooltip:AddLine(" ")
      GameTooltip:AddLine("|cffffff00SHIFT + Click & Drag|r  =  Move", 1, 1, 1)
      GameTooltip:AddLine("|cffffff00ALT   + Click & Drag|r  =  Scale", 1, 1, 1)
      GameTooltip:Show()
    end)

    f:SetScript("OnLeave", function(s)
      s._iconHover = false
      GameTooltip:Hide()
    end)

    -- Ein OnUpdate: Ziehen/Skalieren ersetzt das Script nicht mehr (sonst ging das Overlay kaputt).
    f:SetScript("OnUpdate", function(s)
      if _isDragging then
        local cx, cy = GetCursorPosition()
        local sc = UIParent:GetEffectiveScale()
        local dx = cx / sc - _dragStartX
        local dy = cy / sc - _dragStartY
        rOrbsDB[offXKey] = math.floor(_dragStartOX + dx + 0.5)
        rOrbsDB[offYKey] = math.floor(_dragStartOY + dy + 0.5)
        if updateLayoutFn then updateLayoutFn() end
        return
      end
      if _isScaling then
        local _, cy = GetCursorPosition()
        cy = cy / UIParent:GetEffectiveScale()
        local dy = _scaleStartY - cy
        local newSize = math.max(8, math.min(128, math.floor(_scaleStartSz + dy * 0.15 + 0.5)))
        rOrbsDB[sizeKey] = newSize
        if updateLayoutFn then updateLayoutFn() end
        return
      end
      updateIconModifierOverlay()
    end)

    f:SetScript("OnMouseDown", function(s, button)
      if button ~= "LeftButton" then return end
      if IsIconDragLocked() then return end
      if not ns.cfg or not ns.cfg.framesUserplaced then return end

      if IsShiftKeyDown() then
        _isDragging = true
        local sx, sy = GetCursorPosition()
        local scale  = UIParent:GetEffectiveScale()
        _dragStartX  = sx / scale
        _dragStartY  = sy / scale
        _dragStartOX = rOrbsDB[offXKey] or 0
        _dragStartOY = rOrbsDB[offYKey] or 0
      elseif IsAltKeyDown() then
        _isScaling = true
        local _, sy = GetCursorPosition()
        _scaleStartY  = sy / UIParent:GetEffectiveScale()
        _scaleStartSz = rOrbsDB[sizeKey] or 28
      end
    end)

    f:SetScript("OnMouseUp", function(s, button)
      if _isDragging or _isScaling then
        local wasDragging = _isDragging
        _isDragging = false
        _isScaling  = false
        if wasDragging and rOrbsDB and rOrbsDB.snapToGrid then
          local g = 10
          rOrbsDB[offXKey] = math.floor((rOrbsDB[offXKey] or 0) / g + 0.5) * g
          rOrbsDB[offYKey] = math.floor((rOrbsDB[offYKey] or 0) / g + 0.5) * g
          if updateLayoutFn then updateLayoutFn() end
        end
        if ns.GUI and ns.GUI.RefreshValues then
          ns.GUI:RefreshValues()
        end
      end
    end)
  end

  local initUnitParameters = function(self)
    self:SetFrameStrata("LOW")
    self:SetFrameLevel(1)
    self:SetSize(self.cfg.size, self.cfg.size)
    self:SetScale(self.cfg.scale)
    self:SetPoint(self.cfg.pos.a1, self.cfg.pos.af, self.cfg.pos.a2, self.cfg.pos.x, self.cfg.pos.y)
    -- Mouse interaction and drag are handled by individual orb frames (health/power orb).
    -- The parent frame needs NO mouse interaction to avoid an invisible clickable frame in the center.
    self:EnableMouse(false)
    -- Note: DO NOT call applyDragFunctionality here. Each orb handles its own dragging.
  end

  -- PostUpdate: update text values, low HP glow, skull
  -- SECRET-SAFE: All text rendering uses C-side functions
  -- (SetFormattedText, AbbreviateLargeNumbers). Threshold
  -- checks use StatusBar fill height ratio (pixel-based, clean).
  local function postUpdate(bar, unitId, cur, max)
    local orb = bar._orb
    local self = orb:GetParent()
    local unit = self.unit or "player"

    -- SECRET-SAFE: cur/max from oUF PostUpdate are secret values in WoW 12.0+.
    -- NEVER do Lua arithmetic (+/-/*//), comparisons (</>), or string.format on them.
    -- ONLY pass them to C-side API: bar:SetValue(secret), bar:SetMinMaxValues(0, secret).
    --
    -- For hide/show logic we ONLY use boolean unit state functions (never value comparisons):
    --   UnitExists, UnitIsDeadOrGhost, UnitIsConnected — these return plain booleans, safe.
    -- For lowHP glow threshold we use UnitHealthPercent via SetFormattedText (C-side only).

    local isDead = UnitIsDeadOrGhost(unitId)
    local exists = UnitExists(unitId)

    -- Orb fill color: resolved here so it works live without reload.
    -- colorClass/colorHealth are always false; we drive color manually.
    -- For HEALTH: class color if colorAuto enabled, else config color.
    -- For POWER:  oUF colorPower handles it via SetStatusBarColor hook.
    if orb.type == "HEALTH" then
      local orbcfg = db.char.HEALTH
      local r, g, b
      if orbcfg.filling.colorAuto and not isDead then
        local _, class = UnitClass(unitId)
        local cc = class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[class]
        r = cc and cc.r or orbcfg.filling.color.r
        g = cc and cc.g or orbcfg.filling.color.g
        b = cc and cc.b or orbcfg.filling.color.b
      else
        r, g, b = orbcfg.filling.color.r, orbcfg.filling.color.g, orbcfg.filling.color.b
      end
      bar:SetStatusBarColor(r, g, b)  -- hook propagates to galaxies/bubbles/fillTex
    end

    -- low hp glow + skull (health only)
    if orb.type == "HEALTH" then
      if isDead then
        orb.lowHP:Show()
        orb.skull:Show()
      else
        -- LowHP threshold: use UnitHealthPercent via C-side SetFormattedText on a hidden
        -- scratch FontString, then read the resulting plain text string for comparison.
        -- This is the only secret-safe way to threshold-check percentage in Lua.
        -- We delegate the visual to ns.ShowLowHPGlow which handles this internally.
        ns.UpdateLowHPGlow(orb, unitId)
        orb.skull:Hide()
      end
    end

    -- hide/show values:
    -- hideOnEmpty: only hide when unit is dead/ghost or does not exist.
    -- hideOnFull: we cannot compare secret values, so hideOnFull is skipped (safe default: show).
    -- This matches MSUF's approach: never compare secret numeric values in Lua.
    local valcfg = db.char[orb.type].value
    -- Respect user's manual text show/hide preference
    local userTextHide = rOrbsDB and rOrbsDB["textShow_"..orb.type] == false
    -- Hover mode: only show text when mouse is over the orb
    local hoverMode = rOrbsDB and rOrbsDB["textHover_"..orb.type] == true
    if userTextHide then
      orb.values:Hide()
    elseif hoverMode and not orb.isHovered then
      orb.values:Hide()
    elseif valcfg.hideOnEmpty and (isDead or not exists) then
      orb.values:Hide()
    elseif not hoverMode and not userTextHide and not orb.values:IsShown() then
      orb.values:Show()
    end

    -- Update text via C-side functions only (SetFormattedText / AbbreviateLargeNumbers).
    local textMode  = rOrbsDB and rOrbsDB["textMode_"..orb.type] or "BOTH"
    local noSign    = rOrbsDB and rOrbsDB["hidePercentSign_"..orb.type] == true
    -- Center single-line text; restore two-line offset for BOTH mode
    local singleLine = (textMode == "PERCENT" or textMode == "VALUE")
    if singleLine then
      orb.values.top:ClearAllPoints()
      orb.values.top:SetPoint("CENTER", 0, 0)
    else
      orb.values.top:ClearAllPoints()
      orb.values.top:SetPoint("CENTER", 0, 10)
    end
    if orb.type == "HEALTH" then
      if textMode == "PERCENT" then
        ns.RenderPercent(orb.values.top, unit, true, noSign)
        orb.values.bottom:SetText("")
      elseif textMode == "VALUE" then
        ns.RenderAbbreviated(orb.values.top, unit, true)
        orb.values.bottom:SetText("")
      else -- BOTH
        ns.RenderPercent(orb.values.top, unit, true, noSign)
        ns.RenderAbbreviated(orb.values.bottom, unit, true)
      end
    elseif orb.type == "POWER" then
      if textMode == "PERCENT" then
        ns.RenderPowerTop(orb.values.top, unit, noSign)
        orb.values.bottom:SetText("")
      elseif textMode == "VALUE" then
        ns.RenderPowerBottom(orb.values.top, unit)
        orb.values.bottom:SetText("")
      else -- BOTH
        ns.RenderPowerTop(orb.values.top, unit, noSign)
        ns.RenderPowerBottom(orb.values.bottom, unit)
      end
    end

    if isDead then
      bar:SetValue(0)
    end
  end

  -- Update color of spark/galaxies/bubbles/fill
  local function updateStatusBarColor(bar, r, g, b)
    local orb = bar._orb
    if not orb then return end
    if orb.fillTex then orb.fillTex:SetVertexColor(r, g, b) end
    if orb.galaxies then
      for _, galaxy in pairs(orb.galaxies) do galaxy:SetVertexColor(r, g, b) end
    end
    if orb.bubbles then
      for _, bubble in pairs(orb.bubbles) do bubble:SetVertexColor(r, g, b) end
    end
  end

  -- Create rotating galaxy/bubble texture
  local function createGalaxy(parent, x, y, size, duration, texture, sublevel, degree)
    local t = parent:CreateTexture(nil, "ARTWORK", nil, sublevel)
    t:SetSize(size, size)
    t:SetPoint("CENTER", x, y)
    t:SetTexture("Interface\\AddOns\\rOrbs\\media\\"..texture)
    t:SetBlendMode("ADD")
    t.ag = t:CreateAnimationGroup()
    t.ag.anim = t.ag:CreateAnimation("Rotation")
    t.ag.anim:SetDegrees(degree)
    t.ag.anim:SetDuration(duration)
    t.ag:Play()
    t.ag:SetLooping("REPEAT")
    return t
  end

  -- Spark am Orb-Rand nur bei mittlerem Füllstand (wie Ziel-Orb); ohne Spark no-op.
  local function updateOrb(bar, value)
    local orb = bar._orb
    if not orb or not orb.spark then return end
    local u = orb.unit or "player"
    if not UnitExists(u) or UnitIsDeadOrGhost(u) then
      if orb.spark:IsShown() then orb.spark:Hide() end
      return
    end
    local ok, hide = pcall(function()
      local _, max = bar:GetMinMaxValues()
      if max <= 0 then return true end
      local ratio = value / max
      return ratio >= 0.97 or ratio <= 0.04
    end)
    if not ok or hide then
      if orb.spark:IsShown() then orb.spark:Hide() end
    else
      if not orb.spark:IsShown() then orb.spark:Show() end
    end
  end

  ---------------------------------------------
  -- CREATE ORB
  ---------------------------------------------

  local function createOrb(self, orbType)
    local orbcfg = db.char[orbType]
    local name = (orbType == "HEALTH") and "rOrbsHealthOrb" or "rOrbsPowerOrb"
    local size = self.cfg.size

    -- BASE FRAME
    local orb = CreateFrame("Button", name, self, "SecureUnitButtonTemplate")
    orb.self = self
    orb.type = orbType
    orb.size = size
    orb.unit = "player"  -- Set unit property for tooltips
    orb:SetSize(size, size)
    orb.isHovered = false  -- track hover state for text-on-hover feature

    if orbType == "POWER" then
      orb:SetPoint(self.cfg.pos.a1, self.cfg.pos.af, self.cfg.pos.a2, self.cfg.pos.x * (-1), self.cfg.pos.y)
      orb:RegisterForClicks("AnyDown")
      -- Power orb hover: show/hide text values
      orb:SetScript("OnEnter", function(self)
        self.isHovered = true
        if self.values and rOrbsDB and rOrbsDB["textHover_"..self.type] then
          self.values:Show()
        end
      end)
      orb:SetScript("OnLeave", function(self)
        self.isHovered = false
        if self.values and rOrbsDB and rOrbsDB["textHover_"..self.type] then
          self.values:Hide()
        end
        GameTooltip:Hide()
      end)
      func.applyDragFunctionality(orb, "orb")
    else
      orb:SetPoint(self.cfg.pos.a1, self.cfg.pos.af, self.cfg.pos.a2, self.cfg.pos.x, self.cfg.pos.y)
      orb:RegisterForClicks("AnyDown")
      -- Set unit attribute so right-click works like the Blizzard unit frame
      orb:SetAttribute("unit", "player")
      orb:SetAttribute("*type1", "target")
      orb:SetAttribute("*type2", "togglemenu")
      -- Health orb hover: show tooltip + text values
      orb:SetScript("OnEnter", function(self)
        self.isHovered = true
        if self.values and rOrbsDB and rOrbsDB["textHover_"..self.type] then
          self.values:Show()
        end
        UnitFrame_OnEnter(self)
      end)
      orb:SetScript("OnLeave", function(self)
        self.isHovered = false
        if self.values and rOrbsDB and rOrbsDB["textHover_"..self.type] then
          self.values:Hide()
        end
        UnitFrame_OnLeave(self)
      end)
      func.applyDragFunctionality(orb, "orb")
    end

    -- DEBUFF GLOW (health only)
    if orbType == "HEALTH" then
      local glow = orb:CreateTexture("$parentGlow", "BACKGROUND", nil, -7)
      glow:SetPoint("CENTER")
      glow:SetSize(size + 5, size + 5)
      glow:SetBlendMode("BLEND")
      glow:SetVertexColor(0, 1, 1, 0)
      glow:SetTexture("Interface\\AddOns\\rOrbs\\media\\orb_debuff_glow")
      orb.glow = glow
      self.DebuffHighlight = glow
      self.DebuffHighlightAlpha = 1
      self.DebuffHighlightFilter = false
    end

    -------------------------------------------------------
    -- LAYER 1: BACKGROUND
    -------------------------------------------------------
    local bg = orb:CreateTexture("$parentBG", "BACKGROUND", nil, -6)
    bg:SetAllPoints()
    bg:SetTexture("Interface\\AddOns\\rOrbs\\media\\orb_back2")
    orb.bg = bg

    -------------------------------------------------------
    -- LAYER 2: VERTICAL STATUSBAR (the oUF element + visible fill)
    -------------------------------------------------------
    local fill = CreateFrame("StatusBar", "$parentFill", orb)
    fill:SetAllPoints()
    fill:SetFrameLevel(orb:GetFrameLevel() + 1)
    fill:SetOrientation("VERTICAL")
    fill:SetStatusBarTexture(orbcfg.filling.texture)
    fill:SetStatusBarColor(orbcfg.filling.color.r, orbcfg.filling.color.g, orbcfg.filling.color.b)
    fill:SetMinMaxValues(0, 1)
    fill:SetValue(0)
    fill:EnableMouse(false)
    fill._orb = orb
    orb.fill = fill

    local fillTex = fill:GetStatusBarTexture()
    fillTex:SetDrawLayer("BACKGROUND", 0)
    orb.fillTex = fillTex
    
    -- updateOrb Hook aktiviert - Spark wird dynamisch ein/ausgeblendet
    fill:SetScript("OnValueChanged", updateOrb)

    -------------------------------------------------------
    -- LAYER 3: CLIP FRAME (clips animations to fill level)
    -------------------------------------------------------
    local clipFrame = CreateFrame("Frame", "$parentClip", orb)
    clipFrame:SetFrameLevel(orb:GetFrameLevel() + 2)
    clipFrame:SetPoint("BOTTOMLEFT", fill, "BOTTOMLEFT")
    clipFrame:SetPoint("BOTTOMRIGHT", fill, "BOTTOMRIGHT")
    clipFrame:SetPoint("TOP", fillTex, "TOP", 0, 0)
    clipFrame:SetClipsChildren(true)
    clipFrame:EnableMouse(false)
    orb.clipFrame = clipFrame

    -- Holder inside clipFrame (full orb size, anchored at bottom)
    local holder = CreateFrame("Frame", nil, clipFrame)
    holder:SetSize(size, size)
    holder:SetPoint("BOTTOM", orb, "BOTTOM")
    holder:EnableMouse(false)
    orb.modelHolder = holder

    -- 3D MODEL im holder (wie rOrbs_alteversion): clipFrame (+2) liegt über fill (+1) → Modell/Fx über der Füllung.
    -- Hinweis: PlayerModel wird von clip oft nicht geometrisch beschnitten, Z-Order ist trotzdem korrekt.
    local model = CreateFrame("PlayerModel", nil, holder)
    model:SetFrameLevel(holder:GetFrameLevel() + 1)
    ns.ApplyOrbModelAnchors(model, holder)
    model:EnableMouse(false)
    model:SetAlpha(orbcfg.model.alpha or 1)
    model.orbRef = orb  -- Speichere Referenz zum Orb für Scale-Berechnung
    model.baseConfig = orbcfg.model  -- Speichere Basis-Config
    function model:Update()
      local c = self.baseConfig or db.char[self.type].model
      local orbScale = self.orbRef and self.orbRef:GetScale() or 1
      
      -- WICHTIG: Passe portraitZoom an, nicht camDistanceScale
      -- Bei größerer Scale (z.B. 2.0) muss das Model KLEINER gezoomt werden
      -- Bei kleinerer Scale (z.B. 0.5) muss das Model GRÖßER gezoomt werden
      local adjustedZoom = c.portraitZoom / orbScale
      
      self:SetCamDistanceScale(c.camDistanceScale)
      self:SetPosition(0, c.pos_x, c.pos_y)
      self:SetRotation(c.rotation)
      self:SetPortraitZoom(adjustedZoom)
      self:ClearModel()
      self:SetDisplayInfo(c.displayInfo)
      if not self._tmpl and self.orbRef then
        ns.UpdateOrbModelWithFillRatio(self.orbRef, ns.GetOrbFillRatioForViewport(self.orbRef))
      end
    end
    model.type = orbType
    model:SetScript("OnEvent", function(s)
      -- Skip if template engine is managing this model
      -- NOTE: engine sets model._tmpl (not _templateConfig) → use _tmpl as the guard
      if s._tmpl then return end
      s:Update()
    end)
    model:RegisterEvent("PLAYER_ENTERING_WORLD")
    model:SetScript("OnShow", function(s)
      -- Skip if template engine is managing this model
      -- OnShow fires when UIParent is shown/hidden (e.g. DialogueUI camera fade)
      -- and would overwrite template zoom/scale with wrong values
      -- NOTE: engine sets model._tmpl (not _templateConfig) → use _tmpl as the guard
      if s._tmpl then return end
      -- Delay by one frame so layout/scale is settled before updating
      C_Timer.After(0, function() if s and s:IsVisible() then s:Update() end end)
    end)
    model:Update()
    orb.model = model
    ns.UpdateOrbModelWithFillRatio(orb, ns.GetOrbFillRatioForViewport(orb))

    -- Model-Viewport unter clipFrame (Z-Order), Galaxien/Blasen zeichnen darüber.
    -- Hook fill OnValueChanged to hide model when orb is empty.
    -- TAINT-FREI: GetMinMaxValues() ist in WoW 12.x tainted, auch pcall hilft nicht.
    -- Loesung: Unit-boolean statt Wert-Vergleich. Model zeigen wenn Unit lebt.
    fill:HookScript("OnValueChanged", function(bar, value)
      if not orb.model then return end
      if orb.model._tmpl then return end -- handled by OrbTemplateEngine (_tmpl is the correct field)
      local cfg = ns.db and ns.db.char and ns.db.char[orbType]
      if cfg and cfg.model and cfg.model.enable == false then return end
      local unit = orb.unit or "player"
      if UnitExists(unit) and not UnitIsDead(unit) then
        if not orb.model:IsShown() then orb.model:Show() end
      else
        if orb.model:IsShown() then orb.model:Hide() end
      end
      ns.UpdateOrbModelWithFillRatio(orb, ns.GetOrbFillRatioForViewport(orb))
    end)

    -- GALAXIES (inside holder, clipped by clipFrame)
    orb.galaxies = {}
    tinsert(orb.galaxies, createGalaxy(holder, 0, 0, size, 120, "galaxy2", 1, 360))
    tinsert(orb.galaxies, createGalaxy(holder, 0, -2, size-20, 90, "galaxy", 2, 360))
    tinsert(orb.galaxies, createGalaxy(holder, 0, -4, size-5, 60, "galaxy4", 3, 360))
    for _, galaxy in pairs(orb.galaxies) do
      galaxy:SetVertexColor(orbcfg.filling.color.r, orbcfg.filling.color.g, orbcfg.filling.color.b)
      galaxy:SetAlpha(orbcfg.galaxies.alpha or 0)
    end

    -- BUBBLES (inside holder, clipped by clipFrame)
    orb.bubbles = {}
    tinsert(orb.bubbles, createGalaxy(holder, 0, 0, size-12, 10, "orb_rotation_bubbles1", 4, -360))
    tinsert(orb.bubbles, createGalaxy(holder, 0, 0, size, 20, "orb_rotation_bubbles2", 5, 360))
    tinsert(orb.bubbles, createGalaxy(holder, 0, 0, size-12, 15, "orb_rotation_bubbles1", 6, 360))
    tinsert(orb.bubbles, createGalaxy(holder, 0, 0, size, 20, "orb_rotation_bubbles2", 5, -360))
    for _, bubble in pairs(orb.bubbles) do
      bubble:SetVertexColor(orbcfg.filling.color.r, orbcfg.filling.color.g, orbcfg.filling.color.b)
      bubble:SetAlpha(orbcfg.bubbles.alpha or 1)
    end

    -------------------------------------------------------
    -- LAYER 4: OVERLAY (above clip - für Spark, Skull, etc)
    -------------------------------------------------------
    local overlay = CreateFrame("Frame", "$parentOverlay", orb)
    overlay:SetAllPoints(orb)
    overlay:SetFrameLevel(orb:GetFrameLevel() + 10)
    overlay:EnableMouse(false)

    -- SKULL + LOW HP (health only)
    if orbType == "HEALTH" then
      local skull = overlay:CreateTexture(nil, "BACKGROUND", nil, 1)
      skull:SetPoint("CENTER")
      skull:SetSize(size - 40, size - 40)
      skull:SetTexture("Interface\\AddOns\\rOrbs\\media\\d2_skull")
      skull:SetBlendMode("ADD")
      skull:SetAlpha(0.6)
      skull:Hide()
      orb.skull = skull

      local lowHP = overlay:CreateTexture(nil, "BACKGROUND", nil, 2)
      lowHP:SetPoint("CENTER")
      lowHP:SetSize(size - 15, size - 15)
      lowHP:SetTexture("Interface\\AddOns\\rOrbs\\media\\orb_lowhp_glow")
      lowHP:SetBlendMode("ADD")
      lowHP:SetVertexColor(1, 0, 0, 1)
      lowHP:Hide()
      orb.lowHP = lowHP
    end

    -- GLOSS
    local glossChoice = rOrbsDB and rOrbsDB["glossTex_"..orbType] or "new"
    local glossTex = (glossChoice == "classic")
        and "Interface\\AddOns\\rOrbs\\media\\orb_gloss"
        or  "Interface\\AddOns\\rOrbs\\media\\orb_gloss_new"
    local highlight = overlay:CreateTexture("$parentHighlight", "BACKGROUND", nil, 3)
    highlight:SetAllPoints()
    highlight:SetTexture(glossTex)
    highlight:SetAlpha(orbcfg.highlight.alpha or 1)
    orb.highlight = highlight

    -- DECORATIVE RING
    local ringSize = math.floor(size * 1.05)
    local ring = overlay:CreateTexture("$parentRing", "ARTWORK", nil, 1)
    ring:SetSize(ringSize, ringSize)
    ring:SetPoint("CENTER", orb, "CENTER", 0, 1)
    ring:SetTexture("Interface\\AddOns\\rOrbs\\media\\nelson_neto_round")
    ring:SetBlendMode("BLEND")
    local ringDBKey = (orbType == "HEALTH") and "healthRingShow" or "powerRingShow"
    if rOrbsDB and rOrbsDB[ringDBKey] == false then ring:Hide() end
    orb.ring = ring

    -- RING ROTATION ANIMATION
    do
      local ringRotateKey = (orbType == "HEALTH") and "healthRingRotate" or "powerRingRotate"
      local ringSpeedKey  = (orbType == "HEALTH") and "healthRingSpeed"  or "powerRingSpeed"
      local ringDirKey    = (orbType == "HEALTH") and "healthRingDir"    or "powerRingDir"
      if rOrbsDB and rOrbsDB[ringRotateKey] then
        ring.ag = ring:CreateAnimationGroup()
        ring.ag.anim = ring.ag:CreateAnimation("Rotation")
        local ringSpeed = rOrbsDB[ringSpeedKey] or 30
        local ringDir   = rOrbsDB[ringDirKey]   or "CW"
        ring.ag.anim:SetDegrees(ringDir == "CCW" and 360 or -360)
        ring.ag.anim:SetDuration(ringSpeed)
        ring.ag:SetLooping("REPEAT")
        ring.ag:Play()
      end
    end

    -------------------------------------------------------
    -- LAYER 5: TEXT VALUES
    -------------------------------------------------------
    local values = CreateFrame("Frame", "$parentValues", overlay)
    values:SetAllPoints(orb)
    values:SetFrameLevel(overlay:GetFrameLevel() + 1)
    values:EnableMouse(false)
    values:SetAlpha(0.5)
    local topFontSize = (rOrbsDB and rOrbsDB["fontSize_"..orbType]) or 28
    local botFontSize = math.max(8, topFontSize - 12)
    values.top = func.createFontString(values, cfg.fonts.textFont, topFontSize, "THINOUTLINE")
    values.top:SetPoint("CENTER", 0, 10)
    values.top:SetTextColor(orbcfg.value.top.color.r, orbcfg.value.top.color.g, orbcfg.value.top.color.b)
    values.bottom = func.createFontString(values, cfg.fonts.textFont, botFontSize, "THINOUTLINE")
    values.bottom:SetPoint("CENTER", 0, -10)
    values.bottom:SetTextColor(orbcfg.value.top.color.r, orbcfg.value.top.color.g, orbcfg.value.top.color.b)
    orb.values = values

    -- dragframe wird vor dem Overlay erzeugt; ohne Raise liegt Skalierungs-Text unter Gloss/Values.
    if orb.dragframe then
      orb.dragframe:SetFrameLevel(values:GetFrameLevel() + 2)
    end

    -- -------------------------------------------------------
    -- ABSORB OVERLAYS (health only) – oUF HealthPrediction
    -- -------------------------------------------------------
    if orbType == "HEALTH" and not rOrbs:CheckClientVersion(WOW_PROJECT_CLASSIC) then
      if not (self.cfg.absorb and self.cfg.absorb.show == false) then

        local damageAbsorb = CreateFrame("StatusBar", nil, fill)
        damageAbsorb:SetAllPoints(fill)
        damageAbsorb:SetOrientation("VERTICAL")
        damageAbsorb:SetStatusBarTexture(orbcfg.filling.texture)
        damageAbsorb:SetStatusBarColor(0, 0.78, 1, 0.7)
        damageAbsorb:SetFrameLevel(fill:GetFrameLevel() + 2)
        damageAbsorb:SetMinMaxValues(0, 1)
        damageAbsorb:SetValue(0)
        damageAbsorb:EnableMouse(false)
        damageAbsorb:Hide()
        orb.damageAbsorb = damageAbsorb

        local healAbsorb = CreateFrame("StatusBar", nil, fill)
        healAbsorb:SetAllPoints(fill)
        healAbsorb:SetOrientation("VERTICAL")
        healAbsorb:SetReverseFill(true)
        healAbsorb:SetStatusBarTexture(orbcfg.filling.texture)
        healAbsorb:SetStatusBarColor(0.65, 0, 0.1, 0.8)
        healAbsorb:SetFrameLevel(fill:GetFrameLevel() + 2)
        healAbsorb:SetMinMaxValues(0, 1)
        healAbsorb:SetValue(0)
        healAbsorb:EnableMouse(false)
        healAbsorb:Hide()
        orb.healAbsorb = healAbsorb

        local absorbEventFrame = CreateFrame("Frame")
        absorbEventFrame:RegisterUnitEvent("UNIT_ABSORB_AMOUNT_CHANGED",     "player")
        absorbEventFrame:RegisterUnitEvent("UNIT_HEAL_ABSORB_AMOUNT_CHANGED", "player")
        absorbEventFrame:RegisterEvent("PLAYER_DEAD")
        absorbEventFrame:RegisterEvent("PLAYER_ALIVE")
        absorbEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
        absorbEventFrame:SetScript("OnEvent", function(_, event, unitArg)
          if event == "PLAYER_DEAD" then
            damageAbsorb:Hide()
            healAbsorb:Hide()
          elseif event == "UNIT_ABSORB_AMOUNT_CHANGED" then
            if not UnitIsDeadOrGhost("player") then
              damageAbsorb:Show()
            end
          elseif event == "UNIT_HEAL_ABSORB_AMOUNT_CHANGED" then
            if not UnitIsDeadOrGhost("player") then
              healAbsorb:Show()
            end
          elseif event == "PLAYER_ALIVE" or event == "PLAYER_ENTERING_WORLD" then
            damageAbsorb:Hide()
            healAbsorb:Hide()
          end
        end)

        damageAbsorb:Hide()
        healAbsorb:Hide()

        self.HealthPrediction = {
          damageAbsorb = damageAbsorb,
          healAbsorb   = healAbsorb,
          PostUpdate = function(element, unit)
            local da = element.damageAbsorb
            local ha = element.healAbsorb
            local maxHP = UnitHealthMax(unit)
            da:SetMinMaxValues(0, maxHP)
            da:SetValue(UnitGetTotalAbsorbs(unit))
            ha:SetMinMaxValues(0, maxHP)
            ha:SetValue(UnitGetTotalHealAbsorbs(unit))
            if UnitIsDeadOrGhost(unit) then
              da:Hide()
              ha:Hide()
            end
          end,
        }
      end
    end

    -------------------------------------------------------
    -- CONNECT TO oUF
    -------------------------------------------------------
    if orbType == "POWER" then
      self.Power = fill
      ns.PowerOrb = orb
      self.Power.frequentUpdates = self.cfg.power.frequentUpdates or false
      -- colorPower intentionally false: we drive power color manually via
      -- UNIT_DISPLAYPOWER + PowerBarColor (Blizzard-native, no reload needed).
      self.Power.colorPower = false
      self.Power.PostUpdate = postUpdate
      hooksecurefunc(fill, "SetStatusBarColor", function(_, r, g, b)
        updateStatusBarColor(fill, r, g, b)
      end)

      -- -------------------------------------------------------
      -- LIVE POWER COLOR  (Blizzard-native, works without reload)
      -- Listens to UNIT_DISPLAYPOWER (fires on shapeshift / stance /
      -- spec changes that alter the power type) and applies
      -- PowerBarColor[powerToken] – the same table Blizzard itself uses.
      -- ns.ForcePowerOrbColor() can be called from the GUI checkbox.
      -- -------------------------------------------------------
      do
        local function applyPowerColor()
          if not ns.PowerOrb or not ns.PowerOrb.fill then return end
          local orbcfg = db.char.POWER
          if not orbcfg then return end
          local r, g, b
          if orbcfg.filling.colorAuto then
            local _, powerToken = UnitPowerType("player")
            local pcolor = powerToken and PowerBarColor and PowerBarColor[powerToken]
            if pcolor then
              r, g, b = pcolor.r, pcolor.g, pcolor.b
            end
          end
          if not r then
            r = orbcfg.filling.color.r
            g = orbcfg.filling.color.g
            b = orbcfg.filling.color.b
          end
          ns.PowerOrb.fill:SetStatusBarColor(r, g, b)
        end
        local pColorFrame = CreateFrame("Frame")
        pColorFrame:RegisterUnitEvent("UNIT_DISPLAYPOWER", "player")
        pColorFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
        pColorFrame:SetScript("OnEvent", applyPowerColor)
        ns.ForcePowerOrbColor = applyPowerColor
      end

    else
      self.Health = fill
      ns.HealthOrb = orb
      self.Health.frequentUpdates = self.cfg.health.frequentUpdates or false
      self.Health.colorClass = false   -- color driven manually in postUpdate
      self.Health.colorHealth = false  -- color driven manually in postUpdate
      self.Health.PostUpdate = postUpdate
      hooksecurefunc(fill, "SetStatusBarColor", function(_, r, g, b)
        updateStatusBarColor(fill, r, g, b)
      end)

      -- -------------------------------------------------------
      -- LIVE CLASS COLOR  (Blizzard-native, works without reload)
      -- Listens to PLAYER_ENTERING_WORLD, PLAYER_SPECIALIZATION_CHANGED
      -- and UNIT_PORTRAIT_UPDATE so the class color is always in sync
      -- with the current setting – no health event required.
      -- Uses C_ClassColor.GetClassColor() (retail API, Blizzard-native)
      -- with RAID_CLASS_COLORS as fallback.
      -- ns.ForceHealthOrbColor() can be called from the GUI checkbox.
      -- -------------------------------------------------------
      do
        local function applyHealthColor()
          if not ns.HealthOrb or not ns.HealthOrb.fill then return end
          local orbcfg = db.char.HEALTH
          if not orbcfg then return end
          local r, g, b
          if orbcfg.filling.colorAuto and not UnitIsDeadOrGhost("player") then
            local _, class = UnitClass("player")
            if class then
              -- C_ClassColor is the modern retail Blizzard API
              local cc = C_ClassColor and C_ClassColor.GetClassColor(class)
              if cc then
                r, g, b = cc:GetRGB()
              else
                -- Fallback: RAID_CLASS_COLORS (legacy but always present)
                local rcc = RAID_CLASS_COLORS and RAID_CLASS_COLORS[class]
                if rcc then r, g, b = rcc.r, rcc.g, rcc.b end
              end
            end
          end
          if not r then
            r = orbcfg.filling.color.r
            g = orbcfg.filling.color.g
            b = orbcfg.filling.color.b
          end
          ns.HealthOrb.fill:SetStatusBarColor(r, g, b)
        end
        local hColorFrame = CreateFrame("Frame")
        hColorFrame:RegisterUnitEvent("UNIT_PORTRAIT_UPDATE", "player")
        hColorFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
        hColorFrame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
        hColorFrame:SetScript("OnEvent", applyHealthColor)
        ns.ForceHealthOrbColor = applyHealthColor
      end

    end

    -- Stelle gespeicherte Scale wieder her NACH der Erstellung
    if rOrbsDB and rOrbsDB.frameScales then
      local orbName = orb:GetName()
      if orbName and rOrbsDB.frameScales[orbName] then
        local savedScale = rOrbsDB.frameScales[orbName]
        orb:SetScale(savedScale)
        if orb.model and orb.model.Update then
          C_Timer.After(0.1, function()
            orb.model:Update()
          end)
        end
      end
    end
    
    -- Stelle gespeicherte Position wieder her
    if rOrbsDB and rOrbsDB.framePositions then
      local orbName = orb:GetName()
      if orbName and rOrbsDB.framePositions[orbName] then
        local pos = rOrbsDB.framePositions[orbName]
        orb:ClearAllPoints()
        orb:SetPoint(pos.point, _G[pos.relativeTo] or UIParent, pos.relativePoint, pos.x, pos.y)
      end
    end
  end

  ---------------------------------------------
  -- STYLE + SPAWN
  ---------------------------------------------

  local createStyle = function(self)
    self.cfg = ns.cfg.units.player
    self.cfg.style = "player"
    initUnitParameters(self)
    createOrb(self, "HEALTH")
    createOrb(self, "POWER")
    self:EnableMouse(false)
    self:SetHitRectInsets(10000, 10000, 10000, 10000)
    unit.player = self

    -- ----------------------------------------------------------------
    -- PLAYER COMBAT + RESTING INDICATORS
    -- CRITICAL: Do NOT use self.CombatIndicator or self.RestingIndicator on
    -- the oUF SecureUnitButtonTemplate. Setting those references activates oUF
    -- elements (combatindicator.lua / restingindicator.lua) which call SetAtlas /
    -- SetTexture / Hide() during oUF:Spawn() – a protected secure context.
    -- That taints WoW global Lua env, causing other addons (e.g. Pawn) to lose
    -- their globals (PawnOnEvent becomes nil).
    -- Solution: standalone non-secure UIParent frames + own event listeners.
    -- ----------------------------------------------------------------
    C_Timer.After(0, function()
      local hOrb = ns.HealthOrb
      if not hOrb then return end
      local orbSize = ns.cfg and ns.cfg.units and ns.cfg.units.player and ns.cfg.units.player.size or 160
      local iconOff = math.floor(orbSize * 0.55)

      -- ----------------------------------------------------------------
      -- COMBAT ICON (crossed swords) – plain UIParent Frame
      -- ----------------------------------------------------------------
      local combatFrame = CreateFrame("Frame", "rOrbsPlayerCombatFrame", UIParent)
      combatFrame:SetFrameStrata("MEDIUM")
      combatFrame:SetFrameLevel(60)
      do
        local cSz = rOrbsDB.playerCombatIconSize or 28
        combatFrame:SetSize(cSz, cSz)
        combatFrame:SetPoint("CENTER", hOrb, "CENTER",
          0 + (rOrbsDB.playerCombatIconOffX or 0),
          iconOff + (rOrbsDB.playerCombatIconOffY or 0))
      end
      local combatTex = combatFrame:CreateTexture(nil, "ARTWORK")
      combatTex:SetAllPoints(combatFrame)
      combatTex:SetAtlas("UI-HUD-UnitFrame-Player-CombatIcon")
      ns.PlayerCombatIcon = combatFrame
      combatFrame:Hide()

      -- Live-update helper: reposition/resize combat icon from current DB values
      ns.UpdateCombatIconLayout = function()
        local f = ns.PlayerCombatIcon
        if not f then return end
        local ho = ns.HealthOrb
        if not ho then return end
        local oSize = ns.cfg and ns.cfg.units and ns.cfg.units.player and ns.cfg.units.player.size or 160
        local iOff = math.floor(oSize * 0.55)
        local sz = rOrbsDB.playerCombatIconSize or 28
        f:SetSize(sz, sz)
        f:ClearAllPoints()
        f:SetPoint("CENTER", ho, "CENTER",
          0 + (rOrbsDB.playerCombatIconOffX or 0),
          iOff + (rOrbsDB.playerCombatIconOffY or 0))
      end

      -- Drag + Scale + Tooltip für Combat Icon
      applyIconDragFunctionality(
        combatFrame,
        "combat",
        "playerCombatIconSize",
        "playerCombatIconOffX",
        "playerCombatIconOffY",
        function() if ns.UpdateCombatIconLayout then ns.UpdateCombatIconLayout() end end
      )

      -- Merkt sich ob wir gerade in einem PEW-Ladebildschirm-Cooldown sind.
      -- Während dieser Zeit wird Show() auf combatFrame geblockt.
      local combatPEWLock = false

      local function UpdateCombat()
        if rOrbsDB and rOrbsDB.playerCombatIconShow == false then
          combatFrame:Hide(); return
        end
        if combatPEWLock then combatFrame:Hide(); return end
        if UnitAffectingCombat("player") then combatFrame:Show() else combatFrame:Hide() end
      end
      -- OnShow-Blocker: verhindert dass irgendetwas den Frame während des
      -- PEW-Cooldowns einblendet (z.B. Blizzard-Frames, andere Addons).
      combatFrame:SetScript("OnShow", function(self)
        if combatPEWLock then self:Hide() end
      end)
      local combatListener = CreateFrame("Frame", nil, UIParent)
      combatListener:RegisterEvent("PLAYER_REGEN_DISABLED")
      combatListener:RegisterEvent("PLAYER_REGEN_ENABLED")
      combatListener:RegisterEvent("PLAYER_ENTERING_WORLD")
      combatListener:SetScript("OnEvent", function(_, event)
        if event == "PLAYER_ENTERING_WORLD" then
          -- Lock für 2s: in dieser Zeit bleibt Combat-Icon immer versteckt.
          -- Danach echten Zustand prüfen.
          combatPEWLock = true
          combatFrame:Hide()
          C_Timer.After(2.0, function()
            combatPEWLock = false
            UpdateCombat()
          end)
        else
          UpdateCombat()
        end
      end)
      UpdateCombat()

      -- ----------------------------------------------------------------
      -- RESTING ICON (animated zzz) – steal Blizzard's PlayerRestLoop
      -- ----------------------------------------------------------------
      local restFrame
      local blizzRest = PlayerFrame
          and PlayerFrame.PlayerFrameContent
          and PlayerFrame.PlayerFrameContent.PlayerFrameContentContextual
          and PlayerFrame.PlayerFrameContent.PlayerFrameContentContextual.PlayerRestLoop

      if blizzRest then
        blizzRest:SetParent(UIParent)
        blizzRest:SetFrameStrata("MEDIUM")
        blizzRest:SetFrameLevel(60)
        blizzRest:ClearAllPoints()

        -- Fixe Basis-Breite EINMALIG speichern (vor jeder Scale-Änderung).
        -- GetWidth() gibt die unveränderte logische Breite zurück, solange wir
        -- noch keine SetScale() aufgerufen haben. Ab dann ist dieser Wert stabil.
        local blizzRestBaseWidth = blizzRest:GetWidth()
        if not (blizzRestBaseWidth and blizzRestBaseWidth > 0) then
          blizzRestBaseWidth = 28  -- Fallback
        end
        -- Diese Basis auf dem Frame selbst speichern damit UpdateRestingIconLayout sie verwenden kann
        blizzRest._baseWidth = blizzRestBaseWidth

        local desired = rOrbsDB.playerRestingIconSize or 28
        blizzRest:SetScale(desired / blizzRestBaseWidth)
        blizzRest:SetPoint("CENTER", hOrb, "CENTER",
          -30 + (rOrbsDB.playerRestingIconOffX or 0),
          iconOff + (rOrbsDB.playerRestingIconOffY or 0))

        restFrame = blizzRest
      else
        -- Fallback: static zzz texture if Blizzard's frame is unavailable
        restFrame = CreateFrame("Frame", "rOrbsPlayerRestingFrame", UIParent)
        restFrame:SetFrameStrata("MEDIUM")
        restFrame:SetFrameLevel(60)
        do
          local rSz = rOrbsDB.playerRestingIconSize or 28
          restFrame:SetSize(rSz, rSz)
          restFrame:SetPoint("CENTER", hOrb, "CENTER",
            -30 + (rOrbsDB.playerRestingIconOffX or 0),
            iconOff + (rOrbsDB.playerRestingIconOffY or 0))
        end
        local restTex = restFrame:CreateTexture(nil, "ARTWORK")
        restTex:SetAllPoints(restFrame)
        restTex:SetTexture("Interface\\AddOns\\rOrbs\\media\\zzz")
      end
      ns.PlayerRestingIcon = restFrame
      restFrame:Hide()

      -- Apply vertex color to all textures inside the resting frame
      ns.ApplyRestingIconColor = function(r, g, b)
        local f = ns.PlayerRestingIcon
        if not f then return end
        for _, region in ipairs({ f:GetRegions() }) do
          if region.SetVertexColor then
            region:SetVertexColor(r, g, b)
          end
        end
        for _, child in ipairs({ f:GetChildren() }) do
          if child.GetRegions then
            for _, region in ipairs({ child:GetRegions() }) do
              if region.SetVertexColor then
                region:SetVertexColor(r, g, b)
              end
            end
          end
        end
      end
      do
        local col = rOrbsDB.playerRestingIconColor or { r = 1, g = 1, b = 1 }
        ns.ApplyRestingIconColor(col.r, col.g, col.b)
      end

      -- Live-update helper: reposition/resize resting icon from current DB values
      ns.UpdateRestingIconLayout = function()
        local f = ns.PlayerRestingIcon
        if not f then return end
        local ho = ns.HealthOrb
        if not ho then return end
        local oSize = ns.cfg and ns.cfg.units and ns.cfg.units.player and ns.cfg.units.player.size or 160
        local iOff = math.floor(oSize * 0.55)

        if blizzRest and f == blizzRest then
          -- Verwende die einmalig gespeicherte Basis-Breite.
          -- NIEMALS GetWidth()/GetScale() hier verwenden – nach dem ersten
          -- SetScale() ist GetWidth() bereits skaliert und erzeugt exponentiellen Drift.
          local baseW = blizzRest._baseWidth or 28
          local desired = rOrbsDB.playerRestingIconSize or 28
          blizzRest:SetScale(desired / baseW)
        else
          local sz = rOrbsDB.playerRestingIconSize or 28
          f:SetSize(sz, sz)
        end
        f:ClearAllPoints()
        f:SetPoint("CENTER", ho, "CENTER",
          -30 + (rOrbsDB.playerRestingIconOffX or 0),
          iOff + (rOrbsDB.playerRestingIconOffY or 0))
      end

      -- Drag + Scale + Tooltip für Resting Icon
      applyIconDragFunctionality(
        restFrame,
        "resting",
        "playerRestingIconSize",
        "playerRestingIconOffX",
        "playerRestingIconOffY",
        function() if ns.UpdateRestingIconLayout then ns.UpdateRestingIconLayout() end end
      )

      -- PEW-Lock: während 2s nach Ladebildschirm bleibt Resting-Icon immer versteckt.
      -- Blizzard ruft PlayerRestLoop:Show() in seinem eigenen PLAYER_ENTERING_WORLD-
      -- Handler auf (läuft NACH unserem) – dagegen hilft nur ein OnShow-Blocker.
      local restPEWLock = false

      local function UpdateResting()
        if rOrbsDB and rOrbsDB.playerRestingIconShow == false then
          restFrame:Hide(); return
        end
        if restPEWLock then restFrame:Hide(); return end
        if IsResting() then
          restFrame:Show()
          local col = rOrbsDB.playerRestingIconColor or { r = 1, g = 1, b = 1 }
          if ns.ApplyRestingIconColor then ns.ApplyRestingIconColor(col.r, col.g, col.b) end
          -- Kick-start Blizzard's looping animations after reparent
          if blizzRest and restFrame == blizzRest then
            local regions = { blizzRest:GetRegions() }
            for _, r in ipairs(regions) do
              if r.GetAnimationGroups then
                for _, ag in ipairs({ r:GetAnimationGroups() }) do
                  if not ag:IsPlaying() then ag:Play() end
                end
              end
            end
            if blizzRest.GetAnimationGroups then
              for _, ag in ipairs({ blizzRest:GetAnimationGroups() }) do
                if not ag:IsPlaying() then ag:Play() end
              end
            end
          end
        else
          restFrame:Hide()
        end
      end
      -- OnShow-Blocker: Blizzard ruft PlayerRestLoop:Show() in seinem eigenen
      -- PEW-Handler auf – dieser Blocker unterbricht das sofort.
      restFrame:SetScript("OnShow", function(self)
        if restPEWLock then self:Hide() end
      end)
      local restListener = CreateFrame("Frame", nil, UIParent)
      restListener:RegisterEvent("PLAYER_UPDATE_RESTING")
      restListener:RegisterEvent("PLAYER_ENTERING_WORLD")
      restListener:SetScript("OnEvent", function(_, event)
        if event == "PLAYER_ENTERING_WORLD" then
          restPEWLock = true
          restFrame:Hide()
          C_Timer.After(2.0, function()
            restPEWLock = false
            UpdateResting()
          end)
        else
          UpdateResting()
        end
      end)
      UpdateResting()

      -- ----------------------------------------------------------------
      -- LEADER ICON – standalone, draggable (Shift+LClick), uses leader.tga
      -- ----------------------------------------------------------------
      local leaderFrame = CreateFrame("Frame", "rOrbsPlayerLeaderFrame", UIParent)
      leaderFrame:SetFrameStrata("MEDIUM")
      leaderFrame:SetFrameLevel(60)
      do
        local lSz = rOrbsDB.playerLeaderIconSize or 22
        leaderFrame:SetSize(lSz, lSz)
        leaderFrame:SetPoint("CENTER", hOrb, "CENTER",
          30 + (rOrbsDB.playerLeaderIconOffX or 0),
          iconOff + (rOrbsDB.playerLeaderIconOffY or 0))
      end
      local leaderTex = leaderFrame:CreateTexture(nil, "ARTWORK")
      leaderTex:SetAllPoints(leaderFrame)
      leaderTex:SetTexture(ns.mediapath .. "leader")
      ns.PlayerLeaderIcon = leaderFrame
      leaderFrame:Hide()

      ns.UpdateLeaderIconLayout = function()
        local f = ns.PlayerLeaderIcon
        if not f then return end
        local ho2 = ns.HealthOrb
        if not ho2 then return end
        local oSize = ns.cfg and ns.cfg.units and ns.cfg.units.player and ns.cfg.units.player.size or 160
        local iOff = math.floor(oSize * 0.55)
        local sz = rOrbsDB.playerLeaderIconSize or 22
        f:SetSize(sz, sz)
        f:ClearAllPoints()
        f:SetPoint("CENTER", ho2, "CENTER",
          30 + (rOrbsDB.playerLeaderIconOffX or 0),
          iOff + (rOrbsDB.playerLeaderIconOffY or 0))
      end

      applyIconDragFunctionality(
        leaderFrame,
        "leader",
        "playerLeaderIconSize",
        "playerLeaderIconOffX",
        "playerLeaderIconOffY",
        function() if ns.UpdateLeaderIconLayout then ns.UpdateLeaderIconLayout() end end
      )

      -- PEW-Lock analog zu combatPEWLock
      local leaderPEWLock = false

      local function UpdateLeader()
        if rOrbsDB and rOrbsDB.playerLeaderIconShow == false then
          leaderFrame:Hide(); return
        end
        if leaderPEWLock then leaderFrame:Hide(); return end
        if UnitIsGroupLeader("player") and IsInGroup() then
          leaderFrame:Show()
        else
          leaderFrame:Hide()
        end
      end
      -- OnShow-Blocker
      leaderFrame:SetScript("OnShow", function(self)
        if leaderPEWLock then self:Hide() end
      end)
      local leaderListener = CreateFrame("Frame", nil, UIParent)
      leaderListener:RegisterEvent("PARTY_LEADER_CHANGED")
      leaderListener:RegisterEvent("GROUP_ROSTER_UPDATE")
      leaderListener:RegisterEvent("PLAYER_ENTERING_WORLD")
      leaderListener:SetScript("OnEvent", function(_, event)
        if event == "PLAYER_ENTERING_WORLD" then
          leaderPEWLock = true
          leaderFrame:Hide()
          C_Timer.After(2.0, function()
            leaderPEWLock = false
            UpdateLeader()
          end)
        else
          UpdateLeader()
        end
      end)
      UpdateLeader()

      -- Nach Fahrzeug: init.lua blendet Icons/Figuren ein — hier die echte Sichtbarkeit wiederherstellen
      ns.RefreshStandalonePlayerIcons = function()
        UpdateCombat()
        UpdateResting()
        UpdateLeader()
      end
    end)

    -- ----------------------------------------------------------------
    -- FIGURE TEXTURE  (left of Health orb) – Angel OR Diablo, not both
    -- DEMON TEXTURE   (right of Power orb) – unchanged
    -- ----------------------------------------------------------------

    -- Diablo texture native size: 454 x 335 px  →  aspect ratio ≈ 1.3552
    local DIABLO_ASPECT = 454 / 335
    -- Figure_Demon.tga native size: 385 x 286 px  →  aspect ratio ≈ 1.3462
    local DEMON_ASPECT  = 385 / 286
    -- Figure_Skull.tga native size: 392 x 225 px  →  aspect ratio ≈ 1.7422
    local SKULL_ASPECT  = 392 / 225
    -- NelsonNeto_angel_{left,right}.tga native size: 506 x 429 px  →  aspect ratio ≈ 1.1792
    local ANGEL_ASPECT = 506 / 429

    -- Textures available for "Diablo" mode
    -- ("Angel" mode always uses NelsonNeto_angel_left)
    ns.diabloTextures = {
        { key = "diablo_health",  label = "Diablo",          path = "Interface\\AddOns\\rOrbs\\media\\diablo_health" },
        { key = "d3_head_diablo", label = "D3 Head Diablo",  path = "Interface\\AddOns\\rOrbs\\media\\d3_head_diablo" },
        { key = "d3_head_new",    label = "D3 Head New",     path = "Interface\\AddOns\\rOrbs\\media\\d3_head_new" },
        { key = "d3_head_trans",  label = "D3 Head Trans",   path = "Interface\\AddOns\\rOrbs\\media\\d3_head_trans" },
        { key = "d3_head_skull",  label = "D3 Head Skull",   path = "Interface\\AddOns\\rOrbs\\media\\d3_head_skull" },
        { key = "d3_head_elite",  label = "D3 Head Elite",   path = "Interface\\AddOns\\rOrbs\\media\\d3_head_elite" },
        { key = "d3_head_garg",   label = "D3 Head Garg",    path = "Interface\\AddOns\\rOrbs\\media\\d3_head_garg" },
        { key = "d2_skull",       label = "D2 Skull",        path = "Interface\\AddOns\\rOrbs\\media\\d2_skull" },
    }

    local function GetDiabloTexPath()
        local key = rOrbsDB and rOrbsDB.diabloTexture or "diablo_health"
        for _, entry in ipairs(ns.diabloTextures) do
            if entry.key == key then return entry.path end
        end
        return "Interface\\AddOns\\rOrbs\\media\\diablo_health"
    end

    local function UpdateFigureLayout(figFrame, orbFrame, anchorSide, dbScale, dbOffX, dbOffY)
        if not figFrame or not orbFrame then return end
        local orbSize = ns.cfg and ns.cfg.units and ns.cfg.units.player and ns.cfg.units.player.size or 160
        local scale   = dbScale  or 1.0
        local offX    = dbOffX   or 0
        local offY    = dbOffY   or 0
        local figH    = math.floor(orbSize * 1.3 * scale)
        -- Diablo mode: use native aspect ratio so texture is not squished.
        -- Angel mode: use native aspect ratio so texture is not squished.
        local mode = rOrbsDB and rOrbsDB.healthFigureMode or "angel"
        local figW
        if figFrame == ns.FigureFrame and mode == "diablo" then
            figW = math.floor(figH * DIABLO_ASPECT)
        elseif figFrame == ns.FigureFrame and mode == "angel" then
            figW = math.floor(figH * ANGEL_ASPECT)
        elseif figFrame == ns.FigureFrame and mode == "skull" then
            figW = math.floor(figH * SKULL_ASPECT)
        elseif figFrame == ns.FigureFrame and mode == "demon" then
            figW = math.floor(figH * DEMON_ASPECT)
        elseif figFrame == ns.DemonFrame then
            local pMode = rOrbsDB and rOrbsDB.powerFigureMode or "angel"
            if pMode == "demon" then
                figW = math.floor(figH * DEMON_ASPECT)
            elseif pMode == "skull" then
                figW = math.floor(figH * SKULL_ASPECT)
            elseif pMode == "diablo" then
                figW = math.floor(figH * DIABLO_ASPECT)
            else
                figW = math.floor(figH * ANGEL_ASPECT) -- angel: preserve native aspect
            end
        else
            figW = math.floor(figH * ANGEL_ASPECT)
        end
        figFrame:SetSize(figW, figH)
        figFrame:ClearAllPoints()
        if anchorSide == "LEFT" then
            figFrame:SetPoint("RIGHT", orbFrame, "LEFT", math.floor(orbSize * 0.22) + offX, offY)
        else
            figFrame:SetPoint("LEFT", orbFrame, "RIGHT", -math.floor(orbSize * 0.22) + offX, offY)
        end
    end

    ns.UpdateFigureLayout = UpdateFigureLayout

    -- Determine initial figure texture based on mode
    local function GetFigureTexPath()
        local mode = rOrbsDB and rOrbsDB.healthFigureMode or "angel"
        if mode == "angel" then
            return "Interface\\AddOns\\rOrbs\\media\\NelsonNeto_angel_left"
        elseif mode == "diablo" then
            return GetDiabloTexPath()
        elseif mode == "skull" then
            return "Interface\\AddOns\\rOrbs\\media\\Figure_Skull"
        elseif mode == "demon" then
            return "Interface\\AddOns\\rOrbs\\media\\Figure_Demon"
        end
        return "Interface\\AddOns\\rOrbs\\media\\NelsonNeto_angel_left"
    end

    -- Migration: if old angel/diablo separate settings exist, carry them over
    do
        if rOrbsDB then
            -- Migrate old angelScale/angelOffX/angelOffY → figureScale/figureOffX/figureOffY
            if rOrbsDB.figureScale == nil and rOrbsDB.angelScale then
                rOrbsDB.figureScale = rOrbsDB.angelScale
            end
            if rOrbsDB.figureOffX == nil and rOrbsDB.angelOffX then
                rOrbsDB.figureOffX = rOrbsDB.angelOffX
            end
            if rOrbsDB.figureOffY == nil and rOrbsDB.angelOffY then
                rOrbsDB.figureOffY = rOrbsDB.angelOffY
            end
            -- Migrate old mode: if healthAngelShow was false and diabloHealthShow was true → diablo
            if rOrbsDB.healthFigureMode == nil then
                if rOrbsDB.healthAngelShow == false and rOrbsDB.diabloHealthShow ~= false then
                    rOrbsDB.healthFigureMode = "diablo"
                elseif rOrbsDB.healthAngelShow == false and rOrbsDB.diabloHealthShow == false then
                    rOrbsDB.healthFigureMode = "none"
                else
                    rOrbsDB.healthFigureMode = "angel"
                end
            end
        end
    end

    -- SINGLE FIGURE FRAME (Angel OR Diablo, depending on rOrbsDB.healthFigureMode)
    local figureFrame = CreateFrame("Frame", "rOrbsAngelFrame", UIParent)  -- keep name for vehicle-hide compat
    figureFrame:SetFrameStrata("MEDIUM")
    figureFrame:SetFrameLevel(50)
    figureFrame:EnableMouse(false)
    local figureTex = figureFrame:CreateTexture(nil, "ARTWORK")
    figureTex:SetAllPoints(figureFrame)
    figureTex:SetTexture(GetFigureTexPath())
    figureFrame._tex = figureTex

    UpdateFigureLayout(figureFrame, ns.HealthOrb, "LEFT",
        rOrbsDB and rOrbsDB.figureScale,
        rOrbsDB and rOrbsDB.figureOffX,
        rOrbsDB and rOrbsDB.figureOffY)

    local figureMode = rOrbsDB and rOrbsDB.healthFigureMode or "angel"
    if figureMode == "none" then figureFrame:Hide() end

    ns.AngelFrame     = figureFrame   -- keep legacy reference
    ns.FigureFrame    = figureFrame
    -- Legacy compat: ns.DiabloHealthFrame no longer separate
    ns.DiabloHealthFrame = figureFrame

    -- Helper: apply current mode (swap texture, resize, show/hide)
    ns.ApplyFigureMode = function()
        if not ns.FigureFrame or not ns.HealthOrb then return end
        local mode = rOrbsDB and rOrbsDB.healthFigureMode or "angel"
        if mode == "none" then
            ns.FigureFrame:Hide()
        else
            ns.FigureFrame._tex:SetTexture(GetFigureTexPath())
            UpdateFigureLayout(ns.FigureFrame, ns.HealthOrb, "LEFT",
                rOrbsDB.figureScale, rOrbsDB.figureOffX, rOrbsDB.figureOffY)
            ns.FigureFrame:Show()
        end
    end

    -- Helper: update layout only (scale/offsets changed)
    ns.UpdateFigurePositionLayout = function()
        if not ns.FigureFrame or not ns.HealthOrb then return end
        local mode = rOrbsDB and rOrbsDB.healthFigureMode or "angel"
        if mode == "none" then return end
        UpdateFigureLayout(ns.FigureFrame, ns.HealthOrb, "LEFT",
            rOrbsDB.figureScale, rOrbsDB.figureOffX, rOrbsDB.figureOffY)
    end

    -- Legacy helpers still referenced elsewhere
    ns.UpdateDiabloHealthLayout  = ns.UpdateFigurePositionLayout
    ns.UpdateDiabloHealthTexture = function()
        if not ns.FigureFrame then return end
        ns.FigureFrame._tex:SetTexture(GetDiabloTexPath())
    end

    -- POWER FIGURE (Angel right OR Demon, right side)
    local function GetPowerFigureTexPath()
        local mode = rOrbsDB and rOrbsDB.powerFigureMode or "angel"
        if mode == "demon" then
            return "Interface\\AddOns\\rOrbs\\media\\Figure_Demon"
        elseif mode == "skull" then
            return "Interface\\AddOns\\rOrbs\\media\\Figure_Skull"
        elseif mode == "diablo" then
            return "Interface\\AddOns\\rOrbs\\media\\diablo_health"
        end
        return "Interface\\AddOns\\rOrbs\\media\\NelsonNeto_angel_right"
    end

    -- Migration: powerDemonShow → powerFigureMode
    do
        if rOrbsDB then
            if rOrbsDB.powerFigureMode == nil then
                if rOrbsDB.powerDemonShow == false then
                    rOrbsDB.powerFigureMode = "none"
                else
                    rOrbsDB.powerFigureMode = "angel"
                end
            end
        end
    end

    local demonFrame = CreateFrame("Frame", "rOrbsDemonFrame", UIParent)
    demonFrame:SetFrameStrata("MEDIUM")
    demonFrame:SetFrameLevel(50)
    demonFrame:EnableMouse(false)
    local demonTex = demonFrame:CreateTexture(nil, "ARTWORK")
    demonTex:SetAllPoints(demonFrame)
    demonTex:SetTexture(GetPowerFigureTexPath())
    demonFrame._tex = demonTex
    -- IMPORTANT: assign ns.DemonFrame BEFORE UpdateFigureLayout so the aspect-ratio
    -- branch inside that function can identify this frame correctly on first call.
    ns.DemonFrame = demonFrame
    UpdateFigureLayout(demonFrame, ns.PowerOrb, "RIGHT",
        rOrbsDB and rOrbsDB.demonScale,
        rOrbsDB and rOrbsDB.demonOffX,
        rOrbsDB and rOrbsDB.demonOffY)
    local initPowerMode = rOrbsDB and rOrbsDB.powerFigureMode or "angel"
    if initPowerMode == "none" then demonFrame:Hide() end

    -- Helper: apply current power figure mode (swap texture + show/hide)
    ns.ApplyPowerFigureMode = function()
        if not ns.DemonFrame or not ns.PowerOrb then return end
        local mode = rOrbsDB and rOrbsDB.powerFigureMode or "angel"
        if mode == "none" then
            ns.DemonFrame:Hide()
        else
            local texPath = GetPowerFigureTexPath()
            ns.DemonFrame._tex:SetTexture(texPath)
            UpdateFigureLayout(ns.DemonFrame, ns.PowerOrb, "RIGHT",
                rOrbsDB.demonScale, rOrbsDB.demonOffX, rOrbsDB.demonOffY)
            ns.DemonFrame:Show()
        end
    end

    -- Helper: update layout only (scale/offset changed)
    ns.UpdatePowerFigurePositionLayout = function()
        if not ns.DemonFrame or not ns.PowerOrb then return end
        local mode = rOrbsDB and rOrbsDB.powerFigureMode or "angel"
        if mode == "none" then return end
        UpdateFigureLayout(ns.DemonFrame, ns.PowerOrb, "RIGHT",
            rOrbsDB.demonScale, rOrbsDB.demonOffX, rOrbsDB.demonOffY)
    end
  end

  local respawnQueued = false
  local function Init()
    if respawnQueued then return end
    respawnQueued = true
    cfg = ns.cfg
    if cfg.units.player.show then
      C_Timer.After(0, function()
        respawnQueued = false
        local styleName = "diablo:player"..GetTime()
        local f = _G["rOrbsPlayerFrame"]
        if f then
          UnregisterUnitWatch(f)
          f:UnregisterAllEvents()
          for _, child in ipairs({f:GetChildren()}) do
            child:UnregisterAllEvents()
            child:Hide()
          end
          f:Hide()
          f:SetParent(nil)
          _G["rOrbsPlayerFrame"] = nil
        end
        oUF:RegisterStyle(styleName, createStyle)
        oUF:SetActiveStyle(styleName)
        local frame = oUF:Spawn("player", "rOrbsPlayerFrame")
        frame:EnableMouse(false)
        frame:SetHitRectInsets(10000, 10000, 10000, 10000)
        frame:SetAttribute('*type1', nil)
        frame:SetAttribute('*type2', nil)
        frame:SetAttribute('toggleForVehicle', nil)
        frame:UpdateAllElements("ForceUpdate")
        C_Timer.After(0.5, function()
          if frame then
            frame:EnableMouse(false)
            frame:SetHitRectInsets(10000, 10000, 10000, 10000)
          end
          if frame and frame.UpdateAllElements then
            frame:UpdateAllElements("ForceUpdate")
          end
        end)
      end)
    end
  end

  ns:RegisterInit(Init)
