--------------------------------------------------------------------------------
-- Nanami-UI: Character Panel (CharacterPanel.lua)
-- Replaces the default CharacterFrame with a modern rounded UI
--------------------------------------------------------------------------------

SFrames.CharacterPanel = {}

local CP = SFrames.CharacterPanel
local panel, tabs, pages

--------------------------------------------------------------------------------
-- Theme
--------------------------------------------------------------------------------
local T = SFrames.Theme:Extend({
    repColors  = {
        [1] = { 0.8, 0.13, 0.13 }, [2] = { 0.8, 0.3, 0.22 },
        [3] = { 0.75, 0.55, 0.15 }, [4] = { 0.9, 0.7, 0.0 },
        [5] = { 0.0, 0.6, 0.1 }, [6] = { 0.0, 0.7, 0.3 },
        [7] = { 0.0, 0.6, 0.75 }, [8] = { 0.0, 0.5, 0.9 },
    },
    resistColors = {
        [2] = { 1, 0.5, 0.15 }, [3] = { 0.35, 0.9, 0.25 },
        [4] = { 0.45, 0.7, 1 }, [5] = { 0.6, 0.35, 0.9 },
        [6] = { 0.95, 0.9, 0.4 },
    },
})

local FRAME_W = 340
local FRAME_H = 490
local HEADER_H = 28
local TAB_H = 22
local TAB_BAR_H = 26
local CONTENT_TOP = -(HEADER_H + TAB_BAR_H)
local SIDE_PAD = 8
local INNER_PAD = 4
local CONTENT_W = FRAME_W - INNER_PAD * 2
local SCROLL_W = CONTENT_W - 10

local SLOT_SIZE = 32
local SLOT_GAP = 2

local QUALITY_COLORS = {
    [0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 },
    [2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 },
    [4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 },
}

local SLOT_LABEL = {
    HeadSlot = "头部", NeckSlot = "颈部", ShoulderSlot = "肩部",
    BackSlot = "背部", ChestSlot = "胸部", ShirtSlot = "衬衣",
    WristSlot = "手腕", HandsSlot = "手套", WaistSlot = "腰带",
    LegsSlot = "腿部", FeetSlot = "脚部",
    Finger0Slot = "戒指1", Finger1Slot = "戒指2",
    Trinket0Slot = "饰品1", Trinket1Slot = "饰品2",
    MainHandSlot = "主手", SecondaryHandSlot = "副手",
    RangedSlot = "远程", AmmoSlot = "弹药", TabardSlot = "战袍",
}

local EQUIP_SLOTS_LEFT = {
    { id = 1, name = "HeadSlot" }, { id = 2, name = "NeckSlot" },
    { id = 3, name = "ShoulderSlot" }, { id = 15, name = "BackSlot" },
    { id = 5, name = "ChestSlot" }, { id = 4, name = "ShirtSlot" },
    { id = 19, name = "TabardSlot" }, { id = 9, name = "WristSlot" },
}
local EQUIP_SLOTS_RIGHT = {
    { id = 10, name = "HandsSlot" }, { id = 6, name = "WaistSlot" },
    { id = 7, name = "LegsSlot" }, { id = 8, name = "FeetSlot" },
    { id = 11, name = "Finger0Slot" }, { id = 12, name = "Finger1Slot" },
    { id = 13, name = "Trinket0Slot" }, { id = 14, name = "Trinket1Slot" },
}
local EQUIP_SLOTS_BOTTOM = {
    { id = 16, name = "MainHandSlot" }, { id = 17, name = "SecondaryHandSlot" },
    { id = 18, name = "RangedSlot" }, { id = 0, name = "AmmoSlot" },
}

local TAB_NAMES = { "装备", "声望", "技能", "荣誉" }
local STAT_NAMES = { "力量", "敏捷", "耐力", "智力", "精神" }
local RESIST_NAMES = { [2] = "火焰", [3] = "自然", [4] = "冰霜", [5] = "暗影", [6] = "奥术" }
local REP_STANDING = {
    [1] = "仇恨", [2] = "敌对", [3] = "不友好", [4] = "中立",
    [5] = "友善", [6] = "尊敬", [7] = "崇敬", [8] = "崇拜",
}

local PET_TAB_INDEX = nil

local PET_FOOD_MAP = {
    ["Meat"] = "肉类", ["Fish"] = "鱼类", ["Cheese"] = "奶酪",
    ["Bread"] = "面包", ["Fungus"] = "蘑菇", ["Fruit"] = "水果",
    ["Raw Meat"] = "生肉", ["Raw Fish"] = "生鱼",
    ["Cooked Meat"] = "熟肉", ["Cooked Fish"] = "熟鱼",
}

local PET_HAPPINESS = {
    [1] = { text = "不高兴", color = { 0.9, 0.2, 0.2 } },
    [2] = { text = "满足", color = { 0.9, 0.75, 0.2 } },
    [3] = { text = "高兴", color = { 0.2, 0.9, 0.2 } },
}

local BEAST_TRAINING_NAMES = {
    ["Beast Training"] = true, ["训练野兽"] = true,
}

--------------------------------------------------------------------------------
-- EP Stat Weights per class (Turtle WoW)
-- Physical DPS: 1 AP = 1 EP; Caster DPS: 1 SP = 1 EP
-- Source: Turtle WoW deep theorycraft & EP evaluation report
--------------------------------------------------------------------------------
local STAT_WEIGHTS = {
    WARRIOR = {
        STR = 2.0, AGI = 1.5, STA = 1.0, INT = 0, SPI = 0,
        ATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18,
        DEFENSE = 1.5, DODGE = 12, PARRY = 12, BLOCK = 8, BLOCKVALUE = 0.5,
        ARMOR = 0.02, HEALTHREG = 2, HEALTH = 0.1,
    },
    ROGUE = {
        STR = 1.0, AGI = 2.0, STA = 0.5, INT = 0, SPI = 0,
        ATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18,
        DODGE = 10, HEALTH = 0.067,
    },
    HUNTER = {
        STR = 0.5, AGI = 2.0, STA = 0.5, INT = 0.3, SPI = 0.1,
        ATTACKPOWER = 1.0, RANGEDATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18,
        RANGEDCRIT = 25, HEALTH = 0.067, MANA = 0.033,
    },
    DRUID = {
        STR = 2.4, AGI = 1.5, STA = 1.0, INT = 0.5, SPI = 0.5,
        ATTACKPOWER = 1.0, ATTACKPOWERFERAL = 1.0,
        CRIT = 25, TOHIT = 18,
        DMG = 1.0, HEAL = 0.8, SPELLCRIT = 8, SPELLTOHIT = 14,
        DEFENSE = 1.2, DODGE = 10, ARMOR = 0.02,
        MANAREG = 3, HEALTHREG = 2, HEALTH = 0.1, MANA = 0.033,
    },
    PALADIN = {
        STR = 2.0, AGI = 1.0, STA = 1.0, INT = 0.5, SPI = 0.3,
        ATTACKPOWER = 1.0, CRIT = 25, TOHIT = 18,
        DMG = 1.0, HEAL = 0.8, HOLYDMG = 1.0,
        SPELLCRIT = 8, SPELLTOHIT = 14,
        DEFENSE = 1.5, DODGE = 12, PARRY = 12, BLOCK = 8, BLOCKVALUE = 0.5,
        ARMOR = 0.02, MANAREG = 3, HEALTH = 0.1, MANA = 0.033,
    },
    SHAMAN = {
        STR = 1.0, AGI = 1.0, STA = 0.8, INT = 0.5, SPI = 0.3,
        ATTACKPOWER = 1.0, CRIT = 20, TOHIT = 15,
        DMG = 1.0, HEAL = 0.8, NATUREDMG = 1.0,
        SPELLCRIT = 8, SPELLTOHIT = 14,
        MANAREG = 3, HEALTH = 0.1, MANA = 0.033,
    },
    MAGE = {
        STR = 0, AGI = 0, STA = 0.3, INT = 0.125, SPI = 0.5,
        DMG = 1.0, ARCANEDMG = 1.0, FIREDMG = 1.0, FROSTDMG = 1.0,
        SPELLCRIT = 8, SPELLTOHIT = 14,
        MANAREG = 3, HEALTH = 0.067, MANA = 0.033,
    },
    WARLOCK = {
        STR = 0, AGI = 0, STA = 0.5, INT = 0.125, SPI = 0.3,
        DMG = 1.0, SHADOWDMG = 1.0, FIREDMG = 1.0,
        SPELLCRIT = 8, SPELLTOHIT = 14,
        MANAREG = 3, HEALTH = 0.1, MANA = 0.033,
    },
    PRIEST = {
        STR = 0, AGI = 0, STA = 0.3, INT = 0.5, SPI = 1.0,
        DMG = 0.8, HEAL = 1.0, SHADOWDMG = 0.8, HOLYDMG = 0.8,
        SPELLCRIT = 8, SPELLTOHIT = 14,
        MANAREG = 3, HEALTH = 0.067, MANA = 0.033,
    },
}

local widgetId = 0

--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetFont()
    if SFrames and SFrames.GetFont then return SFrames:GetFont() end
    return "Fonts\\ARIALN.TTF"
end

local function SetRoundBackdrop(frame, bgColor, borderColor)
    frame:SetBackdrop({
        bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 }
    })
    local bg = bgColor or T.bg
    local bd = borderColor or T.border
    frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1)
    frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1)
end

local function SetPixelBackdrop(frame, bgColor, borderColor)
    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false, tileSize = 0, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 }
    })
    if bgColor then frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1) end
    if borderColor then frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) end
end

local function CreateShadow(parent, size)
    local s = CreateFrame("Frame", nil, parent)
    local sz = size or 4
    s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz)
    s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz)
    s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0))
    s:SetBackdrop({
        bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    s:SetBackdropColor(0, 0, 0, 0.6)
    s:SetBackdropBorderColor(0, 0, 0, 0.45)
    return s
end

local function MakeSep(parent, x1, y1, x2, y2)
    local sep = parent:CreateTexture(nil, "ARTWORK")
    sep:SetTexture("Interface\\Buttons\\WHITE8X8")
    sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
    sep:SetHeight(1)
    sep:SetPoint("TOPLEFT", parent, "TOPLEFT", x1, y1)
    sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", x2, y1)
    return sep
end

local function MakeFS(parent, size, justifyH, color)
    local fs = parent:CreateFontString(nil, "OVERLAY")
    fs:SetFont(GetFont(), size or 11, "OUTLINE")
    fs:SetJustifyH(justifyH or "LEFT")
    local c = color or T.valueText
    fs:SetTextColor(c[1], c[2], c[3])
    return fs
end

local function NextName(p)
    widgetId = widgetId + 1
    return "SFramesCP" .. (p or "") .. tostring(widgetId)
end

local function Clamp(v, lo, hi)
    if v < lo then return lo end
    if v > hi then return hi end
    return v
end

--------------------------------------------------------------------------------
-- Combat stat helpers: multi-strategy retrieval for Turtle WoW compatibility
--   1) Standard 1.12 API  (GetCritChance, etc.)
--   2) GetCombatRatingBonus  (TBC-style, Turtle WoW may support)
--   3) Manual calc: agility/intellect base + ItemBonusLib gear bonus
--------------------------------------------------------------------------------
local function TryAPIs(names)
    for _, name in ipairs(names) do
        local fn = _G[name]
        if fn then
            local ok, val = pcall(fn)
            if ok and type(val) == "number" and val > 0 then return val end
        end
    end
    return 0
end
local function TryAPIsArg(names, arg1)
    for _, name in ipairs(names) do
        local fn = _G[name]
        if fn then
            local ok, val = pcall(fn, arg1)
            if ok and type(val) == "number" and val > 0 then return val end
        end
    end
    return 0
end
local function TryCombatRating(crId)
    if GetCombatRatingBonus then
        local ok, val = pcall(GetCombatRatingBonus, crId)
        if ok and type(val) == "number" and val > 0 then return val end
    end
    return 0
end

-- Try to get ItemBonusLib from AceLibrary (provides gear-only crit/hit/etc.)
local function GetItemBonusLib()
    if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("ItemBonusLib-1.0") then
        return AceLibrary("ItemBonusLib-1.0")
    end
    return nil
end
local function GetGearBonus(key)
    local lib = GetItemBonusLib()
    if lib and lib.GetBonus then return lib:GetBonus(key) or 0 end
    return 0
end

local function CalcItemEP(link, class)
    local weights = STAT_WEIGHTS[class]
    if not weights or not link then return 0 end
    local lib = GetItemBonusLib()
    if not lib or not lib.ScanItemLink then return 0 end
    local ok, info = pcall(function() return lib:ScanItemLink(link) end)
    if not ok or not info or not info.bonuses then return 0 end
    local ep = 0
    for stat, value in pairs(info.bonuses) do
        local w = weights[stat]
        if w and type(value) == "number" then
            ep = ep + value * w
        end
    end
    return ep
end

-- Turtle WoW agility → melee crit (agi per 1% crit at level 60)
local AGI_PER_MELEE_CRIT = {
    WARRIOR = 20, PALADIN = 20, ROGUE = 29, HUNTER = 52.91,
    DRUID = 20, SHAMAN = 20, MAGE = 0, WARLOCK = 0, PRIEST = 0,
}
local AGI_PER_RANGED_CRIT = {
    HUNTER = 52.91, WARRIOR = 20, ROGUE = 29,
}
-- Turtle WoW agility → dodge (agi per 1% dodge at level 60)
local AGI_PER_DODGE = {
    ROGUE = 14.5, WARRIOR = 20, PALADIN = 20,
    SHAMAN = 20, DRUID = 20, HUNTER = 0, MAGE = 0, WARLOCK = 0, PRIEST = 0,
}
-- Turtle WoW intellect → spell crit (int per 1% crit at level 60)
local INT_PER_SPELL_CRIT = {
    MAGE = 59.5, WARLOCK = 60.6, PRIEST = 59.5,
    DRUID = 60, SHAMAN = 59.2, PALADIN = 29.5,
}
-- Turtle WoW base melee crit at level 60 (before agility)
local BASE_MELEE_CRIT = {
    WARRIOR = 0, PALADIN = 0.7, ROGUE = 0, HUNTER = 0,
    DRUID = 0.9, SHAMAN = 1.7, MAGE = 0, WARLOCK = 0, PRIEST = 0,
}
local BASE_SPELL_CRIT = {
    MAGE = 0.2, WARLOCK = 1.7, PRIEST = 0.8,
    DRUID = 1.8, SHAMAN = 2.3, PALADIN = 0,
}

local function CalcMeleeCrit()
    local _, class = UnitClass("player")
    class = class or ""
    local baseCrit = BASE_MELEE_CRIT[class] or 0
    local ratio = AGI_PER_MELEE_CRIT[class] or 0
    local agiCrit = 0
    if ratio > 0 then
        local _, agi = UnitStat("player", 2)
        agiCrit = (agi or 0) / ratio
    end
    local gearCrit = GetGearBonus("CRIT")
    return baseCrit + agiCrit + gearCrit
end
local function CalcRangedCrit()
    local _, class = UnitClass("player")
    class = class or ""
    local baseCrit = BASE_MELEE_CRIT[class] or 0
    local ratio = AGI_PER_RANGED_CRIT[class] or 0
    local agiCrit = 0
    if ratio > 0 then
        local _, agi = UnitStat("player", 2)
        agiCrit = (agi or 0) / ratio
    end
    local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT")
    return baseCrit + agiCrit + gearCrit
end
local function CalcSpellCrit()
    local _, class = UnitClass("player")
    class = class or ""
    local baseCrit = BASE_SPELL_CRIT[class] or 0
    local ratio = INT_PER_SPELL_CRIT[class] or 0
    local intCrit = 0
    if ratio > 0 then
        local _, intel = UnitStat("player", 4)
        intCrit = (intel or 0) / ratio
    end
    local gearCrit = GetGearBonus("SPELLCRIT")
    return baseCrit + intCrit + gearCrit
end

local function SafeGetMeleeCrit()
    local v = TryAPIs({ "GetCritChance", "GetMeleeCritChance", "GetPlayerCritChance" })
    if v > 0 then return v end
    v = TryCombatRating(_G.CR_CRIT_MELEE or 9)
    if v > 0 then return v end
    return CalcMeleeCrit()
end
local function SafeGetRangedCrit()
    local v = TryAPIs({ "GetRangedCritChance" })
    if v > 0 then return v end
    v = TryCombatRating(_G.CR_CRIT_RANGED or 10)
    if v > 0 then return v end
    return CalcRangedCrit()
end
local function SafeGetSpellCrit()
    local v = TryAPIsArg({ "GetSpellCritChance" }, 2)
    if v > 0 then return v end
    v = TryCombatRating(_G.CR_CRIT_SPELL or 11)
    if v > 0 then return v end
    return CalcSpellCrit()
end
local function SafeGetMeleeHit()
    local v = TryAPIs({ "GetHitModifier", "GetMeleeHitModifier", "GetCombatMissChance" })
    if v > 0 then return v end
    v = TryCombatRating(_G.CR_HIT_MELEE or 6)
    if v > 0 then return v end
    return GetGearBonus("TOHIT")
end
local function SafeGetSpellHit()
    local v = TryAPIs({ "GetSpellHitModifier" })
    if v > 0 then return v end
    v = TryCombatRating(_G.CR_HIT_SPELL or 8)
    if v > 0 then return v end
    return GetGearBonus("SPELLTOHIT")
end
local function IsCritFromAPI()
    return TryAPIs({ "GetCritChance", "GetMeleeCritChance", "GetPlayerCritChance" }) > 0
        or TryCombatRating(_G.CR_CRIT_MELEE or 9) > 0
end

--------------------------------------------------------------------------------
-- Talent scanning: identify hit/crit bonuses from talent trees
-- Uses both English and Chinese name matching for Turtle WoW compatibility
--------------------------------------------------------------------------------
local TALENT_DB = {
    meleeHit = {
        { names = {"Precision", "精准"}, perRank = 1 },
        { names = {"Surefooted", "稳固射击", "脚踏实地"}, perRank = 1 },
        { names = {"Nature's Guidance", "自然指引", "自然引导"}, perRank = 1 },
    },
    spellHit = {
        { names = {"Elemental Precision", "元素精准"}, perRank = 2 },
        { names = {"Arcane Focus", "奥术集中"}, perRank = 2 },
        { names = {"Suppression", "镇压"}, perRank = 2 },
        { names = {"Shadow Focus", "暗影集中"}, perRank = 2 },
        { names = {"Nature's Guidance", "自然指引", "自然引导"}, perRank = 1 },
    },
    meleeCrit = {
        { names = {"Cruelty", "残忍"}, perRank = 1 },
        { names = {"Malice", "恶意"}, perRank = 1 },
        { names = {"Lethal Shots", "致命射击"}, perRank = 1 },
        { names = {"Conviction", "信念", "坚定信念"}, perRank = 1 },
        { names = {"Sharpened Claws", "利爪强化", "尖锐之爪"}, perRank = 2 },
        { names = {"Thundering Strikes", "雷霆一击", "雷霆之击"}, perRank = 1 },
    },
    spellCrit = {
        { names = {"Arcane Instability", "奥术不稳定"}, perRank = 1 },
        { names = {"Critical Mass", "临界质量"}, perRank = 2 },
        { names = {"Holy Specialization", "神圣专精"}, perRank = 1 },
        { names = {"Tidal Mastery", "潮汐掌握"}, perRank = 1 },
    },
}

local _talentCache, _talentCacheTime

local function ScanTalentBonuses()
    local now = GetTime and GetTime() or 0
    if _talentCache and _talentCacheTime and (now - _talentCacheTime) < 5 then
        return _talentCache
    end
    local r = { meleeHit = 0, spellHit = 0, meleeCrit = 0, spellCrit = 0,
                talentDetails = {} }
    if not GetNumTalentTabs then _talentCache = r; _talentCacheTime = now; return r end

    local function NameMatch(tname, patterns)
        if not tname then return false end
        for _, p in ipairs(patterns) do
            if string.find(tname, p, 1, true) then return true end
        end
        return false
    end

    for cat, entries in pairs(TALENT_DB) do
        if cat ~= "talentDetails" then
            for tab = 1, GetNumTalentTabs() do
                for i = 1, GetNumTalents(tab) do
                    local name, _, _, _, rank, maxRank = GetTalentInfo(tab, i)
                    if name and rank and rank > 0 then
                        for _, entry in ipairs(entries) do
                            if NameMatch(name, entry.names) then
                                local bonus = rank * entry.perRank
                                r[cat] = r[cat] + bonus
                                table.insert(r.talentDetails,
                                    { cat = cat, name = name,
                                      rank = rank, maxRank = maxRank or rank,
                                      bonus = bonus })
                            end
                        end
                    end
                end
            end
        end
    end
    _talentCache = r; _talentCacheTime = now
    return r
end

local function GetTalentBonus(cat)
    return ScanTalentBonuses()[cat] or 0
end
local function GetTalentDetailsFor(cat)
    local info = ScanTalentBonuses()
    local out = {}
    for _, d in ipairs(info.talentDetails or {}) do
        if d.cat == cat then table.insert(out, d) end
    end
    return out
end

-- Comprehensive stat builders (base + agi/int + gear + talent)
local function FullMeleeCrit()
    local _, class = UnitClass("player")
    class = class or ""
    local baseCrit = BASE_MELEE_CRIT[class] or 0
    local ratio = AGI_PER_MELEE_CRIT[class] or 0
    local agiCrit = 0
    if ratio > 0 then
        local _, agi = UnitStat("player", 2); agiCrit = (agi or 0) / ratio
    end
    local gearCrit    = GetGearBonus("CRIT")
    local talentCrit  = GetTalentBonus("meleeCrit")
    return baseCrit + agiCrit + gearCrit + talentCrit,
           baseCrit, agiCrit, gearCrit, talentCrit
end
local function FullRangedCrit()
    local _, class = UnitClass("player")
    class = class or ""
    local baseCrit = BASE_MELEE_CRIT[class] or 0
    local ratio = AGI_PER_RANGED_CRIT[class] or 0
    local agiCrit = 0
    if ratio > 0 then
        local _, agi = UnitStat("player", 2); agiCrit = (agi or 0) / ratio
    end
    local gearCrit   = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT")
    local talentCrit = GetTalentBonus("meleeCrit")
    return baseCrit + agiCrit + gearCrit + talentCrit,
           baseCrit, agiCrit, gearCrit, talentCrit
end
local function FullSpellCrit()
    local _, class = UnitClass("player")
    class = class or ""
    local baseCrit = BASE_SPELL_CRIT[class] or 0
    local ratio = INT_PER_SPELL_CRIT[class] or 0
    local intCrit = 0
    if ratio > 0 then
        local _, intel = UnitStat("player", 4); intCrit = (intel or 0) / ratio
    end
    local gearCrit   = GetGearBonus("SPELLCRIT")
    local talentCrit = GetTalentBonus("spellCrit")
    return baseCrit + intCrit + gearCrit + talentCrit,
           baseCrit, intCrit, gearCrit, talentCrit
end
local function FullMeleeHit()
    local gearHit   = GetGearBonus("TOHIT")
    local talentHit = GetTalentBonus("meleeHit")
    return gearHit + talentHit, gearHit, talentHit
end
local function FullSpellHit()
    local gearHit   = GetGearBonus("SPELLTOHIT")
    local talentHit = GetTalentBonus("spellHit")
    return gearHit + talentHit, gearHit, talentHit
end

-- Override Safe* to use Full* when API fails
local _origSafeGetMeleeCrit = SafeGetMeleeCrit
SafeGetMeleeCrit = function()
    local v = TryAPIs({ "GetCritChance", "GetMeleeCritChance", "GetPlayerCritChance" })
    if v > 0 then return v end
    v = TryCombatRating(_G.CR_CRIT_MELEE or 9)
    if v > 0 then return v end
    local total = FullMeleeCrit()
    return total
end
SafeGetRangedCrit = function()
    local v = TryAPIs({ "GetRangedCritChance" })
    if v > 0 then return v end
    v = TryCombatRating(_G.CR_CRIT_RANGED or 10)
    if v > 0 then return v end
    local total = FullRangedCrit()
    return total
end
SafeGetSpellCrit = function()
    local v = TryAPIsArg({ "GetSpellCritChance" }, 2)
    if v > 0 then return v end
    v = TryCombatRating(_G.CR_CRIT_SPELL or 11)
    if v > 0 then return v end
    local total = FullSpellCrit()
    return total
end
SafeGetMeleeHit = function()
    local v = TryAPIs({ "GetHitModifier", "GetMeleeHitModifier", "GetCombatMissChance" })
    if v > 0 then return v end
    v = TryCombatRating(_G.CR_HIT_MELEE or 6)
    if v > 0 then return v end
    local total = FullMeleeHit()
    return total
end
SafeGetSpellHit = function()
    local v = TryAPIs({ "GetSpellHitModifier" })
    if v > 0 then return v end
    v = TryCombatRating(_G.CR_HIT_SPELL or 8)
    if v > 0 then return v end
    local total = FullSpellHit()
    return total
end

--------------------------------------------------------------------------------
-- CS: Combat Stats utility table (single upvalue for all tooltip helpers,
-- avoids Lua 5.0's 32-upvalue-per-closure limit)
--------------------------------------------------------------------------------
local CS = {}
function CS.TipKV(key, val, kr, kg, kb, vr, vg, vb)
    GameTooltip:AddDoubleLine(key, val,
        kr or 0.7, kg or 0.7, kb or 0.75,
        vr or 1,   vg or 1,   vb or 1)
end
function CS.TipLine(txt, r, g, b)
    GameTooltip:AddLine(txt, r or 0.65, g or 0.65, b or 0.7, 1)
end
function CS.CalcArmorReduction(armor, level)
    local lvl = level or UnitLevel("player") or 60
    local k = 400 + 85 * lvl
    local pct = armor / (armor + k) * 100
    if pct < 0  then pct = 0  end
    if pct > 75 then pct = 75 end
    return pct
end
function CS.StatBaseBonus(idx)
    local base, eff = UnitStat("player", idx)
    base = math.floor(base or 0)
    eff  = math.floor(eff  or 0)
    return base, eff, eff - base
end
function CS.TipStatValues(base, eff, bonus)
    CS.TipKV("基础值：", tostring(base))
    if bonus ~= 0 then
        CS.TipKV("装备/Buff 加成：", string.format("%+d", bonus),
            0.7,0.7,0.75, bonus > 0 and 0 or 1, bonus > 0 and 1 or 0, 0)
    end
    CS.TipKV("当前合计：", tostring(eff), 0.7,0.7,0.75, 1,0.9,0.6)
end
CS.SafeGetMeleeCrit  = SafeGetMeleeCrit
CS.SafeGetRangedCrit = SafeGetRangedCrit
CS.SafeGetSpellCrit  = SafeGetSpellCrit
CS.SafeGetMeleeHit   = SafeGetMeleeHit
CS.SafeGetSpellHit   = SafeGetSpellHit
CS.IsCritFromAPI     = IsCritFromAPI
CS.TryAPIs           = TryAPIs
CS.TryAPIsArg        = TryAPIsArg
CS.FullMeleeCrit     = FullMeleeCrit
CS.FullRangedCrit    = FullRangedCrit
CS.FullSpellCrit     = FullSpellCrit
CS.FullMeleeHit      = FullMeleeHit
CS.FullSpellHit      = FullSpellHit
CS.GetTalentDetailsFor = GetTalentDetailsFor
CS.GetGearBonus      = GetGearBonus
CS.GetItemBonusLib   = GetItemBonusLib
CS.AGI_PER_MELEE_CRIT = AGI_PER_MELEE_CRIT

SFrames.CharacterPanel.CS = CS

local function GetItemQualityFromLink(link)
    if not link then return nil end
    local _, _, q = string.find(link, "|c(%x+)|H")
    if not q then return nil end
    local m = {
        ["ff9d9d9d"] = 0, ["ffffffff"] = 1, ["ff1eff00"] = 2,
        ["ff0070dd"] = 3, ["ffa335ee"] = 4, ["ffff8000"] = 5,
    }
    return m[q]
end

--------------------------------------------------------------------------------
-- Scroll helper
--------------------------------------------------------------------------------
local function CreateScrollFrame(parent, width, height)
    local holder = CreateFrame("Frame", NextName("SH"), parent)
    holder:SetWidth(width)
    holder:SetHeight(height)

    local scroll = CreateFrame("ScrollFrame", NextName("SF"), holder)
    scroll:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0)
    scroll:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -10, 0)

    local child = CreateFrame("Frame", NextName("SC"), scroll)
    child:SetWidth(width - 14)
    child:SetHeight(1)
    scroll:SetScrollChild(child)

    local slider = CreateFrame("Slider", NextName("SB"), holder)
    slider:SetWidth(6)
    slider:SetPoint("TOPRIGHT", holder, "TOPRIGHT", -1, -2)
    slider:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -1, 2)
    slider:SetOrientation("VERTICAL")
    slider:SetMinMaxValues(0, 1)
    slider:SetValue(0)
    SetPixelBackdrop(slider, { 0.1, 0.1, 0.12, 0.4 }, { 0.15, 0.15, 0.18, 0.3 })

    local thumb = slider:CreateTexture(nil, "OVERLAY")
    thumb:SetTexture("Interface\\Buttons\\WHITE8X8")
    thumb:SetVertexColor(0.4, 0.4, 0.48, 0.7)
    thumb:SetWidth(6)
    thumb:SetHeight(28)
    slider:SetThumbTexture(thumb)

    slider:SetScript("OnValueChanged", function()
        scroll:SetVerticalScroll(this:GetValue())
    end)
    scroll:EnableMouseWheel(1)
    scroll:SetScript("OnMouseWheel", function()
        local cur = slider:GetValue()
        local step = 28
        local _, mx = slider:GetMinMaxValues()
        if arg1 > 0 then
            slider:SetValue(math.max(cur - step, 0))
        else
            slider:SetValue(math.min(cur + step, mx))
        end
    end)

    holder.scroll = scroll
    holder.child = child
    holder.slider = slider
    holder.SetContentHeight = function(self, h)
        child:SetHeight(h)
        local visH = scroll:GetHeight()
        local maxS = math.max(h - visH, 0)
        slider:SetMinMaxValues(0, maxS)
        if maxS == 0 then slider:Hide() else slider:Show() end
        slider:SetValue(math.min(slider:GetValue(), maxS))
    end
    return holder
end

--------------------------------------------------------------------------------
-- Main Frame
--------------------------------------------------------------------------------
local function SaveCharPanelPosition()
    if not (panel and SFramesDB) then return end
    if not SFramesDB.charPanel then SFramesDB.charPanel = {} end
    local point, _, relPoint, x, y = panel:GetPoint()
    if not point or not relPoint then return end
    SFramesDB.charPanel.position = {
        point = point,
        relPoint = relPoint,
        x = x or 0,
        y = y or 0,
    }
end

local function ApplyCharPanelPosition(f)
    f:ClearAllPoints()
    local pos = SFramesDB and SFramesDB.charPanel and SFramesDB.charPanel.position
    if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then
        f:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y)
    else
        f:SetPoint("LEFT", UIParent, "LEFT", 20, 0)
    end
end

local function CreateMainFrame()
    if panel then return panel end

    local f = CreateFrame("Frame", "SFramesCharacterPanel", UIParent)
    f:SetWidth(FRAME_W)
    f:SetHeight(FRAME_H)
    ApplyCharPanelPosition(f)
    f:SetFrameStrata("HIGH")
    f:EnableMouse(true)
    f:SetMovable(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", function() this:StartMoving() end)
    f:SetScript("OnDragStop", function()
        this:StopMovingOrSizing()
        SaveCharPanelPosition()
    end)
    f:SetClampedToScreen(true)
    SetRoundBackdrop(f, T.bg, T.border)
    CreateShadow(f, 4)
    f:Hide()

    table.insert(UISpecialFrames, "SFramesCharacterPanel")

    f:SetScript("OnHide", function()
        if SFrames.StatSummary and SFrames.StatSummary.Hide then
            SFrames.StatSummary:Hide()
        end
    end)

    -- Title bar: Class icon + Name + subtitle + title button + close
    f.classIcon = SFrames:CreateClassIcon(f, 16)
    f.classIcon.overlay:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD + 2, -6)

    f.titleText = MakeFS(f, 12, "LEFT", T.gold)
    f.titleText:SetPoint("LEFT", f.classIcon.overlay, "RIGHT", 4, 0)

    f.subtitleText = MakeFS(f, 9, "LEFT", T.dimText)
    f.subtitleText:SetPoint("LEFT", f.titleText, "RIGHT", 6, 0)

    -- Weight level display (hoverable, shows iLvl in tooltip)
    local weightBtn = CreateFrame("Button", nil, f)
    weightBtn:SetHeight(14)
    weightBtn:SetWidth(80)
    weightBtn:SetPoint("LEFT", f.titleText, "RIGHT", 4, 0)
    weightBtn:SetFrameLevel(f:GetFrameLevel() + 3)
    local weightText = MakeFS(weightBtn, 9, "LEFT", { 0.55, 0.95, 0.55 })
    weightText:SetPoint("LEFT", weightBtn, "LEFT", 0, 0)
    weightText:SetText("")
    weightBtn.text = weightText
    weightBtn.cachedAvgIlvl = nil
    weightBtn.cachedAvgEP = nil
    weightBtn.cachedClass = nil
    weightBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
        GameTooltip:AddLine("装备权重等级", 0.55, 0.95, 0.55)
        if this.cachedAvgEP then
            GameTooltip:AddDoubleLine("权重等级 (EP)", string.format("%.1f", this.cachedAvgEP), 0.8, 0.8, 0.8, 1, 1, 1)
        end
        if this.cachedAvgIlvl then
            GameTooltip:AddDoubleLine("平均装等 (iLvl)", string.format("%.1f", this.cachedAvgIlvl), 0.8, 0.8, 0.8, 1, 0.82, 0)
        end
        if this.cachedClass then
            local classNames = {
                WARRIOR = "战士", ROGUE = "盗贼", HUNTER = "猎人",
                DRUID = "德鲁伊", PALADIN = "圣骑士", SHAMAN = "萨满祭司",
                MAGE = "法师", WARLOCK = "术士", PRIEST = "牧师",
            }
            GameTooltip:AddDoubleLine("权重模板", classNames[this.cachedClass] or this.cachedClass, 0.8, 0.8, 0.8, 0.6, 0.8, 1)
        end
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("基于职业属性EP权重计算", 0.5, 0.5, 0.5)
        GameTooltip:Show()
    end)
    weightBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    f.weightBtn = weightBtn

    -- Stat summary toggle button
    local statBtn = CreateFrame("Button", nil, f)
    statBtn:SetWidth(14)
    statBtn:SetHeight(14)
    statBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -42, -7)
    statBtn:SetFrameLevel(f:GetFrameLevel() + 3)
    SetPixelBackdrop(statBtn, T.btnBg, T.btnBorder)
    local stTxt = MakeFS(statBtn, 9, "CENTER", T.dimText)
    stTxt:SetPoint("CENTER", statBtn, "CENTER", 0, 0)
    stTxt:SetText("S")
    statBtn:SetScript("OnEnter", function()
        this:SetBackdropColor(T.btnHover[1], T.btnHover[2], T.btnHover[3], T.btnHover[4])
        GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
        GameTooltip:AddLine("属性总览/装备附魔", 1, 1, 1)
        GameTooltip:Show()
    end)
    statBtn:SetScript("OnLeave", function()
        this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
        GameTooltip:Hide()
    end)
    statBtn:SetScript("OnClick", function()
        if SFrames.StatSummary and SFrames.StatSummary.Toggle then
            SFrames.StatSummary:Toggle()
        else
            DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] StatSummary module not loaded!|r")
        end
    end)
    f.statBtn = statBtn

    -- Title selection button (small, next to subtitle)
    local titleBtn = CreateFrame("Button", nil, f)
    titleBtn:SetWidth(14)
    titleBtn:SetHeight(14)
    titleBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -26, -7)
    titleBtn:SetFrameLevel(f:GetFrameLevel() + 3)
    SetPixelBackdrop(titleBtn, T.btnBg, T.btnBorder)
    local tbTxt = MakeFS(titleBtn, 9, "CENTER", T.dimText)
    tbTxt:SetPoint("CENTER", titleBtn, "CENTER", 0, 0)
    tbTxt:SetText("T")
    titleBtn:SetScript("OnEnter", function()
        this:SetBackdropColor(T.btnHover[1], T.btnHover[2], T.btnHover[3], T.btnHover[4])
        GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
        GameTooltip:AddLine("选择称号", 1, 1, 1)
        GameTooltip:Show()
    end)
    titleBtn:SetScript("OnLeave", function()
        this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
        GameTooltip:Hide()
    end)
    titleBtn:SetScript("OnClick", function()
        CP:ToggleTitlePopup()
    end)
    f.titleBtn = titleBtn

    -- Close button (round)
    local closeBtn = CreateFrame("Button", nil, f)
    closeBtn:SetWidth(16)
    closeBtn:SetHeight(16)
    closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -7, -6)
    closeBtn:SetFrameLevel(f:GetFrameLevel() + 3)
    SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder)
    local closeIco = SFrames:CreateIcon(closeBtn, "close", 10)
    closeIco:SetDrawLayer("OVERLAY")
    closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
    closeIco:SetVertexColor(1, 0.7, 0.7)
    closeBtn:SetScript("OnClick", function() f:Hide() end)
    closeBtn:SetScript("OnEnter", function()
        this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
        this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4])
    end)
    closeBtn:SetScript("OnLeave", function()
        this:SetBackdropColor(T.buttonDownBg[1], T.buttonDownBg[2], T.buttonDownBg[3], T.buttonDownBg[4])
        this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
    end)

    MakeSep(f, 6, -HEADER_H, -6, -HEADER_H)

    local _, playerClass = UnitClass("player")
    if (playerClass == "HUNTER" or playerClass == "WARLOCK") and not PET_TAB_INDEX then
        table.insert(TAB_NAMES, "宠物")
        PET_TAB_INDEX = table.getn(TAB_NAMES)
    end

    -- Tab bar
    tabs = {}
    pages = {}
    local numTabs = table.getn(TAB_NAMES)
    local gap = 2
    local tabW = (FRAME_W - SIDE_PAD * 2 - (numTabs - 1) * gap) / numTabs

    for i, name in ipairs(TAB_NAMES) do
        local btn = CreateFrame("Button", NextName("Tab"), f)
        btn:SetWidth(tabW)
        btn:SetHeight(TAB_H)
        btn:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD + (i - 1) * (tabW + gap), -(HEADER_H + 3))
        btn:SetFrameLevel(f:GetFrameLevel() + 2)
        SetRoundBackdrop(btn, T.tabBg, T.tabBorder)

        local txt = MakeFS(btn, 9, "CENTER", T.tabText)
        txt:SetPoint("CENTER", btn, "CENTER", 0, 0)
        txt:SetText(name)
        btn.label = txt
        btn.tabIndex = i

        btn:SetScript("OnClick", function() CP:SetTab(this.tabIndex) end)
        btn:SetScript("OnEnter", function()
            if this.tabIndex ~= CP.activeTab then
                this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], 0.6)
            end
        end)
        btn:SetScript("OnLeave", function()
            if this.tabIndex ~= CP.activeTab then
                this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4])
            end
        end)

        tabs[i] = btn

        local page = CreateFrame("Frame", NextName("Page"), f)
        page:SetPoint("TOPLEFT", f, "TOPLEFT", 4, CONTENT_TOP)
        page:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 4)
        page:Hide()
        pages[i] = page
    end

    MakeSep(f, 6, CONTENT_TOP, -6, CONTENT_TOP)

    panel = f
    return f
end

function CP:SetTab(index)
    self.activeTab = index
    for i = 1, table.getn(TAB_NAMES) do
        if i == index then
            tabs[i]:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4])
            tabs[i]:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4])
            tabs[i].label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
            pages[i]:Show()
        else
            tabs[i]:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4])
            tabs[i]:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], T.tabBorder[4])
            tabs[i].label:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
            pages[i]:Hide()
        end
    end
    self:UpdateCurrentTab()
end

function CP:UpdateTitle()
    if not panel then return end
    local name = UnitName("player") or ""
    local level = UnitLevel("player") or 0
    local classLocal, classEn = UnitClass("player")
    classLocal = classLocal or ""
    classEn = classEn or ""
    local raceLocal = UnitRace("player") or ""

    local cc = SFrames.Config and SFrames.Config.colors and SFrames.Config.colors.class and SFrames.Config.colors.class[classEn]

    local selectedTitle = SFramesDB and SFramesDB.charSelectedTitle or nil
    local titlePrefix = ""
    if selectedTitle and selectedTitle ~= "" then
        titlePrefix = selectedTitle .. " "
    elseif selectedTitle == nil then
        if GetPVPRankInfo and UnitPVPRank then
            local rankName = GetPVPRankInfo(UnitPVPRank("player"))
            if rankName and rankName ~= "" then
                titlePrefix = rankName .. " "
            end
        end
    end

    local fullName = titlePrefix .. name

    if pages and pages[1] and pages[1].modelNameText then
        if cc then
            pages[1].modelNameText:SetTextColor(cc.r, cc.g, cc.b)
        else
            pages[1].modelNameText:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
        end
        pages[1].modelNameText:SetText(fullName)
    end

    if pages and pages[1] and pages[1].modelGuildText then
        local guildName = (SFramesDB and SFramesDB.charShowGuild ~= false) and GetGuildInfo and GetGuildInfo("player") or nil
        if guildName and guildName ~= "" then
            pages[1].modelGuildText:SetText("<" .. guildName .. ">")
        else
            pages[1].modelGuildText:SetText("")
        end
    end

    local parts = {}
    table.insert(parts, "Lv." .. level)
    table.insert(parts, raceLocal .. classLocal)

    local scoreSlots = { 1,2,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18 }
    local totalIlvl, ilvlCount = 0, 0
    local totalEP, epCount = 0, 0
    local showIlvl = (not SFramesDB or SFramesDB.showItemLevel ~= false) and LibItem_Level

    for _, sid in ipairs(scoreSlots) do
        local link = GetInventoryItemLink("player", sid)
        if link then
            if showIlvl then
                local _, _, itemId = string.find(link, "item:(%d+)")
                local ilvl = itemId and LibItem_Level[tonumber(itemId)]
                if ilvl and ilvl > 0 then
                    totalIlvl = totalIlvl + ilvl
                    ilvlCount = ilvlCount + 1
                end
            end
            local ep = CalcItemEP(link, classEn)
            if ep > 0 then
                totalEP = totalEP + ep
                epCount = epCount + 1
            end
        end
    end

    local avgIlvl = ilvlCount > 0 and (totalIlvl / ilvlCount) or nil
    local avgEP = epCount > 0 and (totalEP / epCount) or nil

    if panel.weightBtn then
        panel.weightBtn.cachedAvgIlvl = avgIlvl
        panel.weightBtn.cachedAvgEP = avgEP
        panel.weightBtn.cachedClass = classEn
        if avgEP then
            panel.weightBtn.text:SetText(string.format("EP:%.1f", avgEP))
            panel.weightBtn:SetWidth(panel.weightBtn.text:GetStringWidth() + 4)
            panel.weightBtn:Show()
        elseif avgIlvl then
            panel.weightBtn.text:SetText(string.format("iLvl:%.1f", avgIlvl))
            panel.weightBtn:SetWidth(panel.weightBtn.text:GetStringWidth() + 4)
            panel.weightBtn:Show()
        else
            panel.weightBtn.text:SetText("")
            panel.weightBtn:Hide()
        end
    end

    if cc then
        panel.titleText:SetTextColor(cc.r, cc.g, cc.b)
    else
        panel.titleText:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
    end
    panel.titleText:SetText(table.concat(parts, " "))
    panel.subtitleText:SetText("")

    if panel.classIcon then
        SFrames:SetClassIcon(panel.classIcon, classEn)
    end
end

function CP:UpdateCurrentTab()
    if not panel or not panel:IsShown() then return end
    self:UpdateTitle()
    local tab = self.activeTab or 1
    if tab == 1 then self:UpdateEquipment()
    elseif tab == 2 then self:UpdateReputation()
    elseif tab == 3 then self:UpdateSkills()
    elseif tab == 4 then self:UpdateHonor()
    elseif PET_TAB_INDEX and tab == PET_TAB_INDEX then self:UpdatePet()
    end
end

function CP:Toggle(tab)
    CreateMainFrame()
    self:BuildAllPages()
    if panel:IsShown() then
        if tab and self.activeTab ~= tab then self:SetTab(tab)
        else panel:Hide() end
    else
        local scale = SFramesDB and SFramesDB.charPanelScale or 1.0
        panel:SetScale(scale)
        panel:Show()
        self:SetTab(tab or self.activeTab or 1)
        if SFrames.StatSummary and SFrames.StatSummary.Show then
            SFrames.StatSummary:Show()
        end
    end
end

function CP:Show(tab)
    CreateMainFrame()
    self:BuildAllPages()
    panel:Show()
    self:SetTab(tab or self.activeTab or 1)
    if SFrames.StatSummary and SFrames.StatSummary.Show then
        SFrames.StatSummary:Show()
    end
end

function CP:Hide()
    if panel then panel:Hide() end
end

--------------------------------------------------------------------------------
-- Title selection popup
--------------------------------------------------------------------------------
local titlePopup

function CP:ToggleTitlePopup()
    if titlePopup and titlePopup:IsShown() then
        titlePopup:Hide()
        return
    end
    self:ShowTitlePopup()
end

function CP:ShowTitlePopup()
    if not panel then return end

    if not titlePopup then
        titlePopup = CreateFrame("Frame", "SFramesCPTitlePopup", panel)
        titlePopup:SetWidth(160)
        titlePopup:SetFrameStrata("TOOLTIP")
        titlePopup:SetFrameLevel(200)
        SetRoundBackdrop(titlePopup, T.bg, T.border)
        titlePopup:Hide()
        titlePopup:EnableMouse(true)
    end

    -- Clear old children
    if titlePopup.rows then
        for _, row in ipairs(titlePopup.rows) do
            if row.frame then row.frame:Hide() end
        end
    end
    titlePopup.rows = {}

    -- Build title list from all available sources
    local titles = {}
    local seen = {}
    table.insert(titles, { name = "无称号", key = "" })

    -- PVP rank title
    if GetPVPRankInfo and UnitPVPRank then
        local rank = UnitPVPRank("player")
        if rank and rank > 0 then
            local rankName = GetPVPRankInfo(rank)
            if rankName and rankName ~= "" and not seen[rankName] then
                table.insert(titles, { name = rankName, key = rankName })
                seen[rankName] = true
            end
        end
    end

    -- Turtle WoW custom titles: GetNumTitles / GetTitleName / IsTitleKnown
    if GetNumTitles and GetTitleName then
        local numT = GetNumTitles()
        if numT and numT > 0 then
            for ti = 1, numT do
                local tName = GetTitleName(ti)
                if tName and tName ~= "" and not seen[tName] then
                    local known = true
                    if IsTitleKnown then known = IsTitleKnown(ti) end
                    if known then
                        table.insert(titles, { name = tName, key = tName, titleId = ti })
                        seen[tName] = true
                    end
                end
            end
        end
    end

    -- Fallback: scan CharacterTitleText if it exists (some private servers)
    if table.getn(titles) <= 1 and CharacterTitleText then
        local curTitle = CharacterTitleText:GetText()
        if curTitle and curTitle ~= "" and not seen[curTitle] then
            table.insert(titles, { name = curTitle, key = curTitle })
            seen[curTitle] = true
        end
    end

    local currentTitle = SFramesDB and SFramesDB.charSelectedTitle or ""
    local rowH = 20
    local y = -4
    local popH = 8

    for i, t in ipairs(titles) do
        local row = CreateFrame("Button", nil, titlePopup)
        row:SetWidth(152)
        row:SetHeight(rowH)
        row:SetPoint("TOPLEFT", titlePopup, "TOPLEFT", 4, y)

        local isActive = (currentTitle == t.key) or (currentTitle == "" and t.key == "")
        local bg = isActive and T.tabActiveBg or T.tabBg
        local bd = isActive and T.tabActiveBorder or T.tabBorder
        SetPixelBackdrop(row, bg, bd)

        local txt = MakeFS(row, 10, "LEFT", isActive and T.tabActiveText or T.valueText)
        txt:SetPoint("LEFT", row, "LEFT", 6, 0)
        txt:SetText(t.name)

        if isActive then
            local mark = MakeFS(row, 9, "RIGHT", T.gold)
            mark:SetPoint("RIGHT", row, "RIGHT", -6, 0)
            mark:SetText("*")
        end

        row.titleKey = t.key
        row.titleId = t.titleId
        row:SetScript("OnClick", function()
            SFramesDB = SFramesDB or {}
            SFramesDB.charSelectedTitle = this.titleKey
            -- Try to set server-side title via Turtle WoW API
            if SetCurrentTitle and this.titleId then
                SetCurrentTitle(this.titleId)
            elseif SetCurrentTitle and this.titleKey == "" then
                SetCurrentTitle(0)
            end
            titlePopup:Hide()
            CP:UpdateTitle()
        end)
        row:SetScript("OnEnter", function()
            if not isActive then
                this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], 0.5)
            end
        end)
        row:SetScript("OnLeave", function()
            if not isActive then
                this:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
            end
        end)

        table.insert(titlePopup.rows, { frame = row })
        y = y - rowH - 1
        popH = popH + rowH + 1
    end

    titlePopup:SetHeight(popH)
    titlePopup:ClearAllPoints()
    titlePopup:SetPoint("TOPRIGHT", panel.titleBtn, "BOTTOMRIGHT", 0, -2)
    titlePopup:Show()
end

function CP:BuildAllPages()
    if self.built then return end
    self.built = true
    self:BuildEquipmentPage()
    self:BuildReputationPage()
    self:BuildSkillsPage()
    self:BuildHonorPage()
    if PET_TAB_INDEX then self:BuildPetPage() end
end

--------------------------------------------------------------------------------
-- Stat helpers (shared by equipment page and others)
--------------------------------------------------------------------------------
function CP:CreateStatSection(parent, title, yOffset)
    local header = MakeFS(parent, 11, "LEFT", T.sectionTitle)
    header:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, yOffset)
    header:SetText(title)
    local sep = parent:CreateTexture(nil, "ARTWORK")
    sep:SetTexture("Interface\\Buttons\\WHITE8X8")
    sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
    sep:SetHeight(1)
    sep:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, yOffset - 14)
    sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -8, yOffset - 14)
    return yOffset - 18
end

function CP:CreateStatRow(parent, label, yOffset, color)
    local row = {}
    row.label = MakeFS(parent, 10, "LEFT", T.labelText)
    row.label:SetPoint("TOPLEFT", parent, "TOPLEFT", 14, yOffset)
    row.label:SetText(label)
    row.value = MakeFS(parent, 10, "RIGHT", color or T.valueText)
    row.value:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -14, yOffset)
    row.value:SetWidth(120)
    row.value:SetJustifyH("RIGHT")
    return row, yOffset - 16
end

--------------------------------------------------------------------------------
-- Tab 1: Equipment + Stats (merged, scrollable)
--------------------------------------------------------------------------------
function CP:BuildEquipmentPage()
    local page = pages[1]
    if page.built then return end
    page.built = true

    local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4
    local scrollArea = CreateScrollFrame(page, CONTENT_W, contentH)
    scrollArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
    page.scrollArea = scrollArea

    local child = scrollArea.child
    local cw = SCROLL_W

    -- 3D Model: created on page level (not scroll child) so it follows panel drag
    local slotColW = SLOT_SIZE + 6
    local modelW = cw - slotColW * 2 - 26
    if modelW < 80 then modelW = 80 end
    local modelH = 8 * (SLOT_SIZE + SLOT_GAP) - SLOT_GAP

    -- Background frame in scroll child (for the dark border visual)
    local modelBg = CreateFrame("Frame", nil, child)
    modelBg:SetWidth(modelW)
    modelBg:SetHeight(modelH)
    modelBg:SetPoint("TOP", child, "TOP", 0, -4)
    SetRoundBackdrop(modelBg, T.modelBg, T.modelBorder)
    page.modelBgFrame = modelBg

    -- PlayerModel on page level, anchored to the background frame
    local modelFrame = CreateFrame("Frame", nil, page)
    modelFrame:SetWidth(modelW - 8)
    modelFrame:SetHeight(modelH - 24)
    modelFrame:SetPoint("TOP", modelBg, "TOP", 0, -4)
    modelFrame:SetFrameLevel(page:GetFrameLevel() + 5)

    local model = CreateFrame("PlayerModel", NextName("Model"), modelFrame)
    model:SetAllPoints(modelFrame)
    page.model = model
    page.modelFrame = modelFrame

    model:EnableMouse(true)
    model:EnableMouseWheel(1)
    model.rotating = false
    model.panning = false
    model.curFacing = 0.4
    model.curScale = 1.0
    model.posX = 0
    model.posY = 0
    model.posZ = 0

    model:SetScript("OnMouseDown", function()
        if arg1 == "LeftButton" then
            this.rotating = true
            this.startX = GetCursorPosition()
            this.startFacing = this.curFacing or 0
        elseif arg1 == "RightButton" then
            this.panning = true
            local cx, cy = GetCursorPosition()
            this.panStartX = cx
            this.panStartY = cy
            this.panOriginX = this.posX or 0
            this.panOriginY = this.posY or 0
        end
    end)
    model:SetScript("OnMouseUp", function()
        if arg1 == "LeftButton" then
            this.rotating = false
        elseif arg1 == "RightButton" then
            this.panning = false
        end
    end)
    model:SetScript("OnMouseWheel", function()
        local step = 0.1
        local ns = (this.curScale or 1) + arg1 * step
        if ns < 0.2 then ns = 0.2 end
        if ns > 4.0 then ns = 4.0 end
        this.curScale = ns
        this:SetModelScale(ns)
    end)
    model:SetScript("OnUpdate", function()
        if this.rotating then
            local cx = GetCursorPosition()
            local diff = (cx - (this.startX or cx)) * 0.01
            local nf = (this.startFacing or 0) + diff
            this.curFacing = nf
            this:SetFacing(nf)
        elseif this.panning then
            local cx, cy = GetCursorPosition()
            local es = this:GetEffectiveScale()
            if es < 0.01 then es = 1 end
            local dx = (cx - (this.panStartX or cx)) / (es * 35)
            local dy = (cy - (this.panStartY or cy)) / (es * 35)
            this.posX = (this.panOriginX or 0) + dx
            this.posY = (this.panOriginY or 0) + dy
            this:SetPosition(this.posY, 0, this.posX)
        elseif this.autoRotateDir then
            local speed = 1.5 * (arg1 or 0.016)
            this.curFacing = (this.curFacing or 0) + speed * this.autoRotateDir
            this:SetFacing(this.curFacing)
        end
    end)
    model.ResetView = function(self)
        self.curFacing = 0.4
        self.curScale = 1.0
        self.posX = 0
        self.posY = 0
        self:SetFacing(0.4)
        self:SetModelScale(1.0)
        self:SetPosition(0, 0, 0)
    end

    -- Name/guild overlay floats on top of 3D model
    local nameOverlay = CreateFrame("Frame", nil, page)
    nameOverlay:SetWidth(modelW)
    nameOverlay:SetHeight(30)
    nameOverlay:SetPoint("TOP", modelBg, "TOP", 0, -4)
    nameOverlay:SetFrameLevel(page:GetFrameLevel() + 7)
    nameOverlay:EnableMouse(false)

    local modelNameText = nameOverlay:CreateFontString(nil, "OVERLAY")
    modelNameText:SetFont(GetFont(), 11, "OUTLINE")
    modelNameText:SetPoint("TOP", nameOverlay, "TOP", 0, -5)
    modelNameText:SetJustifyH("CENTER")
    modelNameText:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
    page.modelNameText = modelNameText

    local modelGuildText = nameOverlay:CreateFontString(nil, "OVERLAY")
    modelGuildText:SetFont(GetFont(), 9, "OUTLINE")
    modelGuildText:SetPoint("TOP", modelNameText, "BOTTOM", 0, -1)
    modelGuildText:SetJustifyH("CENTER")
    modelGuildText:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
    page.modelGuildText = modelGuildText

    -- Model toolbar: unified strip with 3 segments
    local tbH = 14
    local segW = 24
    local tbW = segW * 3 + 2
    local toolbar = CreateFrame("Frame", nil, page)
    toolbar:SetWidth(tbW)
    toolbar:SetHeight(tbH)
    toolbar:SetPoint("BOTTOM", modelBg, "BOTTOM", 0, 5)
    toolbar:SetFrameLevel(page:GetFrameLevel() + 6)
    SetPixelBackdrop(toolbar, { 0.04, 0.04, 0.06, 0.75 }, { 0.22, 0.22, 0.28, 0.5 })
    page.modelToolbar = toolbar

    local function MakeToolBtn(parent, w, text, anchor, ox)
        local btn = CreateFrame("Button", nil, parent)
        btn:SetWidth(w)
        btn:SetHeight(tbH - 2)
        btn:SetPoint("LEFT", parent, "LEFT", ox, 0)
        btn:SetFrameLevel(parent:GetFrameLevel() + 1)
        btn:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            tile = false, tileSize = 0, edgeSize = 0,
        })
        btn:SetBackdropColor(0, 0, 0, 0)
        local fs = MakeFS(btn, 9, "CENTER", { 0.5, 0.5, 0.55 })
        fs:SetPoint("CENTER", btn, "CENTER", 0, 0)
        fs:SetText(text)
        btn.label = fs
        btn:SetScript("OnEnter", function()
            this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5)
            this.label:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
        end)
        btn:SetScript("OnLeave", function()
            this:SetBackdropColor(0, 0, 0, 0)
            this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
        end)
        return btn
    end

    local rotLeft = MakeToolBtn(toolbar, segW, "<", "LEFT", 1)
    rotLeft:SetScript("OnMouseDown", function()
        model.autoRotateDir = 1
        this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4] or 0.6)
    end)
    rotLeft:SetScript("OnMouseUp", function()
        model.autoRotateDir = nil
        this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5)
    end)
    rotLeft:SetScript("OnLeave", function()
        this:SetBackdropColor(0, 0, 0, 0)
        this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
        model.autoRotateDir = nil
    end)
    page.rotLeft = rotLeft

    local sep1 = toolbar:CreateTexture(nil, "OVERLAY")
    sep1:SetTexture("Interface\\Buttons\\WHITE8X8")
    sep1:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4] or 0.4)
    sep1:SetWidth(1)
    sep1:SetHeight(tbH - 4)
    sep1:SetPoint("LEFT", toolbar, "LEFT", segW + 1, 0)

    local resetBtn = MakeToolBtn(toolbar, segW, "O", "LEFT", segW + 1)
    resetBtn:SetScript("OnClick", function() model:ResetView() end)
    resetBtn:SetScript("OnEnter", function()
        this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5)
        this.label:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
        GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
        GameTooltip:AddLine("重置视角", 1, 1, 1)
        GameTooltip:Show()
    end)
    resetBtn:SetScript("OnLeave", function()
        this:SetBackdropColor(0, 0, 0, 0)
        this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
        GameTooltip:Hide()
    end)
    page.resetBtn = resetBtn

    local sep2 = toolbar:CreateTexture(nil, "OVERLAY")
    sep2:SetTexture("Interface\\Buttons\\WHITE8X8")
    sep2:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4] or 0.4)
    sep2:SetWidth(1)
    sep2:SetHeight(tbH - 4)
    sep2:SetPoint("LEFT", toolbar, "LEFT", segW * 2 + 1, 0)

    local rotRight = MakeToolBtn(toolbar, segW, ">", "LEFT", segW * 2 + 1)
    rotRight:SetScript("OnMouseDown", function()
        model.autoRotateDir = -1
        this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4] or 0.6)
    end)
    rotRight:SetScript("OnMouseUp", function()
        model.autoRotateDir = nil
        this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5)
    end)
    rotRight:SetScript("OnLeave", function()
        this:SetBackdropColor(0, 0, 0, 0)
        this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
        model.autoRotateDir = nil
    end)
    page.rotRight = rotRight

    -- Equipment slots
    local slotY = -6
    page.equipSlots = {}

    for idx, si in ipairs(EQUIP_SLOTS_LEFT) do
        local slot = self:CreateEquipSlot(child, si.id, si.name)
        slot:SetPoint("TOPLEFT", child, "TOPLEFT", 2, slotY - (idx - 1) * (SLOT_SIZE + SLOT_GAP))
        page.equipSlots[si.id] = slot
    end

    for idx, si in ipairs(EQUIP_SLOTS_RIGHT) do
        local slot = self:CreateEquipSlot(child, si.id, si.name)
        slot:SetPoint("TOPRIGHT", child, "TOPRIGHT", -2, slotY - (idx - 1) * (SLOT_SIZE + SLOT_GAP))
        page.equipSlots[si.id] = slot
    end

    local bottomY = slotY - table.getn(EQUIP_SLOTS_LEFT) * (SLOT_SIZE + SLOT_GAP) - 4
    local bottomCount = table.getn(EQUIP_SLOTS_BOTTOM)
    local totalBW = bottomCount * SLOT_SIZE + (bottomCount - 1) * (SLOT_GAP + 2)
    local bsx = math.max((cw - totalBW) / 2, 4)

    for idx, si in ipairs(EQUIP_SLOTS_BOTTOM) do
        local slot = self:CreateEquipSlot(child, si.id, si.name)
        slot:SetPoint("TOPLEFT", child, "TOPLEFT", bsx + (idx - 1) * (SLOT_SIZE + SLOT_GAP + 1), bottomY)
        page.equipSlots[si.id] = slot
    end

    local infoY = bottomY - SLOT_SIZE

    -- Stats panel: fixed at bottom of page (not in scroll child)
    -- This avoids scroll/overflow issues
    local STAT_PANEL_H = 100
    local ROW_H = 14

    local statPanel = CreateFrame("Frame", nil, page)
    statPanel:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0)
    statPanel:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0)
    statPanel:SetHeight(STAT_PANEL_H)
    statPanel:SetFrameLevel(page:GetFrameLevel() + 2)
    page.statPanel = statPanel

    -- Resize scroll area to not overlap stat panel
    scrollArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, STAT_PANEL_H)

    MakeSep(statPanel, 4, 0, -4, 0)

    -- Category buttons
    local catNames = { "基础/抗性", "攻击", "法术/防御" }
    local catBtnW = math.floor((CONTENT_W - 12) / 3)
    page.statCatBtns = {}
    page.statCatFrames = {}
    page.activeStatCat = 1

    for ci = 1, 3 do
        local cb = CreateFrame("Button", NextName("SC"), statPanel)
        cb:SetWidth(catBtnW)
        cb:SetHeight(16)
        cb:SetPoint("TOPLEFT", statPanel, "TOPLEFT", 4 + (ci - 1) * (catBtnW + 2), -4)
        SetPixelBackdrop(cb, T.tabBg, T.tabBorder)
        local cbt = MakeFS(cb, 8, "CENTER", T.tabText)
        cbt:SetPoint("CENTER", cb, "CENTER", 0, 0)
        cbt:SetText(catNames[ci])
        cb.label = cbt
        cb.catIndex = ci
        cb:SetScript("OnClick", function()
            page.activeStatCat = this.catIndex
            CP:RefreshStatCatVisual(page)
            CP:RefreshStatValues(page)
        end)
        cb:SetScript("OnEnter", function()
            if this.catIndex ~= page.activeStatCat then
                this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], 0.5)
            end
        end)
        cb:SetScript("OnLeave", function()
            if this.catIndex ~= page.activeStatCat then
                this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4])
            end
        end)
        page.statCatBtns[ci] = cb
    end

    local catContentY = -26

    local function MakeStatHitArea(parent, anchorFrom, offX, yOff, isRight)
        local btn = CreateFrame("Button", nil, parent)
        btn:SetHeight(ROW_H)
        btn:SetFrameLevel(parent:GetFrameLevel() + 4)
        if isRight then
            btn:SetPoint("TOPLEFT",  parent, "TOP",      offX,  yOff)
            btn:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4,    yOff)
        else
            btn:SetPoint("TOPLEFT",  parent, "TOPLEFT",  4,     yOff)
            btn:SetPoint("TOPRIGHT", parent, "TOP",      -4,    yOff)
        end
        if btn.SetBackdrop then btn:SetBackdrop(nil) end
        if btn.SetNormalTexture    then btn:SetNormalTexture(nil) end
        if btn.SetHighlightTexture then btn:SetHighlightTexture(nil) end
        if btn.SetPushedTexture    then btn:SetPushedTexture(nil) end
        return btn
    end

    local function Make2ColRow(parent, labelL, labelR, yOff, colorL, colorR)
        local r = {}
        if labelL then
            r.labelL = MakeFS(parent, 9, "LEFT", T.labelText)
            r.labelL:SetPoint("TOPLEFT", parent, "TOPLEFT", 6, yOff)
            r.labelL:SetText(labelL)
            r.valueL = MakeFS(parent, 9, "RIGHT", colorL or T.valueText)
            r.valueL:SetPoint("TOPLEFT", parent, "TOPLEFT", 6, yOff)
            r.valueL:SetPoint("TOPRIGHT", parent, "TOP", -4, yOff)
            r.valueL:SetJustifyH("RIGHT")
            r.hitL = MakeStatHitArea(parent, "TOPLEFT", 0, yOff, false)
        end
        if labelR then
            r.labelR = MakeFS(parent, 9, "LEFT", T.labelText)
            r.labelR:SetPoint("TOPLEFT", parent, "TOP", 6, yOff)
            r.labelR:SetText(labelR)
            r.valueR = MakeFS(parent, 9, "RIGHT", colorR or T.valueText)
            r.valueR:SetPoint("TOPLEFT", parent, "TOP", 6, yOff)
            r.valueR:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -6, yOff)
            r.valueR:SetJustifyH("RIGHT")
            r.hitR = MakeStatHitArea(parent, "TOP", 0, yOff, true)
        end
        return r, yOff - ROW_H
    end

    -- Cat 1: Base + Resist
    local cat1 = CreateFrame("Frame", nil, statPanel)
    cat1:SetAllPoints(statPanel)
    page.statCatFrames[1] = cat1
    page.cat1Rows = {}
    local resistSchools = { 2, 3, 4, 5, 6 }
    local resistNames   = { "火焰", "自然", "冰霜", "暗影", "奥术" }

    local resistSchoolName = { [2]="火焰", [3]="自然", [4]="冰霜", [5]="暗影", [6]="奥术" }

    local statOnEnter = {
        -- 力量
        function()
            local base, eff, bonus = CS.StatBaseBonus(1)
            GameTooltip:AddLine("力量 (Strength)", 1, 0.82, 0.0)
            CS.TipLine("提升近战攻强(AP)与格挡值。")
            GameTooltip:AddLine(" ")
            CS.TipLine("攻击强度转化：", 0.5,0.8,1)
            CS.TipLine("  战/骑/萨/德(熊豹)：1力 = 2AP")
            CS.TipLine("  猫德+野性之心：1力 = 2.4AP")
            CS.TipLine("  盗/猎/法/牧/术：1力 = 1AP")
            GameTooltip:AddLine(" ")
            CS.TipLine("格挡值（需装备盾牌）：", 0.5,0.8,1)
            CS.TipLine("  战/骑/萨：每20点额外力量 +1 BV")
            CS.TipLine("  BV = (力量-种族初始力量)/20")
            GameTooltip:AddLine(" ")
            CS.TipStatValues(base, eff, bonus)
        end,
        -- 敏捷
        function()
            local base, eff, bonus = CS.StatBaseBonus(2)
            local crit  = CS.SafeGetMeleeCrit()
            local dodge = GetDodgeChance and GetDodgeChance() or 0
            local _, effArmor = UnitArmor("player")
            GameTooltip:AddLine("敏捷 (Agility)", 1, 0.82, 0.0)
            CS.TipLine("提升暴击、护甲、闪避。1敏 = 2护甲。")
            GameTooltip:AddLine(" ")
            CS.TipLine("攻击强度转化：", 0.5,0.8,1)
            CS.TipLine("  猎人：1敏 = 2远程AP + 1近战AP")
            CS.TipLine("  盗贼：1敏 = 1近战/远程AP")
            CS.TipLine("  德鲁伊(熊/豹)：1敏 = 1AP")
            CS.TipLine("  其他职业：不提供额外AP")
            GameTooltip:AddLine(" ")
            CS.TipLine("暴击转化（60级）：", 0.5,0.8,1)
            CS.TipLine("  战/骑/萨/德：20敏 = 1%暴击")
            CS.TipLine("  盗贼：29敏 = 1%暴击(最高效)")
            CS.TipLine("  猎人：52.91敏 = 1%暴击(最严苛)")
            GameTooltip:AddLine(" ")
            CS.TipLine("闪避转化（60级）：", 0.5,0.8,1)
            CS.TipLine("  盗贼：14.5敏 = 1%闪避(最强)")
            CS.TipLine("  战/骑/萨/德：20敏 = 1%闪避")
            GameTooltip:AddLine(" ")
            CS.TipStatValues(base, eff, bonus)
            GameTooltip:AddLine(" ")
            CS.TipKV("当前物理暴击：", string.format("%.2f%%", crit),  0.7,0.7,0.75, 1,1,0.5)
            if not CS.IsCritFromAPI() and crit > 0 then
                CS.TipLine("（基础+敏捷+装备+天赋，无Buff）", 0.8,0.5,0.3)
            end
            CS.TipKV("当前闪避率：",   string.format("%.2f%%", dodge), 0.7,0.7,0.75, 1,1,0.5)
            CS.TipKV("当前护甲值：",   tostring(math.floor(effArmor or 0)), 0.7,0.7,0.75, 1,1,0.5)
        end,
        -- 耐力
        function()
            local base, eff, bonus = CS.StatBaseBonus(3)
            local hp = UnitHealthMax("player") or 0
            GameTooltip:AddLine("耐力 (Stamina)", 1, 0.82, 0.0)
            CS.TipLine("所有职业通用的生存核心属性。")
            GameTooltip:AddLine(" ")
            CS.TipLine("生命值转化：", 0.5,0.8,1)
            CS.TipLine("  前20点：每1耐 = 1生命值")
            CS.TipLine("  超过20点：每1耐 = 10生命值")
            GameTooltip:AddLine(" ")
            CS.TipLine("特殊加成：", 0.5,0.8,1)
            CS.TipLine("  牛头人：生命值总额 ×5%")
            CS.TipLine("  术士：可通过生命分流转法力")
            GameTooltip:AddLine(" ")
            CS.TipStatValues(base, eff, bonus)
            CS.TipKV("最大生命值：", tostring(hp), 0.7,0.7,0.75, 0.4,1,0.4)
        end,
        -- 智力
        function()
            local base, eff, bonus = CS.StatBaseBonus(4)
            local mana  = UnitManaMax("player") or 0
            local scrit = CS.SafeGetSpellCrit()
            GameTooltip:AddLine("智力 (Intellect)", 1, 0.82, 0.0)
            CS.TipLine("增加法力值和法术暴击率。")
            CS.TipLine("1智力 = 15法力值（通用）。")
            GameTooltip:AddLine(" ")
            CS.TipLine("法爆转化（60级）：", 0.5,0.8,1)
            CS.TipLine("  圣骑：29.5智 = 1%法爆(最高效!)")
            CS.TipLine("  法师：59.5智 = 1%(基础0.2%)")
            CS.TipLine("  牧师：59.5智 = 1%(基础0.8%)")
            CS.TipLine("  萨满：59.2智 = 1%(基础2.3%)")
            CS.TipLine("  德鲁：60.0智 = 1%(基础1.8%)")
            CS.TipLine("  术士：60.6智 = 1%(基础1.7%)")
            CS.TipLine("  战士/盗贼：无法系收益")
            GameTooltip:AddLine(" ")
            CS.TipStatValues(base, eff, bonus)
            CS.TipKV("最大法力值：",  tostring(mana), 0.7,0.7,0.75, 0.4,0.7,1)
            CS.TipKV("法术暴击率：",  string.format("%.2f%%", scrit), 0.7,0.7,0.75, 1,1,0.5)
        end,
        -- 精神
        function()
            local base, eff, bonus = CS.StatBaseBonus(5)
            GameTooltip:AddLine("精神 (Spirit)", 1, 0.82, 0.0)
            CS.TipLine("控制脱战/五秒规则外的回复速度。")
            CS.TipLine("停止施法5秒后触发精神回蓝。")
            GameTooltip:AddLine(" ")
            CS.TipLine("法力回复（每2秒/跳）：", 0.5,0.8,1)
            CS.TipLine("  牧师/法师：13 + 精神/4")
            CS.TipLine("  德/萨/骑/猎：15 + 精神/5")
            CS.TipLine("  术士(惩罚)：8 + 精神/4")
            GameTooltip:AddLine(" ")
            CS.TipLine("各职业收益：", 0.5,0.8,1)
            CS.TipLine("  牧师(极高)：冥想天赋施法中回蓝")
            CS.TipLine("    精神指引：25%精神→法强/治疗")
            CS.TipLine("  德鲁伊(核心)：配合激活回蓝")
            CS.TipLine("  法师(重要)：配合奥术冥想天赋")
            CS.TipLine("  萨满：MP5更稳定")
            CS.TipLine("  战士：脱战回血 = 精神/50")
            CS.TipLine("    巨魔(精神战)：战斗中25%回复")
            GameTooltip:AddLine(" ")
            CS.TipStatValues(base, eff, bonus)
        end,
    }

    local cy = catContentY
    for i = 1, 5 do
        local row
        row, cy = Make2ColRow(cat1, STAT_NAMES[i], resistNames[i], cy, nil, T.resistColors[resistSchools[i]])
        row.baseIdx    = i
        row.resistSchool = resistSchools[i]
        if row.hitL then
            local fn = statOnEnter[i]
            row.hitL:SetScript("OnEnter", function()
                GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
                GameTooltip:ClearLines()
                fn()
                GameTooltip:Show()
            end)
            row.hitL:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end
        if row.hitR then
            local school = resistSchools[i]
            local rname  = resistNames[i]
            row.hitR:SetScript("OnEnter", function()
                local base, total = UnitResistance("player", school)
                base  = math.floor(base  or 0)
                total = math.floor(total or base)
                local bonus = total - base
                -- Resistance → damage reduction: Classic formula uses total resist / (total resist + 5*attackerLevel)
                -- At player level vs same-level attacker: average ~(R/(R+5L))*0.75 full absorption chance
                local lvl = UnitLevel("player") or 60
                local avgPct = total / (total + 5 * lvl) * 75
                GameTooltip:SetOwner(this, "ANCHOR_LEFT")
                GameTooltip:ClearLines()
                GameTooltip:AddLine(rname .. "抗性", 1, 0.82, 0.0)
                CS.TipLine("减少受到的" .. rname .. "系魔法伤害。")
                CS.TipLine("抗性值越高，被完全抵抗或部分减免的概率越大。")
                GameTooltip:AddLine(" ")
                CS.TipKV("基础抗性：", tostring(base))
                if bonus ~= 0 then
                    CS.TipKV("装备加成：", (bonus > 0 and "+" or "") .. tostring(bonus),
                        0.7,0.7,0.75, bonus>0 and 0 or 1, bonus>0 and 1 or 0, 0)
                end
                CS.TipKV("当前合计：", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6)
                if total > 0 then
                    CS.TipKV("平均减伤约：", string.format("%.1f%%", avgPct), 0.7,0.7,0.75, 1,1,0.5)
                end
                GameTooltip:Show()
            end)
            row.hitR:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end
        table.insert(page.cat1Rows, row)
    end

    -- Cat 2: Melee + Ranged
    local cat2 = CreateFrame("Frame", nil, statPanel)
    cat2:SetAllPoints(statPanel)
    cat2:Hide()
    page.statCatFrames[2] = cat2
    page.cat2Rows = {}
    local meleeL = { "攻强", "伤害", "速度", "暴击", "命中" }
    local meleeK = { "meleeAP", "meleeDmg", "meleeSpeed", "meleeCrit", "meleeHit" }
    local rangedL = { "攻强", "伤害", "速度", "暴击", "" }
    local rangedK = { "rangedAP", "rangedDmg", "rangedSpeed", "rangedCrit", nil }

    local meleeOnEnter = {
        -- 近战攻强
        function()
            local base, pos, neg = UnitAttackPower("player")
            local total = math.floor((base or 0) + (pos or 0) + (neg or 0))
            local dps = total / 14
            local bmod = math.floor((pos or 0) + (neg or 0))
            GameTooltip:AddLine("近战攻击强度 (AP)", 1, 0.82, 0.0)
            CS.TipLine("直接提升近战白字伤害。")
            CS.TipLine("每 14 点攻击强度 = +1 DPS。")
            GameTooltip:AddLine(" ")
            CS.TipLine("来源：力量(战/骑/萨/德 1力=2AP，", 0.5,0.8,1)
            CS.TipLine("盗/猎/法系 1力=1AP) + 装备/Buff", 0.5,0.8,1)
            GameTooltip:AddLine(" ")
            CS.TipKV("基础 AP：", tostring(math.floor(base or 0)))
            if bmod ~= 0 then
                CS.TipKV("装备/Buff 加成：", string.format("%+d", bmod),
                    0.7,0.7,0.75, bmod > 0 and 0 or 1, bmod > 0 and 1 or 0, 0)
            end
            CS.TipKV("当前合计：", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6)
            CS.TipKV("等效 DPS 增益：", string.format("+%.1f", dps), 0.7,0.7,0.75, 1,1,0.5)
        end,
        -- 近战伤害
        function()
            local lo, hi = UnitDamage("player")
            lo = math.floor(lo or 0); hi = math.floor(hi or 0)
            local ms = UnitAttackSpeed("player") or 2
            local avgDps = ms > 0 and (lo + hi) / 2 / ms or 0
            GameTooltip:AddLine("近战伤害", 1, 0.82, 0.0)
            CS.TipLine("主手武器基础伤害 + 攻击强度加成后的实际白字伤害区间。")
            CS.TipLine("AP 加成 = 武器速度 × AP / 14。")
            GameTooltip:AddLine(" ")
            CS.TipKV("伤害区间：", lo .. " - " .. hi)
            CS.TipKV("主手 DPS：", string.format("%.1f", avgDps), 0.7,0.7,0.75, 1,1,0.5)
        end,
        -- 近战速度
        function()
            local ms, os = UnitAttackSpeed("player")
            GameTooltip:AddLine("近战攻击速度", 1, 0.82, 0.0)
            CS.TipLine("每次自动攻击的间隔（秒），数值越低攻击越快。")
            CS.TipLine("受急速效果（如狂暴/风怒图腾）影响。")
            GameTooltip:AddLine(" ")
            CS.TipKV("主手速度：", string.format("%.2f 秒", ms or 0))
            if os and os > 0 then
                CS.TipKV("副手速度：", string.format("%.2f 秒", os))
                CS.TipLine(" ")
                CS.TipLine("副手白字伤害为主手50%。", 0.5,0.8,1)
                CS.TipLine("双持白字未命中：26.4%(300技能)", 0.5,0.8,1)
                CS.TipLine("315技能仍有24.0%未命中。", 0.5,0.8,1)
            end
        end,
        -- 近战暴击
        function()
            local fromAPI = CS.IsCritFromAPI()
            local _, class = UnitClass("player")
            local ratio = CS.AGI_PER_MELEE_CRIT[class or ""] or 0
            GameTooltip:AddLine("近战暴击率", 1, 0.82, 0.0)
            CS.TipLine("近战暴击造成 200% 伤害。")
            if ratio > 0 then
                if ratio == math.floor(ratio) then
                    CS.TipLine(string.format("当前职业：每 %d 敏捷 = 1%% 暴击。", ratio))
                else
                    CS.TipLine(string.format("当前职业：每 %.2f 敏捷 = 1%% 暴击。", ratio))
                end
            end
            GameTooltip:AddLine(" ")
            if fromAPI then
                local crit = CS.SafeGetMeleeCrit()
                CS.TipKV("当前暴击率：", string.format("%.2f%%", crit), 0.7,0.7,0.75, 1,1,0.5)
            else
                local total, base, agiC, gearC, talC = CS.FullMeleeCrit()
                CS.TipLine("来源分项：", 0.5,0.8,1)
                if base > 0 then CS.TipKV("  基础暴击：",  string.format("%.2f%%", base)) end
                if agiC > 0 then CS.TipKV("  敏捷暴击：",  string.format("%.2f%%", agiC)) end
                if gearC > 0 then CS.TipKV("  装备暴击：", string.format("+%d%%", gearC)) end
                if talC > 0 then
                    CS.TipKV("  天赋暴击：", string.format("+%d%%", talC))
                    for _, d in ipairs(CS.GetTalentDetailsFor("meleeCrit")) do
                        CS.TipLine(string.format("    %s (%d/%d) +%d%%",
                            d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
                    end
                end
                GameTooltip:AddLine(" ")
                CS.TipKV("合计暴击率：", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5)
                CS.TipLine("（Buff 暴击未计入）", 0.8,0.5,0.3)
            end
        end,
        -- 近战命中
        function()
            local fromAPI = CS.TryAPIs({ "GetHitModifier", "GetMeleeHitModifier" }) > 0
            GameTooltip:AddLine("近战命中", 1, 0.82, 0.0)
            CS.TipLine("减少近战未命中概率。物理DPS最优先属性。")
            CS.TipLine("乌龟服对BOSS基础未命中率 8%。")
            GameTooltip:AddLine(" ")
            if fromAPI then
                local hit = CS.SafeGetMeleeHit()
                CS.TipKV("当前命中加成：", string.format("%+d%%", hit), 0.7,0.7,0.75, 1,1,0.5)
            else
                local total, gearH, talH = CS.FullMeleeHit()
                CS.TipLine("来源分项：", 0.5,0.8,1)
                if gearH > 0 then CS.TipKV("  装备命中：", string.format("+%d%%", gearH)) end
                if talH > 0 then
                    CS.TipKV("  天赋命中：", string.format("+%d%%", talH))
                    for _, d in ipairs(CS.GetTalentDetailsFor("meleeHit")) do
                        CS.TipLine(string.format("    %s (%d/%d) +%d%%",
                            d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
                    end
                end
                GameTooltip:AddLine(" ")
                CS.TipKV("合计命中：", string.format("+%d%%", total), 0.7,0.7,0.75, 1,1,0.5)
            end
            local totalHit = CS.SafeGetMeleeHit()
            local lvl = UnitLevel("player") or 60
            if lvl >= 60 then
                GameTooltip:AddLine(" ")
                CS.TipLine("达标判定（对+3级BOSS）：", 1, 0.82, 0.0)
                CS.TipLine("已移除隐性1%命中压制。", 0.5,0.8,1)
                local cap = 8
                if totalHit >= cap then
                    CS.TipKV("  黄字8%(300技能)：", "已达标",  0.7,0.7,0.75, 0.3,1,0.3)
                else
                    CS.TipKV("  黄字8%(300技能)：", string.format("差%d%%", cap - totalHit),
                        0.7,0.7,0.75, 1,0.3,0.3)
                end
                if totalHit >= 5 then
                    CS.TipKV("  种族5%(305技能)：", "已达标",  0.7,0.7,0.75, 0.3,1,0.3)
                else
                    CS.TipKV("  种族5%(305技能)：", string.format("差%d%%", 5 - totalHit),
                        0.7,0.7,0.75, 1,0.3,0.3)
                end
                if totalHit >= 2 then
                    CS.TipKV("  高技能2%(310)：", "已达标",  0.7,0.7,0.75, 0.3,1,0.3)
                else
                    CS.TipKV("  高技能2%(310)：", string.format("差%d%%", 2 - totalHit),
                        0.7,0.7,0.75, 1,0.3,0.3)
                end
                CS.TipLine("  315技能时命中需求归零", 0.55,0.55,0.6)
                CS.TipLine("  (人类剑锤/兽人斧/矮人枪锤/侏儒短剑)", 0.55,0.55,0.6)
                GameTooltip:AddLine(" ")
                CS.TipLine("偏斜减免（白字对BOSS）：", 0.5,0.8,1)
                CS.TipLine("  300技能：约35%伤害损失", 0.55,0.55,0.6)
                CS.TipLine("  305技能：约15%伤害损失", 0.55,0.55,0.6)
                CS.TipLine("  315技能：无减免(全额)", 0.55,0.55,0.6)
            end
        end,
    }
    local rangedOnEnter = {
        -- 远程攻强
        function()
            local base, pos, neg = UnitRangedAttackPower("player")
            local total = math.floor((base or 0) + (pos or 0) + (neg or 0))
            local bmod = math.floor((pos or 0) + (neg or 0))
            GameTooltip:AddLine("远程攻击强度 (RAP)", 1, 0.82, 0.0)
            CS.TipLine("直接提升弓/枪/弩/投掷武器的伤害。")
            CS.TipLine("每 14 点远程攻击强度 = +1 DPS。")
            GameTooltip:AddLine(" ")
            CS.TipLine("猎人核心属性：1 敏捷 = 2 远程AP。", 0.5,0.8,1)
            GameTooltip:AddLine(" ")
            CS.TipKV("基础 RAP：", tostring(math.floor(base or 0)))
            if bmod ~= 0 then
                CS.TipKV("装备/Buff 加成：", string.format("%+d", bmod),
                    0.7,0.7,0.75, bmod > 0 and 0 or 1, bmod > 0 and 1 or 0, 0)
            end
            CS.TipKV("当前合计：", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6)
            CS.TipKV("等效 DPS 增益：", string.format("+%.1f", total / 14), 0.7,0.7,0.75, 1,1,0.5)
        end,
        -- 远程伤害
        function()
            local _, lo, hi = UnitRangedDamage("player")
            lo = math.floor(lo or 0); hi = math.floor(hi or 0)
            local rspd = UnitRangedDamage("player") or 2
            local avgDps = rspd > 0 and (lo + hi) / 2 / rspd or 0
            GameTooltip:AddLine("远程伤害", 1, 0.82, 0.0)
            CS.TipLine("远程武器基础伤害 + 远程攻击强度加成后的实际伤害区间。")
            GameTooltip:AddLine(" ")
            CS.TipKV("伤害区间：", lo .. " - " .. hi)
            CS.TipKV("远程 DPS：", string.format("%.1f", avgDps), 0.7,0.7,0.75, 1,1,0.5)
        end,
        -- 远程速度
        function()
            local spd = UnitRangedDamage("player") or 0
            GameTooltip:AddLine("远程攻击速度", 1, 0.82, 0.0)
            CS.TipLine("远程武器每次射击的间隔（秒）。")
            CS.TipLine("受急速效果影响（如猎人急速射击天赋）。")
            GameTooltip:AddLine(" ")
            CS.TipKV("射击间隔：", string.format("%.2f 秒", spd))
        end,
        -- 远程暴击
        function()
            local fromAPI = CS.TryAPIs({ "GetRangedCritChance" }) > 0
            GameTooltip:AddLine("远程暴击率", 1, 0.82, 0.0)
            CS.TipLine("远程暴击造成 200% 伤害。猎人：每 52.91 敏 = 1%。")
            GameTooltip:AddLine(" ")
            if fromAPI then
                CS.TipKV("当前暴击率：", string.format("%.2f%%", CS.SafeGetRangedCrit()), 0.7,0.7,0.75, 1,1,0.5)
            else
                local total, base, agiC, gearC, talC = CS.FullRangedCrit()
                CS.TipLine("来源分项：", 0.5,0.8,1)
                if base > 0 then CS.TipKV("  基础暴击：",  string.format("%.2f%%", base)) end
                if agiC > 0 then CS.TipKV("  敏捷暴击：",  string.format("%.2f%%", agiC)) end
                if gearC > 0 then CS.TipKV("  装备暴击：", string.format("+%d%%", gearC)) end
                if talC > 0 then
                    CS.TipKV("  天赋暴击：", string.format("+%d%%", talC))
                    for _, d in ipairs(CS.GetTalentDetailsFor("meleeCrit")) do
                        CS.TipLine(string.format("    %s (%d/%d) +%d%%",
                            d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
                    end
                end
                GameTooltip:AddLine(" ")
                CS.TipKV("合计暴击率：", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5)
                CS.TipLine("（Buff 暴击未计入）", 0.8,0.5,0.3)
            end
        end,
    }

    cy = catContentY
    for i = 1, 5 do
        local row
        row, cy = Make2ColRow(cat2, meleeL[i], rangedL[i] ~= "" and rangedL[i] or nil, cy)
        row.meleeKey  = meleeK[i]
        row.rangedKey = rangedK[i]
        if row.hitL and meleeOnEnter[i] then
            local fn = meleeOnEnter[i]
            row.hitL:SetScript("OnEnter", function()
                GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
                GameTooltip:ClearLines()
                fn()
                GameTooltip:Show()
            end)
            row.hitL:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end
        if row.hitR and rangedOnEnter[i] then
            local fn = rangedOnEnter[i]
            row.hitR:SetScript("OnEnter", function()
                GameTooltip:SetOwner(this, "ANCHOR_LEFT")
                GameTooltip:ClearLines()
                fn()
                GameTooltip:Show()
            end)
            row.hitR:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end
        table.insert(page.cat2Rows, row)
    end

    -- Cat 3: Spell + Defense
    local cat3 = CreateFrame("Frame", nil, statPanel)
    cat3:SetAllPoints(statPanel)
    cat3:Hide()
    page.statCatFrames[3] = cat3
    page.cat3Rows = {}
    local spellL = { "法伤", "治疗", "暴击", "命中", "" }
    local spellK = { "spellDmg", "spellHeal", "spellCrit", "spellHit", nil }
    local defL   = { "护甲", "防御", "闪避", "招架", "格挡" }
    local defK   = { "armor", "defense", "dodge", "parry", "block" }

    local spellOnEnter = {
        -- 法术伤害
        function()
            local maxDmg = 0
            local perSchool = {}
            local fromAPI = false
            if GetSpellBonusDamage then
                for s = 2, 7 do
                    local d = GetSpellBonusDamage(s) or 0
                    perSchool[s] = d
                    if d > maxDmg then maxDmg = d; fromAPI = true end
                end
            end
            if maxDmg == 0 then
                local lib = CS.GetItemBonusLib and CS.GetItemBonusLib() or nil
                if lib and lib.GetBonus then
                    local baseDmg = lib:GetBonus("DMG") or 0
                    local schoolKeys = {
                        [2] = "FIREDMG", [3] = "NATUREDMG", [4] = "FROSTDMG",
                        [5] = "SHADOWDMG", [6] = "ARCANEDMG", [7] = "HOLYDMG",
                    }
                    for s = 2, 7 do
                        local sd = baseDmg + (lib:GetBonus(schoolKeys[s]) or 0)
                        perSchool[s] = sd
                        if sd > maxDmg then maxDmg = sd end
                    end
                end
            end
            GameTooltip:AddLine("法术伤害加成", 1, 0.82, 0.0)
            CS.TipLine("提升伤害法术输出，按系别独立计算。")
            CS.TipLine("加成 = 法伤 × 系数(0.3~1.0)。")
            GameTooltip:AddLine(" ")
            CS.TipLine("乌龟服特色：", 0.5,0.8,1)
            CS.TipLine("  部分锁/皮甲装备带法伤属性", 0.5,0.8,1)
            CS.TipLine("  增强萨：震击获10%AP法伤", 0.5,0.8,1)
            CS.TipLine("  惩戒骑：十字军受33%SP加成", 0.5,0.8,1)
            GameTooltip:AddLine(" ")
            CS.TipKV("最高法术伤害：", tostring(math.floor(maxDmg)), 0.7,0.7,0.75, 1,0.9,0.6)
            local snames = {[2]="火焰",[3]="自然",[4]="冰霜",[5]="暗影",[6]="奥术",[7]="神圣"}
            for s = 2, 7 do
                if perSchool[s] and perSchool[s] > 0 then
                    CS.TipKV("  " .. (snames[s] or s) .. "：", tostring(math.floor(perSchool[s])))
                end
            end
            if not fromAPI and maxDmg > 0 then
                CS.TipLine("（来自装备扫描，Buff 加成未计入）", 0.8,0.5,0.3)
            end
        end,
        -- 治疗
        function()
            local heal = GetSpellBonusHealing and GetSpellBonusHealing() or 0
            local fromAPI = heal > 0
            if heal == 0 then
                heal = CS.GetGearBonus("HEAL")
            end
            heal = math.floor(heal)
            GameTooltip:AddLine("治疗加成", 1, 0.82, 0.0)
            CS.TipLine("提升所有治疗法术的恢复量。")
            CS.TipLine("实际加成 = 治疗加成 × 法术系数（通常 0.4~1.0）。")
            GameTooltip:AddLine(" ")
            CS.TipLine("神圣骑/恢复德/牧师核心属性。", 0.5,0.8,1)
            CS.TipLine("骑士Ironclad：护甲1-2%→治疗量", 0.5,0.8,1)
            GameTooltip:AddLine(" ")
            CS.TipKV("当前治疗加成：", tostring(heal), 0.7,0.7,0.75, 1,0.9,0.6)
            if not fromAPI and heal > 0 then
                CS.TipLine("（来自装备扫描，Buff 加成未计入）", 0.8,0.5,0.3)
            end
        end,
        -- 法术暴击
        function()
            local fromAPI = CS.TryAPIsArg({ "GetSpellCritChance" }, 2) > 0
            GameTooltip:AddLine("法术暴击率", 1, 0.82, 0.0)
            CS.TipLine("法术暴击造成 150% 伤害/治疗量（+50%）。")
            GameTooltip:AddLine(" ")
            if fromAPI then
                CS.TipKV("当前法术暴击：", string.format("%.2f%%", CS.SafeGetSpellCrit()), 0.7,0.7,0.75, 1,1,0.5)
            else
                local total, base, intC, gearC, talC = CS.FullSpellCrit()
                CS.TipLine("来源分项：", 0.5,0.8,1)
                if base > 0 then CS.TipKV("  基础暴击：",  string.format("%.2f%%", base)) end
                if intC > 0 then CS.TipKV("  智力暴击：",  string.format("%.2f%%", intC)) end
                if gearC > 0 then CS.TipKV("  装备暴击：", string.format("+%d%%", gearC)) end
                if talC > 0 then
                    CS.TipKV("  天赋暴击：", string.format("+%d%%", talC))
                    for _, d in ipairs(CS.GetTalentDetailsFor("spellCrit")) do
                        CS.TipLine(string.format("    %s (%d/%d) +%d%%",
                            d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
                    end
                end
                GameTooltip:AddLine(" ")
                CS.TipKV("合计法术暴击：", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5)
                CS.TipLine("（Buff 暴击未计入）", 0.8,0.5,0.3)
            end
        end,
        -- 法术命中
        function()
            local fromAPI = CS.TryAPIs({ "GetSpellHitModifier" }) > 0
            GameTooltip:AddLine("法术命中", 1, 0.82, 0.0)
            CS.TipLine("减少法术被抵抗概率。法系最优先。")
            CS.TipLine("对+3级BOSS基础未命中16%。")
            GameTooltip:AddLine(" ")
            if fromAPI then
                CS.TipKV("当前法术命中：", string.format("%+d%%", CS.SafeGetSpellHit()), 0.7,0.7,0.75, 1,1,0.5)
            else
                local total, gearH, talH = CS.FullSpellHit()
                CS.TipLine("来源分项：", 0.5,0.8,1)
                if gearH > 0 then CS.TipKV("  装备命中：", string.format("+%d%%", gearH)) end
                if talH > 0 then
                    CS.TipKV("  天赋命中：", string.format("+%d%%", talH))
                    for _, d in ipairs(CS.GetTalentDetailsFor("spellHit")) do
                        CS.TipLine(string.format("    %s (%d/%d) +%d%%",
                            d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
                    end
                    CS.TipLine("  (天赋命中可能仅对特定系别)", 0.8,0.5,0.3)
                end
                GameTooltip:AddLine(" ")
                CS.TipKV("合计法术命中：", string.format("+%d%%", total), 0.7,0.7,0.75, 1,1,0.5)
            end
            local totalHit = CS.SafeGetSpellHit()
            local lvl = UnitLevel("player") or 60
            if lvl >= 60 then
                GameTooltip:AddLine(" ")
                local cap = 16
                CS.TipLine("达标（对+3级BOSS需16%）：", 1, 0.82, 0.0)
                if totalHit >= cap then
                    CS.TipKV("  状态：", "已达标", 0.7,0.7,0.75, 0.3,1,0.3)
                else
                    CS.TipKV("  状态：", string.format("差%d%%", cap - totalHit),
                        0.7,0.7,0.75, 1,0.3,0.3)
                end
                CS.TipLine("  法师精准6%/奥术集中10%", 0.55,0.55,0.6)
                CS.TipLine("  术士镇压10%/暗牧集中10%", 0.55,0.55,0.6)
            end
        end,
    }
    local defOnEnter = {
        -- 护甲
        function()
            local baseArmor, effArmor = UnitArmor("player")
            effArmor  = math.floor(effArmor  or baseArmor or 0)
            baseArmor = math.floor(baseArmor or 0)
            local bonusArmor = effArmor - baseArmor
            local lvl = UnitLevel("player") or 60
            local pct = CS.CalcArmorReduction(effArmor, lvl)
            local pctBoss = CS.CalcArmorReduction(effArmor, lvl + 3)
            GameTooltip:AddLine("护甲", 1, 0.82, 0.0)
            CS.TipLine("减少受到的物理伤害。减伤上限为 75%。")
            GameTooltip:AddLine(" ")
            CS.TipLine("公式：护甲/(护甲+400+85×等级)", 0.5,0.8,1)
            CS.TipLine("来源：装备 + 敏捷护甲加成。", 0.5,0.8,1)
            CS.TipLine("熊德/防战护甲需求最高。", 0.5,0.8,1)
            GameTooltip:AddLine(" ")
            CS.TipKV("基础护甲：", tostring(baseArmor))
            if bonusArmor ~= 0 then
                CS.TipKV("Buff/敏捷加成：", string.format("%+d", bonusArmor),
                    0.7,0.7,0.75, bonusArmor > 0 and 0 or 1, bonusArmor > 0 and 1 or 0, 0)
            end
            CS.TipKV("当前护甲：", tostring(effArmor), 0.7,0.7,0.75, 1,0.9,0.6)
            GameTooltip:AddLine(" ")
            CS.TipKV("减伤(同级)：",  string.format("%.1f%%", pct),     0.7,0.7,0.75, 0.4,1,0.4)
            CS.TipKV("减伤(+3BOSS)：", string.format("%.1f%%", pctBoss), 0.7,0.7,0.75, 1,0.7,0.3)
        end,
        -- 防御
        function()
            local base, mod = UnitDefense("player")
            base = math.floor(base or 0)
            mod  = math.floor(mod  or 0)
            local total = base + mod
            local uncritCap = 440
            GameTooltip:AddLine("防御技能", 1, 0.82, 0.0)
            CS.TipLine("提升闪避/招架/格挡，降低被暴击/碾压。")
            GameTooltip:AddLine(" ")
            CS.TipLine("每1点防御（超出攻击者）：", 0.5,0.8,1)
            CS.TipLine("  +0.04%闪避/招架/格挡")
            CS.TipLine("  -0.04%被暴击/被碾压")
            GameTooltip:AddLine(" ")
            CS.TipLine("坦克达标（对63级BOSS）：", 1, 0.82, 0.0)
            CS.TipLine("  免暴击：防御≥440(额外140)", 1,0.4,0.4)
            CS.TipLine("  BOSS暴击率5.6%，每防-0.04%", 0.55,0.55,0.6)
            GameTooltip:AddLine(" ")
            CS.TipLine("  免碾压(102.4%圆桌)：", 1,0.4,0.4)
            CS.TipLine("  闪避+招架+格挡≥102.4%", 0.55,0.55,0.6)
            CS.TipLine("  碾压(150%伤)被排挤出圆桌", 0.55,0.55,0.6)
            CS.TipLine("  防战靠盾挡，防骑靠神圣之盾", 0.55,0.55,0.6)
            GameTooltip:AddLine(" ")
            CS.TipLine("坦怪仇恨压力大，命中不可忽视。", 0.5,0.8,1)
            GameTooltip:AddLine(" ")
            CS.TipKV("基础防御：", tostring(base))
            if mod ~= 0 then
                CS.TipKV("装备加成：", string.format("%+d", mod),
                    0.7,0.7,0.75, mod > 0 and 0 or 1, mod > 0 and 1 or 0, 0)
            end
            CS.TipKV("当前防御：", tostring(total), 0.7,0.7,0.75, 1,0.9,0.6)
            if total >= uncritCap then
                CS.TipKV("免暴击(440)：", "已达标", 0.7,0.7,0.75, 0.3,1,0.3)
            else
                CS.TipKV("免暴击(440)：", string.format("差%d防御", uncritCap - total),
                    0.7,0.7,0.75, 1,0.3,0.3)
            end
        end,
        -- 闪避
        function()
            local dodge = GetDodgeChance and GetDodgeChance() or 0
            if dodge == 0 then dodge = CS.GetGearBonus("DODGE") end
            GameTooltip:AddLine("闪避", 1, 0.82, 0.0)
            CS.TipLine("完全规避一次近战物理攻击。")
            CS.TipLine("闪避时不受伤害，不触发命中特效。")
            CS.TipLine("注意：对远程攻击无效。")
            GameTooltip:AddLine(" ")
            CS.TipLine("来源：敏捷+防御+天赋+装备。", 0.5,0.8,1)
            CS.TipLine("熊德无法招架，闪避是主要减伤。", 0.5,0.8,1)
            GameTooltip:AddLine(" ")
            CS.TipKV("当前闪避率：", string.format("%.2f%%", dodge), 0.7,0.7,0.75, 1,1,0.5)
            if (GetDodgeChance and GetDodgeChance() or 0) == 0 and dodge > 0 then
                CS.TipLine("（仅含装备加成）", 0.8,0.5,0.3)
            end
        end,
        -- 招架
        function()
            local parry = GetParryChance and GetParryChance() or 0
            if parry == 0 then parry = CS.GetGearBonus("PARRY") end
            GameTooltip:AddLine("招架", 1, 0.82, 0.0)
            CS.TipLine("完全免疫一次近战攻击伤害，")
            CS.TipLine("并使下次攻击加速40%(招架反击)。")
            GameTooltip:AddLine(" ")
            CS.TipLine("可招架：战士/骑士/盗贼/猎人。", 0.5,0.8,1)
            CS.TipLine("熊德无法招架(仅靠闪避+护甲)。", 1,0.5,0.5)
            CS.TipLine("只能招架正面攻击。", 0.5,0.8,1)
            GameTooltip:AddLine(" ")
            CS.TipKV("当前招架率：", string.format("%.2f%%", parry), 0.7,0.7,0.75, 1,1,0.5)
            if (GetParryChance and GetParryChance() or 0) == 0 and parry > 0 then
                CS.TipLine("（仅含装备加成）", 0.8,0.5,0.3)
            end
        end,
        -- 格挡
        function()
            local block = GetBlockChance and GetBlockChance() or 0
            local bv = 0
            if GetBlockValue then bv = GetBlockValue() or 0 end
            GameTooltip:AddLine("格挡", 1, 0.82, 0.0)
            CS.TipLine("盾牌格挡吸收等同格挡值的伤害。")
            CS.TipLine("不能完全免伤，但大幅降低受伤。")
            GameTooltip:AddLine(" ")
            CS.TipLine("需装备盾牌。来源：防御+装备。", 0.5,0.8,1)
            CS.TipLine("持盾：战士/圣骑/萨满。", 0.5,0.8,1)
            CS.TipLine("BV = (力量-初始力量)/20", 0.5,0.8,1)
            GameTooltip:AddLine(" ")
            CS.TipLine("102.4%圆桌中格挡占重要份额。", 0.5,0.8,1)
            GameTooltip:AddLine(" ")
            CS.TipKV("当前格挡率：", string.format("%.2f%%", block), 0.7,0.7,0.75, 1,1,0.5)
            if bv > 0 then
                CS.TipKV("格挡值 (BV)：", tostring(math.floor(bv)), 0.7,0.7,0.75, 0.4,1,0.4)
                CS.TipLine("（每次格挡吸收该数值的物理伤害）", 0.55,0.55,0.6)
            end
        end,
    }

    cy = catContentY
    for i = 1, 5 do
        local row
        local lbl = spellL[i] ~= "" and spellL[i] or nil
        row, cy = Make2ColRow(cat3, lbl, defL[i], cy)
        row.spellKey = spellK[i]
        row.defKey   = defK[i]
        if row.hitL and spellOnEnter[i] then
            local fn = spellOnEnter[i]
            row.hitL:SetScript("OnEnter", function()
                GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
                GameTooltip:ClearLines()
                fn()
                GameTooltip:Show()
            end)
            row.hitL:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end
        if row.hitR and defOnEnter[i] then
            local fn = defOnEnter[i]
            row.hitR:SetScript("OnEnter", function()
                GameTooltip:SetOwner(this, "ANCHOR_LEFT")
                GameTooltip:ClearLines()
                fn()
                GameTooltip:Show()
            end)
            row.hitR:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end
        table.insert(page.cat3Rows, row)
    end

    -- Set scroll content height (equipment area only, stats are fixed)
    page.totalContentH = math.abs(infoY) + 4
    scrollArea:SetContentHeight(page.totalContentH)

    -- Initialize stat category visual (highlight first button)
    CP:RefreshStatCatVisual(page)
end

function CP:RefreshStatCatVisual(page)
    for ci = 1, 3 do
        if ci == page.activeStatCat then
            page.statCatBtns[ci]:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4])
            page.statCatBtns[ci]:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4])
            page.statCatBtns[ci].label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
            page.statCatFrames[ci]:Show()
        else
            page.statCatBtns[ci]:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4])
            page.statCatBtns[ci]:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], T.tabBorder[4])
            page.statCatBtns[ci].label:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
            page.statCatFrames[ci]:Hide()
        end
    end
end

--------------------------------------------------------------------------------
-- Equipment Swap Popup (Alt+Hover to show bag alternatives)
--------------------------------------------------------------------------------
local SLOT_TO_EQUIP_LOCS = {
    [1]  = { INVTYPE_HEAD = true },
    [2]  = { INVTYPE_NECK = true },
    [3]  = { INVTYPE_SHOULDER = true },
    [4]  = { INVTYPE_BODY = true },
    [5]  = { INVTYPE_CHEST = true, INVTYPE_ROBE = true },
    [6]  = { INVTYPE_WAIST = true },
    [7]  = { INVTYPE_LEGS = true },
    [8]  = { INVTYPE_FEET = true },
    [9]  = { INVTYPE_WRIST = true },
    [10] = { INVTYPE_HAND = true },
    [11] = { INVTYPE_FINGER = true },
    [12] = { INVTYPE_FINGER = true },
    [13] = { INVTYPE_TRINKET = true },
    [14] = { INVTYPE_TRINKET = true },
    [15] = { INVTYPE_CLOAK = true },
    [16] = { INVTYPE_WEAPON = true, INVTYPE_2HWEAPON = true, INVTYPE_WEAPONMAINHAND = true },
    [17] = {
        INVTYPE_SHIELD = true, INVTYPE_WEAPONOFFHAND = true, INVTYPE_HOLDABLE = true,
        INVTYPE_WEAPON = true, INVTYPE_WEAPONMAINHAND = true
    },
    [18] = { INVTYPE_RANGED = true, INVTYPE_RANGEDRIGHT = true, INVTYPE_THROWN = true, INVTYPE_RELIC = true },
    [19] = { INVTYPE_TABARD = true },
}

local WEAPON_SLOT_EQUIP_LOCS = {
    INVTYPE_WEAPON = true,
    INVTYPE_2HWEAPON = true,
    INVTYPE_WEAPONMAINHAND = true,
    INVTYPE_WEAPONOFFHAND = true,
    INVTYPE_SHIELD = true,
    INVTYPE_HOLDABLE = true,
}

local swapPopup
local SWAP_POPUP_W = 235
local SWAP_ROW_H = 26
local SWAP_MAX_VISIBLE = 8
local SWAP_ICON_SIZE = 20

local function TryGetEquipLoc(link)
    if not link then return nil end
    local name, _, quality, _, _, _, _, equipLoc, tex = GetItemInfo(link)
    if equipLoc and equipLoc ~= "" then
        return name, quality, equipLoc, tex
    end
    local _, _, itemString = string.find(link, "(item:[%d:]+)")
    if itemString then
        name, _, quality, _, _, _, _, equipLoc, tex = GetItemInfo(itemString)
        if equipLoc and equipLoc ~= "" then
            return name, quality, equipLoc, tex
        end
    end
    local _, _, itemId = string.find(link, "item:(%d+)")
    if itemId then
        name, _, quality, _, _, _, _, equipLoc, tex = GetItemInfo(tonumber(itemId))
        if equipLoc and equipLoc ~= "" then
            return name, quality, equipLoc, tex
        end
    end
    return nil
end

local function ScanBagsForSlot(slotID)
    local locSet = SLOT_TO_EQUIP_LOCS[slotID]
    if not locSet then return {}, "no_mapping" end
    local items = {}
    local debugEquipLocs = {}
    local isWeaponSlot = (slotID == 16 or slotID == 17)
    for bag = 0, 4 do
        local numSlots = GetContainerNumSlots(bag)
        if numSlots and numSlots > 0 then
            for slot = 1, numSlots do
                local link = GetContainerItemLink(bag, slot)
                if link then
                    local name, quality, equipLoc, tex = TryGetEquipLoc(link)
                    if equipLoc then
                        debugEquipLocs[equipLoc] = (debugEquipLocs[equipLoc] or 0) + 1
                        local matched = locSet[equipLoc]
                        if (not matched) and isWeaponSlot and WEAPON_SLOT_EQUIP_LOCS[equipLoc] then
                            matched = true
                        end
                        if matched then
                            if not tex then tex = GetContainerItemInfo(bag, slot) end
                            if not quality then quality = GetItemQualityFromLink(link) end
                            local ilvl
                            if LibItem_Level then
                                local _, _, itemId = string.find(link, "item:(%d+)")
                                ilvl = itemId and LibItem_Level[tonumber(itemId)]
                            end
                            table.insert(items, {
                                bag = bag, slot = slot, link = link,
                                name = name or "?", texture = tex,
                                quality = quality or 1,
                                ilvl = ilvl,
                            })
                        end
                    end
                end
            end
        end
    end
    table.sort(items, function(a, b)
        if a.quality ~= b.quality then return a.quality > b.quality end
        if (a.ilvl or 0) ~= (b.ilvl or 0) then return (a.ilvl or 0) > (b.ilvl or 0) end
        return a.name < b.name
    end)
    return items, debugEquipLocs
end

local function GetOrCreateSwapPopup()
    if swapPopup then return swapPopup end

    local f = CreateFrame("Frame", "SFramesCPSwapPopup", UIParent)
    f:SetWidth(SWAP_POPUP_W)
    f:SetHeight(60)
    f:SetFrameStrata("TOOLTIP")
    f:EnableMouse(true)
    SetRoundBackdrop(f, T.bg, T.border)
    CreateShadow(f, 3)
    f:Hide()

    local title = MakeFS(f, 10, "CENTER", T.accentLight)
    title:SetPoint("TOP", f, "TOP", 0, -6)
    f.title = title

    local hint = f:CreateFontString(nil, "OVERLAY")
    hint:SetFont(GetFont(), 8, "OUTLINE")
    hint:SetTextColor(0.55, 0.55, 0.55)
    hint:SetText("")
    hint:SetPoint("BOTTOM", f, "BOTTOM", 0, 4)
    f.hint = hint

    f.rows = {}
    f.scrollOffset = 0
    f.totalItems = 0
    f.items = {}
    f.anchorSlot = nil

    f:EnableMouseWheel(1)
    f:SetScript("OnMouseWheel", function()
        local maxOff = math.max(f.totalItems - SWAP_MAX_VISIBLE, 0)
        if arg1 > 0 then
            f.scrollOffset = math.max(f.scrollOffset - 1, 0)
        else
            f.scrollOffset = math.min(f.scrollOffset + 1, maxOff)
        end
        CP:RefreshSwapRows()
    end)

    f:SetScript("OnUpdate", function()
        if not IsAltKeyDown() then
            f:Hide()
            return
        end
        if panel and not panel:IsShown() then
            f:Hide()
            return
        end
        local overPopup = MouseIsOver and MouseIsOver(f)
        local overSlot = f.anchorSlot and MouseIsOver and MouseIsOver(f.anchorSlot)
        if not overPopup and not overSlot then
            if not f._gracePeriod then
                f._gracePeriod = GetTime()
            elseif GetTime() - f._gracePeriod > 0.3 then
                f._gracePeriod = nil
                f:Hide()
            end
        else
            f._gracePeriod = nil
        end
    end)

    f:SetScript("OnLeave", function()
    end)

    swapPopup = f
    return f
end

function CP:RefreshSwapRows()
    local popup = swapPopup
    if not popup then return end
    local items = popup.items
    local offset = popup.scrollOffset
    local total = table.getn(items)
    local numVis = math.min(total - offset, SWAP_MAX_VISIBLE)

    for i = 1, table.getn(popup.rows) do
        local row = popup.rows[i]
        if i <= numVis then
            local item = items[offset + i]
            row.icon:SetTexture(item.texture)
            row.nameText:SetText(item.name)
            local qc = QUALITY_COLORS[item.quality] or QUALITY_COLORS[1]
            row.nameText:SetTextColor(qc[1], qc[2], qc[3])
            row.ilvlText:SetText(item.ilvl and tostring(item.ilvl) or "")
            row.itemBag = item.bag
            row.itemSlot = item.slot
            row.itemLink = item.link
            row:Show()
        else
            row:Hide()
        end
    end
end

function CP:ShowSwapPopup(slot)
    local items, debugInfo = ScanBagsForSlot(slot.slotID)
    local count = table.getn(items)

    local popup = GetOrCreateSwapPopup()
    popup.anchorSlot = slot
    popup.items = items
    popup.totalItems = count
    popup.scrollOffset = 0

    local label = SLOT_LABEL[slot.slotName] or slot.slotName

    for i = 1, table.getn(popup.rows) do popup.rows[i]:Hide() end

    if count == 0 then
        popup.title:SetText(label .. " - 背包中无可替换装备")
        popup:SetHeight(30)
        popup.hint:SetText("")
    else
        popup.title:SetText(label .. " - 可替换装备 (" .. count .. ")")

        local numVis = math.min(count, SWAP_MAX_VISIBLE)
        local hasScroll = count > SWAP_MAX_VISIBLE
        local hintH = hasScroll and 14 or 0
        popup:SetHeight(22 + numVis * SWAP_ROW_H + 6 + hintH)
        popup.hint:SetText(hasScroll and "滚轮翻页" or "")

        for i = 1, numVis do
            if not popup.rows[i] then
                local row = CreateFrame("Button", nil, popup)
                row:SetWidth(SWAP_POPUP_W - 12)
                row:SetHeight(SWAP_ROW_H)
                row:EnableMouse(true)

                local icon = row:CreateTexture(nil, "ARTWORK")
                icon:SetPoint("LEFT", row, "LEFT", 2, 0)
                icon:SetWidth(SWAP_ICON_SIZE)
                icon:SetHeight(SWAP_ICON_SIZE)
                icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
                row.icon = icon

                local nameText = row:CreateFontString(nil, "OVERLAY")
                nameText:SetFont(GetFont(), 10, "OUTLINE")
                nameText:SetPoint("LEFT", icon, "RIGHT", 4, 0)
                nameText:SetPoint("RIGHT", row, "RIGHT", -28, 0)
                nameText:SetJustifyH("LEFT")
                row.nameText = nameText

                local ilvlText = row:CreateFontString(nil, "OVERLAY")
                ilvlText:SetFont(GetFont(), 9, "OUTLINE")
                ilvlText:SetPoint("RIGHT", row, "RIGHT", -4, 0)
                ilvlText:SetJustifyH("RIGHT")
                ilvlText:SetTextColor(1, 0.82, 0)
                row.ilvlText = ilvlText

                local hl = row:CreateTexture(nil, "HIGHLIGHT")
                hl:SetTexture("Interface\\Buttons\\WHITE8X8")
                hl:SetVertexColor(T.accentLight[1], T.accentLight[2], T.accentLight[3], 0.12)
                hl:SetAllPoints(row)

                row:SetScript("OnClick", function()
                    local targetID = popup.anchorSlot and popup.anchorSlot.slotID
                    if targetID then
                        PickupContainerItem(this.itemBag, this.itemSlot)
                        PickupInventoryItem(targetID)
                    else
                        UseContainerItem(this.itemBag, this.itemSlot)
                    end
                    popup:Hide()
                    CP:ScheduleEquipUpdate()
                end)

                row:SetScript("OnEnter", function()
                    GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
                    GameTooltip:SetBagItem(this.itemBag, this.itemSlot)
                    GameTooltip:Show()
                end)

                row:SetScript("OnLeave", function()
                    GameTooltip:Hide()
                end)

                popup.rows[i] = row
            end
            popup.rows[i]:SetPoint("TOPLEFT", popup, "TOPLEFT", 6, -(20 + (i - 1) * SWAP_ROW_H))
        end

        for i = numVis + 1, table.getn(popup.rows) do
            popup.rows[i]:Hide()
        end

        self:RefreshSwapRows()
    end

    popup:ClearAllPoints()
    local slotLeft = slot:GetLeft() or 0
    local screenW = GetScreenWidth() or 1024
    if slotLeft + SLOT_SIZE + SWAP_POPUP_W + 10 > screenW then
        popup:SetPoint("TOPRIGHT", slot, "TOPLEFT", -4, 0)
    else
        popup:SetPoint("TOPLEFT", slot, "TOPRIGHT", 4, 0)
    end
    popup:Show()
end

function CP:HideSwapPopup()
    if swapPopup then swapPopup:Hide() end
end

function CP:CreateEquipSlot(parent, slotID, slotName)
    local frame = CreateFrame("Button", NextName("Slot"), parent)
    frame:SetWidth(SLOT_SIZE)
    frame:SetHeight(SLOT_SIZE)
    SetRoundBackdrop(frame, T.slotBg, T.slotBorder)

    local icon = frame:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("TOPLEFT", frame, "TOPLEFT", 4, -4)
    icon:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -4, 4)
    icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    frame.icon = icon

    local ilvlText = frame:CreateFontString(nil, "OVERLAY")
    ilvlText:SetFont(GetFont(), 8, "OUTLINE")
    ilvlText:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, 2)
    ilvlText:SetTextColor(1, 0.82, 0)
    ilvlText:SetText("")
    frame.ilvlText = ilvlText

    local glow = frame:CreateTexture(nil, "OVERLAY")
    glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
    glow:SetBlendMode("ADD")
    glow:SetAlpha(0.8)
    glow:SetWidth(SLOT_SIZE * 2)
    glow:SetHeight(SLOT_SIZE * 2)
    glow:SetPoint("CENTER", frame, "CENTER", 0, 0)
    glow:Hide()
    frame.qualGlow = glow

    frame.slotID = slotID
    frame.slotName = slotName
    local _, emptyTex = GetInventorySlotInfo(slotName)
    frame.emptyTexture = emptyTex

    frame:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    frame:RegisterForDrag("LeftButton")

    frame:SetScript("OnClick", function()
        local sid = this.slotID
        if CursorHasItem and CursorHasItem() then
            PickupInventoryItem(sid)
        elseif arg1 == "RightButton" then
            if GetInventoryItemLink("player", sid) then
                UseInventoryItem(sid)
            end
        elseif IsShiftKeyDown() and arg1 == "LeftButton" then
            local link = GetInventoryItemLink("player", sid)
            if link and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
                ChatFrameEditBox:Insert(link)
            end
        else
            PickupInventoryItem(sid)
        end
        CP:ScheduleEquipUpdate()
    end)

    frame:SetScript("OnDragStart", function()
        PickupInventoryItem(this.slotID)
        CP:ScheduleEquipUpdate()
    end)

    frame:SetScript("OnReceiveDrag", function()
        PickupInventoryItem(this.slotID)
        CP:ScheduleEquipUpdate()
    end)

    frame:SetScript("OnEnter", function()
        this._mouseOver = true
        this._swapActive = nil
        this:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4])
        if IsAltKeyDown() then
            this._swapActive = true
            GameTooltip:Hide()
            CP:ShowSwapPopup(this)
        else
            GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
            if GetInventoryItemLink("player", this.slotID) then
                GameTooltip:SetInventoryItem("player", this.slotID)
            else
                local label = SLOT_LABEL[this.slotName] or this.slotName
                GameTooltip:AddLine(label .. " - 未装备", 0.5, 0.5, 0.5)
            end
            GameTooltip:AddLine("[右键] 使用装备效果", 0.45, 0.45, 0.45)
            GameTooltip:AddLine("[Alt] 查看可替换装备", 0.45, 0.45, 0.45)
            GameTooltip:Show()
        end
    end)

    frame:SetScript("OnLeave", function()
        this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
        GameTooltip:Hide()
        this._mouseOver = nil
        this._swapActive = nil
    end)

    frame:SetScript("OnUpdate", function()
        if not this._mouseOver then return end
        if IsAltKeyDown() then
            if not this._swapActive then
                this._swapActive = true
                GameTooltip:Hide()
                CP:ShowSwapPopup(this)
            end
        else
            if this._swapActive then
                this._swapActive = nil
                if swapPopup then swapPopup:Hide() end
                GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
                if GetInventoryItemLink("player", this.slotID) then
                    GameTooltip:SetInventoryItem("player", this.slotID)
                else
                    local label = SLOT_LABEL[this.slotName] or this.slotName
                    GameTooltip:AddLine(label .. " - 未装备", 0.5, 0.5, 0.5)
                end
                GameTooltip:AddLine("[Alt] 查看可替换装备", 0.45, 0.45, 0.45)
                GameTooltip:Show()
            end
        end
    end)

    return frame
end

local equipUpdateTimer = CreateFrame("Frame", nil, UIParent)
equipUpdateTimer:Hide()
equipUpdateTimer._elapsed = 0
equipUpdateTimer._pending = 0
equipUpdateTimer:SetScript("OnUpdate", function()
    this._elapsed = this._elapsed + arg1
    if this._elapsed >= 0.15 then
        this._elapsed = 0
        this._pending = this._pending - 1
        CP:UpdateEquipment()
        if this._pending <= 0 then
            this._pending = 0
            this:Hide()
        end
    end
end)

function CP:ScheduleEquipUpdate()
    equipUpdateTimer._elapsed = 0
    equipUpdateTimer._pending = 3
    equipUpdateTimer:Show()
end

function CP:UpdateEquipment()
    local page = pages[1]
    if not page or not page.built then return end

    local showModel = not SFramesDB or SFramesDB.charShowModel ~= false
    if page.model then
        if showModel then
            page.model:Show()
            if page.modelFrame then page.modelFrame:Show() end
            if page.modelBgFrame then page.modelBgFrame:Show() end
            if page.modelToolbar then page.modelToolbar:Show() end
            page.model:SetUnit("player")
            page.model:SetFacing(page.model.curFacing or 0.4)
            page.model:SetModelScale(page.model.curScale or 1.0)
            page.model:SetPosition(page.model.posY or 0, 0, page.model.posX or 0)
        else
            page.model:Hide()
            if page.modelFrame then page.modelFrame:Hide() end
            if page.modelBgFrame then page.modelBgFrame:Hide() end
            if page.modelToolbar then page.modelToolbar:Hide() end
        end
    end
    local showGlow = not SFramesDB or SFramesDB.charShowQualityGlow ~= false

    local allSlots = {}
    for _, t in ipairs({ EQUIP_SLOTS_LEFT, EQUIP_SLOTS_RIGHT, EQUIP_SLOTS_BOTTOM }) do
        for _, s in ipairs(t) do table.insert(allSlots, s) end
    end

    for _, si in ipairs(allSlots) do
        local slot = page.equipSlots[si.id]
        if slot then
            local tex = GetInventoryItemTexture("player", si.id)
            local link = GetInventoryItemLink("player", si.id)
            if tex then
                slot.icon:SetTexture(tex)
                slot.icon:SetVertexColor(1, 1, 1)
                if slot.ilvlText then
                    local showIlvl = not SFramesDB or SFramesDB.showItemLevel ~= false
                    if showIlvl and link and LibItem_Level then
                        local _, _, itemId = string.find(link, "item:(%d+)")
                        local ilvl = itemId and LibItem_Level[tonumber(itemId)]
                        slot.ilvlText:SetText(ilvl and tostring(ilvl) or "")
                    else
                        slot.ilvlText:SetText("")
                    end
                end
                local quality = GetItemQualityFromLink(link)
                if quality and quality >= 2 and QUALITY_COLORS[quality] then
                    local qc = QUALITY_COLORS[quality]
                    if showGlow then
                        slot.qualGlow:SetVertexColor(qc[1], qc[2], qc[3])
                        slot.qualGlow:Show()
                    else
                        slot.qualGlow:Hide()
                    end
                else
                    slot.qualGlow:Hide()
                end
            else
                if slot.emptyTexture then
                    slot.icon:SetTexture(slot.emptyTexture)
                    slot.icon:SetVertexColor(T.emptySlot[1], T.emptySlot[2], T.emptySlot[3], T.emptySlot[4])
                else slot.icon:SetTexture(nil) end
                slot:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
                slot.currentBorderColor = nil
                slot.qualGlow:Hide()
                if slot.ilvlText then slot.ilvlText:SetText("") end
            end
        end
    end

    self:RefreshStatValues(page)
end

local function GetStatVal(key)
    if key == "meleeAP" then
        local b, p, n = UnitAttackPower("player"); return tostring(math.floor((b or 0) + (p or 0) + (n or 0)))
    elseif key == "meleeDmg" then
        local lo, hi = UnitDamage("player"); return math.floor(lo or 0) .. "-" .. math.floor(hi or 0)
    elseif key == "meleeSpeed" then
        local ms, os = UnitAttackSpeed("player")
        local t = string.format("%.1f", ms or 0)
        if os and os > 0 then t = t .. "/" .. string.format("%.1f", os) end; return t
    elseif key == "meleeCrit" then
        return string.format("%.1f%%", SafeGetMeleeCrit())
    elseif key == "meleeHit" then
        return string.format("%d%%", SafeGetMeleeHit())
    elseif key == "rangedAP" then
        local b, p, n = UnitRangedAttackPower("player"); return tostring(math.floor((b or 0) + (p or 0) + (n or 0)))
    elseif key == "rangedDmg" then
        local _, lo, hi = UnitRangedDamage("player"); return math.floor(lo or 0) .. "-" .. math.floor(hi or 0)
    elseif key == "rangedSpeed" then
        local sp = UnitRangedDamage("player"); return string.format("%.1f", sp or 0)
    elseif key == "rangedCrit" then
        return string.format("%.1f%%", SafeGetRangedCrit())
    elseif key == "spellDmg" then
        local mx = 0
        if GetSpellBonusDamage then for s = 2, 7 do local d = GetSpellBonusDamage(s); if d and d > mx then mx = d end end end
        if mx == 0 then
            local lib = GetItemBonusLib()
            if lib then
                local baseDmg = lib:GetBonus("DMG") or 0
                mx = baseDmg
                local schools = { "FIREDMG","FROSTDMG","SHADOWDMG","ARCANEDMG","NATUREDMG","HOLYDMG" }
                for _, sk in ipairs(schools) do
                    local sv = baseDmg + (lib:GetBonus(sk) or 0)
                    if sv > mx then mx = sv end
                end
            end
        end
        return tostring(math.floor(mx))
    elseif key == "spellHeal" then
        local v = GetSpellBonusHealing and GetSpellBonusHealing() or 0
        if v == 0 then v = GetGearBonus("HEAL") end
        return tostring(math.floor(v))
    elseif key == "spellCrit" then
        return string.format("%.1f%%", SafeGetSpellCrit())
    elseif key == "spellHit" then
        return string.format("%d%%", SafeGetSpellHit())
    elseif key == "armor" then
        local b, e = UnitArmor("player"); return tostring(math.floor(e or b or 0))
    elseif key == "defense" then
        local b, m = UnitDefense("player"); return tostring(math.floor((b or 0) + (m or 0)))
    elseif key == "dodge" then
        local v = GetDodgeChance and GetDodgeChance() or 0
        if v == 0 then v = GetGearBonus("DODGE") end
        return string.format("%.1f%%", v)
    elseif key == "parry" then
        local v = GetParryChance and GetParryChance() or 0
        if v == 0 then v = GetGearBonus("PARRY") end
        return string.format("%.1f%%", v)
    elseif key == "block" then
        local v = GetBlockChance and GetBlockChance() or 0
        if v == 0 then v = GetGearBonus("BLOCK") end
        return string.format("%.1f%%", v)
    end
    return "0"
end

function CP:RefreshStatValues(page)
    if not page.built then return end
    CP:RefreshStatCatVisual(page)

    -- Cat 1: base (left) + resist (right)
    if page.cat1Rows then
        for _, row in ipairs(page.cat1Rows) do
            local stat, eff, pos, neg = UnitStat("player", row.baseIdx)
            local e = eff or stat or 0
            row.valueL:SetText(tostring(math.floor(e)))
            if row.valueR and row.resistSchool then
                local b, total = UnitResistance("player", row.resistSchool)
                row.valueR:SetText(tostring(math.floor(total or b or 0)))
            end
        end
    end

    -- Cat 2: melee (left) + ranged (right)
    if page.cat2Rows then
        for _, row in ipairs(page.cat2Rows) do
            if row.meleeKey then row.valueL:SetText(GetStatVal(row.meleeKey)) end
            if row.rangedKey and row.valueR then row.valueR:SetText(GetStatVal(row.rangedKey)) end
        end
    end

    -- Cat 3: spell (left) + defense (right)
    if page.cat3Rows then
        for _, row in ipairs(page.cat3Rows) do
            if row.spellKey and row.valueL then row.valueL:SetText(GetStatVal(row.spellKey)) end
            if row.defKey and row.valueR then row.valueR:SetText(GetStatVal(row.defKey)) end
        end
    end
end

--------------------------------------------------------------------------------
-- Tab 2: Reputation (was Tab 3)
--------------------------------------------------------------------------------
function CP:BuildReputationPage()
    local page = pages[2]
    if page.built then return end
    page.built = true
    local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4
    local scrollArea = CreateScrollFrame(page, CONTENT_W, contentH)
    scrollArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
    page.scrollArea = scrollArea
    page.repRows = {}
end

function CP:UpdateReputation()
    local page = pages[2]
    if not page or not page.built then return end
    local child = page.scrollArea.child
    if page.repRows then
        for _, row in ipairs(page.repRows) do if row.frame then row.frame:Hide() end end
    end
    page.repRows = {}

    local numFactions = GetNumFactions()
    local y = -6
    local rowH, barH, headerH = 30, 9, 24

    for i = 1, numFactions do
        local name, desc, standingId, barMin, barMax, barValue, atWar, canWar, isHeader, isCollapsed, hasRep, isWatched, isChild
        if GetFactionInfo then
            name, desc, standingId, barMin, barMax, barValue, atWar, canWar, isHeader, isCollapsed, hasRep, isWatched, isChild = GetFactionInfo(i)
        end
        if not name then break end

        if isHeader then
            local hf = CreateFrame("Button", nil, child)
            hf:SetWidth(SCROLL_W - 16)
            hf:SetHeight(headerH)
            hf:SetPoint("TOPLEFT", child, "TOPLEFT", 8, y)
            local arrow = MakeFS(hf, 10, "LEFT", T.dimText)
            arrow:SetPoint("LEFT", hf, "LEFT", 0, 0)
            arrow:SetText(isCollapsed and "+" or "-")
            local ht = MakeFS(hf, 11, "LEFT", T.sectionTitle)
            ht:SetPoint("LEFT", arrow, "RIGHT", 3, 0)
            ht:SetText(name)
            hf.factionIndex = i
            hf:SetScript("OnClick", function()
                if isCollapsed then ExpandFactionHeader(this.factionIndex) else CollapseFactionHeader(this.factionIndex) end
                CP:UpdateReputation()
            end)
            table.insert(page.repRows, { frame = hf })
            y = y - headerH
        else
            local rf = CreateFrame("Frame", nil, child)
            rf:SetWidth(SCROLL_W - 24)
            rf:SetHeight(rowH)
            rf:SetPoint("TOPLEFT", child, "TOPLEFT", isChild and 22 or 14, y)
            local nfs = MakeFS(rf, 9, "LEFT", T.valueText)
            nfs:SetPoint("TOPLEFT", rf, "TOPLEFT", 0, -2)
            nfs:SetText(name or "")
            local rc = T.repColors[standingId] or { 0.5, 0.5, 0.5 }
            local sfs = MakeFS(rf, 8, "RIGHT", rc)
            sfs:SetPoint("TOPRIGHT", rf, "TOPRIGHT", 0, -2)
            sfs:SetText(REP_STANDING[standingId] or "")

            local bf = CreateFrame("Frame", nil, rf)
            bf:SetHeight(barH)
            bf:SetPoint("BOTTOMLEFT", rf, "BOTTOMLEFT", 0, 2)
            bf:SetPoint("BOTTOMRIGHT", rf, "BOTTOMRIGHT", 0, 2)
            SetPixelBackdrop(bf, T.barBg, { 0.15, 0.15, 0.18, 0.5 })
            local bar = bf:CreateTexture(nil, "ARTWORK")
            bar:SetTexture(SFrames:GetTexture())
            bar:SetVertexColor(rc[1], rc[2], rc[3], 0.85)
            bar:SetPoint("TOPLEFT", bf, "TOPLEFT", 1, -1)
            bar:SetPoint("BOTTOMLEFT", bf, "BOTTOMLEFT", 1, 1)
            local range = math.max((barMax or 1) - (barMin or 0), 1)
            local fill = Clamp(((barValue or 0) - (barMin or 0)) / range, 0, 1)
            bar:SetWidth(math.max((SCROLL_W - 26) * fill, 1))
            local vfs = MakeFS(bf, 7, "CENTER", { 0.85, 0.85, 0.85 })
            vfs:SetPoint("CENTER", bf, "CENTER", 0, 0)
            vfs:SetText(math.floor((barValue or 0) - (barMin or 0)) .. "/" .. math.floor(range))
            table.insert(page.repRows, { frame = rf })
            y = y - rowH
        end
    end
    page.scrollArea:SetContentHeight(math.abs(y) + 16)
end

--------------------------------------------------------------------------------
-- Tab 3: Skills (was Tab 4)
--------------------------------------------------------------------------------
function CP:BuildSkillsPage()
    local page = pages[3]
    if page.built then return end
    page.built = true
    local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4
    local scrollArea = CreateScrollFrame(page, CONTENT_W, contentH)
    scrollArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
    page.scrollArea = scrollArea
    page.skillRows = {}
end

do
    local TRADE_HEADERS = { ["Trade Skills"] = true, ["专业技能"] = true, ["Professions"] = true,
                            ["商业技能"] = true, ["专业"] = true }
    local pendingAbandonIndex

    StaticPopupDialogs = StaticPopupDialogs or {}
    StaticPopupDialogs["NANAMI_ABANDON_SKILL"] = {
        text = "确定要遗弃 %s 吗？\n该操作不可撤销！",
        button1 = "确定",
        button2 = "取消",
        OnAccept = function()
            if pendingAbandonIndex and AbandonSkill then
                AbandonSkill(pendingAbandonIndex)
                pendingAbandonIndex = nil
                CP:UpdateSkills()
            end
        end,
        OnCancel = function() pendingAbandonIndex = nil end,
        timeout = 0, whileDead = true, hideOnEscape = true,
    }

    function CP:UpdateSkills()
        local page = pages[3]
        if not page or not page.built then return end
        local child = page.scrollArea.child
        if page.skillRows then
            for _, row in ipairs(page.skillRows) do if row.frame then row.frame:Hide() end end
        end
        page.skillRows = {}

        local numSkills = GetNumSkillLines and GetNumSkillLines() or 0
        local y = -6
        local rowH, barH, headerH = 28, 7, 24
        local currentHeader = ""
        local delBtnSize = 14

        for i = 1, numSkills do
            local sn, isH, isE, sr, nt, sm, smr, isAbandonable
            if GetSkillLineInfo then sn, isH, isE, sr, nt, sm, smr, isAbandonable = GetSkillLineInfo(i) end
            if not sn then break end

            if isH then
                currentHeader = sn or ""
                local hf = CreateFrame("Button", nil, child)
                hf:SetWidth(SCROLL_W - 16)
                hf:SetHeight(headerH)
                hf:SetPoint("TOPLEFT", child, "TOPLEFT", 8, y)
                local arrow = MakeFS(hf, 10, "LEFT", T.dimText)
                arrow:SetPoint("LEFT", hf, "LEFT", 0, 0)
                arrow:SetText(isE and "-" or "+")
                local ht = MakeFS(hf, 11, "LEFT", T.sectionTitle)
                ht:SetPoint("LEFT", arrow, "RIGHT", 3, 0)
                ht:SetText(sn)
                hf.skillIndex = i
                hf:SetScript("OnClick", function()
                    if isE then CollapseSkillHeader(this.skillIndex) else ExpandSkillHeader(this.skillIndex) end
                    CP:UpdateSkills()
                end)
                table.insert(page.skillRows, { frame = hf })
                y = y - headerH
            else
                local canAbandon = TRADE_HEADERS[currentHeader]
                local rightPad = canAbandon and (delBtnSize + 6) or 0

                local sf = CreateFrame("Frame", nil, child)
                sf:SetWidth(SCROLL_W - 24)
                sf:SetHeight(rowH)
                sf:SetPoint("TOPLEFT", child, "TOPLEFT", 18, y)
                local nfs = MakeFS(sf, 9, "LEFT", T.valueText)
                nfs:SetPoint("TOPLEFT", sf, "TOPLEFT", 0, -2)
                nfs:SetText(sn or "")
                local rt = tostring(sr or 0)
                if smr and smr > 0 then rt = rt .. "/" .. tostring(smr) end
                local rfs = MakeFS(sf, 8, "RIGHT", T.dimText)
                rfs:SetPoint("TOPRIGHT", sf, "TOPRIGHT", -rightPad, -2)
                rfs:SetText(rt)
                if smr and smr > 0 then
                    local bf = CreateFrame("Frame", nil, sf)
                    bf:SetHeight(barH)
                    bf:SetPoint("BOTTOMLEFT", sf, "BOTTOMLEFT", 0, 2)
                    bf:SetPoint("BOTTOMRIGHT", sf, "BOTTOMRIGHT", -rightPad, 2)
                    SetPixelBackdrop(bf, T.barBg, { 0.15, 0.15, 0.18, 0.5 })
                    local bar = bf:CreateTexture(nil, "ARTWORK")
                    bar:SetTexture(SFrames:GetTexture())
                    bar:SetVertexColor(0.4, 0.65, 0.85, 0.85)
                    bar:SetPoint("TOPLEFT", bf, "TOPLEFT", 1, -1)
                    bar:SetPoint("BOTTOMLEFT", bf, "BOTTOMLEFT", 1, 1)
                    bar:SetWidth(math.max((SCROLL_W - 26 - rightPad) * Clamp((sr or 0) / smr, 0, 1), 1))
                end

                if canAbandon then
                    local db = CreateFrame("Button", nil, sf)
                    db:SetWidth(delBtnSize)
                    db:SetHeight(delBtnSize)
                    db:SetPoint("RIGHT", sf, "RIGHT", 0, 0)
                    db:SetFrameLevel(sf:GetFrameLevel() + 2)
                    SetPixelBackdrop(db, { 0.25, 0.08, 0.08, 0.7 }, { 0.5, 0.15, 0.15, 0.6 })
                    local ico = SFrames:CreateIcon(db, "close", 8)
                    ico:SetDrawLayer("OVERLAY")
                    ico:SetPoint("CENTER", db, "CENTER", 0, 0)
                    ico:SetVertexColor(0.9, 0.4, 0.4)
                    db.skillIndex = i
                    db.skillName = sn
                    db:SetScript("OnClick", function()
                        pendingAbandonIndex = this.skillIndex
                        if StaticPopup_Show then
                            StaticPopup_Show("NANAMI_ABANDON_SKILL", this.skillName)
                        end
                    end)
                    db:SetScript("OnEnter", function()
                        this:SetBackdropColor(0.45, 0.1, 0.1, 0.9)
                        this:SetBackdropBorderColor(0.8, 0.2, 0.2, 0.9)
                        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
                        GameTooltip:AddLine("遗弃 " .. (this.skillName or ""), 1, 0.4, 0.4)
                        GameTooltip:AddLine("点击遗弃该技能", 0.7, 0.7, 0.7)
                        GameTooltip:Show()
                    end)
                    db:SetScript("OnLeave", function()
                        this:SetBackdropColor(0.25, 0.08, 0.08, 0.7)
                        this:SetBackdropBorderColor(0.5, 0.15, 0.15, 0.6)
                        GameTooltip:Hide()
                    end)
                end

                if BEAST_TRAINING_NAMES[sn] then
                    local tp = 0
                    if GetPetTrainingPoints then
                        local ok2, total, spent = pcall(GetPetTrainingPoints)
                        if ok2 then tp = (total or 0) - (spent or 0) end
                    end
                    local tpFs = MakeFS(sf, 8, "LEFT", { 0.55, 0.85, 0.4 })
                    tpFs:SetPoint("TOPLEFT", nfs, "BOTTOMLEFT", 0, -1)
                    tpFs:SetText("可用训练点数: " .. tostring(tp))
                    sf:EnableMouse(true)
                    sf.skillName = sn
                    sf:SetScript("OnEnter", function()
                        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
                        GameTooltip:AddLine(this.skillName or "Beast Training", 1, 0.82, 0)
                        GameTooltip:AddLine("打开训练野兽窗口，教授宠物技能", 0.7, 0.7, 0.7)
                        GameTooltip:AddLine("消耗训练点数来教授宠物各种技能", 0.7, 0.7, 0.7)
                        GameTooltip:Show()
                    end)
                    sf:SetScript("OnLeave", function() GameTooltip:Hide() end)
                    sf:SetHeight(rowH + 12)
                    y = y - 12
                end

                table.insert(page.skillRows, { frame = sf })
                y = y - rowH
            end
        end
        page.scrollArea:SetContentHeight(math.abs(y) + 16)
    end
end

--------------------------------------------------------------------------------
-- Tab 4: Honor (was Tab 5)
--------------------------------------------------------------------------------
function CP:BuildHonorPage()
    local page = pages[4]
    if page.built then return end
    page.built = true

    local pad = SIDE_PAD + 2
    page.rankIcon = page:CreateTexture(nil, "ARTWORK")
    page.rankIcon:SetWidth(42)
    page.rankIcon:SetHeight(42)
    page.rankIcon:SetPoint("TOPLEFT", page, "TOPLEFT", pad, -16)

    page.rankName = MakeFS(page, 13, "LEFT", T.gold)
    page.rankName:SetPoint("LEFT", page.rankIcon, "RIGHT", 10, 5)
    page.rankProgress = MakeFS(page, 10, "LEFT", T.dimText)
    page.rankProgress:SetPoint("LEFT", page.rankIcon, "RIGHT", 10, -8)

    local barY = -68
    local bf = CreateFrame("Frame", nil, page)
    bf:SetHeight(12)
    bf:SetPoint("TOPLEFT", page, "TOPLEFT", pad, barY)
    bf:SetPoint("TOPRIGHT", page, "TOPRIGHT", -pad, barY)
    SetPixelBackdrop(bf, T.barBg, { 0.15, 0.15, 0.18, 0.5 })
    page.rankBarFrame = bf

    local bfill = bf:CreateTexture(nil, "ARTWORK")
    bfill:SetTexture(SFrames:GetTexture())
    bfill:SetVertexColor(0.85, 0.6, 0.1, 0.9)
    bfill:SetPoint("TOPLEFT", bf, "TOPLEFT", 1, -1)
    bfill:SetPoint("BOTTOMLEFT", bf, "BOTTOMLEFT", 1, 1)
    bfill:SetWidth(1)
    page.rankBarFill = bfill

    page.rankBarText = MakeFS(bf, 8, "CENTER", { 1, 1, 1 })
    page.rankBarText:SetPoint("CENTER", bf, "CENTER", 0, 0)

    local sY = barY - 26
    sY = self:CreateStatSection(page, "荣誉统计", sY)
    page.honorStats = {}
    local hl = { "本次击杀", "本次荣誉", "昨日击杀", "昨日荣誉", "本周击杀", "上周击杀", "上周荣誉", "上周排名", "终身击杀" }
    for _, lbl in ipairs(hl) do
        local row; row, sY = self:CreateStatRow(page, lbl, sY)
        table.insert(page.honorStats, { row = row })
    end
end

function CP:UpdateHonor()
    local page = pages[4]
    if not page or not page.built then return end

    local rn, rnum
    if GetPVPRankInfo then rn, rnum = GetPVPRankInfo(UnitPVPRank("player")) end
    rn = rn or "无军衔"; rnum = rnum or 0
    page.rankName:SetText(rn)
    if rnum > 0 then
        page.rankIcon:SetTexture(string.format("Interface\\PvPRankBadges\\PvPRank%02d", rnum))
    else page.rankIcon:SetTexture(nil) end

    local prog = GetPVPRankProgress and GetPVPRankProgress() or 0
    page.rankProgress:SetText(string.format("进度: %.1f%%", prog * 100))
    local bw = page.rankBarFrame:GetWidth() - 2
    page.rankBarFill:SetWidth(math.max(bw * prog, 1))
    page.rankBarText:SetText(string.format("%.1f%%", prog * 100))

    local thk, tho = 0, 0; if GetPVPSessionStats then thk, tho = GetPVPSessionStats() end
    local yhk, yho = 0, 0; if GetPVPYesterdayStats then yhk, yho = GetPVPYesterdayStats() end
    local whk = 0; if GetPVPThisWeekStats then whk = GetPVPThisWeekStats() end
    local lhk, lho, ls = 0, 0, 0; if GetPVPLastWeekStats then lhk, lho, ls = GetPVPLastWeekStats() end
    local ltk = 0; if GetPVPLifetimeStats then ltk = GetPVPLifetimeStats() end

    local vals = {
        tostring(thk or 0), tostring(math.floor(tho or 0)),
        tostring(yhk or 0), tostring(math.floor(yho or 0)),
        tostring(whk or 0), tostring(lhk or 0),
        tostring(math.floor(lho or 0)), tostring(ls or 0), tostring(ltk or 0),
    }
    for i, stat in ipairs(page.honorStats) do
        stat.row.value:SetText(vals[i] or "0")
    end
end

--------------------------------------------------------------------------------
-- Tab 5: Pet (Hunter)
--------------------------------------------------------------------------------
function CP:BuildPetPage()
    if not PET_TAB_INDEX then return end
    local page = pages[PET_TAB_INDEX]
    if not page or page.built then return end
    page.built = true

    local cw = CONTENT_W
    local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4
    local pad = 8

    local pc = CreateFrame("Frame", nil, page)
    pc:SetAllPoints(page)
    page.petContent = pc

    page.noPetText = MakeFS(page, 12, "CENTER", T.dimText)
    page.noPetText:SetPoint("CENTER", page, "CENTER", 0, 0)
    page.noPetText:SetText("当前没有宠物")
    page.noPetText:Hide()

    -- 3D Model
    local modelH = 180
    local modelW = cw - 8
    local modelBg = CreateFrame("Frame", nil, pc)
    modelBg:SetWidth(modelW)
    modelBg:SetHeight(modelH)
    modelBg:SetPoint("TOP", pc, "TOP", 0, -2)
    SetRoundBackdrop(modelBg, T.modelBg, T.modelBorder)
    page.modelBgFrame = modelBg

    local modelFrame = CreateFrame("Frame", nil, pc)
    modelFrame:SetWidth(modelW - 8)
    modelFrame:SetHeight(modelH - 8)
    modelFrame:SetPoint("CENTER", modelBg, "CENTER", 0, 0)
    modelFrame:SetFrameLevel(pc:GetFrameLevel() + 5)

    local model = CreateFrame("PlayerModel", NextName("PetModel"), modelFrame)
    model:SetAllPoints(modelFrame)
    page.model = model
    page.modelFrame = modelFrame

    model:EnableMouse(true)
    model:EnableMouseWheel(1)
    model.rotating = false
    model.curFacing = 0.4
    model.curScale = 0.55
    model.posX = 0
    model.posY = -0.5

    model:SetScript("OnMouseDown", function()
        if arg1 == "LeftButton" then
            this.rotating = true
            this.startX = GetCursorPosition()
            this.startFacing = this.curFacing or 0
        elseif arg1 == "RightButton" then
            this.panning = true
            local cx, cy = GetCursorPosition()
            this.panStartX = cx
            this.panStartY = cy
            this.panOriginX = this.posX or 0
            this.panOriginY = this.posY or 0
        end
    end)
    model:SetScript("OnMouseUp", function()
        if arg1 == "LeftButton" then this.rotating = false
        elseif arg1 == "RightButton" then this.panning = false end
    end)
    model:SetScript("OnMouseWheel", function()
        local ns = (this.curScale or 1) + arg1 * 0.1
        if ns < 0.3 then ns = 0.3 end
        if ns > 3.0 then ns = 3.0 end
        this.curScale = ns
        this:SetModelScale(ns)
    end)
    model:SetScript("OnUpdate", function()
        if this.rotating then
            local cx = GetCursorPosition()
            local diff = (cx - (this.startX or cx)) * 0.01
            this.curFacing = (this.startFacing or 0) + diff
            this:SetFacing(this.curFacing)
        elseif this.panning then
            local cx, cy = GetCursorPosition()
            local es = this:GetEffectiveScale()
            if es < 0.01 then es = 1 end
            local dx = (cx - (this.panStartX or cx)) / (es * 35)
            local dy = (cy - (this.panStartY or cy)) / (es * 35)
            this.posX = (this.panOriginX or 0) + dx
            this.posY = (this.panOriginY or 0) + dy
            this:SetPosition(this.posY, 0, this.posX)
        end
    end)

    -- Name overlay at bottom of model
    page.petNameText = MakeFS(pc, 12, "LEFT", T.gold)
    page.petNameText:SetPoint("BOTTOMLEFT", modelBg, "BOTTOMLEFT", 8, 4)
    page.petFamilyText = MakeFS(pc, 9, "RIGHT", T.dimText)
    page.petFamilyText:SetPoint("BOTTOMRIGHT", modelBg, "BOTTOMRIGHT", -8, 4)

    -- Scrollable stats area below model
    local statsTop = -(modelH + 6)
    local statsH = contentH - modelH - 6
    local scrollArea = CreateScrollFrame(pc, cw, statsH)
    scrollArea:SetPoint("TOPLEFT", pc, "TOPLEFT", 0, statsTop)
    page.scrollArea = scrollArea
    local child = scrollArea.child

    local sY = -4

    -- Info line: happiness, loyalty, training points
    page.happyLabel = MakeFS(child, 9, "LEFT", T.labelText)
    page.happyLabel:SetPoint("TOPLEFT", child, "TOPLEFT", pad, sY)
    page.happyLabel:SetText("心情:")
    page.happyValue = MakeFS(child, 9, "LEFT", T.valueText)
    page.happyValue:SetPoint("LEFT", page.happyLabel, "RIGHT", 2, 0)

    page.loyalLabel = MakeFS(child, 9, "LEFT", T.labelText)
    page.loyalLabel:SetPoint("LEFT", page.happyValue, "RIGHT", 10, 0)
    page.loyalLabel:SetText("忠诚度:")
    page.loyalValue = MakeFS(child, 9, "LEFT", T.valueText)
    page.loyalValue:SetPoint("LEFT", page.loyalLabel, "RIGHT", 2, 0)

    page.tpLabel = MakeFS(child, 9, "LEFT", T.labelText)
    page.tpLabel:SetPoint("LEFT", page.loyalValue, "RIGHT", 10, 0)
    page.tpLabel:SetText("训练点:")
    page.tpValue = MakeFS(child, 9, "LEFT", { 0.55, 0.85, 0.4 })
    page.tpValue:SetPoint("LEFT", page.tpLabel, "RIGHT", 2, 0)
    sY = sY - 16

    -- XP bar
    page.xpSectionLabel = MakeFS(child, 9, "LEFT", T.sectionTitle)
    page.xpSectionLabel:SetPoint("TOPLEFT", child, "TOPLEFT", pad, sY)
    page.xpSectionLabel:SetText("经验值")
    sY = sY - 12

    local xpBf = CreateFrame("Frame", nil, child)
    xpBf:SetHeight(8)
    xpBf:SetPoint("TOPLEFT", child, "TOPLEFT", pad, sY)
    xpBf:SetPoint("TOPRIGHT", child, "TOPRIGHT", -pad, sY)
    SetPixelBackdrop(xpBf, T.barBg, { 0.15, 0.15, 0.18, 0.5 })
    page.xpBarFrame = xpBf

    local xpFill = xpBf:CreateTexture(nil, "ARTWORK")
    xpFill:SetTexture(SFrames:GetTexture())
    xpFill:SetVertexColor(0.4, 0.65, 0.85, 0.9)
    xpFill:SetPoint("TOPLEFT", xpBf, "TOPLEFT", 1, -1)
    xpFill:SetPoint("BOTTOMLEFT", xpBf, "BOTTOMLEFT", 1, 1)
    xpFill:SetWidth(1)
    page.xpBarFill = xpFill

    page.xpBarText = MakeFS(xpBf, 7, "CENTER", { 1, 1, 1 })
    page.xpBarText:SetPoint("CENTER", xpBf, "CENTER", 0, 0)
    sY = sY - 14

    -- Stats dual-column section
    sY = self:CreateStatSection(child, "属性与攻防", sY)

    local leftLabels = { "力量", "敏捷", "耐力", "智力", "精神" }
    local rightLabels = { "攻击", "强度", "伤害", "防御", "护甲" }
    page.petStatLeft = {}
    page.petStatRight = {}

    for idx = 1, 5 do
        local row1 = {}
        row1.label = MakeFS(child, 9, "LEFT", T.labelText)
        row1.label:SetPoint("TOPLEFT", child, "TOPLEFT", 14, sY)
        row1.label:SetText(leftLabels[idx] .. ":")
        row1.value = MakeFS(child, 9, "RIGHT", T.valueText)
        row1.value:SetPoint("TOPLEFT", child, "TOPLEFT", 56, sY)
        row1.value:SetWidth(80)
        row1.value:SetJustifyH("RIGHT")
        table.insert(page.petStatLeft, row1)

        local row2 = {}
        row2.label = MakeFS(child, 9, "LEFT", T.labelText)
        row2.label:SetPoint("TOPLEFT", child, "TOPLEFT", 160, sY)
        row2.label:SetText(rightLabels[idx] .. ":")
        row2.value = MakeFS(child, 9, "RIGHT", T.valueText)
        row2.value:SetPoint("TOPRIGHT", child, "TOPRIGHT", -14, sY)
        row2.value:SetWidth(80)
        row2.value:SetJustifyH("RIGHT")
        table.insert(page.petStatRight, row2)

        sY = sY - 14
    end

    -- Resistances
    sY = sY - 4
    sY = self:CreateStatSection(child, "抗性", sY)
    page.resStats = {}
    local resSchools = { 2, 3, 4, 5, 6 }
    local resPerRow = 3
    local resColW = math.floor((cw - 28) / resPerRow)

    for idx = 1, 5 do
        local col = math.mod(idx - 1, resPerRow)
        local rowOff = math.floor((idx - 1) / resPerRow)
        local rx = 14 + col * resColW
        local ry = sY - rowOff * 14
        local row = {}
        local school = resSchools[idx]
        local rc = T.resistColors[school] or T.labelText
        row.label = MakeFS(child, 9, "LEFT", rc)
        row.label:SetPoint("TOPLEFT", child, "TOPLEFT", rx, ry)
        row.label:SetText(RESIST_NAMES[school] .. ":")
        row.value = MakeFS(child, 9, "LEFT", T.valueText)
        row.value:SetPoint("LEFT", row.label, "RIGHT", 2, 0)
        row.school = school
        table.insert(page.resStats, row)
    end
    sY = sY - math.ceil(5 / resPerRow) * 14

    -- Food (manually created so we can show/hide for warlock)
    sY = sY - 4
    local foodHeader = MakeFS(child, 11, "LEFT", T.sectionTitle)
    foodHeader:SetPoint("TOPLEFT", child, "TOPLEFT", 8, sY)
    foodHeader:SetText("喜好食物")
    local foodSep = child:CreateTexture(nil, "ARTWORK")
    foodSep:SetTexture("Interface\\Buttons\\WHITE8X8")
    foodSep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
    foodSep:SetHeight(1)
    foodSep:SetPoint("TOPLEFT", child, "TOPLEFT", 8, sY - 14)
    foodSep:SetPoint("TOPRIGHT", child, "TOPRIGHT", -8, sY - 14)
    sY = sY - 18
    page.foodHeader = foodHeader
    page.foodSep = foodSep
    page.foodText = MakeFS(child, 9, "LEFT", T.valueText)
    page.foodText:SetPoint("TOPLEFT", child, "TOPLEFT", 14, sY)
    page.foodText:SetWidth(cw - 28)
    sY = sY - 16

    page.fullContentH = math.abs(sY) + 8
    scrollArea:SetContentHeight(page.fullContentH)
end

function CP:UpdatePet()
    if not PET_TAB_INDEX then return end
    local page = pages[PET_TAB_INDEX]
    if not page or not page.built then return end

    local hasPetUI = HasPetUI and HasPetUI()
    local hasPet = UnitExists("pet") and hasPetUI

    if not hasPet then
        page.petContent:Hide()
        page.noPetText:Show()
        return
    end

    page.petContent:Show()
    page.noPetText:Hide()

    -- Detect hunter pet vs warlock demon
    local _, isHunterPet = HasPetUI()

    -- Set 3D model (only reload when pet identity changes to avoid animation reset)
    if page.model then
        local petKey = (UnitName("pet") or "") .. ":" .. (UnitLevel("pet") or 0)
        if page.model.lastPetKey ~= petKey then
            page.model.lastPetKey = petKey
            page.model.curFacing = 0.4
            page.model.curScale = 0.55
            page.model.posX = 0
            page.model.posY = -0.5
            page.model:SetUnit("pet")
            page.model:SetFacing(0.4)
            page.model:SetModelScale(0.55)
            page.model:SetPosition(-0.5, 0, 0)
        end
    end

    local petName = UnitName("pet") or "未知"
    local petLevel = UnitLevel("pet") or 0
    page.petNameText:SetText(petName .. "  |cff88bbddLv." .. petLevel .. "|r")

    local family = ""
    if UnitCreatureFamily then
        local ok, val = pcall(UnitCreatureFamily, "pet")
        if ok and val then family = val end
    end
    page.petFamilyText:SetText(family)

    -- Hunter-only: happiness, loyalty, training points, XP, food
    if isHunterPet then
        page.happyLabel:Show(); page.happyValue:Show()
        page.loyalLabel:Show(); page.loyalValue:Show()
        page.tpLabel:Show(); page.tpValue:Show()
        if page.xpSectionLabel then page.xpSectionLabel:Show() end
        page.xpBarFrame:Show()
        if page.foodHeader then page.foodHeader:Show() end
        if page.foodSep then page.foodSep:Show() end
        page.foodText:Show()

        local happiness = 0
        if GetPetHappiness then
            local ok, val = pcall(GetPetHappiness)
            if ok and val then happiness = val end
        end
        local hData = PET_HAPPINESS[happiness]
        if hData then
            page.happyValue:SetText(hData.text)
            page.happyValue:SetTextColor(hData.color[1], hData.color[2], hData.color[3])
        else
            page.happyValue:SetText("--")
            page.happyValue:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
        end

        local loyalty = "--"
        if GetPetLoyalty then
            local ok, val = pcall(GetPetLoyalty)
            if ok and val then loyalty = val end
        end
        page.loyalValue:SetText(tostring(loyalty))

        local tp = 0
        if GetPetTrainingPoints then
            local ok, total, spent = pcall(GetPetTrainingPoints)
            if ok then tp = (total or 0) - (spent or 0) end
        end
        page.tpValue:SetText(tostring(tp))

        local curXP, maxXP = 0, 1
        if GetPetExperience then
            local ok, cx, mx = pcall(GetPetExperience)
            if ok then curXP = cx or 0; maxXP = mx or 1 end
        end
        if maxXP == 0 then maxXP = 1 end
        local xpPct = curXP / maxXP
        local bw = page.xpBarFrame:GetWidth() - 2
        if bw < 1 then bw = 1 end
        page.xpBarFill:SetWidth(math.max(bw * xpPct, 1))
        page.xpBarText:SetText(curXP .. " / " .. maxXP)
    else
        page.happyLabel:Hide(); page.happyValue:Hide()
        page.loyalLabel:Hide(); page.loyalValue:Hide()
        page.tpLabel:Hide(); page.tpValue:Hide()
        if page.xpSectionLabel then page.xpSectionLabel:Hide() end
        page.xpBarFrame:Hide()
        if page.foodHeader then page.foodHeader:Hide() end
        if page.foodSep then page.foodSep:Hide() end
        page.foodText:Hide()
    end

    -- Base stats (UnitStat returns base, effective in vanilla; use first non-nil)
    local statLabels = { "力量", "敏捷", "耐力", "智力", "精神" }
    for i, r in ipairs(page.petStatLeft) do
        local base, eff = UnitStat("pet", i)
        local val = eff or base or 0
        r.label:SetText(statLabels[i] .. ":")
        r.value:SetText(tostring(val))
    end

    -- Combat stats
    local mainBase, mainMod = 0, 0
    if UnitAttack then
        local ok, b, m = pcall(UnitAttack, "pet")
        if ok then mainBase = b or 0; mainMod = m or 0 end
    end
    local apBase, apPos, apNeg = 0, 0, 0
    if UnitAttackPower then
        local ok, b, p, n = pcall(UnitAttackPower, "pet")
        if ok then apBase = b or 0; apPos = p or 0; apNeg = n or 0 end
    end
    local ap = apBase + apPos + apNeg
    local minDmg, maxDmg = 0, 0
    if UnitDamage then
        local ok, d1, d2 = pcall(UnitDamage, "pet")
        if ok then minDmg = d1 or 0; maxDmg = d2 or 0 end
    end
    local defBase, defMod = 0, 0
    if UnitDefense then
        local ok, b, m = pcall(UnitDefense, "pet")
        if ok then defBase = b or 0; defMod = m or 0 end
    end
    local armorBase, armorEff = 0, 0
    if UnitArmor then
        local ok, b, e = pcall(UnitArmor, "pet")
        if ok then armorBase = b or 0; armorEff = e or 0 end
    end

    local combatLabels = { "攻击:", "强度:", "伤害:", "防御:", "护甲:" }
    local combatVals = {
        tostring(mainBase + mainMod),
        tostring(ap),
        string.format("%d-%d", math.floor(minDmg), math.floor(maxDmg)),
        tostring(defBase + defMod),
        tostring(armorEff > 0 and armorEff or armorBase),
    }
    for i, r in ipairs(page.petStatRight) do
        r.label:SetText(combatLabels[i])
        r.value:SetText(combatVals[i] or "0")
    end

    for _, r in ipairs(page.resStats) do
        local base, bonus = 0, 0
        if UnitResistance then
            local ok, b, bn = pcall(UnitResistance, "pet", r.school)
            if ok then base = b or 0; bonus = bn or 0 end
        end
        r.label:SetText(RESIST_NAMES[r.school] .. ":")
        r.value:SetText(tostring(base + bonus))
    end

    if isHunterPet then
        local foodStr = ""
        if GetPetFoodTypes then
            local ok, r1, r2, r3, r4, r5, r6 = pcall(GetPetFoodTypes)
            if ok then
                local foods = {}
                if r1 and r1 ~= "" then table.insert(foods, PET_FOOD_MAP[r1] or r1) end
                if r2 and r2 ~= "" then table.insert(foods, PET_FOOD_MAP[r2] or r2) end
                if r3 and r3 ~= "" then table.insert(foods, PET_FOOD_MAP[r3] or r3) end
                if r4 and r4 ~= "" then table.insert(foods, PET_FOOD_MAP[r4] or r4) end
                if r5 and r5 ~= "" then table.insert(foods, PET_FOOD_MAP[r5] or r5) end
                if r6 and r6 ~= "" then table.insert(foods, PET_FOOD_MAP[r6] or r6) end
                foodStr = table.concat(foods, "、")
            end
        end
        page.foodText:SetText(foodStr)
        page.scrollArea:SetContentHeight(page.fullContentH)
    else
        page.scrollArea:SetContentHeight(page.fullContentH)
    end
end

--------------------------------------------------------------------------------
-- Events
--------------------------------------------------------------------------------
local eventFrame = CreateFrame("Frame", "SFramesCPEvents", UIParent)
local cpEvents = {
    "UNIT_INVENTORY_CHANGED", "PLAYER_AURAS_CHANGED",
    "UPDATE_FACTION", "SKILL_LINES_CHANGED",
    "PLAYER_PVP_KILLS_CHANGED", "PLAYER_PVP_RANK_CHANGED",
    "UNIT_ATTACK_POWER", "UNIT_RANGEDDAMAGE",
    "UNIT_ATTACK", "UNIT_DEFENSE", "UNIT_RESISTANCES",
    "CHAT_MSG_SKILL", "CHAT_MSG_COMBAT_HONOR_GAIN",
    "CHARACTER_POINTS_CHANGED", "PLAYER_ENTERING_WORLD",
    "UNIT_PET", "PET_UI_UPDATE", "PET_BAR_UPDATE",
    "UNIT_PET_EXPERIENCE", "PET_UI_CLOSE", "UNIT_HAPPINESS",
}
for _, ev in ipairs(cpEvents) do
    pcall(function() eventFrame:RegisterEvent(ev) end)
end
eventFrame:SetScript("OnEvent", function()
    if not panel or not panel:IsShown() then return end
    if event == "UNIT_INVENTORY_CHANGED" then
        CP:ScheduleEquipUpdate()
    end
    CP:UpdateCurrentTab()
end)

--------------------------------------------------------------------------------
-- Hook: replace ToggleCharacter
--------------------------------------------------------------------------------
-- Save CharacterFrame's original Show before HideBlizzardFrames may suppress it
local origCharFrameShow = CharacterFrame and CharacterFrame.Show
local origToggleCharacter = ToggleCharacter
ToggleCharacter = function(tab)
    if SFramesDB and SFramesDB.charPanelEnable == false then
        if CharacterFrame then
            if origCharFrameShow then
                CharacterFrame.Show = origCharFrameShow
            end
            if origToggleCharacter then
                origToggleCharacter(tab)
            else
                if CharacterFrame:IsShown() then
                    HideUIPanel(CharacterFrame)
                else
                    ShowUIPanel(CharacterFrame)
                end
            end
        end
        return
    end
    if tab == "PetPaperDollFrame" then
        CreateMainFrame()
        CP:Toggle(PET_TAB_INDEX or 1)
        return
    end
    local tabMap = {
        ["PaperDollFrame"] = 1,
        ["ReputationFrame"] = 2,
        ["SkillFrame"] = 3,
        ["HonorFrame"] = 4,
    }
    CP:Toggle(tabMap[tab] or 1)
end
