  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
  ---------------------------------------------

  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)

    -- 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"
    if orb.type == "HEALTH" then
      if textMode == "PERCENT" then
        ns.RenderPercent(orb.values.top, unit, true)
        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)
        ns.RenderAbbreviated(orb.values.bottom, unit, true)
      end
    elseif orb.type == "POWER" then
      if textMode == "PERCENT" then
        ns.RenderPowerTop(orb.values.top, unit)
        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)
        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

  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
  ---------------------------------------------

  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)

    -- 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"
    if orb.type == "HEALTH" then
      if textMode == "PERCENT" then
        ns.RenderPercent(orb.values.top, unit, true)
        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)
        ns.RenderAbbreviated(orb.values.bottom, unit, true)
      end
    elseif orb.type == "POWER" then
      if textMode == "PERCENT" then
        ns.RenderPowerTop(orb.values.top, unit)
        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)
        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

  ---------------------------------------------
  -- 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
    --print(string.format("[rOrbs DEBUG] %s Orb: clipFrame created", orbType))

    -- 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)
    --print(string.format("[rOrbs DEBUG] %s Orb: holder created, size: %d", orbType, size))

    -- 3D MODEL inside holder
    local model = CreateFrame("PlayerModel", nil, holder)
    model:SetFrameLevel(holder:GetFrameLevel() + 1)
    model:SetAllPoints(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)
      --print(string.format("[rOrbs MODEL DEBUG] %s Model - orbScale: %.2f, zoom: %.2f -> %.2f", 
      --  self.type or "?", orbScale, c.portraitZoom, adjustedZoom))
    end
    model.type = orbType
    model:SetScript("OnEvent", function(s)
      -- Skip if template engine is managing this model
      if s._templateConfig 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
      if s._templateConfig 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

    -- PlayerModel is NOT clipped by clipFrame:SetClipsChildren in WoW.
    -- 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._templateConfig then return end -- handled by OrbTemplateEngine
      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
    end)
    
    --print(string.format("[rOrbs DEBUG] %s Orb: Model created with dynamic scaling compensation", orbType))

    -- 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
    --print(string.format("[rOrbs DEBUG] %s Orb: Galaxies created - count: %d, alpha: %.2f", 
    --  orbType, #orb.galaxies, orbcfg.galaxies.alpha or 0))

    -- 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)

    -- SPARK - Kind von overlay (FrameLevel > Gloss), Position an clipFrame:TOP

    -- SKULL + LOW HP (health only)
    -- BACKGROUND Layer wie im Original mit Sublevel 1 und 2
    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 - MUSS GANZ VORNE SEIN!
    -- BACKGROUND Layer wie im Original mit Sublevel 3 (höchstes Level!)
    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

    -- INNERSHADOW ENTFERNT - wird im Original nicht verwendet!

    -- DECORATIVE RING (same layer as target-health orb)
    -- Visibility controlled by rOrbsDB.healthRingShow / rOrbsDB.powerRingShow (default: true).
    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
    -- DB keys: healthRingRotate / powerRingRotate (bool)
    --          healthRingSpeed  / powerRingSpeed  (seconds per revolution, default 30)
    --          healthRingDir    / powerRingDir    ("CW" or "CCW")
    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

    -- TAGS
    -- TAGS REMOVED: Text rendering is now done directly in PostUpdate
    -- using secret-safe C-side functions (SetFormattedText, AbbreviateLargeNumbers).
    -- oUF's tag system would overwrite PostUpdate's text with empty strings.

    -- ABSORB (health only)
    if self.cfg.absorb.show and orbType == "HEALTH" and not rOrbs:CheckClientVersion(WOW_PROJECT_CLASSIC) then
      local absorbBar = CreateFrame("StatusBar", nil, values)
      absorbBar:SetPoint("CENTER")
      absorbBar:SetSize(size - 5, size - 5)
      absorbBar:EnableMouse(false)
      absorbBar.bg = absorbBar:CreateTexture(nil, "BACKGROUND", nil, -8)
      absorbBar.bg:SetAllPoints()
      absorbBar.bg:SetTexture("Interface\\AddOns\\rOrbs\\media\\orb_absorb_glow")
      absorbBar.bg:SetAlpha(0.2)
      absorbBar.texture = absorbBar:CreateTexture(nil, "OVERLAY", nil, -8)
      absorbBar.texture:SetPoint("TOPLEFT")
      absorbBar.texture:SetPoint("TOPRIGHT")
      absorbBar.texture.maxHeight = absorbBar:GetHeight()
      absorbBar.texture:SetHeight(absorbBar.texture.maxHeight)
      absorbBar.texture:SetTexture("Interface\\AddOns\\rOrbs\\media\\orb_absorb_glow")
      self.TotalAbsorb = absorbBar
    end

    -------------------------------------------------------
    -- CONNECT TO oUF
    -- The fill StatusBar IS the oUF element
    -------------------------------------------------------
    if orbType == "POWER" then
      self.Power = fill
      ns.PowerOrb = orb
      self.Power.frequentUpdates = self.cfg.power.frequentUpdates or false
      self.Power.colorPower = orbcfg.filling.colorAuto or false
      self.Power.PostUpdate = postUpdate
      hooksecurefunc(fill, "SetStatusBarColor", function(_, r, g, b)
        updateStatusBarColor(fill, r, g, b)
      end)
    else
      self.Health = fill
      ns.HealthOrb = orb
      self.Health.frequentUpdates = self.cfg.health.frequentUpdates or false
      self.Health.colorClass = orbcfg.filling.colorAuto or false
      self.Health.colorHealth = orbcfg.filling.colorAuto or false
      self.Health.PostUpdate = postUpdate
      hooksecurefunc(fill, "SetStatusBarColor", function(_, r, g, b)
        updateStatusBarColor(fill, r, g, b)
      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)
        -- Update model nach Scale-Änderung
        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")
    -- Ensure parent frame never intercepts mouse – individual orbs handle it
    self:EnableMouse(false)
    self:SetHitRectInsets(0, 0, 0, 0)
    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, no oUF involvement
      local combatFrame = CreateFrame("Frame", "rOrbsPlayerCombatFrame", UIParent)
      combatFrame:SetFrameStrata("MEDIUM")
      combatFrame:SetFrameLevel(60)
      combatFrame:EnableMouse(false)
      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

      local function UpdateCombat()
        if rOrbsDB and rOrbsDB.playerCombatIconShow == false then
          combatFrame:Hide(); return
        end
        if UnitAffectingCombat("player") then combatFrame:Show() else combatFrame: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() UpdateCombat() end)
      UpdateCombat()

      -- RESTING ICON (animated zzz) – steal Blizzard's PlayerRestLoop
      -- Blizzard path: PlayerFrame.PlayerFrameContent.PlayerFrameContentContextual.PlayerRestLoop
      -- oUF reparents PlayerFrame to hiddenParent, so we grab the animated
      -- rest-loop frame out and reparent it to UIParent for our own use.
      local restFrame  -- will hold the reparented Blizzard frame (or a static fallback)
      local blizzRest = PlayerFrame
          and PlayerFrame.PlayerFrameContent
          and PlayerFrame.PlayerFrameContent.PlayerFrameContentContextual
          and PlayerFrame.PlayerFrameContent.PlayerFrameContentContextual.PlayerRestLoop

      if blizzRest then
        -- Pull it out of the hidden PlayerFrame hierarchy
        blizzRest:SetParent(UIParent)
        blizzRest:SetFrameStrata("MEDIUM")
        blizzRest:SetFrameLevel(60)
        blizzRest:EnableMouse(false)
        blizzRest:ClearAllPoints()

        -- Use SetScale for sizing – the Blizzard frame has its own intrinsic
        -- dimensions and internal animation textures, so scaling is cleaner.
        local baseSize = blizzRest:GetWidth()
        if baseSize and baseSize > 0 then
          local desired = rOrbsDB.playerRestingIconSize or 28
          blizzRest:SetScale(desired / baseSize)
        end
        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)
        restFrame:EnableMouse(false)
        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
        -- Color all child textures (RestTexture and any animation flipbook textures)
        for _, region in ipairs({ f:GetRegions() }) do
          if region.SetVertexColor then
            region:SetVertexColor(r, g, b)
          end
        end
        -- Also recurse one level into child frames (Blizzard nests textures)
        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
      -- Apply saved color on init
      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
          -- Scale-based sizing for the animated Blizzard frame
          local baseW = blizzRest:GetWidth() / (blizzRest:GetScale() or 1)
          if baseW and baseW > 0 then
            local desired = rOrbsDB.playerRestingIconSize or 28
            blizzRest:SetScale(desired / baseW)
          end
        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

      local function UpdateResting()
        if rOrbsDB and rOrbsDB.playerRestingIconShow == false then
          restFrame:Hide(); return
        end
        if IsResting() then
          restFrame:Show()
          -- Re-apply saved vertex color
          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
            -- Also check animation groups on the frame itself
            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
      local restListener = CreateFrame("Frame", nil, UIParent)
      restListener:RegisterEvent("PLAYER_UPDATE_RESTING")
      restListener:RegisterEvent("PLAYER_ENTERING_WORLD")
      restListener:SetScript("OnEvent", function() UpdateResting() end)
      UpdateResting()
    end)
    -- ----------------------------------------------------------------
    -- ANGEL TEXTURE  (left of Health orb)  /  DEMON TEXTURE (right of Power orb)
    -- Parented to UIParent so they are never clipped by orb clip frames.
    -- Size and position are driven by rOrbsDB keys so the GUI sliders work.
    -- ----------------------------------------------------------------

    -- Helper: apply current DB offsets/scale to one figure frame
    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 figSize = math.floor(orbSize * 1.3 * scale)
        figFrame:SetSize(figSize, figSize)
        figFrame:ClearAllPoints()
        if anchorSide == "LEFT" then
            -- Angel: RIGHT edge of figure touches LEFT area of health orb
            figFrame:SetPoint("RIGHT", orbFrame, "LEFT", math.floor(orbSize * 0.22) + offX, offY)
        else
            -- Demon: LEFT edge of figure touches RIGHT area of power orb
            figFrame:SetPoint("LEFT", orbFrame, "RIGHT", -math.floor(orbSize * 0.22) + offX, offY)
        end
    end

    -- Store on ns so GUI can call it after slider change
    ns.UpdateFigureLayout = UpdateFigureLayout

    -- ANGEL (NelsonNeto_angel_left) – left of health orb
    local angelFrame = CreateFrame("Frame", "rOrbsAngelFrame", UIParent)
    angelFrame:SetFrameStrata("MEDIUM")
    angelFrame:SetFrameLevel(50)
    angelFrame:EnableMouse(false)
    local angelTex = angelFrame:CreateTexture(nil, "ARTWORK")
    angelTex:SetAllPoints(angelFrame)
    angelTex:SetTexture("Interface\\AddOns\\rOrbs\\media\\NelsonNeto_angel_left")
    UpdateFigureLayout(angelFrame, ns.HealthOrb, "LEFT",
        rOrbsDB and rOrbsDB.angelScale,
        rOrbsDB and rOrbsDB.angelOffX,
        rOrbsDB and rOrbsDB.angelOffY)
    if rOrbsDB and rOrbsDB.healthAngelShow == false then angelFrame:Hide() end
    ns.AngelFrame = angelFrame

    -- DEMON (NelsonNeto_angel_right) – right of power orb
    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("Interface\\AddOns\\rOrbs\\media\\NelsonNeto_angel_right")
    UpdateFigureLayout(demonFrame, ns.PowerOrb, "RIGHT",
        rOrbsDB and rOrbsDB.demonScale,
        rOrbsDB and rOrbsDB.demonOffX,
        rOrbsDB and rOrbsDB.demonOffY)
    if rOrbsDB and rOrbsDB.powerDemonShow == false then demonFrame:Hide() end
    ns.DemonFrame = demonFrame
  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")
        -- SecureUnitButtonTemplate enables mouse by default. Disable it on
        -- the parent – individual health/power orb buttons handle all interaction.
        frame:EnableMouse(false)
        frame:SetHitRectInsets(0, 0, 0, 0)
        frame:UpdateAllElements("ForceUpdate")
        C_Timer.After(0.5, function()
          if frame and frame.UpdateAllElements then
            frame:UpdateAllElements("ForceUpdate")
          end
        end)
      end)
    end
  end

  ns:RegisterInit(Init)
