local addon, ns = ...

-- ============================================================
--  rOrbs – Settings GUI  (v4 – target tab, slider fix, font scale)
-- ============================================================

local L   = ns.L
local db  = nil
local LSM = LibStub("LibSharedMedia-3.0")

local IS_MAINLINE     = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE
local IS_NEW_SETTINGS = IS_MAINLINE and Settings and Settings.RegisterCanvasLayoutCategory

-- ============================================================
--  COLOR SCHEME  (rThreat-style red palette)
-- ============================================================
local C = {
    -- structure
    bg         = { 0.055, 0.055, 0.062, 0.98 },
    bgDeep     = { 0.040, 0.040, 0.046, 1.00 },
    sidebar    = { 0.045, 0.045, 0.052, 1.00 },
    content    = { 0.068, 0.068, 0.075, 0.97 },
    header     = { 0.10,  0.028, 0.028, 1.00 },
    border     = { 0.20,  0.20,  0.22,  1.00 },
    separator  = { 0.22,  0.22,  0.26,  0.55 },
    -- accent (red)
    accent     = { 0.90,  0.18,  0.18,  1.00 },
    accentHi   = { 1.00,  0.42,  0.42,  1.00 },
    accentDim  = { 0.55,  0.08,  0.08,  1.00 },
    -- tabs
    tabActive  = { 0.22,  0.04,  0.04,  1.00 },
    tabIdle    = { 0.045, 0.045, 0.052, 1.00 },
    tabHover   = { 0.09,  0.03,  0.03,  1.00 },
    -- text
    text       = { 0.90,  0.90,  0.92,  1.00 },
    textDim    = { 0.48,  0.48,  0.52,  1.00 },
    textBright = { 1.00,  1.00,  1.00,  1.00 },
    yellow     = { 1.00,  0.80,  0.20,  1.00 },
    green      = { 0.35,  0.82,  0.35,  1.00 },
    red        = { 0.90,  0.18,  0.18,  1.00 },
    -- slider
    sliderTrack = { 0.16, 0.16,  0.18,  1.00 },
    sliderFill  = { 0.90, 0.18,  0.18,  0.85 },
    sliderThumb = { 0.90, 0.18,  0.18,  1.00 },
    section    = { 0.90,  0.18,  0.18,  1.00 },
}

local PANEL_W  = 840
local PANEL_H  = 800
local TITLE_H  = 50
local BOT_H    = 40
local TAB_H    = 40
local PAD      = 18
local SLIDER_W = 240

-- ============================================================
--  BACKGROUND BUILDER
-- ============================================================
local function AddBackground(frame)
    local bg = frame:CreateTexture(nil, "BACKGROUND", nil, -8)
    bg:SetAllPoints()
    bg:SetColorTexture(C.bg[1], C.bg[2], C.bg[3], C.bg[4])
    frame._bgTex = bg

    local vig = frame:CreateTexture(nil, "BACKGROUND", nil, -6)
    vig:SetAllPoints()
    vig:SetColorTexture(0, 0, 0, 0.22)

    local function Edge(pt, rpt, w, h)
        local e = frame:CreateTexture(nil, "BORDER", nil, -7)
        e:SetColorTexture(C.border[1], C.border[2], C.border[3], 0.85)
        e:SetSize(w, h)
        e:SetPoint(pt, frame, rpt)
    end
    Edge("TOPLEFT",    "TOPLEFT",    PANEL_W, 1)
    Edge("BOTTOMLEFT", "BOTTOMLEFT", PANEL_W, 1)
    Edge("TOPLEFT",    "TOPLEFT",    1, PANEL_H)
    Edge("TOPRIGHT",   "TOPRIGHT",   1, PANEL_H)

    local stripe = frame:CreateTexture(nil, "ARTWORK", nil, 1)
    stripe:SetPoint("TOPLEFT")
    stripe:SetPoint("TOPRIGHT")
    stripe:SetHeight(3)
    stripe:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.9)
end

-- ============================================================
--  WIDGET HELPERS
-- ============================================================

local function Tip(owner, text)
    GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
    GameTooltip:SetText(text, C.text[1], C.text[2], C.text[3], 1)
    GameTooltip:Show()
end

local function MakeSep(parent, yOffset)
    local line = parent:CreateTexture(nil, "BACKGROUND")
    line:SetHeight(1)
    line:SetPoint("TOPLEFT",  parent, "TOPLEFT",  PAD, yOffset)
    line:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -PAD, yOffset)
    line:SetColorTexture(C.separator[1], C.separator[2], C.separator[3], C.separator[4])
    return line
end

local function MakeSectionHeader(parent, text, x, y, noTopLine)
    -- Subtle full-width separator line above the header (visual section divider)
    if not noTopLine then
        local sep = parent:CreateTexture(nil, "BACKGROUND")
        sep:SetHeight(1)
        sep:SetPoint("TOPLEFT",  parent, "TOPLEFT",  PAD, y + 14)
        sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -PAD, y + 14)
        sep:SetColorTexture(C.separator[1], C.separator[2], C.separator[3], 0.45)
    end

    local lbl = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    lbl:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    lbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3], 1)
    lbl:SetText(text)
    -- Gradient line from label to right edge
    local line = parent:CreateTexture(nil, "BACKGROUND")
    line:SetHeight(1)
    line:SetPoint("TOPLEFT",  lbl,    "BOTTOMLEFT",  0, -3)
    line:SetPoint("TOPRIGHT", parent, "TOPRIGHT",   -PAD, y)
    line:SetGradient("HORIZONTAL",
        CreateColor(C.accent[1], C.accent[2], C.accent[3], 0.7),
        CreateColor(0, 0, 0, 0))
    return lbl
end

local function MakeInfoText(parent, text, x, y, w)
    local lbl = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    lbl:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    lbl:SetWidth(w or (PANEL_W - CONTENT_X - PAD - SB_W - 30))
    lbl:SetJustifyH("LEFT")
    lbl:SetNonSpaceWrap(false)
    lbl:SetText(text)
    lbl:SetTextColor(C.textDim[1] * 1.3, C.textDim[2] * 1.3, C.textDim[3] * 1.3)
    return lbl
end

-- ---- rThreat-style Checkbox (plain Frame, identical to rThreat's ModernCheckbox) -------
-- desc (3rd arg = tooltip string) is shown as a small static text below the label,
-- exactly like rThreat. No GameTooltip popup. Border NEVER changes color when checked;
-- only the red chk-texture is shown/hidden. Border only goes red on hover.
local function MakeCheckbox(parent, labelText, desc, x, y)
    local con = CreateFrame("Frame", nil, parent)
    con:SetSize(16, 16)
    con:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    con:EnableMouse(true)

    -- Dark box background
    local boxBg = con:CreateTexture(nil, "BACKGROUND")
    boxBg:SetAllPoints()
    boxBg:SetColorTexture(0.07, 0.07, 0.08, 1)

    -- Border – stays gray always (only turns red on hover, like rThreat)
    local boxBdr = con:CreateTexture(nil, "BORDER")
    boxBdr:SetPoint("TOPLEFT",     con, "TOPLEFT",     1, -1)
    boxBdr:SetPoint("BOTTOMRIGHT", con, "BOTTOMRIGHT", -1, 1)
    boxBdr:SetColorTexture(C.border[1], C.border[2], C.border[3], 1)

    -- Red checkmark (hidden by default, shown when checked)
    local chk = con:CreateTexture(nil, "OVERLAY")
    chk:SetSize(11, 11)
    chk:SetPoint("CENTER")
    chk:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
    chk:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 1)
    chk:Hide()

    -- Main label (right of box)
    local lbl = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    lbl:SetPoint("LEFT", con, "RIGHT", 9, 0)
    lbl:SetText(labelText)
    lbl:SetTextColor(C.text[1], C.text[2], C.text[3])
    con.label = lbl

    -- Description text below label (like rThreat's desc)
    if desc and desc ~= "" then
        local d = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        d:SetPoint("TOPLEFT", lbl, "BOTTOMLEFT", 0, -2)
        d:SetPoint("RIGHT", parent, "RIGHT", -PAD, 0)
        d:SetJustifyH("LEFT")
        d:SetTextColor(C.textDim[1], C.textDim[2], C.textDim[3])
        d:SetText(desc)
        con.descLabel = d
    end

    -- Internal state
    local _checked = false
    local _onClick = nil

    -- SyncVisual: ONLY show/hide chk. Border stays gray (rThreat behavior).
    local function SyncVisual(on)
        _checked = on
        if on then chk:Show() else chk:Hide() end
    end

    -- Public API
    con.GetChecked = function(self) return _checked end
    con.SetChecked = function(self, val)
        SyncVisual(val and val ~= false)
    end
    con.SetScript = function(self, event, fn)
        if event == "OnClick" then
            _onClick = fn
        else
            getmetatable(self).__index.SetScript(self, event, fn)
        end
    end

    -- Hover: border → red (only on hover, NOT on checked state)
    con:SetScript("OnEnter", function(self)
        boxBdr:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.8)
        lbl:SetTextColor(C.accentHi[1], C.accentHi[2], C.accentHi[3], 1)
    end)
    con:SetScript("OnLeave", function()
        boxBdr:SetColorTexture(C.border[1], C.border[2], C.border[3], 1)
        lbl:SetTextColor(C.text[1], C.text[2], C.text[3])
    end)
    con:SetScript("OnMouseDown", function(self, btn)
        if btn == "LeftButton" then
            SyncVisual(not _checked)
            if _onClick then _onClick(con) end
        end
    end)

    return con
end

-- ---- Modern Slider (rThreat-style: full-width track) --------
-- Returns: slider, label
local function MakeSlider(parent, labelText, tipText, minV, maxV, step, x, y, width)
    -- Label row
    local lbl = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    lbl:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    lbl:SetText(labelText)
    lbl:SetTextColor(C.text[1], C.text[2], C.text[3])
    lbl:SetJustifyH("LEFT")

    -- Value label pinned to top-right
    local valLabel = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    valLabel:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -PAD, y - 1)
    valLabel:SetTextColor(C.accent[1], C.accent[2], C.accent[3], 1)

    -- Track texture spanning full width like rThreat (6px inset each side)
    local TRACK_INSET = 6
    local track = parent:CreateTexture(nil, "BACKGROUND")
    track:SetHeight(3)
    track:SetPoint("TOPLEFT",  parent, "TOPLEFT",  x + TRACK_INSET,  y - 36)
    track:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -(PAD - TRACK_INSET), y - 36)
    track:SetColorTexture(0.16, 0.16, 0.18, 1)

    local fill = parent:CreateTexture(nil, "ARTWORK")
    fill:SetHeight(3)
    fill:SetPoint("LEFT", track, "LEFT", 0, 0)
    fill:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.85)

    -- Invisible Slider frame anchored to the track
    local s = CreateFrame("Slider", nil, parent)
    s:SetHeight(18)
    s:SetPoint("LEFT",  track, "LEFT",  0, 0)
    s:SetPoint("RIGHT", track, "RIGHT", 0, 0)
    s:SetMinMaxValues(minV, maxV)
    s:SetValueStep(step)
    s:SetObeyStepOnDrag(true)
    s:SetOrientation("HORIZONTAL")

    local thumb = parent:CreateTexture(nil, "OVERLAY")
    thumb:SetSize(10, 14)
    thumb:SetTexture("Interface\\Buttons\\WHITE8X8")
    thumb:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 1)
    s:SetThumbTexture(thumb)

    s.valLabel = valLabel
    s.fillTex  = fill

    local function UpdateFill()
        local lo, hi = s:GetMinMaxValues()
        local pct = math.max(0, (s:GetValue() - lo) / math.max(hi - lo, 0.001))
        local w = track:GetWidth()
        if w and w > 0 then fill:SetWidth(w * pct) end
    end

    local function UpdateVal()
        local v = s:GetValue()
        local fmt = (step < 1) and "%.2f" or "%d"
        valLabel:SetText(string.format(fmt, v))
        UpdateFill()
    end

    s._updateVisuals = function(slider, val) UpdateVal() end
    s._refresh       = function(val) s:SetValue(val); UpdateVal() end

    s:SetScript("OnValueChanged", function(self, val) UpdateVal() end)
    s:SetScript("OnSizeChanged",  function() UpdateFill() end)
    s:SetScript("OnEnter", function(self)
        thumb:SetVertexColor(C.accentHi[1], C.accentHi[2], C.accentHi[3], 1)
        if tipText then Tip(self, tipText) end
    end)
    s:SetScript("OnLeave", function(self)
        thumb:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 1)
        GameTooltip:Hide()
    end)
    s:SetValue(minV)
    UpdateVal()
    return s, lbl
end

-- Helper: Set slider OnValueChanged WITH visual update
local function SetSliderHandler(slider, handler)
    slider:SetScript("OnValueChanged", function(self, val)
        if self._updateVisuals then self._updateVisuals(self, val) end
        handler(self, val)
    end)
end

-- ============================================================
--  CUSTOM DROPDOWN  (replaces UIDropDownMenuTemplate)
-- ============================================================
-- Shim state: one "current" dd during Initialize calls
local _ddCurrentInit = nil
local _ddItems       = {}   -- dd → list of {text, value, func, disabled}
local _ddValue       = {}   -- dd → current selected value
local _ddOpenFrame   = nil  -- only one popup open at a time

-- Shim functions that replace the global UIDropDownMenu_* API locally.
-- The existing call-sites are unchanged; these locals shadow the globals.
local function UIDropDownMenu_CreateInfo()
    return {}
end

local function UIDropDownMenu_AddButton(info, level)
    if _ddCurrentInit then
        tinsert(_ddItems[_ddCurrentInit], {
            text     = info.text or "",
            value    = info.value,
            func     = info.func,
            disabled = info.disabled,
        })
    end
end

local function UIDropDownMenu_SetText(dd, text)
    if dd and dd._labelText then dd._labelText:SetText(text or "") end
end

local function UIDropDownMenu_SetSelectedValue(dd, val)
    if dd then _ddValue[dd] = val end
end

local function UIDropDownMenu_GetSelectedValue(dd)
    return dd and _ddValue[dd]
end

-- Close any open popup
local function CloseDropdownPopup()
    if _ddOpenFrame then
        _ddOpenFrame:Hide()
        _ddOpenFrame = nil
    end
end

-- Build/show the popup list for a dropdown
local function ShowDropdownPopup(dd)
    CloseDropdownPopup()

    local items  = _ddItems[dd] or {}
    if #items == 0 then return end

    local ddW    = dd._width or 220
    local itemH  = 22
    local visMax = 15   -- max items before scrolling
    local needsScroll = #items > visMax
    local scrollBarW = needsScroll and 6 or 0
    local popH   = math.min(#items, visMax) * itemH + 4

    local pop = CreateFrame("Frame", nil, UIParent)
    pop:SetFrameStrata("TOOLTIP")
    pop:SetFrameLevel(200)
    pop:SetSize(ddW, popH)
    pop:SetPoint("TOPLEFT", dd._btn, "BOTTOMLEFT", 0, -2)
    pop:SetClipsChildren(true)

    -- Background
    local bg = pop:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(C.bgDeep[1], C.bgDeep[2], C.bgDeep[3], 0.98)
    -- Border
    local function PopEdge(pt, rpt, w, h)
        local e = pop:CreateTexture(nil, "BORDER")
        e:SetColorTexture(C.border[1], C.border[2], C.border[3], 0.90)
        e:SetSize(w, h)
        e:SetPoint(pt, pop, rpt, 0, 0)
    end
    PopEdge("TOPLEFT",     "TOPLEFT",     1, popH)
    PopEdge("TOPRIGHT",    "TOPRIGHT",    1, popH)
    PopEdge("TOPLEFT",     "TOPLEFT",     ddW, 1)
    PopEdge("BOTTOMLEFT",  "BOTTOMLEFT",  ddW, 1)

    -- Scroll frame for many items
    local sf   = CreateFrame("ScrollFrame", nil, pop)
    sf:SetPoint("TOPLEFT",     pop, "TOPLEFT",     1, -2)
    sf:SetPoint("BOTTOMRIGHT", pop, "BOTTOMRIGHT", -(1 + scrollBarW), 2)
    local content = CreateFrame("Frame", nil, sf)
    content:SetSize(ddW - 2 - scrollBarW, #items * itemH)
    sf:SetScrollChild(content)
    sf:SetVerticalScroll(0)
    -- Enable mouse wheel scrolling within the dropdown popup
    sf:EnableMouseWheel(true)
    pop:EnableMouseWheel(true)

    -- Scrollbar track + thumb (only when list exceeds visMax)
    local scrollThumb = nil
    local maxScroll = math.max(0, #items * itemH - popH)
    if needsScroll then
        -- Track
        local sbTrack = pop:CreateTexture(nil, "ARTWORK")
        sbTrack:SetWidth(scrollBarW)
        sbTrack:SetPoint("TOPRIGHT",    pop, "TOPRIGHT",    -1,  -2)
        sbTrack:SetPoint("BOTTOMRIGHT", pop, "BOTTOMRIGHT", -1,   2)
        sbTrack:SetColorTexture(C.bgDeep[1]*2, C.bgDeep[2]*2, C.bgDeep[3]*2, 1)
        -- Thumb
        scrollThumb = pop:CreateTexture(nil, "OVERLAY")
        scrollThumb:SetWidth(scrollBarW - 1)
        local thumbH = math.max(14, (popH / (#items * itemH)) * (popH - 4))
        scrollThumb:SetHeight(thumbH)
        scrollThumb:SetPoint("TOPRIGHT", pop, "TOPRIGHT", -1.5, -2)
        scrollThumb:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.85)
    end

    local function UpdateScrollThumb()
        if not scrollThumb then return end
        local cur = sf:GetVerticalScroll()
        local pct = maxScroll > 0 and (cur / maxScroll) or 0
        local thumbH = scrollThumb:GetHeight()
        local travelRange = popH - 4 - thumbH
        scrollThumb:SetPoint("TOPRIGHT", pop, "TOPRIGHT", -1.5, -(2 + pct * travelRange))
    end

    local function OnDropdownScroll(_, delta)
        local current  = sf:GetVerticalScroll()
        local newScroll = math.max(0, math.min(maxScroll, current - delta * itemH * 2))
        sf:SetVerticalScroll(newScroll)
        UpdateScrollThumb()
    end
    sf:SetScript("OnMouseWheel", OnDropdownScroll)
    pop:SetScript("OnMouseWheel", OnDropdownScroll)

    for idx, item in ipairs(items) do
        local row = CreateFrame("Button", nil, content)
        row:SetSize(ddW - 2, itemH)
        row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(idx - 1) * itemH)

        local rowBg = row:CreateTexture(nil, "BACKGROUND")
        rowBg:SetAllPoints()
        rowBg:SetColorTexture(0, 0, 0, 0)

        local rowTxt = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        rowTxt:SetPoint("LEFT", row, "LEFT", 8, 0)
        rowTxt:SetPoint("RIGHT", row, "RIGHT", -4, 0)
        rowTxt:SetJustifyH("LEFT")
        rowTxt:SetText(item.text)
        if item.disabled then
            rowTxt:SetTextColor(C.textDim[1], C.textDim[2], C.textDim[3])
        else
            rowTxt:SetTextColor(C.text[1], C.text[2], C.text[3])
        end

        -- Highlight current selection
        if _ddValue[dd] ~= nil and _ddValue[dd] == item.value then
            rowBg:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.20)
            rowTxt:SetTextColor(C.yellow[1], C.yellow[2], C.yellow[3])
        end

        if not item.disabled then
            row:SetScript("OnEnter", function()
                rowBg:SetColorTexture(C.accentDim[1], C.accentDim[2], C.accentDim[3], 0.35)
                rowTxt:SetTextColor(C.yellow[1], C.yellow[2], C.yellow[3])
            end)
            row:SetScript("OnLeave", function()
                if _ddValue[dd] ~= nil and _ddValue[dd] == item.value then
                    rowBg:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.20)
                    rowTxt:SetTextColor(C.yellow[1], C.yellow[2], C.yellow[3])
                else
                    rowBg:SetColorTexture(0, 0, 0, 0)
                    rowTxt:SetTextColor(C.text[1], C.text[2], C.text[3])
                end
            end)
            row:SetScript("OnClick", function()
                if item.func then item.func() end
                CloseDropdownPopup()
            end)
        end
    end

    -- Close when clicking outside
    pop:SetScript("OnHide", function() if _ddOpenFrame == pop then _ddOpenFrame = nil end end)
    local catch = CreateFrame("Frame", nil, UIParent)
    catch:SetAllPoints(UIParent)
    catch:SetFrameStrata("DIALOG")
    catch:SetFrameLevel(199)
    catch:EnableMouse(true)
    -- Do NOT enable mouse wheel on catch so scroll events pass through to the popup
    catch:EnableMouseWheel(false)
    catch:SetScript("OnMouseDown", function()
        CloseDropdownPopup()
        catch:Hide()
        catch:SetParent(nil)
    end)
    catch:Show()
    pop:SetScript("OnHide", function()
        if _ddOpenFrame == pop then _ddOpenFrame = nil end
        catch:Hide()
        catch:SetParent(nil)
    end)

    pop:Show()
    _ddOpenFrame = pop
end

local function UIDropDownMenu_Initialize(dd, fn)
    _ddItems[dd] = {}
    _ddCurrentInit = dd
    fn(dd, 1)
    _ddCurrentInit = nil
    -- Rebuild label if we have a selected value already
end

-- ---- Dropdown widget ----------------------------------------
local function MakeDropdown(parent, labelText, tipText, x, y, width)
    width = width or 220

    -- Label above
    local lbl = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    lbl:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    lbl:SetText(labelText)
    lbl:SetTextColor(C.text[1], C.text[2], C.text[3])

    local H = 22   -- button height

    -- Main button
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(width, H)
    btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y - 18)

    -- Button background
    local btnBg = btn:CreateTexture(nil, "BACKGROUND")
    btnBg:SetAllPoints()
    btnBg:SetColorTexture(C.bgDeep[1], C.bgDeep[2], C.bgDeep[3], 0.95)

    -- Border edges
    local function BtnEdge(pt, rpt, w, h)
        local e = btn:CreateTexture(nil, "BORDER")
        e:SetColorTexture(C.border[1], C.border[2], C.border[3], 0.85)
        e:SetSize(w, h)
        e:SetPoint(pt, btn, rpt, 0, 0)
    end
    BtnEdge("TOPLEFT",     "TOPLEFT",     1, H)
    BtnEdge("TOPRIGHT",    "TOPRIGHT",    1, H)
    BtnEdge("TOPLEFT",     "TOPLEFT",     width, 1)
    BtnEdge("BOTTOMLEFT",  "BOTTOMLEFT",  width, 1)

    -- Selected label text
    local selTxt = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    selTxt:SetPoint("LEFT",  btn, "LEFT",  8, 0)
    selTxt:SetPoint("RIGHT", btn, "RIGHT", -28, 0)
    selTxt:SetJustifyH("LEFT")
    selTxt:SetTextColor(C.text[1], C.text[2], C.text[3])
    selTxt:SetText("")

    -- Icon indicator (replaces text arrow) - right side box with skull icon
    local iconBox = btn:CreateTexture(nil, "BACKGROUND", nil, 1)
    iconBox:SetSize(H + 2, H + 2)
    iconBox:SetPoint("RIGHT", btn, "RIGHT", 1, 0)
    iconBox:SetColorTexture(C.bgDeep[1]*0.5, C.bgDeep[2]*0.5, C.bgDeep[3]*0.5, 1)

    local iconBoxBorder = btn:CreateTexture(nil, "BORDER", nil, 2)
    iconBoxBorder:SetSize(H + 2, H + 2)
    iconBoxBorder:SetPoint("RIGHT", btn, "RIGHT", 1, 0)
    iconBoxBorder:SetColorTexture(C.border[1], C.border[2], C.border[3], 0.90)

    local iconBoxInner = btn:CreateTexture(nil, "ARTWORK", nil, 3)
    iconBoxInner:SetSize(H, H)
    iconBoxInner:SetPoint("RIGHT", btn, "RIGHT", 0, 0)
    iconBoxInner:SetColorTexture(C.tabActive[1], C.tabActive[2], C.tabActive[3], 1)

    local iconTex = btn:CreateTexture(nil, "OVERLAY", nil, 4)
    iconTex:SetSize(H - 2, H - 2)
    iconTex:SetPoint("RIGHT", btn, "RIGHT", -1, 0)
    iconTex:SetTexture("Interface\\AddOns\\rOrbs\\media\\dropdown_icon.png")
    iconTex:SetAlpha(0.85)
    local arrow = iconTex  -- keep 'arrow' reference for hover scripts below

    -- The "dd" object is the btn itself; attach helpers
    -- IMPORTANT: dd must be defined BEFORE the SetScript closures reference it
    local dd = btn
    dd._btn       = btn
    dd._labelText = selTxt
    dd._width     = width
    _ddItems[dd]  = {}
    _ddValue[dd]  = nil

    -- Hover
    btn:SetScript("OnEnter", function()
        btnBg:SetColorTexture(C.tabHover[1], C.tabHover[2], C.tabHover[3], 0.95)
        arrow:SetAlpha(1.0)
        iconBoxInner:SetColorTexture(C.accentDim[1], C.accentDim[2], C.accentDim[3], 1)
    end)
    btn:SetScript("OnLeave", function()
        btnBg:SetColorTexture(C.bgDeep[1], C.bgDeep[2], C.bgDeep[3], 0.95)
        arrow:SetAlpha(0.85)
        iconBoxInner:SetColorTexture(C.tabActive[1], C.tabActive[2], C.tabActive[3], 1)
    end)

    btn:SetScript("OnClick", function()
        if _ddOpenFrame then
            CloseDropdownPopup()
        end
        ShowDropdownPopup(dd)
    end)

    if tipText then
        btn:SetScript("OnEnter", function(s)
            btnBg:SetColorTexture(C.tabHover[1], C.tabHover[2], C.tabHover[3], 0.95)
            arrow:SetAlpha(1.0)
            iconBoxInner:SetColorTexture(C.accentDim[1], C.accentDim[2], C.accentDim[3], 1)
            Tip(s, tipText)
        end)
        btn:SetScript("OnLeave", function()
            btnBg:SetColorTexture(C.bgDeep[1], C.bgDeep[2], C.bgDeep[3], 0.95)
            arrow:SetAlpha(0.85)
            iconBoxInner:SetColorTexture(C.tabActive[1], C.tabActive[2], C.tabActive[3], 1)
            GameTooltip:Hide()
        end)
        -- Restore OnClick after tipText block overwrote OnEnter/OnLeave
        btn:SetScript("OnClick", function()
            if _ddOpenFrame then
                CloseDropdownPopup()
            end
            ShowDropdownPopup(dd)
        end)
    end

    return dd, lbl
end

-- ---- Color Swatch --------------------------------------------
local function MakeColorSwatch(parent, x, y, r, g, b, labelText, onClick)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(22, 22)
    btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)

    local border = btn:CreateTexture(nil, "BACKGROUND", nil, -1)
    border:SetAllPoints()
    border:SetColorTexture(C.border[1], C.border[2], C.border[3], 1)

    local fill = btn:CreateTexture(nil, "ARTWORK", nil, 0)
    fill:SetPoint("TOPLEFT",     btn, "TOPLEFT",     2, -2)
    fill:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
    fill:SetColorTexture(r or 1, g or 1, b or 1, 1)
    btn.fill = fill

    if labelText then
        local lbl = btn:GetParent():CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        lbl:SetPoint("LEFT", btn, "RIGHT", 6, 0)
        lbl:SetText(labelText)
        lbl:SetTextColor(C.textDim[1], C.textDim[2], C.textDim[3])
    end

    btn:SetScript("OnClick", onClick)
    btn:SetScript("OnEnter", function(s)
        Tip(s, L["ORB_COLOR_TIP"])
    end)
    btn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    btn.SetColor = function(self, nr, ng, nb)
        r, g, b = nr, ng, nb
        fill:SetColorTexture(nr, ng, nb, 1)
    end
    return btn
end

-- ---- Styled button -------------------------------------------
local function MakeButton(parent, text, x, y, w, h, r, g, b)
    r, g, b = r or C.accentDim[1], g or C.accentDim[2], b or C.accentDim[3]
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(w or 180, h or 26)
    btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)

    local bgTex = btn:CreateTexture(nil, "BACKGROUND")
    bgTex:SetAllPoints()
    bgTex:SetColorTexture(r * 0.6, g * 0.6, b * 0.6, 1)

    local borderTex = btn:CreateTexture(nil, "BORDER")
    borderTex:SetAllPoints()
    borderTex:SetColorTexture(C.border[1], C.border[2], C.border[3], 0.8)

    local innerTex = btn:CreateTexture(nil, "ARTWORK")
    innerTex:SetPoint("TOPLEFT",     btn, "TOPLEFT",     1, -1)
    innerTex:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 1)
    innerTex:SetColorTexture(r, g, b, 1)
    btn.innerTex = innerTex

    local fs = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    fs:SetAllPoints()
    fs:SetJustifyH("CENTER")
    fs:SetText(text)
    fs:SetTextColor(C.text[1], C.text[2], C.text[3])
    btn.fs = fs

    btn:SetScript("OnEnter", function(self)
        self.innerTex:SetColorTexture(math.min(1,r*1.4), math.min(1,g*1.5), math.min(1,b*1.5), 1)
    end)
    btn:SetScript("OnLeave", function(self)
        self.innerTex:SetColorTexture(r, g, b, 1)
    end)
    btn:SetScript("OnMouseDown", function(self) fs:SetPoint("CENTER", 1, -1) end)
    btn:SetScript("OnMouseUp",   function(self) fs:SetPoint("CENTER", 0, 0) end)
    return btn
end

-- ============================================================
--  MAIN PANEL
-- ============================================================

local Panel         = nil
local contentFrames = {}
local activeTab     = "general"

local function CreateMainPanel()
    if Panel then return end

    Panel = CreateFrame("Frame", "rOrbsSettingsPanel", UIParent, "BackdropTemplate")
    Panel:SetSize(PANEL_W, PANEL_H)
    Panel:SetPoint("CENTER")
    Panel:SetFrameStrata("DIALOG")
    Panel:SetFrameLevel(100)
    Panel:SetMovable(true)
    Panel:EnableMouse(true)
    Panel:RegisterForDrag("LeftButton")
    Panel:SetScript("OnDragStart", function(self) self:StartMoving() end)
    Panel:SetScript("OnDragStop",  function(self) self:StopMovingOrSizing() end)
    Panel:SetScript("OnKeyDown", function(self, key)
        if key == "ESCAPE" then ns.GUI:Close() end
    end)
    Panel:SetPropagateKeyboardInput(true)
    Panel:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8",
        tile=false, edgeSize=2, insets={left=0,right=0,top=0,bottom=0}})
    Panel:SetBackdropColor(C.bg[1], C.bg[2], C.bg[3], C.bg[4])
    Panel:SetBackdropBorderColor(C.accent[1]*0.6, C.accent[2]*0.12, C.accent[3]*0.12, 1)
    Panel:SetAlpha(0)
    Panel:Hide()
    tinsert(UISpecialFrames, "rOrbsSettingsPanel")

    -- ── TITLE BAR ─────────────────────────────────────────────────────────────
    local titleBg = CreateFrame("Frame", nil, Panel, "BackdropTemplate")
    titleBg:SetPoint("TOPLEFT",  Panel, "TOPLEFT",   2, -2)
    titleBg:SetPoint("TOPRIGHT", Panel, "TOPRIGHT",  -2, -2)
    titleBg:SetHeight(TITLE_H)
    titleBg:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8",
        tile=false, edgeSize=1, insets={left=0,right=0,top=0,bottom=0}})
    titleBg:SetBackdropColor(C.accent[1]*0.19, C.accent[2]*0.06, C.accent[3]*0.06, 1)
    titleBg:SetBackdropBorderColor(C.accent[1]*0.45, 0.05, 0.05, 1)

    -- Left red accent stripe
    local ta = titleBg:CreateTexture(nil, "ARTWORK")
    ta:SetWidth(3)
    ta:SetPoint("TOPLEFT",    titleBg, "TOPLEFT",    0, 0)
    ta:SetPoint("BOTTOMLEFT", titleBg, "BOTTOMLEFT", 0, 0)
    ta:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 1)

    -- Logo
    local logoTex = titleBg:CreateTexture(nil, "OVERLAY")
    logoTex:SetTexture("Interface\\AddOns\\rOrbs\\media\\d2_skull")
    logoTex:SetSize(32, 32)
    logoTex:SetPoint("LEFT", titleBg, "LEFT", 14, 0)

    -- Title text (right of logo)
    local title = titleBg:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
    title:SetPoint("LEFT", logoTex, "RIGHT", 10, 0)
    title:SetTextColor(1, 1, 1, 1)
    title:SetText(L["SETTINGS_TITLE"])

    -- Bottom gradient accent line
    local tgl = titleBg:CreateTexture(nil, "ARTWORK")
    tgl:SetHeight(2)
    tgl:SetPoint("BOTTOMLEFT",  titleBg, "BOTTOMLEFT",  0, 0)
    tgl:SetPoint("BOTTOMRIGHT", titleBg, "BOTTOMRIGHT", 0, 0)
    tgl:SetGradient("HORIZONTAL",
        CreateColor(C.accent[1], C.accent[2], C.accent[3], 1),
        CreateColor(C.accent[1]*0.2, 0, 0, 0))

    -- Close button (standard WoW X button)
    local closeBtn = CreateFrame("Button", nil, Panel, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", Panel, "TOPRIGHT", 0, 0)
    closeBtn:SetScript("OnClick", function() ns.GUI:Close() end)

    -- ── SIDEBAR ───────────────────────────────────────────────────────────────
    local SIDEBAR_W = 180
    local HEADER_H  = TITLE_H

    local sidebar = CreateFrame("Frame", nil, Panel, "BackdropTemplate")
    sidebar:SetPoint("TOPLEFT",    titleBg, "BOTTOMLEFT",   2,  0)
    sidebar:SetPoint("BOTTOMLEFT", Panel,   "BOTTOMLEFT",   2,  BOT_H+2)
    sidebar:SetWidth(SIDEBAR_W)
    sidebar:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8"})
    sidebar:SetBackdropColor(C.sidebar[1], C.sidebar[2], C.sidebar[3], C.sidebar[4])

    -- Right border line of sidebar
    local sbl = sidebar:CreateTexture(nil, "BORDER")
    sbl:SetWidth(1)
    sbl:SetPoint("TOPRIGHT",    sidebar, "TOPRIGHT",    0,  0)
    sbl:SetPoint("BOTTOMRIGHT", sidebar, "BOTTOMRIGHT", 0,  0)
    sbl:SetColorTexture(C.accent[1]*0.45, 0.04, 0.04, 1)

    -- ── CONTENT AREA ──────────────────────────────────────────────────────────
    local contentBg = CreateFrame("Frame", nil, Panel, "BackdropTemplate")
    contentBg:SetPoint("TOPLEFT",     sidebar,  "TOPRIGHT",    1,  0)
    contentBg:SetPoint("BOTTOMRIGHT", Panel,    "BOTTOMRIGHT", -2, BOT_H+2)
    contentBg:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8",
        tile=false, edgeSize=1, insets={left=0,right=0,top=0,bottom=0}})
    contentBg:SetBackdropColor(C.content[1], C.content[2], C.content[3], C.content[4])
    contentBg:SetBackdropBorderColor(C.border[1], C.border[2], C.border[3], 0.5)

    -- Content header (shows active tab name)
    local contentHeader = contentBg:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    contentHeader:SetPoint("TOPLEFT", contentBg, "TOPLEFT", PAD, -12)
    contentHeader:SetTextColor(C.accent[1], C.accent[2], C.accent[3], 0.7)
    contentHeader:SetText("")
    Panel.contentHeader = contentHeader

    local chLine = contentBg:CreateTexture(nil, "BACKGROUND")
    chLine:SetHeight(1)
    chLine:SetPoint("TOPLEFT",  contentBg, "TOPLEFT",  PAD,  -32)
    chLine:SetPoint("TOPRIGHT", contentBg, "TOPRIGHT", -PAD, -32)
    chLine:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.20)

    -- ── BOTTOM BAR ────────────────────────────────────────────────────────────
    local bot = CreateFrame("Frame", nil, Panel, "BackdropTemplate")
    bot:SetPoint("BOTTOMLEFT",  Panel, "BOTTOMLEFT",   2, 2)
    bot:SetPoint("BOTTOMRIGHT", Panel, "BOTTOMRIGHT", -2, 2)
    bot:SetHeight(BOT_H)
    bot:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8",
        tile=false, edgeSize=1, insets={left=0,right=0,top=0,bottom=0}})
    bot:SetBackdropColor(0.04, 0.04, 0.046, 1)
    bot:SetBackdropBorderColor(C.border[1], C.border[2], C.border[3], 0.5)

    -- Top accent line on bottom bar
    local botLine = bot:CreateTexture(nil, "ARTWORK")
    botLine:SetHeight(1)
    botLine:SetPoint("TOPLEFT",  bot, "TOPLEFT",  0, 0)
    botLine:SetPoint("TOPRIGHT", bot, "TOPRIGHT", 0, 0)
    botLine:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.35)

    -- ── GUI Scale stepper (rThreat-style: ◄ 1.00 ►) ─────────────────────────
    do
        local SCALE_STEP = 0.05
        local SCALE_MIN  = 0.5
        local SCALE_MAX  = 2.0

        local scaleLbl = bot:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        scaleLbl:SetPoint("LEFT", bot, "LEFT", 10, 0)
        scaleLbl:SetTextColor(unpack(C.textDim))
        scaleLbl:SetText("GUI Scale")

        local function GetScale()  return (rOrbsDB and rOrbsDB.guiScale) or 1.0 end
        local function ApplyScale(v)
            v = math.max(SCALE_MIN, math.min(SCALE_MAX, v))
            v = math.floor(v / SCALE_STEP + 0.5) * SCALE_STEP
            v = math.floor(v * 100 + 0.5) / 100
            if rOrbsDB then rOrbsDB.guiScale = v end
            Panel:SetScale(v)
        end

        local valBox = CreateFrame("EditBox", nil, bot, "BackdropTemplate")
        valBox:SetSize(46, 22)
        valBox:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8x8", edgeFile="Interface\\Buttons\\WHITE8x8", edgeSize=1,
            insets={left=0,right=0,top=0,bottom=0}})
        valBox:SetBackdropColor(C.accent[1]*0.06, 0.01, 0.01, 0.9)
        valBox:SetBackdropBorderColor(C.accent[1]*0.4, 0.05, 0.05, 1)
        valBox:SetFont("Fonts\\FRIZQT__.TTF", 11, "")
        valBox:SetTextColor(1, 0.9, 0.8, 1)
        valBox:SetJustifyH("CENTER")
        valBox:SetMaxLetters(5)
        valBox:SetAutoFocus(false)
        valBox:EnableMouseWheel(true)

        local function SyncBox()
            valBox:SetText(string.format("%.2f", GetScale()))
            Panel:SetScale(GetScale())
        end

        local function MakeArrowBtn(label, dir)
            local b = CreateFrame("Button", nil, bot, "BackdropTemplate")
            b:SetSize(20, 22)
            b:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8x8", edgeFile="Interface\\Buttons\\WHITE8x8", edgeSize=1,
                insets={left=1,right=1,top=1,bottom=1}})
            b:SetBackdropColor(C.accent[1]*0.10, 0.02, 0.02, 1)
            b:SetBackdropBorderColor(C.accent[1]*0.5, 0.05, 0.05, 1)
            local t = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            t:SetAllPoints(); t:SetText(label)
            t:SetTextColor(C.accentHi[1], C.accentHi[2], C.accentHi[3], 1)
            b:SetScript("OnEnter", function(s)
                s:SetBackdropColor(C.accent[1]*0.18, 0.04, 0.04, 1)
                s:SetBackdropBorderColor(C.accent[1], 0.10, 0.10, 1)
            end)
            b:SetScript("OnLeave", function(s)
                s:SetBackdropColor(C.accent[1]*0.10, 0.02, 0.02, 1)
                s:SetBackdropBorderColor(C.accent[1]*0.5, 0.05, 0.05, 1)
                s.held = false
            end)
            local ticker
            b:SetScript("OnClick", function() ApplyScale(GetScale() + dir*SCALE_STEP); SyncBox() end)
            b:SetScript("OnMouseDown", function()
                b.held = true
                C_Timer.After(0.4, function()
                    if b.held then
                        ticker = C_Timer.NewTicker(0.08, function()
                            if b.held then ApplyScale(GetScale() + dir*SCALE_STEP); SyncBox()
                            else ticker:Cancel(); ticker = nil end
                        end)
                    end
                end)
            end)
            b:SetScript("OnMouseUp", function() b.held=false; if ticker then ticker:Cancel(); ticker=nil end end)
            return b
        end

        local btnMinus = MakeArrowBtn("<", -1)
        btnMinus:SetPoint("LEFT", scaleLbl, "RIGHT", 8, 0)
        valBox:SetPoint("LEFT", btnMinus, "RIGHT", 2, 0)
        local btnPlus = MakeArrowBtn(">", 1)
        btnPlus:SetPoint("LEFT", valBox, "RIGHT", 2, 0)

        valBox:SetScript("OnMouseWheel", function(_, d) ApplyScale(GetScale() + d*SCALE_STEP); SyncBox() end)
        local function CommitTyped()
            local v = tonumber(valBox:GetText())
            if v then ApplyScale(v) end
            SyncBox(); valBox:ClearFocus()
        end
        valBox:SetScript("OnEnterPressed",  CommitTyped)
        valBox:SetScript("OnTabPressed",    CommitTyped)
        valBox:SetScript("OnEditFocusLost", CommitTyped)
        valBox:SetScript("OnEscapePressed", function() SyncBox(); valBox:ClearFocus() end)
        Panel:HookScript("OnShow", SyncBox)
        Panel.SyncScale = SyncBox
        SyncBox()
    end

    -- Close button (bottom bar)
    local function StyledBtn(parent, text, w, onClick)
        local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
        btn:SetSize(w or 90, 26)
        btn:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8",
            tile=false, edgeSize=1, insets={left=0,right=0,top=0,bottom=0}})
        btn:SetBackdropColor(C.accent[1]*0.18, C.accent[2]*0.06, C.accent[3]*0.06, 1)
        btn:SetBackdropBorderColor(C.accent[1]*0.70, 0.08, 0.08, 1)
        local lbl = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        lbl:SetPoint("CENTER"); lbl:SetText(text)
        lbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3], 1)
        btn:SetScript("OnEnter", function(s)
            s:SetBackdropColor(C.accent[1]*0.38, 0.06, 0.06, 1)
            s:SetBackdropBorderColor(C.accentHi[1], 0.15, 0.15, 1)
            lbl:SetTextColor(1, 1, 1, 1)
        end)
        btn:SetScript("OnLeave", function(s)
            s:SetBackdropColor(C.accent[1]*0.18, C.accent[2]*0.06, C.accent[3]*0.06, 1)
            s:SetBackdropBorderColor(C.accent[1]*0.70, 0.08, 0.08, 1)
            lbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3], 1)
        end)
        btn:SetScript("OnClick", onClick)
        return btn, lbl
    end

    StyledBtn(bot, L["SETTINGS_TITLE"] and "Close" or "Close", 90, function() ns.GUI:Close() end)
        :SetPoint("RIGHT", bot, "RIGHT", -12, 0)

    -- ── TAB DEFINITIONS (with icons) ─────────────────────────────────────────
    local tabData = {
        { key = "general",  label = L["TAB_GENERAL"],              icon = "Interface\\Icons\\INV_Misc_Note_06" },
        { key = "health",   label = L["TAB_HEALTH_ORB"],           icon = "Interface\\Icons\\spell_shadow_lifedrain" },
        { key = "power",    label = L["TAB_POWER_ORB"],            icon = "Interface\\Icons\\spell_shadow_manaburn" },
        { key = "target",   label = L["TAB_TARGET_ORB"],           icon = "Interface\\Icons\\Ability_Spy" },
        { key = "tot",      label = L["TAB_TOT_ORB"] or "ToT Orb", icon = "Interface\\Icons\\Ability_Rogue_Shadowstep" },
        { key = "models",   label = L["TAB_MODELS"] or "Models",   icon = "Interface\\Icons\\INV_Misc_Toy_08" },
    }
    Panel.tabs = {}

    -- Sidebar background (already created above as `sidebar`)
    -- Nav buttons
    local TAB_GAP = 2
    for i, td in ipairs(tabData) do
        local tab = CreateFrame("Button", nil, sidebar)
        tab:SetHeight(TAB_H)
        tab:SetPoint("TOPLEFT",  sidebar, "TOPLEFT",  0, -(i-1)*(TAB_H+TAB_GAP))
        tab:SetPoint("TOPRIGHT", sidebar, "TOPRIGHT", -1, -(i-1)*(TAB_H+TAB_GAP))

        -- Background texture
        local bbg = tab:CreateTexture(nil, "BACKGROUND"); bbg:SetAllPoints()
        bbg:SetColorTexture(C.tabIdle[1], C.tabIdle[2], C.tabIdle[3], 1)
        tab.bg = bbg

        -- Left accent bar (3px red) - hidden when inactive
        local bbar = tab:CreateTexture(nil, "OVERLAY")
        bbar:SetWidth(3)
        bbar:SetPoint("TOPLEFT",    tab, "TOPLEFT",    0, 0)
        bbar:SetPoint("BOTTOMLEFT", tab, "BOTTOMLEFT", 0, 0)
        bbar:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0)
        tab.underline = bbar

        -- Separator line at bottom
        local sep = tab:CreateTexture(nil, "BORDER"); sep:SetHeight(1)
        sep:SetPoint("BOTTOMLEFT",  tab, "BOTTOMLEFT",  0, 0)
        sep:SetPoint("BOTTOMRIGHT", tab, "BOTTOMRIGHT", 0, 0)
        sep:SetColorTexture(0.12, 0.12, 0.14, 1)

        -- Icon
        local icon = tab:CreateTexture(nil, "OVERLAY"); icon:SetSize(18, 18)
        icon:SetPoint("LEFT", tab, "LEFT", 14, 0)
        icon:SetTexture(td.icon)
        icon:SetVertexColor(0.50, 0.50, 0.50, 1)
        tab.icon = icon

        -- Label
        local bfs = tab:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        bfs:SetPoint("LEFT",  tab.icon, "RIGHT",  8, 0)
        bfs:SetPoint("RIGHT", tab,      "RIGHT", -6, 0)
        bfs:SetJustifyH("LEFT")
        bfs:SetText(td.label)
        bfs:SetTextColor(0.75, 0.73, 0.72, 1)
        tab.fs = bfs

        local ii = i
        tab:SetScript("OnClick", function() ns.GUI:SetTab(td.key) end)
        tab:SetScript("OnEnter", function(s)
            if activeTab ~= td.key then
                s.bg:SetColorTexture(C.tabHover[1], C.tabHover[2], C.tabHover[3], 1)
                s.fs:SetTextColor(C.accentHi[1], C.accentHi[2], C.accentHi[3], 1)
                s.icon:SetVertexColor(C.accent[1], C.accent[2], C.accent[3], 1)
            end
        end)
        tab:SetScript("OnLeave", function(s)
            if activeTab ~= td.key then
                s.bg:SetColorTexture(C.tabIdle[1], C.tabIdle[2], C.tabIdle[3], 1)
                s.fs:SetTextColor(0.75, 0.73, 0.72, 1)
                s.icon:SetVertexColor(0.50, 0.50, 0.50, 1)
            end
        end)
        tab.key    = td.key
        tab.label  = td.label
        Panel.tabs[td.key] = tab
    end

    -- ── SCROLL PANE BUILDER ───────────────────────────────────────────────────
    local contentTop = 0
    local CONTENT_X  = SIDEBAR_W + 6
    local SB_W = 10

    local function BuildScrollPane(key)
        local container = CreateFrame("Frame", nil, contentBg)
        container:SetPoint("TOPLEFT",     contentBg, "TOPLEFT",     2,     -38)
        container:SetPoint("BOTTOMRIGHT", contentBg, "BOTTOMRIGHT", -2,      4)
        container:Hide()

        -- Scrollbar track
        local sbTrack = CreateFrame("Button", nil, container)
        sbTrack:SetWidth(SB_W)
        sbTrack:SetPoint("TOPRIGHT",    container, "TOPRIGHT",    0, -2)
        sbTrack:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", 0,  2)
        local sbTrackBg = sbTrack:CreateTexture(nil, "BACKGROUND")
        sbTrackBg:SetAllPoints()
        sbTrackBg:SetColorTexture(0.06, 0.03, 0.03, 1)

        local sbBorderTex = container:CreateTexture(nil, "BORDER")
        sbBorderTex:SetWidth(1)
        sbBorderTex:SetPoint("TOPRIGHT",    container, "TOPRIGHT",    -SB_W, -2)
        sbBorderTex:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -SB_W,  2)
        sbBorderTex:SetColorTexture(C.border[1], C.border[2], C.border[3], 0.5)

        local sbThumb = CreateFrame("Button", nil, container)
        sbThumb:SetWidth(SB_W - 2)
        sbThumb:SetPoint("TOPRIGHT", container, "TOPRIGHT", -1, -2)
        local sbThumbBg = sbThumb:CreateTexture(nil, "BACKGROUND")
        sbThumbBg:SetAllPoints()
        sbThumbBg:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.70)
        local sbThumbHover = sbThumb:CreateTexture(nil, "ARTWORK")
        sbThumbHover:SetAllPoints()
        sbThumbHover:SetColorTexture(1, 0.5, 0.5, 0)
        sbThumb:SetScript("OnEnter", function() sbThumbHover:SetColorTexture(1,0.5,0.5,0.25) end)
        sbThumb:SetScript("OnLeave", function() sbThumbHover:SetColorTexture(1,0.5,0.5,0) end)

        local sf = CreateFrame("ScrollFrame", nil, container)
        sf:SetPoint("TOPLEFT",     container, "TOPLEFT",     0,        0)
        sf:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -(SB_W+2), 0)
        sf:EnableMouseWheel(true)

        local child = CreateFrame("Frame", nil, sf)
        child:SetWidth(PANEL_W - CONTENT_X - PAD - SB_W - 14)
        child:SetHeight(3200)
        sf:SetScrollChild(child)
        sf.child = child

        local function GetMaxScroll()
            local containerH = container:GetHeight() - 4
            local contentH   = child._contentH or child:GetHeight()
            return math.max(0, contentH - containerH)
        end

        local function UpdateThumb()
            local containerH = container:GetHeight() - 4
            local contentH   = child._contentH or child:GetHeight()
            if containerH <= 0 or contentH <= 0 then return end
            local ratio  = math.min(1, containerH / contentH)
            local thumbH = math.max(24, containerH * ratio)
            sbThumb:SetHeight(thumbH)
            local maxScroll = GetMaxScroll()
            local cur       = sf:GetVerticalScroll()
            local pct       = maxScroll > 0 and (cur / maxScroll) or 0
            local travel    = containerH - thumbH
            sbThumb:ClearAllPoints()
            sbThumb:SetPoint("TOPRIGHT", container, "TOPRIGHT", -1, -(2 + pct * travel))
            if ratio >= 1 then sbThumb:Hide() ; sbTrackBg:SetColorTexture(0.04,0.02,0.02,1)
            else sbThumb:Show() ; sbTrackBg:SetColorTexture(0.06,0.03,0.03,1) end
        end

        local function ScrollTo(newScroll)
            local maxS = GetMaxScroll()
            sf:SetVerticalScroll(math.max(0, math.min(maxS, newScroll)))
            UpdateThumb()
        end

        sf:SetScript("OnMouseWheel", function(self, delta) ScrollTo(sf:GetVerticalScroll() - delta * 40) end)
        sbTrack:EnableMouseWheel(true)
        sbTrack:SetScript("OnMouseWheel", function(self, delta) ScrollTo(sf:GetVerticalScroll() - delta * 40) end)
        sbTrack:SetScript("OnClick", function(self, btn)
            if btn ~= "LeftButton" then return end
            local _, trackY  = sbTrack:GetCenter()
            local _, mouseY  = GetCursorPosition()
            local scale      = UIParent:GetEffectiveScale()
            mouseY = mouseY / scale
            local containerH = container:GetHeight() - 4
            local thumbH     = sbThumb:GetHeight()
            local travel     = containerH - thumbH
            if travel <= 0 then return end
            local trackTop  = trackY + containerH / 2
            local clickOff  = trackTop - mouseY
            local pct       = math.max(0, math.min(1, (clickOff - thumbH/2) / travel))
            ScrollTo(pct * GetMaxScroll())
        end)

        local _dragging, _dragStartY, _dragStartScroll = false, 0, 0
        sbThumb:SetScript("OnMouseDown", function(self, btn)
            if btn ~= "LeftButton" then return end
            _dragging = true
            local _, y = GetCursorPosition()
            _dragStartY = y / UIParent:GetEffectiveScale()
            _dragStartScroll = sf:GetVerticalScroll()
            self:SetScript("OnUpdate", function()
                if not _dragging then self:SetScript("OnUpdate", nil); return end
                local _, cy = GetCursorPosition()
                cy = cy / UIParent:GetEffectiveScale()
                local dy = _dragStartY - cy
                local containerH = container:GetHeight() - 4
                local thumbH     = sbThumb:GetHeight()
                local travel     = math.max(1, containerH - thumbH)
                ScrollTo(_dragStartScroll + (dy / travel) * GetMaxScroll())
            end)
        end)
        sbThumb:SetScript("OnMouseUp", function(self) _dragging=false; self:SetScript("OnUpdate",nil) end)

        sf:SetScript("OnShow",               function() UpdateThumb() end)
        container:SetScript("OnShow",        function() UpdateThumb() end)
        container:SetScript("OnSizeChanged", function() UpdateThumb() end)
        child:SetScript("OnSizeChanged",     function() UpdateThumb() end)
        child._updateThumb = UpdateThumb

        contentFrames[key] = container
        container._sf      = sf
        return child
    end

    -- ============================================================
    --  TAB: GENERAL
    -- ============================================================
    local genPane = BuildScrollPane("general")
    local yg = -PAD

    -- Zentriertes rOrbs-Logo oben im General-Tab (wie rThreat)
    local genLogo = genPane:CreateTexture(nil, "ARTWORK")
    genLogo:SetTexture("Interface\\AddOns\\rOrbs\\media\\d2_skull")
    genLogo:SetSize(80, 80)
    genLogo:SetPoint("TOP", genPane, "TOP", 0, yg)
    yg = yg - 90

    MakeSectionHeader(genPane, L["TAB_GENERAL"], PAD, yg, true)
    yg = yg - 44

    -- Minimap checkbox
    local cbMinimap = MakeCheckbox(genPane, L["GEN_MINIMAP"], L["GEN_MINIMAP_TIP"], PAD, yg)
    cbMinimap:SetScript("OnClick", function(self)
        local val = self:GetChecked()
        ns.GUI:SetMinimapShown(val)
        ns.GUI:ChatMsg(val and L["CHAT_MINIMAP_SHOW"] or L["CHAT_MINIMAP_HIDE"])
    end)
    Panel.cbMinimap = cbMinimap
    yg = yg - 50

    -- Debug checkbox
    local cbDebug = MakeCheckbox(genPane, L["GEN_DEBUG"], L["GEN_DEBUG_TIP"], PAD, yg)
    cbDebug:SetScript("OnClick", function(self)
        local val = self:GetChecked()
        rOrbsDB.debug = val
        ns.debug = val
        ns.GUI:ChatMsg(val and L["CHAT_DEBUG_ON"] or L["CHAT_DEBUG_OFF"])
    end)
    Panel.cbDebug = cbDebug
    yg = yg - 64


    -- Info text about frame positioning
    MakeSectionHeader(genPane, "Frame Positioning", PAD, yg)
    yg = yg - 44

    MakeInfoText(genPane,
        "|cffffff00SHIFT + Left-Click|r and drag an orb to |cffffff00move|r it.\n"..
        "|cffffff00ALT + Left-Click|r and drag an orb to |cffffff00scale|r it.\n"..
        "Positions and scale are automatically saved.",
        PAD, yg, PANEL_W - CONTENT_X - PAD - SB_W - 30)
    yg = yg - 62



    -- ============================================================
    --  PROFILE SECTION
    -- ============================================================
    yg = yg - 10
    MakeSectionHeader(genPane, L["PROFILE_SECTION"], PAD, yg)
    yg = yg - 44

    MakeInfoText(genPane, L["PROFILE_SECTION_TIP"], PAD, yg, PANEL_W - CONTENT_X - PAD - SB_W - 30)
    yg = yg - 32

    -- Active profile label
    local profActiveLbl = genPane:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    profActiveLbl:SetPoint("TOPLEFT", genPane, "TOPLEFT", PAD, yg)
    profActiveLbl:SetTextColor(C.text[1], C.text[2], C.text[3])
    -- Helper: get the active profile name for the current character
    local function GetActiveProfileName()
        if ns.db and ns.db.GetCharKey and rOrbs_DB_GLOB and rOrbs_DB_GLOB.CHAR_PROFILES then
            return rOrbs_DB_GLOB.CHAR_PROFILES[ns.db:GetCharKey()]
        end
        -- fallback: rOrbs_DB_CHAR.activeProfile (set during same session)
        return rOrbs_DB_CHAR and rOrbs_DB_CHAR.activeProfile
    end

    local function UpdateActiveProfileLabel()
        local name = GetActiveProfileName() or L["PROFILE_NONE"]
        profActiveLbl:SetText(L["PROFILE_ACTIVE"] .. " |cffffff00" .. name .. "|r")
    end
    UpdateActiveProfileLabel()
    Panel.profActiveLbl = profActiveLbl
    yg = yg - 24

    -- Profile dropdown
    local ddProf, ddProfLbl = MakeDropdown(genPane, L["PROFILE_SELECT"], L["PROFILE_SELECT_TIP"], PAD, yg, 220)
    Panel.ddProf = ddProf

    local function RefreshProfileDropdown()
        UIDropDownMenu_Initialize(ddProf, function(self, level)
            local profDB = ns.db
            if not profDB then return end
            local list = profDB.getListProfile()
            if not list or #list == 0 then
                local info = UIDropDownMenu_CreateInfo()
                info.text = "|cff888888-- no profiles --"
                info.disabled = true
                info.notCheckable = true
                UIDropDownMenu_AddButton(info, level)
                return
            end
            local active = GetActiveProfileName()
            for _, entry in ipairs(list) do
                local info = UIDropDownMenu_CreateInfo()
                info.text    = entry.key
                info.value   = entry.key
                info.checked = (entry.key == active)
                info.func    = function()
                    UIDropDownMenu_SetSelectedValue(ddProf, entry.key)
                    UIDropDownMenu_SetText(ddProf, entry.key)
                end
                UIDropDownMenu_AddButton(info, level)
            end
        end)
        -- Set text to active profile
        local active = GetActiveProfileName()
        if active then
            UIDropDownMenu_SetText(ddProf, active)
            UIDropDownMenu_SetSelectedValue(ddProf, active)
        else
            UIDropDownMenu_SetText(ddProf, L["PROFILE_NONE"])
        end
    end
    RefreshProfileDropdown()
    yg = yg - 50

    -- Helper: get selected profile name from dropdown
    local function GetSelectedProfile()
        return UIDropDownMenu_GetSelectedValue(ddProf)
    end

    -- Load / Apply button
    local profLoadBtn = MakeButton(genPane, L["PROFILE_SELECT"], PAD, yg, 140, 26, 0.10, 0.20, 0.10)
    profLoadBtn:SetScript("OnClick", function()
        local name = GetSelectedProfile()
        if not name then ns.GUI:ChatMsg("No profile selected."); return end
        StaticPopupDialogs["RORBS_PROFILE_LOAD_CONFIRM"] = {
            text     = string.format(L["PROFILE_LOAD_CONFIRM"], name),
            button1  = OKAY,
            button2  = CANCEL,
            OnAccept = function() ns.db.loadProfile(name) end,
            timeout = 0, whileDead = true, hideOnEscape = true,
        }
        StaticPopup_Show("RORBS_PROFILE_LOAD_CONFIRM")
    end)

    -- Delete button
    local profDelBtn = MakeButton(genPane, L["PROFILE_DELETE"], PAD + 148, yg, 140, 26, 0.40, 0.05, 0.03)
    profDelBtn:SetScript("OnClick", function()
        local name = GetSelectedProfile()
        if not name then ns.GUI:ChatMsg("No profile selected."); return end
        StaticPopupDialogs["RORBS_PROFILE_DELETE_CONFIRM"] = {
            text     = string.format(L["PROFILE_DELETE_CONFIRM"], name),
            button1  = OKAY,
            button2  = CANCEL,
            OnAccept = function()
                ns.db.deleteProfile(name)
                RefreshProfileDropdown()
                UpdateActiveProfileLabel()
            end,
            timeout = 0, whileDead = true, hideOnEscape = true,
        }
        StaticPopup_Show("RORBS_PROFILE_DELETE_CONFIRM")
    end)
    yg = yg - 34

    -- Helper: safely get text from a StaticPopup editBox on WoW 12.x
    -- In 12.x the OnAccept self may not carry .editBox directly
    local function GetPopupEditBoxText(popupSelf)
        if popupSelf and popupSelf.editBox and popupSelf.editBox.GetText then
            return popupSelf.editBox:GetText()
        end
        for i = 1, 4 do
            local eb = _G["StaticPopup"..i.."EditBox"]
            if eb and eb:IsShown() then
                return eb:GetText()
            end
        end
        return ""
    end

    -- Save As button
    local profSaveBtn = MakeButton(genPane, L["PROFILE_SAVE"], PAD, yg, 140, 26, 0.08, 0.15, 0.25)
    profSaveBtn:SetScript("OnClick", function()
        StaticPopupDialogs["RORBS_PROFILE_SAVE_NAME"] = {
            text         = L["PROFILE_NAME_PROMPT"],
            button1      = OKAY,
            button2      = CANCEL,
            hasEditBox   = true,
            maxLetters   = 40,
            OnAccept     = function(self)
                local name = GetPopupEditBoxText(self)
                if not name or name == "" then return end
                local existingProfiles = rOrbs_DB_GLOB and rOrbs_DB_GLOB.PROFILES
                if existingProfiles and existingProfiles[name] then
                    StaticPopupDialogs["RORBS_PROFILE_OVERWRITE_CONFIRM"] = {
                        text     = L["PROFILE_NAME_EXISTS"],
                        button1  = OKAY,
                        button2  = CANCEL,
                        OnAccept = function()
                            ns.db.saveProfile(name)
                            RefreshProfileDropdown()
                            UpdateActiveProfileLabel()
                        end,
                        timeout = 0, whileDead = true, hideOnEscape = true,
                    }
                    StaticPopup_Show("RORBS_PROFILE_OVERWRITE_CONFIRM")
                else
                    ns.db.saveProfile(name)
                    RefreshProfileDropdown()
                    UpdateActiveProfileLabel()
                end
            end,
            timeout = 0, whileDead = true, hideOnEscape = true,
        }
        StaticPopup_Show("RORBS_PROFILE_SAVE_NAME")
    end)

    -- Copy From button
    local profCopyBtn = MakeButton(genPane, L["PROFILE_COPY"], PAD + 148, yg, 140, 26, 0.08, 0.12, 0.20)
    profCopyBtn:SetScript("OnClick", function()
        local srcName = GetSelectedProfile()
        if not srcName then ns.GUI:ChatMsg("No source profile selected."); return end
        StaticPopupDialogs["RORBS_PROFILE_COPY_NAME"] = {
            text         = L["PROFILE_COPY_NEW"],
            button1      = OKAY,
            button2      = CANCEL,
            hasEditBox   = true,
            maxLetters   = 40,
            OnAccept     = function(self)
                local destName = GetPopupEditBoxText(self)
                if not destName or destName == "" then return end
                ns.db.copyProfile(srcName, destName)
                RefreshProfileDropdown()
            end,
            timeout = 0, whileDead = true, hideOnEscape = true,
        }
        StaticPopup_Show("RORBS_PROFILE_COPY_NAME")
    end)
    yg = yg - 42

    -- Slash Commands (inline in General tab)
    yg = yg - 10
    MakeSectionHeader(genPane, L["CMD_TITLE"], PAD, yg) ; yg = yg - 44

    local cmdListGen = {
        L["CMD_RORBS"], L["CMD_RORBS_SHOW"], L["CMD_RORBS_HIDE"],
        L["CMD_RORBS_RESET"], L["CMD_RORBS_DEBUG"],
        L["CMD_RORBSDEBUG"],
    }
    for _, cmdText in ipairs(cmdListGen) do
        local cmd, desc = cmdText:match("^(/[^–]+)–(.+)$")
        local row = CreateFrame("Frame", nil, genPane)
        row:SetSize(PANEL_W - CONTENT_X - 50, 22)
        row:SetPoint("TOPLEFT", genPane, "TOPLEFT", PAD, yg)
        local fs1 = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        fs1:SetPoint("LEFT") ; fs1:SetWidth(260) ; fs1:SetJustifyH("LEFT")
        fs1:SetTextColor(C.yellow[1], C.yellow[2], C.yellow[3])
        fs1:SetText(cmd and cmd:match("^%s*(.-)%s*$") or cmdText)
        if desc then
            local fs2 = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            fs2:SetPoint("LEFT", fs1, "RIGHT", 4, 0) ; fs2:SetJustifyH("LEFT")
            fs2:SetTextColor(C.textDim[1], C.textDim[2], C.textDim[3])
            fs2:SetText("– "..desc:match("^%s*(.-)%s*$"))
        end
        yg = yg - 24
    end
    yg = yg - 14

    -- Embedded library / framework versions
    do
        local function getVer(name)
            if C_AddOns and C_AddOns.GetAddOnMetadata then
                return C_AddOns.GetAddOnMetadata(name, "Version") or "?"
            end
            return GetAddOnMetadata and GetAddOnMetadata(name, "Version") or "?"
        end
        local lblW = PANEL_W - CONTENT_X - PAD - SB_W - 30

        local function AddVerLine(text)
            local fs = genPane:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            fs:SetPoint("TOPLEFT", genPane, "TOPLEFT", PAD, yg)
            fs:SetWidth(lblW)
            fs:SetJustifyH("LEFT")
            fs:SetTextColor(C.textDim[1], C.textDim[2], C.textDim[3])
            fs:SetText(text)
            yg = yg - 22
        end

        AddVerLine("oUF  |cffffff00v13.3.0|r")
        AddVerLine("LibDBIcon-1.0  |cffffff00v11.2.1|r")
        AddVerLine("LibSharedMedia-3.0  |cffffff00v11.2.1|r")
        yg = yg - 8
    end

    -- Credits / Danksagung
    MakeSectionHeader(genPane,
        (GetLocale() == "deDE") and "Danksagung" or "Credits",
        PAD, yg)
    yg = yg - 32

    local credLine1 = genPane:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    credLine1:SetPoint("TOPLEFT", genPane, "TOPLEFT", PAD, yg)
    credLine1:SetWidth(PANEL_W - CONTENT_X - PAD - SB_W - 30)
    credLine1:SetJustifyH("LEFT")
    credLine1:SetText('|cffffff00Many thanks to "Zorker (Erik Raetz)".|r')
    yg = yg - 24

    local credLine2 = genPane:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    credLine2:SetPoint("TOPLEFT", genPane, "TOPLEFT", PAD, yg)
    credLine2:SetWidth(PANEL_W - CONTENT_X - PAD - SB_W - 30)
    credLine2:SetJustifyH("LEFT")
    credLine2:SetTextColor(0.75, 0.75, 0.75)
    credLine2:SetText("Without your work, this would just be an empty add-on window.")
    yg = yg - 38

    MakeSep(genPane, yg) ; yg = yg - 18

    local resetBtn = MakeButton(genPane, L["SETTINGS_RESET"], PAD, yg, 200, 28, 0.40, 0.05, 0.03)
    resetBtn:SetScript("OnClick", function()
        StaticPopupDialogs["RORBS_RESET_CONFIRM"] = {
            text          = L["SETTINGS_RESET_CONFIRM"],
            button1       = OKAY,
            button2       = CANCEL,
            OnAccept      = function()
                rOrbs_DB_CHAR = nil
                rOrbs_DB_GLOB = nil
                rOrbsDB       = nil
                ns.GUI:ChatMsg(L["CHAT_RESET_DONE"])
                ReloadUI()
            end,
            timeout = 0, whileDead = true, hideOnEscape = true,
        }
        StaticPopup_Show("RORBS_RESET_CONFIRM")
    end)
    yg = yg - 42

    genPane._contentH = math.abs(yg) + PAD + 40
    if genPane._updateThumb then genPane._updateThumb() end

    -- ============================================================
    --  ORB TAB BUILDER  (health, power, target)
    -- ============================================================
    local function BuildOrbTab(key, orbType)
        local pane = BuildScrollPane(key)
        local yo   = -PAD
        Panel[key] = {}

        -- Determine which config section and label to use
        local cfgKey = orbType  -- HEALTH, POWER, TARGET_HEALTH, or TOT_HEALTH
        local tabLabel
        if     orbType == "HEALTH"        then tabLabel = L["TAB_HEALTH_ORB"]
        elseif orbType == "POWER"         then tabLabel = L["TAB_POWER_ORB"]
        elseif orbType == "TARGET_HEALTH" then tabLabel = L["TAB_TARGET_ORB"]
        elseif orbType == "TOT_HEALTH"    then tabLabel = L["TAB_TOT_ORB"] or "ToT Orb"
        end

        -- Ensure db.char[cfgKey] exists (backward compat for new orb types)
        local function ensureCharCfg()
            local db2 = ns.db
            if db2 and not db2.char[cfgKey] then
                local defaults = db2:GetOrbDefaults()
                if defaults and defaults[cfgKey] then
                    db2.char[cfgKey] = defaults[cfgKey]
                    if rOrbs_DB_CHAR then rOrbs_DB_CHAR[cfgKey] = db2.char[cfgKey] end
                end
            end
        end
        ensureCharCfg()

        MakeSectionHeader(pane, tabLabel, PAD, yo)
        yo = yo - 44

        -- ENABLE ORB CHECKBOX (Target and ToT only) ----------------------------
        if cfgKey == "TARGET_HEALTH" or cfgKey == "TOT_HEALTH" then
            local enableKey = (cfgKey == "TARGET_HEALTH") and "targetOrbEnabled" or "totOrbEnabled"
            local cbEnable = MakeCheckbox(pane, L["ORB_ENABLE"] or "Enable this Orb",
                L["ORB_ENABLE_TIP"] or "Toggle this orb on or off. Requires a UI reload.",
                PAD, yo)
            cbEnable:SetChecked(rOrbsDB[enableKey] ~= false)
            cbEnable:SetScript("OnClick", function(self)
                local val = self:GetChecked()
                StaticPopupDialogs["RORBS_RELOAD_ORB_ENABLE_"..cfgKey] = {
                    text = (GetLocale() == "deDE")
                        and "Orb wurde " .. (val and "aktiviert" or "deaktiviert") .. ".\nEin |cffffd700Reload|r ist erforderlich."
                        or  "Orb " .. (val and "enabled" or "disabled") .. ".\nA |cffffd700Reload|r is required for the change to take effect.",
                    button1 = "Reload UI",
                    button2 = CANCEL,
                    OnAccept = function()
                        rOrbsDB[enableKey] = val
                        ReloadUI()
                    end,
                    OnCancel = function()
                        cbEnable:SetChecked(rOrbsDB[enableKey] ~= false)
                    end,
                    timeout = 0, whileDead = true, hideOnEscape = false,
                }
                StaticPopup_Show("RORBS_RELOAD_ORB_ENABLE_"..cfgKey)
            end)
            Panel[key].cbEnable = cbEnable
            yo = yo - 50
            MakeSep(pane, yo) ; yo = yo - 12
        end

        yo = yo - 8
        -- TEXTURE DROPDOWN ---------------------------------------------------
        local ddTex, _ = MakeDropdown(pane, L["ORB_TEXTURE"], L["ORB_TEXTURE_TIP"], PAD, yo, 220)
        Panel[key].ddTex = ddTex
        UIDropDownMenu_Initialize(ddTex, function(self, level)
            local db2 = ns.db
            if not db2 then return end
            local currentTex = db2.char[cfgKey] and db2.char[cfgKey].filling.texture or ""
            for _, entry in ipairs(db2.getListFillingTexture()) do
                local info = UIDropDownMenu_CreateInfo()
                info.text  = entry.key ; info.value = entry.value
                info.checked = (entry.value == currentTex)
                info.func = function()
                    UIDropDownMenu_SetSelectedValue(ddTex, entry.value)
                    UIDropDownMenu_SetText(ddTex, entry.key)
                    ensureCharCfg()
                    if db2.char[cfgKey] and db2.char[cfgKey].filling then
                        db2.char[cfgKey].filling.texture = entry.value
                    end
                    rOrbs_DB_CHAR = db2.char
                    ns.GUI:ApplyTexture(cfgKey, entry.value)
                    ns.GUI:ChatMsg(string.format(L["CHAT_TEXTURE_CHANGED"], entry.key))
                end
                UIDropDownMenu_AddButton(info, level)
            end
        end)
        -- Set initial text (prevents WoW showing "Custom" as default)
        do
            local db2 = ns.db
            if db2 and db2.char[cfgKey] then
                local currentTex = db2.char[cfgKey].filling.texture or ""
                local dispName = "?"
                for _, entry in ipairs(db2.getListFillingTexture()) do
                    if entry.value == currentTex then dispName = entry.key; break end
                end
                UIDropDownMenu_SetText(ddTex, dispName)
                UIDropDownMenu_SetSelectedValue(ddTex, currentTex)
            end
        end
        yo = yo - 56

        -- FILL COLOR ---------------------------------------------------------
        local colLabel = pane:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        colLabel:SetPoint("TOPLEFT", pane, "TOPLEFT", PAD, yo)
        colLabel:SetText(L["ORB_COLOR"])
        colLabel:SetTextColor(C.text[1], C.text[2], C.text[3])
        yo = yo - 22

        local initCol = { r=1, g=1, b=1 }
        if ns.db and ns.db.char and ns.db.char[cfgKey] then
            initCol = ns.db.char[cfgKey].filling.color
        end

        local colSwatch  -- pre-declare so closure can reference it
        colSwatch = MakeColorSwatch(pane, PAD, yo,
            initCol.r, initCol.g, initCol.b, L["ORB_COLOR"], function()
            local db2 = ns.db
            ensureCharCfg()
            if not db2 or not db2.char[cfgKey] then return end
            local col = db2.char[cfgKey].filling.color
            ns.ColorPickerHelper:Open({
                r = col.r, g = col.g, b = col.b,
                onChange = function(r, g, b)
                    db2.char[cfgKey].filling.color = {r=r, g=g, b=b}
                    colSwatch:SetColor(r, g, b)
                    rOrbs_DB_CHAR = db2.char
                    ns.GUI:ApplyColor(cfgKey, r, g, b)
                end,
                onCancel = function()
                    local c = db2.char[cfgKey].filling.color
                    ns.GUI:ApplyColor(cfgKey, c.r, c.g, c.b)
                end,
            })
        end)
        Panel[key].colSwatch = colSwatch

        -- RESET COLOR BUTTON
        local resetColorBtn = MakeButton(pane, "Reset Color", PAD + 140, yo, 100, 22,
            0.20, 0.06, 0.04)
        resetColorBtn:SetScript("OnClick", function()
            local db2 = ns.db
            if not db2 or not db2.char[cfgKey] then return end
            local defaults = db2:GetOrbDefaults()
            if not defaults or not defaults[cfgKey] then return end
            local defCol = defaults[cfgKey].filling.color
            db2.char[cfgKey].filling.color = { r=defCol.r, g=defCol.g, b=defCol.b }
            rOrbs_DB_CHAR = db2.char
            colSwatch:SetColor(defCol.r, defCol.g, defCol.b)
            ns.GUI:ApplyColor(cfgKey, defCol.r, defCol.g, defCol.b)
            ns.GUI:ChatMsg("|cffffff00Orb color|r reset to default.")
        end)
        yo = yo - 44

        -- AUTO / CLASS COLOR (with reload popup) -----------------------------
        local autoLabel = (orbType == "POWER" and L["ORB_COLOR_AUTO"] or L["ORB_COLOR_CLASS"])
        local autoTip   = (orbType == "POWER" and L["ORB_COLOR_AUTO_TIP"] or L["ORB_COLOR_CLASS_TIP"])
        local cbAuto = MakeCheckbox(pane, autoLabel, autoTip, PAD, yo)
        cbAuto:SetScript("OnClick", function(self)
            local val = self:GetChecked()
            ensureCharCfg()
            if not ns.db or not ns.db.char[cfgKey] then return end
            ns.db.char[cfgKey].filling.colorAuto = val
            rOrbs_DB_CHAR = ns.db.char
            StaticPopupDialogs["RORBS_RELOAD_FOR_COLOR"] = {
                text    = (GetLocale() == "deDE")
                    and "Klassenfarbe wurde geändert.\nEin |cffffd700Reload|r ist erforderlich damit die Änderung vollständig wirksam wird."
                    or  "Class color was changed.\nA |cffffd700Reload|r is required for the change to fully take effect.",
                button1 = "Reload UI",
                button2 = CANCEL,
                OnAccept = function()
                    ns.GUI:ApplyColorAuto(cfgKey, val)
                    ReloadUI()
                end,
                OnCancel = function()
                    self:SetChecked(not val)
                    ns.db.char[cfgKey].filling.colorAuto = not val
                    rOrbs_DB_CHAR = ns.db.char
                end,
                timeout = 0, whileDead = true, hideOnEscape = false,
            }
            StaticPopup_Show("RORBS_RELOAD_FOR_COLOR")
        end)
        Panel[key].cbAuto = cbAuto
        yo = yo - 56

        -- DECORATIVE RING (Health and Power orbs) --------------------------------
        if cfgKey == "HEALTH" or cfgKey == "POWER" then
            local ringDBKey  = (cfgKey == "HEALTH") and "healthRingShow" or "powerRingShow"
            local ringOrbGlb = (cfgKey == "HEALTH") and "rOrbsHealthOrb"  or "rOrbsPowerOrb"
            local ringOrbLabel = (cfgKey == "HEALTH") and "health" or "power"
            local cbRing = MakeCheckbox(pane,
                "Show decorative ring",
                "Show or hide the decorative stone ring/wreath around the "..ringOrbLabel.." orb.",
                PAD, yo)
            cbRing:SetChecked(rOrbsDB[ringDBKey] ~= false)
            cbRing:SetScript("OnClick", function(self)
                local val = self:GetChecked()
                rOrbsDB[ringDBKey] = val
                local orb = _G[ringOrbGlb]
                if orb and orb.ring then
                    if val then orb.ring:Show() else orb.ring:Hide() end
                end
                ns.GUI:ChatMsg("|cffffff00Decorative ring ("..ringOrbLabel..")|r "..(val and "|cff00ff00shown|r." or "|cffff4444hidden|r."))
            end)
            Panel[key].cbRing = cbRing ; yo = yo - 50

            -- RING ROTATION CONTROLS
            local ringRotateKey = (cfgKey == "HEALTH") and "healthRingRotate" or "powerRingRotate"
            local ringSpeedKey  = (cfgKey == "HEALTH") and "healthRingSpeed"  or "powerRingSpeed"
            local ringDirKey    = (cfgKey == "HEALTH") and "healthRingDir"    or "powerRingDir"

            local cbRingRotate = MakeCheckbox(pane,
                "Rotate decorative ring",
                "Enable a slow spinning rotation animation on the decorative " .. ringOrbLabel .. " ring.",
                PAD + 20, yo)
            cbRingRotate:SetChecked(rOrbsDB[ringRotateKey] == true)
            cbRingRotate:SetScript("OnClick", function(self)
                local val = self:GetChecked()
                rOrbsDB[ringRotateKey] = val
                local orb = _G[ringOrbGlb]
                if orb and orb.ring then
                    local ring = orb.ring
                    if val then
                        if not ring.ag then
                            ring.ag = ring:CreateAnimationGroup()
                            ring.ag.anim = ring.ag:CreateAnimation("Rotation")
                            local spd = rOrbsDB[ringSpeedKey] or 30
                            local dir = rOrbsDB[ringDirKey] or "CW"
                            ring.ag.anim:SetDegrees(dir == "CCW" and 360 or -360)
                            ring.ag.anim:SetDuration(spd)
                            ring.ag:SetLooping("REPEAT")
                        end
                        ring.ag:Play()
                    else
                        if ring.ag then ring.ag:Stop() end
                    end
                end
                ns.GUI:ChatMsg("|cffffff00Ring rotation (" .. ringOrbLabel .. ")|r " .. (val and "|cff00ff00enabled|r." or "|cffff4444disabled|r."))
            end)
            Panel[key].cbRingRotate = cbRingRotate ; yo = yo - 50

            local sRingSpeed, _ = MakeSlider(pane,
                "Rotation speed – seconds per revolution",
                "How many seconds for one full spin. Lower = faster.",
                5, 120, 1, PAD + 20, yo, SLIDER_W)
            SetSliderHandler(sRingSpeed, function(self, val)
                rOrbsDB[ringSpeedKey] = val
                local orb = _G[ringOrbGlb]
                if orb and orb.ring and orb.ring.ag then
                    local playing = orb.ring.ag:IsPlaying()
                    orb.ring.ag:Stop()
                    orb.ring.ag.anim:SetDuration(val)
                    if playing then orb.ring.ag:Play() end
                end
            end)
            sRingSpeed:SetValue(rOrbsDB[ringSpeedKey] or 30)
            Panel[key].sRingSpeed = sRingSpeed ; yo = yo - 52

            local ddRingDir, _ = MakeDropdown(pane,
                "Rotation direction",
                "Choose clockwise or counter-clockwise rotation direction.",
                PAD + 20, yo, 200)
            Panel[key].ddRingDir = ddRingDir
            UIDropDownMenu_Initialize(ddRingDir, function(self, level)
                local currentDir = rOrbsDB[ringDirKey] or "CW"
                local dirs = {
                    { text = "Clockwise",         value = "CW"  },
                    { text = "Counter-clockwise", value = "CCW" },
                }
                for _, entry in ipairs(dirs) do
                    local info = UIDropDownMenu_CreateInfo()
                    info.text    = entry.text
                    info.value   = entry.value
                    info.checked = (entry.value == currentDir)
                    info.func = function()
                        UIDropDownMenu_SetSelectedValue(ddRingDir, entry.value)
                        UIDropDownMenu_SetText(ddRingDir, entry.text)
                        rOrbsDB[ringDirKey] = entry.value
                        local orb = _G[ringOrbGlb]
                        if orb and orb.ring and orb.ring.ag then
                            local playing = orb.ring.ag:IsPlaying()
                            orb.ring.ag:Stop()
                            orb.ring.ag.anim:SetDegrees(entry.value == "CCW" and 360 or -360)
                            if playing then orb.ring.ag:Play() end
                        end
                        ns.GUI:ChatMsg("|cffffff00Ring direction (" .. ringOrbLabel .. ")|r \u2192 " .. entry.text)
                    end
                    UIDropDownMenu_AddButton(info, level)
                end
            end)
            do
                local dir = rOrbsDB[ringDirKey] or "CW"
                local dirLabels = { CW = "Clockwise", CCW = "Counter-clockwise" }
                UIDropDownMenu_SetText(ddRingDir, dirLabels[dir] or "Clockwise")
                UIDropDownMenu_SetSelectedValue(ddRingDir, dir)
            end
            yo = yo - 56

            -- ANGEL / DEMON FIGURE TOGGLE
            if cfgKey == "HEALTH" then
                local cbAngel = MakeCheckbox(pane,
                    "Show Angel figure",
                    "Show or hide the decorative angel figure to the left of the health orb.",
                    PAD, yo)
                cbAngel:SetChecked(rOrbsDB.healthAngelShow ~= false)
                cbAngel:SetScript("OnClick", function(self)
                    local val = self:GetChecked()
                    rOrbsDB.healthAngelShow = val
                    if ns.AngelFrame then
                        if val then ns.AngelFrame:Show() else ns.AngelFrame:Hide() end
                    end
                    ns.GUI:ChatMsg("|cffffff00Angel figure|r "..(val and "|cff00ff00shown|r." or "|cffff4444hidden|r."))
                end)
                Panel[key].cbAngel = cbAngel ; yo = yo - 50

                -- RUNE / CLASS POINTS TOGGLE
                local cbRunePoints = MakeCheckbox(pane,
                    "Show Rune / Class Points",
                    "Show or hide the rune-style combo / class point indicators above the health orb.\n\nSupported: Rogue & Feral Druid (Combo), Paladin (Holy Power), Monk WW (Chi), Warlock (Soul Shards), Arcane Mage (Arcane Charges), Evoker (Essence).",
                    PAD, yo)
                cbRunePoints:SetChecked(rOrbsDB.runePointsShow ~= false)
                cbRunePoints:SetScript("OnClick", function(self)
                    local val = self:GetChecked()
                    rOrbsDB.runePointsShow = val
                    if ns.RunePoints then
                        ns.RunePoints.ForceRefresh()
                    end
                    ns.GUI:ChatMsg("|cffffff00Rune / Class Points|r "..(val and "|cff00ff00shown|r." or "|cffff4444hidden|r."))
                end)
                Panel[key].cbRunePoints = cbRunePoints ; yo = yo - 78

                -- PLAYER COMBAT ICON TOGGLE
                local cbPlayerCombat = MakeCheckbox(pane,
                    "Show Combat Icon (crossed swords)",
                    "Show or hide the crossed swords icon above the health orb when you are in combat.",
                    PAD, yo)
                cbPlayerCombat:SetChecked(rOrbsDB.playerCombatIconShow ~= false)
                cbPlayerCombat:SetScript("OnClick", function(self)
                    local val = self:GetChecked()
                    rOrbsDB.playerCombatIconShow = val
                    local f = ns.PlayerCombatIcon
                    if f then
                        if val and UnitAffectingCombat("player") then
                            f:Show()
                        else
                            f:Hide()
                        end
                    end
                    ns.GUI:ChatMsg("|cffffff00Player combat icon|r "..(val and "|cff00ff00shown|r." or "|cffff4444hidden|r."))
                end)
                Panel[key].cbPlayerCombatIcon = cbPlayerCombat ; yo = yo - 50

                -- Size / position sliders for Player Combat Icon
                local sPCombatSize, _ = MakeSlider(pane, "Combat Icon – Size",
                    "Width and height of the combat (crossed swords) icon in pixels.",
                    12, 64, 1, PAD + 20, yo, SLIDER_W)
                SetSliderHandler(sPCombatSize, function(self, val)
                    rOrbsDB.playerCombatIconSize = val
                    if ns.UpdateCombatIconLayout then ns.UpdateCombatIconLayout() end
                end)
                sPCombatSize:SetValue(rOrbsDB.playerCombatIconSize or 28)
                Panel[key].sPCombatSize = sPCombatSize ; yo = yo - 46

                local sPCombatOffX, _ = MakeSlider(pane, "Combat Icon – Offset X",
                    "Horizontal offset of the combat icon (added to its default centre-above-orb position).",
                    -200, 200, 1, PAD + 20, yo, SLIDER_W)
                SetSliderHandler(sPCombatOffX, function(self, val)
                    rOrbsDB.playerCombatIconOffX = val
                    if ns.UpdateCombatIconLayout then ns.UpdateCombatIconLayout() end
                end)
                sPCombatOffX:SetValue(rOrbsDB.playerCombatIconOffX or 0)
                Panel[key].sPCombatOffX = sPCombatOffX ; yo = yo - 46

                local sPCombatOffY, _ = MakeSlider(pane, "Combat Icon – Offset Y",
                    "Vertical offset of the combat icon (added to its default position above the orb).",
                    -200, 200, 1, PAD + 20, yo, SLIDER_W)
                SetSliderHandler(sPCombatOffY, function(self, val)
                    rOrbsDB.playerCombatIconOffY = val
                    if ns.UpdateCombatIconLayout then ns.UpdateCombatIconLayout() end
                end)
                sPCombatOffY:SetValue(rOrbsDB.playerCombatIconOffY or 0)
                Panel[key].sPCombatOffY = sPCombatOffY ; yo = yo - 52

                -- PLAYER RESTING ICON TOGGLE
                local cbPlayerResting = MakeCheckbox(pane,
                    "Show Resting Icon (animated zzz)",
                    "Show or hide the animated zzz icon above the health orb when you are resting in an inn (uses Blizzard's PlayerRestLoop).",
                    PAD, yo)
                cbPlayerResting:SetChecked(rOrbsDB.playerRestingIconShow ~= false)
                cbPlayerResting:SetScript("OnClick", function(self)
                    local val = self:GetChecked()
                    rOrbsDB.playerRestingIconShow = val
                    -- ns.PlayerRestingIcon is the non-secure wrapper frame.
                    -- Re-evaluate resting state so we don't show zzz when not resting.
                    if ns.PlayerRestingIcon then
                        if not val then
                            ns.PlayerRestingIcon:Hide()
                        elseif IsResting() then
                            ns.PlayerRestingIcon:Show()
                        else
                            ns.PlayerRestingIcon:Hide()
                        end
                    end
                    ns.GUI:ChatMsg("|cffffff00Player resting icon|r "..(val and "|cff00ff00shown|r." or "|cffff4444hidden|r."))
                end)
                Panel[key].cbPlayerRestingIcon = cbPlayerResting ; yo = yo - 50

                -- Size / position sliders for Player Resting Icon
                local sPRestSize, _ = MakeSlider(pane, "Resting Icon – Size",
                    "Size of the animated resting (zzz) icon in pixels (uses scale for the Blizzard animation).",
                    12, 64, 1, PAD + 20, yo, SLIDER_W)
                SetSliderHandler(sPRestSize, function(self, val)
                    rOrbsDB.playerRestingIconSize = val
                    if ns.UpdateRestingIconLayout then ns.UpdateRestingIconLayout() end
                end)
                sPRestSize:SetValue(rOrbsDB.playerRestingIconSize or 28)
                Panel[key].sPRestSize = sPRestSize ; yo = yo - 46

                local sPRestOffX, _ = MakeSlider(pane, "Resting Icon – Offset X",
                    "Horizontal offset of the resting icon (added to its default -30 px left position).",
                    -200, 200, 1, PAD + 20, yo, SLIDER_W)
                SetSliderHandler(sPRestOffX, function(self, val)
                    rOrbsDB.playerRestingIconOffX = val
                    if ns.UpdateRestingIconLayout then ns.UpdateRestingIconLayout() end
                end)
                sPRestOffX:SetValue(rOrbsDB.playerRestingIconOffX or 0)
                Panel[key].sPRestOffX = sPRestOffX ; yo = yo - 46

                local sPRestOffY, _ = MakeSlider(pane, "Resting Icon – Offset Y",
                    "Vertical offset of the resting icon (added to its default position above the orb).",
                    -200, 200, 1, PAD + 20, yo, SLIDER_W)
                SetSliderHandler(sPRestOffY, function(self, val)
                    rOrbsDB.playerRestingIconOffY = val
                    if ns.UpdateRestingIconLayout then ns.UpdateRestingIconLayout() end
                end)
                sPRestOffY:SetValue(rOrbsDB.playerRestingIconOffY or 0)
                Panel[key].sPRestOffY = sPRestOffY ; yo = yo - 46

                -- Color picker for Player Resting Icon
                local restCol = rOrbsDB.playerRestingIconColor or { r = 1, g = 1, b = 1 }
                local restColorSwatch
                restColorSwatch = MakeColorSwatch(pane, PAD, yo,
                    restCol.r, restCol.g, restCol.b, "Resting Icon – Color", function()
                    local col = rOrbsDB.playerRestingIconColor or { r = 1, g = 1, b = 1 }
                    ns.ColorPickerHelper:Open({
                        r = col.r, g = col.g, b = col.b,
                        onChange = function(r, g, b)
                            rOrbsDB.playerRestingIconColor = { r = r, g = g, b = b }
                            restColorSwatch:SetColor(r, g, b)
                            if ns.ApplyRestingIconColor then ns.ApplyRestingIconColor(r, g, b) end
                        end,
                        onCancel = function()
                            local c = rOrbsDB.playerRestingIconColor or { r = 1, g = 1, b = 1 }
                            if ns.ApplyRestingIconColor then ns.ApplyRestingIconColor(c.r, c.g, c.b) end
                        end,
                    })
                end)
                Panel[key].restColorSwatch = restColorSwatch ; yo = yo - 34

                -- Reset resting icon color to white (original)
                local resetRestColor = MakeButton(pane, "Reset Color", PAD + 200, yo + 34, 100, 22,
                    0.20, 0.06, 0.04)
                resetRestColor:SetScript("OnClick", function()
                    rOrbsDB.playerRestingIconColor = { r = 1, g = 1, b = 1 }
                    restColorSwatch:SetColor(1, 1, 1)
                    if ns.ApplyRestingIconColor then ns.ApplyRestingIconColor(1, 1, 1) end
                    ns.GUI:ChatMsg("|cffffff00Resting icon color|r reset to |cffffffffwhite|r.")
                end)
                Panel[key].resetRestColor = resetRestColor ; yo = yo - 28

                -- Angel position / scale sliders
                MakeSectionHeader(pane, "Angel Figure Position & Scale", PAD, yo) ; yo = yo - 44

                local sAngelScale, _ = MakeSlider(pane, "Scale", "Scale of the angel figure relative to the orb size.", 0.3, 3.0, 0.05, PAD, yo, SLIDER_W)
                SetSliderHandler(sAngelScale, function(self, val)
                    rOrbsDB.angelScale = val
                    if ns.UpdateFigureLayout and ns.AngelFrame and ns.HealthOrb then
                        ns.UpdateFigureLayout(ns.AngelFrame, ns.HealthOrb, "LEFT", rOrbsDB.angelScale, rOrbsDB.angelOffX, rOrbsDB.angelOffY)
                    end
                end)
                sAngelScale:SetValue(rOrbsDB.angelScale or 1.0)
                Panel[key].sAngelScale = sAngelScale ; yo = yo - 52

                local sAngelOffX, _ = MakeSlider(pane, "Offset X", "Horizontal fine-tuning. Negative = further left, positive = overlap more.", -200, 200, 1, PAD, yo, SLIDER_W)
                SetSliderHandler(sAngelOffX, function(self, val)
                    rOrbsDB.angelOffX = val
                    if ns.UpdateFigureLayout and ns.AngelFrame and ns.HealthOrb then
                        ns.UpdateFigureLayout(ns.AngelFrame, ns.HealthOrb, "LEFT", rOrbsDB.angelScale, rOrbsDB.angelOffX, rOrbsDB.angelOffY)
                    end
                end)
                sAngelOffX:SetValue(rOrbsDB.angelOffX or 0)
                Panel[key].sAngelOffX = sAngelOffX ; yo = yo - 52

                local sAngelOffY, _ = MakeSlider(pane, "Offset Y", "Vertical fine-tuning. Negative = lower, positive = higher.", -200, 200, 1, PAD, yo, SLIDER_W)
                SetSliderHandler(sAngelOffY, function(self, val)
                    rOrbsDB.angelOffY = val
                    if ns.UpdateFigureLayout and ns.AngelFrame and ns.HealthOrb then
                        ns.UpdateFigureLayout(ns.AngelFrame, ns.HealthOrb, "LEFT", rOrbsDB.angelScale, rOrbsDB.angelOffX, rOrbsDB.angelOffY)
                    end
                end)
                sAngelOffY:SetValue(rOrbsDB.angelOffY or 0)
                Panel[key].sAngelOffY = sAngelOffY ; yo = yo - 52

            else -- POWER
                local cbDemon = MakeCheckbox(pane,
                    "Show Demon figure",
                    "Show or hide the decorative demon figure to the right of the power orb.",
                    PAD, yo)
                cbDemon:SetChecked(rOrbsDB.powerDemonShow ~= false)
                cbDemon:SetScript("OnClick", function(self)
                    local val = self:GetChecked()
                    rOrbsDB.powerDemonShow = val
                    if ns.DemonFrame then
                        if val then ns.DemonFrame:Show() else ns.DemonFrame:Hide() end
                    end
                    ns.GUI:ChatMsg("|cffffff00Demon figure|r "..(val and "|cff00ff00shown|r." or "|cffff4444hidden|r."))
                end)
                Panel[key].cbDemon = cbDemon ; yo = yo - 50

                -- Demon position / scale sliders
                MakeSectionHeader(pane, "Demon Figure Position & Scale", PAD, yo) ; yo = yo - 44

                local sDemonScale, _ = MakeSlider(pane, "Scale", "Scale of the demon figure relative to the orb size.", 0.3, 3.0, 0.05, PAD, yo, SLIDER_W)
                SetSliderHandler(sDemonScale, function(self, val)
                    rOrbsDB.demonScale = val
                    if ns.UpdateFigureLayout and ns.DemonFrame and ns.PowerOrb then
                        ns.UpdateFigureLayout(ns.DemonFrame, ns.PowerOrb, "RIGHT", rOrbsDB.demonScale, rOrbsDB.demonOffX, rOrbsDB.demonOffY)
                    end
                end)
                sDemonScale:SetValue(rOrbsDB.demonScale or 1.0)
                Panel[key].sDemonScale = sDemonScale ; yo = yo - 52

                local sDemonOffX, _ = MakeSlider(pane, "Offset X", "Horizontal fine-tuning. Negative = further right, positive = overlap more.", -200, 200, 1, PAD, yo, SLIDER_W)
                SetSliderHandler(sDemonOffX, function(self, val)
                    rOrbsDB.demonOffX = val
                    if ns.UpdateFigureLayout and ns.DemonFrame and ns.PowerOrb then
                        ns.UpdateFigureLayout(ns.DemonFrame, ns.PowerOrb, "RIGHT", rOrbsDB.demonScale, rOrbsDB.demonOffX, rOrbsDB.demonOffY)
                    end
                end)
                sDemonOffX:SetValue(rOrbsDB.demonOffX or 0)
                Panel[key].sDemonOffX = sDemonOffX ; yo = yo - 52

                local sDemonOffY, _ = MakeSlider(pane, "Offset Y", "Vertical fine-tuning. Negative = lower, positive = higher.", -200, 200, 1, PAD, yo, SLIDER_W)
                SetSliderHandler(sDemonOffY, function(self, val)
                    rOrbsDB.demonOffY = val
                    if ns.UpdateFigureLayout and ns.DemonFrame and ns.PowerOrb then
                        ns.UpdateFigureLayout(ns.DemonFrame, ns.PowerOrb, "RIGHT", rOrbsDB.demonScale, rOrbsDB.demonOffX, rOrbsDB.demonOffY)
                    end
                end)
                sDemonOffY:SetValue(rOrbsDB.demonOffY or 0)
                Panel[key].sDemonOffY = sDemonOffY ; yo = yo - 52
            end
        end

        -- TARGET/TOT CLASS COLOR + RING + PORTRAIT (Target/ToT Orb only) ------
        if cfgKey == "TARGET_HEALTH" or cfgKey == "TOT_HEALTH" then
            local ccKey   = (cfgKey == "TARGET_HEALTH") and "targetClassColor" or "totClassColor"
            local ringKey = (cfgKey == "TARGET_HEALTH") and "targetRingShow"   or "totRingShow"
            local orbName = (cfgKey == "TARGET_HEALTH") and "rOrbsTargetHealthOrb" or "rOrbsToTHealthOrb"

            local cbTargetCC = MakeCheckbox(pane,
                "Use Target Class Color",
                "When targeting a player, color the orb with their class color. NPCs use the normal Orb Color.",
                PAD, yo)
            cbTargetCC:SetScript("OnClick", function(self)
                local val = self:GetChecked()
                rOrbsDB[ccKey] = val
                ns.GUI:ChatMsg("|cffffff00Class color|r "..(val and "|cff00ff00enabled|r." or "|cffff4444disabled|r."))
            end)
            Panel[key].cbTargetClassColor = cbTargetCC
            yo = yo - 52

            -- DECORATIVE RING
            local cbRing = MakeCheckbox(pane,
                "Show decorative ring",
                "Show or hide the decorative stone ring/wreath around the orb.",
                PAD, yo)
            cbRing:SetChecked(rOrbsDB[ringKey] ~= false)
            cbRing:SetScript("OnClick", function(self)
                local val = self:GetChecked()
                rOrbsDB[ringKey] = val
                local orb = _G[orbName]
                if orb and orb.ring then
                    if val then orb.ring:Show() else orb.ring:Hide() end
                end
                ns.GUI:ChatMsg("|cffffff00Decorative ring|r "..(val and "|cff00ff00shown|r." or "|cffff4444hidden|r."))
            end)
            Panel[key].cbRing = cbRing ; yo = yo - 50

            -- TARGET PORTRAIT (only for Target, not ToT)
            if cfgKey == "TARGET_HEALTH" then
                local cbTargetPortrait = MakeCheckbox(pane,
                    "Show Target Portrait Orb",
                    "Toggle the separate floating 3D portrait orb that shows your target.",
                    PAD, yo)
                cbTargetPortrait:SetScript("OnClick", function(self)
                    local val = self:GetChecked()
                    rOrbsDB.targetPortraitShow = val
                    ns.GUI:ApplyTargetPortrait(val)
                    ns.GUI:ChatMsg("|cffffff00Target portrait orb|r "..(val and "|cff00ff00shown|r." or "|cffff4444hidden|r."))
                end)
                Panel.cbTargetPortrait = cbTargetPortrait ; yo = yo - 50

                -- TARGET COMBAT ICON TOGGLE
                local cbTargetCombat = MakeCheckbox(pane,
                    "Show Combat Icon (crossed swords)",
                    "Show or hide the crossed swords icon above the target orb when the target is in combat.",
                    PAD, yo)
                cbTargetCombat:SetChecked(rOrbsDB.targetCombatIconShow ~= false)
                cbTargetCombat:SetScript("OnClick", function(self)
                    local val = self:GetChecked()
                    rOrbsDB.targetCombatIconShow = val
                    local f = _G["rOrbsTargetCombatFrame"]
                    if f then
                        if val and UnitExists("target") and UnitAffectingCombat("target") then
                            f:Show()
                        else
                            f:Hide()
                        end
                    end
                    ns.GUI:ChatMsg("|cffffff00Target combat icon|r "..(val and "|cff00ff00shown|r." or "|cffff4444hidden|r."))
                end)
                Panel.cbTargetCombatIcon = cbTargetCombat ; yo = yo - 50

                -- Size / position sliders for Target Combat Icon
                local sTCombatSize, _ = MakeSlider(pane, "Target Combat Icon – Size",
                    "Width and height of the target combat (crossed swords) icon in pixels.",
                    12, 64, 1, PAD + 20, yo, SLIDER_W)
                SetSliderHandler(sTCombatSize, function(self, val)
                    rOrbsDB.targetCombatIconSize = val
                    if ns.UpdateTargetCombatIconLayout then ns.UpdateTargetCombatIconLayout() end
                end)
                sTCombatSize:SetValue(rOrbsDB.targetCombatIconSize or 28)
                Panel[key].sTCombatSize = sTCombatSize ; yo = yo - 46

                local sTCombatOffX, _ = MakeSlider(pane, "Target Combat Icon – Offset X",
                    "Horizontal offset of the target combat icon (added to its default centre-above-orb position).",
                    -200, 200, 1, PAD + 20, yo, SLIDER_W)
                SetSliderHandler(sTCombatOffX, function(self, val)
                    rOrbsDB.targetCombatIconOffX = val
                    if ns.UpdateTargetCombatIconLayout then ns.UpdateTargetCombatIconLayout() end
                end)
                sTCombatOffX:SetValue(rOrbsDB.targetCombatIconOffX or 0)
                Panel[key].sTCombatOffX = sTCombatOffX ; yo = yo - 46

                local sTCombatOffY, _ = MakeSlider(pane, "Target Combat Icon – Offset Y",
                    "Vertical offset of the target combat icon (added to its default position above the orb).",
                    -200, 200, 1, PAD + 20, yo, SLIDER_W)
                SetSliderHandler(sTCombatOffY, function(self, val)
                    rOrbsDB.targetCombatIconOffY = val
                    if ns.UpdateTargetCombatIconLayout then ns.UpdateTargetCombatIconLayout() end
                end)
                sTCombatOffY:SetValue(rOrbsDB.targetCombatIconOffY or 0)
                Panel[key].sTCombatOffY = sTCombatOffY ; yo = yo - 52
            end

            -- ── CAST RING (Target-Health tab only) ──────────────────────────
            if cfgKey == "TARGET_HEALTH" then
                MakeSectionHeader(pane, L["CASTRING_SECTION"], PAD, yo) ; yo = yo - 44

                -- Self-learning info text above the checkbox
                local infoW = PANEL_W - CONTENT_X - PAD - SB_W - 30
                local castRingInfoLbl = MakeInfoText(pane,
                    L["CASTRING_SELFLEARN_INFO"] or
                    "|cffffd700Note – Self-Learning Cast Timing:|r\nWoW does not allow addons to read exact cast durations for enemy targets directly (API restriction). The Cast Ring works around this by running Blizzard's own target castbar silently in the background and reading its timing values (primary method). If those values are not yet available, the ring starts with a learned estimate (initial: 3.0 sec) and automatically refines it after every completed cast — so the first or second cast on a new target may look slightly off. Deselecting the target resets progress tracking; the last learned duration is kept until a new cast is observed.",
                    PAD, yo, infoW)
                local infoH = castRingInfoLbl:GetStringHeight() + 6
                yo = yo - infoH - 8

                -- Enable / disable Cast Ring
                local cbCastRing = MakeCheckbox(pane,
                    L["CASTRING_ENABLE"],
                    L["CASTRING_ENABLE_TIP"],
                    PAD, yo)
                cbCastRing:SetChecked(rOrbsDB.castRingEnabled ~= false)
                cbCastRing:SetScript("OnClick", function(self)
                    local val = self:GetChecked()
                    -- Revert checkbox visually until user confirms
                    self:SetChecked(not val)
                    local isDE = (GetLocale() == "deDE")
                    local stateStr = val
                        and (isDE and (L["CASTRING_RELOAD_ON"] or "aktiviert") or (L["CASTRING_RELOAD_ON"] or "enabled"))
                        or  (isDE and (L["CASTRING_RELOAD_OFF"] or "deaktiviert") or (L["CASTRING_RELOAD_OFF"] or "disabled"))
                    local popupText = L["CASTRING_RELOAD_TEXT"]
                        and string.format(L["CASTRING_RELOAD_TEXT"], stateStr)
                        or  (isDE
                            and ("Cast Ring wurde "..stateStr..".\nEin |cffffd700Reload|r ist erforderlich damit die Änderung wirksam wird.")
                            or  ("Cast Ring "..stateStr..".\nA |cffffd700Reload|r is required for this change to take effect."))
                    StaticPopupDialogs["RORBS_RELOAD_CASTRING"] = {
                        text    = popupText,
                        button1 = "Reload UI",
                        button2 = CANCEL,
                        OnAccept = function()
                            rOrbsDB.castRingEnabled = val
                            ReloadUI()
                        end,
                        OnCancel = function()
                            cbCastRing:SetChecked(rOrbsDB.castRingEnabled ~= false)
                        end,
                        timeout = 0, whileDead = true, hideOnEscape = false,
                    }
                    StaticPopup_Show("RORBS_RELOAD_CASTRING")
                end)
                Panel[key].cbCastRing = cbCastRing ; yo = yo - 50

                -- Ring Thickness
                local sRingScale, _ = MakeSlider(pane, L["CASTRING_RING_SCALE"],
                    L["CASTRING_RING_SCALE_TIP"],
                    0.3, 2.0, 0.05, PAD, yo, SLIDER_W)
                sRingScale:SetValue(rOrbsDB.castRingRingScale or 1.0)
                SetSliderHandler(sRingScale, function(self, val)
                    rOrbsDB.castRingRingScale = val
                    if ns.CastRing then ns.CastRing.ApplyScale() end
                end)
                Panel[key].sRingScale = sRingScale ; yo = yo - 52

                -- Cast Color = Target Class Color (checkbox)
                local cbClassColor = MakeCheckbox(pane,
                    L["CASTRING_USE_CLASSCOLOR"],
                    L["CASTRING_USE_CLASSCOLOR_TIP"],
                    PAD, yo)
                cbClassColor:SetChecked(rOrbsDB.castRingUseClassColor == true)
                cbClassColor:SetScript("OnClick", function(self)
                    rOrbsDB.castRingUseClassColor = self:GetChecked()
                    if ns.CastRing then ns.CastRing.ApplyColors() end
                end)
                Panel[key].cbClassColor = cbClassColor ; yo = yo - 50

                -- Cast Color (Fallback) – label + swatch on same row
                local castColLbl = pane:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
                castColLbl:SetPoint("TOPLEFT", pane, "TOPLEFT", PAD, yo)
                castColLbl:SetText(L["CASTRING_CAST_COLOR"])
                castColLbl:SetTextColor(C.text[1], C.text[2], C.text[3])
                local castSwatch
                castSwatch = MakeColorSwatch(pane, PAD + 150, yo + 2,
                    rOrbsDB.castRingCastR or 1, rOrbsDB.castRingCastG or 0.15, rOrbsDB.castRingCastB or 0.05,
                    L["CASTRING_CAST_COLOR_TIP"],
                    function()
                        ns.ColorPickerHelper:Open({
                            r = rOrbsDB.castRingCastR or 1,
                            g = rOrbsDB.castRingCastG or 0.15,
                            b = rOrbsDB.castRingCastB or 0.05,
                            onChange = function(r, g, b)
                                rOrbsDB.castRingCastR = r
                                rOrbsDB.castRingCastG = g
                                rOrbsDB.castRingCastB = b
                                castSwatch:SetColor(r, g, b)
                                if ns.CastRing then ns.CastRing.ApplyColors() end
                            end,
                        })
                    end)
                Panel[key].castSwatch = castSwatch ; yo = yo - 34

                -- Channel Color – label + swatch on same row
                local chanColLbl = pane:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
                chanColLbl:SetPoint("TOPLEFT", pane, "TOPLEFT", PAD, yo)
                chanColLbl:SetText(L["CASTRING_CHANNEL_COLOR"])
                chanColLbl:SetTextColor(C.text[1], C.text[2], C.text[3])
                local chanSwatch
                chanSwatch = MakeColorSwatch(pane, PAD + 120, yo + 2,
                    rOrbsDB.castRingChanR or 0.3, rOrbsDB.castRingChanG or 0.6, rOrbsDB.castRingChanB or 1,
                    L["CASTRING_CHANNEL_COLOR_TIP"],
                    function()
                        ns.ColorPickerHelper:Open({
                            r = rOrbsDB.castRingChanR or 0.3,
                            g = rOrbsDB.castRingChanG or 0.6,
                            b = rOrbsDB.castRingChanB or 1,
                            onChange = function(r, g, b)
                                rOrbsDB.castRingChanR = r
                                rOrbsDB.castRingChanG = g
                                rOrbsDB.castRingChanB = b
                                chanSwatch:SetColor(r, g, b)
                                if ns.CastRing then ns.CastRing.ApplyColors() end
                            end,
                        })
                    end)
                Panel[key].chanSwatch = chanSwatch ; yo = yo - 40

                -- Compass Rose: show checkbox
                local cbCastBg = MakeCheckbox(pane,
                    L["CASTRING_SHOW_BG"],
                    L["CASTRING_SHOW_BG_TIP"],
                    PAD, yo)
                cbCastBg:SetChecked((rOrbsDB.castRingBgMode or "rose_bright") == "rose_bright")
                cbCastBg:SetScript("OnClick", function(self)
                    local val = self:GetChecked()
                    rOrbsDB.castRingBgMode = val and "rose_bright" or "ring_only"
                    if ns.CastRing then ns.CastRing.ApplyBackground() end
                end)
                Panel[key].cbCastBg = cbCastBg ; yo = yo - 50

                -- Compass Rose Opacity
                local sBgAlpha, _ = MakeSlider(pane, L["CASTRING_BG_ALPHA"],
                    L["CASTRING_BG_ALPHA_TIP"],
                    0, 1, 0.05, PAD, yo, SLIDER_W)
                sBgAlpha:SetValue(rOrbsDB.castRingBgAlpha or 0.8)
                SetSliderHandler(sBgAlpha, function(self, val)
                    rOrbsDB.castRingBgAlpha = val
                    if ns.CastRing then ns.CastRing.ApplyBgAlpha(val) end
                end)
                Panel[key].sBgAlpha = sBgAlpha ; yo = yo - 52

                -- Compass Rose Fade Speed
                local sFadeTime, _ = MakeSlider(pane, L["CASTRING_FADE_SPEED"],
                    L["CASTRING_FADE_SPEED_TIP"],
                    0.1, 1.5, 0.05, PAD, yo, SLIDER_W)
                sFadeTime:SetValue(rOrbsDB.castRingFadeTime or 0.4)
                SetSliderHandler(sFadeTime, function(self, val)
                    rOrbsDB.castRingFadeTime = val
                end)
                Panel[key].sFadeTime = sFadeTime ; yo = yo - 52

                -- Compass Rose Color – label + swatch
                local bgColLbl = pane:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
                bgColLbl:SetPoint("TOPLEFT", pane, "TOPLEFT", PAD, yo)
                bgColLbl:SetText(L["CASTRING_BG_COLOR"])
                bgColLbl:SetTextColor(C.text[1], C.text[2], C.text[3])
                local bgSwatch
                bgSwatch = MakeColorSwatch(pane, PAD + 150, yo + 2,
                    rOrbsDB.castRingBgR or 1, rOrbsDB.castRingBgG or 1, rOrbsDB.castRingBgB or 1,
                    L["CASTRING_BG_COLOR_TIP"],
                    function()
                        ns.ColorPickerHelper:Open({
                            r = rOrbsDB.castRingBgR or 1,
                            g = rOrbsDB.castRingBgG or 1,
                            b = rOrbsDB.castRingBgB or 1,
                            onChange = function(r, g, b)
                                rOrbsDB.castRingBgR = r
                                rOrbsDB.castRingBgG = g
                                rOrbsDB.castRingBgB = b
                                bgSwatch:SetColor(r, g, b)
                                if ns.CastRing then ns.CastRing.ApplyBgColor() end
                            end,
                        })
                    end)
                Panel[key].bgSwatch = bgSwatch ; yo = yo - 34

                -- Compass Rose Color = Class Color
                local cbBgClassColor = MakeCheckbox(pane,
                    L["CASTRING_BG_CLASSCOLOR"],
                    L["CASTRING_BG_CLASSCOLOR_TIP"],
                    PAD, yo)
                cbBgClassColor:SetChecked(rOrbsDB.castRingBgClassColor == true)
                cbBgClassColor:SetScript("OnClick", function(self)
                    rOrbsDB.castRingBgClassColor = self:GetChecked()
                    if ns.CastRing then ns.CastRing.ApplyBgColor() end
                end)
                Panel[key].cbBgClassColor = cbBgClassColor ; yo = yo - 50
            end
            -- ── END CAST RING ───────────────────────────────────────────────
        end

        yo = yo - 16
        -- EFFECTS -------------------------------------------------------------
        MakeSectionHeader(pane, "Effects", PAD, yo)
        yo = yo - 44

        -- GALAXIES
        local sGal, _ = MakeSlider(pane, L["ORB_GALAXIES_ALPHA"], L["ORB_GALAXIES_ALPHA_TIP"],
            0, 1, 0.05, PAD, yo, SLIDER_W)
        SetSliderHandler(sGal, function(self, val)
            ensureCharCfg()
            if ns.db and ns.db.char[cfgKey] then ns.db.char[cfgKey].galaxies.alpha = val end
            if rOrbs_DB_CHAR and rOrbs_DB_CHAR[cfgKey] then rOrbs_DB_CHAR[cfgKey].galaxies.alpha = val end
            ns.GUI:ApplyLayerAlpha(cfgKey, "galaxies", val)
        end)
        Panel[key].sGal = sGal ; yo = yo - 52

        -- BUBBLES
        local sBub, _ = MakeSlider(pane, L["ORB_BUBBLES_ALPHA"], L["ORB_BUBBLES_ALPHA_TIP"],
            0, 1, 0.05, PAD, yo, SLIDER_W)
        SetSliderHandler(sBub, function(self, val)
            ensureCharCfg()
            if ns.db and ns.db.char[cfgKey] then ns.db.char[cfgKey].bubbles.alpha = val end
            if rOrbs_DB_CHAR and rOrbs_DB_CHAR[cfgKey] then rOrbs_DB_CHAR[cfgKey].bubbles.alpha = val end
            ns.GUI:ApplyLayerAlpha(cfgKey, "bubbles", val)
        end)
        Panel[key].sBub = sBub ; yo = yo - 52

        -- SPARK
        local sSpark, _ = MakeSlider(pane, L["ORB_SPARK_ALPHA"], L["ORB_SPARK_ALPHA_TIP"],
            0, 1, 0.05, PAD, yo, SLIDER_W)
        SetSliderHandler(sSpark, function(self, val)
            ensureCharCfg()
            if ns.db and ns.db.char[cfgKey] then ns.db.char[cfgKey].spark.alpha = val end
            if rOrbs_DB_CHAR and rOrbs_DB_CHAR[cfgKey] then rOrbs_DB_CHAR[cfgKey].spark.alpha = val end
            ns.GUI:ApplyLayerAlpha(cfgKey, "spark", val)
        end)
        Panel[key].sSpark = sSpark ; yo = yo - 52

        -- GLOSS
        local sHigh, _ = MakeSlider(pane, L["ORB_HIGHLIGHT_ALPHA"], L["ORB_HIGHLIGHT_ALPHA_TIP"],
            0, 1, 0.05, PAD, yo, SLIDER_W)
        SetSliderHandler(sHigh, function(self, val)
            ensureCharCfg()
            if ns.db and ns.db.char[cfgKey] then ns.db.char[cfgKey].highlight.alpha = val end
            if rOrbs_DB_CHAR and rOrbs_DB_CHAR[cfgKey] then rOrbs_DB_CHAR[cfgKey].highlight.alpha = val end
            ns.GUI:ApplyLayerAlpha(cfgKey, "highlight", val)
        end)
        Panel[key].sHigh = sHigh ; yo = yo - 52

        -- GLOSS TEXTURE SELECTOR
        local glossLabel = pane:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        glossLabel:SetPoint("TOPLEFT", pane, "TOPLEFT", PAD, yo)
        glossLabel:SetText("Gloss Texture")
        glossLabel:SetTextColor(C.text[1], C.text[2], C.text[3])
        yo = yo - 22

        local glossDBKey = "glossTex_"..cfgKey
        local currentGloss = rOrbsDB[glossDBKey] or "new"

        local cbGlossNew = MakeCheckbox(pane, "New Gloss",
            "Use the new gloss texture on this orb.", PAD, yo)
        cbGlossNew:SetChecked(currentGloss ~= "classic")

        local cbGlossClassic = MakeCheckbox(pane, "Classic Gloss",
            "Use the original classic gloss texture on this orb.", PAD + 160, yo)
        cbGlossClassic:SetChecked(currentGloss == "classic")

        -- Mutual exclusion + live apply
        local function applyGloss(choice)
            rOrbsDB[glossDBKey] = choice
            cbGlossNew:SetChecked(choice ~= "classic")
            cbGlossClassic:SetChecked(choice == "classic")
            local orb = ns.GUI and ns.GUI.GetOrb and ns.GUI:GetOrb(cfgKey)
                        or (cfgKey == "HEALTH" and ns.HealthOrb)
                        or (cfgKey == "POWER"  and ns.PowerOrb)
                        or (cfgKey == "TARGET_HEALTH" and ns.TargetHealthOrb)
                        or (cfgKey == "TOT_HEALTH" and ns.ToTHealthOrb)
            if orb and orb.highlight then
                local tex = (choice == "classic")
                    and "Interface\\AddOns\\rOrbs\\media\\orb_gloss"
                    or  "Interface\\AddOns\\rOrbs\\media\\orb_gloss_new"
                orb.highlight:SetTexture(tex)
            end
        end
        cbGlossNew:SetScript("OnClick", function(self)
            applyGloss("new")
            ns.GUI:ChatMsg("|cffffff00Gloss ("..cfgKey..")|r → |cff00ff00New|r")
        end)
        cbGlossClassic:SetScript("OnClick", function(self)
            applyGloss("classic")
            ns.GUI:ChatMsg("|cffffff00Gloss ("..cfgKey..")|r → |cffffff00Classic|r")
        end)
        Panel[key].cbGlossNew     = cbGlossNew
        Panel[key].cbGlossClassic = cbGlossClassic
        yo = yo - 50

        -- ORB TEMPLATE PRESET ------------------------------------------------
        MakeSectionHeader(pane, L["TEMPLATE_SECTION"] or "Orb Template (3D Model Preset)", PAD, yo) ; yo = yo - 44

        -- Show 3D model (built-in animated model)
        local cbOrbModel = MakeCheckbox(pane,
            "Show 3D model",
            "Toggle the animated 3D model displayed inside this orb.",
            PAD, yo)
        cbOrbModel:SetScript("OnClick", function(self)
            local val = self:GetChecked()
            ensureCharCfg()
            if not ns.db or not ns.db.char[cfgKey] then return end
            ns.db.char[cfgKey].model.enable = val
            if rOrbs_DB_CHAR and rOrbs_DB_CHAR[cfgKey] then rOrbs_DB_CHAR[cfgKey].model.enable = val end
            ns.GUI:ApplyOrbModel(cfgKey, val)
            ns.GUI:ChatMsg("|cffffff00"..cfgKey.." model|r "..(val and "|cff00ff00shown|r." or "|cffff4444hidden|r."))
        end)
        Panel[key].cbOrbModel = cbOrbModel ; yo = yo - 50

        -- Model Opacity (applies to built-in 3D model)
        local sOrbModelAlpha, _ = MakeSlider(pane, L["TARGET_MODEL_ALPHA"] or "Model Opacity",
            "Opacity of the 3D model inside this orb.",
            0, 1, 0.05, PAD, yo, SLIDER_W)
        SetSliderHandler(sOrbModelAlpha, function(self, val)
            if cfgKey == "TARGET_HEALTH" then
                rOrbsDB.targetModelAlpha = val
            elseif cfgKey == "TOT_HEALTH" then
                rOrbsDB.totModelAlpha = val
            else
                if ns.db and ns.db.char[cfgKey] then ns.db.char[cfgKey].model.alpha = val end
                if rOrbs_DB_CHAR and rOrbs_DB_CHAR[cfgKey] then rOrbs_DB_CHAR[cfgKey].model.alpha = val end
            end
            ns.GUI:ApplyOrbModelAlpha(cfgKey, val)
        end)
        Panel[key].sOrbModelAlpha = sOrbModelAlpha ; yo = yo - 52

        -- Show / Hide 3D Model Preset
        local cbTemplateShow = MakeCheckbox(pane,
            "Show 3D Model Preset",
            "Show: activates the selected 3D model preset. Hide: removes the preset and restores the normal fill texture.",
            PAD, yo)
        cbTemplateShow:SetScript("OnClick", function(self)
            local val = self:GetChecked()
            rOrbsDB["tmplShow_"..cfgKey] = val
            local orb
            if cfgKey == "HEALTH" then orb = ns.HealthOrb
            elseif cfgKey == "POWER" then orb = ns.PowerOrb
            elseif cfgKey == "TARGET_HEALTH" then orb = ns.TargetHealthOrb
            elseif cfgKey == "TOT_HEALTH" then orb = ns.ToTHealthOrb
            end
            if val then
                local tmplKey = rOrbsDB["orbTemplate_"..cfgKey]
                if tmplKey and tmplKey ~= "none" and ns.OrbTemplateEngine then
                    ns.OrbTemplateEngine:Apply(cfgKey, tmplKey, false)
                end
            else
                if orb and ns.OrbTemplateEngine then
                    ns.OrbTemplateEngine:RemoveTemplate(orb)
                end
                if orb and ns.db and ns.db.char[cfgKey] then
                    local texPath = ns.db.char[cfgKey].filling and ns.db.char[cfgKey].filling.texture or ""
                    ns.GUI:ApplyTexture(cfgKey, texPath)
                end
            end
        end)
        Panel[key].cbTemplateShow = cbTemplateShow ; yo = yo - 50

        local ddTemplate, _ = MakeDropdown(pane,
            L["TEMPLATE_DROPDOWN"] or "Template",
            L["TEMPLATE_DROPDOWN_TIP"] or "Choose a preset 3D model.",
            PAD, yo, 220)
        Panel[key].ddTemplate = ddTemplate
        UIDropDownMenu_Initialize(ddTemplate, function(self, level)
            local list = ns.orbTemplateList
            if not list then return end
            local currentKey = rOrbsDB and rOrbsDB["orbTemplate_"..cfgKey] or "none"
            for _, entry in ipairs(list) do
                local info = UIDropDownMenu_CreateInfo()
                info.text     = entry.label
                info.value    = entry.key
                info.checked  = (entry.key == currentKey)
                info.func = function()
                    UIDropDownMenu_SetSelectedValue(ddTemplate, entry.key)
                    UIDropDownMenu_SetText(ddTemplate, entry.label)
                    if ns.OrbTemplateEngine then
                        ns.OrbTemplateEngine:Apply(cfgKey, entry.key, false)
                    end
                    rOrbsDB["tmplShow_"..cfgKey] = true
                    if Panel[key].cbTemplateShow then Panel[key].cbTemplateShow:SetChecked(true) end
                    ns.GUI:ChatMsg("|cffffff00Template ("..cfgKey..")|r: "..entry.label)
                end
                UIDropDownMenu_AddButton(info, level)
            end
        end)
        do
            local currentKey = rOrbsDB and rOrbsDB["orbTemplate_"..cfgKey] or "none"
            local dispName = "None (Custom)"
            if ns.orbTemplateList then
                for _, entry in ipairs(ns.orbTemplateList) do
                    if entry.key == currentKey then dispName = entry.label; break end
                end
            end
            UIDropDownMenu_SetText(ddTemplate, dispName)
            UIDropDownMenu_SetSelectedValue(ddTemplate, currentKey)
        end
        yo = yo - 56

        -- TEMPLATE FINE-TUNING SLIDERS (directly under template dropdown) ------
        local sTmplScale, _ = MakeSlider(pane, "Scale", "Scale the 3D model. 1.0 = default template size.", 0.1, 3.0, 0.05, PAD, yo, SLIDER_W)
        Panel[key].sTmplScale = sTmplScale
        SetSliderHandler(sTmplScale, function(self, val)
            if rOrbsDB then rOrbsDB["tmplScale_"..cfgKey] = val end
            if ns.OrbTemplateEngine then ns.OrbTemplateEngine:RefreshModel(cfgKey) end
        end)
        sTmplScale:SetValue(1.0)
        yo = yo - 52

        local sTmplOffX, _ = MakeSlider(pane, "Offset X", "Verschiebt das 3D-Modell horizontal (links/rechts).", -1.0, 1.0, 0.01, PAD, yo, SLIDER_W)
        Panel[key].sTmplOffX = sTmplOffX
        SetSliderHandler(sTmplOffX, function(self, val)
            if rOrbsDB then rOrbsDB["tmplOffsetX_"..cfgKey] = val end
            if ns.OrbTemplateEngine then ns.OrbTemplateEngine:RefreshModel(cfgKey) end
        end)
        sTmplOffX:SetValue(0.0)
        yo = yo - 52

        local sTmplOffZ, _ = MakeSlider(pane, "Offset Z", "Verschiebt das 3D-Modell vertikal (oben/unten).", -1.0, 1.0, 0.01, PAD, yo, SLIDER_W)
        Panel[key].sTmplOffZ = sTmplOffZ
        SetSliderHandler(sTmplOffZ, function(self, val)
            if rOrbsDB then rOrbsDB["tmplOffsetZ_"..cfgKey] = val end
            if ns.OrbTemplateEngine then ns.OrbTemplateEngine:RefreshModel(cfgKey) end
        end)
        sTmplOffZ:SetValue(0.0)
        yo = yo - 52

        local sTmplOpacity, _ = MakeSlider(pane, "Opacity", "Opacity of the 3D model (overrides template default).", 0.0, 1.0, 0.05, PAD, yo, SLIDER_W)
        Panel[key].sTmplOpacity = sTmplOpacity
        SetSliderHandler(sTmplOpacity, function(self, val)
            if rOrbsDB then rOrbsDB["tmplOpacity_"..cfgKey] = val end
            if ns.OrbTemplateEngine then ns.OrbTemplateEngine:RefreshModel(cfgKey) end
        end)
        sTmplOpacity:SetValue(1.0)
        yo = yo - 60

        -- ============================================================
        --  CUSTOM TEMPLATE CREATOR
        -- ============================================================
        MakeSectionHeader(pane, L["CUSTOM_TMPL_SECTION"] or "Custom Template Creator", PAD, yo) ; yo = yo - 44

        MakeInfoText(pane,
            (GetLocale() == "deDE")
                and "Gib eine DisplayInfo ID ein und klicke Vorschau um das 3D-Modell live zu testen. IDs findest du im |cffffff00Model Viewer|r-Tab oben."
                or  "Enter a DisplayInfo ID and click Preview to test the 3D model live. Find IDs in the |cffffff00Model Viewer|r tab above.",
            PAD, yo, PANEL_W - CONTENT_X - PAD - SB_W - 30)
        yo = yo - 38

        -- DisplayInfo ID input
        local idLabel = pane:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        idLabel:SetPoint("TOPLEFT", pane, "TOPLEFT", PAD, yo)
        idLabel:SetText(L["CUSTOM_TMPL_ID_LABEL"] or "DisplayInfo ID")
        idLabel:SetTextColor(C.text[1], C.text[2], C.text[3])
        yo = yo - 20

        local idBox = CreateFrame("EditBox", nil, pane, "InputBoxTemplate")
        idBox:SetPoint("TOPLEFT", pane, "TOPLEFT", PAD + 4, yo)
        idBox:SetSize(120, 22)
        idBox:SetAutoFocus(false)
        idBox:SetNumeric(true)
        idBox:SetMaxLetters(10)
        idBox:SetText("")
        idBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
        idBox:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
        Panel[key].idBox = idBox
        yo = yo - 30

        -- Preview CamScale slider
        local sPrevCam, _ = MakeSlider(pane, L["CUSTOM_TMPL_CAMSCALE"] or "Preview CamScale",
            L["CUSTOM_TMPL_CAMSCALE_TIP"] or "Camera distance for preview. Adjusts zoom of the 3D model.",
            0.1, 12.0, 0.1, PAD, yo, SLIDER_W)
        sPrevCam:SetValue(1.0)
        Panel[key].sPrevCam = sPrevCam
        yo = yo - 46

        -- Preview Offset Z slider
        local sPrevPosZ, _ = MakeSlider(pane, "Preview Offset Z",
            (GetLocale() == "deDE")
                and "Vertikale Verschiebung für die Vorschau."
                or  "Vertical offset for the preview model.",
            -8.0, 8.0, 0.01, PAD, yo, SLIDER_W)
        sPrevPosZ:SetValue(0.0)
        Panel[key].sPrevPosZ = sPrevPosZ
        yo = yo - 46

        -- Preview Offset X slider
        local sPrevPosX, _ = MakeSlider(pane, "Preview Offset X",
            (GetLocale() == "deDE")
                and "Horizontale Verschiebung für die Vorschau."
                or  "Horizontal offset for the preview model.",
            -8.0, 8.0, 0.01, PAD, yo, SLIDER_W)
        sPrevPosX:SetValue(0.0)
        Panel[key].sPrevPosX = sPrevPosX

        yo = yo - 46

        -- Helper: run FULL preview (loads the model from scratch)
        local function RunPreview()
            local idText = idBox:GetText()
            local id = tonumber(idText)
            if not id or id <= 0 then return end
            local cam  = sPrevCam:GetValue()
            local posZ = sPrevPosZ:GetValue()
            local posX = sPrevPosX:GetValue()
            if ns.CustomTemplateCreator then
                ns.CustomTemplateCreator:StartPreview(cfgKey, id, cam, posZ)
                if posX ~= 0 then
                    ns.CustomTemplateCreator:LiveUpdate(cfgKey, nil, nil, posX)
                end
                if Panel[key].cbTemplateShow then Panel[key].cbTemplateShow:SetChecked(true) end
            end
        end

        -- Hook slider OnValueChanged to live-update the preview model
        SetSliderHandler(sPrevCam, function(self, val)
            if ns.CustomTemplateCreator and ns.CustomTemplateCreator._active[cfgKey] then
                ns.CustomTemplateCreator:LiveUpdate(cfgKey, val, nil)
            end
        end)
        SetSliderHandler(sPrevPosZ, function(self, val)
            if ns.CustomTemplateCreator and ns.CustomTemplateCreator._active[cfgKey] then
                ns.CustomTemplateCreator:LiveUpdate(cfgKey, nil, val, nil)
            end
        end)
        SetSliderHandler(sPrevPosX, function(self, val)
            if ns.CustomTemplateCreator and ns.CustomTemplateCreator._active[cfgKey] then
                ns.CustomTemplateCreator:LiveUpdate(cfgKey, nil, nil, val)
            end
        end)

        -- Preview + Stop buttons
        local previewBtn = MakeButton(pane, L["CUSTOM_TMPL_PREVIEW"] or "Preview", PAD, yo, 110, 26, 0.10, 0.20, 0.10)
        previewBtn:SetScript("OnClick", function()
            local idText = idBox:GetText()
            local id = tonumber(idText)
            if not id or id <= 0 then
                ns.GUI:ChatMsg("|cffff4444Invalid DisplayInfo ID.|r")
                return
            end
            RunPreview()
            ns.GUI:ChatMsg("|cffffff00Preview|r: DisplayInfo ID |cff00ff00" .. id .. "|r")
            idBox:ClearFocus()
        end)

        local stopBtn = MakeButton(pane, L["CUSTOM_TMPL_STOP"] or "Stop", PAD + 118, yo, 110, 26, 0.40, 0.05, 0.03)
        stopBtn:SetScript("OnClick", function()
            if ns.CustomTemplateCreator then
                ns.CustomTemplateCreator:StopPreview(cfgKey)
            end
            if Panel[key].cbTemplateShow then Panel[key].cbTemplateShow:SetChecked(false) end
            ns.GUI:ChatMsg("|cffffff00Preview stopped.|r")
        end)
        yo = yo - 34

        -- Reset button: centers the 3D model and resets sliders
        local resetBtn = MakeButton(pane,
            (GetLocale() == "deDE") and "Zentrieren" or "Center",
            PAD, yo, 228, 26, 0.15, 0.15, 0.15)
        resetBtn:SetScript("OnClick", function()
            if ns.CustomTemplateCreator then
                ns.CustomTemplateCreator:ResetPreviewToCenter(cfgKey)
                -- Reset preview sliders
                sPrevCam:SetValue(1.0)
                sPrevPosZ:SetValue(0.0)
                if Panel[key].sPrevPosX then Panel[key].sPrevPosX:SetValue(0.0) end
                -- Also reset Template Adjustment sliders
                if Panel[key].sTmplScale   then Panel[key].sTmplScale:SetValue(1.0)  end
                if Panel[key].sTmplOffX    then Panel[key].sTmplOffX:SetValue(0.0)   end
                if Panel[key].sTmplOffZ    then Panel[key].sTmplOffZ:SetValue(0.0)   end
                if Panel[key].sTmplOpacity then Panel[key].sTmplOpacity:SetValue(1.0) end
                ns.GUI:ChatMsg("|cffffff00Model centered.|r")
            end
        end)
        yo = yo - 34

        -- Save Template button
        local saveBtn = MakeButton(pane, L["CUSTOM_TMPL_SAVE"] or "Save Template", PAD, yo, 228, 26, 0.08, 0.15, 0.25)
        saveBtn:SetScript("OnClick", function()
            local idText = idBox:GetText()
            local id = tonumber(idText)
            if not id or id <= 0 then
                ns.GUI:ChatMsg("|cffff4444Enter a DisplayInfo ID first.|r")
                return
            end
            StaticPopupDialogs["RORBS_CUSTOM_TMPL_SAVE"] = {
                text         = L["CUSTOM_TMPL_NAME_PROMPT"] or "Enter a name for this template:",
                button1      = OKAY,
                button2      = CANCEL,
                hasEditBox   = true,
                maxLetters   = 40,
                OnAccept     = function(self)
                    local name = ""
                    if self and self.editBox and self.editBox.GetText then
                        name = self.editBox:GetText()
                    else
                        for i = 1, 4 do
                            local eb = _G["StaticPopup"..i.."EditBox"]
                            if eb and eb:IsShown() then name = eb:GetText(); break end
                        end
                    end
                    if not name or name == "" then return end
                    local cam = sPrevCam:GetValue()
                    local posZ = sPrevPosZ:GetValue()
                    local posX = sPrevPosX:GetValue()
                    local state = {
                        displayInfoID  = id,
                        camScale       = cam,
                        modelOpacity   = 1.0,
                        posAdjustZ     = posZ,
                        posAdjustX     = posX,
                        statusBarColor = {1, 1, 1, 1},
                        sparkColor     = {1, 1, 1, 1},
                    }
                    local ok, savedKey = ns.CustomTemplateCreator:Save(name, state)
                    if ok then
                        -- Stop preview, apply the real saved template
                        ns.CustomTemplateCreator._active[cfgKey] = false
                        ns.orbTemplates["_preview_"] = nil
                        if ns.OrbTemplateEngine then
                            ns.OrbTemplateEngine:Apply(cfgKey, savedKey, false)
                        end
                        rOrbsDB["tmplShow_"..cfgKey] = true
                        if Panel[key].cbTemplateShow then Panel[key].cbTemplateShow:SetChecked(true) end
                        -- Refresh custom dropdown
                        if Panel[key].ddCustomTmpl and Panel[key]._ddCustomTmplInit then
                            UIDropDownMenu_Initialize(Panel[key].ddCustomTmpl, Panel[key]._ddCustomTmplInit)
                            UIDropDownMenu_SetText(Panel[key].ddCustomTmpl, name)
                            UIDropDownMenu_SetSelectedValue(Panel[key].ddCustomTmpl, savedKey)
                        end
                        ns.GUI:ChatMsg((L["CUSTOM_TMPL_SAVED"] or "Custom template saved: ") .. "|cff00ff00" .. name .. "|r")
                    end
                end,
                timeout = 0, whileDead = true, hideOnEscape = true,
            }
            StaticPopup_Show("RORBS_CUSTOM_TMPL_SAVE")
        end)
        yo = yo - 42

        -- My Templates dropdown
        local ddCustomTmpl, _ = MakeDropdown(pane,
            L["CUSTOM_TMPL_DROPDOWN"] or "My Templates",
            L["CUSTOM_TMPL_DROPDOWN_TIP"] or "Choose from your saved custom templates.",
            PAD, yo, 220)
        Panel[key].ddCustomTmpl = ddCustomTmpl

        local function initCustomDropdown(self, level)
            local list = ns.customTemplateList
            if not list or #list == 0 then
                local info = UIDropDownMenu_CreateInfo()
                info.text = "|cff888888-- " .. ((GetLocale() == "deDE") and "keine Vorlagen" or "no templates") .. " --"
                info.disabled = true
                UIDropDownMenu_AddButton(info, level)
                return
            end
            local currentKey = rOrbsDB and rOrbsDB["orbTemplate_"..cfgKey] or "none"
            for _, entry in ipairs(list) do
                local info = UIDropDownMenu_CreateInfo()
                info.text     = entry.label
                info.value    = entry.key
                info.checked  = (entry.key == currentKey)
                info.func = function()
                    UIDropDownMenu_SetSelectedValue(ddCustomTmpl, entry.key)
                    UIDropDownMenu_SetText(ddCustomTmpl, entry.label)
                    if entry.key == "custom-none" then
                        if ns.OrbTemplateEngine then
                            ns.OrbTemplateEngine:Apply(cfgKey, "none", false)
                        end
                        rOrbsDB["tmplShow_"..cfgKey] = false
                        if Panel[key].cbTemplateShow then Panel[key].cbTemplateShow:SetChecked(false) end
                    else
                        ns.orbTemplates["_preview_"] = nil
                        if ns.OrbTemplateEngine then
                            ns.OrbTemplateEngine:Apply(cfgKey, entry.key, false)
                        end
                        rOrbsDB["tmplShow_"..cfgKey] = true
                        if Panel[key].cbTemplateShow then Panel[key].cbTemplateShow:SetChecked(true) end
                        -- Clear preset dropdown display
                        UIDropDownMenu_SetText(ddTemplate, "—")
                        UIDropDownMenu_SetSelectedValue(ddTemplate, nil)
                    end
                    ns.GUI:ChatMsg("|cffffff00Custom Template ("..cfgKey..")|r: "..entry.label)
                end
                UIDropDownMenu_AddButton(info, level)
            end
        end
        Panel[key]._ddCustomTmplInit = initCustomDropdown
        UIDropDownMenu_Initialize(ddCustomTmpl, initCustomDropdown)
        do
            local currentKey = rOrbsDB and rOrbsDB["orbTemplate_"..cfgKey] or "none"
            local dispName = "None (Custom)"
            local isCustom = false
            if ns.customTemplateList then
                for _, entry in ipairs(ns.customTemplateList) do
                    if entry.key == currentKey then
                        dispName = entry.label
                        isCustom = (entry.key ~= "custom-none")
                        break
                    end
                end
            end
            UIDropDownMenu_SetText(ddCustomTmpl, isCustom and dispName or "None (Custom)")
            UIDropDownMenu_SetSelectedValue(ddCustomTmpl, isCustom and currentKey or "custom-none")
        end
        yo = yo - 48

        -- Info text: after saving a template use "Orb Template" section for adjustments
        MakeInfoText(pane,
            (GetLocale() == "deDE")
                and "|cffffff00Tipp:|r Nach dem Speichern einer Vorlage alle weiteren Einstellungen (Skalierung, Versatz, Deckkraft) über den Bereich |cffffff00Orb Template (3D Model Preset)|r weiter oben vornehmen."
                or  "|cffffff00Tip:|r After saving a template, use the |cffffff00Orb Template (3D Model Preset)|r section above to adjust scale, offset, and opacity.",
            PAD, yo, PANEL_W - CONTENT_X - PAD - SB_W - 30)
        yo = yo - 32

        -- Delete button for custom templates
        local delCustomBtn = MakeButton(pane, L["CUSTOM_TMPL_DELETE"] or "Delete", PAD, yo, 110, 26, 0.40, 0.05, 0.03)
        delCustomBtn:SetScript("OnClick", function()
            local selKey = UIDropDownMenu_GetSelectedValue(ddCustomTmpl)
            if not selKey or selKey == "custom-none" then
                ns.GUI:ChatMsg("|cffff4444No custom template selected.|r")
                return
            end
            local selLabel = ""
            if ns.customTemplateList then
                for _, e in ipairs(ns.customTemplateList) do
                    if e.key == selKey then selLabel = e.label; break end
                end
            end
            StaticPopupDialogs["RORBS_CUSTOM_TMPL_DELETE"] = {
                text = string.format(L["CUSTOM_TMPL_DELETE_CONFIRM"] or "Delete custom template '%s'?", selLabel),
                button1 = OKAY,
                button2 = CANCEL,
                OnAccept = function()
                    local activeKey = rOrbsDB["orbTemplate_"..cfgKey]
                    if activeKey == selKey then
                        if ns.OrbTemplateEngine then
                            ns.OrbTemplateEngine:Apply(cfgKey, "none", false)
                        end
                        rOrbsDB["tmplShow_"..cfgKey] = false
                        if Panel[key].cbTemplateShow then Panel[key].cbTemplateShow:SetChecked(false) end
                    end
                    ns.CustomTemplateCreator:Delete(selKey)
                    UIDropDownMenu_Initialize(ddCustomTmpl, Panel[key]._ddCustomTmplInit)
                    UIDropDownMenu_SetText(ddCustomTmpl, "None (Custom)")
                    UIDropDownMenu_SetSelectedValue(ddCustomTmpl, "custom-none")
                    ns.GUI:ChatMsg((L["CUSTOM_TMPL_DELETED"] or "Custom template deleted: ") .. "|cffff4444" .. selLabel .. "|r")
                end,
                timeout = 0, whileDead = true, hideOnEscape = true,
            }
            StaticPopup_Show("RORBS_CUSTOM_TMPL_DELETE")
        end)
        yo = yo - 42

        yo = yo - 16
        -- TEXT SETTINGS (last section) ----------------------------------------
        MakeSectionHeader(pane, L["TEXT_SECTION"] or "Text Settings", PAD, yo) ; yo = yo - 44

        -- TEXT SHOW ----------------------------------------------------------
        local cbText = MakeCheckbox(pane, L["ORB_TEXT_SHOW"], L["ORB_TEXT_SHOW_TIP"], PAD, yo)
        cbText:SetScript("OnClick", function(self)
            local val = self:GetChecked()
            rOrbsDB["textShow_"..cfgKey] = val
            ns.GUI:ApplyTextShow(cfgKey, val)
            ns.GUI:ChatMsg(val and L["CHAT_TEXT_SHOWN"] or L["CHAT_TEXT_HIDDEN"])
        end)
        Panel[key].cbText = cbText
        yo = yo - 50

        -- SHOW TEXT ON HOVER -------------------------------------------------
        local cbHover = MakeCheckbox(pane, "Show on mouseover only",
            "Hide text values normally. Show them only when the mouse hovers over the orb.", PAD + 20, yo)
        cbHover:SetScript("OnClick", function(self)
            local val = self:GetChecked()
            rOrbsDB["textHover_"..cfgKey] = val
            local orbFrame = cfgKey == "HEALTH" and _G["rOrbsHealthOrb"]
                          or cfgKey == "POWER"  and _G["rOrbsPowerOrb"]
                          or cfgKey == "TARGET_HEALTH" and _G["rOrbsTargetHealthOrb"]
                          or cfgKey == "TOT_HEALTH" and _G["rOrbsToTHealthOrb"]
            if orbFrame and orbFrame.values then
                if val then orbFrame.values:Hide()
                else orbFrame.values:Show() end
            end
            ns.GUI:ChatMsg("|cffffff00Text on hover|r "..(val and "|cff00ff00enabled|r." or "|cffff4444disabled|r."))
        end)
        Panel[key].cbHover = cbHover
        yo = yo - 50

        -- TEXT MODE DROPDOWN -------------------------------------------------
        local ddTextMode, _ = MakeDropdown(pane, "Text Display Mode",
            "Choose what information is displayed on the orb.", PAD, yo, 180)
        Panel[key].ddTextMode = ddTextMode
        UIDropDownMenu_Initialize(ddTextMode, function(self, level)
            local currentMode = rOrbsDB["textMode_"..cfgKey] or "BOTH"
            local modes = {
                { text = "Both (HP + %)",  value = "BOTH"    },
                { text = "HP / Value only", value = "VALUE"   },
                { text = "Percent only",   value = "PERCENT" },
            }
            for _, entry in ipairs(modes) do
                local info = UIDropDownMenu_CreateInfo()
                info.text    = entry.text
                info.value   = entry.value
                info.checked = (entry.value == currentMode)
                info.func    = function()
                    UIDropDownMenu_SetSelectedValue(ddTextMode, entry.value)
                    UIDropDownMenu_SetText(ddTextMode, entry.text)
                    StaticPopupDialogs["RORBS_RELOAD_FOR_TEXTMODE"] = {
                        text    = (GetLocale() == "deDE")
                            and "Textmodus wurde geändert.\nEin |cffffd700Reload|r ist erforderlich damit die Änderung sichtbar wird."
                            or  "Text display mode changed.\nA |cffffd700Reload|r is required for the change to take effect.",
                        button1 = "Reload UI",
                        button2 = CANCEL,
                        OnAccept = function()
                            rOrbsDB["textMode_"..cfgKey] = entry.value
                            ReloadUI()
                        end,
                        OnCancel = function()
                            local prev = rOrbsDB["textMode_"..cfgKey] or "BOTH"
                            local modeLabels = { BOTH = "Both (HP + %)", VALUE = "HP / Value only", PERCENT = "Percent only" }
                            UIDropDownMenu_SetText(ddTextMode, modeLabels[prev] or "Both (HP + %)")
                            UIDropDownMenu_SetSelectedValue(ddTextMode, prev)
                        end,
                        timeout = 0, whileDead = true, hideOnEscape = false,
                    }
                    StaticPopup_Show("RORBS_RELOAD_FOR_TEXTMODE")
                end
                UIDropDownMenu_AddButton(info, level)
            end
        end)
        do
            local mode = rOrbsDB["textMode_"..cfgKey] or "BOTH"
            local modeLabels = { BOTH = "Both (HP + %)", VALUE = "HP / Value only", PERCENT = "Percent only" }
            UIDropDownMenu_SetText(ddTextMode, modeLabels[mode] or "Both (HP + %)")
            UIDropDownMenu_SetSelectedValue(ddTextMode, mode)
        end
        yo = yo - 56

        local sFontSize, _ = MakeSlider(pane, L["ORB_FONT_SCALE"], L["ORB_FONT_SCALE_TIP"],
            8, 48, 1, PAD, yo, SLIDER_W)
        SetSliderHandler(sFontSize, function(self, val)
            rOrbsDB["fontSize_"..cfgKey] = val
            ns.GUI:ApplyFontSize(cfgKey, val)
        end)
        Panel[key].sFontSize = sFontSize
        yo = yo - 52

        local sAlpha, _ = MakeSlider(pane, L["ORB_TEXT_ALPHA"], L["ORB_TEXT_ALPHA_TIP"],
            0, 1, 0.05, PAD, yo, SLIDER_W)
        SetSliderHandler(sAlpha, function(self, val)
            rOrbsDB["textAlpha_"..cfgKey] = val
            ns.GUI:ApplyTextAlpha(cfgKey, val)
        end)
        Panel[key].sAlpha = sAlpha
        yo = yo - 52

        -- TEXT COLOR ---------------------------------------------------------
        local tcLabel = pane:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        tcLabel:SetPoint("TOPLEFT", pane, "TOPLEFT", PAD, yo)
        tcLabel:SetText("Text Color")
        tcLabel:SetTextColor(C.text[1], C.text[2], C.text[3])
        yo = yo - 22

        local initTC = { r=1, g=1, b=1 }
        if rOrbsDB and rOrbsDB["textColor_"..cfgKey] then
            initTC = rOrbsDB["textColor_"..cfgKey]
        end

        local tcSwatch
        tcSwatch = MakeColorSwatch(pane, PAD, yo,
            initTC.r, initTC.g, initTC.b, "Click to change text color",
            function()
                local cur = rOrbsDB["textColor_"..cfgKey] or {r=1,g=1,b=1}
                ns.ColorPickerHelper:Open({
                    r = cur.r, g = cur.g, b = cur.b,
                    onChange = function(r, g, b)
                        rOrbsDB["textColor_"..cfgKey] = {r=r, g=g, b=b}
                        tcSwatch:SetColor(r, g, b)
                        ns.GUI:ApplyTextColor(cfgKey, r, g, b)
                    end,
                    onCancel = function()
                        local c = rOrbsDB["textColor_"..cfgKey] or {r=1,g=1,b=1}
                        ns.GUI:ApplyTextColor(cfgKey, c.r, c.g, c.b)
                    end,
                })
            end)
        Panel[key].tcSwatch = tcSwatch

        local resetTcBtn = MakeButton(pane, "Reset Color", PAD + 140, yo, 100, 22, 0.20, 0.06, 0.04)
        resetTcBtn:SetScript("OnClick", function()
            rOrbsDB["textColor_"..cfgKey] = {r=1, g=1, b=1}
            tcSwatch:SetColor(1, 1, 1)
            ns.GUI:ApplyTextColor(cfgKey, 1, 1, 1)
            ns.GUI:ChatMsg("|cffffff00Text color|r reset to default.")
        end)
        yo = yo - 32

        -- FONT DROPDOWN ------------------------------------------------------
        local ddFont, _ = MakeDropdown(pane, L["ORB_FONT"], L["ORB_FONT_TIP"], PAD, yo, 220)
        Panel[key].ddFont = ddFont
        UIDropDownMenu_Initialize(ddFont, function(self, level)
            local fonts = LSM:List("font")
            if not fonts then return end
            local currentFont = rOrbsDB and rOrbsDB["font_"..cfgKey] or "Enchanted Land"
            for _, fname in ipairs(fonts) do
                local path = LSM:Fetch("font", fname)
                if path and path ~= "" then
                    local info = UIDropDownMenu_CreateInfo()
                    info.text = fname ; info.value = fname
                    info.checked = (fname == currentFont)
                    info.func = function()
                        UIDropDownMenu_SetText(ddFont, fname)
                        UIDropDownMenu_SetSelectedValue(ddFont, fname)
                        rOrbsDB["font_"..cfgKey] = fname
                        ns.GUI:ApplyFont(cfgKey, path)
                        ns.GUI:ChatMsg(string.format(L["CHAT_FONT_CHANGED"], fname))
                    end
                    UIDropDownMenu_AddButton(info, level)
                end
            end
        end)
        do
            local fname = rOrbsDB and rOrbsDB["font_"..cfgKey] or "Enchanted Land"
            UIDropDownMenu_SetText(ddFont, fname)
            UIDropDownMenu_SetSelectedValue(ddFont, fname)
        end
        yo = yo - 56

        -- NAME SETTINGS (Target and ToT Orb tabs) --------------------
        if cfgKey == "TARGET_HEALTH" or cfgKey == "TOT_HEALTH" then
            local nkShow  = (cfgKey == "TARGET_HEALTH") and "targetNameShow"       or "totNameShow"
            local nkFSize = (cfgKey == "TARGET_HEALTH") and "targetNameFontSize"   or "totNameFontSize"
            local nkPos   = (cfgKey == "TARGET_HEALTH") and "targetNamePos"        or "totNamePos"
            local nkCC    = (cfgKey == "TARGET_HEALTH") and "targetNameClassColor" or "totNameClassColor"
            local nkOffY  = (cfgKey == "TARGET_HEALTH") and "targetNameOffY"       or "totNameOffY"
            yo = yo - 20   -- extra gap before Target Name header

            MakeSectionHeader(pane, L["TARGET_NAME_SHOW"] or "Target Name", PAD, yo) ; yo = yo - 44

            local cbTargetName = MakeCheckbox(pane, L["TARGET_NAME_SHOW"], L["TARGET_NAME_SHOW_TIP"], PAD, yo)
            cbTargetName:SetScript("OnClick", function(self)
                local val = self:GetChecked()
                rOrbsDB[nkShow] = val
                ns.GUI:ApplyNameShow(cfgKey, val)
                ns.GUI:ChatMsg(val and "|cffffff00Name|r |cff00ff00shown|r." or "|cffffff00Name|r |cffff4444hidden|r.")
            end)
            cbTargetName:SetChecked(rOrbsDB[nkShow] ~= false)
            Panel[key].cbTargetName = cbTargetName
            yo = yo - 50

            -- POSITION DROPDOWN (above / below) --------------------------------
            local ddNamePos, _ = MakeDropdown(pane, L["TARGET_NAME_POS"] or "Name Position",
                L["TARGET_NAME_POS_TIP"] or "Display the target name above or below the orb.",
                PAD, yo, 180)
            Panel[key].ddNamePos = ddNamePos
            UIDropDownMenu_Initialize(ddNamePos, function(self, level)
                local currentPos = rOrbsDB[nkPos] or "TOP"
                local positions = {
                    { text = (GetLocale() == "deDE") and "Oben"  or "Above orb", value = "TOP"    },
                    { text = (GetLocale() == "deDE") and "Unten" or "Below orb", value = "BOTTOM" },
                }
                for _, entry in ipairs(positions) do
                    local info = UIDropDownMenu_CreateInfo()
                    info.text    = entry.text
                    info.value   = entry.value
                    info.checked = (entry.value == currentPos)
                    info.func    = function()
                        UIDropDownMenu_SetSelectedValue(ddNamePos, entry.value)
                        UIDropDownMenu_SetText(ddNamePos, entry.text)
                        rOrbsDB[nkPos] = entry.value
                        ns.GUI:ApplyNamePos(cfgKey, entry.value, rOrbsDB[nkOffY] or -8)
                    end
                    UIDropDownMenu_AddButton(info, level)
                end
            end)
            do
                local pos = rOrbsDB[nkPos] or "TOP"
                local posLabels = { TOP = (GetLocale() == "deDE") and "Oben" or "Above orb",
                                    BOTTOM = (GetLocale() == "deDE") and "Unten" or "Below orb" }
                UIDropDownMenu_SetText(ddNamePos, posLabels[pos] or "Above orb")
                UIDropDownMenu_SetSelectedValue(ddNamePos, pos)
            end
            yo = yo - 56

            -- CLASS COLOR CHECKBOX ----------------------------------------------
            local cbNameClass = MakeCheckbox(pane, L["TARGET_NAME_CLASSCOLOR"] or "Use Class Color",
                L["TARGET_NAME_CLASSCOLOR_TIP"] or "Color the target name by class color for player targets.",
                PAD, yo)
            cbNameClass:SetScript("OnClick", function(self)
                local val = self:GetChecked()
                StaticPopupDialogs["RORBS_RELOAD_NAME_CLASSCOLOR"] = {
                    text = (GetLocale() == "deDE")
                        and "Klassenfarbe für Zielname wurde " .. (val and "aktiviert" or "deaktiviert") .. ".\nEin |cffffd700Reload|r ist erforderlich."
                        or  "Name class color " .. (val and "enabled" or "disabled") .. ".\nA |cffffd700Reload|r is required for the change to take effect.",
                    button1 = "Reload UI",
                    button2 = CANCEL,
                    OnAccept = function()
                        rOrbsDB[nkCC] = val
                        ReloadUI()
                    end,
                    OnCancel = function()
                        cbNameClass:SetChecked(rOrbsDB[nkCC] == true)
                    end,
                    timeout = 0, whileDead = true, hideOnEscape = false,
                }
                StaticPopup_Show("RORBS_RELOAD_NAME_CLASSCOLOR")
            end)
            cbNameClass:SetChecked(rOrbsDB[nkCC] == true)
            Panel[key].cbNameClass = cbNameClass
            yo = yo - 50

            -- FONT SIZE SLIDER --------------------------------------------------
            local sNameSize, _ = MakeSlider(pane, L["TARGET_NAME_FONTSIZE"] or "Name Font Size",
                L["TARGET_NAME_FONTSIZE_TIP"] or "Adjust the font size of the target name text.",
                10, 40, 1, PAD, yo, SLIDER_W)
            sNameSize:SetValue(rOrbsDB[nkFSize] or 22)
            SetSliderHandler(sNameSize, function(self, val)
                rOrbsDB[nkFSize] = val
                ns.GUI:ApplyNameFontSize(cfgKey, val)
            end)
            Panel[key].sNameSize = sNameSize
            yo = yo - 52

            -- Y-OFFSET SLIDER ---------------------------------------------------
            local sNameOffY, _ = MakeSlider(pane, L["TARGET_NAME_OFFY"] or "Name Y-Offset",
                L["TARGET_NAME_OFFY_TIP"] or "Fine-tune the vertical distance of the name from the orb edge.",
                -40, 40, 1, PAD, yo, SLIDER_W)
            sNameOffY:SetValue(rOrbsDB[nkOffY] or -8)
            SetSliderHandler(sNameOffY, function(self, val)
                rOrbsDB[nkOffY] = val
                ns.GUI:ApplyNamePos(cfgKey, rOrbsDB[nkPos] or "TOP", val)
            end)
            Panel[key].sNameOffY = sNameOffY
            yo = yo - 52
        end

        local resetTabLabel
        if cfgKey == "HEALTH" then resetTabLabel = "Reset Health Orb"
        elseif cfgKey == "POWER" then resetTabLabel = "Reset Power Orb"
        elseif cfgKey == "TOT_HEALTH" then resetTabLabel = "Reset ToT Orb"
        else resetTabLabel = "Reset Target Orb" end

        local resetTabBtn = MakeButton(pane, resetTabLabel, PAD, yo, 180, 28, 0.40, 0.05, 0.03)
        resetTabBtn:SetScript("OnClick", function()
            StaticPopupDialogs["RORBS_RESET_ORB_"..cfgKey] = {
                text = "Reset all "..resetTabLabel.." settings to defaults?",
                button1 = OKAY,
                button2 = CANCEL,
                OnAccept = function()
                    -- Restore defaults for this cfgKey from db
                    if ns.db then
                        local defaults = ns.db:GetOrbDefaults()
                        if defaults and defaults[cfgKey] then
                            -- Deep copy defaults into current db
                            local function deepcopy(orig)
                                local copy = {}
                                for k, v in pairs(orig) do
                                    if type(v) == "table" then copy[k] = deepcopy(v)
                                    else copy[k] = v end
                                end
                                return copy
                            end
                            ns.db.char[cfgKey] = deepcopy(defaults[cfgKey])
                            if rOrbs_DB_CHAR then rOrbs_DB_CHAR[cfgKey] = deepcopy(defaults[cfgKey]) end
                        end
                    end
                    -- Reset rOrbsDB keys for this cfgKey
                    rOrbsDB["textShow_"..cfgKey]  = nil
                    rOrbsDB["textAlpha_"..cfgKey] = nil
                    rOrbsDB["textColor_"..cfgKey] = nil
                    rOrbsDB["font_"..cfgKey]       = nil
                    rOrbsDB["fontSize_"..cfgKey]   = nil
                    rOrbsDB["textMode_"..cfgKey]   = nil
                    rOrbsDB["textHover_"..cfgKey]  = nil
                    rOrbsDB["orbTemplate_"..cfgKey] = nil
                    -- Reset name settings for target/tot
                    if cfgKey == "TARGET_HEALTH" then
                        rOrbsDB["targetNameShow"]       = nil
                        rOrbsDB["targetNameFontSize"]   = nil
                        rOrbsDB["targetNamePos"]        = nil
                        rOrbsDB["targetNameClassColor"] = nil
                        rOrbsDB["targetNameOffY"]       = nil
                        rOrbsDB["targetClassColor"]     = nil
                        rOrbsDB["targetRingShow"]       = nil
                    elseif cfgKey == "TOT_HEALTH" then
                        rOrbsDB["totNameShow"]       = nil
                        rOrbsDB["totNameFontSize"]   = nil
                        rOrbsDB["totNamePos"]        = nil
                        rOrbsDB["totNameClassColor"] = nil
                        rOrbsDB["totNameOffY"]       = nil
                        rOrbsDB["totClassColor"]     = nil
                        rOrbsDB["totRingShow"]       = nil
                    end
                    rOrbsDB["tmplShow_"..cfgKey]    = nil
                    rOrbsDB["tmplScale_"..cfgKey]   = nil
                    rOrbsDB["tmplOffsetX_"..cfgKey] = nil
                    rOrbsDB["tmplOffsetZ_"..cfgKey] = nil
                    rOrbsDB["tmplOpacity_"..cfgKey] = nil
                    if ns.OrbTemplateEngine then
                        ns.OrbTemplateEngine:Apply(cfgKey, "none", false)
                    end
                    ns.GUI:ChatMsg("|cffffff00"..resetTabLabel.."|r reset to defaults.")
                    ns.GUI:RefreshValues()
                end,
                timeout = 0, whileDead = true, hideOnEscape = true,
            }
            StaticPopup_Show("RORBS_RESET_ORB_"..cfgKey)
        end)
        pane._contentH = math.abs(yo) + PAD + 40
        if pane._updateThumb then pane._updateThumb() end
    end

    BuildOrbTab("health", "HEALTH")
    BuildOrbTab("power",  "POWER")
    BuildOrbTab("target", "TARGET_HEALTH")
    BuildOrbTab("tot",    "TOT_HEALTH")

    -- ============================================================
    --  TAB: CREDITS / DANKSAGUNG
    -- ============================================================
    -- (Tab "commands" removed – slash commands are now listed in the General tab)

    -- ============================================================
    --  TAB: MODEL VIEWER  (integrated rIngameModelViewer)
    -- ============================================================
    -- This tab embeds a fully interactive WoW model browser that lets
    -- the user browse Display IDs and send them directly to the
    -- Custom Template Creator on any orb tab.
    -- ============================================================

    do
        local mvPane = BuildScrollPane("models")
        -- The model viewer renders directly inside Panel, not inside the
        -- scroll pane child, so we need a fixed-size container anchored to
        -- the scroll pane container frame.
        local mvContainer = contentFrames["models"]   -- the outer container frame

        -- ---- Palette shortcuts (reuse parent C table) ---------------
        local MV = {}   -- model viewer state

        MV.modelSize    = 80           -- px per model cell (smallest = most cells visible)
        MV.page         = 1
        MV.cols         = 0
        MV.rows         = 0
        MV.models       = {}           -- array of PlayerModel frames
        MV.initialized  = false
        MV.filterEmpty  = false    -- hide cells with no model
        MV.emptyCache   = {}       -- [displayId] = true if known empty
        MV.validCache   = {}       -- [displayId] = true if known valid

        -- Cached displayIndex list (sorted desc, same as rIMV)
        -- We reuse the embedded db from rOrbs or build a small inline list.
        -- The full db.lua list from rIngameModelViewer is embedded below.
        MV.displayList  = nil          -- populated lazily

        -- ---- Math helpers ------------------------------------------
        local pi, halfpi = math.pi, math.pi / 2

        local function MV_RoundNumber(n)
            return math.floor(n * 10) / 10
        end

        local function MV_SetOrientation(model, distance, yaw, pitch)
            if not model:HasCustomCamera() then return end
            model._mvDist, model._mvYaw, model._mvPitch = distance, yaw, pitch
            local x = distance * math.cos(yaw) * math.cos(pitch)
            local y = distance * math.sin(-yaw) * math.cos(pitch)
            local z = distance * math.sin(-pitch)
            model:SetCameraPosition(x, y, z)
        end

        local function MV_ResetModel(model)
            model._mvCamDist = 1
            model._mvZoom    = 0
            model._mvRotation = 0
            model:SetPortraitZoom(0)
            model:SetCamDistanceScale(1)
            model:SetPosition(0, 0, 0)
            model:SetRotation(0)
            model:RefreshCamera()
            model:SetCustomCamera(1)
            if model:HasCustomCamera() then
                local x, y, z = model:GetCameraPosition()
                if x ~= 0 then
                    MV_SetOrientation(model, math.sqrt(x*x + y*y + z*z),
                        -math.atan(y / x), -math.atan(z / x))
                end
            end
        end

        -- ---- Mouse handlers ----------------------------------------
        local function MV_CameraOnUpdate(model)
            if not model:HasCustomCamera() then return end
            local x, y = GetCursorPosition()
            local pitch = model._mvPitch + (y - model._mvCY) * pi / 256
            local limit = pitch > halfpi - 0.05 or pitch < -halfpi + 0.05
            if limit then
                local rot = math.abs(math.deg(((x - model._mvCX)/64 + model:GetFacing())) % 360)
                model:SetRotation(math.rad(rot))
                model._mvRotation = rot
            else
                MV_SetOrientation(model, model._mvDist,
                    model._mvYaw + (x - model._mvCX)*pi/256, pitch)
            end
            model._mvCX, model._mvCY = x, y
        end

        local function MV_RotationOnUpdate(model)
            local x, y = GetCursorPosition()
            local rot = math.abs(math.deg(((x - model._mvCX)/84 + model:GetFacing())) % 360)
            model:SetRotation(math.rad(rot))
            model._mvRotation = rot
            model._mvCX, model._mvCY = x, y
        end

        local function MV_PositionOnUpdate(model)
            local x, y = GetCursorPosition()
            local px, py, pz = model:GetPosition()
            if IsShiftKeyDown() then
                model:SetPosition(tonumber(string.format("%.2f", px + (y - model._mvCY)/100)), py, pz)
            else
                model:SetPosition(px,
                    tonumber(string.format("%.2f", py + (x - model._mvCX)/84)),
                    tonumber(string.format("%.2f", pz + (y - model._mvCY)/84)))
            end
            model._mvCX, model._mvCY = x, y
        end

        local function MV_OnMouseDown(model, button)
            if button == "LeftButton" then
                if IsShiftKeyDown() then
                    if model:HasCustomCamera() then
                        model._mvCX, model._mvCY = GetCursorPosition()
                        model:SetScript("OnUpdate", MV_CameraOnUpdate)
                    end
                else
                    -- send Display ID to a Custom Template Creator tab
                    local displayId = model._mvDisplayId
                    if displayId and displayId > 0 then
                        -- Try to set idBox on the currently active orb tab
                        local targetKeys = {"health","power","target","tot"}
                        for _, tk in ipairs(targetKeys) do
                            if Panel[tk] and Panel[tk].idBox then
                                Panel[tk].idBox:SetText(tostring(displayId))
                            end
                        end
                        -- Switch to health tab so user sees it
                        ns.GUI:SetTab("health")
                        ns.GUI:ChatMsg(
                            (GetLocale() == "deDE" and "|cffffff00Display ID|r |cff00ff00"..displayId.."|r → Vorlage-Ersteller"
                            or "|cffffff00Display ID|r |cff00ff00"..displayId.."|r → Custom Template Creator"))
                    end
                end
            elseif button == "RightButton" then
                if IsControlKeyDown() then
                    MV_ResetModel(model)
                else
                    model._mvCX, model._mvCY = GetCursorPosition()
                    model:SetScript("OnUpdate", MV_RotationOnUpdate)
                end
            elseif button == "MiddleButton" or button == "Button4" or button == "Button5" then
                model._mvCX, model._mvCY = GetCursorPosition()
                model:SetScript("OnUpdate", MV_PositionOnUpdate)
            end
        end

        local function MV_OnMouseUp(model)
            model:SetScript("OnUpdate", nil)
        end

        local function MV_OnWheel(model, delta)
            if model:HasCustomCamera() then
                model._mvDist = math.max(0.1, math.min(40, (model._mvDist or 5) - delta * 0.15))
                MV_SetOrientation(model, model._mvDist, model._mvYaw or 0, model._mvPitch or 0)
            else
                if IsShiftKeyDown() then
                    model._mvZoom = math.max(0, math.min(1, (model._mvZoom or 0) + delta*0.15))
                    model:SetPortraitZoom(model._mvZoom)
                else
                    model._mvCamDist = math.max(0.1, math.min(10, (model._mvCamDist or 1) - delta*0.15))
                    model:SetCamDistanceScale(model._mvCamDist)
                end
            end
        end

        local function MV_OnEnter(model)
            if not IsAltKeyDown() then return end
            local pz, px, py = model:GetPosition()
            GameTooltip:SetOwner(model, "ANCHOR_CURSOR")
            GameTooltip:AddLine("Model Info", 0, 1, 0.5)
            GameTooltip:AddLine(" ")
            GameTooltip:AddDoubleLine("DisplayID",     tostring(model._mvDisplayId or "?"),  1,1,1, 1,1,1)
            GameTooltip:AddDoubleLine("SetPosition",   string.format("(%.1f, %.1f, %.1f)", MV_RoundNumber(pz), MV_RoundNumber(px), MV_RoundNumber(py)), 1,1,1, 1,1,1)
            GameTooltip:AddDoubleLine("SetRotation",   tostring(model._mvRotation or 0),     1,1,1, 1,1,1)
            GameTooltip:AddDoubleLine("ModelFileID",   tostring(model._mvFileId   or "?"),   1,1,1, 1,1,1)
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("|cffffff00Left-click|r  → send ID to Template Creator", 0.8,0.8,0.8)
            GameTooltip:AddLine("|cffffff00Shift+drag|r  → orbit camera",              0.8,0.8,0.8)
            GameTooltip:AddLine("|cffffff00Right-drag|r  → rotate",                    0.8,0.8,0.8)
            GameTooltip:AddLine("|cffffff00Ctrl+Right|r  → reset",                     0.8,0.8,0.8)
            GameTooltip:AddLine("|cffffff00Wheel|r       → zoom",                      0.8,0.8,0.8)
            GameTooltip:Show()
        end

        local function MV_OnLeave()
            GameTooltip:Hide()
        end

        -- ---- Create a model cell ------------------------------------
        local function MV_CreateModel(parent, id)
            local cell = CreateFrame("Frame", nil, parent)
            cell:EnableMouse(true)
            cell:EnableMouseWheel(true)
            cell._mvId = id

            -- border
            local border = cell:CreateTexture(nil, "BACKGROUND", nil, -2)
            border:SetAllPoints()
            border:SetColorTexture(C.border[1], C.border[2], C.border[3], 0.5)

            -- background
            local bg = cell:CreateTexture(nil, "BACKGROUND", nil, -1)
            bg:SetPoint("TOPLEFT",  cell, "TOPLEFT",  1, -1)
            bg:SetPoint("BOTTOMRIGHT", cell, "BOTTOMRIGHT", -1, 1)
            bg:SetColorTexture(0.06, 0.04, 0.03, 1)
            cell._mvBg = bg

            -- hover glow
            local hover = cell:CreateTexture(nil, "ARTWORK", nil, 0)
            hover:SetAllPoints()
            hover:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0)
            cell._mvHover = hover

            -- PlayerModel
            local model = CreateFrame("PlayerModel", nil, cell)
            model:SetPoint("TOPLEFT",  cell, "TOPLEFT",  1, -1)
            model:SetPoint("BOTTOMRIGHT", cell, "BOTTOMRIGHT", -1, 1)
            model:EnableMouse(true)
            model:EnableMouseWheel(true)
            model._mvParentCell = cell
            model._mvDisplayId  = 0
            model._mvFileId     = 0
            model._mvCamDist    = 1
            model._mvZoom       = 0
            model._mvRotation   = 0
            model:SetScript("OnMouseDown", MV_OnMouseDown)
            model:SetScript("OnMouseUp",   MV_OnMouseUp)
            model:SetScript("OnMouseWheel",MV_OnWheel)
            model:SetScript("OnEnter",     MV_OnEnter)
            model:SetScript("OnLeave",     MV_OnLeave)
            cell._mvModel = model

            -- Display ID label at bottom of cell
            local idLbl = cell:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            idLbl:SetPoint("BOTTOM", cell, "BOTTOM", 0, 3)
            idLbl:SetTextColor(C.textDim[1]*1.5, C.textDim[2]*1.5, C.textDim[3]*1.5)
            idLbl:SetText("")
            cell._mvIdLbl = idLbl

            cell:SetScript("OnEnter", function(self)
                hover:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.15)
                border:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.9)
            end)
            cell:SetScript("OnLeave", function(self)
                hover:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0)
                border:SetColorTexture(C.border[1], C.border[2], C.border[3], 0.5)
            end)

            return cell
        end

        -- ---- Update a model cell with a display ID ------------------
        local function MV_UpdateCell(cell, displayId)
            local model = cell._mvModel
            if not model then return end
            cell._mvDisplayId   = displayId
            model._mvDisplayId  = displayId
            -- Load model
            model:ClearModel()
            cell:Show()
            if displayId > 0 then
                model:SetDisplayInfo(displayId)
                -- Filter mode: check after short delay if model actually loaded
                if MV.filterEmpty then
                    model:SetScript("OnModelLoaded", function(self)
                        if self._mvDisplayId ~= displayId then return end
                        local fileId = self:GetModelFileID()
                        if fileId and fileId ~= 0 then
                            MV.validCache[displayId] = true
                            MV.emptyCache[displayId] = nil
                        else
                            MV.emptyCache[displayId] = true
                            MV.validCache[displayId] = nil
                            cell:Hide()
                        end
                        self:SetScript("OnModelLoaded", nil)
                    end)
                    -- Fallback timer: if OnModelLoaded never fires → empty
                    C_Timer.After(0.5, function()
                        if cell._mvDisplayId ~= displayId then return end
                        if MV.validCache[displayId] or MV.emptyCache[displayId] then return end
                        local fileId = model:GetModelFileID()
                        if not fileId or fileId == 0 then
                            MV.emptyCache[displayId] = true
                            cell:Hide()
                        else
                            MV.validCache[displayId] = true
                        end
                    end)
                else
                    model:SetScript("OnModelLoaded", nil)
                end
                local fileId = model:GetModelFileID()
                model._mvFileId = fileId or 0
            end
            MV_ResetModel(model)
            cell._mvIdLbl:SetText(tostring(displayId))
        end

        -- ---- Update all visible cells --------------------------------
        local function MV_UpdateGrid()
            if not mvContainer or not mvContainer:IsShown() then return end
            local containerW = mvContainer:GetWidth()  - 10  -- minus scrollbar
            local containerH = mvContainer:GetHeight() - 66  -- minus toolbar

            MV.cols = math.max(1, math.floor(containerW / MV.modelSize))
            MV.rows = math.max(1, math.floor(containerH / MV.modelSize))
            local count = MV.cols * MV.rows
            local startId = ((MV.page - 1) * count) + 1

            -- Update page editbox
            if MV.pageBox then MV.pageBox:SetText(tostring(MV.page)) end
            if MV.displayIdBox then MV.displayIdBox:SetText(tostring(startId)) end

            -- Position and populate cells
            for idx = 1, count do
                if not MV.models[idx] then
                    MV.models[idx] = MV_CreateModel(MV.gridFrame, idx)
                end
                local cell = MV.models[idx]
                local col = ((idx-1) % MV.cols)
                local row = math.floor((idx-1) / MV.cols)
                cell:SetSize(MV.modelSize, MV.modelSize)
                cell:ClearAllPoints()
                cell:SetPoint("TOPLEFT", MV.gridFrame, "TOPLEFT",
                    col * MV.modelSize, -row * MV.modelSize)
                local displayId = startId + (idx - 1)
                MV_UpdateCell(cell, displayId)
                cell:Show()
            end

            -- Hide unused cells
            for idx = count + 1, #MV.models do
                MV.models[idx]:Hide()
            end

            -- Resize grid frame
            MV.gridFrame:SetHeight(MV.rows * MV.modelSize + 4)
        end

        -- ---- Build Model Viewer UI inside the container -------------
        local function MV_Build()
            if MV.initialized then return end
            MV.initialized = true

            -- Toolbar at the bottom of the container
            local toolbar = CreateFrame("Frame", nil, mvContainer)
            toolbar:SetHeight(54)
            toolbar:SetPoint("BOTTOMLEFT",  mvContainer, "BOTTOMLEFT",  4, 4)
            toolbar:SetPoint("BOTTOMRIGHT", mvContainer, "BOTTOMRIGHT", -(SB_W+6), 4)
            local toolbarBg = toolbar:CreateTexture(nil, "BACKGROUND")
            toolbarBg:SetAllPoints()
            toolbarBg:SetColorTexture(C.bgDeep[1], C.bgDeep[2], C.bgDeep[3], 0.95)
            local toolbarLine = toolbar:CreateTexture(nil, "BORDER")
            toolbarLine:SetHeight(1)
            toolbarLine:SetPoint("TOPLEFT")
            toolbarLine:SetPoint("TOPRIGHT")
            toolbarLine:SetColorTexture(C.border[1], C.border[2], C.border[3], 0.6)

            -- Grid frame (fills container above toolbar)
            MV.gridFrame = CreateFrame("Frame", nil, mvContainer)
            MV.gridFrame:SetPoint("TOPLEFT",     mvContainer, "TOPLEFT",     4,  -4)
            MV.gridFrame:SetPoint("TOPRIGHT",    mvContainer, "TOPRIGHT",    -(SB_W+6), -4)
            MV.gridFrame:SetPoint("BOTTOM",      toolbar,     "TOP",         0,  2)

            -- Info text above toolbar (inside grid frame bottom area)
            local infoLbl = mvContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            infoLbl:SetPoint("BOTTOMLEFT",  toolbar, "TOPLEFT",  4, 2)
            infoLbl:SetPoint("BOTTOMRIGHT", toolbar, "TOPRIGHT", -4, 2)
            infoLbl:SetJustifyH("LEFT")
            infoLbl:SetTextColor(C.textDim[1]*1.2, C.textDim[2]*1.2, C.textDim[3]*1.2)
            infoLbl:SetText(GetLocale() == "deDE"
                and "|cffffff00Linksklick|r → ID senden  |cffffff00Shift+Ziehen|r → Kamera  |cffffff00Rechts-Ziehen|r → Rotieren  |cffffff00Mausrad|r → Zoom  |cffffff00Alt+Hover|r → Details  |cffffff00Ctrl+Rechts|r → Reset"
                or  "|cffffff00Left-click|r → send ID  |cffffff00Shift+drag|r → camera  |cffffff00Right-drag|r → rotate  |cffffff00Wheel|r → zoom  |cffffff00Alt+hover|r → details  |cffffff00Ctrl+Right|r → reset")

            -- Helper: make a styled toolbar button
            local function MkTBtn(text, w, tipText)
                local btn = MakeButton(toolbar, text, 0, 0, w, 26, 0.15, 0.05, 0.04)
                btn:SetPoint("TOP", toolbar, "TOP", 0, -4)  -- repositioned after
                if tipText then
                    btn:SetScript("OnEnter", function(s) Tip(s, tipText) end)
                    btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
                end
                return btn
            end

            -- Prev button
            local btnPrev = MkTBtn(L["MODELS_PREV"] or "< Prev", 64,
                GetLocale()=="deDE" and "Vorherige Seite" or "Previous page")
            btnPrev:ClearAllPoints()
            btnPrev:SetPoint("LEFT", toolbar, "LEFT", 8, 4)
            btnPrev:SetScript("OnClick", function()
                MV.page = math.max(1, MV.page - 1)
                MV_UpdateGrid()
            end)

            -- Page label + editbox
            local pageLbl = toolbar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            pageLbl:SetPoint("LEFT", btnPrev, "RIGHT", 6, 0)
            pageLbl:SetTextColor(C.textDim[1]*1.4, C.textDim[2]*1.4, C.textDim[3]*1.4)
            pageLbl:SetText(L["MODELS_PAGE"] or "Page")

            local pageBox = CreateFrame("EditBox", nil, toolbar, "InputBoxTemplate")
            pageBox:SetSize(50, 22)
            pageBox:SetPoint("LEFT", pageLbl, "RIGHT", 4, 0)
            pageBox:SetAutoFocus(false)
            pageBox:SetNumeric(true)
            pageBox:SetMaxLetters(6)
            pageBox:SetText("1")
            pageBox:SetScript("OnEscapePressed", function(s) s:ClearFocus() end)
            pageBox:SetScript("OnEnterPressed", function(s)
                local v = tonumber(s:GetText())
                if v and v >= 1 then
                    MV.page = math.floor(v)
                    MV_UpdateGrid()
                end
                s:ClearFocus()
            end)
            MV.pageBox = pageBox

            -- Next button
            local btnNext = MkTBtn(L["MODELS_NEXT"] or "Next >", 64,
                GetLocale()=="deDE" and "Nächste Seite" or "Next page")
            btnNext:ClearAllPoints()
            btnNext:SetPoint("LEFT", pageBox, "RIGHT", 6, 0)
            btnNext:SetScript("OnClick", function()
                MV.page = MV.page + 1
                MV_UpdateGrid()
            end)

            -- DisplayId jump box
            local dispIdLbl = toolbar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            dispIdLbl:SetPoint("LEFT", btnNext, "RIGHT", 14, 0)
            dispIdLbl:SetTextColor(C.textDim[1]*1.4, C.textDim[2]*1.4, C.textDim[3]*1.4)
            dispIdLbl:SetText(L["MODELS_DISPLAY_ID"] or "Display ID")

            local dispIdBox = CreateFrame("EditBox", nil, toolbar, "InputBoxTemplate")
            dispIdBox:SetSize(70, 22)
            dispIdBox:SetPoint("LEFT", dispIdLbl, "RIGHT", 4, 0)
            dispIdBox:SetAutoFocus(false)
            dispIdBox:SetNumeric(true)
            dispIdBox:SetMaxLetters(8)
            dispIdBox:SetText("1")
            dispIdBox:SetScript("OnEscapePressed", function(s) s:ClearFocus() end)
            dispIdBox:SetScript("OnEnterPressed", function(s)
                local displayId = tonumber(s:GetText())
                if displayId and displayId >= 1 then
                    local count = math.max(1, MV.cols) * math.max(1, MV.rows)
                    MV.page = math.max(1, math.ceil(displayId / count))
                    MV_UpdateGrid()
                end
                s:ClearFocus()
            end)
            MV.displayIdBox = dispIdBox

            -- Size buttons
            local btnMinus = MkTBtn(L["MODELS_SIZE_MINUS"] or "- Size", 60,
                GetLocale()=="deDE" and "Modellgröße verkleinern" or "Decrease model cell size")
            btnMinus:ClearAllPoints()
            btnMinus:SetPoint("LEFT", dispIdBox, "RIGHT", 14, 0)
            btnMinus:SetScript("OnClick", function()
                MV.modelSize = math.max(80, MV.modelSize - 20)
                MV_UpdateGrid()
            end)

            local btnPlus = MkTBtn(L["MODELS_SIZE_PLUS"] or "+ Size", 60,
                GetLocale()=="deDE" and "Modellgröße vergrößern" or "Increase model cell size")
            btnPlus:ClearAllPoints()
            btnPlus:SetPoint("LEFT", btnMinus, "RIGHT", 4, 0)
            btnPlus:SetScript("OnClick", function()
                MV.modelSize = math.min(400, MV.modelSize + 20)
                MV_UpdateGrid()
            end)

            -- "Hide empty" checkbox
            local cbEmpty = CreateFrame("CheckButton", nil, toolbar, "UICheckButtonTemplate")
            cbEmpty:SetSize(24, 24)
            cbEmpty:SetPoint("LEFT", btnPlus, "RIGHT", 14, 0)
            cbEmpty:SetChecked(false)
            local cbEmptyLbl = toolbar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            cbEmptyLbl:SetPoint("LEFT", cbEmpty, "RIGHT", 2, 0)
            cbEmptyLbl:SetTextColor(C.textDim[1]*1.4, C.textDim[2]*1.4, C.textDim[3]*1.4)
            cbEmptyLbl:SetText(GetLocale()=="deDE" and "Leere ausblenden" or "Hide empty")
            cbEmpty:SetScript("OnClick", function(self)
                MV.filterEmpty = self:GetChecked()
                -- Clear caches when toggling so re-check happens fresh
                if MV.filterEmpty then
                    MV.emptyCache = {}
                    MV.validCache = {}
                end
                MV_UpdateGrid()
            end)

            -- Row 2 of toolbar: send ID target selector
            local sendLbl = toolbar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            sendLbl:SetPoint("BOTTOMLEFT", toolbar, "BOTTOMLEFT", 8, 6)
            sendLbl:SetTextColor(C.textDim[1]*1.4, C.textDim[2]*1.4, C.textDim[3]*1.4)
            sendLbl:SetText(GetLocale()=="deDE"
                and "ID senden an → Klicke ein Modell an um die ID zu übertragen  |cffaaaaaa(Orb-Tab manuell wechseln um Vorlage zu speichern)|r"
                or  "Click any model to send its Display ID to all Custom Template Creator input boxes  |cffaaaaaa(switch orb tab to preview/save)|r")

            -- Initial render triggered when tab becomes visible
            mvContainer:HookScript("OnShow", function()
                C_Timer.After(0.05, function()
                    if MV.initialized and MV.gridFrame then
                        MV_UpdateGrid()
                    end
                end)
            end)
        end

        -- Build info header in the scroll pane child (static text above the model grid area)
        local ymv = -PAD
        MakeSectionHeader(mvPane, L["TAB_MODELS"] or "Model Viewer", PAD, ymv) ; ymv = ymv - 30
        MakeInfoText(mvPane,
            GetLocale() == "deDE"
                and "Klicke auf ein Modell um seine Display-ID direkt in alle Vorlage-Ersteller-Eingabefelder zu übertragen, dann wechsle zum gewünschten Orb-Tab für Vorschau & Speichern."
                or  "Click any model to send its Display ID to all Custom Template Creator inputs. Then switch to the desired orb tab to preview and save.",
            PAD, ymv, PANEL_W - CONTENT_X - PAD - SB_W - 30)
        ymv = ymv - 50

        -- Note: the actual model grid is built lazily when the tab is first shown.
        -- We trigger the build by hooking the container OnShow via MV_Build().
        -- Register a deferred init so the container frame exists before we try to
        -- hook it.
        mvPane._contentH = 100
        if mvPane._updateThumb then mvPane._updateThumb() end

        -- Hook: when the models tab container is shown, lazily init the viewer
        local _mvBuildScheduled = false
        local origContainerShow = contentFrames["models"].SetScript
        -- Use a one-shot OnShow on the outer container
        contentFrames["models"]:HookScript("OnShow", function()
            if not _mvBuildScheduled then
                _mvBuildScheduled = true
                C_Timer.After(0.02, MV_Build)
            end
        end)
    end  -- do block for Model Viewer

    return Panel
end

-- ============================================================
--  FADE
-- ============================================================

local function FadeIn(frame, dur)
    frame:SetAlpha(0) ; frame:Show()
    local e = 0
    frame:SetScript("OnUpdate", function(self, dt)
        e = e + dt
        local a = math.min(e / dur, 1)
        self:SetAlpha(a)
        if a >= 1 then self:SetScript("OnUpdate", nil) end
    end)
end

local function FadeOut(frame, dur, cb)
    local startA = frame:GetAlpha()
    local e = 0
    frame:SetScript("OnUpdate", function(self, dt)
        e = e + dt
        local pct = math.min(e / dur, 1)
        self:SetAlpha(startA * (1 - pct))
        if pct >= 1 then
            self:SetScript("OnUpdate", nil)
            self:Hide() ; self:SetAlpha(0)
            if cb then cb() end
        end
    end)
end

-- ============================================================
--  GUI CONTROLLER
-- ============================================================

ns.GUI = {}
local GUI = ns.GUI

function GUI:ChatMsg(msg)  print(L["CHAT_PREFIX"]..msg) end
function GUI:Debug(msg)    if ns.debug then print(L["DEBUG_PREFIX"]..tostring(msg)) end end

function GUI:Open()
    CreateMainPanel()
    GUI:RefreshValues()
    GUI:SetTab(activeTab)
    if not Panel:IsShown() then FadeIn(Panel, 0.20) end
end

function GUI:Close()
    if Panel and Panel:IsShown() then FadeOut(Panel, 0.18) end
end

function GUI:Toggle()
    if Panel and Panel:IsShown() then GUI:Close() else GUI:Open() end
end

function GUI:SetTab(key)
    activeTab = key
    for k, sf in pairs(contentFrames) do
        if k == key then
            sf:Show()
            local child = sf._sf and sf._sf:GetScrollChild()
            if child and child._updateThumb then
                local fn = child._updateThumb
                sf:SetScript("OnUpdate", function(self)
                    self:SetScript("OnUpdate", nil)
                    fn()
                end)
            end
        else
            sf:Hide()
        end
    end
    if not Panel then return end
    -- Update content header
    if Panel.contentHeader then
        for _, tab in pairs(Panel.tabs or {}) do
            if tab.key == key and tab.label then
                Panel.contentHeader:SetText(tab.label)
                break
            end
        end
    end
    for k, tab in pairs(Panel.tabs or {}) do
        if k == key then
            tab.bg:SetColorTexture(C.accent[1]*0.22, 0.04, 0.04, 1)
            tab.fs:SetTextColor(1, 0.95, 0.93, 1)
            tab.underline:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 1)
            if tab.icon then tab.icon:SetVertexColor(1, 0.85, 0.85, 1) end
        else
            tab.bg:SetColorTexture(C.tabIdle[1], C.tabIdle[2], C.tabIdle[3], 1)
            tab.fs:SetTextColor(0.75, 0.73, 0.72, 1)
            tab.underline:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0)
            if tab.icon then tab.icon:SetVertexColor(0.50, 0.50, 0.50, 1) end
        end
    end
end

function GUI:RefreshValues()
    if not Panel or not rOrbsDB then return end
    db = ns.db

    if Panel.cbMinimap then Panel.cbMinimap:SetChecked(not (rOrbsDB.minimap and rOrbsDB.minimap.hide)) end
    if Panel.cbDebug   then Panel.cbDebug:SetChecked(rOrbsDB.debug == true) end

    -- Refresh all three orb tabs (wrapped in local function for Lua 5.1 early-exit)
    local function refreshTab(key, cfgKey)
        local w = Panel[key]
        if not w or not db then return end
        -- Ensure db.char[cfgKey] exists
        if db.char and not db.char[cfgKey] then
            local defaults = db:GetOrbDefaults()
            if defaults and defaults[cfgKey] then
                db.char[cfgKey] = defaults[cfgKey]
                if rOrbs_DB_CHAR then rOrbs_DB_CHAR[cfgKey] = db.char[cfgKey] end
            end
        end
        if not db.char or not db.char[cfgKey] then return end
        local c = db.char[cfgKey]

        -- Texture dropdown
        if w.ddTex then
            local texPath = c.filling.texture or ""
            local dispName = texPath:match("[^\\]+$") or "?"
            for _, entry in ipairs(db.getListFillingTexture()) do
                if entry.value == texPath then dispName = entry.key; break end
            end
            UIDropDownMenu_SetText(w.ddTex, dispName)
            UIDropDownMenu_SetSelectedValue(w.ddTex, texPath)
        end
        if w.colSwatch then
            local col = c.filling.color
            w.colSwatch:SetColor(col.r, col.g, col.b)
        end
        if w.cbAuto  then w.cbAuto:SetChecked(c.filling.colorAuto == true) end
        if w.cbTargetClassColor then
            local ccRefKey = (cfgKey == "TARGET_HEALTH") and "targetClassColor" or (cfgKey == "TOT_HEALTH") and "totClassColor"
            if ccRefKey then w.cbTargetClassColor:SetChecked(rOrbsDB[ccRefKey] == true) end
        end
        if w.cbText  then
            local val = rOrbsDB["textShow_"..cfgKey]
            w.cbText:SetChecked(val == nil or val == true)
        end
        if w.cbHover then w.cbHover:SetChecked(rOrbsDB["textHover_"..cfgKey] == true) end
        if w.ddTextMode then
            local mode = rOrbsDB["textMode_"..cfgKey] or "BOTH"
            local modeLabels = { BOTH = "Both (HP + %)", VALUE = "HP / Value only", PERCENT = "Percent only" }
            UIDropDownMenu_SetText(w.ddTextMode, modeLabels[mode] or "Both (HP + %)")
            UIDropDownMenu_SetSelectedValue(w.ddTextMode, mode)
        end
        if w.sFontSize then w.sFontSize:SetValue(rOrbsDB["fontSize_"..cfgKey] or 28) end
        if w.sAlpha  then w.sAlpha:SetValue(rOrbsDB["textAlpha_"..cfgKey] or c.value.alpha or 1) end
        if w.tcSwatch then
            local tc = rOrbsDB["textColor_"..cfgKey] or {r=1,g=1,b=1}
            w.tcSwatch:SetColor(tc.r, tc.g, tc.b)
        end
        if w.ddFont then
            local fname = rOrbsDB["font_"..cfgKey] or "Enchanted Land"
            UIDropDownMenu_SetText(w.ddFont, fname)
            UIDropDownMenu_SetSelectedValue(w.ddFont, fname)
        end
        -- Target/ToT name controls refresh
        if cfgKey == "TARGET_HEALTH" or cfgKey == "TOT_HEALTH" then
            local rnkShow  = (cfgKey == "TARGET_HEALTH") and "targetNameShow"       or "totNameShow"
            local rnkFSize = (cfgKey == "TARGET_HEALTH") and "targetNameFontSize"   or "totNameFontSize"
            local rnkCC    = (cfgKey == "TARGET_HEALTH") and "targetNameClassColor" or "totNameClassColor"
            local rnkOffY  = (cfgKey == "TARGET_HEALTH") and "targetNameOffY"       or "totNameOffY"
            local rnkPos   = (cfgKey == "TARGET_HEALTH") and "targetNamePos"        or "totNamePos"
            if w.cbTargetName then w.cbTargetName:SetChecked(rOrbsDB[rnkShow] ~= false) end
            if w.sNameSize    then w.sNameSize:SetValue(rOrbsDB[rnkFSize] or 22) end
            if w.cbNameClass  then w.cbNameClass:SetChecked(rOrbsDB[rnkCC] == true) end
            if w.sNameOffY    then w.sNameOffY:SetValue(rOrbsDB[rnkOffY] or -8) end
            if w.ddNamePos then
                local pos = rOrbsDB[rnkPos] or "TOP"
                local posLabels = { TOP = (GetLocale() == "deDE") and "Oben" or "Above orb",
                                    BOTTOM = (GetLocale() == "deDE") and "Unten" or "Below orb" }
                UIDropDownMenu_SetText(w.ddNamePos, posLabels[pos] or "Above orb")
                UIDropDownMenu_SetSelectedValue(w.ddNamePos, pos)
            end
            if w.cbEnable then
                local enableKey = (cfgKey == "TARGET_HEALTH") and "targetOrbEnabled" or "totOrbEnabled"
                w.cbEnable:SetChecked(rOrbsDB[enableKey] ~= false)
            end
        end
        if w.sGal    then w.sGal:SetValue(c.galaxies.alpha  or 0.4) end
        if w.sBub    then w.sBub:SetValue(c.bubbles.alpha   or 1.0) end
        if w.sSpark  then w.sSpark:SetValue(c.spark.alpha   or 1.0) end
        if w.sHigh   then w.sHigh:SetValue(c.highlight.alpha or 1.0) end
        if w.cbOrbModel then w.cbOrbModel:SetChecked(c.model and c.model.enable ~= false) end
        if w.sOrbModelAlpha then
            local alphaVal = (cfgKey == "TARGET_HEALTH") and (rOrbsDB.targetModelAlpha or 1)
                             or (cfgKey == "TOT_HEALTH") and (rOrbsDB.totModelAlpha or 1)
                             or (c.model and c.model.alpha or 1)
            w.sOrbModelAlpha:SetValue(alphaVal)
        end
        if w.cbRing then
            local ringKey = (cfgKey == "TARGET_HEALTH") and "targetRingShow"
                         or (cfgKey == "TOT_HEALTH") and "totRingShow"
            if ringKey then w.cbRing:SetChecked(rOrbsDB[ringKey] ~= false) end
        end
        if Panel.cbTargetPortrait then Panel.cbTargetPortrait:SetChecked(rOrbsDB.targetPortraitShow == true) end
        -- Cast Ring controls (Target tab only)
        if cfgKey == "TARGET_HEALTH" then
            if w.cbCastRing      then w.cbCastRing:SetChecked(rOrbsDB.castRingEnabled ~= false)      end
            if w.cbCastBg        then w.cbCastBg:SetChecked((rOrbsDB.castRingBgMode or "rose_bright") == "rose_bright") end
            if w.cbBgClassColor  then w.cbBgClassColor:SetChecked(rOrbsDB.castRingBgClassColor == true) end
            if w.cbClassColor    then w.cbClassColor:SetChecked(rOrbsDB.castRingUseClassColor == true) end
            if w.sBgAlpha        then w.sBgAlpha:SetValue(rOrbsDB.castRingBgAlpha or 0.8)            end
            if w.sFadeTime       then w.sFadeTime:SetValue(rOrbsDB.castRingFadeTime or 0.4)           end
            if w.sRingScale      then w.sRingScale:SetValue(rOrbsDB.castRingRingScale or 1.0)         end
            if w.bgSwatch        then w.bgSwatch:SetColor(
                rOrbsDB.castRingBgR or 1, rOrbsDB.castRingBgG or 1, rOrbsDB.castRingBgB or 1) end
            if w.castSwatch      then w.castSwatch:SetColor(
                rOrbsDB.castRingCastR or 1, rOrbsDB.castRingCastG or 0.15, rOrbsDB.castRingCastB or 0.05) end
            if w.chanSwatch      then w.chanSwatch:SetColor(
                rOrbsDB.castRingChanR or 0.3, rOrbsDB.castRingChanG or 0.6, rOrbsDB.castRingChanB or 1)  end
        end
        -- Health/Power ring checkboxes
        if Panel.health and Panel.health.cbRing  then Panel.health.cbRing:SetChecked(rOrbsDB.healthRingShow ~= false) end
        if Panel.power  and Panel.power.cbRing   then Panel.power.cbRing:SetChecked(rOrbsDB.powerRingShow   ~= false) end
        -- Ring rotation refresh
        if Panel.health and Panel.health.cbRingRotate then Panel.health.cbRingRotate:SetChecked(rOrbsDB.healthRingRotate == true) end
        if Panel.health and Panel.health.sRingSpeed   then Panel.health.sRingSpeed:SetValue(rOrbsDB.healthRingSpeed or 30) end
        if Panel.health and Panel.health.ddRingDir    then
            local _dir = rOrbsDB.healthRingDir or "CW"
            local _dl  = { CW = "Clockwise", CCW = "Counter-clockwise" }
            UIDropDownMenu_SetText(Panel.health.ddRingDir, _dl[_dir] or "Clockwise")
            UIDropDownMenu_SetSelectedValue(Panel.health.ddRingDir, _dir)
        end
        if Panel.power  and Panel.power.cbRingRotate  then Panel.power.cbRingRotate:SetChecked(rOrbsDB.powerRingRotate == true) end
        if Panel.power  and Panel.power.sRingSpeed    then Panel.power.sRingSpeed:SetValue(rOrbsDB.powerRingSpeed or 30) end
        if Panel.power  and Panel.power.ddRingDir     then
            local _dir = rOrbsDB.powerRingDir or "CW"
            local _dl  = { CW = "Clockwise", CCW = "Counter-clockwise" }
            UIDropDownMenu_SetText(Panel.power.ddRingDir, _dl[_dir] or "Clockwise")
            UIDropDownMenu_SetSelectedValue(Panel.power.ddRingDir, _dir)
        end
        if Panel.health and Panel.health.cbAngel  then Panel.health.cbAngel:SetChecked(rOrbsDB.healthAngelShow ~= false) end
        if Panel.power  and Panel.power.cbDemon   then Panel.power.cbDemon:SetChecked(rOrbsDB.powerDemonShow   ~= false) end
        if Panel.health and Panel.health.cbRunePoints then Panel.health.cbRunePoints:SetChecked(rOrbsDB.runePointsShow ~= false) end
        if Panel.health and Panel.health.cbPlayerCombatIcon then Panel.health.cbPlayerCombatIcon:SetChecked(rOrbsDB.playerCombatIconShow ~= false) end
        if Panel.health and Panel.health.cbPlayerRestingIcon then Panel.health.cbPlayerRestingIcon:SetChecked(rOrbsDB.playerRestingIconShow ~= false) end
        if Panel.target and Panel.target.cbTargetCombatIcon  then Panel.target.cbTargetCombatIcon:SetChecked(rOrbsDB.targetCombatIconShow ~= false) end
        -- Player Combat Icon sliders
        if Panel.health and Panel.health.sPCombatSize  then Panel.health.sPCombatSize:SetValue(rOrbsDB.playerCombatIconSize  or 28) end
        if Panel.health and Panel.health.sPCombatOffX  then Panel.health.sPCombatOffX:SetValue(rOrbsDB.playerCombatIconOffX  or 0)  end
        if Panel.health and Panel.health.sPCombatOffY  then Panel.health.sPCombatOffY:SetValue(rOrbsDB.playerCombatIconOffY  or 0)  end
        -- Player Resting Icon sliders
        if Panel.health and Panel.health.sPRestSize    then Panel.health.sPRestSize:SetValue(rOrbsDB.playerRestingIconSize or 28) end
        if Panel.health and Panel.health.sPRestOffX    then Panel.health.sPRestOffX:SetValue(rOrbsDB.playerRestingIconOffX or 0)  end
        if Panel.health and Panel.health.sPRestOffY    then Panel.health.sPRestOffY:SetValue(rOrbsDB.playerRestingIconOffY or 0)  end
        -- Target Combat Icon sliders
        if Panel.target and Panel.target.sTCombatSize  then Panel.target.sTCombatSize:SetValue(rOrbsDB.targetCombatIconSize  or 28) end
        if Panel.target and Panel.target.sTCombatOffX  then Panel.target.sTCombatOffX:SetValue(rOrbsDB.targetCombatIconOffX  or 0)  end
        if Panel.target and Panel.target.sTCombatOffY  then Panel.target.sTCombatOffY:SetValue(rOrbsDB.targetCombatIconOffY  or 0)  end
        if Panel.health and Panel.health.sAngelScale then Panel.health.sAngelScale:SetValue(rOrbsDB.angelScale or 1.0) end
        if Panel.health and Panel.health.sAngelOffX  then Panel.health.sAngelOffX:SetValue(rOrbsDB.angelOffX  or 0)   end
        if Panel.health and Panel.health.sAngelOffY  then Panel.health.sAngelOffY:SetValue(rOrbsDB.angelOffY  or 0)   end
        if Panel.power  and Panel.power.sDemonScale  then Panel.power.sDemonScale:SetValue(rOrbsDB.demonScale  or 1.0) end
        if Panel.power  and Panel.power.sDemonOffX   then Panel.power.sDemonOffX:SetValue(rOrbsDB.demonOffX   or 0)   end
        if Panel.power  and Panel.power.sDemonOffY   then Panel.power.sDemonOffY:SetValue(rOrbsDB.demonOffY   or 0)   end
        -- Template section
        if w.cbTemplateShow then w.cbTemplateShow:SetChecked(rOrbsDB["tmplShow_"..cfgKey] or false) end
        if w.ddTemplate then
            local currentKey = rOrbsDB["orbTemplate_"..cfgKey] or "none"
            local dispName = "None (Custom)"
            if ns.orbTemplateList then
                for _, entry in ipairs(ns.orbTemplateList) do
                    if entry.key == currentKey then dispName = entry.label; break end
                end
            end
            UIDropDownMenu_SetText(w.ddTemplate, dispName)
            UIDropDownMenu_SetSelectedValue(w.ddTemplate, currentKey)
        end
        if w.sTmplScale   then w.sTmplScale:SetValue(rOrbsDB["tmplScale_"..cfgKey]   or 1.0) end
        if w.sTmplOffX    then w.sTmplOffX:SetValue(rOrbsDB["tmplOffsetX_"..cfgKey]  or 0.0) end
        if w.sTmplOffZ    then w.sTmplOffZ:SetValue(rOrbsDB["tmplOffsetZ_"..cfgKey]  or 0.0) end
        if w.sTmplOpacity then w.sTmplOpacity:SetValue(rOrbsDB["tmplOpacity_"..cfgKey] or 1.0) end
        -- Gloss texture selector
        if w.cbGlossNew or w.cbGlossClassic then
            local g = rOrbsDB["glossTex_"..cfgKey] or "new"
            if w.cbGlossNew     then w.cbGlossNew:SetChecked(g ~= "classic") end
            if w.cbGlossClassic then w.cbGlossClassic:SetChecked(g == "classic") end
        end
    end
    refreshTab("health", "HEALTH")
    refreshTab("power",  "POWER")
    refreshTab("target", "TARGET_HEALTH")
    refreshTab("tot",    "TOT_HEALTH")
end

-- ============================================================
--  APPLY FUNCTIONS
-- ============================================================

local function GetOrb(orbType)
    if orbType == "HEALTH" then return ns.HealthOrb
    elseif orbType == "POWER" then return ns.PowerOrb
    elseif orbType == "TARGET_HEALTH" then return ns.TargetHealthOrb
    elseif orbType == "TOT_HEALTH" then return ns.ToTHealthOrb
    end
    return nil
end

function GUI:ApplyTexture(orbType, texPath)
    local orb = GetOrb(orbType)
    if not orb or not orb.fill then return end
    orb.fill:SetStatusBarTexture(texPath)
    orb.fillTex = orb.fill:GetStatusBarTexture()
end

function GUI:ApplyColor(orbType, r, g, b)
    local orb = GetOrb(orbType)
    if not orb then return end
    if orb.fillTex  then orb.fillTex:SetVertexColor(r, g, b) end
    if orb.spark    then orb.spark:SetVertexColor(r, g, b) end
    if orb.galaxies then for _, t in pairs(orb.galaxies) do t:SetVertexColor(r, g, b) end end
    if orb.bubbles  then for _, t in pairs(orb.bubbles)  do t:SetVertexColor(r, g, b) end end
end

function GUI:ApplyColorAuto(orbType, val)
    if not ns.db or not ns.db.char[orbType] then return end
    ns.db.char[orbType].filling.colorAuto = val
    if rOrbs_DB_CHAR and rOrbs_DB_CHAR[orbType] then rOrbs_DB_CHAR[orbType].filling.colorAuto = val end
    -- For HEALTH/POWER, update the player frame
    if orbType == "HEALTH" or orbType == "POWER" then
        local f = _G["rOrbsPlayerFrame"]
        if f then
            if orbType == "HEALTH" and f.Health then
                f.Health.colorClass  = val
                f.Health.colorHealth = val
            elseif orbType == "POWER" and f.Power then
                f.Power.colorPower = val
            end
            if f.UpdateAllElements then f:UpdateAllElements("ForceUpdate") end
        end
    end
    -- For TARGET_HEALTH, update the target frame
    if orbType == "TARGET_HEALTH" or orbType == "HEALTH" then
        local tf = _G["rOrbsTargetHealthFrame"]
        if tf and tf.UpdateAllElements then tf:UpdateAllElements("ForceUpdate") end
    end
end

function GUI:ApplyTextShow(orbType, val)
    local orb = GetOrb(orbType)
    if orb and orb.values then
        if val then orb.values:Show() else orb.values:Hide() end
    end
    -- Legacy: HEALTH also affects target if target uses HEALTH settings
    if orbType == "HEALTH" and ns.TargetHealthOrb and ns.TargetHealthOrb.values then
        -- Only apply if TARGET_HEALTH doesn't have its own setting
        if rOrbsDB["textShow_TARGET_HEALTH"] == nil then
            if val then ns.TargetHealthOrb.values:Show() else ns.TargetHealthOrb.values:Hide() end
        end
    end
end

function GUI:ApplyTextAlpha(orbType, val)
    local orb = GetOrb(orbType)
    if orb and orb.values then orb.values:SetAlpha(val) end
end

function GUI:ApplyTextColor(orbType, r, g, b)
    local function ApplyToOrb(orb)
        if not orb or not orb.values then return end
        if orb.values.top    then orb.values.top:SetTextColor(r, g, b) end
        if orb.values.bottom then orb.values.bottom:SetTextColor(r, g, b) end
    end
    ApplyToOrb(GetOrb(orbType))
end

function GUI:ApplyFont(orbType, fontPath)
    local function ApplyToOrb(orb)
        if not orb or not orb.values then return end
        fontPath = fontPath or "Fonts\\FRIZQT__.TTF"
        if orb.values.top    then local _,sz,fl = orb.values.top:GetFont()    ; orb.values.top:SetFont(fontPath, sz or 28, fl or "THINOUTLINE") end
        if orb.values.bottom then local _,sz,fl = orb.values.bottom:GetFont() ; orb.values.bottom:SetFont(fontPath, sz or 16, fl or "THINOUTLINE") end
        -- Also update target name font if this is the target orb
        if orb.nameFrame and orb.nameFrame.text then
            local _,sz,fl = orb.nameFrame.text:GetFont()
            orb.nameFrame.text:SetFont(fontPath, sz or 22, fl or "OUTLINE")
        end
    end
    ApplyToOrb(GetOrb(orbType))
end

function GUI:ApplyFontSize(orbType, topSize)
    local orb = GetOrb(orbType)
    if not orb or not orb.values then return end
    topSize = topSize or 28
    local botSize = math.max(8, topSize - 12)
    local fallbackFont = "Fonts\\FRIZQT__.TTF"
    if orb.values.top then
        local font, _, flags = orb.values.top:GetFont()
        orb.values.top:SetFont(font or fallbackFont, topSize, flags or "THINOUTLINE")
    end
    if orb.values.bottom then
        local font, _, flags = orb.values.bottom:GetFont()
        orb.values.bottom:SetFont(font or fallbackFont, botSize, flags or "THINOUTLINE")
    end
end

function GUI:ApplyLayerAlpha(orbType, layer, val)
    local function ApplyToOrb(orb)
        if not orb then return end
        if layer == "galaxies"  and orb.galaxies  then for _, t in pairs(orb.galaxies)  do t:SetAlpha(val) end end
        if layer == "bubbles"   and orb.bubbles   then for _, t in pairs(orb.bubbles)   do t:SetAlpha(val) end end
        if layer == "spark"     and orb.spark     then orb.spark:SetAlpha(val) end
        if layer == "highlight" and orb.highlight then orb.highlight:SetAlpha(val) end
    end
    ApplyToOrb(GetOrb(orbType))
end

function GUI:ApplyTargetModel(show)
    local orb = ns.TargetHealthOrb
    if not orb then return end
    if not orb.model then
        ns.GUI:BuildTargetModel(orb)
    end
    if orb.model then
        if show then orb.model:Show() else orb.model:Hide() end
    end
end

-- Generic name apply functions (work for TARGET_HEALTH and TOT_HEALTH)
local function GetNameOrb(orbType)
    if orbType == "TARGET_HEALTH" then return ns.TargetHealthOrb
    elseif orbType == "TOT_HEALTH" then return ns.ToTHealthOrb
    end
end

function GUI:ApplyNameShow(orbType, val)
    local orb = GetNameOrb(orbType)
    if orb and orb.nameFrame then
        if val then orb.nameFrame:Show() else orb.nameFrame:Hide() end
    end
end

function GUI:ApplyNameFontSize(orbType, size)
    local orb = GetNameOrb(orbType)
    if orb and orb.nameFrame and orb.nameFrame.text then
        local font, _, flags = orb.nameFrame.text:GetFont()
        orb.nameFrame.text:SetFont(font or "Fonts\\FRIZQT__.TTF", size or 22, flags or "OUTLINE")
    end
end

function GUI:ApplyNamePos(orbType, pos, offY)
    local orb = GetNameOrb(orbType)
    if not orb or not orb.nameFrame then return end
    pos  = pos  or "TOP"
    offY = offY or -8
    orb.nameFrame:ClearAllPoints()
    if pos == "BOTTOM" then
        orb.nameFrame:SetPoint("TOP", orb, "BOTTOM", 0, offY)
    else
        orb.nameFrame:SetPoint("BOTTOM", orb, "TOP", 0, offY)
    end
end

-- Legacy aliases
function GUI:ApplyTargetNameShow(val) GUI:ApplyNameShow("TARGET_HEALTH", val) end
function GUI:ApplyTargetNameFontSize(size) GUI:ApplyNameFontSize("TARGET_HEALTH", size) end
function GUI:ApplyTargetNamePos(pos, offY) GUI:ApplyNamePos("TARGET_HEALTH", pos, offY) end

function GUI:ApplyOrbModel(orbType, show)
    local orb = GetOrb(orbType)
    if not orb then return end
    -- Build the model lazily if it doesn't exist yet (same pattern as ApplyTargetModel)
    if not orb.model then
        GUI:BuildTargetModel(orb, orbType)
    end
    if not orb.model then return end
    if show then orb.model:Show() else orb.model:Hide() end
end

function GUI:ApplyOrbModelAlpha(orbType, val)
    local orb = GetOrb(orbType)
    if not orb or not orb.model then return end
    orb.model:SetAlpha(val)
end

function GUI:ApplyTargetPortrait(show)
    local f = _G["rOrbsTargetFrame"]
    if not f then return end
    if show then
        f:Show()
    else
        f:Hide()
    end
    rOrbsDB.targetPortraitShow = show
end

function GUI:BuildTargetModel(orb, orbCfgKey)
    if orb.model then return end
    orbCfgKey = orbCfgKey or "TARGET_HEALTH"
    local holder = nil
    if orb.clipFrame then
        for _, child in ipairs({orb.clipFrame:GetChildren()}) do
            holder = child
            break
        end
    end
    if not holder then return end

    local db2 = ns.db
    local orbcfg = db2 and db2.char and (db2.char[orbCfgKey] or db2.char.HEALTH)
    if not orbcfg then return end

    local alphaKey = (orbCfgKey == "TOT_HEALTH") and "totModelAlpha" or "targetModelAlpha"
    local model = CreateFrame("PlayerModel", nil, holder)
    model:SetFrameLevel(holder:GetFrameLevel() + 1)
    model:SetAllPoints(holder)
    model:EnableMouse(false)
    model:SetAlpha(rOrbsDB[alphaKey] or orbcfg.model.alpha or 1)
    model.baseConfig = orbcfg.model
    model.orbRef = orb
    model._orbCfgKey = orbCfgKey

    function model:Update()
        local c = self.baseConfig or (db2.char[self._orbCfgKey] or db2.char.HEALTH).model
        local orbScale = self.orbRef and self.orbRef:GetScale() or 1
        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)
    end

    model:SetScript("OnEvent", function(s)
      if s._templateConfig then return end
      s:Update()
    end)
    model:RegisterEvent("PLAYER_ENTERING_WORLD")
    model:SetScript("OnShow", function(s)
      -- Skip if template engine manages this model (e.g. after DialogueUI/camera fade hides UIParent)
      -- Without this check, OnShow fires with wrong scale and shrinks the 3D model
      if s._templateConfig then return end
      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.
    if orb.fill then
        orb.fill:HookScript("OnValueChanged", function(bar, value)
            if not orb.model then return end
            if orb.model._templateConfig then return end -- handled by OrbTemplateEngine
            -- Respect the saved model.enable setting
            local orbcfg2 = ns.db and ns.db.char and (ns.db.char[orbCfgKey] or ns.db.char.HEALTH)
            if orbcfg2 and orbcfg2.model and orbcfg2.model.enable == false then return end
            -- Use pcall to safely compare tainted secret number values from GetMinMaxValues
            local ok, isEmpty = pcall(function()
                local _, max = bar:GetMinMaxValues()
                local ratio = (max and max > 0) and (value / max) or 1
                return ratio <= 0
            end)
            if ok then
                if isEmpty then orb.model:Hide() else orb.model:Show() end
            end
        end)
    end

    -- Apply saved enable state: hide if model.enable == false
    local enableState = orbcfg and orbcfg.model and orbcfg.model.enable
    if enableState == false then model:Hide() end
end

function GUI:ApplyPosition(unitKey, x, y, size)
    local frameNames = {
        player="rOrbsPlayerFrame", targetHealth="rOrbsTargetHealthFrame", target="rOrbsTargetFrame", targettarget="rOrbsToTHealthFrame"
    }
    local frame = _G[frameNames[unitKey] or ""]
    if not frame then GUI:Debug(L["ERR_FRAME_NOT_FOUND"]..tostring(unitKey)); return end
    rOrbsDB["pos_"..unitKey.."_x"] = x
    rOrbsDB["pos_"..unitKey.."_y"] = y
    rOrbsDB["size_"..unitKey]      = size
    frame:ClearAllPoints()
    frame:SetPoint("BOTTOM", UIParent, "BOTTOM", x, y)
    if ns.cfg and ns.cfg.units and ns.cfg.units[unitKey] then
        ns.cfg.units[unitKey].size = size
    end
    -- Re-apply figure layout so angel/demon scale with the orb
    if unitKey == "player" and ns.UpdateFigureLayout then
        if ns.AngelFrame and ns.HealthOrb then
            ns.UpdateFigureLayout(ns.AngelFrame, ns.HealthOrb, "LEFT", rOrbsDB.angelScale, rOrbsDB.angelOffX, rOrbsDB.angelOffY)
        end
        if ns.DemonFrame and ns.PowerOrb then
            ns.UpdateFigureLayout(ns.DemonFrame, ns.PowerOrb, "RIGHT", rOrbsDB.demonScale, rOrbsDB.demonOffX, rOrbsDB.demonOffY)
        end
    end
end

function GUI:SetMinimapShown(show)
    rOrbsDB.minimap = rOrbsDB.minimap or {}
    rOrbsDB.minimap.hide = not show
    if ns.MinimapIcon then
        if show then ns.MinimapIcon:Show("rOrbs") else ns.MinimapIcon:Hide("rOrbs") end
    end
end

-- ============================================================
--  INTERFACE OPTIONS INTEGRATION
-- ============================================================

local function RegisterInterfaceOptions()
    local proxy = CreateFrame("Frame", "rOrbsInterfaceOptionsProxy")
    proxy.name = "rOrbs"

    local bg = proxy:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(C.bg[1], C.bg[2], C.bg[3], 1)

    -- Centered logo (rThreat-style)
    local logo = proxy:CreateTexture(nil, "ARTWORK")
    logo:SetTexture("Interface\\AddOns\\rOrbs\\media\\d2_skull")
    logo:SetSize(80, 80)
    logo:SetPoint("TOP", proxy, "TOP", 0, -30)

    -- Addon title below logo
    local t1 = proxy:CreateFontString(nil, "ARTWORK", "GameFontNormalHuge")
    t1:SetPoint("TOP", logo, "BOTTOM", 0, -10)
    t1:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    t1:SetText(L["ADDON_NAME"])

    -- Styled open-settings button (rThreat StyledButton look)
    local openBtn = CreateFrame("Button", nil, proxy, "BackdropTemplate")
    openBtn:SetSize(220, 36)
    openBtn:SetPoint("TOP", t1, "BOTTOM", 0, -20)
    openBtn:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false, edgeSize = 1,
        insets = { left=0, right=0, top=0, bottom=0 },
    })
    openBtn:SetBackdropColor(C.accent[1]*0.18, C.accent[2]*0.06, C.accent[3]*0.06, 1)
    openBtn:SetBackdropBorderColor(C.accent[1]*0.70, 0.08, 0.08, 1)
    local openLbl = openBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    openLbl:SetPoint("CENTER")
    openLbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3], 1)
    openLbl:SetText(L["SETTINGS_TITLE"])
    openBtn:SetScript("OnEnter", function(s)
        s:SetBackdropColor(C.accent[1]*0.38, 0.06, 0.06, 1)
        s:SetBackdropBorderColor(C.accentHi[1], 0.15, 0.15, 1)
        openLbl:SetTextColor(1, 1, 1, 1)
    end)
    openBtn:SetScript("OnLeave", function(s)
        s:SetBackdropColor(C.accent[1]*0.18, C.accent[2]*0.06, C.accent[3]*0.06, 1)
        s:SetBackdropBorderColor(C.accent[1]*0.70, 0.08, 0.08, 1)
        openLbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3], 1)
    end)
    openBtn:SetScript("OnClick", function()
        ns.GUI:Open()
        -- Close the Blizzard GameMenu / Interface Options if open
        if SettingsPanel and SettingsPanel:IsShown() then
            SettingsPanel:Hide()
        elseif InterfaceOptionsFrame and InterfaceOptionsFrame:IsShown() then
            HideUIPanel(InterfaceOptionsFrame)
        end
        if GameMenuFrame and GameMenuFrame:IsShown() then
            HideUIPanel(GameMenuFrame)
        end
    end)

    if IS_NEW_SETTINGS then
        local category = Settings.RegisterCanvasLayoutCategory(proxy, "rOrbs")
        rOrbs.categoryID = category:GetID()
        Settings.RegisterAddOnCategory(category)
    else
        if InterfaceOptions_AddCategory then
            InterfaceOptions_AddCategory(proxy)
        end
    end
end

-- ============================================================
--  INIT
-- ============================================================

ns.RegisterInit("rOrbs_GUI", function()
    db = ns.db
    rOrbsDB = rOrbsDB or {}
    rOrbsDB.minimap = rOrbsDB.minimap or { hide = false }
    ns.debug = rOrbsDB.debug == true
    RegisterInterfaceOptions()

    -- After orbs spawn: apply target model visibility, text color, font size
    C_Timer.After(0.3, function()
        -- Target model
        if ns.TargetHealthOrb then
            if rOrbsDB.targetModelShow ~= false then
                GUI:BuildTargetModel(ns.TargetHealthOrb, "TARGET_HEALTH")
            end
            if ns.TargetHealthOrb.model then
                ns.TargetHealthOrb.model:SetAlpha(rOrbsDB.targetModelAlpha or 1)
                if rOrbsDB.targetModelShow == false then
                    ns.TargetHealthOrb.model:Hide()
                end
            end
        end

        -- Apply saved orb model visibility (health, power)
        for _, pair in ipairs({
            { orbType = "HEALTH",  orb = ns.HealthOrb },
            { orbType = "POWER",   orb = ns.PowerOrb  },
        }) do
            if pair.orb and pair.orb.model then
                local cfg2 = ns.db and ns.db.char and ns.db.char[pair.orbType]
                if cfg2 and cfg2.model then
                    if cfg2.model.enable == false then
                        pair.orb.model:Hide()
                    end
                    pair.orb.model:SetAlpha(cfg2.model.alpha or 1)
                end
            end
        end

        -- ToT model
        if ns.ToTHealthOrb then
            if rOrbsDB.totModelShow ~= false and GUI.BuildTargetModel then
                GUI:BuildTargetModel(ns.ToTHealthOrb, "TOT_HEALTH")
            end
            if ns.ToTHealthOrb.model then
                ns.ToTHealthOrb.model:SetAlpha(rOrbsDB.totModelAlpha or 1)
                if rOrbsDB.totModelShow == false then
                    ns.ToTHealthOrb.model:Hide()
                end
            end
        end

        -- Apply saved text colors, alpha, show/hide, font size
        for _, cfgKey in ipairs({"HEALTH","POWER","TARGET_HEALTH","TOT_HEALTH"}) do
            local tc = rOrbsDB["textColor_"..cfgKey]
            if tc then GUI:ApplyTextColor(cfgKey, tc.r, tc.g, tc.b) end
            local ta = rOrbsDB["textAlpha_"..cfgKey]
            if ta then GUI:ApplyTextAlpha(cfgKey, ta) end
            local ts = rOrbsDB["textShow_"..cfgKey]
            if ts == false then GUI:ApplyTextShow(cfgKey, false) end
            local fs = rOrbsDB["fontSize_"..cfgKey]
            if fs then GUI:ApplyFontSize(cfgKey, fs) end
        end

        -- Apply saved name settings (Target and ToT)
        for _, pair in ipairs({
            { orbType = "TARGET_HEALTH", show = "targetNameShow", fs = "targetNameFontSize", pos = "targetNamePos", offY = "targetNameOffY" },
            { orbType = "TOT_HEALTH",    show = "totNameShow",    fs = "totNameFontSize",    pos = "totNamePos",    offY = "totNameOffY"    },
        }) do
            if rOrbsDB[pair.show] == false then
                GUI:ApplyNameShow(pair.orbType, false)
            end
            if rOrbsDB[pair.fs] then
                GUI:ApplyNameFontSize(pair.orbType, rOrbsDB[pair.fs])
            end
            GUI:ApplyNamePos(pair.orbType, rOrbsDB[pair.pos] or "TOP", rOrbsDB[pair.offY] or -8)
        end

        -- Re-apply angel/demon figure layout from saved values
        if ns.UpdateFigureLayout then
            if ns.AngelFrame and ns.HealthOrb then
                ns.UpdateFigureLayout(ns.AngelFrame, ns.HealthOrb, "LEFT",
                    rOrbsDB.angelScale or 0.95,
                    rOrbsDB.angelOffX  or 57,
                    rOrbsDB.angelOffY  or 10)
            end
            if ns.DemonFrame and ns.PowerOrb then
                ns.UpdateFigureLayout(ns.DemonFrame, ns.PowerOrb, "RIGHT",
                    rOrbsDB.demonScale or 1.0,
                    rOrbsDB.demonOffX  or -74,
                    rOrbsDB.demonOffY  or 4)
            end
            if ns.AngelFrame and rOrbsDB.healthAngelShow == false then ns.AngelFrame:Hide() end
            if ns.DemonFrame and rOrbsDB.powerDemonShow  == false then ns.DemonFrame:Hide() end
        end

        -- Restore orb templates
        if ns.OrbTemplateEngine then
            C_Timer.After(0.5, function()
                ns.OrbTemplateEngine:RestoreAll()
            end)
        end
    end)
end)
