--[[
    Warband Nexus - Tooltip Factory
    UI frame creation for custom tooltips
    
    Layout:
    â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
    â”‚ [Icon]  Title                    â”‚
    â”‚         Description (optional)   â”‚
    â”‚â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”‚
    â”‚ Data lines...                    â”‚
    â”‚ Left:                     Right  â”‚
    â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜
    
    Follows SharedWidgets pattern:
    - Theme-aware styling
    - Frame pooling and recycling
    - Consistent visual design
]]

local ADDON_NAME, ns = ...
local issecretvalue = issecretvalue
local FontManager = ns.FontManager  -- Centralized font management

--- SharedWidgets sets `ns.UI_SPACING`; never capture nil at file load (TOC/embed order can defer assignment).
local function GetTooltipUISpacing()
    local sp = ns.UI_SPACING or ns.UI_LAYOUT
    if sp then return sp end
    return { SIDE_MARGIN = 10, TOP_MARGIN = 8, AFTER_ELEMENT = 8 }
end

local function GetTooltipPaddingH()
    local spacing = GetTooltipUISpacing()
    return spacing.SIDE_MARGIN or 12
end

local function GetTooltipPaddingV()
    local spacing = GetTooltipUISpacing()
    return spacing.TOP_MARGIN or spacing.SIDE_MARGIN or 12
end

-- TOOLTIP FACTORY

ns.UI = ns.UI or {}
ns.UI.TooltipFactory = {}

-- Cache colors (updated on theme change)
local COLORS = nil

local function SemanticGoldRGB()
    if ns.UI_GetSemanticGoldColor then
        return ns.UI_GetSemanticGoldColor()
    end
    return 1, 0.82, 0, 1
end

local function TextRoleRGB(role)
    if ns.UI_GetTextRoleRGB then
        return ns.UI_GetTextRoleRGB(role)
    end
    local c = ns.UI_COLORS
    if c and c["text" .. role] then
        local t = c["text" .. role]
        return t[1], t[2], t[3], t[4] or 1
    end
    if role == "Muted" then return 0.7, 0.7, 0.72, 1 end
    if role == "Dim" then return 0.55, 0.55, 0.55, 1 end
    return 0.85, 0.85, 0.85, 1
end

--- Set ink + light-mode outline (markup-aware).
local function ApplyTooltipInk(fs, text, r, g, b)
    if not fs then return end
    if text ~= nil then
        fs:SetText(text)
    end
    if r ~= nil and g ~= nil and b ~= nil then
        if ns.UI_SetInkColor then
            ns.UI_SetInkColor(fs, r, g, b, 1)
        else
            fs:SetTextColor(r, g, b, 1)
        end
    end
    if ns.UI_ApplyFontStringPresentation then
        local presentationText = text
        if presentationText == nil and fs.GetText then
            local got = fs:GetText()
            if not (issecretvalue and issecretvalue(got)) then
                presentationText = got
            end
        end
        ns.UI_ApplyFontStringPresentation(fs, presentationText)
    elseif FontManager and FontManager.RefreshInkAwareFont then
        FontManager:RefreshInkAwareFont(fs)
    end
end

local function BootstrapTooltipColors()
    if ns.UI_COLORS then
        return ns.UI_COLORS
    end
    local bg, border = { 0.08, 0.08, 0.10, 1 }, { 0.20, 0.20, 0.25, 1 }
    if ns.UI_GetTooltipShellBackdrop then
        bg, border = ns.UI_GetTooltipShellBackdrop()
    end
    return {
        bgCard = bg,
        border = border,
        accent = { 0.45, 0.35, 0.72, 1 },
    }
end

-- Layout constants
local ICON_SIZE = 32
local ICON_PADDING = 8
local FALLBACK_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"

local function TintTooltipAccentLines(frame)
    if not frame then return end
    local c = ns.UI_COLORS or COLORS or {}
    local ac = c.accent or { 0.45, 0.35, 0.72 }
    if frame.separator then
        frame.separator:SetColorTexture(ac[1], ac[2], ac[3], 0.42)
    end
    if frame.iconFrame and frame.iconFrame.border then
        frame.iconFrame.border:SetColorTexture(ac[1], ac[2], ac[3], 0.42)
    end
end

-- Wide tooltips (e.g. PvE vault summary): TooltipService may preset frame.fixedWidth before layout.
local WIDTH_CAP_TOP = 820
local VAULT_GRID_GAP_NAME_REALM = 10
local VAULT_GRID_GAP_BEFORE_VAULT = 12
local VAULT_GRID_GAP_VAULT_COL = 6
-- Minimum width per Raid/M+/World column (three slot icons or â€œPendingâ€¦â€).
local VAULT_GRID_TRACK_MIN_W = 72
local VAULT_TRACK_GAP_COL = 10

--[[
    Create singleton tooltip frame
    @return Frame - Reusable tooltip frame
]]
function ns.UI.TooltipFactory:CreateTooltipFrame()
    -- Get current theme colors
    COLORS = BootstrapTooltipColors()
    
    -- Create frame
    local frame = CreateFrame("Frame", "WarbandNexusTooltip", UIParent, "BackdropTemplate")
    frame:SetFrameStrata("TOOLTIP")
    frame:SetFrameLevel(10000)
    frame:SetSize(250, 100)
    frame:Hide()
    
    -- Phase 2: elevated surface + atlas underlay + accent inset border (parity with tab cards).
    if ns.UI_ApplyStandardCardElevatedChrome then
        ns.UI_ApplyStandardCardElevatedChrome(frame)
    else
        local bg, border = { COLORS.bgCard[1], COLORS.bgCard[2], COLORS.bgCard[3], COLORS.bgCard[4] or 0.98 },
            { COLORS.border[1], COLORS.border[2], COLORS.border[3], 1 }
        if ns.UI_GetTooltipShellBackdrop then
            bg, border = ns.UI_GetTooltipShellBackdrop()
        end
        frame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
            insets = { left = 0, right = 0, top = 0, bottom = 0 }
        })
        frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 0.98)
        frame:SetBackdropBorderColor(border[1], border[2], border[3], border[4] or 1)
    end
    
    -- Icon (top-left, created once, reused)
    local iconFrame = CreateFrame("Frame", nil, frame)
    iconFrame:SetSize(ICON_SIZE, ICON_SIZE)
    
    local iconTexture = iconFrame:CreateTexture(nil, "ARTWORK")
    iconTexture:SetAllPoints()
    iconTexture:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    iconFrame.texture = iconTexture
    
    -- Icon border (thin accent border)
    local iconBorder = iconFrame:CreateTexture(nil, "OVERLAY")
    iconBorder:SetPoint("TOPLEFT", -1, 1)
    iconBorder:SetPoint("BOTTOMRIGHT", 1, -1)
    iconFrame.border = iconBorder
    
    -- Icon sits behind the texture (border effect)
    iconTexture:SetDrawLayer("ARTWORK", 1)
    iconBorder:SetDrawLayer("ARTWORK", 0)
    
    frame.iconFrame = iconFrame
    
    -- Separator line (thin horizontal line between header and body)
    local separator = frame:CreateTexture(nil, "ARTWORK")
    separator:SetHeight(1)
    frame.separator = separator
    
    TintTooltipAccentLines(frame)
    
    -- FontString pools (recycled for performance)
    frame.lines = {}
    frame.linePool = {}
    frame.doubleLines = {}
    frame.doubleLinePool = {}
    frame.vaultGridRows = {}
    frame.vaultGridLinePool = {}
    frame.vaultTrackRows = {}
    frame.vaultTrackLinePool = {}
    frame.titleLine = nil
    frame.descLine = nil
    frame.titleAffixLines = {}
    frame.hasTitleAffixPair = false
    frame.titleAffixPairLeft = nil
    frame.titleAffixPairRight = nil
    frame.bodyDividerPool = {}
    frame.allLines = {}
    frame._balancedSplit = nil
    frame._deferLayout = false
    frame._layoutDirty = false

    local function RequestLayout(tooltipFrame)
        if tooltipFrame._deferLayout then
            tooltipFrame._layoutDirty = true
            return
        end
        tooltipFrame:LayoutLines()
    end

    frame.BeginBatchLayout = function(self)
        self._deferLayout = true
        self._layoutDirty = false
    end

    frame.EndBatchLayout = function(self)
        self._deferLayout = false
        RequestLayout(self)
        self._layoutDirty = false
    end
    
    -- Layout state (dynamic width, clamped to min/max)
    local MIN_WIDTH = 120
    local MAX_WIDTH = 450
    frame.currentHeight = 10
    frame.fixedWidth = 350
    frame.paddingH = GetTooltipPaddingH()
    frame.paddingV = GetTooltipPaddingV()
    frame.hasIcon = false
    
    -- API: Clear all content
    frame.Clear = function(self)
        -- Hide icon
        self.iconFrame:Hide()
        self.hasIcon = false
        self.separator:Hide()
        
        -- Hide and clear title line
        if self.titleLine then
            self.titleLine:Hide()
            self.titleLine:SetText("")
            self.titleLine:ClearAllPoints()
        end
        
        -- Hide and clear description line
        if self.descLine then
            self.descLine:Hide()
            self.descLine:SetText("")
            self.descLine:ClearAllPoints()
        end

        for i = 1, #self.titleAffixLines do
            local tLine = self.titleAffixLines[i]
            tLine:Hide()
            tLine:SetText("")
            tLine:ClearAllPoints()
            table.insert(self.linePool, tLine)
        end
        table.wipe(self.titleAffixLines)

        if self.titleAffixPairLeft then
            self.titleAffixPairLeft:Hide()
            self.titleAffixPairRight:Hide()
            self.titleAffixPairLeft:SetText("")
            self.titleAffixPairRight:SetText("")
            self.titleAffixPairLeft:ClearAllPoints()
            self.titleAffixPairRight:ClearAllPoints()
        end
        self.hasTitleAffixPair = false
        for i = 1, #self.bodyDividerPool do
            local div = self.bodyDividerPool[i]
            if div then
                div:Hide()
                div:ClearAllPoints()
            end
        end
        self._balancedSplit = nil
        self._wnStableWidth = nil
        
        -- Return all lines to pool
        for i = 1, #self.lines do
            local line = self.lines[i]
            line:Hide()
            line:SetText("")
            line:ClearAllPoints()
            if line.SetJustifyH then
                line:SetJustifyH("LEFT")
            end
            table.insert(self.linePool, line)
        end
        table.wipe(self.lines)
        
        -- Return double lines to pool
        for i = 1, #self.doubleLines do
            local dLine = self.doubleLines[i]
            dLine.left:Hide()
            dLine.right:Hide()
            dLine.left:SetText("")
            dLine.right:SetText("")
            dLine.left:ClearAllPoints()
            dLine.right:ClearAllPoints()
            table.insert(self.doubleLinePool, dLine)
        end
        table.wipe(self.doubleLines)

        for i = 1, #self.vaultGridRows do
            local vr = self.vaultGridRows[i]
            vr.nameFs:Hide()
            vr.realmFs:Hide()
            vr.raidFs:Hide()
            vr.mplusFs:Hide()
            vr.worldFs:Hide()
            vr.nameFs:SetText("")
            vr.realmFs:SetText("")
            vr.raidFs:SetText("")
            vr.mplusFs:SetText("")
            vr.worldFs:SetText("")
            vr.nameFs:ClearAllPoints()
            vr.realmFs:ClearAllPoints()
            vr.raidFs:ClearAllPoints()
            vr.mplusFs:ClearAllPoints()
            vr.worldFs:ClearAllPoints()
            table.insert(self.vaultGridLinePool, vr)
        end
        table.wipe(self.vaultGridRows)

        for i = 1, #self.vaultTrackRows do
            local tr = self.vaultTrackRows[i]
            tr.raidFs:Hide()
            tr.mplusFs:Hide()
            tr.worldFs:Hide()
            tr.raidFs:SetText("")
            tr.mplusFs:SetText("")
            tr.worldFs:SetText("")
            tr.raidFs:ClearAllPoints()
            tr.mplusFs:ClearAllPoints()
            tr.worldFs:ClearAllPoints()
            table.insert(self.vaultTrackLinePool, tr)
        end
        table.wipe(self.vaultTrackRows)
        
        for i = 1, #self.allLines do
            local lineData = self.allLines[i]
            if lineData.type == "divider" and lineData.element then
                local div = lineData.element
                div:Hide()
                div:ClearAllPoints()
                table.insert(self.bodyDividerPool, div)
            end
        end

        -- Clear unified line list
        table.wipe(self.allLines)
        
        -- Reset sizing (MAX_WIDTH as initial, LayoutLines will compute actual)
        self.currentHeight = 10
        self.fixedWidth = MAX_WIDTH
        self.paddingH = GetTooltipPaddingH()
        self.paddingV = GetTooltipPaddingV()
        self:SetSize(self.fixedWidth, 10)
        self._deferLayout = false
        self._layoutDirty = false
    end
    
    -- API: Set icon (top-left corner)
    frame.SetIcon = function(self, iconPath, isAtlas)
        local tex = self.iconFrame.texture
        if iconPath then
            if isAtlas then
                local ok = pcall(function()
                    tex:SetAtlas(iconPath, false)
                end)
                if ok then
                    tex:SetTexCoord(0, 1, 0, 1)
                else
                    tex:SetTexture("Interface\\Icons\\INV_Misc_Chest_03")
                    tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
                end
            elseif type(iconPath) == "number" then
                tex:SetTexture(iconPath)
                tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            else
                tex:SetTexture(iconPath)
                tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            end
        else
            tex:SetTexture(FALLBACK_ICON)
            tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        end
        self.hasIcon = true
        self.iconFrame:Show()
    end
    
    -- API: Set title (always at top, next to icon if present)
    frame.SetTitle = function(self, text, r, g, b)
        local titleLine = self:GetOrCreateTitleLine()
        titleLine:SetText(text or "")
        if r or g or b then
            titleLine:SetTextColor(r or 1, g or 0.82, b or 0)
        else
            local tr, tg, tb = SemanticGoldRGB()
            titleLine:SetTextColor(tr, tg, tb, 1)
        end
        titleLine:Show()
    end

    -- Lines directly under the colored title (before separator) â€” e.g. Gear "need more crests".
    frame.AddTitleAffix = function(self, text, r, g, b, wrap)
        local line = self:GetOrCreateLine()
        line:SetText(text or "")
        if r or g or b then
            line:SetTextColor(r, g, b, 1)
        else
            local lr, lg, lb = TextRoleRGB("Bright")
            line:SetTextColor(lr, lg, lb, 1)
        end
        local contentWidth = (self.fixedWidth or MAX_WIDTH) - (self.paddingH * 2)
        line:SetWidth(contentWidth)
        if wrap then
            line:SetWordWrap(true)
            line:SetNonSpaceWrap(true)
            line:SetMaxLines(0)
            line:SetHeight(0)
        else
            line:SetWordWrap(false)
            line:SetHeight(0)
        end
        line:Show()
        table.insert(self.titleAffixLines, line)
        RequestLayout(self)
    end

    -- Character row under title: name (left) + ilvl (right), header band only.
    frame.AddTitleAffixPair = function(self, leftText, rightText, lr, lg, lb, rr, rg, rb)
        if not self.titleAffixPairLeft then
            self.titleAffixPairLeft = FontManager:CreateFontString(self, "title", "OVERLAY")
            self.titleAffixPairRight = FontManager:CreateFontString(self, "body", "OVERLAY")
            self.titleAffixPairLeft:SetJustifyH("LEFT")
            self.titleAffixPairRight:SetJustifyH("RIGHT")
        end
        self.titleAffixPairLeft:SetText(leftText or "")
        if lr ~= nil or lg ~= nil or lb ~= nil then
            ApplyTooltipInk(self.titleAffixPairLeft, leftText, lr or 1, lg or 1, lb or 1)
        else
            local tlR, tlG, tlB = TextRoleRGB("Bright")
            ApplyTooltipInk(self.titleAffixPairLeft, leftText, tlR, tlG, tlB)
        end
        self.titleAffixPairRight:SetText(rightText or "")
        if rr ~= nil or rg ~= nil or rb ~= nil then
            ApplyTooltipInk(self.titleAffixPairRight, rightText, rr or 1, rg or 1, rb or 1)
        else
            local trR, trG, trB = SemanticGoldRGB()
            ApplyTooltipInk(self.titleAffixPairRight, rightText, trR, trG, trB)
        end
        self.titleAffixPairLeft:Show()
        self.titleAffixPairRight:Show()
        self.hasTitleAffixPair = true
        RequestLayout(self)
    end
    
    -- API: Set description (below title, smaller font, wrapping)
    frame.SetDescription = function(self, text, r, g, b)
        if not text or text == "" then return end
        local descLine = self:GetOrCreateDescLine()
        descLine:SetText(text)
        if r or g or b then
            descLine:SetTextColor(r, g, b, 1)
        else
            local dr, dg, db = TextRoleRGB("Normal")
            descLine:SetTextColor(dr, dg, db, 1)
        end
        descLine:SetWordWrap(true)
        descLine:SetNonSpaceWrap(true)
        descLine:SetMaxLines(0)
        descLine:Show()
    end
    
    -- API: Add single text line
    frame.AddLine = function(self, text, r, g, b, wrap)
        local line = self:GetOrCreateLine()
        if type(r) == "table" and g == nil and b == nil then
            ApplyTooltipInk(line, text, r[1] or 1, r[2] or 1, r[3] or 1)
        elseif type(r) == "number" and type(g) == "number" and type(b) == "number" then
            ApplyTooltipInk(line, text, r, g, b)
        elseif r ~= nil or g ~= nil or b ~= nil then
            ApplyTooltipInk(line, text, tonumber(r) or 1, tonumber(g) or 1, tonumber(b) or 1)
        else
            local lr, lg, lb = TextRoleRGB("Bright")
            ApplyTooltipInk(line, text, lr, lg, lb)
        end
        
        local contentWidth = (self.fixedWidth or MAX_WIDTH) - (self.paddingH * 2)
        line:SetWidth(contentWidth)
        
        if wrap then
            line:SetWordWrap(true)
            line:SetNonSpaceWrap(true)
            line:SetMaxLines(0)
            line:SetHeight(0)
        else
            line:SetWordWrap(false)
            line:SetHeight(0)
        end
        
        line:Show()
        table.insert(self.lines, line)
        table.insert(self.allLines, {type = "single", element = line})
        
        RequestLayout(self)
    end
    
    -- API: Add double-column line (left/right text)
    frame.AddDoubleLine = function(self, leftText, rightText, lr, lg, lb, rr, rg, rb, opts)
        opts = opts or {}
        local dLine = self:GetOrCreateDoubleLine()
        
        dLine.left:SetText(leftText)
        if lr or lg or lb then
            ApplyTooltipInk(dLine.left, leftText, lr, lg, lb)
        else
            local lR, lG, lB = TextRoleRGB("Bright")
            ApplyTooltipInk(dLine.left, leftText, lR, lG, lB)
        end
        
        dLine.right:SetText(rightText)
        if rr or rg or rb then
            ApplyTooltipInk(dLine.right, rightText, rr, rg, rb)
        else
            local rR, rG, rB = TextRoleRGB("Bright")
            ApplyTooltipInk(dLine.right, rightText, rR, rG, rB)
        end
        dLine.right:SetJustifyH("RIGHT")
        
        dLine.left:Show()
        dLine.right:Show()
        
        table.insert(self.doubleLines, dLine)
        table.insert(self.allLines, {
            type = "double",
            element = dLine,
            balanced = opts.balanced == true,
        })
        
        RequestLayout(self)
    end

    frame.AddCenteredLine = function(self, text, r, g, b)
        local line = self:GetOrCreateLine()
        line:SetText(text or "")
        if r or g or b then
            line:SetTextColor(r, g, b, 1)
        else
            local cr, cg, cb = TextRoleRGB("Bright")
            line:SetTextColor(cr, cg, cb, 1)
        end
        line:SetJustifyH("CENTER")
        line:Show()
        table.insert(self.lines, line)
        table.insert(self.allLines, { type = "centered", element = line })
        RequestLayout(self)
    end

    frame.AddSectionLabel = function(self, text, r, g, b)
        local line
        if #self.linePool > 0 then
            line = table.remove(self.linePool)
        else
            line = FontManager:CreateFontString(self, "small", "OVERLAY")
        end
        line:SetText(text or "")
        if r or g or b then
            line:SetTextColor(r, g, b, 1)
        else
            local mr, mg, mb = TextRoleRGB("Muted")
            line:SetTextColor(mr, mg, mb, 1)
        end
        line:SetJustifyH("CENTER")
        line:Show()
        table.insert(self.lines, line)
        table.insert(self.allLines, { type = "section_label", element = line })
        RequestLayout(self)
    end

    frame.GetOrCreateBodyDivider = function(self)
        if #self.bodyDividerPool > 0 then
            return table.remove(self.bodyDividerPool)
        end
        local div = self:CreateTexture(nil, "ARTWORK")
        div:SetHeight(1)
        return div
    end

    frame.AddBodyDivider = function(self)
        self:AddSpacer(6)
        local div = self:GetOrCreateBodyDivider()
        local ac = (COLORS and COLORS.accent) or { 0.45, 0.35, 0.72 }
        div:SetColorTexture(ac[1], ac[2], ac[3], 0.32)
        div:Show()
        table.insert(self.allLines, { type = "divider", element = div })
        self:AddSpacer(6)
    end

    -- API: Add spacer
    frame.AddSpacer = function(self, height)
        height = height or 6
        local spacerLine = self:GetOrCreateLine()
        spacerLine:SetText("")
        spacerLine:SetHeight(height)
        spacerLine:Show()
        table.insert(self.lines, spacerLine)
        table.insert(self.allLines, {type = "spacer", element = spacerLine, height = height})
        RequestLayout(self)
    end
    
    -- INTERNAL: Get or create title line (larger font)
    frame.GetOrCreateTitleLine = function(self)
        if not self.titleLine then
            self.titleLine = FontManager:CreateFontString(self, "title", "OVERLAY")
            self.titleLine:SetJustifyH("LEFT")
        end
        return self.titleLine
    end
    
    -- INTERNAL: Get or create description line
    frame.GetOrCreateDescLine = function(self)
        if not self.descLine then
            self.descLine = FontManager:CreateFontString(self, "body", "OVERLAY")
            self.descLine:SetJustifyH("LEFT")
        end
        return self.descLine
    end
    
    -- INTERNAL: Get or create single line from pool
    frame.GetOrCreateLine = function(self)
        if #self.linePool > 0 then
            return table.remove(self.linePool)
        end
        local line = FontManager:CreateFontString(self, "body", "OVERLAY")
        line:SetJustifyH("LEFT")
        return line
    end
    
    -- INTERNAL: Get or create double line from pool
    frame.GetOrCreateDoubleLine = function(self)
        if #self.doubleLinePool > 0 then
            return table.remove(self.doubleLinePool)
        end
        local dLine = {}
        dLine.left = FontManager:CreateFontString(self, "body", "OVERLAY")
        dLine.left:SetJustifyH("LEFT")
        dLine.right = FontManager:CreateFontString(self, "body", "OVERLAY")
        dLine.right:SetJustifyH("RIGHT")
        return dLine
    end

    -- INTERNAL: Vault summary row â€” fixed columns (name | realm | raid | m+ | world)
    frame.GetOrCreateVaultGridRow = function(self)
        if #self.vaultGridLinePool > 0 then
            return table.remove(self.vaultGridLinePool)
        end
        local row = {}
        row.nameFs = FontManager:CreateFontString(self, "body", "OVERLAY")
        row.nameFs:SetJustifyH("LEFT")
        row.nameFs:SetWordWrap(false)
        row.realmFs = FontManager:CreateFontString(self, "body", "OVERLAY")
        row.realmFs:SetJustifyH("LEFT")
        row.realmFs:SetWordWrap(false)
        row.raidFs = FontManager:CreateFontString(self, "body", "OVERLAY")
        row.raidFs:SetJustifyH("CENTER")
        row.raidFs:SetWordWrap(false)
        row.mplusFs = FontManager:CreateFontString(self, "body", "OVERLAY")
        row.mplusFs:SetJustifyH("CENTER")
        row.mplusFs:SetWordWrap(false)
        row.worldFs = FontManager:CreateFontString(self, "body", "OVERLAY")
        row.worldFs:SetJustifyH("CENTER")
        row.worldFs:SetWordWrap(false)
        return row
    end

    --[[
        Five-column row for Great Vault summary tooltip (aligned name/realm + vault tracks).
        widths = { nameW, realmW, raidW, mplusW, worldW } in pixels.
        opts.isHeader â€” gold column labels for vault headers.
    ]]
    frame.AddVaultGridRow = function(self, nameText, realmText, colRaid, colMplus, colWorld, widths, opts)
        opts = opts or {}
        local row = self:GetOrCreateVaultGridRow()
        row.nameFs:SetText(nameText or "")
        row.realmFs:SetText(realmText or "")
        row.raidFs:SetText(colRaid or "")
        row.mplusFs:SetText(colMplus or "")
        row.worldFs:SetText(colWorld or "")
        row.widthName = (widths and widths[1]) or 80
        row.widthRealm = (widths and widths[2]) or 80
        row.widthRaid = (widths and widths[3]) or 48
        row.widthMplus = (widths and widths[4]) or 48
        row.widthWorld = (widths and widths[5]) or 48
        row.isHeader = opts.isHeader == true
        if opts.isHeader then
            local hr, hg, hb = SemanticGoldRGB()
            row.nameFs:SetTextColor(hr, hg, hb, 1)
            row.realmFs:SetTextColor(hr, hg, hb, 1)
            row.raidFs:SetTextColor(hr, hg, hb, 1)
            row.mplusFs:SetTextColor(hr, hg, hb, 1)
            row.worldFs:SetTextColor(hr, hg, hb, 1)
        else
            ns.UI_SetTextColorRole(row.nameFs, "Bright")
            ns.UI_SetTextColorRole(row.realmFs, "Bright")
            ns.UI_SetTextColorRole(row.raidFs, "Bright")
            ns.UI_SetTextColorRole(row.mplusFs, "Bright")
            ns.UI_SetTextColorRole(row.worldFs, "Bright")
        end
        row.nameFs:Show()
        row.realmFs:Show()
        row.raidFs:SetJustifyH("CENTER")
        row.mplusFs:SetJustifyH("CENTER")
        row.worldFs:SetJustifyH("CENTER")
        row.raidFs:Show()
        row.mplusFs:Show()
        row.worldFs:Show()
        table.insert(self.vaultGridRows, row)
        table.insert(self.allLines, { type = "vault_grid", element = row })
        RequestLayout(self)
    end

    -- INTERNAL: Three-column vault track row (Raid / M+ / World only)
    frame.GetOrCreateVaultTrackRow = function(self)
        if #self.vaultTrackLinePool > 0 then
            return table.remove(self.vaultTrackLinePool)
        end
        local row = {}
        row.raidFs = FontManager:CreateFontString(self, "body", "OVERLAY")
        row.raidFs:SetJustifyH("CENTER")
        row.raidFs:SetWordWrap(false)
        row.mplusFs = FontManager:CreateFontString(self, "body", "OVERLAY")
        row.mplusFs:SetJustifyH("CENTER")
        row.mplusFs:SetWordWrap(false)
        row.worldFs = FontManager:CreateFontString(self, "body", "OVERLAY")
        row.worldFs:SetJustifyH("CENTER")
        row.worldFs:SetWordWrap(false)
        return row
    end

    frame.AddVaultTrackRow = function(self, colRaid, colMplus, colWorld, colW, opts)
        opts = opts or {}
        local row = self:GetOrCreateVaultTrackRow()
        row.raidFs:SetText(colRaid or "")
        row.mplusFs:SetText(colMplus or "")
        row.worldFs:SetText(colWorld or "")
        row.colW = tonumber(colW) or VAULT_GRID_TRACK_MIN_W
        row.isHeader = opts.isHeader == true
        if opts.isHeader then
            local hr, hg, hb = SemanticGoldRGB()
            row.raidFs:SetTextColor(hr, hg, hb, 1)
            row.mplusFs:SetTextColor(hr, hg, hb, 1)
            row.worldFs:SetTextColor(hr, hg, hb, 1)
        else
            ns.UI_SetTextColorRole(row.raidFs, "Bright")
            ns.UI_SetTextColorRole(row.mplusFs, "Bright")
            ns.UI_SetTextColorRole(row.worldFs, "Bright")
        end
        row.raidFs:Show()
        row.mplusFs:Show()
        row.worldFs:Show()
        table.insert(self.vaultTrackRows, row)
        table.insert(self.allLines, { type = "vault_track", element = row })
        RequestLayout(self)
    end
    
    -- INTERNAL: Layout - header (icon + title + desc) then body lines
    frame.LayoutLines = function(self)
        local spacing = GetTooltipUISpacing()
        local padding = self.paddingH or GetTooltipPaddingH()
        local paddingV = self.paddingV or GetTooltipPaddingV()
        local lineSpacing = 3
        
        -- â”€â”€ Phase 1: Measure natural content width â”€â”€
        local maxContentW = 0
        
        -- Measure title
        if self.titleLine and self.titleLine:IsShown() then
            self.titleLine:SetWidth(0)  -- Unconstrain to measure natural width
            local tw = self.titleLine:GetStringWidth() or 0
            local headerW = tw + (self.hasIcon and (ICON_SIZE + ICON_PADDING) or 0)
            if headerW > maxContentW then maxContentW = headerW end
        end
        
        -- Measure description
        if self.descLine and self.descLine:IsShown() then
            self.descLine:SetWidth(0)
            local dw = self.descLine:GetStringWidth() or 0
            local descHeaderW = dw + (self.hasIcon and (ICON_SIZE + ICON_PADDING) or 0)
            if descHeaderW > maxContentW then maxContentW = descHeaderW end
            if dw > maxContentW then maxContentW = dw end
        end

        -- Measure title affixes (lines under the item name, right of icon)
        if self.titleAffixLines then
            for i = 1, #self.titleAffixLines do
                local aLine = self.titleAffixLines[i]
                aLine:SetWidth(0)
                local w = aLine:GetStringWidth() or 0
                local withIcon = w + (self.hasIcon and (ICON_SIZE + ICON_PADDING) or 0)
                if withIcon > maxContentW then maxContentW = withIcon end
            end
        end

        if self.hasTitleAffixPair and self.titleAffixPairLeft and self.titleAffixPairRight then
            self.titleAffixPairLeft:SetWidth(0)
            self.titleAffixPairRight:SetWidth(0)
            local pairW = (self.titleAffixPairLeft:GetStringWidth() or 0) + (self.titleAffixPairRight:GetStringWidth() or 0) + 28
            local withIcon = pairW + (self.hasIcon and (ICON_SIZE + ICON_PADDING) or 0)
            if withIcon > maxContentW then maxContentW = withIcon end
        end
        
        -- Measure body lines
        local maxBalancedLeft = 0
        for i = 1, #self.allLines do
            local lineData = self.allLines[i]
            if lineData.type == "single" then
                lineData.element:SetWidth(0)
                local lw = lineData.element:GetStringWidth() or 0
                if lw > maxContentW then maxContentW = lw end
            elseif lineData.type == "centered" or lineData.type == "section_label" then
                lineData.element:SetWidth(0)
                local lw = lineData.element:GetStringWidth() or 0
                if lw > maxContentW then maxContentW = lw end
            elseif lineData.type == "double" then
                lineData.element.left:SetWidth(0)
                lineData.element.right:SetWidth(0)
                local lw = (lineData.element.left:GetStringWidth() or 0) + (lineData.element.right:GetStringWidth() or 0) + 20
                if lw > maxContentW then maxContentW = lw end
                if lineData.balanced then
                    maxBalancedLeft = math.max(maxBalancedLeft, lineData.element.left:GetStringWidth() or 0)
                end
            elseif lineData.type == "vault_grid" then
                local v = lineData.element
                local lw = v.widthName + VAULT_GRID_GAP_NAME_REALM + v.widthRealm + VAULT_GRID_GAP_BEFORE_VAULT
                    + v.widthRaid + VAULT_GRID_GAP_VAULT_COL + v.widthMplus + VAULT_GRID_GAP_VAULT_COL + v.widthWorld
                if lw > maxContentW then maxContentW = lw end
            elseif lineData.type == "vault_track" then
                local t = lineData.element
                local lw = t.colW * 3 + VAULT_TRACK_GAP_COL * 2
                if lw > maxContentW then maxContentW = lw end
            end
        end
        
        -- Clamp width: respect TooltipService maxWidth preset (Vault summary), cap at WIDTH_CAP_TOP.
        local requestedCap = tonumber(self.fixedWidth) or MAX_WIDTH
        local widthCap = math.min(WIDTH_CAP_TOP, math.max(MAX_WIDTH, requestedCap))
        local computedWidth = math.max(MIN_WIDTH, math.min(widthCap, maxContentW + padding * 2))
        if self._wnStableWidth and self.fixedWidth then
            computedWidth = self.fixedWidth
        end
        self.fixedWidth = computedWidth
        self:SetWidth(computedWidth)
        self._balancedSplit = (maxBalancedLeft > 0) and (padding + maxBalancedLeft + 14) or nil
        
        -- Re-constrain wrapping lines to the new width
        local contentWidth = computedWidth - padding * 2
        for i = 1, #self.allLines do
            local lineData = self.allLines[i]
            if lineData.type == "single" or lineData.type == "centered" or lineData.type == "section_label" then
                lineData.element:SetWidth(contentWidth)
            end
        end
        if self.titleLine and self.titleLine:IsShown() then
            local textLeftX = self.hasIcon and (padding + ICON_SIZE + ICON_PADDING) or padding
            self.titleLine:SetWidth(computedWidth - textLeftX - padding)
        end
        if self.descLine and self.descLine:IsShown() then
            local textLeftX = self.hasIcon and (padding + ICON_SIZE + ICON_PADDING) or padding
            self.descLine:SetWidth(computedWidth - textLeftX - padding)
        end
        if self.titleAffixLines and #self.titleAffixLines > 0 then
            local tlx = self.hasIcon and (padding + ICON_SIZE + ICON_PADDING) or padding
            for i = 1, #self.titleAffixLines do
                local aLine = self.titleAffixLines[i]
                aLine:SetWidth(computedWidth - tlx - padding)
            end
        end
        
        -- â”€â”€ Phase 2: Position elements â”€â”€
        local yOffset = -paddingV
        
        local textLeftX = padding
        local headerBottom = yOffset
        
        if self.hasIcon then
            -- Position icon top-left
            self.iconFrame:ClearAllPoints()
            self.iconFrame:SetPoint("TOPLEFT", self, "TOPLEFT", padding, yOffset)
            textLeftX = padding + ICON_SIZE + ICON_PADDING
        end
        
        -- Title (right of icon if present)
        local titleBottom = yOffset
        if self.titleLine and self.titleLine:IsShown() then
            self.titleLine:ClearAllPoints()
            self.titleLine:SetPoint("TOPLEFT", self, "TOPLEFT", textLeftX, yOffset)
            self.titleLine:SetWidth((self.fixedWidth or 350) - textLeftX - padding)
            titleBottom = yOffset - self.titleLine:GetHeight()
        end

        -- Title affix pair (character + ilvl)
        local afterHeaderText = titleBottom
        if self.hasTitleAffixPair and self.titleAffixPairLeft and self.titleAffixPairRight then
            local yTop = titleBottom - 2
            self.titleAffixPairLeft:ClearAllPoints()
            self.titleAffixPairRight:ClearAllPoints()
            self.titleAffixPairLeft:SetPoint("TOPLEFT", self, "TOPLEFT", textLeftX, yTop)
            self.titleAffixPairRight:SetPoint("TOPRIGHT", self, "TOPRIGHT", -padding, yTop)
            local pairH = math.max(self.titleAffixPairLeft:GetStringHeight(), self.titleAffixPairRight:GetStringHeight())
            afterHeaderText = yTop - pairH
        end

        -- Title affixes: directly under the colored name (before separator, same indent as title)
        if self.titleAffixLines and #self.titleAffixLines > 0 then
            local yTop = titleBottom
            for i = 1, #self.titleAffixLines do
                local aLine = self.titleAffixLines[i]
                yTop = yTop - 2
                aLine:ClearAllPoints()
                aLine:SetPoint("TOPLEFT", self, "TOPLEFT", textLeftX, yTop)
                aLine:SetWidth((self.fixedWidth or 350) - textLeftX - padding)
                aLine:SetHeight(0)
                yTop = yTop - aLine:GetStringHeight()
            end
            afterHeaderText = yTop
        end
        
        -- Description (below title/affix, still indented if icon)
        local descBottom = afterHeaderText
        if self.descLine and self.descLine:IsShown() then
            self.descLine:ClearAllPoints()
            self.descLine:SetPoint("TOPLEFT", self, "TOPLEFT", textLeftX, afterHeaderText - 2)
            self.descLine:SetWidth((self.fixedWidth or 350) - textLeftX - padding)
            self.descLine:SetHeight(0)
            descBottom = afterHeaderText - 2 - self.descLine:GetStringHeight()
        else
            descBottom = afterHeaderText
        end
        
        -- Header bottom = lowest of icon bottom vs text bottom
        local iconBottom = yOffset
        if self.hasIcon then
            iconBottom = yOffset - ICON_SIZE
        end
        headerBottom = math.min(iconBottom, descBottom)
        
        local bodyStartY = headerBottom
        local showSeparator = false
        if self.titleLine and self.titleLine:IsShown() and #self.allLines > 0 then
            -- Only show separator if there's both header and body content
            for i = 1, #self.allLines do
                local lineData = self.allLines[i]
                if lineData.type ~= "spacer" and lineData.type ~= "divider" then
                    showSeparator = true
                    break
                end
            end
        end
        
        if showSeparator then
            bodyStartY = headerBottom - 4
            self.separator:ClearAllPoints()
            self.separator:SetPoint("TOPLEFT", self, "TOPLEFT", padding, bodyStartY)
            self.separator:SetPoint("TOPRIGHT", self, "TOPRIGHT", -padding, bodyStartY)
            self.separator:Show()
            bodyStartY = bodyStartY - 1 - 6  -- separator height + gap to first body line
        else
            self.separator:Hide()
        end
        
        yOffset = bodyStartY
        local prevElement = nil
        
        for i = 1, #self.allLines do
            local lineData = self.allLines[i]
            if lineData.type == "single" or lineData.type == "spacer" then
                local line = lineData.element
                line:ClearAllPoints()
                
                if prevElement then
                    line:SetPoint("TOPLEFT", prevElement, "BOTTOMLEFT", 0, -lineSpacing)
                else
                    line:SetPoint("TOPLEFT", self, "TOPLEFT", padding, yOffset)
                end
                
                yOffset = yOffset - line:GetHeight() - lineSpacing
                prevElement = line

            elseif lineData.type == "centered" or lineData.type == "section_label" then
                local line = lineData.element
                line:ClearAllPoints()
                line:SetJustifyH("CENTER")
                line:SetWidth(contentWidth)
                if prevElement then
                    line:SetPoint("TOPLEFT", self, "TOPLEFT", padding, yOffset - lineSpacing)
                else
                    line:SetPoint("TOPLEFT", self, "TOPLEFT", padding, yOffset)
                end
                local lineHeight = line:GetStringHeight()
                yOffset = yOffset - lineHeight - lineSpacing
                prevElement = line

            elseif lineData.type == "divider" then
                local div = lineData.element
                div:ClearAllPoints()
                local divY = yOffset - 4
                div:SetPoint("TOPLEFT", self, "TOPLEFT", padding, divY)
                div:SetPoint("TOPRIGHT", self, "TOPRIGHT", -padding, divY)
                yOffset = divY - 1 - 6
                prevElement = div

            elseif lineData.type == "double" then
                local dLine = lineData.element
                dLine.left:ClearAllPoints()
                dLine.right:ClearAllPoints()
                
                if prevElement then
                    dLine.left:SetPoint("TOPLEFT", prevElement, "BOTTOMLEFT", 0, -lineSpacing)
                else
                    dLine.left:SetPoint("TOPLEFT", self, "TOPLEFT", padding, yOffset)
                end
                
                if lineData.balanced and self._balancedSplit then
                    dLine.right:SetJustifyH("LEFT")
                    dLine.right:SetPoint("TOPLEFT", self, "TOPLEFT", self._balancedSplit, yOffset)
                else
                    dLine.right:SetJustifyH("RIGHT")
                    dLine.right:SetPoint("TOPRIGHT", self, "TOPRIGHT", -padding, yOffset)
                end
                
                local lineHeight = math.max(dLine.left:GetHeight(), dLine.right:GetHeight())
                yOffset = yOffset - lineHeight - lineSpacing
                prevElement = dLine.left
            elseif lineData.type == "vault_grid" then
                local v = lineData.element
                v.nameFs:ClearAllPoints()
                v.realmFs:ClearAllPoints()
                v.raidFs:ClearAllPoints()
                v.mplusFs:ClearAllPoints()
                v.worldFs:ClearAllPoints()

                if prevElement then
                    v.nameFs:SetPoint("TOPLEFT", prevElement, "BOTTOMLEFT", 0, -lineSpacing)
                else
                    v.nameFs:SetPoint("TOPLEFT", self, "TOPLEFT", padding, yOffset)
                end

                v.nameFs:SetWidth(v.widthName)
                v.realmFs:SetWidth(v.widthRealm)
                v.raidFs:SetWidth(v.widthRaid)
                v.mplusFs:SetWidth(v.widthMplus)
                v.worldFs:SetWidth(v.widthWorld)

                v.realmFs:SetPoint("TOPLEFT", v.nameFs, "TOPRIGHT", VAULT_GRID_GAP_NAME_REALM, 0)
                v.raidFs:SetPoint("TOPLEFT", v.realmFs, "TOPRIGHT", VAULT_GRID_GAP_BEFORE_VAULT, 0)
                v.mplusFs:SetPoint("TOPLEFT", v.raidFs, "TOPRIGHT", VAULT_GRID_GAP_VAULT_COL, 0)
                v.worldFs:SetPoint("TOPLEFT", v.mplusFs, "TOPRIGHT", VAULT_GRID_GAP_VAULT_COL, 0)

                local lineHeight = math.max(
                    v.nameFs:GetStringHeight(),
                    v.realmFs:GetStringHeight(),
                    v.raidFs:GetStringHeight(),
                    v.mplusFs:GetStringHeight(),
                    v.worldFs:GetStringHeight()
                )
                yOffset = yOffset - lineHeight - lineSpacing
                prevElement = v.nameFs
            elseif lineData.type == "vault_track" then
                local t = lineData.element
                local contentWidth = (self.fixedWidth or 350) - padding * 2
                local trackW = t.colW * 3 + VAULT_TRACK_GAP_COL * 2
                local trackLeft = padding + math.max(0, math.floor((contentWidth - trackW) * 0.5))

                t.raidFs:ClearAllPoints()
                t.mplusFs:ClearAllPoints()
                t.worldFs:ClearAllPoints()

                if prevElement then
                    t.raidFs:SetPoint("TOPLEFT", prevElement, "BOTTOMLEFT", 0, -lineSpacing)
                else
                    t.raidFs:SetPoint("TOPLEFT", self, "TOPLEFT", trackLeft, yOffset)
                end

                t.raidFs:SetWidth(t.colW)
                t.mplusFs:SetWidth(t.colW)
                t.worldFs:SetWidth(t.colW)
                t.mplusFs:SetPoint("TOPLEFT", t.raidFs, "TOPRIGHT", VAULT_TRACK_GAP_COL, 0)
                t.worldFs:SetPoint("TOPLEFT", t.mplusFs, "TOPRIGHT", VAULT_TRACK_GAP_COL, 0)

                local lineHeight = math.max(
                    t.raidFs:GetStringHeight(),
                    t.mplusFs:GetStringHeight(),
                    t.worldFs:GetStringHeight()
                )
                yOffset = yOffset - lineHeight - lineSpacing
                prevElement = t.raidFs
            end
        end
        
        -- Update frame size
        self.currentHeight = math.abs(yOffset) + paddingV
        self:SetWidth(self.fixedWidth or 350)
        self:SetHeight(self.currentHeight)
    end
    
    -- API: Update theme colors
    local function ReapplyTooltipTypography(tipFrame)
        if not tipFrame or not FontManager then return end
        local function applyFs(fs)
            if not fs or not fs.SetFont then return end
            if fs._fontOverlay then
                FontManager:ApplyBarOverlayFont(fs)
            elseif fs._wnNavLabel and ns.UI_SetNavLabelFontStyle then
                local parent = fs.GetParent and fs:GetParent()
                ns.UI_SetNavLabelFontStyle(fs, parent and parent.active == true)
            else
                local cat = fs._fontCategory or "body"
                FontManager:ApplyFont(fs, cat)
                if FontManager.ApplyReadableEdge then
                    FontManager:ApplyReadableEdge(fs, cat)
                end
            end
        end
        local function applyVaultRow(row)
            if not row then return end
            applyFs(row.nameFs)
            applyFs(row.realmFs)
            applyFs(row.raidFs)
            applyFs(row.mplusFs)
            applyFs(row.worldFs)
        end
        local function applyTrackRow(row)
            if not row then return end
            applyFs(row.raidFs)
            applyFs(row.mplusFs)
            applyFs(row.worldFs)
        end
        applyFs(tipFrame.titleLine)
        applyFs(tipFrame.descLine)
        if tipFrame.titleAffixPairLeft then applyFs(tipFrame.titleAffixPairLeft) end
        if tipFrame.titleAffixPairRight then applyFs(tipFrame.titleAffixPairRight) end
        if tipFrame.titleAffixLines then
            for i = 1, #tipFrame.titleAffixLines do
                applyFs(tipFrame.titleAffixLines[i])
            end
        end
        if tipFrame.lines then
            for i = 1, #tipFrame.lines do
                applyFs(tipFrame.lines[i])
            end
        end
        if tipFrame.linePool then
            for i = 1, #tipFrame.linePool do
                applyFs(tipFrame.linePool[i])
            end
        end
        if tipFrame.doubleLines then
            for i = 1, #tipFrame.doubleLines do
                local d = tipFrame.doubleLines[i]
                if d then applyFs(d.left) applyFs(d.right) end
            end
        end
        if tipFrame.doubleLinePool then
            for i = 1, #tipFrame.doubleLinePool do
                local d = tipFrame.doubleLinePool[i]
                if d then applyFs(d.left) applyFs(d.right) end
            end
        end
        if tipFrame.vaultGridRows then
            for i = 1, #tipFrame.vaultGridRows do
                applyVaultRow(tipFrame.vaultGridRows[i])
            end
        end
        if tipFrame.vaultGridLinePool then
            for i = 1, #tipFrame.vaultGridLinePool do
                applyVaultRow(tipFrame.vaultGridLinePool[i])
            end
        end
        if tipFrame.vaultTrackRows then
            for i = 1, #tipFrame.vaultTrackRows do
                applyTrackRow(tipFrame.vaultTrackRows[i])
            end
        end
        if tipFrame.vaultTrackLinePool then
            for i = 1, #tipFrame.vaultTrackLinePool do
                applyTrackRow(tipFrame.vaultTrackLinePool[i])
            end
        end
    end

    frame.UpdateTheme = function(self)
        COLORS = BootstrapTooltipColors()

        if ns.UI_ApplyStandardCardElevatedChrome then
            ns.UI_ApplyStandardCardElevatedChrome(self)
        else
            local bg, border = { COLORS.bgCard[1], COLORS.bgCard[2], COLORS.bgCard[3], COLORS.bgCard[4] or 0.98 },
                { COLORS.border[1], COLORS.border[2], COLORS.border[3], 1 }
            if ns.UI_GetTooltipShellBackdrop then
                bg, border = ns.UI_GetTooltipShellBackdrop()
            end
            self:SetBackdrop({
                bgFile = "Interface\\Buttons\\WHITE8x8",
                edgeFile = "Interface\\Buttons\\WHITE8x8",
                edgeSize = 1,
                insets = { left = 0, right = 0, top = 0, bottom = 0 }
            })
            self:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 0.98)
            self:SetBackdropBorderColor(border[1], border[2], border[3], border[4] or 1)
        end
        TintTooltipAccentLines(self)
        ReapplyTooltipTypography(self)
        if self:IsShown() and self.LayoutLines then
            self:LayoutLines()
        end
        if ns.TooltipService and ns.TooltipService.RepositionIfVisible then
            ns.TooltipService:RepositionIfVisible(true)
        end
    end
    
    return frame
end
