local addonName, ns = ...
local L = ns.L

-- Cached globals
local sin, cos, rad, floor = math.sin, math.cos, math.rad, math.floor
local atan2, sqrt, deg = math.atan2, math.sqrt, math.deg
local CreateFrame = CreateFrame
local BreakUpLargeNumbers = BreakUpLargeNumbers
local GetCursorPosition = GetCursorPosition
local GameTooltip = GameTooltip
local C_Timer = C_Timer
local wipe = wipe
local GetTime = GetTime
local format = string.format
local GetAverageItemLevel = GetAverageItemLevel
local GetItemLevelColor = GetItemLevelColor

ns.Chart = {}
local Chart = ns.Chart

local RESIZE_TEX   = "Interface\\AddOns\\StatOrb\\media\\resize.tga"
local TRACK_TEX    = "Interface\\AddOns\\StatOrb\\media\\ring_track"
local WEDGE_TEX    = "Interface\\AddOns\\StatOrb\\media\\ring_wedge"

-- Geometry of the ring (in pixels).
local RING_SIZE   = 200   -- ring area (square)
local OUTER       = 91    -- outer radius
local INNER       = 58    -- inner radius (donut hole)
local MIDR        = (OUTER + INNER) / 2
local THICK       = OUTER - INNER
local WEDGE_M     = 2       -- radial AA margin baked into the wedge texture
local SLICES      = 90      -- annular sector wedges (4 degrees each)
local SLICE_DEG   = 360 / SLICES
local WEDGE_OVL   = 1.18    -- tangential overlap baked into the texture
local WEDGE_HALF  = rad(SLICE_DEG * 0.5 * WEDGE_OVL)
local WEDGE_W     = 2 * (OUTER + WEDGE_M) * sin(WEDGE_HALF)
local WEDGE_H     = THICK + WEDGE_M * 2
local GAP_MIN_LEN = 2       -- min visible wedges before applying separation
local DIM_FACTOR  = 0.40    -- hover dim via color, not alpha
local WHITE_TEX   = "Interface\\Buttons\\WHITE8X8"

-- Shared dark→light shading for bar fills and ring wedges.
local function SetStatGradient(tex, r, g, b, orientation, factor)
    if not tex then return end
    factor = factor or 1
    r, g, b = r * factor, g * factor, b * factor
    local minC = CreateColor(r * 0.36, g * 0.36, b * 0.36, 1)
    local maxC = CreateColor(
        math.min(1, r * 1.25 + 0.06),
        math.min(1, g * 1.25 + 0.06),
        math.min(1, b * 1.25 + 0.06),
        1)
    orientation = orientation or "HORIZONTAL"
    tex:SetVertexColor(1, 1, 1, 1)
    local ok = pcall(tex.SetGradient, tex, orientation, minC, maxC)
    if not ok then
        tex:SetVertexColor(r, g, b, 1)
    end
end

local function StyleBarFill(tex, r, g, b, factor)
    if not tex then return end
    tex:SetTexture(WHITE_TEX)
    SetStatGradient(tex, r, g, b, "HORIZONTAL", factor)
end

local function StyleWedge(tex, r, g, b, factor)
    if not tex then return end
    -- Radial-ish shading along the wedge texture (dark → light).
    SetStatGradient(tex, r, g, b, "VERTICAL", factor)
end

-- Panel chrome and layout.
local PAD          = 14    -- inner padding of the panel
local METAL_TITLE_Y_OFFSET = 20
local HEADER_H     = 26    -- title-bar height
local SEP_GAP      = 10    -- space reserved for the header separator
local DEF_PANEL_W  = RING_SIZE + PAD * 2
local PANEL_WIDTH  = math.floor(DEF_PANEL_W * 1.2 + 0.5)   -- fixed width (+20%)
local MIN_PANEL_W  = PANEL_WIDTH
local MAX_PANEL_W  = PANEL_WIDTH
local CONTENT_TOP  = PAD + HEADER_H + SEP_GAP  -- y-offset from panel top to ring

-- ElvUI theme: flat near-black panel with a subtle vertical gradient and a
-- thin cool-gray border, tuned to sit cleanly next to an ElvUI-skinned frame.
local PANEL_TOP_COLOR    = { 0.09, 0.09, 0.11, 0.60 }
local PANEL_BOTTOM_COLOR = { 0.045, 0.045, 0.055, 0.60 }
local BORDER_COLOR       = { 0.24, 0.24, 0.28, 0.90 }
local SEP_COLOR          = { 1, 1, 1, 0.07 }

-- Classic theme: original Blizzard textures matching the default character panel.
local CLASSIC_BAR_TEX    = "Interface\\PaperDollInfoFrame\\UI-Character-StatBackground"
local CLASSIC_BAR_COORDS = { 0, 0.8984375, 0.125, 0.1953125 }  -- middle band slice
local CLASSIC_TITLE_COLOR = { 1, 0.82, 0 }
local CLASSIC_BG_ATLAS   = "transmog-locationbg-glow"
local CLASSIC_BG_SCALE   = 1.45   -- >1 zooms out so more of the atlas is visible
local CLASSIC_BG_INSET   = 5      -- horizontal/bottom inset inside the border
local CLASSIC_BG_TOP     = 30     -- start below the title bar (spec name row)
local METAL_BG_TEX       = "Interface\\AddOns\\StatOrb\\media\\TextureKit-Metal.png"
-- Crop the baked-in black margins so the metal plate fills the panel.
local METAL_BG_TEXCOORD  = { 0.11, 0.89, 0.07, 0.93 }
local METAL_BG_OVERSCAN  = 1.06   -- slight zoom so chiseled edges reach the frame
local MENU_BTN_ATLAS     = "category-icons_misc_inactive"

-- Embed chrome + compact layout (table keeps the main chunk under 200 locals).
-- Chrome atlases mirror CharacterStatsPane category / item-level bounce.
local EMBED = {
    titleAtlas = "UI-Character-Info-Title",
    bounceAtlas = "UI-Character-Info-ItemLevel-Bounce",
    classBgFmt = "UI-Character-Info-%s-BG",
    specCatH = 40,
    specBounceH = 29,
    specTop = 2,
    bounceAlpha = 0.3,
    ringScale = 0.80,
    legendFont = 11.5,
    legendRowH = 17,
    legendGap = 1,
    legendIcon = 14,
    legendSideInset = 4,
    centerIcon = 90,
    pad = 12,
    contentTop = 10, -- specialization chrome sits above the scroll child
    priorityFont = 10,
    titleFont = 13,
    centerBig = 30,
    centerSmall = 11,
}

-- Live width resize: grip code remains but is hidden and non-interactive.
local RESIZE_GRIP_LIVE   = false

local STAT_FONT    = STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
local STAT_FONT_FLAGS = "OUTLINE"
local LEGEND_ROW_H = 20
local LEGEND_PAD   = 10
local LEGEND_ROW_GAP = 3
local LEGEND_ICON_SIZE = 16
local LEGEND_SWATCH_W  = 3
local LEGEND_SIDE_INSET = 6
local CENTER_ICON_SIZE = 90
local CENTER_ICON_ALPHA = 0.30
local CENTER_ILVL_ICON = "Interface\\AddOns\\StatOrb\\media\\RPGIcons\\curse"
-- Horizontal bar chart layout (alternative to the circular ring).
-- Grouped into tables to stay under Lua's 200-local main-chunk limit.
-- Design: bold full-bleed tracks are the focus; % appears on hover (header + row).
local BAR = {
    rowH = 32,
    trackH = 16,
    gap = 8,
    icon = 18,
    -- Name column; track claims everything to the right edge.
    labelW = 108,
    -- Hover-only % chip at the track's right end.
    pctW = 56,
    -- Title → item-level block (extra gap pushes Gegenstandsstufe downward).
    headerTitleGap = 18,
    headerH = 52,
    embedHeaderH = 48,
    -- Gaps around the vertically-centered bar block.
    afterHeaderGap = 18,
    beforePriorityGap = 18,
    -- Full-bleed side margins.
    sideInset = 4,
    embedSideInset = 2,
    -- Bottom padding under Werte-Priorität (~1 text line).
    bottomPad = 16,
    embedBottomPad = 14,
    -- Item-level number scale (relative to centerBig / centerSmall).
    headerBigScale = 1.08,
    headerSmallScale = 1.05,
    embedRowH = 28,
    embedTrackH = 14,
    embedGap = 7,
    embedIcon = 16,
    embedLabelW = 100,
    trackAlpha = 0.14,
    -- Dark overlay on hover, sized to the full attribute track.
    hoverHlAlpha = 0.40,
    -- Pack rows when more than this many stats are shown.
    compactAfter = 5,
    minRowH = 18,
    minTrackH = 8,
    minGap = 1,
}

local function Round(v)
    return floor(v + 0.5)
end

-- Shrinks bar row gap/height so `barCount` rows fit into `availableH`.
-- Comfortable spacing stays for ≤ compactAfter when space allows.
local function FitBarSpacing(metrics, barCount, availableH)
    local rowH = metrics.barRowH or BAR.rowH
    local gap = metrics.barGap or BAR.gap
    local trackH = metrics.barTrackH or BAR.trackH
    local baseRow = rowH
    local baseTrack = trackH
    local n = math.max(barCount or 1, 1)
    local minRow = BAR.minRowH or 18
    local minTrack = BAR.minTrackH or 8
    local minGap = BAR.minGap or 1

    local function bodyH(rh, g)
        return n * rh + math.max(0, n - 1) * g
    end

    local need = availableH
    if not ns.IsSafeNumber(need) or need < 40 then
        need = 40
    end

    if bodyH(rowH, gap) <= need then
        return rowH, gap, trackH
    end

    -- Overflow: tighten gaps first, then row height (typical when >5 stats).
    if n > 1 then
        local maxGap = (need - n * rowH) / (n - 1)
        if maxGap >= minGap then
            gap = math.min(gap, maxGap)
            if bodyH(rowH, gap) <= need then
                return rowH, gap, trackH
            end
        end
        gap = minGap
    end

    rowH = (need - math.max(0, n - 1) * gap) / n
    if rowH > baseRow then rowH = baseRow end
    if rowH < minRow then rowH = minRow end
    trackH = math.max(minTrack, Round(baseTrack * (rowH / baseRow)))
    if trackH > rowH - 4 then
        trackH = math.max(minTrack, rowH - 4)
    end
    return rowH, gap, trackH
end

local function ClampPanelWidth(w)
    if w < MIN_PANEL_W then return MIN_PANEL_W end
    if w > MAX_PANEL_W then return MAX_PANEL_W end
    return w
end

local contentWidth = RING_SIZE

-- Embed state: when embedded the panel lives inside the character window as a
-- sidebar-tab pane instead of floating next to it.
local embedded = false
local embedTarget

-- Lazily created frame references.
local frame, scrollFrame, contentFrame, ringHolder, ringTrack, centerHit, centerBig, centerSmall, centerIcon, titleText, emptyText
local specIcon, specIconHolder, priorityText, headerLine, resizeGrip, menuBtn, metalBg, metalBgClip
local classicChrome, classicHeaderBar, specIconMask, portraitRingOverlay
local fgWedges = {}
local legendRows = {}
local chromeBorder = {}
local barUI = { rows = {} } -- holder, headerBig, headerSmall, emptyText
-- Embed-only CharacterStatsPane chrome (class watermark + spec category/bounce).
local embedUI = {}

local function IsBarLayout()
    return ns.db and ns.db.chartLayout == "bar"
end

local function GetContentPad()
    return embedded and EMBED.pad or PAD
end

local function GetContentWidth()
    if not frame then return RING_SIZE end
    local pad = GetContentPad()
    return math.max(40, frame:GetWidth() - pad * 2)
end

-- Legend uses a tighter side inset than the general content pad so rows
-- can use the unused horizontal space beside the list.
local function GetLegendWidth()
    if not frame then return RING_SIZE end
    local inset = embedded and EMBED.legendSideInset or LEGEND_SIDE_INSET
    return math.max(40, frame:GetWidth() - inset * 2)
end

local function GetBarChartWidth()
    if not frame then return RING_SIZE end
    local inset = embedded and BAR.embedSideInset or BAR.sideInset
    local w = frame:GetWidth()
    if not ns.IsSafeNumber(w) or w <= 0 then
        w = PANEL_WIDTH
    end
    return math.max(40, w - inset * 2)
end

-- Returns sizing metrics for the active display mode (float vs embed).
local function GetLayoutMetrics()
    if embedded then
        -- Classic CharacterStatsPane chrome is visible only while its category is shown.
        local classicEmbed = embedUI.category and embedUI.category:IsShown()
        return {
            ringScale = EMBED.ringScale,
            legendFont = EMBED.legendFont,
            legendRowH = EMBED.legendRowH,
            legendGap = EMBED.legendGap,
            pad = EMBED.pad,
            -- Classic embed: specialization chrome sits above the scroll child.
            -- ElvUI/metal embed: title lives inside the scroll area.
            contentTop = classicEmbed and EMBED.contentTop or (EMBED.pad + EMBED.titleFont + 28),
            menuX = -4,
            menuY = classicEmbed and -(EMBED.specTop + 4) or -4,
            centerBig = EMBED.centerBig,
            centerSmall = EMBED.centerSmall,
            priorityFont = EMBED.priorityFont,
            titleFont = EMBED.titleFont,
            ringGap = 6,
            priorityGap = 6,
            barRowH = BAR.embedRowH,
            barTrackH = BAR.embedTrackH,
            barGap = BAR.embedGap,
            barIcon = BAR.embedIcon,
            barLabelW = BAR.embedLabelW,
            barHeaderH = BAR.embedHeaderH,
            barBottomPad = BAR.embedBottomPad,
            barHeaderBigScale = BAR.headerBigScale,
            barHeaderSmallScale = BAR.headerSmallScale,
        }
    end
    return {
        ringScale = 1,
        legendFont = 12,
        legendRowH = LEGEND_ROW_H,
        legendGap = LEGEND_ROW_GAP,
        pad = PAD,
        contentTop = CONTENT_TOP,
        menuX = -5,
        menuY = -25,
        centerBig = 30,
        centerSmall = 12,
        priorityFont = 11,
        titleFont = 14,
        ringGap = 8,
        priorityGap = 10,
        barRowH = BAR.rowH,
        barTrackH = BAR.trackH,
        barGap = BAR.gap,
        barIcon = BAR.icon,
        barLabelW = BAR.labelW,
        barHeaderH = BAR.headerH,
        barBottomPad = BAR.bottomPad,
        barHeaderBigScale = BAR.headerBigScale,
        barHeaderSmallScale = BAR.headerSmallScale,
    }
end

local function GetChartBodyHeight(metrics, barCount)
    if IsBarLayout() then
        local rows = math.max(barCount or 0, 1)
        local rowH = metrics.barRowH or BAR.rowH
        local gap = metrics.barGap or BAR.gap
        return rows * rowH + math.max(0, rows - 1) * gap
    end
    return RING_SIZE * (metrics.ringScale or 1)
end

local function CountVisibleBarRows()
    local n = 0
    for i = 1, #barUI.rows do
        if barUI.rows[i] and barUI.rows[i]:IsShown() then
            n = n + 1
        end
    end
    return n
end

-- Pins Werte-Priorität to the panel bottom (bar + circle layouts).
local function PinPriorityToBottom(metrics)
    if not priorityText or not frame then return end
    metrics = metrics or GetLayoutMetrics()
    local bottomPad = metrics.barBottomPad or BAR.bottomPad or 16
    local width = IsBarLayout() and GetBarChartWidth() or GetLegendWidth()
    priorityText:ClearAllPoints()
    priorityText:SetWidth(math.max(40, width - 4))
    priorityText:SetJustifyH("CENTER")
    if priorityText:GetParent() ~= frame then
        priorityText:SetParent(frame)
    end
    priorityText:SetPoint("BOTTOM", frame, "BOTTOM", 0, bottomPad)
    if priorityText.SetDrawLayer then
        priorityText:SetDrawLayer("OVERLAY", 7)
    end
    priorityText:Show()
end

-- Forward declaration: defined with bar-row helpers below.
local EnsureBarHover

-- Positions bar-layout chrome on the panel frame:
--   item level in a fixed band under the title,
--   bold full-bleed bars centered between item level and Werte-Priorität,
--   % only on hover (track overlay + header).
--   With >5 stats, row gap/height shrink so the list never covers Werte-Priorität.
local function LayoutBarChartChrome(metrics)
    if not barUI.holder or not contentFrame or not frame then return end
    metrics = metrics or GetLayoutMetrics()
    local barW = GetBarChartWidth()
    local barCount = math.max(CountVisibleBarRows(), 1)
    local bottomPad = metrics.barBottomPad or BAR.bottomPad
    local titleGap = BAR.headerTitleGap or 6
    local headerH = metrics.barHeaderH or BAR.headerH
    local afterHeader = BAR.afterHeaderGap or 18
    local beforePriority = BAR.beforePriorityGap or 18
    local iconW = metrics.barIcon or BAR.icon
    local labelW = metrics.barLabelW or BAR.labelW
    local trackAlpha = BAR.trackAlpha or 0.14

    local frameH = frame:GetHeight()
    if not ns.IsSafeNumber(frameH) or frameH <= 0 then
        frameH = 400
    end

    local titleBottomFromTop = (metrics.pad or PAD) + (metrics.titleFont or 14) + 6
    if embedded and embedUI.bounce and embedUI.bounce:IsShown()
        and embedUI.bounce.GetBottom and frame.GetTop then
        local ft, bb = frame:GetTop(), embedUI.bounce:GetBottom()
        if ft and bb then
            titleBottomFromTop = ft - bb
        end
    elseif titleText and titleText.GetBottom and frame.GetTop then
        local ft, tb = frame:GetTop(), titleText:GetBottom()
        if ft and tb then
            titleBottomFromTop = ft - tb
        end
    end

    local priorityH = 34
    if priorityText and priorityText.GetStringHeight then
        priorityH = math.max(28, (priorityText:GetStringHeight() or 28) + 4)
    end

    local headerTop = titleBottomFromTop + titleGap

    -- Keep chrome above the scroll frame so bar rows receive mouse hover.
    local chromeLevel = (scrollFrame and scrollFrame:GetFrameLevel() or frame:GetFrameLevel()) + 20
    if barUI.holder:GetParent() ~= frame then
        barUI.holder:SetParent(frame)
    end
    barUI.holder:SetFrameLevel(chromeLevel)
    barUI.holder:EnableMouse(false)
    if barUI.headerRegion then
        if barUI.headerRegion:GetParent() ~= frame then
            barUI.headerRegion:SetParent(frame)
        end
        barUI.headerRegion:SetFrameLevel(chromeLevel)
        barUI.headerRegion:EnableMouse(false)
    end
    if barUI.midRegion then
        barUI.midRegion:Hide()
    end
    if scrollFrame and IsBarLayout() then
        scrollFrame:EnableMouse(false)
    end

    -- 1) Priority at bottom.
    PinPriorityToBottom(metrics)

    -- 2) Item-level band (fixed height under title).
    if barUI.headerRegion then
        barUI.headerRegion:ClearAllPoints()
        barUI.headerRegion:SetSize(barW, headerH)
        barUI.headerRegion:SetPoint("TOP", frame, "TOP", 0, -headerTop)
        barUI.headerRegion:Show()
    end
    if barUI.headerBig then
        local big = metrics.centerBig * (metrics.barHeaderBigScale or BAR.headerBigScale)
        barUI.headerBig:SetFont(STAT_FONT, big, STAT_FONT_FLAGS)
        barUI.headerBig:ClearAllPoints()
        barUI.headerBig:SetPoint("CENTER", barUI.headerRegion or frame, "CENTER", 0, 8)
    end
    if barUI.headerSmall then
        local small = metrics.centerSmall * (metrics.barHeaderSmallScale or BAR.headerSmallScale)
        barUI.headerSmall:SetFont(STAT_FONT, small, STAT_FONT_FLAGS)
        barUI.headerSmall:ClearAllPoints()
        barUI.headerSmall:SetPoint("TOP", barUI.headerBig, "BOTTOM", 0, -4)
    end

    -- Available band for the bar list (between item level and priority).
    local chartTop = headerTop + headerH + afterHeader
    local chartBottom = bottomPad + priorityH + beforePriority
    local avail = frameH - chartTop - chartBottom
    if barUI.headerRegion and priorityText and barUI.headerRegion.GetBottom and priorityText.GetTop then
        local hb = barUI.headerRegion:GetBottom()
        local pt = priorityText:GetTop()
        if hb and pt and pt < hb then
            avail = (hb - pt) - afterHeader - beforePriority
        end
    end
    if avail < 40 then avail = 40 end

    -- Pack spacing when many stats would overflow into Werte-Priorität.
    local rowH, gap, trackH = FitBarSpacing(metrics, barCount, avail)
    metrics.barRowH = rowH
    metrics.barGap = gap
    metrics.barTrackH = trackH
    local barsH = GetChartBodyHeight(metrics, barCount)

    -- 3) Place bars in the band (centered when spare room remains).
    local barsTop = chartTop
    if avail > barsH then
        barsTop = chartTop + (avail - barsH) * 0.5
    end

    barUI.holder:ClearAllPoints()
    barUI.holder:SetSize(barW, math.max(barsH, 1))
    barUI.holder:SetPoint("TOP", frame, "TOP", 0, -barsTop)
    barUI.holder:Show()

    -- Track claims full remaining width to the right edge.
    local trackAvail = barW - iconW - 5 - labelW - 6
    if trackAvail < 32 then trackAvail = 32 end

    local prev
    for i = 1, #barUI.rows do
        local row = barUI.rows[i]
        if row and row:IsShown() then
            if row:GetParent() ~= barUI.holder then
                row:SetParent(barUI.holder)
            end
            row:SetFrameLevel(chromeLevel + 2)
            row:EnableMouse(true)
            row:SetWidth(barW)
            row:SetHeight(rowH)
            if row.icon then
                local iconSize = math.min(metrics.barIcon or BAR.icon, math.max(12, rowH - 4))
                row.icon:SetSize(iconSize, iconSize)
            end
            row:ClearAllPoints()
            if not prev then
                row:SetPoint("TOPLEFT", barUI.holder, "TOPLEFT", 0, 0)
                row:SetPoint("TOPRIGHT", barUI.holder, "TOPRIGHT", 0, 0)
            else
                row:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -gap)
                row:SetPoint("TOPRIGHT", prev, "BOTTOMRIGHT", 0, -gap)
            end

            if row.label then
                row.label:SetWidth(labelW)
            end
            if row.track then
                row.track:ClearAllPoints()
                row.track:SetPoint("LEFT", row.label, "RIGHT", 6, 0)
                row.track:SetPoint("RIGHT", row, "RIGHT", 0, 0)
                row.track:SetHeight(trackH)
                row.track:SetColorTexture(1, 1, 1, trackAlpha)
            end
            if row.fill and row.entry then
                local share = row.entry._barShare or 0.04
                local c = row.entry.color or { 1, 1, 1 }
                row.fill:ClearAllPoints()
                row.fill:SetPoint("LEFT", row.track, "LEFT", 0, 0)
                row.fill:SetHeight(trackH)
                row.fill:SetWidth(math.max(3, trackAvail * share))
                StyleBarFill(row.fill, c[1], c[2], c[3], 1)
            end
            if row.sheen then
                row.sheen:SetHeight(math.max(2, trackH * 0.42))
            end
            EnsureBarHover(row)
            if row.hover then
                row.hover:Hide()
            end
            prev = row
        end
    end

    embedUI.LayoutIlvlBounce()
end

-- Applies legend row sizing for the current mode.
local function StyleLegendRow(row, metrics)
    if not row or not metrics then return end
    row:SetHeight(metrics.legendRowH)
    local iconSize = embedded and EMBED.legendIcon or LEGEND_ICON_SIZE
    local swatchH = embedded and 13 or 16
    if row.swatch then
        row.swatch:SetSize(LEGEND_SWATCH_W, swatchH)
    end
    if row.icon then
        row.icon:SetSize(iconSize, iconSize)
    end
    if row.pct then
        row.pct:SetFont(STAT_FONT, metrics.legendFont, STAT_FONT_FLAGS)
    end
    if row.label then
        row.label:SetFont(STAT_FONT, metrics.legendFont, STAT_FONT_FLAGS)
    end
    if centerIcon then
        local cz = embedded and EMBED.centerIcon or CENTER_ICON_SIZE
        centerIcon:SetSize(cz, cz)
    end
end

-- Keeps the scroll child tall enough for the content; enables wheel only in embed.
local function UpdateScrollExtent()
    if not scrollFrame or not contentFrame or not frame then return end

    local metrics = GetLayoutMetrics()
    local w = frame:GetWidth()
    if not ns.IsSafeNumber(w) or w <= 0 then
        w = PANEL_WIDTH
    end
    contentFrame:SetWidth(w)

    local needed = metrics.contentTop + GetChartBodyHeight(metrics, 0) + metrics.pad
    if priorityText and priorityText:IsShown() then
        if IsBarLayout() then
            local barCount = CountVisibleBarRows()
            local bodyH = GetChartBodyHeight(metrics, barCount)
            local bottomPad = metrics.barBottomPad or BAR.bottomPad
            -- Full pane: title + item-level band + bars + priority.
            local viewH = frame:GetHeight()
            if ns.IsSafeNumber(viewH) and viewH > 0 then
                needed = viewH
            else
                needed = (metrics.pad or PAD) + 56 + bodyH + bottomPad
                    + (priorityText:GetStringHeight() or 28) + 40
            end
        else
            -- Priority is pinned to the panel bottom; scroll height follows legend/ring.
            local legendCount = 0
            local lastLegend
            for i = 1, #legendRows do
                if legendRows[i] and legendRows[i]:IsShown() then
                    legendCount = legendCount + 1
                    lastLegend = legendRows[i]
                end
            end
            local top = contentFrame:GetTop()
            local bottom = lastLegend and lastLegend.GetBottom and lastLegend:GetBottom()
            if (not bottom) and ringHolder and ringHolder.GetBottom then
                bottom = ringHolder:GetBottom()
            end
            if top and bottom then
                needed = (top - bottom) + metrics.pad
                    + (priorityText:GetStringHeight() or 28) + (metrics.barBottomPad or BAR.bottomPad or 16)
            else
                local bodyH = GetChartBodyHeight(metrics, 0)
                needed = metrics.contentTop
                    + bodyH
                    + metrics.ringGap
                    + legendCount * (metrics.legendRowH + metrics.legendGap)
                    + (priorityText:GetStringHeight() or 28)
                    + (metrics.barBottomPad or BAR.bottomPad or 16)
                    + metrics.pad
            end
        end
    end

    local viewH = frame:GetHeight()
    if not ns.IsSafeNumber(viewH) or viewH <= 0 then
        viewH = needed
    end

    if embedded then
        contentFrame:SetHeight(math.max(needed, viewH))
        scrollFrame:EnableMouseWheel(needed > viewH + 1)
        local maxScroll = scrollFrame:GetVerticalScrollRange() or 0
        local cur = scrollFrame:GetVerticalScroll() or 0
        if cur > maxScroll then
            scrollFrame:SetVerticalScroll(maxScroll)
        end
    else
        scrollFrame:EnableMouseWheel(false)
        scrollFrame:SetVerticalScroll(0)
        contentFrame:SetHeight(viewH)
    end
end

local scrollUpdatePending = false
local function QueueScrollUpdate()
    if scrollUpdatePending then return end
    scrollUpdatePending = true
    C_Timer.After(0, function()
        scrollUpdatePending = false
        UpdateScrollExtent()
    end)
end

-- Positions fonts, ring scale and the menu button for float/embed.
local function ApplyModeLayout()
    if not frame then return end
    local metrics = GetLayoutMetrics()
    local anchor = contentFrame or frame
    local barLayout = IsBarLayout()

    if scrollFrame then
        scrollFrame:ClearAllPoints()
        -- Only inset under CharacterStatsPane specialization chrome (classic embed).
        if embedded and embedUI.category and embedUI.category:IsShown() then
            local topInset = EMBED.specTop + EMBED.specCatH + EMBED.specBounceH
            scrollFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -topInset)
            scrollFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
        else
            scrollFrame:SetAllPoints(frame)
        end
    end

    if titleText then
        if embedded and embedUI.category and embedUI.category:IsShown() then
            titleText:SetFont(STAT_FONT, metrics.titleFont, "")
        else
            titleText:SetFont(STAT_FONT, metrics.titleFont, STAT_FONT_FLAGS)
        end
    end
    if priorityText then
        priorityText:SetFont(STAT_FONT, metrics.priorityFont, STAT_FONT_FLAGS)
    end
    if centerBig then
        centerBig:SetFont(STAT_FONT, metrics.centerBig, STAT_FONT_FLAGS)
    end
    if centerSmall then
        centerSmall:SetFont(STAT_FONT, metrics.centerSmall, STAT_FONT_FLAGS)
    end
    if barUI.headerBig then
        local big = metrics.centerBig * (metrics.barHeaderBigScale or BAR.headerBigScale)
        barUI.headerBig:SetFont(STAT_FONT, big, STAT_FONT_FLAGS)
    end
    if barUI.headerSmall then
        local small = metrics.centerSmall * (metrics.barHeaderSmallScale or BAR.headerSmallScale)
        barUI.headerSmall:SetFont(STAT_FONT, small, STAT_FONT_FLAGS)
    end

    if ringHolder then
        ringHolder:SetScale(metrics.ringScale)
        ringHolder:ClearAllPoints()
        local topOffset = metrics.contentTop
        if metrics.ringScale < 1 then
            topOffset = metrics.contentTop - RING_SIZE * (1 - metrics.ringScale) * 0.5
        end
        ringHolder:SetPoint("TOP", anchor, "TOP", 0, -topOffset)
        if barLayout then
            ringHolder:Hide()
        else
            ringHolder:Show()
        end
    end

    if barLayout then
        LayoutBarChartChrome(metrics)
    else
        if barUI.holder then
            barUI.holder:Hide()
        end
        if barUI.headerRegion then
            barUI.headerRegion:Hide()
        end
        if barUI.headerIcon then
            barUI.headerIcon:Hide()
        end
        if barUI.midRegion then
            barUI.midRegion:Hide()
        end
        if embedUI.ilvlBounce then
            embedUI.ilvlBounce:Hide()
        end
        PinPriorityToBottom(metrics)
        if scrollFrame then
            scrollFrame:EnableMouse(true)
        end
    end

    if menuBtn then
        menuBtn:ClearAllPoints()
        menuBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", metrics.menuX, metrics.menuY)
        menuBtn:SetFrameLevel((frame:GetFrameLevel() or 1) + 40)
    end

    for i = 1, #legendRows do
        StyleLegendRow(legendRows[i], metrics)
    end

    UpdateScrollExtent()
end

local IsAddOnLoaded = (C_AddOns and C_AddOns.IsAddOnLoaded) or _G.IsAddOnLoaded

-- Flat/modern UI skins that make the ElvUI-style panel the better auto match.
local FLAT_SKIN_ADDONS = { "ElvUI", "Aurora", "AuroraClassic", "NDui", "Tukui" }

-- True when a flat-skin addon is present (used by the "auto" theme).
local function IsFlatSkinLoaded()
    if type(IsAddOnLoaded) ~= "function" then return false end
    for _, name in ipairs(FLAT_SKIN_ADDONS) do
        local ok, loaded = pcall(IsAddOnLoaded, name)
        if ok and loaded then
            return true
        end
    end
    return false
end

-- Resolves the configured theme ("auto" follows flat-skin presence).
local function ResolveTheme()
    local t = ns.db and ns.db.theme
    if t == "classic" or t == "elvui" or t == "metal" then
        return t
    end
    return IsFlatSkinLoaded() and "elvui" or "classic"
end

local function GetPanelContentParent()
    return contentFrame or frame
end

-- ElvUI panel fill and border (hidden for classic and metal themes).
local function SetElvPanelChromeVisible(visible)
    if frame and frame.bg then
        if visible then
            frame.bg:SetAlpha(1)
            frame.bg:Show()
        else
            frame.bg:SetAlpha(0)
            frame.bg:Hide()
        end
    end
    if headerLine then
        if visible then headerLine:Show() else headerLine:Hide() end
    end
    for i = 1, #chromeBorder do
        local edge = chromeBorder[i]
        if edge then
            if visible then edge:Show() else edge:Hide() end
        end
    end
end

-- Finds the NineSlice layout the live CharacterFrame uses, with safe fallbacks.
local function GetCharacterFrameLayout()
    if not (NineSliceUtil and NineSliceUtil.GetLayout) then return nil end
    local candidates = {}
    local cf = CharacterFrame
    if cf then
        candidates[#candidates + 1] = cf.layoutType
        if cf.NineSlice then
            candidates[#candidates + 1] = cf.NineSlice.layoutType
        end
    end
    candidates[#candidates + 1] = "PortraitFrameTemplate"
    candidates[#candidates + 1] = "ButtonFrameTemplateNoPortrait"
    for _, name in ipairs(candidates) do
        if type(name) == "string" then
            local ok, layout = pcall(NineSliceUtil.GetLayout, name)
            if ok and type(layout) == "table" then
                return layout
            end
        end
    end
    return nil
end

-- Metal theme: custom metal texture background (ElvUI layout, no border).
local function ApplyMetalBackground(tex)
    tex:SetTexture(METAL_BG_TEX)
    local tc = METAL_BG_TEXCOORD
    tex:SetTexCoord(tc[1], tc[2], tc[3], tc[4])
    tex:SetAlpha(1)
end

-- Sizes the cropped metal texture to fill the full panel height/width.
local function LayoutMetalBackground()
    local clip = metalBgClip or frame
    if not metalBg or not clip then return end
    local w, h = clip:GetWidth(), clip:GetHeight()
    if not w or w <= 0 or not h or h <= 0 then return end
    metalBg:ClearAllPoints()
    if METAL_BG_OVERSCAN > 1 then
        metalBg:SetSize(w * METAL_BG_OVERSCAN, h * METAL_BG_OVERSCAN)
        metalBg:SetPoint("CENTER", clip, "CENTER", 0, 0)
    else
        metalBg:SetAllPoints(clip)
    end
end

-- Applies the classic panel background atlas.
local function ApplyClassicBackground(tex)
    local ok = pcall(function()
        tex:SetAtlas(CLASSIC_BG_ATLAS, false)
    end)
    if ok then
        tex:SetAlpha(0.72)
        return
    end
    tex:SetColorTexture(0.04, 0.04, 0.05, 0.95)
end

-- Sizes the classic bg larger than the clip area so more of the atlas is visible.
local function LayoutClassicBackground(container)
    local clip = container and container.bgClip
    local bg = container and container.bg
    if not clip or not bg then return end
    local w, h = clip:GetWidth(), clip:GetHeight()
    if not w or w <= 0 or not h or h <= 0 then return end
    bg:ClearAllPoints()
    bg:SetSize(w * CLASSIC_BG_SCALE, h * CLASSIC_BG_SCALE)
    bg:SetPoint("CENTER", clip, "CENTER", 0, 0)
end

-- Hides the built-in corner and draws a copy on a high frame level above the icon.
local function CreatePortraitRingOverlay(chrome, parent)
    if not chrome or not parent then return end
    local corner = chrome.TopLeftCorner
    if not corner then return end

    corner:Hide()

    if not portraitRingOverlay then
        portraitRingOverlay = CreateFrame("Frame", nil, parent)
        portraitRingOverlay.tex = portraitRingOverlay:CreateTexture(nil, "OVERLAY")
        portraitRingOverlay.tex:SetAllPoints()
    end

    portraitRingOverlay:SetFrameLevel(parent:GetFrameLevel() + 25)
    portraitRingOverlay:SetSize(corner:GetWidth(), corner:GetHeight())

    local point, relativeTo, relativePoint, x, y = corner:GetPoint()
    portraitRingOverlay:ClearAllPoints()
    if point and relativeTo then
        portraitRingOverlay:SetPoint(point, relativeTo, relativePoint, x or 0, y or 0)
    else
        portraitRingOverlay:SetPoint("TOPLEFT", chrome, "TOPLEFT", 0, 0)
    end

    local tex = portraitRingOverlay.tex
    pcall(function()
        if corner.GetAtlas then
            local atlas = corner:GetAtlas()
            if atlas and atlas ~= "" then
                tex:SetAtlas(atlas)
                return
            end
        end
        if corner.GetTexture then
            local file = corner:GetTexture()
            if file then
                tex:SetTexture(file)
                local ul, ur, vl, vr = corner:GetTexCoord()
                tex:SetTexCoord(ul, ur, vl, vr)
            end
        end
    end)
end

-- Builds the classic border + background onto the given container frame.
local function BuildClassicChrome(container, parentFrame)
    -- Solid dark base fill first so there is never a transparent gap and the
    -- overall panel stays dark (the artifact art sits on top at reduced alpha).
    local base = container:CreateTexture(nil, "BACKGROUND", nil, -1)
    base:SetAllPoints(container)
    base:SetColorTexture(0.035, 0.035, 0.04, 0.98)
    container.base = base

    -- Clip frame: atlas starts below the title bar, not under the spec name.
    local bgClip = CreateFrame("Frame", nil, container)
    bgClip:SetPoint("TOPLEFT", container, "TOPLEFT", CLASSIC_BG_INSET, -CLASSIC_BG_TOP)
    bgClip:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -CLASSIC_BG_INSET, CLASSIC_BG_INSET)
    bgClip:SetClipsChildren(true)
    container.bgClip = bgClip

    local bg = bgClip:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(bgClip)
    ApplyClassicBackground(bg)
    container.bg = bg

    local layout = GetCharacterFrameLayout()
    if layout and NineSliceUtil.ApplyLayout then
        pcall(NineSliceUtil.ApplyLayout, container, layout)
    elseif container.SetBackdrop or BackdropTemplateMixin then
        -- Last-resort ornate border if the NineSlice layout is unavailable.
        if not container.SetBackdrop and BackdropTemplateMixin then
            Mixin(container, BackdropTemplateMixin)
        end
        if container.SetBackdrop then
            container:SetBackdrop({
                edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
                edgeSize = 32,
                insets = { left = 11, right = 12, top = 12, bottom = 11 },
            })
        end
    end

    if parentFrame then
        CreatePortraitRingOverlay(container, parentFrame)
    end
end

-- Current chart state (rebuilt on every Refresh).
local segments = {}   -- { entry, startSlice, endSlice, drawStart, drawEnd, color }
local hoveredKey      -- key currently hovered, nil if none

-- Forward declarations (legend rows reference these as upvalues).
local ApplyHighlight, SetStatCenter, RestoreCenter, ShowBlizzardStatTooltip, ShowItemLevelTooltip

----------------------------------------------------------------------
-- Construction
----------------------------------------------------------------------

local function BuildRing()
    ringTrack = ringHolder:CreateTexture(nil, "BACKGROUND")
    ringTrack:SetSize(RING_SIZE, RING_SIZE)
    ringTrack:SetPoint("CENTER", ringHolder, "CENTER", 0, 0)
    ringTrack:SetTexture(TRACK_TEX)
end

local function BuildRingWedges()
    for i = 0, SLICES - 1 do
        local angle = (i + 0.5) * SLICE_DEG
        local r = rad(angle)
        local x = MIDR * sin(r)
        local y = MIDR * cos(r)

        local fg = ringHolder:CreateTexture(nil, "ARTWORK")
        fg:SetTexture(WEDGE_TEX)
        fg:SetSize(WEDGE_W, WEDGE_H)
        fg:SetPoint("CENTER", ringHolder, "CENTER", x, y)
        fg:SetRotation(-r)
        fg:Hide()
        fgWedges[i + 1] = fg
    end
end

local function GetLegendRow(index)
    local row = legendRows[index]
    if row then return row end

    local metrics = GetLayoutMetrics()
    row = CreateFrame("Frame", nil, GetPanelContentParent())
    row:SetSize(contentWidth, metrics.legendRowH)
    row:EnableMouse(true)

    row.hl = row:CreateTexture(nil, "BACKGROUND")
    row.hl:SetPoint("TOPLEFT", row, "TOPLEFT", -4, 0)
    row.hl:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 4, 0)
    row.hl:SetColorTexture(1, 1, 1, 0.07)
    row.hl:Hide()

    -- Thin color pip links the row to its ring segment color.
    row.swatch = row:CreateTexture(nil, "ARTWORK")
    row.swatch:SetPoint("LEFT", row, "LEFT", 0, 0)

    -- RPG attribute icon (TGA from media/RPGIcons).
    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetPoint("LEFT", row.swatch, "RIGHT", 4, 0)
    row.icon:SetTexCoord(0.06, 0.94, 0.06, 0.94)

    -- Percent only; raw rating lives in the tooltip.
    row.pct = row:CreateFontString(nil, "OVERLAY")
    row.pct:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    row.pct:SetJustifyH("RIGHT")

    -- Label is bounded on the right by the percent column so long stat names
    -- (e.g. "Critical Strike") truncate with an ellipsis instead of overlapping.
    row.label = row:CreateFontString(nil, "OVERLAY")
    row.label:SetPoint("LEFT", row.icon, "RIGHT", 5, 0)
    row.label:SetPoint("RIGHT", row.pct, "LEFT", -6, 0)
    row.label:SetJustifyH("LEFT")
    row.label:SetWordWrap(false)

    StyleLegendRow(row, metrics)

    row:SetScript("OnEnter", function(self)
        if not self.entry then return end
        hoveredKey = self.entry.key
        SetStatCenter(self.entry)
        ApplyHighlight(self.entry.key)
        ShowBlizzardStatTooltip(self.entry)
    end)
    row:SetScript("OnLeave", function()
        hoveredKey = nil
        GameTooltip:Hide()
        RestoreCenter()
        ApplyHighlight(nil)
    end)

    legendRows[index] = row
    return row
end

local function HideBarFrom(index)
    for i = index, #barUI.rows do
        if barUI.rows[i] then
            barUI.rows[i]:Hide()
        end
    end
end

-- Hover dimmer: a child frame sized exactly to the attribute track (full bar).
EnsureBarHover = function(row)
    if not row then return end
    if not row.hover then
        row.hover = CreateFrame("Frame", nil, row)
        row.hover:EnableMouse(false)
        row.hover:Hide()

        row.hover.bg = row.hover:CreateTexture(nil, "BACKGROUND")
        row.hover.bg:SetAllPoints()
        row.hover.bg:SetColorTexture(0, 0, 0, BAR.hoverHlAlpha or 0.40)

        row.hover.pct = row.hover:CreateFontString(nil, "OVERLAY")
        row.hover.pct:SetPoint("RIGHT", row.hover, "RIGHT", -2, 0)
        row.hover.pct:SetJustifyH("RIGHT")
    end

    -- Keep legacy row.hl / pctBg hidden so they never clip a partial band.
    if row.hl then
        row.hl:Hide()
        row.hl:SetAlpha(0)
    end
    if row.pctBg then
        row.pctBg:Hide()
        row.pctBg:SetAlpha(0)
    end
    if row.pct then
        row.pct:Hide()
    end

    if row.track then
        row.hover:ClearAllPoints()
        row.hover:SetPoint("TOPLEFT", row.track, "TOPLEFT", 0, 0)
        row.hover:SetPoint("BOTTOMRIGHT", row.track, "BOTTOMRIGHT", 0, 0)
        local lvl = row:GetFrameLevel() or 1
        row.hover:SetFrameLevel(lvl + 10)
    end
    if row.hover.bg then
        row.hover.bg:SetColorTexture(0, 0, 0, BAR.hoverHlAlpha or 0.40)
    end
end

local function StyleBarRow(row, metrics)
    if not row or not metrics then return end
    row:SetHeight(metrics.barRowH)
    if row.icon then
        row.icon:SetSize(metrics.barIcon, metrics.barIcon)
    end
    if row.track then
        row.track:SetHeight(metrics.barTrackH)
    end
    if row.fill then
        row.fill:SetHeight(metrics.barTrackH)
    end
    if row.sheen then
        row.sheen:SetDrawLayer("ARTWORK", 1)
    end
    if row.label then
        row.label:SetFont(STAT_FONT, metrics.legendFont, STAT_FONT_FLAGS)
        row.label:SetWidth(metrics.barLabelW)
        row.label:SetDrawLayer("ARTWORK")
    end
    EnsureBarHover(row)
    if row.hover and row.hover.pct then
        row.hover.pct:SetFont(STAT_FONT, metrics.legendFont, STAT_FONT_FLAGS)
        row.hover.pct:SetWidth(BAR.pctW or 56)
        row.hover.pct:SetJustifyH("RIGHT")
    end
end

local function GetBarRow(index)
    local row = barUI.rows[index]
    if row then return row end
    if not barUI.holder then return nil end

    local metrics = GetLayoutMetrics()
    row = CreateFrame("Button", nil, barUI.holder)
    row:SetSize(contentWidth, metrics.barRowH)
    row:EnableMouse(true)

    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetPoint("LEFT", row, "LEFT", 0, 0)
    row.icon:SetTexCoord(0.06, 0.94, 0.06, 0.94)

    row.label = row:CreateFontString(nil, "ARTWORK")
    row.label:SetPoint("LEFT", row.icon, "RIGHT", 5, 0)
    row.label:SetJustifyH("LEFT")
    row.label:SetWordWrap(false)

    -- Full-bleed track to the right edge; hover frame matches this exactly.
    row.track = row:CreateTexture(nil, "BACKGROUND", nil, 1)
    row.track:SetPoint("LEFT", row.label, "RIGHT", 6, 0)
    row.track:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    row.track:SetColorTexture(1, 1, 1, BAR.trackAlpha or 0.18)

    row.fill = row:CreateTexture(nil, "ARTWORK")
    row.fill:SetPoint("LEFT", row.track, "LEFT", 0, 0)
    row.fill:SetHeight(metrics.barTrackH)
    row.fill:SetTexture(WHITE_TEX)

    -- Soft top sheen for a more polished bar look (below the hover overlay).
    row.sheen = row:CreateTexture(nil, "ARTWORK", nil, 1)
    row.sheen:SetTexture(WHITE_TEX)
    row.sheen:SetPoint("TOPLEFT", row.fill, "TOPLEFT", 0, 0)
    row.sheen:SetPoint("TOPRIGHT", row.fill, "TOPRIGHT", 0, 0)
    row.sheen:SetHeight(math.max(2, (metrics.barTrackH or BAR.trackH) * 0.42))
    pcall(row.sheen.SetGradient, row.sheen, "VERTICAL",
        CreateColor(1, 1, 1, 0),
        CreateColor(1, 1, 1, 0.28))

    EnsureBarHover(row)
    StyleBarRow(row, metrics)

    row:SetScript("OnEnter", function(self)
        if not self.entry then return end
        hoveredKey = self.entry.key
        SetStatCenter(self.entry)
        ApplyHighlight(self.entry.key)
        ShowBlizzardStatTooltip(self.entry)
    end)
    row:SetScript("OnLeave", function()
        hoveredKey = nil
        GameTooltip:Hide()
        RestoreCenter()
        ApplyHighlight(nil)
    end)

    barUI.rows[index] = row
    return row
end

local function BuildBarHolder()
    if barUI.holder or not contentFrame then return end
    barUI.holder = CreateFrame("Frame", nil, contentFrame)
    barUI.holder:SetSize(RING_SIZE, RING_SIZE)
    barUI.holder:EnableMouse(false)
    barUI.holder:Hide()

    -- Item-level band under the specialization title.
    barUI.headerRegion = CreateFrame("Frame", nil, contentFrame)
    barUI.headerRegion:SetSize(RING_SIZE, 1)
    barUI.headerRegion:EnableMouse(false)
    barUI.headerRegion:Hide()

    barUI.headerBig = barUI.headerRegion:CreateFontString(nil, "OVERLAY")
    barUI.headerBig:SetFont(STAT_FONT, 28, STAT_FONT_FLAGS)
    barUI.headerBig:SetPoint("CENTER", barUI.headerRegion, "CENTER", 0, 8)

    barUI.headerSmall = barUI.headerRegion:CreateFontString(nil, "OVERLAY")
    barUI.headerSmall:SetFont(STAT_FONT, 12, STAT_FONT_FLAGS)
    barUI.headerSmall:SetTextColor(0.85, 0.85, 0.85)
    barUI.headerSmall:SetPoint("TOP", barUI.headerBig, "BOTTOM", 0, -4)

    -- Hover: full-opacity attribute icon replaces the item-level number.
    barUI.headerIcon = barUI.headerRegion:CreateTexture(nil, "ARTWORK")
    barUI.headerIcon:SetSize(40, 40)
    barUI.headerIcon:SetPoint("CENTER", barUI.headerRegion, "CENTER", 0, 2)
    barUI.headerIcon:SetTexCoord(0.06, 0.94, 0.06, 0.94)
    barUI.headerIcon:SetAlpha(1)
    barUI.headerIcon:Hide()

    barUI.emptyText = barUI.holder:CreateFontString(nil, "OVERLAY")
    barUI.emptyText:SetFont(STAT_FONT, 12, STAT_FONT_FLAGS)
    barUI.emptyText:SetTextColor(0.6, 0.6, 0.6)
    barUI.emptyText:SetPoint("CENTER", barUI.holder, "CENTER", 0, 0)
    barUI.emptyText:SetText(L["NO_DATA"])
    barUI.emptyText:Hide()
end

----------------------------------------------------------------------
-- Positioning
----------------------------------------------------------------------

local function SetFrameMovable(movable)
    if not frame then return end
    frame:SetMovable(movable)
    if movable then
        frame:RegisterForDrag("LeftButton")
        if ringHolder then
            ringHolder:RegisterForDrag("LeftButton")
        end
    else
        frame:RegisterForDrag()
        if ringHolder then
            ringHolder:RegisterForDrag()
        end
    end
end

function Chart:ApplyPosition()
    if not frame then return end
    if embedded then return end  -- position is driven by the embed pane
    local db = ns.db
    frame:ClearAllPoints()
    if db.freeMove and db.pos then
        frame:SetPoint(db.pos.point, UIParent, db.pos.relPoint, db.pos.x, db.pos.y)
    else
        frame:SetPoint("LEFT", CharacterFrame, "RIGHT", 12, 0)
    end
end

function Chart:UpdateMovable()
    SetFrameMovable(ns.db.freeMove)
end

function Chart:ResetPosition()
    ns.db.pos = nil
    self:ApplyPosition()
end

-- Builds CharacterStatsPane-matching chrome used only while embedded.
-- Assigned on embedUI (not local) to stay under the 200-local main-chunk limit.
function embedUI.Ensure()
    if not frame or embedUI.classBg then return end

    local classBg = frame:CreateTexture(nil, "BACKGROUND", nil, -2)
    classBg:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
    classBg:Hide()
    embedUI.classBg = classBg

    local category = CreateFrame("Frame", nil, frame)
    category:SetSize(197, EMBED.specCatH)
    category:SetFrameLevel((frame:GetFrameLevel() or 1) + 18)
    category:Hide()
    embedUI.category = category

    local catBg = category:CreateTexture(nil, "BACKGROUND")
    pcall(function()
        catBg:SetAtlas(EMBED.titleAtlas, true)
    end)
    catBg:SetPoint("TOPLEFT", category, "TOPLEFT", 0, 0)
    embedUI.categoryBg = catBg

    local catTitle = category:CreateFontString(nil, "ARTWORK")
    catTitle:SetFontObject(GameFontHighlight)
    catTitle:SetPoint("CENTER", category, "CENTER", 0, 1)
    catTitle:SetText(SPECIALIZATION or L["TITLE"])
    embedUI.categoryTitle = catTitle

    local bounce = CreateFrame("Frame", nil, frame)
    bounce:SetSize(187, EMBED.specBounceH)
    bounce:SetFrameLevel((frame:GetFrameLevel() or 1) + 18)
    bounce:Hide()
    embedUI.bounce = bounce

    local bounceBg = bounce:CreateTexture(nil, "BORDER")
    pcall(function()
        bounceBg:SetAtlas(EMBED.bounceAtlas, true)
    end)
    bounceBg:SetAlpha(EMBED.bounceAlpha)
    bounceBg:SetPoint("CENTER", bounce, "CENTER", 0, 0)
    embedUI.bounceBg = bounceBg
end

-- Refreshes the class watermark atlas (same as CharacterStatsPane.ClassBackground).
function embedUI.UpdateClassBackground()
    if not embedUI.classBg then return end
    local _, class = UnitClass("player")
    if not class then
        embedUI.classBg:Hide()
        return
    end
    local ok = pcall(function()
        embedUI.classBg:SetAtlas(EMBED.classBgFmt:format(class), true)
    end)
    if not ok then
        embedUI.classBg:Hide()
        return
    end
    local theme = ResolveTheme()
    if embedded and theme == "classic" then
        embedUI.classBg:Show()
    end
end

-- Places the ornate specialization category + ItemLevel-style bounce band.
function embedUI.LayoutSpec()
    if not frame or not embedUI.category then return end

    local chromeLevel = (scrollFrame and scrollFrame:GetFrameLevel() or frame:GetFrameLevel()) + 10
    embedUI.category:SetFrameLevel(chromeLevel)
    embedUI.bounce:SetFrameLevel(chromeLevel)

    -- Match CharacterStatsPane sizes: title ornament and bounce keep native width.
    embedUI.category:ClearAllPoints()
    embedUI.category:SetSize(197, EMBED.specCatH)
    embedUI.category:SetPoint("TOP", frame, "TOP", 0, -EMBED.specTop)
    embedUI.category:Show()

    if embedUI.categoryBg then
        embedUI.categoryBg:ClearAllPoints()
        pcall(function()
            embedUI.categoryBg:SetAtlas(EMBED.titleAtlas, true)
        end)
        embedUI.categoryBg:SetPoint("TOPLEFT", embedUI.category, "TOPLEFT", 0, 0)
    end

    embedUI.bounce:ClearAllPoints()
    embedUI.bounce:SetSize(187, EMBED.specBounceH)
    embedUI.bounce:SetPoint("TOP", embedUI.category, "BOTTOM", 0, 0)
    embedUI.bounce:Show()

    if embedUI.bounceBg then
        embedUI.bounceBg:ClearAllPoints()
        pcall(function()
            embedUI.bounceBg:SetAtlas(EMBED.bounceAtlas, true)
        end)
        embedUI.bounceBg:SetAlpha(EMBED.bounceAlpha)
        embedUI.bounceBg:SetPoint("CENTER", embedUI.bounce, "CENTER", 0, 0)
    end

    if titleText then
        titleText:SetDrawLayer("OVERLAY")
    end
end

-- Class crest only (ElvUI embed): no category ornaments, watermark like the
-- ElvUI-skinned CharacterStatsPane.
function embedUI.ShowClassCrestOnly()
    embedUI.Ensure()
    embedUI.UpdateClassBackground()
    if embedUI.classBg then
        embedUI.classBg:Show()
    end
    if embedUI.category then embedUI.category:Hide() end
    if embedUI.bounce then embedUI.bounce:Hide() end
end

function embedUI.SetVisible(visible)
    if embedUI.classBg then
        if visible then
            embedUI.UpdateClassBackground()
            embedUI.classBg:Show()
        else
            embedUI.classBg:Hide()
        end
    end
    if embedUI.category then
        if visible then embedUI.category:Show() else embedUI.category:Hide() end
    end
    if embedUI.bounce then
        if visible then embedUI.bounce:Show() else embedUI.bounce:Hide() end
    end
end

function embedUI.HideAll()
    if embedUI.classBg then embedUI.classBg:Hide() end
    if embedUI.category then embedUI.category:Hide() end
    if embedUI.bounce then embedUI.bounce:Hide() end
    if embedUI.ilvlBounce then embedUI.ilvlBounce:Hide() end
    embedUI.showIlvlBounce = false
end

-- Item-level bounce texture behind the bar-layout item-level band (CharacterStatsPane).
function embedUI.EnsureIlvlBounce()
    if embedUI.ilvlBounce or not barUI.headerRegion then return end
    local tex = barUI.headerRegion:CreateTexture(nil, "BACKGROUND")
    pcall(function()
        tex:SetAtlas(EMBED.bounceAtlas, true)
    end)
    tex:SetAlpha(EMBED.bounceAlpha)
    tex:SetPoint("CENTER", barUI.headerRegion, "CENTER", 0, 0)
    tex:Hide()
    embedUI.ilvlBounce = tex
end

function embedUI.LayoutIlvlBounce()
    if not embedUI.showIlvlBounce or not barUI.headerRegion or not barUI.headerRegion:IsShown() then
        if embedUI.ilvlBounce then embedUI.ilvlBounce:Hide() end
        return
    end
    embedUI.EnsureIlvlBounce()
    if not embedUI.ilvlBounce then return end
    embedUI.ilvlBounce:ClearAllPoints()
    pcall(function()
        embedUI.ilvlBounce:SetAtlas(EMBED.bounceAtlas, true)
    end)
    embedUI.ilvlBounce:SetAlpha(EMBED.bounceAlpha)
    embedUI.ilvlBounce:SetPoint("CENTER", barUI.headerRegion, "CENTER", 0, 0)
    embedUI.ilvlBounce:Show()
end

-- Positions/tints the title and spec icon for the active theme. In classic the
-- icon fills the frame's portrait ring (round mask); in ElvUI it sits inline.
-- Embedded + classic uses CharacterStatsPane category + bounce chrome.
local function LayoutTitle(theme)
    if not titleText then return end
    titleText:ClearAllPoints()
    if specIcon then specIcon:ClearAllPoints() end
    local anchor = contentFrame or frame
    local metrics = GetLayoutMetrics()
    local classicEmbed = embedded and theme == "classic"

    if classicEmbed then
        embedUI.Ensure()
        embedUI.LayoutSpec()
        -- Spec name + icon sit in the ItemLevel-style bounce band under the header.
        titleText:SetParent(embedUI.bounce or frame)
        titleText:SetTextColor(1, 1, 1)
        titleText:SetFontObject(GameFontHighlight)
        if titleText.SetFont then
            titleText:SetFont(STAT_FONT, metrics.titleFont, "")
        end
        if specIcon then
            specIcon:SetParent(embedUI.bounce or frame)
            specIcon:SetSize(18, 18)
            if specIconMask and specIcon.RemoveMaskTexture then
                specIcon:RemoveMaskTexture(specIconMask)
            end
            specIcon:SetPoint("RIGHT", titleText, "LEFT", -5, 0)
        end
        titleText:SetPoint("CENTER", embedUI.bounce or frame, "CENTER", 10, -1)
        if embedUI.categoryTitle then
            embedUI.categoryTitle:SetText(SPECIALIZATION or L["TITLE"])
        end
        return
    end

    -- Floating panel / ElvUI+metal embed: restore content-frame parenting.
    if contentFrame then
        titleText:SetParent(contentFrame)
        if specIcon then
            if specIconHolder then
                specIcon:SetParent(specIconHolder)
            else
                specIcon:SetParent(contentFrame)
            end
        end
    end
    if titleText.SetFont then
        titleText:SetFont(STAT_FONT, metrics.titleFont, STAT_FONT_FLAGS)
    end

    if theme == "classic" and not embedded then
        if specIcon then
            -- Match the Blizzard portrait: filled circle inside the ring corner.
            local corner = portraitRingOverlay or (classicChrome and classicChrome.TopLeftCorner)
            if corner then
                local cw = corner:GetWidth()
                if not cw or cw <= 1 then cw = 64 end
                local d = math.max(28, cw * 0.72)
                specIcon:SetSize(d, d)
                specIcon:SetPoint("CENTER", corner, "CENTER", -1, 1)
            else
                specIcon:SetSize(46, 46)
                specIcon:SetPoint("CENTER", frame, "TOPLEFT", 33, -33)
            end
            if specIconMask then specIcon:AddMaskTexture(specIconMask) end
        end
        titleText:SetPoint("TOP", frame, "TOP", 0, -6)
        titleText:SetTextColor(CLASSIC_TITLE_COLOR[1], CLASSIC_TITLE_COLOR[2], CLASSIC_TITLE_COLOR[3])
    else
        local yExtra = (theme == "metal" and not embedded) and METAL_TITLE_Y_OFFSET or 0
        if specIcon then
            specIcon:SetSize(20, 20)
            specIcon:SetPoint("RIGHT", titleText, "LEFT", -6, 0)
            if specIconMask and specIcon.RemoveMaskTexture then
                specIcon:RemoveMaskTexture(specIconMask)
            end
        end
        -- Icon sits left of the title; a slight right offset keeps the pair centered.
        local xOff = 10
        titleText:SetPoint("TOP", anchor, "TOP", xOff, -(metrics.pad - 3 + yExtra))
        titleText:SetTextColor(1, 0.85, 0.45)
    end
end

-- Reports whether the panel is currently embedded in the character window.
function Chart:IsEmbedded()
    return embedded
end

-- Keeps the panel height locked to the character window height.
function Chart:UpdateHeight()
    if not frame then return end
    if embedded then return end  -- height follows the pane anchors
    local cf = CharacterFrame
    if not cf or not cf.GetHeight then return end
    local h = cf:GetHeight()
    if ns.IsSafeNumber(h) and h > 0 then
        frame:SetHeight(h)
    end
    if ResolveTheme() == "metal" then
        LayoutMetalBackground()
    end
end

-- Shows the chrome set for the active theme (or hides everything if disabled).
function Chart:ApplyTheme()
    if not frame then return end

    local show = ns.db.showChrome ~= false
    local theme = ResolveTheme()
    local classic = (theme == "classic")
    local metal = (theme == "metal")
    local elvui = (theme == "elvui")
    -- Embedded classic: CharacterStatsPane watermark + category ornaments.
    local useStatsPane = embedded and classic

    if useStatsPane then
        embedUI.Ensure()
        embedUI.showIlvlBounce = true
        if frame.bg then
            frame.bg:Hide()
        end
        if metalBg then metalBg:Hide() end
        if metalBgClip then metalBgClip:Hide() end
        if classicChrome then classicChrome:Hide() end
        if portraitRingOverlay then portraitRingOverlay:Hide() end
        if classicHeaderBar then classicHeaderBar:Hide() end
        if headerLine then headerLine:Hide() end
        for i = 1, #chromeBorder do
            local edge = chromeBorder[i]
            if edge then edge:Hide() end
        end
        embedUI.SetVisible(true)
        LayoutTitle("classic")
        return
    end

    -- Embedded ElvUI: fully transparent content over the live CharacterStatsPane
    -- backdrop (ElvUI glass + class crest). No classic ornaments / opaque fill.
    if embedded and elvui then
        embedUI.HideAll()
        if frame.bg then
            frame.bg:Hide()
        end
        if metalBg then metalBg:Hide() end
        if metalBgClip then metalBgClip:Hide() end
        if classicChrome then classicChrome:Hide() end
        if portraitRingOverlay then portraitRingOverlay:Hide() end
        if classicHeaderBar then classicHeaderBar:Hide() end
        if headerLine then headerLine:Hide() end
        for i = 1, #chromeBorder do
            local edge = chromeBorder[i]
            if edge then edge:Hide() end
        end
        LayoutTitle("elvui")
        return
    end

    embedUI.HideAll()

    if frame.bg then
        frame.bg:SetAlpha(1)
    end

    -- Embedded metal / floating: apply the selected theme chrome.
    if embedded then
        if classicChrome then classicChrome:Hide() end
        if portraitRingOverlay then portraitRingOverlay:Hide() end
        if classicHeaderBar then classicHeaderBar:Hide() end
    end

    local showElv = show and elvui
    SetElvPanelChromeVisible(showElv)
    if metalBg then
        if show and metal then
            metalBg:Show()
            LayoutMetalBackground()
        else
            metalBg:Hide()
        end
    end
    if metalBgClip then
        if show and metal then metalBgClip:Show() else metalBgClip:Hide() end
    end

    local showClassic = show and classic and not embedded
    if classicChrome then
        if showClassic then classicChrome:Show() else classicChrome:Hide() end
    end
    if portraitRingOverlay then
        if showClassic then portraitRingOverlay:Show() else portraitRingOverlay:Hide() end
    end
    if showClassic and classicChrome then
        LayoutClassicBackground(classicChrome)
    end
    if classicHeaderBar then
        classicHeaderBar:Hide()
    end

    LayoutTitle(theme)
end

function Chart:ApplyChrome()
    self:ApplyTheme()
end

function Chart:ApplyLayout()
    if not frame then return end
    if embedded and embedTarget then
        local w = embedTarget:GetWidth()
        if ns.IsSafeNumber(w) and w > 0 then
            frame:SetWidth(w)
        end
    else
        frame:SetWidth(PANEL_WIDTH)
    end
    contentWidth = GetLegendWidth()

    for i = 1, #legendRows do
        local row = legendRows[i]
        if row then
            row:SetWidth(contentWidth)
        end
    end
    if priorityText then
        priorityText:SetWidth(contentWidth - 4)
    end
    self:ApplyChrome()
    ApplyModeLayout()
    local theme = ResolveTheme()
    if theme == "metal" then
        LayoutMetalBackground()
    end
    if not embedded and classicChrome and theme == "classic" then
        LayoutClassicBackground(classicChrome)
    elseif embedded and theme == "classic" and embedUI.category then
        embedUI.LayoutSpec()
    end
    self:UpdateHeight()
    UpdateScrollExtent()
end

----------------------------------------------------------------------
-- Embed / float mode switching
----------------------------------------------------------------------

-- Moves the panel inside the character window, filling the given pane frame.
function Chart:EnterEmbed(pane)
    if not frame or not pane then return end
    embedded = true
    embedTarget = pane
    SetFrameMovable(false)
    frame:SetParent(pane)
    frame:ClearAllPoints()
    frame:SetAllPoints(pane)
    frame:SetFrameStrata(pane:GetFrameStrata())
    frame:SetFrameLevel((pane:GetFrameLevel() or 1) + 5)
    frame:SetClipsChildren(true)
    if scrollFrame then
        scrollFrame:SetVerticalScroll(0)
    end
    self:ApplyLayout()
    self:Refresh()
    frame:Show()
end

-- Returns the panel to its floating state next to the character window.
function Chart:ExitEmbed()
    if not frame then return end
    embedded = false
    embedTarget = nil
    frame:SetParent(CharacterFrame)
    frame:SetFrameStrata("HIGH")
    frame:SetFrameLevel((CharacterFrame:GetFrameLevel() or 1) + 10)
    frame:SetClipsChildren(false)
    if scrollFrame then
        scrollFrame:EnableMouseWheel(false)
        scrollFrame:SetVerticalScroll(0)
    end
    if ringHolder then
        ringHolder:SetScale(1)
    end
    self:UpdateMovable()
    self:ApplyPosition()
    self:ApplyLayout()
    self:Refresh()
    -- Visibility follows the CharacterFrame parent (as it did before embedding);
    -- an explicit Hide here would keep the child hidden even once the parent shows.
    frame:Show()
end

local function AttachResizeGrip()
    if not frame or resizeGrip then return end

    local grip = CreateFrame("Button", "StatOrbResizeGrip", frame)
    grip:EnableMouse(true)
    grip:SetSize(22, 22)
    grip:SetFrameStrata(frame:GetFrameStrata())
    grip:SetFrameLevel(frame:GetFrameLevel() + 55)
    grip:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)

    local gt = grip:CreateTexture(nil, "OVERLAY")
    gt:SetAllPoints()
    gt:SetTexture(RESIZE_TEX)
    gt:SetVertexColor(0.9, 0.18, 0.18, 0.9)

    grip:SetScript("OnEnter", function()
        gt:SetVertexColor(1, 0.42, 0.42, 1)
        GameTooltip:SetOwner(grip, "ANCHOR_LEFT")
        GameTooltip:SetText(L["RESIZE_GRIP"], 1, 1, 1)
        GameTooltip:Show()
    end)
    grip:SetScript("OnLeave", function()
        gt:SetVertexColor(0.9, 0.18, 0.18, 0.9)
        GameTooltip:Hide()
    end)

    local startX, startW
    local livePending = false
    local lastLiveT = 0
    local LIVE_MIN_DT = 0.05

    local function FireLiveResize()
        livePending = false
        lastLiveT = GetTime() or 0
        Chart:ApplyLayout()
    end

    local function QueueLiveResize()
        local now = GetTime() or 0
        if (now - lastLiveT) >= LIVE_MIN_DT then
            FireLiveResize()
        elseif not livePending then
            livePending = true
            local remaining = math.max(0, LIVE_MIN_DT - (now - lastLiveT))
            C_Timer.After(remaining, function()
                if livePending then FireLiveResize() end
            end)
        end
    end

    grip:RegisterForDrag("LeftButton")
    grip:SetScript("OnDragStart", function()
        if not RESIZE_GRIP_LIVE then return end
        local scale = frame:GetEffectiveScale()
        if not scale or scale == 0 then scale = 1 end
        startX = select(1, GetCursorPosition()) / scale
        startW = frame:GetWidth()
        grip.dragging = true
    end)
    grip:SetScript("OnUpdate", function()
        if not RESIZE_GRIP_LIVE or not grip.dragging then return end
        local scale = frame:GetEffectiveScale()
        if not scale or scale == 0 then scale = 1 end
        local curX = select(1, GetCursorPosition()) / scale
        local newW = ClampPanelWidth(startW + (curX - startX))
        if math.abs(newW - frame:GetWidth()) >= 1 then
            frame:SetWidth(newW)
            QueueLiveResize()
        end
    end)
    grip:SetScript("OnDragStop", function()
        grip.dragging = false
        if RESIZE_GRIP_LIVE then
            ns.db.panelWidth = Round(frame:GetWidth())
            Chart:ApplyLayout()
            if Chart.Refresh then
                Chart:Refresh()
            end
        end
    end)

    grip:Hide()
    grip:EnableMouse(RESIZE_GRIP_LIVE)
    frame:HookScript("OnShow", function()
        if RESIZE_GRIP_LIVE then grip:Show() else grip:Hide() end
    end)
    frame:HookScript("OnHide", function() grip:Hide() end)

    resizeGrip = grip
end

----------------------------------------------------------------------
-- Drawing
----------------------------------------------------------------------

local function HideAllWedges()
    for i = 1, SLICES do
        fgWedges[i]:Hide()
    end
end

local function HideLegendFrom(index)
    for i = index, #legendRows do
        legendRows[i]:Hide()
    end
end

-- Percent shown in the legend (effect when available, otherwise slice share).
local function GetDisplayPercent(entry)
    if type(entry.effect) == "number" then
        return entry.effect
    end
    return entry.percent or 0
end

local function SortDataByPercent(data, total)
    if not ns.IsSafeNumber(total) or total <= 0 then return end
    for _, entry in ipairs(data) do
        entry.percent = (entry.value / total) * 100
    end
    table.sort(data, function(a, b)
        local pa, pb = GetDisplayPercent(a), GetDisplayPercent(b)
        if pa ~= pb then
            return pa > pb
        end
        return a.key < b.key
    end)
end

-- Ring slices use effect % for secondaries (matches the legend) while keeping
-- the total secondary arc length driven by raw rating share vs primaries.
local function ComputeChartWeights(data)
    local secondaryValueSum = 0
    local secondaryEffectSum = 0
    for _, entry in ipairs(data) do
        if type(entry.effect) == "number" then
            secondaryValueSum = secondaryValueSum + entry.value
            secondaryEffectSum = secondaryEffectSum + entry.effect
        end
    end

    local secondaryScale = 1
    if secondaryEffectSum > 0 and secondaryValueSum > 0 then
        secondaryScale = secondaryValueSum / secondaryEffectSum
    end

    local chartTotal = 0
    for _, entry in ipairs(data) do
        if type(entry.effect) == "number" then
            entry.chartWeight = entry.effect * secondaryScale
        else
            entry.chartWeight = entry.value
        end
        chartTotal = chartTotal + entry.chartWeight
    end
    return chartTotal
end

-- Returns overall, equipped and PvP item level for the player.
local function GetItemLevelInfo()
    local ok, overall, equipped, pvp = pcall(GetAverageItemLevel)
    if not ok then return nil end
    if not ns.IsSafeNumber(equipped) or equipped <= 0 then
        return nil
    end
    local r, g, b = 1, 1, 1
    if GetItemLevelColor then
        local okColor, cr, cg, cb = pcall(GetItemLevelColor)
        if okColor and ns.IsSafeNumber(cr) then
            r, g, b = cr, cg, cb
        end
    end
    return {
        overall = ns.IsSafeNumber(overall) and overall or nil,
        equipped = equipped,
        pvp = ns.IsSafeNumber(pvp) and pvp or nil,
        color = { r, g, b },
    }
end

local function PositionTooltipBelowFrame()
    if not frame or not GameTooltip:IsShown() then return end
    GameTooltip:ClearAllPoints()
    GameTooltip:SetPoint("TOP", frame, "BOTTOM", 0, -8)
end

local tooltipRepositionTimer

local function ScheduleTooltipReposition()
    if tooltipRepositionTimer then
        tooltipRepositionTimer:Cancel()
    end
    tooltipRepositionTimer = C_Timer.NewTimer(0, function()
        tooltipRepositionTimer = nil
        PositionTooltipBelowFrame()
    end)
end

local function ShouldPinTooltipBelow()
    return frame and frame:IsShown()
        and (hoveredKey or (centerHit and centerHit:IsMouseOver()))
end

local function AttachTooltipHook()
    if GameTooltip.StatOrbTooltipHook then return end
    GameTooltip.StatOrbTooltipHook = true
    GameTooltip:HookScript("OnShow", function()
        if ShouldPinTooltipBelow() then
            PositionTooltipBelowFrame()
        end
    end)
end

local function NormalizeStatLabel(text)
    if not text then return "" end
    return text:gsub(":%s*$", "")
end

local function GetStatFrameLabel(statFrame)
    if statFrame.Label and statFrame.Label.GetText then
        return statFrame.Label:GetText()
    end
    local name = statFrame:GetName()
    if name then
        local label = _G[name .. "Label"]
        if label and label.GetText then
            return label:GetText()
        end
    end
end

local function FindPaperDollStatFrame(entry)
    if not CharacterStatsPane or not entry then return nil end
    local target = NormalizeStatLabel(entry.name)
    -- Embed hides CharacterStatsPane (classic) or strips its widgets (ElvUI);
    -- still scan those frames so their tooltips remain available.
    local allowHidden = embedded

    local function Scan(parent)
        if not parent then return nil end
        local children = { parent:GetChildren() }
        for i = 1, #children do
            local child = children[i]
            if child then
                local shown = child.IsShown and child:IsShown()
                if allowHidden or shown then
                    local labelText = GetStatFrameLabel(child)
                    if labelText and NormalizeStatLabel(labelText) == target then
                        return child
                    end
                    local found = Scan(child)
                    if found then return found end
                end
            end
        end
    end

    return Scan(CharacterStatsPane)
end

local proxyStatFrame

local function GetProxyStatFrame()
    if proxyStatFrame then return proxyStatFrame end
    proxyStatFrame = CreateFrame("Frame", "StatOrbStatProxy", UIParent)
    proxyStatFrame:Hide()
    proxyStatFrame.Label = proxyStatFrame:CreateFontString("StatOrbStatProxyLabel", "ARTWORK")
    proxyStatFrame.Value = proxyStatFrame:CreateFontString("StatOrbStatProxyValue", "ARTWORK")
    return proxyStatFrame
end

local STAT_PAPERDOLL_KEYS = {
    crit        = { "CRIT_RATING", "CRITCHANCE", "SPELLCRIT" },
    haste       = { "HASTE_RATING", "HASTE", "SPELL_HASTE" },
    mastery     = { "MASTERY" },
    versatility = { "VERSATILITY", "VERSATILITY_RATING" },
    leech       = { "LIFESTEAL", "LEECH" },
    avoidance   = { "AVOIDANCE" },
    speed       = { "SPEED", "SPEED_RATING" },
    strength    = { "STRENGTH" },
    agility     = { "AGILITY" },
    stamina     = { "STAMINA" },
    intellect   = { "INTELLECT" },
}

local function TriggerStatFrameTooltip(statFrame)
    if not statFrame then return false end
    local onEnter = statFrame:GetScript("OnEnter")
    if type(onEnter) == "function" then
        local ok = pcall(onEnter, statFrame)
        ScheduleTooltipReposition()
        if ok and GameTooltip:IsShown() then
            PositionTooltipBelowFrame()
            return true
        end
    end
    if PaperDollStatTooltip and statFrame.tooltip then
        local ok = pcall(PaperDollStatTooltip, statFrame)
        if ok and GameTooltip:IsShown() then
            PositionTooltipBelowFrame()
            ScheduleTooltipReposition()
            return true
        end
    end
    return false
end

-- Last-resort tooltip from PAPERDOLL_STATINFO fields (works while stats pane is hidden).
local function ShowProxyStatTooltip(proxy)
    if not proxy or not proxy.tooltip then return false end
    local owner = frame or UIParent
    GameTooltip:SetOwner(owner, "ANCHOR_NONE")
    GameTooltip:ClearLines()
    GameTooltip:SetText(proxy.tooltip, 1, 0.82, 0, 1, true)
    if proxy.tooltip2 then
        GameTooltip:AddLine(proxy.tooltip2, 1, 1, 1, true)
    end
    GameTooltip:Show()
    PositionTooltipBelowFrame()
    ScheduleTooltipReposition()
    return GameTooltip:IsShown()
end

function ShowBlizzardStatTooltip(entry)
    if not entry then return end

    local statFrame = FindPaperDollStatFrame(entry)
    if TriggerStatFrameTooltip(statFrame) then
        return
    end

    if not PAPERDOLL_STATINFO then return end
    local keys = STAT_PAPERDOLL_KEYS[entry.key]
    if not keys then return end

    local proxy = GetProxyStatFrame()
    for i = 1, #keys do
        local info = PAPERDOLL_STATINFO[keys[i]]
        if info and info.updateFunc then
            proxy.tooltip = nil
            proxy.tooltip2 = nil
            pcall(info.updateFunc, proxy, "player")
            if TriggerStatFrameTooltip(proxy) then
                return
            end
            if ShowProxyStatTooltip(proxy) then
                return
            end
        end
    end
end

-- Soft RPG icon in the donut hole, behind the center text.
local function SetCenterIconTexture(path)
    if not centerIcon then return end
    if path then
        centerIcon:SetTexture(path)
        centerIcon:SetAlpha(CENTER_ICON_ALPHA)
        centerIcon:Show()
    else
        centerIcon:Hide()
    end
end

local function SetCenterIcon(entry)
    local def = entry and ns.STAT_BY_KEY[entry.key]
    SetCenterIconTexture(def and def.icon)
end

local function SetItemLevelCenter()
    local ilvl = GetItemLevelInfo()
    if not ilvl then
        centerBig:SetText("")
        centerSmall:SetText("")
        SetCenterIconTexture(nil)
        return
    end
    centerBig:SetText(tostring(Round(ilvl.equipped)))
    centerBig:SetTextColor(ilvl.color[1], ilvl.color[2], ilvl.color[3])
    centerSmall:SetText(STAT_AVERAGE_ITEM_LEVEL or L["ITEM_LEVEL"])
    SetCenterIconTexture(CENTER_ILVL_ICON)
end

local function SetBarHeader(entry)
    if not barUI.headerBig then return end

    local function ShowItemLevel()
        if barUI.headerIcon then
            barUI.headerIcon:Hide()
        end
        barUI.headerBig:Show()
        barUI.headerSmall:Show()
        local ilvl = GetItemLevelInfo()
        if not ilvl then
            barUI.headerBig:SetText("")
            barUI.headerSmall:SetText("")
            return
        end
        barUI.headerBig:SetText(tostring(Round(ilvl.equipped)))
        barUI.headerBig:SetTextColor(ilvl.color[1], ilvl.color[2], ilvl.color[3])
        barUI.headerSmall:SetText(STAT_AVERAGE_ITEM_LEVEL or L["ITEM_LEVEL"])
        barUI.headerSmall:SetTextColor(0.85, 0.85, 0.85)
    end

    if not entry then
        ShowItemLevel()
        return
    end

    -- Hover: keep the band, but swap the number for the attribute icon at full opacity.
    local def = ns.STAT_BY_KEY and ns.STAT_BY_KEY[entry.key]
    local icon = def and def.icon
    if icon and barUI.headerIcon then
        barUI.headerBig:Hide()
        barUI.headerSmall:Hide()
        local size = (embedded and 36 or 42) * 1.6
        barUI.headerIcon:SetSize(size, size)
        barUI.headerIcon:SetTexture(icon)
        barUI.headerIcon:SetVertexColor(1, 1, 1, 1)
        barUI.headerIcon:SetAlpha(1)
        barUI.headerIcon:Show()
    else
        -- Fallback: attribute name only (no % in the header).
        if barUI.headerIcon then
            barUI.headerIcon:Hide()
        end
        barUI.headerBig:Show()
        barUI.headerSmall:Show()
        barUI.headerBig:SetText("")
        barUI.headerSmall:SetText(entry.name or "")
        if entry.color then
            barUI.headerSmall:SetTextColor(entry.color[1], entry.color[2], entry.color[3])
        end
    end
end

function SetStatCenter(entry)
    if IsBarLayout() then
        SetBarHeader(entry)
        return
    end
    if not entry then
        SetItemLevelCenter()
        return
    end
    if type(entry.effect) == "number" then
        centerBig:SetText(("%.2f%%"):format(entry.effect))
    else
        centerBig:SetText(("%d%%"):format(Round(entry.percent or 0)))
    end
    centerBig:SetTextColor(entry.color[1], entry.color[2], entry.color[3])
    centerSmall:SetText(entry.name)
    SetCenterIcon(entry)
end

function RestoreCenter()
    if IsBarLayout() then
        SetBarHeader(nil)
        return
    end
    SetItemLevelCenter()
end

function ShowItemLevelTooltip(anchor)
    local itemFrame = CharacterStatsPane and CharacterStatsPane.ItemLevelFrame
    if itemFrame then
        local onEnter = itemFrame:GetScript("OnEnter")
        if type(onEnter) == "function" then
            local ok = pcall(onEnter, itemFrame)
            if ok and GameTooltip:IsShown() then
                PositionTooltipBelowFrame()
                ScheduleTooltipReposition()
                return
            end
        end
    end

    local ilvl = GetItemLevelInfo()
    if not ilvl then return end
    if not STAT_AVERAGE_ITEM_LEVEL or not STAT_AVERAGE_ITEM_LEVEL_TOOLTIP then return end

    local line1 = ""
    if ilvl.overall then
        line1 = line1 .. HIGHLIGHT_FONT_COLOR_CODE
            .. format(PAPERDOLLFRAME_TOOLTIP_FORMAT, STAT_AVERAGE_ITEM_LEVEL)
            .. " " .. Round(ilvl.overall)
    end
    if ilvl.overall and ilvl.equipped and Round(ilvl.overall) ~= Round(ilvl.equipped) then
        if STAT_AVERAGE_ITEM_LEVEL_EQUIPPED then
            line1 = line1 .. "  " .. format(STAT_AVERAGE_ITEM_LEVEL_EQUIPPED, Round(ilvl.equipped))
        end
    end
    if ilvl.overall then
        line1 = line1 .. FONT_COLOR_CODE_CLOSE
    end

    local line2 = STAT_AVERAGE_ITEM_LEVEL_TOOLTIP
    if ilvl.overall and ilvl.pvp and Round(ilvl.overall) ~= Round(ilvl.pvp) and STAT_AVERAGE_PVP_ITEM_LEVEL then
        line2 = line2 .. "\n\n" .. STAT_AVERAGE_PVP_ITEM_LEVEL:format(tostring(Round(ilvl.pvp)))
    end

    GameTooltip:SetOwner(anchor or centerHit or frame, "ANCHOR_NONE")
    GameTooltip:ClearLines()
    GameTooltip:AddLine(line1, 1, 1, 1)
    if NORMAL_FONT_COLOR then
        GameTooltip:AddLine(line2, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, true)
    else
        GameTooltip:AddLine(line2, 1, 0.82, 0, true)
    end
    GameTooltip:Show()
    PositionTooltipBelowFrame()
    ScheduleTooltipReposition()
end

-- Ring/legend: brighten hovered segment and dim the rest.
-- Bar rows: dark overlay + % on the hovered row only (other bars stay full color).
-- Passing nil restores the chart to its neutral state.
function ApplyHighlight(key)
    for _, seg in ipairs(segments) do
        local factor = (key ~= nil and seg.entry.key ~= key) and DIM_FACTOR or 1
        local c = seg.color
        for t = seg.drawStart, seg.drawEnd - 1 do
            local fg = fgWedges[t + 1]
            if fg and fg:IsShown() then
                StyleWedge(fg, c[1], c[2], c[3], factor)
                fg:SetAlpha(1)
            end
        end
    end
    for _, row in ipairs(legendRows) do
        if key and row.entry and row.entry.key == key then
            row.hl:Show()
        else
            row.hl:Hide()
        end
        if row.swatch and row.entry and row.entry.color then
            local c = row.entry.color
            local factor = (key ~= nil and row.entry.key ~= key) and DIM_FACTOR or 1
            row.swatch:SetTexture(WHITE_TEX)
            SetStatGradient(row.swatch, c[1], c[2], c[3], "VERTICAL", factor)
        end
    end
    for _, row in ipairs(barUI.rows) do
        if row:IsShown() and row.fill and row.entry and row.entry.color then
            local c = row.entry.color
            -- Keep other bars at full color; only the hovered track gets the overlay.
            StyleBarFill(row.fill, c[1], c[2], c[3], 1)
            if row.sheen then
                row.sheen:SetHeight(math.max(2, (row.fill:GetHeight() or BAR.trackH) * 0.42))
                row.sheen:SetAlpha(1)
            end
            EnsureBarHover(row)
            local active = key and row.entry.key == key
            if row.hover then
                if active then
                    if row.hover.pct then
                        if type(row.entry.effect) == "number" then
                            row.hover.pct:SetText(("|cffffffff%.2f%%|r"):format(row.entry.effect))
                        else
                            row.hover.pct:SetText(("|cffffffff%d%%|r"):format(Round(row.entry.percent or 0)))
                        end
                        row.hover.pct:Show()
                    end
                    row.hover:Show()
                else
                    row.hover:Hide()
                end
            end
            if row.hl then row.hl:Hide() end
            if row.pctBg then row.pctBg:Hide() end
            if row.pct then row.pct:Hide() end
        else
            if row.hover then row.hover:Hide() end
            if row.hl then row.hl:Hide() end
            if row.pctBg then row.pctBg:Hide() end
            if row.pct then row.pct:Hide() end
        end
    end
end

-- Returns the stat entry under the cursor, or nil if outside the ring band.
local function GetEntryUnderCursor()
    if #segments == 0 then return nil end
    local scale = ringHolder:GetEffectiveScale()
    if not scale or scale == 0 then return nil end
    local cx, cy = ringHolder:GetCenter()
    if not cx then return nil end

    local mx, my = GetCursorPosition()
    local dx, dy = (mx / scale) - cx, (my / scale) - cy
    local dist = sqrt(dx * dx + dy * dy)
    if dist < INNER or dist > OUTER then
        return nil
    end

    local angle = deg(atan2(dx, dy))  -- 0 = top, clockwise
    if angle < 0 then angle = angle + 360 end
    local slice = (angle / 360) * SLICES
    for _, seg in ipairs(segments) do
        if slice >= seg.startSlice and slice < seg.endSlice then
            return seg.entry
        end
    end
    return nil
end

-- Reacts to the cursor moving over a (different) segment.
local function UpdateHover()
    local entry = GetEntryUnderCursor()
    local key = entry and entry.key or nil
    if key == hoveredKey then return end
    hoveredKey = key
    ApplyHighlight(key)
    if entry then
        SetStatCenter(entry)
        ShowBlizzardStatTooltip(entry)
    elseif centerHit and centerHit:IsMouseOver() then
        SetItemLevelCenter()
        ShowItemLevelTooltip(centerHit)
    else
        GameTooltip:Hide()
        RestoreCenter()
    end
end

-- Updates the spec header (icon + name) and the priority info string.
local function UpdateSpecHeader()
    local _, specName, iconPath = ns.GetCurrentSpecInfo()
    titleText:SetText(specName or L["TITLE"])
    if iconPath then
        specIcon:SetTexture(iconPath)
        specIcon:Show()
    else
        specIcon:Hide()
    end
    if embedded and ResolveTheme() == "classic" then
        embedUI.Ensure()
        if embedUI.categoryTitle then
            embedUI.categoryTitle:SetText(SPECIALIZATION or L["TITLE"])
        end
        embedUI.UpdateClassBackground()
    end

    local chain = ns.GetPriorityString()
    if chain and chain ~= "" then
        priorityText:SetText(("|cff9a9a9a%s|r\n%s"):format(L["PRIORITY"], chain))
    else
        priorityText:SetText("")
    end
end

function Chart:Refresh()
    if not frame then return end

    local metrics = GetLayoutMetrics()
    local barLayout = IsBarLayout()
    contentWidth = GetLegendWidth()
    UpdateSpecHeader()
    BuildBarHolder()

    local data, total = ns.Stats:Collect()
    SortDataByPercent(data, total)
    local chartTotal = ComputeChartWeights(data)

    wipe(segments)
    hoveredKey = nil
    GameTooltip:Hide()

    if not ns.IsSafeNumber(chartTotal) or chartTotal <= 0 or #data == 0 then
        HideAllWedges()
        HideLegendFrom(1)
        HideBarFrom(1)
        if barLayout then
            if ringHolder then ringHolder:Hide() end
            if barUI.holder then
                barUI.holder:Show()
                SetBarHeader(nil)
                if barUI.emptyText then barUI.emptyText:Show() end
            end
        else
            if ringHolder then ringHolder:Show() end
            if barUI.holder then barUI.holder:Hide() end
            SetItemLevelCenter()
            emptyText:Show()
        end
        if barLayout then
            SetBarHeader(nil)
            LayoutBarChartChrome(metrics)
        else
            PinPriorityToBottom(metrics)
        end
        if not embedded then
            frame:SetHeight(metrics.contentTop + GetChartBodyHeight(metrics, 0)
                + (priorityText:GetStringHeight() or 24) + 10 + metrics.pad)
        end
        self:UpdateHeight()
        ApplyModeLayout()
        UpdateScrollExtent()
        QueueScrollUpdate()
        return
    end
    emptyText:Hide()
    if barUI.emptyText then barUI.emptyText:Hide() end

    if barLayout then
        -- Horizontal bar chart: proportional fill per stat share.
        HideAllWedges()
        HideLegendFrom(1)
        if ringHolder then ringHolder:Hide() end
        if barUI.holder then barUI.holder:Show() end

        local maxWeight = 0
        for _, entry in ipairs(data) do
            if entry.chartWeight > maxWeight then
                maxWeight = entry.chartWeight
            end
        end
        if maxWeight <= 0 then maxWeight = 1 end

        local barW = GetBarChartWidth()
        contentWidth = barW
        local bodyH = GetChartBodyHeight(metrics, #data)
        barUI.holder:SetSize(barW, math.max(bodyH, 1))

        for i, entry in ipairs(data) do
            local row = GetBarRow(i)
            if row then
                row.entry = entry
                StyleBarRow(row, metrics)
                row:SetWidth(barW)
                if row.hover then row.hover:Hide() end
                if row.hl then row.hl:Hide() end
                row:ClearAllPoints()
                if i == 1 then
                    row:SetPoint("TOPLEFT", barUI.holder, "TOPLEFT", 0, 0)
                    row:SetPoint("TOPRIGHT", barUI.holder, "TOPRIGHT", 0, 0)
                else
                    row:SetPoint("TOPLEFT", barUI.rows[i - 1], "BOTTOMLEFT", 0, -(metrics.barGap or BAR.gap))
                    row:SetPoint("TOPRIGHT", barUI.rows[i - 1], "BOTTOMRIGHT", 0, -(metrics.barGap or BAR.gap))
                end

                local c = entry.color
                local def = ns.STAT_BY_KEY[entry.key]
                if def and def.icon then
                    row.icon:SetTexture(def.icon)
                    row.icon:SetVertexColor(1, 1, 1, 1)
                    row.icon:Show()
                else
                    row.icon:Hide()
                end

                local nameHex = ("%02x%02x%02x"):format(c[1] * 255, c[2] * 255, c[3] * 255)
                row.label:SetText(("|cff%s%s|r"):format(nameHex, entry.name))

                local share = entry.chartWeight / maxWeight
                if share < 0.04 then share = 0.04 end
                entry._barShare = share
                local trackAvail = barW - (metrics.barIcon or BAR.icon) - 5
                    - (metrics.barLabelW or BAR.labelW) - 6
                if trackAvail < 32 then trackAvail = 32 end

                row.track:ClearAllPoints()
                row.track:SetPoint("LEFT", row.label, "RIGHT", 6, 0)
                row.track:SetPoint("RIGHT", row, "RIGHT", 0, 0)
                row.track:SetHeight(metrics.barTrackH)
                row.track:SetColorTexture(1, 1, 1, BAR.trackAlpha or 0.14)

                row.fill:ClearAllPoints()
                row.fill:SetPoint("LEFT", row.track, "LEFT", 0, 0)
                row.fill:SetHeight(metrics.barTrackH)
                row.fill:SetWidth(math.max(3, trackAvail * share))
                StyleBarFill(row.fill, c[1], c[2], c[3], 1)
                if not row.sheen then
                    row.sheen = row:CreateTexture(nil, "ARTWORK", nil, 1)
                    row.sheen:SetTexture(WHITE_TEX)
                    row.sheen:SetPoint("TOPLEFT", row.fill, "TOPLEFT", 0, 0)
                    row.sheen:SetPoint("TOPRIGHT", row.fill, "TOPRIGHT", 0, 0)
                    pcall(row.sheen.SetGradient, row.sheen, "VERTICAL",
                        CreateColor(1, 1, 1, 0),
                        CreateColor(1, 1, 1, 0.28))
                else
                    row.sheen:SetDrawLayer("ARTWORK", 1)
                end
                row.sheen:SetHeight(math.max(2, (metrics.barTrackH or BAR.trackH) * 0.42))
                row.sheen:SetAlpha(1)

                -- Re-anchor after track points are final so overlay matches full bar width.
                EnsureBarHover(row)
                if row.hover then row.hover:Hide() end

                row:Show()

                segments[#segments + 1] = {
                    entry = entry,
                    startSlice = 0,
                    endSlice = 0,
                    drawStart = 0,
                    drawEnd = 0,
                    color = { c[1], c[2], c[3] },
                }
            end
        end
        HideBarFrom(#data + 1)

        SetBarHeader(nil)
        LayoutBarChartChrome(metrics)
        ApplyHighlight(hoveredKey)
        self:UpdateHeight()
        ApplyModeLayout()
        UpdateScrollExtent()
        QueueScrollUpdate()
        return
    end

    -- Circular ring layout (default).
    if barUI.holder then barUI.holder:Hide() end
    HideBarFrom(1)
    if ringHolder then ringHolder:Show() end

    -- Annular trapezoid wedges grouped by stat share.
    HideAllWedges()
    local prevSlice = 0
    local acc = 0
    local statCount = #data
    for i, entry in ipairs(data) do
        acc = acc + entry.chartWeight
        local endSlice = Round((acc / chartTotal) * SLICES)
        if endSlice > SLICES then endSlice = SLICES end

        local drawStart = prevSlice
        local drawEnd = endSlice
        if statCount > 1 then
            -- One wedge gap between consecutive stats (end of stat i OR start of stat i+1).
            if i < statCount then
                drawEnd = drawEnd - 1
            end
            -- Wrap boundary at 12 o'clock: first and last stat must not touch.
            if i == 1 then
                drawStart = drawStart + 1
            end
        end
        if drawEnd - drawStart < GAP_MIN_LEN then
            drawStart = prevSlice
            drawEnd = endSlice
        end

        local c = entry.color
        for t = drawStart, drawEnd - 1 do
            local fg = fgWedges[t + 1]
            if fg then
                StyleWedge(fg, c[1], c[2], c[3], 1)
                fg:SetAlpha(1)
                fg:Show()
            end
        end
        segments[#segments + 1] = {
            entry = entry,
            startSlice = prevSlice,
            endSlice = endSlice,
            drawStart = drawStart,
            drawEnd = drawEnd,
            color = { c[1], c[2], c[3] },
        }
        prevSlice = endSlice
    end

    SetItemLevelCenter()

    -- Legend flows down from the ring; Werte-Priorität is pinned to the panel bottom.
    local db = ns.db
    if db.showLegend then
        for i, entry in ipairs(data) do
            local row = GetLegendRow(i)
            row.entry = entry
            StyleLegendRow(row, metrics)
            row:SetWidth(contentWidth)
            row.hl:Hide()
            row:ClearAllPoints()
            if i == 1 then
                row:SetPoint("TOP", ringHolder, "BOTTOM", 0, -metrics.ringGap)
            else
                row:SetPoint("TOP", legendRows[i - 1], "BOTTOM", 0, -metrics.legendGap)
            end
            local c = entry.color
            row.swatch:SetTexture(WHITE_TEX)
            SetStatGradient(row.swatch, c[1], c[2], c[3], "VERTICAL", 1)

            local def = ns.STAT_BY_KEY[entry.key]
            if def and def.icon then
                row.icon:SetTexture(def.icon)
                row.icon:SetVertexColor(1, 1, 1, 1)
                row.icon:Show()
            else
                row.icon:Hide()
            end

            local nameHex = ("%02x%02x%02x"):format(c[1] * 255, c[2] * 255, c[3] * 255)
            row.label:SetText(("|cff8a8a8a%d.|r |cff%s%s|r"):format(i, nameHex, entry.name))
            if type(entry.effect) == "number" then
                row.pct:SetText(("|cffffffff%.2f%%|r"):format(entry.effect))
            else
                row.pct:SetText(("|cffffffff%d%%|r"):format(Round(entry.percent or 0)))
            end
            row:Show()
        end
        HideLegendFrom(#data + 1)
    else
        HideLegendFrom(1)
    end

    PinPriorityToBottom(metrics)

    -- Re-apply any active hover state after the rebuild.
    ApplyHighlight(hoveredKey)

    self:UpdateHeight()
    ApplyModeLayout()
    UpdateScrollExtent()
    QueueScrollUpdate()
end

----------------------------------------------------------------------
-- Init
----------------------------------------------------------------------

function Chart:IsShown()
    return frame and frame:IsShown()
end

function Chart:Init()
    if frame then return end
    if not CharacterFrame then return end

    frame = CreateFrame("Frame", "StatOrbChart", CharacterFrame)
    frame:SetSize(DEF_PANEL_W, DEF_PANEL_W)
    frame:SetFrameStrata("HIGH")
    frame:SetFrameLevel((CharacterFrame:GetFrameLevel() or 1) + 10)
    frame:SetClampedToScreen(true)
    frame:EnableMouse(true)

    -- Panel background: vertical dark gradient with a graceful solid fallback.
    local bg = frame:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(frame)
    local okGradient = pcall(function()
        bg:SetColorTexture(1, 1, 1, 1)
        bg:SetGradient("VERTICAL",
            CreateColor(PANEL_BOTTOM_COLOR[1], PANEL_BOTTOM_COLOR[2], PANEL_BOTTOM_COLOR[3], PANEL_BOTTOM_COLOR[4]),
            CreateColor(PANEL_TOP_COLOR[1], PANEL_TOP_COLOR[2], PANEL_TOP_COLOR[3], PANEL_TOP_COLOR[4]))
    end)
    if not okGradient then
        bg:SetColorTexture(PANEL_TOP_COLOR[1], PANEL_TOP_COLOR[2], PANEL_TOP_COLOR[3], PANEL_TOP_COLOR[4])
    end
    frame.bg = bg
    frame.bg:Hide()

    -- Metal theme: custom metal texture (no border, ElvUI-style content layout).
    metalBgClip = CreateFrame("Frame", nil, frame)
    metalBgClip:SetAllPoints(frame)
    metalBgClip:SetFrameLevel(frame:GetFrameLevel() + 2)
    metalBgClip:SetClipsChildren(true)
    metalBgClip:Hide()
    frame.metalBgClip = metalBgClip

    metalBg = metalBgClip:CreateTexture(nil, "BACKGROUND", nil, -2)
    ApplyMetalBackground(metalBg)
    metalBg:Hide()
    frame.metalBg = metalBg

    -- Scrollable content area (wheel enabled only in embed mode).
    scrollFrame = CreateFrame("ScrollFrame", "StatOrbScrollFrame", frame)
    scrollFrame:SetAllPoints(frame)
    scrollFrame:EnableMouse(true)
    scrollFrame:EnableMouseWheel(false)
    scrollFrame:SetFrameLevel(frame:GetFrameLevel() + 15)
    scrollFrame:SetScript("OnMouseWheel", function(self, delta)
        if not embedded then return end
        local cur = self:GetVerticalScroll() or 0
        local maxScroll = self:GetVerticalScrollRange() or 0
        local step = 28
        local newScroll = cur - delta * step
        if newScroll < 0 then newScroll = 0 end
        if newScroll > maxScroll then newScroll = maxScroll end
        self:SetVerticalScroll(newScroll)
    end)
    frame.scrollFrame = scrollFrame

    -- All interactive content sits above theme backgrounds and scrolls as a unit.
    contentFrame = CreateFrame("Frame", "StatOrbScrollChild", scrollFrame)
    contentFrame:SetSize(DEF_PANEL_W, DEF_PANEL_W)
    contentFrame:SetFrameLevel(scrollFrame:GetFrameLevel() + 5)
    scrollFrame:SetScrollChild(contentFrame)
    frame.contentFrame = contentFrame

    -- Thin 1px border framing the panel.
    local function MakeBorderLine()
        local t = frame:CreateTexture(nil, "BORDER")
        t:SetColorTexture(BORDER_COLOR[1], BORDER_COLOR[2], BORDER_COLOR[3], BORDER_COLOR[4])
        return t
    end
    local topEdge = MakeBorderLine()
    topEdge:SetHeight(1)
    topEdge:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
    topEdge:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
    chromeBorder[1] = topEdge
    local bottomEdge = MakeBorderLine()
    bottomEdge:SetHeight(1)
    bottomEdge:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0)
    bottomEdge:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
    chromeBorder[2] = bottomEdge
    local leftEdge = MakeBorderLine()
    leftEdge:SetWidth(1)
    leftEdge:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
    leftEdge:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0)
    chromeBorder[3] = leftEdge
    local rightEdge = MakeBorderLine()
    rightEdge:SetWidth(1)
    rightEdge:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
    rightEdge:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
    chromeBorder[4] = rightEdge

    -- Classic chrome: mirrors the real CharacterFrame (NineSlice border + bg).
    -- Sits behind the panel so it never covers the ring or the stat list.
    classicChrome = CreateFrame("Frame", nil, frame)
    classicChrome:SetFrameStrata(frame:GetFrameStrata())
    classicChrome:SetFrameLevel(math.max(1, (frame:GetFrameLevel() or 2) - 1))
    classicChrome:SetAllPoints(frame)
    classicChrome:Hide()
    BuildClassicChrome(classicChrome, frame)

    -- Beveled category-style header bar behind the spec title (classic theme).
    classicHeaderBar = frame:CreateTexture(nil, "ARTWORK")
    classicHeaderBar:SetTexture(CLASSIC_BAR_TEX)
    classicHeaderBar:SetTexCoord(CLASSIC_BAR_COORDS[1], CLASSIC_BAR_COORDS[2],
        CLASSIC_BAR_COORDS[3], CLASSIC_BAR_COORDS[4])
    classicHeaderBar:SetHeight(HEADER_H - 2)
    classicHeaderBar:SetPoint("TOPLEFT", frame, "TOPLEFT", PAD, -(PAD - 2))
    classicHeaderBar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -PAD, -(PAD - 2))
    classicHeaderBar:Hide()

    frame:SetScript("OnDragStart", function(self)
        if ns.db.freeMove then
            self:StartMoving()
        end
    end)
    frame:SetScript("OnDragStop", function(self)
        if not ns.db.freeMove then return end
        self:StopMovingOrSizing()
        local point, _, relPoint, x, y = self:GetPoint(1)
        ns.db.pos = { point = point, relPoint = relPoint, x = x, y = y }
    end)
    frame:SetScript("OnMouseUp", function(self, button)
        if button == "RightButton" and ns.Config and ns.Config.OpenMenu then
            ns.Config:OpenMenu(self)
        end
    end)

    titleText = contentFrame:CreateFontString(nil, "OVERLAY")
    titleText:SetFont(STAT_FONT, 14, STAT_FONT_FLAGS)
    titleText:SetTextColor(1, 0.85, 0.45)
    titleText:SetPoint("TOP", contentFrame, "TOP", 10, -(PAD - 3))
    titleText:SetText(L["TITLE"])

    specIconHolder = CreateFrame("Frame", nil, contentFrame)
    specIconHolder:SetFrameLevel(contentFrame:GetFrameLevel() + 5)

    specIcon = specIconHolder:CreateTexture(nil, "ARTWORK")
    specIcon:SetSize(20, 20)
    specIcon:SetPoint("RIGHT", titleText, "LEFT", -6, 0)
    specIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    specIcon:Hide()

    -- Circular mask so the spec icon reads as a portrait inside the classic ring.
    specIconMask = specIconHolder:CreateMaskTexture()
    specIconMask:SetAllPoints(specIcon)
    specIconMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask",
        "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")

    -- Menu stays fixed on the outer frame so it does not scroll away in embed mode.
    menuBtn = CreateFrame("Button", nil, frame)
    menuBtn:SetSize(22, 22)
    menuBtn:SetFrameLevel(frame:GetFrameLevel() + 40)
    menuBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -5, -25)
    local menuIcon = menuBtn:CreateTexture(nil, "ARTWORK")
    menuIcon:SetAllPoints()
    pcall(function() menuIcon:SetAtlas(MENU_BTN_ATLAS) end)
    menuBtn:SetScript("OnClick", function(self)
        if ns.Config and ns.Config.OpenMenu then
            ns.Config:OpenMenu(self)
        end
    end)
    menuBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT")
        GameTooltip:SetText(L["MENU_TITLE"], 1, 1, 1)
        GameTooltip:Show()
    end)
    menuBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    headerLine = frame:CreateTexture(nil, "ARTWORK")
    headerLine:SetColorTexture(SEP_COLOR[1], SEP_COLOR[2], SEP_COLOR[3], SEP_COLOR[4])
    headerLine:SetHeight(1)
    headerLine:SetPoint("TOPLEFT", frame, "TOPLEFT", PAD, -(PAD + HEADER_H))
    headerLine:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -PAD, -(PAD + HEADER_H))

    ringHolder = CreateFrame("Frame", nil, contentFrame)
    ringHolder:SetSize(RING_SIZE, RING_SIZE)
    ringHolder:SetPoint("TOP", contentFrame, "TOP", 0, -CONTENT_TOP)

    -- Ring is interactive: hover for tooltips, drag to move, right-click for menu.
    ringHolder:EnableMouse(true)
    ringHolder:SetScript("OnDragStart", function()
        if ns.db.freeMove then
            frame:StartMoving()
        end
    end)
    ringHolder:SetScript("OnDragStop", function()
        if not ns.db.freeMove then return end
        frame:StopMovingOrSizing()
        local point, _, relPoint, x, y = frame:GetPoint(1)
        ns.db.pos = { point = point, relPoint = relPoint, x = x, y = y }
    end)
    ringHolder:SetScript("OnMouseUp", function(self, button)
        if button == "RightButton" and ns.Config and ns.Config.OpenMenu then
            ns.Config:OpenMenu(self)
        end
    end)

    local hoverElapsed = 0
    local function OnRingUpdate(_, elapsed)
        hoverElapsed = hoverElapsed + elapsed
        if hoverElapsed < 0.03 then return end
        hoverElapsed = 0
        UpdateHover()
    end
    ringHolder:SetScript("OnEnter", function(self)
        hoverElapsed = 1
        self:SetScript("OnUpdate", OnRingUpdate)
        UpdateHover()
    end)
    ringHolder:SetScript("OnLeave", function(self)
        self:SetScript("OnUpdate", nil)
        if centerHit and centerHit:IsMouseOver() then
            SetItemLevelCenter()
            ShowItemLevelTooltip(centerHit)
        else
            hoveredKey = nil
            GameTooltip:Hide()
            RestoreCenter()
            ApplyHighlight(nil)
        end
    end)

    centerHit = CreateFrame("Frame", nil, ringHolder)
    centerHit:SetSize(INNER * 2 - 4, INNER * 2 - 4)
    centerHit:SetPoint("CENTER", ringHolder, "CENTER", 0, 0)
    centerHit:EnableMouse(true)
    centerHit:SetFrameLevel(ringHolder:GetFrameLevel() + 20)
    centerHit:SetScript("OnEnter", function()
        hoveredKey = nil
        ApplyHighlight(nil)
        SetItemLevelCenter()
        ShowItemLevelTooltip(centerHit)
    end)
    centerHit:SetScript("OnLeave", function()
        if GetEntryUnderCursor() then
            UpdateHover()
        else
            GameTooltip:Hide()
            RestoreCenter()
        end
    end)

    -- Soft attribute icon behind the center text (donut hole).
    centerIcon = ringHolder:CreateTexture(nil, "ARTWORK", nil, -1)
    centerIcon:SetSize(CENTER_ICON_SIZE, CENTER_ICON_SIZE)
    centerIcon:SetPoint("CENTER", ringHolder, "CENTER", 0, 2)
    centerIcon:SetTexCoord(0.06, 0.94, 0.06, 0.94)
    centerIcon:SetAlpha(CENTER_ICON_ALPHA)
    centerIcon:Hide()

    centerBig = ringHolder:CreateFontString(nil, "OVERLAY")
    centerBig:SetFont(STAT_FONT, 30, STAT_FONT_FLAGS)
    centerBig:SetPoint("CENTER", ringHolder, "CENTER", 0, 8)

    centerSmall = ringHolder:CreateFontString(nil, "OVERLAY")
    centerSmall:SetFont(STAT_FONT, 12, STAT_FONT_FLAGS)
    centerSmall:SetTextColor(0.85, 0.85, 0.85)
    centerSmall:SetPoint("TOP", centerBig, "BOTTOM", 0, -3)

    emptyText = ringHolder:CreateFontString(nil, "OVERLAY")
    emptyText:SetFont(STAT_FONT, 12, STAT_FONT_FLAGS)
    emptyText:SetTextColor(0.6, 0.6, 0.6)
    emptyText:SetPoint("CENTER", ringHolder, "CENTER", 0, 0)
    emptyText:SetText(L["NO_DATA"])
    emptyText:Hide()

    priorityText = contentFrame:CreateFontString(nil, "OVERLAY")
    priorityText:SetFont(STAT_FONT, 11, STAT_FONT_FLAGS)
    priorityText:SetWidth(RING_SIZE - 4)
    priorityText:SetWordWrap(true)
    priorityText:SetJustifyH("CENTER")
    priorityText:SetSpacing(2)

    BuildRing()
    BuildRingWedges()
    AttachTooltipHook()
    AttachResizeGrip()

    -- Show/refresh together with the character window.
    CharacterFrame:HookScript("OnShow", function()
        Chart:ApplyPosition()
        Chart:ApplyLayout()
        Chart:Refresh()
    end)

    -- Keep the panel height locked to the character window as it resizes.
    CharacterFrame:HookScript("OnSizeChanged", function()
        Chart:UpdateHeight()
    end)

    self:ApplyLayout()
    self:ApplyPosition()
    self:UpdateMovable()
    frame:Show()  -- visibility now follows the CharacterFrame parent

    if CharacterFrame:IsShown() then
        self:Refresh()
    end
end
