-- ============================================================
-- Asylum Compagnon — UI.lua
-- Fenêtre principale : sidebar (Accueil / Optimisation / Inventaire),
-- listes des personnages synchronisés, code SimC, inventaire partagé.
-- ============================================================

local ADDON_NAME, NS = ...

local unpack = unpack or table.unpack

local WHITE = "Interface\\Buttons\\WHITE8x8"
local MASK  = "Interface\\CharacterFrame\\TempPortraitAlphaMask"
local FONT  = "Fonts\\FRIZQT__.TTF"
local MONO  = "Fonts\\ARIALN.TTF"
local GOLD_ICON  = "Interface\\MoneyFrame\\UI-GoldIcon"
local HOME_IMAGE = "Interface\\AddOns\\AsylumCompagnon\\media\\accueil.tga"

local RAIDBOTS_URL = "https://www.raidbots.com/simbot/topgear"

local WIN_W, WIN_H = 780, 520
local SIDEBAR_W    = 170
local TOPBAR_H     = 44

-- Palette (modifiable à volonté)
local COLORS = {
    bg     = { 0.049, 0.055, 0.071, 0.975 }, -- fond principal  ~#0d0e12
    panel  = { 0.082, 0.090, 0.114, 1.0   }, -- panneaux        ~#151721
    field  = { 0.062, 0.068, 0.088, 1.0   }, -- zones de saisie
    line   = { 1, 1, 1, 0.07 },              -- séparateurs / bordures
    accent = { 0.655, 0.545, 0.980, 1.0   }, -- violet          ~#a78bfa
    text   = { 0.930, 0.930, 0.950, 1.0   },
    muted  = { 0.550, 0.560, 0.620, 1.0   },
    green  = { 0.13, 0.77, 0.37, 1.0 },
    gray   = { 0.42, 0.44, 0.50, 1.0 },
}

-- ---------- Helpers ----------
local function Solid(parent, layer, sublevel)
    local t = parent:CreateTexture(nil, layer or "BACKGROUND", nil, sublevel)
    t:SetTexture(WHITE)
    return t
end

local function Label(parent, size, color, font)
    local fs = parent:CreateFontString(nil, "OVERLAY")
    fs:SetFont(font or FONT, size, "")
    fs:SetTextColor(unpack(color or COLORS.text))
    fs:SetShadowColor(0, 0, 0, 0.7)
    fs:SetShadowOffset(0, -1)
    return fs
end

local function Border(frame, color, thickness)
    thickness = thickness or 1
    local r, g, b, a = unpack(color or COLORS.line)
    for _, side in ipairs({ "TOP", "BOTTOM", "LEFT", "RIGHT" }) do
        local t = Solid(frame, "OVERLAY", 7)
        t:SetVertexColor(r, g, b, a)
        if side == "TOP" then
            t:SetPoint("TOPLEFT"); t:SetPoint("TOPRIGHT"); t:SetHeight(thickness)
        elseif side == "BOTTOM" then
            t:SetPoint("BOTTOMLEFT"); t:SetPoint("BOTTOMRIGHT"); t:SetHeight(thickness)
        elseif side == "LEFT" then
            t:SetPoint("TOPLEFT"); t:SetPoint("BOTTOMLEFT"); t:SetWidth(thickness)
        else
            t:SetPoint("TOPRIGHT"); t:SetPoint("BOTTOMRIGHT"); t:SetWidth(thickness)
        end
    end
end

local function Circle(parent, size, layer, sublevel)
    local t = Solid(parent, layer or "ARTWORK", sublevel)
    t:SetSize(size, size)
    local m = parent:CreateMaskTexture()
    m:SetTexture(MASK)
    m:SetAllPoints(t)
    t:AddMaskTexture(m)
    return t
end

local function RoundIcon(parent, fileID, size)
    local tex = parent:CreateTexture(nil, "ARTWORK")
    tex:SetTexture(fileID)
    tex:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    tex:SetSize(size, size)
    local mask = parent:CreateMaskTexture()
    mask:SetTexture(MASK)
    mask:SetAllPoints(tex)
    tex:AddMaskTexture(mask)
    return tex
end

local function MakeButton(parent, text, w, h)
    local b = CreateFrame("Button", nil, parent)
    b:SetSize(w, h)
    local bg = Solid(b)
    bg:SetAllPoints()
    bg:SetVertexColor(0.13, 0.14, 0.18, 1)
    Border(b, COLORS.line)
    local hl = b:CreateTexture(nil, "HIGHLIGHT")
    hl:SetTexture(WHITE)
    hl:SetAllPoints()
    hl:SetVertexColor(1, 1, 1, 0.05)
    b.label = Label(b, 11)
    b.label:SetPoint("CENTER")
    b.label:SetText(text)
    return b
end

-- Zone de texte copiable : lecture seule mais sélectionnable (Ctrl+C)
local function MakeCopyBox(parent, multiline)
    local eb = CreateFrame("EditBox", nil, parent)
    eb:SetAutoFocus(false)
    eb:SetMultiLine(multiline and true or false)
    eb:SetMaxLetters(0)
    eb:SetFont(MONO, 11, "")
    eb:SetTextColor(unpack(COLORS.text))
    eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    eb:SetScript("OnEditFocusGained", function(self) self:HighlightText() end)
    eb:SetScript("OnMouseUp", function(self) self:HighlightText() end)
    eb:SetScript("OnTextChanged", function(self, user)
        if user then
            self:SetText(self.lockedText or "")
            self:HighlightText()
        end
    end)
    eb.SetLockedText = function(self, txt)
        self.lockedText = txt or ""
        self:SetText(self.lockedText)
    end
    return eb
end

local function ClassColorStr(token)
    local c = token and RAID_CLASS_COLORS and RAID_CLASS_COLORS[token]
    return (c and c.colorStr) or "ffffffff"
end

local function ClassName(token)
    return (token and LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE[token]) or token or "?"
end

local function SpecName(specID)
    if specID and specID > 0 and GetSpecializationInfoByID then
        local _, name = GetSpecializationInfoByID(specID)
        return name
    end
end

local function RelTime(ts)
    if not ts or ts == 0 then return nil end
    local now = (GetServerTime and GetServerTime()) or time()
    local d = math.max(0, now - ts)
    if d < 60 then return "à l'instant" end
    if d < 3600 then return ("il y a %d min"):format(math.floor(d / 60)) end
    if d < 86400 then return ("il y a %d h"):format(math.floor(d / 3600)) end
    return ("il y a %d j"):format(math.floor(d / 86400))
end

local function FormatNumber(n)
    if BreakUpLargeNumbers then return BreakUpLargeNumbers(n) end
    return tostring(n)
end

-- ---------- Fenêtre principale ----------
local UI

-- Arborescence de réglages (esprit menu cooldowns) : sections repliables avec
-- + / −. Repliée = titre seul ; dépliée = cases/steppers dessous, indentés.
-- Compact, ne déborde jamais. tree:section(titre[, ouvert]) → objet avec
-- :check / :stepper ; puis tree:relayout() et tree:refreshAll().
local function MakeSettingsTree(parent, x, topY, width)
    local sections = {}
    local relayout
    local tree = {}

    local function newCheck(sec, text, dbKey, onChange, indent)
        local box = CreateFrame("Button", nil, parent)
        box:SetSize(20, 20)
        local bg = Solid(box); bg:SetAllPoints(); bg:SetVertexColor(unpack(COLORS.field))
        Border(box, COLORS.line)
        local inner = Solid(box, "ARTWORK")
        inner:SetSize(12, 12); inner:SetPoint("CENTER"); inner:SetVertexColor(unpack(COLORS.accent))
        local lbl = Label(box, 12); lbl:SetPoint("LEFT", box, "RIGHT", 10, 0); lbl:SetText(NS.L(text))
        local function upd() inner:SetShown(not (NS.db and NS.db[dbKey] == false)) end
        box:SetScript("OnClick", function()
            if not NS.db then return end
            NS.db[dbKey] = (NS.db[dbKey] == false) and true or false
            upd(); if onChange then onChange() end
        end)
        box:Hide()
        sec.items[#sec.items + 1] = {
            h = 28, upd = upd,
            layout = function(ix, iy)
                box:ClearAllPoints()
                box:SetPoint("TOPLEFT", parent, "TOPLEFT", ix + (indent or 0), iy)
            end,
            show = function(v) box:SetShown(v); if v then upd() end end,
        }
    end

    -- suffix : texte après la valeur ("%" par défaut, "" pour une taille ou un
    -- préréglage) ; step : incrément des boutons -/+ (5 par défaut, 1 pour un
    -- préréglage 1-7 — avant, +5 sur la couleur sautait de 1 à 6 !)
    local function newStepper(sec, labelText, dbKey, default, minV, maxV, color, onChange, suffix, step)
        if suffix == nil then suffix = "%" end
        step = step or 5
        local lbl = Label(parent, 11); lbl:SetText(NS.L(labelText))
        if color then lbl:SetTextColor(unpack(color)) end
        local minus = MakeButton(parent, "-", 24, 20)
        local val = Label(parent, 13); val:SetWidth(46); val:SetJustifyH("CENTER")
        local plus = MakeButton(parent, "+", 24, 20)
        local function get() return (NS.db and NS.db[dbKey]) or default end
        local function upd() val:SetText(("%d"):format(get()) .. suffix) end
        minus:SetScript("OnClick", function()
            if not NS.db then return end
            NS.db[dbKey] = math.max(minV, get() - step); upd(); if onChange then onChange() end
        end)
        plus:SetScript("OnClick", function()
            if not NS.db then return end
            NS.db[dbKey] = math.min(maxV, get() + step); upd(); if onChange then onChange() end
        end)
        lbl:Hide(); minus:Hide(); val:Hide(); plus:Hide()
        sec.items[#sec.items + 1] = {
            h = 52, upd = upd,
            layout = function(ix, iy)
                lbl:ClearAllPoints(); lbl:SetPoint("TOPLEFT", parent, "TOPLEFT", ix, iy)
                minus:ClearAllPoints(); minus:SetPoint("TOPLEFT", parent, "TOPLEFT", ix, iy - 20)
                val:ClearAllPoints(); val:SetPoint("LEFT", minus, "RIGHT", 8, 0)
                plus:ClearAllPoints(); plus:SetPoint("LEFT", val, "RIGHT", 8, 0)
            end,
            show = function(v) lbl:SetShown(v); minus:SetShown(v); val:SetShown(v); plus:SetShown(v); if v then upd() end end,
        }
    end

    -- bouton d'action simple ; onClick reçoit le bouton (pour changer son
    -- libellé) ; updFn optionnel = resynchronise le libellé à l'affichage
    local function newButton(sec, text, onClick, width, updFn)
        local btn = MakeButton(parent, NS.L(text), width or 180, 22)
        if onClick then btn:SetScript("OnClick", function() onClick(btn) end) end
        btn:Hide()
        sec.items[#sec.items + 1] = {
            h = 30, upd = function() if updFn then updFn(btn) end end,
            layout = function(ix, iy)
                btn:ClearAllPoints()
                btn:SetPoint("TOPLEFT", parent, "TOPLEFT", ix, iy)
            end,
            show = function(v) btn:SetShown(v); if v and updFn then updFn(btn) end end,
        }
        return btn
    end

    function tree:section(titleText, expanded)
        local sec = { items = {}, expanded = expanded and true or false }
        local hdr = CreateFrame("Button", nil, parent)
        hdr:SetHeight(24)
        local hl = hdr:CreateTexture(nil, "HIGHLIGHT"); hl:SetTexture(WHITE); hl:SetAllPoints(); hl:SetVertexColor(1, 1, 1, 0.04)
        local plusFs = Label(hdr, 14, COLORS.accent); plusFs:SetPoint("LEFT", 2, 0)
        local lbl = Label(hdr, 12); lbl:SetPoint("LEFT", plusFs, "RIGHT", 8, 0)
        lbl:SetTextColor(0.67, 0.57, 0.98); lbl:SetText(NS.L(titleText))
        sec.hdr, sec.plusFs = hdr, plusFs
        hdr:SetScript("OnClick", function() sec.expanded = not sec.expanded; relayout() end)
        sec.check = function(_, ...) newCheck(sec, ...) end
        sec.stepper = function(_, ...) newStepper(sec, ...) end
        sec.button = function(_, ...) return newButton(sec, ...) end
        sections[#sections + 1] = sec
        return sec
    end

    relayout = function()
        local y = topY
        for _, sec in ipairs(sections) do
            sec.hdr:ClearAllPoints()
            sec.hdr:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
            sec.hdr:SetWidth(width or 300)
            sec.plusFs:SetText(sec.expanded and "-" or "+")
            y = y - 26
            for _, it in ipairs(sec.items) do
                if sec.expanded then
                    it.layout(x + 22, y)
                    it.show(true)
                    y = y - it.h
                else
                    it.show(false)
                end
            end
            y = y - 8
        end
    end
    tree.relayout = relayout
    function tree:refreshAll()
        for _, sec in ipairs(sections) do
            for _, it in ipairs(sec.items) do it.upd() end
        end
    end
    return tree
end

-- Page Toolbox : hors de BuildUI (budget de variables locales séparé).
local function BuildToolboxPage(content, pages)
    local toolbox = CreateFrame("Frame", nil, content)
    toolbox:SetAllPoints()
    toolbox:Hide()
    pages.toolbox = toolbox

    local title = Label(toolbox, 14)
    title:SetPoint("TOPLEFT", 24, -16)
    title:SetText(NS.L("Boîte à outils"))
    local hint = Label(toolbox, 10, COLORS.muted)
    hint:SetPoint("TOPLEFT", 24, -38)
    hint:SetText(NS.L("Automatisations locales (n'affectent que toi)."))

    local function refreshLog()
        if NS.Toolbox and NS.Toolbox.UpdateAutoLog then NS.Toolbox.UpdateAutoLog() end
    end

    local T = MakeSettingsTree(toolbox, 24, -64, 360)
    local s
    s = T:section("Vente automatique")
    s:check("Vendre les objets gris chez un marchand", "tbAutoSell")
    s = T:section("Réparation automatique")
    s:check("Réparer automatiquement", "tbAutoRepair")
    s:check("Fonds de guilde en priorité", "tbRepairGuild", nil, 22)
    s = T:section("Files d'attente")
    s:check("Accepter automatiquement les files", "tbAutoQueue")
    s = T:section("Journaux de combat")
    s:check("Log auto en donjon", "tbAutoLogDungeon", refreshLog)
    s:check("Log auto en raid", "tbAutoLogRaid", refreshLog)
    s = T:section("SpellID / IconID")
    s:check("Afficher SpellID / IconID (infobulles)", "spellIDTooltip")
    T:relayout()

    -- Tout activer / Tout désactiver (haut-droite, au niveau du titre)
    local ALL = { "tbAutoSell", "tbAutoRepair", "tbAutoQueue",
        "spellIDTooltip", "tbAutoLogDungeon", "tbAutoLogRaid" }
    local function setAll(v)
        if not NS.db then return end
        for _, k in ipairs(ALL) do NS.db[k] = v end
        T:refreshAll(); refreshLog()
    end
    local enAll = MakeButton(toolbox, NS.L("Tout activer"), 96, 22)
    enAll:SetPoint("TOPRIGHT", toolbox, "TOPRIGHT", -132, -14)
    enAll:SetScript("OnClick", function() setAll(true) end)
    local disAll = MakeButton(toolbox, NS.L("Tout désactiver"), 112, 22)
    disAll:SetPoint("TOPRIGHT", toolbox, "TOPRIGHT", -16, -14)
    disAll:SetScript("OnClick", function() setAll(false) end)

    return function() T:refreshAll() end
end

-- Page ALERTES — construite hors de BuildUI (comme la Boîte à outils) pour
-- rester sous la limite Lua de 200 variables locales, et pour un agencement
-- STRICTEMENT identique : titre + astuce, arborescence repliée, Tout activer /
-- Tout désactiver en haut-droite.
local function BuildAlertsPage(content, pages)
    local alerts = CreateFrame("Frame", nil, content)
    alerts:SetAllPoints()
    alerts:Hide()
    pages.alerts = alerts

    local title = Label(alerts, 14)
    title:SetPoint("TOPLEFT", 24, -16)
    title:SetText(NS.L("Alertes"))
    local hint = Label(alerts, 10, COLORS.muted)
    hint:SetPoint("TOPLEFT", 24, -38)
    hint:SetText(NS.L("Notifications locales (chat, écran, son)."))

    local durChg = function() if NS.CheckDurability then NS.CheckDurability() end end
    local blPreview = function() if NS.Stats and NS.Stats.PreviewLustAlert then NS.Stats.PreviewLustAlert() end end
    local blColorChg = function()
        if NS.Stats and NS.Stats.SetColorPreset then
            NS.Stats.SetColorPreset((NS.db and NS.db.blAlertColorIdx) or 1)
            NS.Stats.PreviewLustAlert()
        end
    end

    local T = MakeSettingsTree(alerts, 24, -64, 360)
    local s
    s = T:section("Alertes de stock épuisé")
    s:check("Message dans le chat", "alertChat")
    s:check("Alerte au centre de l'écran", "alertRaid")
    s:check("Alerte sonore", "alertSound")
    s = T:section("Fonctionnalités (affichage local)")
    s:check("Rapport chat à l'appel (ready check)", "appelReport")
    s = T:section("Durabilité de l'équipement")
    s:check("Alerte de durabilité faible", "durAlert")
    s:stepper("Seuil jaune (moyen)",    "durYellow", 25, 10, 60, { 0.98, 0.80, 0.25 }, durChg)
    s:stepper("Seuil rouge (critique)", "durRed",    10,  5, 40, { 0.94, 0.36, 0.36 }, durChg)
    s = T:section("Alerte Bloodlust (Potion + Burst)")
    s:check("Message + son quand un Lust est lancé", "blAlert")
    s:check("Jouer un son", "blAlertSound")
    s:stepper("Taille du message", "blAlertSize", 30, 16, 60, { 0.98, 0.86, 0.35 }, blPreview, "")
    s:stepper("Couleur (préréglage 1-7)", "blAlertColorIdx", 1, 1, 7, { 0.98, 0.86, 0.35 }, blColorChg, "", 1)
    -- position du message : mode « déplacer » (aperçu glissable, sauvegardé)
    s:button("Déplacer le message", function(btn)
        if NS.Stats and NS.Stats.ToggleBLMove then
            local moving = NS.Stats.ToggleBLMove()
            btn.label:SetText(NS.L(moving and "Valider la position" or "Déplacer le message"))
        end
    end, 180, function(btn)
        local moving = NS.Stats and NS.Stats.IsBLMoving and NS.Stats.IsBLMoving()
        btn.label:SetText(NS.L(moving and "Valider la position" or "Déplacer le message"))
    end)
    s:button("Réinitialiser la position", function()
        if NS.Stats and NS.Stats.ResetBLPosition then NS.Stats.ResetBLPosition() end
        if NS.Stats and NS.Stats.PreviewLustAlert then NS.Stats.PreviewLustAlert() end
    end, 180)
    T:relayout()

    -- Tout activer / Tout désactiver (haut-droite, au niveau du titre)
    local ALL = { "alertChat", "alertRaid", "alertSound", "appelReport", "durAlert", "blAlert" }
    local function setAll(v)
        if not NS.db then return end
        for _, k in ipairs(ALL) do NS.db[k] = v end
        T:refreshAll(); durChg()
    end
    local enAll = MakeButton(alerts, NS.L("Tout activer"), 96, 22)
    enAll:SetPoint("TOPRIGHT", alerts, "TOPRIGHT", -132, -14)
    enAll:SetScript("OnClick", function() setAll(true) end)
    local disAll = MakeButton(alerts, NS.L("Tout désactiver"), 112, 22)
    disAll:SetPoint("TOPRIGHT", alerts, "TOPRIGHT", -16, -14)
    disAll:SetScript("OnClick", function() setAll(false) end)

    return function() T:refreshAll() end
end

-- Page STATS M+ — historique des donjons (déroulant) + tableau par joueur.
-- Construite hors de BuildUI (limite Lua 200 locals). Se rafraîchit via
-- NS.RefreshStatsPage (appelé à l'ouverture de la page et à la fin d'un donjon).
local function BuildStatsPage(content, pages)
    local page = CreateFrame("Frame", nil, content)
    page:SetAllPoints()
    page:Hide()
    pages.stats = page

    local title = Label(page, 14)
    title:SetPoint("TOPLEFT", 24, -16)
    title:SetText(NS.L("Statistiques Mythique+"))
    local hint = Label(page, 10, COLORS.muted)
    hint:SetPoint("TOPLEFT", 24, -38)
    hint:SetText(NS.L("Actions par catégorie et par joueur (ceux qui ont l'addon)."))

    -- sélecteur d'historique (déroulant maison) — même largeur que le
    -- tableau en dessous (s'arrête au bout de la colonne « Pot »)
    local selBtn = MakeButton(page, "—", 470, 26)
    selBtn:SetPoint("TOPLEFT", 24, -62)
    local caret = Label(selBtn, 11, COLORS.muted)
    caret:SetPoint("RIGHT", -8, 0); caret:SetText("v")

    local listFrame = CreateFrame("Frame", nil, page)
    listFrame:SetPoint("TOPLEFT", selBtn, "BOTTOMLEFT", 0, -2)
    listFrame:SetSize(470, 40)
    listFrame:SetFrameStrata("DIALOG")
    listFrame:Hide()
    local lbg = Solid(listFrame); lbg:SetAllPoints(); lbg:SetVertexColor(0.05, 0.05, 0.07, 0.98)
    Border(listFrame, COLORS.line)
    local listBtns = {}

    local emptyFs = Label(page, 12, COLORS.muted)
    emptyFs:SetPoint("TOPLEFT", 40, -152)
    emptyFs:Hide()

    local selectedIndex = 1

    -- rendu du tableau (colonnes + tooltips) via le module Stats, partagé avec le popup
    local function renderTable(entry)
        local e = entry or { rows = {} }
        if NS.Stats and NS.Stats.RenderTable then
            NS.Stats.RenderTable(page, e, 24, -112, 470)
        end
        local empty = not (entry and entry.rows and #entry.rows > 0)
        emptyFs:SetText(NS.L("Aucune donnée pour ce donjon."))
        emptyFs:SetShown(empty)
    end

    local function refresh()
        local h = (NS.Stats and NS.Stats.GetHistory()) or {}
        if #h == 0 then
            selBtn:SetText(NS.L("Aucun donjon enregistré"))
            renderTable(nil); return
        end
        if selectedIndex < 1 or selectedIndex > #h then selectedIndex = 1 end
        local e = h[selectedIndex]
        selBtn:SetText(("%s +%d  —  %s"):format(e.name or "?", e.level or 0, e.date or ""))
        renderTable(e)
    end
    NS.RefreshStatsPage = refresh

    selBtn:SetScript("OnClick", function()
        if listFrame:IsShown() then listFrame:Hide(); return end
        local h = (NS.Stats and NS.Stats.GetHistory()) or {}
        for _, b in ipairs(listBtns) do b:Hide() end
        if #h == 0 then return end
        local yy = -6
        for i, e in ipairs(h) do
            local b = listBtns[i]
            if not b then
                b = CreateFrame("Button", nil, listFrame)
                b:SetSize(458, 22)
                local bh = b:CreateTexture(nil, "HIGHLIGHT"); bh:SetTexture(WHITE)
                bh:SetAllPoints(); bh:SetVertexColor(1, 1, 1, 0.06)
                b.fs = Label(b, 12); b.fs:SetPoint("LEFT", 8, 0)
                listBtns[i] = b
            end
            b:ClearAllPoints(); b:SetPoint("TOPLEFT", 6, yy)
            b.fs:SetText(("%s |cfff2d159+%d|r  |cff8a8f99%s|r"):format(
                e.name or "?", e.level or 0, e.date or ""))
            b:SetScript("OnClick", function() selectedIndex = i; listFrame:Hide(); refresh() end)
            b:Show()
            yy = yy - 24
        end
        listFrame:SetHeight(math.max(30, -yy + 10))
        listFrame:Show()
    end)

    refresh()
end

-- Page ACCUEIL — tableau de bord (maquette « Option D magazine » validée) :
-- IMAGE ENTIÈRE 4:3 à gauche, Préparation + Amis à droite, bandeau bas
-- Stocks · Clé de la semaine · Derniers runs M+ (cliquable → Stats M+).
-- Construite hors de BuildUI (limite Lua 200 locals).
-- Ne coûte rien en réseau : tout vient de données déjà présentes en local.
-- Renvoie la fonction de rafraîchissement (ouverture + retour sur l'onglet).
local function BuildHomePage(content, pages, gotoPage)
    local home = CreateFrame("Frame", nil, content)
    home:SetAllPoints()
    pages.home = home

    local hello = Label(home, 16)
    hello:SetPoint("TOPLEFT", 24, -20)
    local status = Label(home, 10, COLORS.muted)
    status:SetPoint("TOPRIGHT", -24, -24)

    -- Mise en page « Option D — magazine » (maquette validée) : IMAGE ENTIÈRE
    -- 4:3 à GAUCHE (372×279, aucune coupe), Préparation + Amis à droite sur
    -- toute la hauteur de l'image, puis un bandeau bas plein largeur :
    -- Stocks bas · Clé de la semaine · Derniers runs M+ — zéro espace mort.
    -- (La fenêtre se redimensionne par échelle uniforme → les proportions
    -- tiennent à toutes les tailles.)
    local function MakeCard(x, y, w, h, titleText)
        local card = CreateFrame("Frame", nil, home)
        card:SetPoint("TOPLEFT", x, y)
        card:SetSize(w, h)
        local bg = Solid(card); bg:SetAllPoints(); bg:SetVertexColor(unpack(COLORS.panel))
        Border(card, COLORS.line)
        local t = Label(card, 9, COLORS.muted)
        t:SetPoint("TOPLEFT", 10, -9)
        t:SetText(NS.L(titleText))
        local body = Label(card, 11)
        body:SetPoint("TOPLEFT", 10, -26)
        body:SetPoint("BOTTOMRIGHT", -10, 8)
        body:SetJustifyH("LEFT")
        body:SetJustifyV("TOP")
        card.body = body
        return card
    end

    -- rangée 1 : image ENTIÈRE à gauche (haut du canevas 1024×1024, soit
    -- 1024×768 → SetTexCoord(0,1,0,0.75), cadre 4:3 exact)
    local banner = CreateFrame("Frame", nil, home)
    banner:SetPoint("TOPLEFT", 24, -52)
    banner:SetSize(372, 279)
    local bbg = Solid(banner); bbg:SetAllPoints(); bbg:SetVertexColor(unpack(COLORS.field))
    Border(banner, COLORS.line)
    local bMissing = banner:CreateFontString(nil, "BORDER")
    bMissing:SetFont(FONT, 10, "")
    bMissing:SetTextColor(unpack(COLORS.muted))
    bMissing:SetPoint("CENTER")
    bMissing:SetText("media\\accueil.tga")
    local bimg = banner:CreateTexture(nil, "ARTWORK")
    bimg:SetPoint("TOPLEFT", 1, -1)
    bimg:SetPoint("BOTTOMRIGHT", -1, 1)
    bimg:SetTexture(HOME_IMAGE)
    bimg:SetTexCoord(0, 1, 0, 0.75)

    -- rangée 1, colonne droite : préparation + amis (hauteur de l'image)
    local cardPrep    = MakeCard(404, -52,  180, 135, "Ta préparation")
    local cardFriends = MakeCard(404, -195, 180, 136, "Amis en ligne")

    -- rangée 2 (bandeau bas) : stocks · clé de la semaine · derniers runs
    local cardStock = MakeCard(24, -339, 181, 119, "Stocks bas")

    local keyRow = CreateFrame("Frame", nil, home)
    keyRow:SetPoint("TOPLEFT", 213, -339)
    keyRow:SetSize(181, 119)
    local kbg = Solid(keyRow); kbg:SetAllPoints(); kbg:SetVertexColor(unpack(COLORS.panel))
    Border(keyRow, COLORS.line)
    local kTitle = Label(keyRow, 9, COLORS.muted)
    kTitle:SetPoint("TOPLEFT", 10, -9)
    kTitle:SetText(NS.L("Clé de la semaine"))
    local kText = Label(keyRow, 11)
    kText:SetPoint("TOPLEFT", 10, -26)
    kText:SetPoint("BOTTOMRIGHT", -10, 36)
    kText:SetJustifyH("LEFT")
    kText:SetJustifyV("TOP")
    local kBtn = MakeButton(keyRow, NS.L("Clés & Coffre") .. " »", 161, 22)
    kBtn:SetPoint("BOTTOM", 0, 8)
    kBtn:SetScript("OnClick", function() gotoPage("vault") end)

    -- derniers runs M+ (panneau cliquable → page Stats M+)
    local runsCard = CreateFrame("Button", nil, home)
    runsCard:SetPoint("TOPLEFT", 402, -339)
    runsCard:SetSize(182, 119)
    local rcBg = Solid(runsCard); rcBg:SetAllPoints(); rcBg:SetVertexColor(unpack(COLORS.panel))
    Border(runsCard, COLORS.line)
    local rcHl = runsCard:CreateTexture(nil, "HIGHLIGHT")
    rcHl:SetTexture(WHITE); rcHl:SetAllPoints(); rcHl:SetVertexColor(1, 1, 1, 0.04)
    local rTitle = Label(runsCard, 9, COLORS.muted)
    rTitle:SetPoint("TOPLEFT", 10, -9)
    rTitle:SetText(NS.L("Derniers runs M+"))
    local rBody = Label(runsCard, 11)
    rBody:SetPoint("TOPLEFT", 10, -26)
    rBody:SetPoint("BOTTOMRIGHT", -10, 24)
    rBody:SetJustifyH("LEFT")
    rBody:SetJustifyV("TOP")
    rBody:SetWordWrap(false)
    local rMore = Label(runsCard, 10, COLORS.accent)
    rMore:SetPoint("BOTTOMLEFT", 10, 8)
    rMore:SetText(NS.L("Stats M+") .. " »")
    runsCard:SetScript("OnClick", function() gotoPage("stats") end)

    local function ItemNameOf(id, fallback)
        local fn = (C_Item and C_Item.GetItemInfo) or GetItemInfo
        if fn and id then
            local ok, n = pcall(fn, id)
            if ok and n then return n end
        end
        return fallback or ("objet " .. tostring(id))
    end
    -- coche verte / croix rouge : TEXTURES du jeu (mêmes icônes que le tracker
    -- de kicks) — les glyphes ✓/✗ ne sont pas dans la police du jeu (carrés)
    local OK_T = "|TInterface\\RaidFrame\\ReadyCheck-Ready:12|t"
    local KO_T = "|TInterface\\RaidFrame\\ReadyCheck-NotReady:12|t"
    local function mark(v)
        if v == true then return OK_T end
        if v == false then return KO_T end
        return "|cff8b8fa3—|r"
    end

    local function refresh()
        -- salutation + état de la synchro
        local _, myClass = UnitClass("player")
        local h = tonumber(date("%H")) or 12
        local greet = (h >= 5 and h < 18) and NS.L("Bonjour") or NS.L("Bonsoir")
        hello:SetText(("%s, |c%s%s|r"):format(greet, ClassColorStr(myClass), UnitName("player") or "?"))
        if NS.Sync and NS.Sync.available then
            status:SetText(("|TInterface\\COMMON\\Indicator-Green:10|t %s · v%s")
                :format(NS.L("Synchro active"), NS.VERSION or "?"))
        else
            status:SetText(("|TInterface\\COMMON\\Indicator-Gray:10|t %s · v%s")
                :format(NS.L("Synchro indisponible"), NS.VERSION or "?"))
        end

        -- amis en ligne (max 4 affichés + compteurs)
        local on, offCount = {}, 0
        for k, p in pairs((NS.db and NS.db.peers) or {}) do
            if p.online then
                on[#on + 1] = { name = k:match("^(.-)%-") or k, class = p.class }
            else
                offCount = offCount + 1
            end
        end
        table.sort(on, function(a, b) return a.name < b.name end)
        local lines = {}
        for i = 1, math.min(#on, 4) do
            lines[#lines + 1] = ("|TInterface\\COMMON\\Indicator-Green:10|t |c%s%s|r")
                :format(ClassColorStr(on[i].class), on[i].name)
        end
        if #on > 4 then lines[#lines + 1] = ("|cff8b8fa3+%d|r"):format(#on - 4) end
        if #lines == 0 then lines[1] = "|cff8b8fa3" .. NS.L("Personne d'autre en ligne.") .. "|r" end
        if offCount > 0 then
            lines[#lines + 1] = "|cff55575f" .. (NS.L("%d hors ligne")):format(offCount) .. "|r"
        end
        cardFriends.body:SetText(table.concat(lines, "\n"))

        -- ta préparation (mêmes lectures que la page Buffs & auras)
        local s = (NS.Buffs and NS.Buffs.GetSelfStatus and NS.Buffs.GetSelfStatus()) or {}
        local prep = {
            ("%s  %s"):format(NS.L("Flacon"), mark(s.flask)),
            ("%s  %s"):format(NS.L("Bouffe"), mark(s.food)),
            ("%s  %s"):format(NS.L("Huile"), mark(s.oil)),
        }
        -- enchantements : coche verte si les 8 emplacements suivis sont
        -- enchantés, sinon croix rouge + liste des emplacements manquants
        if NS.Buffs and NS.Buffs.SelfEnchantBits then
            local okE, bits = pcall(NS.Buffs.SelfEnchantBits)
            if okE and type(bits) == "string" and #bits > 0 then
                local miss = {}
                for i, sl in ipairs(NS.ENCHANT_SLOTS or {}) do
                    if bits:sub(i, i) ~= "1" then miss[#miss + 1] = NS.L(sl.label) end
                end
                if #miss == 0 then
                    prep[#prep + 1] = ("%s  %s"):format(NS.L("Enchantements"), mark(true))
                else
                    prep[#prep + 1] = ("%s  %s |cff8b8fa3%s|r"):format(
                        NS.L("Enchantements"), mark(false), table.concat(miss, ", "))
                end
            end
        end
        cardPrep.body:SetText(table.concat(prep, "\n"))

        -- stocks bas (consommables suivis ≤ seuil, les 3 pires)
        local low = NS.LOW_STOCK or 5
        local items = (NS.db and NS.db.myProfile and NS.db.myProfile.items) or {}
        local worst = {}
        for _, t in ipairs(NS.TRACKED_ITEMS or {}) do
            local pid = t.ids and t.ids[1]
            local c = pid and items[pid]
            if c ~= nil and c <= low then
                worst[#worst + 1] = { id = pid, c = c, fb = t.fallback }
            end
        end
        table.sort(worst, function(a, b) return a.c < b.c end)
        local sl = {}
        for i = 1, math.min(#worst, 3) do
            local w = worst[i]
            local col = (w.c <= 0) and "ea4850" or "facd40"
            sl[#sl + 1] = ("%s |cff%s%d|r"):format(ItemNameOf(w.id, w.fb), col, w.c)
        end
        if #sl == 0 then sl[1] = "|cff8b8fa3" .. NS.L("Rien à signaler.") .. "|r" end
        cardStock.body:SetText(table.concat(sl, "\n"))

        -- clé de la semaine
        local keyText = NS.Vault and NS.Vault.MyKeyText and NS.Vault.MyKeyText()
        kText:SetText(keyText or ("|cff8b8fa3" .. NS.L("aucune clé") .. "|r"))

        -- derniers runs M+ (3 max, depuis l'historique des Stats M+)
        local hist = (NS.Stats and NS.Stats.GetHistory and NS.Stats.GetHistory()) or {}
        local rl = {}
        for i = 1, math.min(#hist, 3) do
            local e = hist[i]
            rl[#rl + 1] = ("%s |cfff2d159+%d|r"):format(e.name or "?", e.level or 0)
        end
        if #rl == 0 then rl[1] = "|cff8b8fa3" .. NS.L("Aucun donjon enregistré") .. "|r" end
        rBody:SetText(table.concat(rl, "\n"))
    end

    return refresh
end

local function BuildUI()
    local pages, navButtons = {}, {}
    local currentDetail, currentInv
    local ShowPage, FillDetail, ShowDetail, FillInvDetail, ShowInvDetail
    local refreshOptimList, refreshInvList
    local UpdateKicksPage, UpdateAlertsPage, RefreshBuffsPage, RefreshLootPage, UpdateCDPage, RefreshVaultPage
    local UpdateToolboxPage, UpdateHomePage

    UI = CreateFrame("Frame", "AsylumCompagnonFrame", UIParent)
    UI:SetSize(WIN_W, WIN_H)
    local savedPos = NS.db and NS.db.uiPos
    if savedPos and savedPos.p then
        UI:SetPoint(savedPos.p, UIParent, savedPos.rp or savedPos.p, savedPos.x or 0, savedPos.y or 0)
    else
        UI:SetPoint("CENTER", 0, 30)
    end
    UI:SetFrameStrata("HIGH")
    UI:SetMovable(true)
    UI:SetClampedToScreen(true)
    UI:EnableMouse(true)
    UI:SetScale((NS.db and NS.db.uiScale) or 1)
    UI:Hide()

    table.insert(UISpecialFrames, "AsylumCompagnonFrame") -- Échap ferme

    local bg = Solid(UI)
    bg:SetAllPoints()
    bg:SetVertexColor(unpack(COLORS.bg))

    -- bordure + liseré par-dessus tout le contenu
    local overlay = CreateFrame("Frame", nil, UI)
    overlay:SetAllPoints()
    overlay:SetFrameLevel(UI:GetFrameLevel() + 20)
    Border(overlay, COLORS.line)
    local topline = Solid(overlay, "OVERLAY")
    topline:SetPoint("TOPLEFT", 1, -1)
    topline:SetPoint("TOPRIGHT", -1, -1)
    topline:SetHeight(2)
    topline:SetGradient("HORIZONTAL",
        CreateColor(0.655, 0.545, 0.98, 0.95),
        CreateColor(0.655, 0.545, 0.98, 0.05))

    -- poignée de redimensionnement (bas-droite) : ajuste l'échelle de
    -- toute la fenêtre — tout le contenu (image comprise) se met à
    -- l'échelle proportionnellement ; réglage sauvegardé
    local grip = CreateFrame("Button", nil, overlay)
    grip:SetSize(16, 16)
    grip:SetPoint("BOTTOMRIGHT", -3, 3)
    grip:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
    grip:SetHighlightTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
    grip:SetPushedTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down")
    grip:SetScript("OnMouseDown", function(self)
        local pScale = UIParent:GetEffectiveScale()
        -- coin haut-gauche figé, en pixels écran
        local sx = UI:GetLeft() * UI:GetEffectiveScale()
        local sy = UI:GetTop() * UI:GetEffectiveScale()
        self:SetScript("OnUpdate", function()
            local cx = GetCursorPosition()
            local newScale = (cx - sx) / (WIN_W * pScale)
            if newScale < 0.7 then newScale = 0.7 end
            if newScale > 1.6 then newScale = 1.6 end
            UI:SetScale(newScale)
            UI:ClearAllPoints()
            UI:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT",
                sx / (pScale * newScale), sy / (pScale * newScale))
        end)
    end)
    grip:SetScript("OnMouseUp", function(self)
        self:SetScript("OnUpdate", nil)
        if NS.db then
            NS.db.uiScale = UI:GetScale()
            local p, _, rp, x, y = UI:GetPoint(1)
            NS.db.uiPos = { p = p, rp = rp, x = x, y = y }
        end
    end)

    local function SaveUIPosition()
        if not NS.db then return end
        local p, _, rp, x, y = UI:GetPoint(1)
        NS.db.uiPos = { p = p, rp = rp, x = x, y = y }
    end

    local function MakeDraggable(region)
        region:EnableMouse(true)
        region:SetScript("OnMouseDown", function() UI:StartMoving() end)
        region:SetScript("OnMouseUp", function()
            UI:StopMovingOrSizing()
            SaveUIPosition()
        end)
    end

    -- ----- Sidebar -----
    local sidebar = CreateFrame("Frame", nil, UI)
    sidebar:SetPoint("TOPLEFT", 1, -1)
    sidebar:SetPoint("BOTTOMLEFT", 1, 1)
    sidebar:SetWidth(SIDEBAR_W)
    MakeDraggable(sidebar)

    local sbg = Solid(sidebar)
    sbg:SetAllPoints()
    sbg:SetVertexColor(unpack(COLORS.panel))
    local sdiv = Solid(sidebar, "OVERLAY")
    sdiv:SetPoint("TOPRIGHT")
    sdiv:SetPoint("BOTTOMRIGHT")
    sdiv:SetWidth(1)
    sdiv:SetVertexColor(unpack(COLORS.line))

    local logo = RoundIcon(sidebar, NS.ICON_ID, 24)
    logo:SetPoint("TOPLEFT", 14, -13)
    local sTitle = Label(sidebar, 12)
    sTitle:SetPoint("LEFT", logo, "RIGHT", 9, 0) -- centré verticalement avec le logo
    sTitle:SetWidth(SIDEBAR_W - 46)
    sTitle:SetJustifyH("LEFT")
    sTitle:SetWordWrap(false)
    sTitle:SetText(NS.L("Asylum Compagnon"))

    -- navigation
    local NAV = {
        { key = "home",   label = "Accueil" },
        { key = "optim",  label = "Optimisation" },
        { key = "inv",    label = "Inventaire" },
        { key = "kicks",  label = "Kicks" },
        { key = "cd",     label = "Cooldowns" },
        { key = "stats",  label = "Stats M+" },
        { key = "buffs",  label = "Buffs & auras" },
        -- Menu « Butin » masqué pour l'instant (groupe full stuff → peu utile).
        -- Le code (Loot.lua + page) reste intact : décommenter la ligne ci-dessous
        -- pour le réactiver quand le loot redeviendra intéressant.
        -- { key = "loot",   label = "Butin" },
        { key = "vault",  label = "Clés & Coffre" },
        { key = "toolbox", label = "Toolbox" },
        { key = "alerts", label = "Alertes" },
    }
    local function SetNavActive(activeKey)
        for key, btn in pairs(navButtons) do
            local on = (key == activeKey)
            btn.bar:SetShown(on)
            btn.text:SetTextColor(unpack(on and COLORS.text or COLORS.muted))
        end
    end
    for i, item in ipairs(NAV) do
        local btn = CreateFrame("Button", nil, sidebar)
        btn:SetHeight(32)
        btn:SetPoint("TOPLEFT", 0, -(58 + (i - 1) * 34))
        btn:SetPoint("TOPRIGHT", -1, -(58 + (i - 1) * 34))
        local hl = btn:CreateTexture(nil, "HIGHLIGHT")
        hl:SetTexture(WHITE)
        hl:SetAllPoints()
        hl:SetVertexColor(1, 1, 1, 0.04)
        btn.bar = Solid(btn, "ARTWORK")
        btn.bar:SetPoint("TOPLEFT")
        btn.bar:SetPoint("BOTTOMLEFT")
        btn.bar:SetWidth(3)
        btn.bar:SetVertexColor(unpack(COLORS.accent))
        btn.bar:Hide()
        btn.text = Label(btn, 12, COLORS.muted)
        btn.text:SetPoint("LEFT", 16, 0)
        btn.text:SetText(NS.L(item.label))
        btn:SetScript("OnClick", function() ShowPage(item.key) end)
        navButtons[item.key] = btn
    end

    -- pied de sidebar : état de la synchro + version
    local syncDot = Circle(sidebar, 7, "ARTWORK")
    syncDot:SetPoint("BOTTOMLEFT", 15, 30)
    local syncLabel = Label(sidebar, 10, COLORS.muted)
    syncLabel:SetPoint("LEFT", syncDot, "RIGHT", 6, 0)
    local sVersion = Label(sidebar, 10, COLORS.muted)
    sVersion:SetPoint("BOTTOMLEFT", 14, 12)

    -- ----- Barre du haut -----
    local topbar = CreateFrame("Frame", nil, UI)
    topbar:SetPoint("TOPLEFT", SIDEBAR_W + 1, -1)
    topbar:SetPoint("TOPRIGHT", -1, -1)
    topbar:SetHeight(TOPBAR_H)
    MakeDraggable(topbar)

    local tdiv = Solid(topbar, "OVERLAY")
    tdiv:SetPoint("BOTTOMLEFT")
    tdiv:SetPoint("BOTTOMRIGHT")
    tdiv:SetHeight(1)
    tdiv:SetVertexColor(unpack(COLORS.line))

    local pageTitle = Label(topbar, 14)
    pageTitle:SetPoint("LEFT", 16, 0)

    local close = CreateFrame("Button", nil, topbar)
    close:SetSize(30, 30)
    close:SetPoint("RIGHT", -8, 0)
    local cbg = Solid(close)
    cbg:SetAllPoints()
    cbg:SetVertexColor(1, 0.3, 0.35, 0)
    -- croix « × » DESSINÉE (2 traits diagonaux) : même gabarit 12x12 et même
    -- épaisseur de trait que le carré « agrandir », pour un rendu cohérent.
    local xf = CreateFrame("Frame", nil, close)
    xf:SetSize(12, 12)
    xf:SetPoint("CENTER")
    local xlines = {}
    for _, ang in ipairs({ 45, -45 }) do
        local ln = xf:CreateTexture(nil, "ARTWORK")
        ln:SetTexture(WHITE)
        ln:SetSize(16, 1.7)
        ln:SetPoint("CENTER")
        ln:SetRotation(math.rad(ang))
        xlines[#xlines + 1] = ln
    end
    local function XColor(...) for _, ln in ipairs(xlines) do ln:SetVertexColor(...) end end
    XColor(unpack(COLORS.muted))
    close:SetScript("OnEnter", function()
        cbg:SetVertexColor(1, 0.3, 0.35, 0.16)
        XColor(1, 1, 1)
        GameTooltip:SetOwner(close, "ANCHOR_BOTTOM")
        GameTooltip:SetText(NS.L("Fermer"))
        GameTooltip:Show()
    end)
    close:SetScript("OnLeave", function()
        cbg:SetVertexColor(1, 0.3, 0.35, 0)
        XColor(unpack(COLORS.muted))
        GameTooltip:Hide()
    end)
    close:SetScript("OnClick", function() UI:Hide() end)

    -- bouton « taille max » (juste à gauche de la croix) : bascule entre la
    -- taille normale et la taille max fixée (échelle 1.6), en un clic.
    local MAX_SCALE = 1.6
    local maxBtn = CreateFrame("Button", nil, topbar)
    maxBtn:SetSize(30, 30)
    maxBtn:SetPoint("RIGHT", close, "LEFT", -2, 0)
    local mbg = Solid(maxBtn)
    mbg:SetAllPoints()
    mbg:SetVertexColor(1, 1, 1, 0)
    -- carré vide (style « agrandir »), MÊME gabarit 12x12 et MÊME épaisseur de
    -- trait (1.5) que la croix « × », pour que les deux icônes soient cohérentes.
    local sq = CreateFrame("Frame", nil, maxBtn)
    sq:SetSize(12, 12)
    sq:SetPoint("CENTER")
    local edges = {}
    for _, e in ipairs({ "TOP", "BOTTOM", "LEFT", "RIGHT" }) do
        local t = sq:CreateTexture(nil, "ARTWORK")
        t:SetTexture(WHITE)
        if e == "TOP" then t:SetPoint("TOPLEFT"); t:SetPoint("TOPRIGHT"); t:SetHeight(1.5)
        elseif e == "BOTTOM" then t:SetPoint("BOTTOMLEFT"); t:SetPoint("BOTTOMRIGHT"); t:SetHeight(1.5)
        elseif e == "LEFT" then t:SetPoint("TOPLEFT"); t:SetPoint("BOTTOMLEFT"); t:SetWidth(1.5)
        else t:SetPoint("TOPRIGHT"); t:SetPoint("BOTTOMRIGHT"); t:SetWidth(1.5) end
        edges[#edges + 1] = t
    end
    local function SqColor(...) for _, t in ipairs(edges) do t:SetVertexColor(...) end end
    SqColor(unpack(COLORS.muted))
    maxBtn:SetScript("OnEnter", function()
        mbg:SetVertexColor(1, 1, 1, 0.10)
        SqColor(1, 1, 1)
        GameTooltip:SetOwner(maxBtn, "ANCHOR_BOTTOM")
        GameTooltip:SetText(NS.L("Taille max / normale"))
        GameTooltip:Show()
    end)
    maxBtn:SetScript("OnLeave", function()
        mbg:SetVertexColor(1, 1, 1, 0)
        SqColor(unpack(COLORS.muted))
        GameTooltip:Hide()
    end)
    maxBtn:SetScript("OnClick", function()
        local atMax = UI:GetScale() >= (MAX_SCALE - 0.01)
        UI:SetScale(atMax and 1.0 or MAX_SCALE)
        if NS.db then
            NS.db.uiScale = UI:GetScale()
            local p, _, rp, x, y = UI:GetPoint(1)
            NS.db.uiPos = { p = p, rp = rp, x = x, y = y }
        end
    end)

    -- ----- Conteneur de pages -----
    local content = CreateFrame("Frame", nil, UI)
    content:SetPoint("TOPLEFT", SIDEBAR_W + 1, -(TOPBAR_H + 1))
    content:SetPoint("BOTTOMRIGHT", -1, 1)

    local PAGE_TITLES = {
        home = "Accueil",
        optim = "Optimisation", detail = "Optimisation",
        inv = "Inventaire", invdetail = "Inventaire",
        kicks = "Kicks", cd = "Cooldowns de groupe", buffs = "Buffs & auras",
        loot = "Butin", vault = "Clés & Coffre", alerts = "Alertes",
        toolbox = "Toolbox", stats = "Statistiques Mythique+",
    }
    local NAV_FOR_PAGE = { detail = "optim", invdetail = "inv" }

    ShowPage = function(key)
        for k, p in pairs(pages) do p:SetShown(k == key) end
        pageTitle:SetText(NS.L(PAGE_TITLES[key] or ""))
        SetNavActive(NAV_FOR_PAGE[key] or key)
        if key == "home" and UpdateHomePage then UpdateHomePage() end
        if key == "optim" and refreshOptimList then refreshOptimList() end
        if key == "inv" and refreshInvList then
            if NS.RefreshTrackedItems then NS.RefreshTrackedItems() end
            refreshInvList()
        end
        if key == "kicks" and UpdateKicksPage then UpdateKicksPage() end
        if key == "cd" and UpdateCDPage then UpdateCDPage() end
        if key == "buffs" and RefreshBuffsPage then RefreshBuffsPage() end
        if key == "loot" and RefreshLootPage then RefreshLootPage() end
        if key == "vault" and RefreshVaultPage then RefreshVaultPage() end
        if key == "alerts" and UpdateAlertsPage then UpdateAlertsPage() end
        if key == "toolbox" and UpdateToolboxPage then UpdateToolboxPage() end
        if key == "stats" and NS.RefreshStatsPage then NS.RefreshStatsPage() end
    end

    -- ============================================================
    -- Liste de personnages réutilisable (Optimisation + Inventaire)
    -- Ordre : toi d'abord, puis en ligne, puis par dernière synchro.
    -- ============================================================
    local function BuildEntries()
        local myMeta = (NS.GetMyMeta and NS.GetMyMeta()) or {}
        local mp = NS.db and NS.db.myProfile
        local me = {
            key = "ME", display = UnitName("player") or "?",
            class = (mp and mp.class) or myMeta.class,
            level = (mp and mp.level) or myMeta.level,
            specID = (mp and mp.specID) or myMeta.specID,
            ilvl = (mp and mp.ilvl) or myMeta.ilvl,
            ts = mp and mp.ts, online = true, isMe = true,
        }
        local peers = {}
        for k, p in pairs((NS.db and NS.db.peers) or {}) do
            peers[#peers + 1] = {
                key = k, display = k:match("^(.-)%-") or k,
                class = p.class, level = p.level, specID = p.specID,
                ilvl = p.ilvl, ts = p.ts, online = p.online,
                lastSeen = p.lastSeen,
            }
        end
        table.sort(peers, function(a, b)
            if (a.online and 1 or 0) ~= (b.online and 1 or 0) then
                return a.online and true or false
            end
            local ra = math.max(a.lastSeen or 0, a.ts or 0)
            local rb = math.max(b.lastSeen or 0, b.ts or 0)
            if ra ~= rb then return ra > rb end
            return a.display < b.display
        end)
        local list = { me }
        for _, p in ipairs(peers) do list[#list + 1] = p end
        return list
    end

    local function MakeCharList(page, onClick)
        local listContainer = CreateFrame("Frame", nil, page)
        listContainer:SetPoint("TOPLEFT", 20, -20)
        listContainer:SetPoint("BOTTOMRIGHT", -20, 40)

        local emptyState = Label(page, 11, COLORS.muted)
        emptyState:SetPoint("TOPLEFT", listContainer, "TOPLEFT", 2, -60)
        emptyState:SetText(NS.L("Aucun ami détecté.\nRejoins un groupe ou une guilde où l'addon est installé."))
        emptyState:SetJustifyH("LEFT")
        emptyState:Hide()

        local rows = {}
        local function CreateRow(i)
            local row = CreateFrame("Button", nil, listContainer)
            row:SetHeight(44)
            row:SetPoint("TOPLEFT", 0, -((i - 1) * 48))
            row:SetPoint("TOPRIGHT", 0, -((i - 1) * 48))
            local rbg = Solid(row)
            rbg:SetAllPoints()
            rbg:SetVertexColor(unpack(COLORS.panel))
            Border(row, COLORS.line)
            local hl = row:CreateTexture(nil, "HIGHLIGHT")
            hl:SetTexture(WHITE)
            hl:SetAllPoints()
            hl:SetVertexColor(1, 1, 1, 0.04)

            row.dot = Circle(row, 8, "ARTWORK")
            row.dot:SetPoint("LEFT", 14, 0)
            row.name = Label(row, 13)
            row.name:SetPoint("TOPLEFT", 32, -8)
            row.meta = Label(row, 10, COLORS.muted)
            row.meta:SetPoint("BOTTOMLEFT", 32, 8)
            row.chev = Label(row, 14, COLORS.muted)
            row.chev:SetPoint("RIGHT", -14, 0)
            row.chev:SetText("»")

            row:SetScript("OnClick", function(self) onClick(self.key) end)
            rows[i] = row
            return row
        end

        return function()
            local entries = BuildEntries()
            local maxRows = 8
            for i, e in ipairs(entries) do
                if i > maxRows then break end
                local row = rows[i] or CreateRow(i)
                row.key = e.key
                local suffix = e.isMe and ("  |cff8b8fa3" .. NS.L("(toi)") .. "|r") or ""
                row.name:SetText(("|c%s%s|r%s"):format(ClassColorStr(e.class), e.display, suffix))
                local bits = {}
                bits[#bits + 1] = ClassName(e.class)
                if e.level and e.level > 0 then bits[#bits + 1] = NS.L("Niveau ") ..e.level end
                if e.ilvl and e.ilvl > 0 then bits[#bits + 1] = "ilvl " .. e.ilvl end
                local spec = SpecName(e.specID)
                if spec then bits[#bits + 1] = spec end
                if not e.isMe and not e.online then
                    local seen = RelTime(math.max(e.lastSeen or 0, e.ts or 0))
                    if seen then bits[#bits + 1] = "vu " .. seen end
                end
                row.meta:SetText(table.concat(bits, " · "))
                if e.isMe then
                    row.dot:SetVertexColor(unpack(COLORS.accent))
                elseif e.online then
                    row.dot:SetVertexColor(unpack(COLORS.green))
                else
                    row.dot:SetVertexColor(unpack(COLORS.gray))
                end
                row:Show()
            end
            for i = #entries + 1, #rows do rows[i]:Hide() end
            emptyState:SetShown(#entries == 1)
        end
    end

    -- ============================================================
    -- PAGE : ACCUEIL — tableau de bord (fonction dédiée, hors BuildUI)
    -- ============================================================
    UpdateHomePage = BuildHomePage(content, pages, function(k) ShowPage(k) end)

    -- ============================================================
    -- PAGE : OPTIMISATION — liste
    -- ============================================================
    local optim = CreateFrame("Frame", nil, content)
    optim:SetAllPoints()
    optim:Hide()
    pages.optim = optim

    refreshOptimList = MakeCharList(optim, function(key) ShowDetail(key) end)

    -- ============================================================
    -- PAGE : OPTIMISATION — détail (code SimC)
    -- ============================================================
    local detail = CreateFrame("Frame", nil, content)
    detail:SetAllPoints()
    detail:Hide()
    pages.detail = detail

    local back = MakeButton(detail, NS.L("« Retour"), 84, 22)
    back:SetPoint("TOPLEFT", 20, -12)
    back:SetScript("OnClick", function() ShowPage("optim") end)

    local dName = Label(detail, 16)
    dName:SetPoint("TOPLEFT", 20, -46)

    local dMeta = Label(detail, 11, COLORS.muted)
    dMeta:SetPoint("TOPLEFT", 20, -68)
    dMeta:SetWidth(400)
    dMeta:SetJustifyH("LEFT")

    local refresh = MakeButton(detail, NS.L("Actualiser"), 110, 26)
    refresh:SetPoint("TOPRIGHT", -20, -44)

    local dStatus = Label(detail, 10, COLORS.muted)
    dStatus:SetPoint("TOPRIGHT", -20, -76)

    local step1 = Label(detail, 12)
    step1:SetPoint("TOPLEFT", 20, -92)
    step1:SetText((NS.L("|cff%s1|r · Ouvre Raidbots :")):format(NS.ACCENT))

    local urlContainer = CreateFrame("Frame", nil, detail)
    urlContainer:SetHeight(26)
    urlContainer:SetPoint("TOPLEFT", 20, -110)
    urlContainer:SetPoint("TOPRIGHT", -20, -110)
    local ubg = Solid(urlContainer)
    ubg:SetAllPoints()
    ubg:SetVertexColor(unpack(COLORS.field))
    Border(urlContainer, COLORS.line)
    local urlBox = MakeCopyBox(urlContainer, false)
    urlBox:SetPoint("TOPLEFT", 8, 0)
    urlBox:SetPoint("BOTTOMRIGHT", -8, 0)
    urlBox:SetLockedText(RAIDBOTS_URL)
    urlContainer:EnableMouse(true)
    urlContainer:SetScript("OnMouseDown", function() urlBox:SetFocus() end)

    local step2 = Label(detail, 12)
    step2:SetPoint("TOPLEFT", 20, -150)
    step2:SetText((NS.L("|cff%s2|r · Copie le code (clic = tout sélectionner, puis Ctrl+C) :")):format(NS.ACCENT))

    local codeContainer = CreateFrame("Frame", nil, detail)
    codeContainer:SetPoint("TOPLEFT", 20, -168)
    codeContainer:SetPoint("BOTTOMRIGHT", -20, 38)
    local cobg = Solid(codeContainer)
    cobg:SetAllPoints()
    cobg:SetVertexColor(unpack(COLORS.field))
    Border(codeContainer, COLORS.line)

    local scroll = CreateFrame("ScrollFrame", nil, codeContainer, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 8, -8)
    scroll:SetPoint("BOTTOMRIGHT", -28, 8)
    local codeBox = MakeCopyBox(scroll, true)
    codeBox:SetWidth(530)
    codeBox:SetHeight(60)
    scroll:SetScrollChild(codeBox)
    scroll:SetScript("OnSizeChanged", function(self)
        codeBox:SetWidth(self:GetWidth())
    end)
    codeContainer:EnableMouse(true)
    codeContainer:SetScript("OnMouseDown", function() codeBox:SetFocus() end)

    local step3 = Label(detail, 10, COLORS.muted)
    step3:SetPoint("BOTTOMLEFT", 20, 16)
    step3:SetText((NS.L("|cff%s3|r · Colle sur Raidbots et lance la simulation.")):format(NS.ACCENT))

    local function SetCode(text)
        codeBox:SetLockedText(text or "")
        scroll:SetVerticalScroll(0)
    end

    FillDetail = function(key)
        if key == "ME" then
            if not (NS.db and NS.db.myProfile) and NS.UpdateMyProfile then
                NS.UpdateMyProfile(false)
            end
            local mp = NS.db and NS.db.myProfile
            local myMeta = (NS.GetMyMeta and NS.GetMyMeta()) or {}
            local class = (mp and mp.class) or myMeta.class
            dName:SetText(("|c%s%s|r  |cff8b8fa3%s|r"):format(ClassColorStr(class), UnitName("player") or "?", NS.L("(toi)")))
            local bits = { ClassName(class) }
            local lvl = (mp and mp.level) or myMeta.level
            local ilvl = (mp and mp.ilvl) or myMeta.ilvl
            if lvl and lvl > 0 then bits[#bits + 1] = NS.L("Niveau ") ..lvl end
            if ilvl and ilvl > 0 then bits[#bits + 1] = "ilvl " .. ilvl end
            local spec = SpecName((mp and mp.specID) or myMeta.specID)
            if spec then bits[#bits + 1] = spec end
            dMeta:SetText(table.concat(bits, " · "))
            SetCode(mp and mp.simc or NS.L("# Impossible de générer le code pour l'instant."))
            dStatus:SetText(mp and mp.ts and (NS.L("Code généré ") .. (RelTime(mp.ts) or "")) or "")
        else
            local p = NS.db and NS.db.peers and NS.db.peers[key]
            if not p then ShowPage("optim") return end
            local display = key:match("^(.-)%-") or key
            dName:SetText(("|c%s%s|r"):format(ClassColorStr(p.class), display))
            local bits = { ClassName(p.class) }
            if p.level and p.level > 0 then bits[#bits + 1] = NS.L("Niveau ") ..p.level end
            if p.ilvl and p.ilvl > 0 then bits[#bits + 1] = "ilvl " .. p.ilvl end
            local spec = SpecName(p.specID)
            if spec then bits[#bits + 1] = spec end
            bits[#bits + 1] = p.online and ("|cff22c55e" .. NS.L("en ligne") .. "|r") or NS.L("hors ligne")
            dMeta:SetText(table.concat(bits, " · "))
            if p.simc then
                SetCode(p.simc)
                local stale = p.profileTs and p.ts and p.profileTs > p.ts
                dStatus:SetText(((NS.L("Code reçu %s")):format(RelTime(p.ts) or ""))
                    .. (stale and NS.L("  |cffeab308(plus récent dispo)|r") or ""))
            else
                SetCode(NS.L("# Pas encore de code reçu pour ce personnage.\n")
                    .. NS.L("# Il doit être en ligne avec l'addon installé.\n")
                    .. NS.L("# Clique « Actualiser » pour lui demander son code."))
                dStatus:SetText("")
            end
        end
    end

    refresh:SetScript("OnClick", function()
        if not currentDetail then return end
        if currentDetail == "ME" then
            if NS.UpdateMyProfile then NS.UpdateMyProfile(true) end
            FillDetail("ME")
        else
            if NS.Sync and NS.Sync.available then
                NS.Sync.RequestProfile(currentDetail)
                dStatus:SetText(NS.L("demande envoyée…"))
            else
                dStatus:SetText(NS.L("synchro indisponible"))
            end
        end
    end)

    ShowDetail = function(key)
        currentDetail = key
        FillDetail(key)
        ShowPage("detail")
        -- demande auto si le pair est en ligne et que son code est absent/dépassé
        if key ~= "ME" and NS.Sync and NS.Sync.available then
            local p = NS.db and NS.db.peers and NS.db.peers[key]
            if p and p.online and (not p.simc or ((p.profileTs or 0) > (p.ts or 0))) then
                NS.Sync.RequestProfile(key)
                dStatus:SetText(NS.L("demande envoyée…"))
            end
        end
    end

    -- ============================================================
    -- PAGE : INVENTAIRE — liste
    -- ============================================================
    local inv = CreateFrame("Frame", nil, content)
    inv:SetAllPoints()
    inv:Hide()
    pages.inv = inv

    -- clé "Nom-Royaume" d'une unité / de soi : utilitaire partagé (Core.lua)
    local UnitKeyOf = NS.UnitKey
    -- set des clés du groupe (party/raid), self inclus
    local function GroupKeySet()
        local set = {}
        local me = UnitKeyOf("player"); if me then set[me] = true end
        if IsInRaid() then
            for i = 1, 40 do local k = UnitKeyOf("raid" .. i); if k then set[k] = true end end
        elseif IsInGroup() then
            for i = 1, 4 do local k = UnitKeyOf("party" .. i); if k then set[k] = true end end
        end
        return set
    end

    local invFilter = nil -- itemID sélectionné (filtre), ou nil

    -- ----- Panneau gauche : mes consommables (cliquables → filtre) -----
    local invLeftTitle = Label(inv, 12)
    invLeftTitle:SetPoint("TOPLEFT", 20, -14)
    invLeftTitle:SetText(NS.L("Mes consommables"))

    local myItemRows = {}
    for i, t in ipairs(NS.TRACKED_ITEMS or {}) do
        local id = t.ids and t.ids[1]
        local row = CreateFrame("Button", nil, inv)
        row:SetSize(272, 26)
        row:Hide()
        local rbg = Solid(row); rbg:SetAllPoints(); rbg:SetVertexColor(unpack(COLORS.panel))
        Border(row, COLORS.line)
        row.sel = Solid(row, "ARTWORK")
        row.sel:SetPoint("TOPLEFT"); row.sel:SetPoint("BOTTOMLEFT"); row.sel:SetWidth(3)
        row.sel:SetVertexColor(unpack(COLORS.accent)); row.sel:Hide()
        local hl = row:CreateTexture(nil, "HIGHLIGHT"); hl:SetTexture(WHITE); hl:SetAllPoints(); hl:SetVertexColor(1, 1, 1, 0.05)
        row.icon = row:CreateTexture(nil, "ARTWORK")
        row.icon:SetSize(18, 18); row.icon:SetPoint("LEFT", 8, 0); row.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
        row.name = Label(row, 11)
        row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 0); row.name:SetWidth(180); row.name:SetJustifyH("LEFT"); row.name:SetWordWrap(false)
        row.name:SetText(t.fallback)
        row.count = Label(row, 12); row.count:SetPoint("RIGHT", -8, 0)
        row.itemID = id
        if id and Item and Item.CreateFromItemID then
            local it = Item:CreateFromItemID(id)
            it:ContinueOnItemLoad(function()
                row.name:SetText(it:GetItemName() or t.fallback)
                row.icon:SetTexture(it:GetItemIcon())
            end)
        end
        row:SetScript("OnClick", function()
            invFilter = (invFilter == id) and nil or id
            if refreshInvList then refreshInvList() end
        end)
        myItemRows[i] = row
    end

    -- ----- Panneau droit : détenteurs (groupe) / liste des persos -----
    local invRightTitle = Label(inv, 12)
    invRightTitle:SetPoint("TOPLEFT", 312, -14)
    local invClear = MakeButton(inv, NS.L("Tout afficher"), 100, 20)
    invClear:SetPoint("TOPRIGHT", -20, -12)
    invClear:SetScript("OnClick", function()
        invFilter = nil
        if refreshInvList then refreshInvList() end
    end)
    local invEmpty = Label(inv, 11, COLORS.muted)
    invEmpty:SetPoint("TOPLEFT", 312, -46)
    invEmpty:SetWidth(276); invEmpty:SetJustifyH("LEFT")

    local holderRows = {}
    local function GetHolderRow(i)
        if holderRows[i] then return holderRows[i] end
        local row = CreateFrame("Button", nil, inv)
        row:SetHeight(26)
        row:SetPoint("TOPLEFT", 312, -(40 + (i - 1) * 30))
        row:SetPoint("TOPRIGHT", -20, -(40 + (i - 1) * 30))
        local rbg = Solid(row); rbg:SetAllPoints(); rbg:SetVertexColor(unpack(COLORS.panel))
        Border(row, COLORS.line)
        local hl = row:CreateTexture(nil, "HIGHLIGHT"); hl:SetTexture(WHITE); hl:SetAllPoints(); hl:SetVertexColor(1, 1, 1, 0.05)
        row.dot = Circle(row, 7, "ARTWORK"); row.dot:SetPoint("LEFT", 8, 0)
        row.name = Label(row, 12); row.name:SetPoint("LEFT", 22, 0); row.name:SetJustifyH("LEFT"); row.name:SetWordWrap(false)
        row.count = Label(row, 12); row.count:SetPoint("RIGHT", -10, 0)
        holderRows[i] = row
        return row
    end

    -- Rendu SEUL (lecture du cache) → instantané. Le recalcul des quantités
    -- (léger) se fait à l'ouverture de l'onglet, pas à chaque clic.
    refreshInvList = function()
        local low = NS.LOW_STOCK or 5
        local mp = NS.db and NS.db.myProfile
        local myItems = (mp and mp.items) or {}
        local shownLeft = 0
        for _, row in ipairs(myItemRows) do
            local c = row.itemID and myItems[row.itemID]
            if c == nil then
                row:Hide()
            else
                shownLeft = shownLeft + 1
                row:ClearAllPoints()
                row:SetPoint("TOPLEFT", 20, -(40 + (shownLeft - 1) * 30))
                row:SetWidth(272)
                if c <= 0 then row.count:SetText("0"); row.count:SetTextColor(0.92, 0.28, 0.32, 1)
                elseif c <= low then row.count:SetText(FormatNumber(c)); row.count:SetTextColor(0.98, 0.80, 0.25, 1)
                else row.count:SetText(FormatNumber(c)); row.count:SetTextColor(unpack(COLORS.text)) end
                row.sel:SetShown(invFilter == row.itemID)
                row:Show()
            end
        end

        -- (b) panneau droit
        invClear:SetShown(invFilter ~= nil)
        if invFilter then
            invRightTitle:SetText(NS.L("Qui en a (groupe)"))
            local set = GroupKeySet()
            local meKey = UnitKeyOf("player")
            local holders = {}
            local sc = myItems[invFilter]
            if sc and sc > 0 then
                holders[#holders + 1] = { name = UnitName("player") or "?", class = mp and mp.class, count = sc, isMe = true, online = true }
            end
            for k, p in pairs((NS.db and NS.db.peers) or {}) do
                if set[k] and k ~= meKey and p.items and (p.items[invFilter] or 0) > 0 then
                    holders[#holders + 1] = { name = k:match("^(.-)%-") or k, class = p.class, count = p.items[invFilter], online = p.online }
                end
            end
            table.sort(holders, function(a, b)
                if a.count ~= b.count then return a.count > b.count end
                return a.name < b.name
            end)
            for i, h in ipairs(holders) do
                local row = GetHolderRow(i)
                row.name:SetText(("|c%s%s|r%s"):format(ClassColorStr(h.class), h.name, h.isMe and (" |cff8b8fa3" .. NS.L("(toi)") .. "|r") or ""))
                row.count:SetText(FormatNumber(h.count)); row.count:SetTextColor(unpack(COLORS.text))
                row.dot:SetVertexColor(unpack(h.isMe and COLORS.accent or (h.online and COLORS.green or COLORS.gray)))
                row:SetScript("OnClick", nil)
                row:Show()
            end
            for i = #holders + 1, #holderRows do holderRows[i]:Hide() end
            invEmpty:SetText(NS.L("Personne dans ton groupe n'en a."))
            invEmpty:SetShown(#holders == 0)
        else
            invRightTitle:SetText(NS.L("Tous les personnages"))
            local entries = BuildEntries()
            for i, e in ipairs(entries) do
                local row = GetHolderRow(i)
                row.name:SetText(("|c%s%s|r%s"):format(ClassColorStr(e.class), e.display, e.isMe and (" |cff8b8fa3" .. NS.L("(toi)") .. "|r") or ""))
                row.count:SetText("»"); row.count:SetTextColor(unpack(COLORS.muted))
                row.dot:SetVertexColor(unpack(e.isMe and COLORS.accent or (e.online and COLORS.green or COLORS.gray)))
                row.key = e.key
                row:SetScript("OnClick", function(self) ShowInvDetail(self.key) end)
                row:Show()
            end
            for i = #entries + 1, #holderRows do holderRows[i]:Hide() end
            invEmpty:SetText(NS.L("Clique un consommable à gauche pour voir qui en a."))
            invEmpty:SetShown(#entries <= 1)
        end
    end

    -- ============================================================
    -- PAGE : INVENTAIRE — détail
    -- ============================================================
    local invdetail = CreateFrame("Frame", nil, content)
    invdetail:SetAllPoints()
    invdetail:Hide()
    pages.invdetail = invdetail

    local iback = MakeButton(invdetail, NS.L("« Retour"), 84, 22)
    iback:SetPoint("TOPLEFT", 20, -12)
    iback:SetScript("OnClick", function() ShowPage("inv") end)

    local iName = Label(invdetail, 16)
    iName:SetPoint("TOPLEFT", 20, -46)

    local iMeta = Label(invdetail, 11, COLORS.muted)
    iMeta:SetPoint("TOPLEFT", 20, -68)
    iMeta:SetWidth(400)
    iMeta:SetJustifyH("LEFT")

    local iStatus = Label(invdetail, 10, COLORS.muted)
    iStatus:SetPoint("TOPRIGHT", -20, -50)

    -- seuil « stock bas » : quantité ≤ LOW_STOCK → affichée en jaune
    local LOW_STOCK = (NS.LOW_STOCK) or 5

    -- lignes : consommables suivis (grille 2 colonnes, positionnées
    -- dynamiquement — les objets jamais possédés sont masqués)
    local itemRows = {}
    for i, t in ipairs(NS.TRACKED_ITEMS or {}) do
        local row = CreateFrame("Frame", nil, invdetail)
        row:SetSize(276, 30)
        row:Hide()
        local rbg = Solid(row)
        rbg:SetAllPoints()
        rbg:SetVertexColor(unpack(COLORS.panel))
        Border(row, COLORS.line)

        row.icon = row:CreateTexture(nil, "ARTWORK")
        row.icon:SetSize(20, 20)
        row.icon:SetPoint("LEFT", 6, 0)
        row.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)

        row.name = Label(row, 11)
        row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
        row.name:SetWidth(180)
        row.name:SetJustifyH("LEFT")
        row.name:SetWordWrap(false)
        row.name:SetText(t.fallback)

        row.count = Label(row, 12)
        row.count:SetPoint("RIGHT", -10, 0)

        row.itemID = t.ids and t.ids[1] -- clé = premier ID
        itemRows[i] = row

        -- nom + icône localisés (FR sur client FR), chargés en asynchrone
        if row.itemID and Item and Item.CreateFromItemID then
            local it = Item:CreateFromItemID(row.itemID)
            it:ContinueOnItemLoad(function()
                row.name:SetText(it:GetItemName() or t.fallback)
                row.icon:SetTexture(it:GetItemIcon())
            end)
        end
    end

    local iNote = Label(invdetail, 10, COLORS.muted)
    iNote:SetPoint("BOTTOMLEFT", 20, 16)
    iNote:SetWidth(560)
    iNote:SetJustifyH("LEFT")

    FillInvDetail = function(key)
        -- affiche les lignes : objet jamais possédé → masqué ;
        -- possédé mais épuisé → « 0 » en rouge ; sinon → quantité.
        local function SetValues(items)
            local visible = 0
            for _, row in ipairs(itemRows) do
                local c = items and row.itemID and items[row.itemID]
                if c == nil then
                    row:Hide()
                else
                    local col = visible % 2
                    local r = math.floor(visible / 2)
                    row:ClearAllPoints()
                    row:SetPoint("TOPLEFT", 20 + col * 292, -(110 + r * 36))
                    if c <= 0 then
                        row.count:SetText("0")
                        row.count:SetTextColor(0.92, 0.28, 0.32, 1)      -- rouge : épuisé
                    elseif c <= LOW_STOCK then
                        row.count:SetText(FormatNumber(c))
                        row.count:SetTextColor(0.98, 0.80, 0.25, 1)      -- jaune : stock bas
                    else
                        row.count:SetText(FormatNumber(c))
                        row.count:SetTextColor(unpack(COLORS.text))
                    end
                    row:Show()
                    visible = visible + 1
                end
            end
            return visible
        end

        if key == "ME" then
            -- régénère le profil : compteurs à jour + mémorisation
            -- des objets déjà possédés (itemsSeen)
            if NS.UpdateMyProfile then NS.UpdateMyProfile(false) end
            local myMeta = (NS.GetMyMeta and NS.GetMyMeta()) or {}
            local mp = NS.db and NS.db.myProfile
            local class = (mp and mp.class) or myMeta.class
            iName:SetText(("|c%s%s|r  |cff8b8fa3%s|r"):format(ClassColorStr(class), UnitName("player") or "?", NS.L("(toi)")))
            local bits = { ClassName(class) }
            local lvl = (mp and mp.level) or myMeta.level
            if lvl and lvl > 0 then bits[#bits + 1] = NS.L("Niveau ") ..lvl end
            iMeta:SetText(table.concat(bits, " · "))
            local shown = SetValues(mp and mp.items)
            iStatus:SetText(NS.L("en direct"))
            if shown == 0 then
                iNote:SetText(NS.L("Aucun consommable suivi possédé pour l'instant."))
            else
                iNote:SetText((NS.L("Sacs + banque · |cffea48500|r = épuisé · |cfffacd40≤%d|r = stock bas")):format(LOW_STOCK))
            end
        else
            local p = NS.db and NS.db.peers and NS.db.peers[key]
            if not p then ShowPage("inv") return end
            local display = key:match("^(.-)%-") or key
            iName:SetText(("|c%s%s|r"):format(ClassColorStr(p.class), display))
            local bits = { ClassName(p.class) }
            if p.level and p.level > 0 then bits[#bits + 1] = NS.L("Niveau ") ..p.level end
            bits[#bits + 1] = p.online and ("|cff22c55e" .. NS.L("en ligne") .. "|r") or NS.L("hors ligne")
            iMeta:SetText(table.concat(bits, " · "))
            local shown = SetValues(p.items)
            local seen = RelTime(math.max(p.lastSeen or 0, p.ts or 0))
            iStatus:SetText(seen and (NS.L("synchro ") .. seen) or "")
            if p.items == nil then
                iNote:SetText(NS.L("Inventaire non reçu (version d'addon trop ancienne)."))
            elseif shown == 0 then
                iNote:SetText(NS.L("Aucun consommable suivi possédé."))
            else
                iNote:SetText((NS.L("Dernière synchro · |cffea48500|r = épuisé · |cfffacd40≤%d|r = stock bas")):format(LOW_STOCK))
            end
        end
    end

    ShowInvDetail = function(key)
        currentInv = key
        FillInvDetail(key)
        ShowPage("invdetail")
    end

    -- ============================================================
    -- PAGE : KICKS
    -- ============================================================
    local kicks = CreateFrame("Frame", nil, content)
    kicks:SetAllPoints()
    kicks:Hide()
    pages.kicks = kicks

    local kTitle = Label(kicks, 13)
    kTitle:SetPoint("TOPLEFT", 20, -18)
    kTitle:SetText(NS.L("Suivi des interruptions (kicks)"))

    -- case à cocher : afficher / masquer l'encadré
    local cb = CreateFrame("Button", nil, kicks)
    cb:SetSize(20, 20)
    cb:SetPoint("TOPLEFT", 20, -52)
    local cbBg = Solid(cb)
    cbBg:SetAllPoints()
    cbBg:SetVertexColor(unpack(COLORS.field))
    Border(cb, COLORS.line)
    local cbInner = Solid(cb, "ARTWORK")
    cbInner:SetSize(12, 12)
    cbInner:SetPoint("CENTER")
    cbInner:SetVertexColor(unpack(COLORS.accent))

    local cbLabel = Label(kicks, 12)
    cbLabel:SetPoint("LEFT", cb, "RIGHT", 10, 0)
    cbLabel:SetText(NS.L("Afficher l'encadré des kicks dans l'interface"))

    local function UpdateCheckbox()
        cbInner:SetShown(NS.Kicks and NS.Kicks.IsEnabled() or false)
    end
    cb:SetScript("OnClick", function()
        if not NS.Kicks then return end
        NS.Kicks.SetEnabled(not NS.Kicks.IsEnabled())
        UpdateCheckbox()
    end)

    -- case : verrouiller la taille (masque la poignée). Bloc local (limite Lua).
    do
        local lockCb = CreateFrame("Button", nil, kicks)
        lockCb:SetSize(20, 20)
        lockCb:SetPoint("TOPLEFT", 340, -52)
        local lockBg = Solid(lockCb); lockBg:SetAllPoints(); lockBg:SetVertexColor(unpack(COLORS.field))
        Border(lockCb, COLORS.line)
        local lockInner = Solid(lockCb, "ARTWORK")
        lockInner:SetSize(12, 12); lockInner:SetPoint("CENTER"); lockInner:SetVertexColor(unpack(COLORS.accent))
        local lockLbl = Label(kicks, 12)
        lockLbl:SetPoint("LEFT", lockCb, "RIGHT", 10, 0)
        lockLbl:SetText(NS.L("Verrouiller le cadre"))
        lockInner:SetShown(NS.Kicks and NS.Kicks.IsLocked() or false)
        lockCb:SetScript("OnClick", function()
            if NS.Kicks and NS.Kicks.SetLocked then
                NS.Kicks.SetLocked(not NS.Kicks.IsLocked())
                lockInner:SetShown(NS.Kicks.IsLocked())
            end
        end)
    end

    -- case : rotation de kick (cadres vert/orange). Bloc local (limite Lua).
    do
        local rc = CreateFrame("Button", nil, kicks)
        rc:SetSize(20, 20)
        rc:SetPoint("TOPLEFT", 340, -84)
        local rbg = Solid(rc); rbg:SetAllPoints(); rbg:SetVertexColor(unpack(COLORS.field))
        Border(rc, COLORS.line)
        local rin = Solid(rc, "ARTWORK")
        rin:SetSize(12, 12); rin:SetPoint("CENTER"); rin:SetVertexColor(unpack(COLORS.accent))
        local rl = Label(kicks, 12)
        rl:SetPoint("LEFT", rc, "RIGHT", 10, 0)
        rl:SetText(NS.L("Rotation de kick (badges 1/2)"))
        local function rget() return not (NS.db and NS.db.kickRotation == false) end
        rin:SetShown(rget())
        rc:SetScript("OnClick", function()
            if NS.db then NS.db.kickRotation = not rget() end
            rin:SetShown(rget())
        end)
    end

    local kReset = MakeButton(kicks, NS.L("Réinitialiser la position"), 170, 24)
    kReset:SetPoint("TOPLEFT", 20, -88)
    kReset:SetScript("OnClick", function()
        if NS.Kicks then NS.Kicks.ResetPosition() end
    end)

    -- opacité du fond de l'encadré (curseur)
    local opLabel = Label(kicks, 12)
    opLabel:SetPoint("TOPLEFT", 20, -128)

    local track = CreateFrame("Frame", nil, kicks)
    track:SetSize(180, 12)
    track:SetPoint("TOPLEFT", 20, -148)
    local trackBg = Solid(track)
    trackBg:SetAllPoints()
    trackBg:SetVertexColor(unpack(COLORS.field))
    Border(track, COLORS.line)
    local trackFill = Solid(track, "ARTWORK")
    trackFill:SetPoint("TOPLEFT", 1, -1)
    trackFill:SetPoint("BOTTOMLEFT", 1, 1)
    trackFill:SetVertexColor(unpack(COLORS.accent))

    local function SetOpacityDisplay(a)
        trackFill:SetWidth(math.max(1, 178 * a))
        opLabel:SetText((NS.L("Opacité du fond de l'encadré : |cff%s%d%%|r")):format(NS.ACCENT, math.floor(a * 100 + 0.5)))
    end
    local function OpacityFromCursor()
        local scale = track:GetEffectiveScale()
        local x = GetCursorPosition() / scale - track:GetLeft()
        local a = x / track:GetWidth()
        if a < 0 then a = 0 elseif a > 1 then a = 1 end
        a = math.floor(a * 20 + 0.5) / 20
        SetOpacityDisplay(a)
        if NS.Kicks and NS.Kicks.SetBgAlpha then NS.Kicks.SetBgAlpha(a) end
    end
    track:EnableMouse(true)
    track:SetScript("OnMouseDown", function(self)
        OpacityFromCursor()
        self:SetScript("OnUpdate", OpacityFromCursor)
    end)
    track:SetScript("OnMouseUp", function(self)
        self:SetScript("OnUpdate", nil)
    end)

    local kHow = Label(kicks, 11, COLORS.muted)
    kHow:SetPoint("TOPLEFT", 20, -180)
    kHow:SetWidth(560)
    kHow:SetJustifyH("LEFT")
    kHow:SetText(NS.L("|cff22c55ePRÊT|r = kick disponible · |cfff2a33c12 s|r = en recharge\n|TInterface\\RaidFrame\\ReadyCheck-Ready:14|t = sort bien interrompu · |TInterface\\RaidFrame\\ReadyCheck-NotReady:14|t = kick dans le vent"))

    local kTracked = Label(kicks, 11)
    kTracked:SetPoint("TOPLEFT", 20, -232)
    kTracked:SetWidth(560)
    kTracked:SetJustifyH("LEFT")

    -- Liste des joueurs dans la rotation de kick (coche = inclus). Auto selon
    -- si le joueur a une interruption ; décoche/recoche pour forcer. Bloc local.
    do
        local hdr = Label(kicks, 12)
        hdr:SetPoint("TOPLEFT", 20, -270)
        hdr:SetText(NS.L("Rotation de kick — joueurs inclus :"))
        local note = Label(kicks, 10, COLORS.muted)
        note:SetPoint("TOPLEFT", 20, -288)
        note:SetText(NS.L("Auto : coché si le joueur a une interruption. Décoche/recoche pour forcer."))
        local pool = {}
        local function refresh()
            local list = (NS.Kicks and NS.Kicks.GetRotationList()) or {}
            for _, r in ipairs(pool) do r.cb:Hide(); r.lbl:Hide() end
            local y = -310
            for i, p in ipairs(list) do
                local row = pool[i]
                if not row then
                    local cb = CreateFrame("Button", nil, kicks)
                    cb:SetSize(18, 18)
                    local bg = Solid(cb); bg:SetAllPoints(); bg:SetVertexColor(unpack(COLORS.field))
                    Border(cb, COLORS.line)
                    local inner = Solid(cb, "ARTWORK")
                    inner:SetSize(11, 11); inner:SetPoint("CENTER"); inner:SetVertexColor(unpack(COLORS.accent))
                    local lbl = Label(kicks, 12)
                    row = { cb = cb, inner = inner, lbl = lbl }
                    pool[i] = row
                end
                row.cb:ClearAllPoints(); row.cb:SetPoint("TOPLEFT", 24, y)
                row.inner:SetShown(p.inRotation)
                local col = p.class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[p.class]
                local hex = (col and col.colorStr) or "ffffffff"
                local tag = (p.hasKick == false) and "  |cff8a8f99(pas de kick)|r" or ""
                row.lbl:ClearAllPoints(); row.lbl:SetPoint("LEFT", row.cb, "RIGHT", 8, 0)
                row.lbl:SetText(("|c%s%s|r%s%s"):format(hex, p.name or "?",
                    p.me and "  |cff6a6f7a(toi)|r" or "", tag))
                row.cb:SetScript("OnClick", function()
                    if NS.Kicks and NS.Kicks.SetInRotation then
                        NS.Kicks.SetInRotation(p.key, not p.inRotation)
                        refresh()
                    end
                end)
                row.cb:Show(); row.lbl:Show()
                y = y - 24
            end
        end
        NS.RefreshKickRotationList = refresh
        refresh()
    end

    UpdateKicksPage = function()
        UpdateCheckbox()
        SetOpacityDisplay((NS.db and NS.db.kickBgAlpha) or 0.6)
        local txt = "Suivis actuellement : " .. ((NS.Kicks and NS.Kicks.GetRosterNames()) or "—")
        if NS.Kicks and NS.Kicks.available == false then
            txt = txt .. "\n|cffea4850Détection indisponible (restrictions Blizzard) — un /reload peut aider.|r"
        end
        kTracked:SetText(txt)
        if NS.RefreshKickRotationList then NS.RefreshKickRotationList() end
    end

    -- ============================================================
    -- PAGE : COOLDOWNS DE GROUPE (overlay + sélection des CD)
    -- ============================================================
    local cdPage = CreateFrame("Frame", nil, content)
    cdPage:SetAllPoints()
    cdPage:Hide()
    pages.cd = cdPage

    local cdTitle = Label(cdPage, 13)
    cdTitle:SetPoint("TOPLEFT", 20, -16)
    cdTitle:SetText(NS.L("Tracker de cooldowns de groupe"))

    -- case : afficher / masquer l'encadré
    local cdCb = CreateFrame("Button", nil, cdPage)
    cdCb:SetSize(20, 20)
    cdCb:SetPoint("TOPLEFT", 20, -46)
    local cdCbBg = Solid(cdCb)
    cdCbBg:SetAllPoints()
    cdCbBg:SetVertexColor(unpack(COLORS.field))
    Border(cdCb, COLORS.line)
    local cdCbInner = Solid(cdCb, "ARTWORK")
    cdCbInner:SetSize(12, 12)
    cdCbInner:SetPoint("CENTER")
    cdCbInner:SetVertexColor(unpack(COLORS.accent))
    local cdCbLabel = Label(cdPage, 12)
    cdCbLabel:SetPoint("LEFT", cdCb, "RIGHT", 10, 0)
    cdCbLabel:SetText(NS.L("Afficher l'encadré des cooldowns"))
    local function UpdateCDCheckbox()
        cdCbInner:SetShown(NS.Cooldowns and NS.Cooldowns.IsEnabled() or false)
    end
    cdCb:SetScript("OnClick", function()
        if not NS.Cooldowns then return end
        NS.Cooldowns.SetEnabled(not NS.Cooldowns.IsEnabled())
        UpdateCDCheckbox()
    end)

    local cdReset = MakeButton(cdPage, NS.L("Réinitialiser la position"), 170, 22)
    cdReset:SetPoint("TOPLEFT", 300, -45)
    cdReset:SetScript("OnClick", function()
        if NS.Cooldowns then NS.Cooldowns.ResetPosition() end
    end)

    -- opacité
    local cdOpLabel = Label(cdPage, 11)
    cdOpLabel:SetPoint("TOPLEFT", 20, -78)
    local cdTrack = CreateFrame("Frame", nil, cdPage)
    cdTrack:SetSize(180, 10)
    cdTrack:SetPoint("TOPLEFT", 20, -96)
    local cdTrackBg = Solid(cdTrack)
    cdTrackBg:SetAllPoints()
    cdTrackBg:SetVertexColor(unpack(COLORS.field))
    Border(cdTrack, COLORS.line)
    local cdTrackFill = Solid(cdTrack, "ARTWORK")
    cdTrackFill:SetPoint("TOPLEFT", 1, -1)
    cdTrackFill:SetPoint("BOTTOMLEFT", 1, 1)
    cdTrackFill:SetVertexColor(unpack(COLORS.accent))
    local function SetCDOpacityDisplay(a)
        cdTrackFill:SetWidth(math.max(1, 178 * a))
        cdOpLabel:SetText((NS.L("Opacité du fond : |cff%s%d%%|r")):format(NS.ACCENT, math.floor(a * 100 + 0.5)))
    end
    local function CDOpacityFromCursor()
        local scale = cdTrack:GetEffectiveScale()
        local x = GetCursorPosition() / scale - cdTrack:GetLeft()
        local a = x / cdTrack:GetWidth()
        if a < 0 then a = 0 elseif a > 1 then a = 1 end
        a = math.floor(a * 20 + 0.5) / 20
        SetCDOpacityDisplay(a)
        if NS.Cooldowns and NS.Cooldowns.SetBgAlpha then NS.Cooldowns.SetBgAlpha(a) end
    end
    cdTrack:EnableMouse(true)
    cdTrack:SetScript("OnMouseDown", function(self)
        CDOpacityFromCursor()
        self:SetScript("OnUpdate", CDOpacityFromCursor)
    end)
    cdTrack:SetScript("OnMouseUp", function(self) self:SetScript("OnUpdate", nil) end)

    -- cases (colonne droite) : catégorie dans l'infobulle + suivi des potions
    local function MakeCDToggle(yOff, text, getFn, setFn)
        local cb = CreateFrame("Button", nil, cdPage)
        cb:SetSize(20, 20)
        cb:SetPoint("TOPLEFT", 300, yOff)
        local bg = Solid(cb); bg:SetAllPoints(); bg:SetVertexColor(unpack(COLORS.field))
        Border(cb, COLORS.line)
        local inner = Solid(cb, "ARTWORK")
        inner:SetSize(12, 12); inner:SetPoint("CENTER"); inner:SetVertexColor(unpack(COLORS.accent))
        local lbl = Label(cdPage, 12)
        lbl:SetPoint("LEFT", cb, "RIGHT", 10, 0)
        lbl:SetText(text)
        inner:SetShown(getFn())
        cb:SetScript("OnClick", function() setFn(not getFn()); inner:SetShown(getFn()) end)
    end
    MakeCDToggle(-78, NS.L("Catégorie dans l'infobulle"),
        function() return not (NS.db and NS.db.cdShowCat == false) end,
        function(v) if NS.db then NS.db.cdShowCat = v end end)
    MakeCDToggle(-104, NS.L("Suivre les potions de combat"),
        function() return not (NS.db and NS.db.cdTrackPot == false) end,
        function(v) if NS.db then NS.db.cdTrackPot = v end end)
    MakeCDToggle(-130, NS.L("Verrouiller le cadre"),
        function() return NS.Cooldowns and NS.Cooldowns.IsLocked() or false end,
        function(v) if NS.Cooldowns and NS.Cooldowns.SetLocked then NS.Cooldowns.SetLocked(v) end end)

    -- Espacement vertical entre les lignes de membres (0 = collé). Bloc local
    -- (do/end) pour ne pas alourdir le compteur de variables de BuildUI.
    do
        local gapLbl = Label(cdPage, 12)
        gapLbl:SetPoint("TOPLEFT", 20, -116)
        gapLbl:SetText(NS.L("Espacement des lignes"))
        local gapMinus = MakeButton(cdPage, "−", 22, 20)
        gapMinus:SetPoint("LEFT", gapLbl, "RIGHT", 12, 0)
        local gapNum = Label(cdPage, 12)
        gapNum:SetPoint("LEFT", gapMinus, "RIGHT", 8, 0)
        gapNum:SetWidth(48); gapNum:SetJustifyH("CENTER")
        local gapPlus = MakeButton(cdPage, "+", 22, 20)
        gapPlus:SetPoint("LEFT", gapNum, "RIGHT", 8, 0)
        local function gapRefresh()
            gapNum:SetText(("%d px"):format((NS.db and NS.db.cdRowGap) or 0))
            if NS.Cooldowns and NS.Cooldowns.Refresh then NS.Cooldowns.Refresh() end
        end
        local function gapStep(d)
            if not NS.db then return end
            local g = (NS.db.cdRowGap or 0) + d
            if g < 0 then g = 0 elseif g > 30 then g = 30 end
            NS.db.cdRowGap = g
            gapRefresh()
        end
        gapMinus:SetScript("OnClick", function() gapStep(-2) end)
        gapPlus:SetScript("OnClick", function() gapStep(2) end)
        NS.RefreshCDPage = gapRefresh  -- resync l'affichage après un drag de la poignée
        gapRefresh()
    end

    -- Espacement horizontal pseudo ↔ 1re icône (= largeur de la colonne des
    -- noms) : plus petit → icônes collées au pseudo → fenêtre plus étroite.
    do
        local nwLbl = Label(cdPage, 12)
        nwLbl:SetPoint("TOPLEFT", 20, -140)
        nwLbl:SetText(NS.L("Espacement pseudo/icône"))
        local nwMinus = MakeButton(cdPage, "−", 22, 20)
        nwMinus:SetPoint("LEFT", nwLbl, "RIGHT", 12, 0)
        local nwNum = Label(cdPage, 12)
        nwNum:SetPoint("LEFT", nwMinus, "RIGHT", 8, 0)
        nwNum:SetWidth(48); nwNum:SetJustifyH("CENTER")
        local nwPlus = MakeButton(cdPage, "+", 22, 20)
        nwPlus:SetPoint("LEFT", nwNum, "RIGHT", 8, 0)
        local function nwRefresh()
            nwNum:SetText(("%d px"):format((NS.db and NS.db.cdNameW) or 92))
            if NS.Cooldowns and NS.Cooldowns.Refresh then NS.Cooldowns.Refresh() end
        end
        local function nwStep(d)
            if not NS.db then return end
            local w = (NS.db.cdNameW or 92) + d
            if w < 24 then w = 24 elseif w > 130 then w = 130 end
            NS.db.cdNameW = w
            nwRefresh()
        end
        nwMinus:SetScript("OnClick", function() nwStep(-4) end)
        nwPlus:SetScript("OnClick", function() nwStep(4) end)
        nwRefresh()
    end

    local cdSel = Label(cdPage, 12)
    cdSel:SetPoint("TOPLEFT", 20, -172)
    cdSel:SetText(NS.L("Blocs — clic = tout afficher / tout masquer :"))

    -- boutons par catégorie (bascule tout le bloc offensif/défensif/…)
    local cdBoxes = {}
    local function SetTypeShown(typ, shown)
        for sid, info in pairs(NS.COOLDOWNS or {}) do
            if info.type == typ and NS.db then
                NS.db.cdShow[sid] = shown
                if cdBoxes[sid] then cdBoxes[sid]:SetShown(shown) end
            end
        end
        if NS.Cooldowns and NS.Cooldowns.PruneHidden then NS.Cooldowns.PruneHidden() end
    end
    local function TypeAllShown(typ)
        for sid, info in pairs(NS.COOLDOWNS or {}) do
            if info.type == typ and not (NS.IsCDShown and NS.IsCDShown(sid)) then return false end
        end
        return true
    end
    local CATSHORT = { off = "Offensifs", def = "Défensifs", ext = "Externes", lust = "Bloodlust", res = "Résurr." }
    local catX = 20
    for _, ct in ipairs(NS.CD_TYPES or {}) do
        local btn = MakeButton(cdPage, NS.L(CATSHORT[ct.key] or ct.label), 92, 20)
        btn:SetPoint("TOPLEFT", catX, -192)
        btn:SetScript("OnClick", function()
            SetTypeShown(ct.key, not TypeAllShown(ct.key))
        end)
        catX = catX + 96
    end

    -- liste scrollable des CD par classe (coche = suivi individuel)
    local cdScroll = CreateFrame("ScrollFrame", "AsylumCompagnonCDScroll", cdPage, "UIPanelScrollFrameTemplate")
    cdScroll:SetPoint("TOPLEFT", 16, -220)
    cdScroll:SetPoint("BOTTOMRIGHT", -30, 14)
    local cdChild = CreateFrame("Frame", nil, cdScroll)
    cdChild:SetSize(520, 10)
    cdScroll:SetScrollChild(cdChild)

    local cdBuilt = false
    local TYPETAG = {
        off = "|cffed6b4dOff|r", def = "|cff5a99f2Déf|r", ext = "|cff8dd972Ext|r",
        lust = "|cffd98df2Lust|r", res = "|cfff2d159Rez|r",
    }
    local cdClasses = {}   -- { {class, header, arrow, rows={{box,lbl}}} }
    local cdExpanded = {}  -- [class] = true si déplié (replié par défaut)
    -- (re)positionne les entêtes de classe et, si dépliées, leurs coches
    local function RelayoutCD()
        local y = -4
        for _, cc in ipairs(cdClasses) do
            cc.header:ClearAllPoints()
            cc.header:SetPoint("TOPLEFT", 2, y)
            local open = cdExpanded[cc.class]
            cc.arrow:SetText(open and "−" or "+")
            y = y - 22
            for _, r in ipairs(cc.rows) do
                if open then
                    r.box:ClearAllPoints()
                    r.box:SetPoint("TOPLEFT", 24, y)
                    r.box:Show(); r.lbl:Show()
                    y = y - 20
                else
                    r.box:Hide(); r.lbl:Hide()
                end
            end
            y = y - 6
        end
        cdChild:SetHeight(-y + 10)
    end
    local function BuildCDList()
        if cdBuilt then return end
        cdBuilt = true
        for _, class in ipairs(NS.CLASS_ORDER or {}) do
            local list = {}
            for sid, info in pairs(NS.COOLDOWNS or {}) do
                if info.class == class then list[#list + 1] = { sid = sid, info = info } end
            end
            if #list > 0 then
                table.sort(list, function(a, b)
                    if a.info.type ~= b.info.type then return a.info.type < b.info.type end
                    return a.info.label < b.info.label
                end)
                -- entête de classe cliquable (déplie/replie)
                local header = CreateFrame("Button", nil, cdChild)
                header:SetSize(500, 20)
                local hHl = header:CreateTexture(nil, "HIGHLIGHT")
                hHl:SetTexture(WHITE)
                hHl:SetAllPoints()
                hHl:SetVertexColor(1, 1, 1, 0.05)
                local arrow = Label(header, 14, COLORS.accent)
                arrow:SetPoint("LEFT", 2, 0)
                arrow:SetWidth(14)
                arrow:SetJustifyH("LEFT")
                local htxt = Label(header, 12)
                htxt:SetPoint("LEFT", 18, 0)
                htxt:SetText(("|c%s%s|r  |cff8b8fa3(%d)|r"):format(ClassColorStr(class), NS.ClassLabel(class), #list))
                local cc = { class = class, header = header, arrow = arrow, rows = {} }
                for _, item in ipairs(list) do
                    local sid, info = item.sid, item.info
                    local box = CreateFrame("Button", nil, cdChild)
                    box:SetSize(16, 16)
                    local bg = Solid(box)
                    bg:SetAllPoints()
                    bg:SetVertexColor(unpack(COLORS.field))
                    Border(box, COLORS.line)
                    local inner = Solid(box, "ARTWORK")
                    inner:SetSize(10, 10)
                    inner:SetPoint("CENTER")
                    inner:SetVertexColor(unpack(COLORS.accent))
                    inner:SetShown(NS.IsCDShown and NS.IsCDShown(sid) or false)
                    cdBoxes[sid] = inner
                    local lbl = Label(cdChild, 11)
                    lbl:SetPoint("LEFT", box, "RIGHT", 8, 0)
                    lbl:SetText(("%s  %s  |cff8b8fa3%ds|r"):format(TYPETAG[info.type] or "", NS.SpellName(sid, info.label), info.cd))
                    box:SetScript("OnClick", function()
                        if not NS.db then return end
                        local newv = not (NS.IsCDShown and NS.IsCDShown(sid))
                        NS.db.cdShow[sid] = newv
                        inner:SetShown(newv)
                        if NS.Cooldowns and NS.Cooldowns.PruneHidden then NS.Cooldowns.PruneHidden() end
                    end)
                    cc.rows[#cc.rows + 1] = { box = box, lbl = lbl }
                end
                header:SetScript("OnClick", function()
                    cdExpanded[class] = not cdExpanded[class]
                    RelayoutCD()
                end)
                cdClasses[#cdClasses + 1] = cc
            end
        end
        RelayoutCD()
    end

    UpdateCDPage = function()
        UpdateCDCheckbox()
        SetCDOpacityDisplay((NS.db and NS.db.cdBgAlpha) or 0.6)
        BuildCDList()
    end

    -- ============================================================
    -- PAGE : BUFFS & AURAS (check de préparation raid/M+)
    -- ============================================================
    local buffsPage = CreateFrame("Frame", nil, content)
    buffsPage:SetAllPoints()
    buffsPage:Hide()
    pages.buffs = buffsPage

    local bTitle = Label(buffsPage, 13)
    bTitle:SetPoint("TOPLEFT", 20, -18)
    bTitle:SetText(NS.L("Préparation du groupe"))

    local bRefresh = MakeButton(buffsPage, NS.L("Actualiser"), 110, 24)
    bRefresh:SetPoint("TOPRIGHT", -20, -14)

    local bGrid = CreateFrame("Frame", nil, buffsPage)
    bGrid:SetPoint("TOPLEFT", 20, -54)
    bGrid:SetPoint("BOTTOMRIGHT", -20, 16)

    local bHeaders, bNames, bCells = {}, {}, {}
    local eHeaders, eNames, eCells = {}, {}, {}
    local eTitle
    local CELL_OK   = "|TInterface\\RaidFrame\\ReadyCheck-Ready:13|t"
    local CELL_KO   = "|TInterface\\RaidFrame\\ReadyCheck-NotReady:13|t"
    local CELL_NA   = "|cff8b8fa3—|r"

    -- Dessine un rapport {columns, rows} dans bGrid depuis yTop, avec les
    -- pools fournis. Renvoie le Y du bas (pour empiler une 2e grille dessous).
    local function DrawBuffGrid(rep, yTop, headers, names, cellsPool)
        local nameW = 120
        local nCols = #rep.columns
        local cellW = math.floor(math.min(64, (bGrid:GetWidth() > 0 and (bGrid:GetWidth() - nameW) or 448) / math.max(1, nCols)))
        for j, label in ipairs(rep.columns) do
            local h = headers[j]
            if not h then h = Label(bGrid, 10, COLORS.muted); headers[j] = h end
            h:ClearAllPoints()
            h:SetPoint("TOPLEFT", nameW + (j - 1) * cellW, yTop)
            h:SetWidth(cellW); h:SetJustifyH("CENTER"); h:SetWordWrap(false)
            h:SetText(label); h:Show()
        end
        for j = nCols + 1, #headers do headers[j]:Hide() end
        for i, row in ipairs(rep.rows) do
            local y = yTop - (20 + (i - 1) * 24)
            local nameFS = names[i]
            if not nameFS then nameFS = Label(bGrid, 12); names[i] = nameFS; cellsPool[i] = {} end
            nameFS:ClearAllPoints()
            nameFS:SetPoint("TOPLEFT", 0, y)
            nameFS:SetWidth(nameW - 8); nameFS:SetJustifyH("LEFT"); nameFS:SetWordWrap(false)
            nameFS:SetText(("|c%s%s|r%s"):format(ClassColorStr(row.class), row.name, row.isMe and (" |cff8b8fa3" .. NS.L("(toi)") .. "|r") or ""))
            nameFS:Show()
            for j = 1, nCols do
                local c = cellsPool[i][j]
                if not c then c = Label(bGrid, 12); cellsPool[i][j] = c end
                c:ClearAllPoints()
                c:SetPoint("TOPLEFT", nameW + (j - 1) * cellW, y)
                c:SetWidth(cellW); c:SetJustifyH("CENTER")
                local v = row.cells[j]
                if v == true then c:SetText(CELL_OK)
                elseif v == false then c:SetText(CELL_KO)
                else c:SetText(CELL_NA) end
                c:Show()
            end
            for j = nCols + 1, #cellsPool[i] do cellsPool[i][j]:Hide() end
        end
        for i = #rep.rows + 1, #names do
            names[i]:Hide()
            for _, c in ipairs(cellsPool[i]) do c:Hide() end
        end
        return yTop - (20 + #rep.rows * 24)
    end

    RefreshBuffsPage = function()
        if not (NS.Buffs and NS.Buffs.GetReport) then return end
        local bottom = DrawBuffGrid(NS.Buffs.GetReport(), 0, bHeaders, bNames, bCells)
        -- section Enchantements empilée en dessous (même style de grille)
        if NS.Buffs.GetEnchantReport then
            if not eTitle then eTitle = Label(bGrid, 12) end
            local ty = bottom - 14
            eTitle:ClearAllPoints()
            eTitle:SetPoint("TOPLEFT", 0, ty)
            eTitle:SetText("|cffcfd2dc" .. NS.L("Enchantements") .. "|r")
            eTitle:Show()
            DrawBuffGrid(NS.Buffs.GetEnchantReport(), ty - 22, eHeaders, eNames, eCells)
        end
    end

    bRefresh:SetScript("OnClick", function() RefreshBuffsPage() end)

    -- ============================================================
    -- PAGE : BUTIN (aide au loot — upgrades pour le groupe)
    -- ============================================================
    local lootPage = CreateFrame("Frame", nil, content)
    lootPage:SetAllPoints()
    lootPage:Hide()
    pages.loot = lootPage

    local lTitle = Label(lootPage, 13)
    lTitle:SetPoint("TOPLEFT", 20, -18)
    lTitle:SetText(NS.L("Aide au butin"))

    local lHint = Label(lootPage, 11, COLORS.muted)
    lHint:SetPoint("TOPLEFT", 20, -40)
    lHint:SetWidth(WIN_W - SIDEBAR_W - 150)
    lHint:SetJustifyH("LEFT")
    lHint:SetText(NS.L("Survole un objet équipable (sac, butin, lien de chat) : l'addon dit pour qui du groupe c'est un up, d'après l'équipement partagé. Comparaison par ilvl, filtrée par classe et spé."))

    local lRefresh = MakeButton(lootPage, NS.L("Actualiser"), 110, 24)
    lRefresh:SetPoint("TOPRIGHT", -20, -14)

    -- Case : afficher les upgrades dans les infobulles
    local ttBox = CreateFrame("Button", nil, lootPage)
    ttBox:SetSize(20, 20)
    ttBox:SetPoint("TOPLEFT", 20, -82)
    local ttBg = Solid(ttBox)
    ttBg:SetAllPoints()
    ttBg:SetVertexColor(unpack(COLORS.field))
    Border(ttBox, COLORS.line)
    local ttInner = Solid(ttBox, "ARTWORK")
    ttInner:SetSize(12, 12)
    ttInner:SetPoint("CENTER")
    ttInner:SetVertexColor(unpack(COLORS.accent))
    local ttLbl = Label(lootPage, 12)
    ttLbl:SetPoint("LEFT", ttBox, "RIGHT", 10, 0)
    ttLbl:SetText(NS.L("Afficher les upgrades dans les infobulles"))
    local function UpdateTTBox()
        ttInner:SetShown(not (NS.db and NS.db.lootTooltip == false))
    end
    ttBox:SetScript("OnClick", function()
        if not NS.db then return end
        NS.db.lootTooltip = (NS.db.lootTooltip == false) and true or false
        UpdateTTBox()
    end)

    -- (l'option « SpellID / IconID dans les infobulles » a été déplacée
    --  dans le menu Toolbox)

    local lGrid = CreateFrame("Frame", nil, lootPage)
    lGrid:SetPoint("TOPLEFT", 20, -110)
    lGrid:SetPoint("BOTTOMRIGHT", -20, 16)

    local lRows = {}
    RefreshLootPage = function()
        UpdateTTBox()
        if not (NS.Loot and NS.Loot.GetGroupReport) then return end
        local rep = NS.Loot.GetGroupReport()
        for i, e in ipairs(rep) do
            local fs = lRows[i]
            if not fs then
                fs = Label(lGrid, 12)
                fs:SetPoint("TOPLEFT", 0, -(i - 1) * 26)
                fs:SetPoint("RIGHT", lGrid, "RIGHT", 0, 0)
                fs:SetJustifyH("LEFT")
                fs:SetWordWrap(false)
                lRows[i] = fs
            end
            local weakParts = {}
            for _, w in ipairs(e.weak) do
                weakParts[#weakParts + 1] =
                    (NS.L(NS.LOOT_SLOT_LABEL[w.inv]) or ("slot " .. w.inv)) .. " " .. w.ilvl
            end
            local weakStr = (#weakParts > 0)
                and (NS.L(" |cff8b8fa3· faibles : ") .. table.concat(weakParts, ", ") .. "|r") or ""
            local avgStr = (e.avg > 0)
                and (NS.L(" |cffcfd2dc· ilvl moy ") .. e.avg .. "|r")
                or NS.L(" |cff8b8fa3· pas de données|r")
            fs:SetText(("|c%s%s|r%s%s%s"):format(
                ClassColorStr(e.class), e.name,
                e.isMe and (" |cff8b8fa3" .. NS.L("(toi)") .. "|r") or "", avgStr, weakStr))
            fs:Show()
        end
        for i = #rep + 1, #lRows do lRows[i]:Hide() end
    end
    lRefresh:SetScript("OnClick", function() RefreshLootPage() end)

    -- appelé par Loot.lua quand des slots d'un pair arrivent
    function NS.RefreshLootUI()
        if UI and UI:IsShown() and pages.loot and pages.loot:IsShown() then
            RefreshLootPage()
        end
    end

    -- ============================================================
    -- PAGE : CLÉS & COFFRE (clé M+ de la semaine + Great Vault)
    -- ============================================================
    local vaultPage = CreateFrame("Frame", nil, content)
    vaultPage:SetAllPoints()
    vaultPage:Hide()
    pages.vault = vaultPage

    local vTitle = Label(vaultPage, 13)
    vTitle:SetPoint("TOPLEFT", 20, -18)
    vTitle:SetText(NS.L("Clés Mythique+ & Coffre convoité"))

    local vRefresh = MakeButton(vaultPage, NS.L("Actualiser"), 110, 24)
    vRefresh:SetPoint("TOPRIGHT", -20, -14)

    local vHint = Label(vaultPage, 11, COLORS.muted)
    vHint:SetPoint("TOPLEFT", 20, -42)
    vHint:SetText(NS.L("Survole un nom pour afficher son coffre (9 cases). Données partagées par les amis."))

    -- réglages de l'assistant de clé (auto-slot + boutons)
    local function VaultCheck(y, text, dbKey, onToggle)
        local box = CreateFrame("Button", nil, vaultPage)
        box:SetSize(18, 18)
        box:SetPoint("TOPLEFT", 20, y)
        local bg = Solid(box)
        bg:SetAllPoints()
        bg:SetVertexColor(unpack(COLORS.field))
        Border(box, COLORS.line)
        local inner = Solid(box, "ARTWORK")
        inner:SetSize(10, 10)
        inner:SetPoint("CENTER")
        inner:SetVertexColor(unpack(COLORS.accent))
        inner:SetShown(not (NS.db and NS.db[dbKey] == false))
        local lbl = Label(vaultPage, 11)
        lbl:SetPoint("LEFT", box, "RIGHT", 8, 0)
        lbl:SetText(NS.L(text))
        box:SetScript("OnClick", function()
            if not NS.db then return end
            NS.db[dbKey] = (NS.db[dbKey] == false) and true or false
            inner:SetShown(NS.db[dbKey] ~= false)
            if onToggle then onToggle() end
        end)
    end
    VaultCheck(-62, "Insérer ma clé automatiquement", "keyAutoSlot")
    VaultCheck(-84, "Boutons Ready check + Pull sur la fenêtre de clé", "keyAssistant",
        function() if NS.Keystone and NS.Keystone.RefreshButtons then NS.Keystone.RefreshButtons() end end)

    -- popup des 9 cases (3 pistes × 3), affiché au survol
    local vaultPopup = CreateFrame("Frame", nil, UIParent)
    vaultPopup:SetFrameStrata("DIALOG")
    vaultPopup:SetSize(520, 236)
    vaultPopup:SetClampedToScreen(true) -- ne sort jamais de l'écran
    vaultPopup:Hide()
    local vpBg = Solid(vaultPopup)
    vpBg:SetAllPoints()
    vpBg:SetVertexColor(0.05, 0.055, 0.07, 0.98)
    Border(vaultPopup, COLORS.accent)
    local vpTitle = Label(vaultPopup, 13)
    vpTitle:SetPoint("TOPLEFT", 16, -12)
    vaultPopup.tracks = {}
    for r = 1, 3 do
        local yy = -44 - (r - 1) * 50
        local lab = Label(vaultPopup, 13, COLORS.muted)
        lab:SetPoint("TOPLEFT", 16, yy)
        lab:SetWidth(84)
        lab:SetJustifyH("LEFT")
        lab:SetWordWrap(false)
        local cells = {}
        for c = 1, 3 do
            local cell = CreateFrame("Frame", nil, vaultPopup)
            cell:SetSize(92, 42)
            cell:SetPoint("TOPLEFT", 108 + (c - 1) * 102, yy + 6)
            local cbg = Solid(cell)
            cbg:SetAllPoints()
            cbg:SetVertexColor(unpack(COLORS.field))
            Border(cell, COLORS.line)
            local fs = Label(cell, 14)
            fs:SetPoint("CENTER")
            cells[c] = { frame = cell, fs = fs, bg = cbg }
        end
        local hintFs = Label(vaultPopup, 14)
        hintFs:SetPoint("LEFT", cells[3].frame, "RIGHT", 12, 0)
        hintFs:SetJustifyH("LEFT")
        vaultPopup.tracks[r] = { label = lab, cells = cells, hint = hintFs }
    end
    local vpEmpty = Label(vaultPopup, 12, COLORS.muted)
    vpEmpty:SetPoint("TOPLEFT", 16, -44)
    vpEmpty:SetText(NS.L("Pas encore de données de coffre."))
    local vpLegend = Label(vaultPopup, 11, COLORS.muted)
    vpLegend:SetPoint("BOTTOMLEFT", 16, 10)
    vpLegend:SetText(NS.L("cases = ilvl du reward · → ilvl au palier suivant"))

    local function FillPopup(entry)
        vpTitle:SetText(("|c%s%s|r  |cff8b8fa3%s|r"):format(
            ClassColorStr(entry.class), entry.name, entry.keyText or ""))
        local tracks = entry.tracks or {}
        vpEmpty:SetShown(#tracks == 0)
        vpLegend:SetShown(#tracks > 0)
        for r = 1, 3 do
            local ui = vaultPopup.tracks[r]
            local tr = tracks[r]
            if tr then
                ui.label:SetText(tr.label)
                ui.label:Show()
                for c = 1, 3 do
                    local cell = ui.cells[c]
                    local data = tr.cells and tr.cells[c]
                    if data then
                        if data.filled then
                            cell.fs:SetText(data.ilvl and tostring(data.ilvl)
                                or ("%d/%d"):format(data.progress, data.threshold))
                            cell.fs:SetTextColor(0.35, 0.85, 0.45, 1)
                            cell.bg:SetVertexColor(0.12, 0.30, 0.16, 1)
                        else
                            cell.fs:SetText(("%d/%d"):format(data.progress, data.threshold))
                            cell.fs:SetTextColor(0.55, 0.56, 0.62, 1)
                            cell.bg:SetVertexColor(unpack(COLORS.field))
                        end
                        cell.frame:Show()
                    else
                        cell.frame:Hide()
                    end
                end
                if tr.nextIlvl then
                    ui.hint:SetText(("→ %d"):format(tr.nextIlvl))
                    ui.hint:SetTextColor(0.35, 0.85, 0.45, 1)
                else
                    ui.hint:SetText("")
                end
                ui.hint:Show()
            else
                ui.label:Hide()
                ui.hint:SetText("")
                for c = 1, 3 do ui.cells[c].frame:Hide() end
            end
        end
    end

    local vRows = {}
    RefreshVaultPage = function()
        if not (NS.Vault and NS.Vault.GetReport) then return end
        local rep = NS.Vault.GetReport()
        for i, e in ipairs(rep) do
            local row = vRows[i]
            if not row then
                row = CreateFrame("Button", nil, vaultPage)
                row:SetHeight(24)
                row:SetPoint("TOPLEFT", 20, -(112 + (i - 1) * 26))
                row:SetPoint("TOPRIGHT", -20, -(112 + (i - 1) * 26))
                local hl = row:CreateTexture(nil, "HIGHLIGHT")
                hl:SetTexture(WHITE)
                hl:SetAllPoints()
                hl:SetVertexColor(1, 1, 1, 0.05)
                row.txt = Label(row, 12)
                row.txt:SetPoint("LEFT", 4, 0)
                row.txt:SetJustifyH("LEFT")
                row:SetScript("OnEnter", function(self)
                    if not self.entry then return end
                    FillPopup(self.entry)
                    vaultPopup:ClearAllPoints()
                    vaultPopup:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 40, -2)
                    vaultPopup:Show()
                end)
                row:SetScript("OnLeave", function() vaultPopup:Hide() end)
                vRows[i] = row
            end
            row.entry = e
            row.txt:SetText(("|c%s%s|r%s   |cff8b8fa3%s|r"):format(
                ClassColorStr(e.class), e.name,
                e.isMe and (" |cff8b8fa3" .. NS.L("(toi)") .. "|r") or "", e.keyText or ""))
            row:Show()
        end
        for i = #rep + 1, #vRows do vRows[i]:Hide() end
    end
    vRefresh:SetScript("OnClick", function() RefreshVaultPage() end)

    function NS.RefreshVaultUI()
        if UI and UI:IsShown() and pages.vault and pages.vault:IsShown() then
            RefreshVaultPage()
        end
    end

    -- ============================================================
    -- PAGE : ALERTES (choix des notifications)
    -- ============================================================
    -- Page ALERTES : construite hors de BuildUI (fonction dédiée), exactement
    -- comme la Boîte à outils, pour un agencement identique et pour rester sous
    -- la limite Lua des 200 variables locales par fonction.
    UpdateAlertsPage = BuildAlertsPage(content, pages)

    -- appelé par Sync.lua / SimC.lua quand des données changent
    function NS.RefreshOptimUI(kind, who)
        if not UI or not UI:IsShown() then return end
        if pages.optim:IsShown() then
            refreshOptimList()
        elseif pages.inv:IsShown() then
            refreshInvList()
        elseif pages.detail:IsShown() and currentDetail then
            if (kind == "self" and currentDetail == "ME") or (who and who == currentDetail) then
                FillDetail(currentDetail)
            end
        elseif pages.invdetail:IsShown() and currentInv then
            if (kind == "self" and currentInv == "ME") or (who and who == currentInv) then
                FillInvDetail(currentInv)
            end
        end
    end

    -- ============================================================
    -- PAGE : TOOLBOX — construite hors de BuildUI (fonction dédiée) pour
    -- rester sous la limite Lua de 200 variables locales par fonction.
    -- ============================================================
    UpdateToolboxPage = BuildToolboxPage(content, pages)

    -- PAGE : STATS M+ (fonction dédiée ; se rafraîchit via NS.RefreshStatsPage)
    BuildStatsPage(content, pages)

    -- ----- Animation d'ouverture + sons -----
    local anim = UI:CreateAnimationGroup()
    local fade = anim:CreateAnimation("Alpha")
    fade:SetFromAlpha(0)
    fade:SetToAlpha(1)
    fade:SetDuration(0.16)
    fade:SetSmoothing("OUT")
    local grow = anim:CreateAnimation("Scale")
    grow:SetScaleFrom(0.96, 0.96)
    grow:SetScaleTo(1, 1)
    grow:SetDuration(0.16)
    grow:SetSmoothing("OUT")
    grow:SetOrigin("CENTER", 0, 0)

    UI:SetScript("OnShow", function()
        anim:Play()
        if SOUNDKIT then PlaySound(SOUNDKIT.IG_MAINMENU_OPEN) end
        sVersion:SetText("v" .. (NS.VERSION or "0.3.0"))
        if NS.Sync and NS.Sync.available then
            syncDot:SetVertexColor(unpack(COLORS.green))
            syncLabel:SetText(NS.L("Synchro active"))
        else
            syncDot:SetVertexColor(unpack(COLORS.gray))
            syncLabel:SetText(NS.L("Synchro indisponible"))
        end
        if pages.home:IsShown() and UpdateHomePage then UpdateHomePage() end
        if pages.optim:IsShown() then refreshOptimList() end
        if pages.inv:IsShown() then refreshInvList() end
    end)
    UI:SetScript("OnHide", function()
        if SOUNDKIT then PlaySound(SOUNDKIT.IG_MAINMENU_CLOSE) end
    end)

    ShowPage("home")
end

-- ---------- API publique ----------
function NS.Toggle()
    if not UI then BuildUI() end
    if UI:IsShown() then
        UI:Hide()
    else
        UI:Show()
    end
end
-- fin de UI.lua
