-- ============================================================
-- 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

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

    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, 13)
    sTitle:SetPoint("LEFT", logo, "RIGHT", 8, 1)
    sTitle:SetWidth(SIDEBAR_W - 54) -- jamais de débordement sur le contenu
    sTitle:SetJustifyH("LEFT")
    sTitle:SetWordWrap(false)
    sTitle:SetText("Asylum Compagnon")

    -- navigation
    local NAV = {
        { key = "home",   label = "Accueil" },
        { key = "optim",  label = "Optimisation" },
        { key = "inv",    label = "Inventaire" },
        { key = "kicks",  label = "Kicks" },
        { key = "buffs",  label = "Buffs & auras" },
        { 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(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)
    local ctext = Label(close, 15, COLORS.muted)
    ctext:SetPoint("CENTER")
    ctext:SetText("×")
    close:SetScript("OnEnter", function()
        cbg:SetVertexColor(1, 0.3, 0.35, 0.16)
        ctext:SetTextColor(1, 1, 1, 1)
    end)
    close:SetScript("OnLeave", function()
        cbg:SetVertexColor(1, 0.3, 0.35, 0)
        ctext:SetTextColor(unpack(COLORS.muted))
    end)
    close:SetScript("OnClick", function() UI:Hide() 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", buffs = "Buffs & auras", alerts = "Alertes",
    }
    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(PAGE_TITLES[key] or "")
        SetNavActive(NAV_FOR_PAGE[key] or key)
        if key == "optim" and refreshOptimList then refreshOptimList() end
        if key == "inv" and refreshInvList then refreshInvList() end
        if key == "kicks" and UpdateKicksPage then UpdateKicksPage() end
        if key == "buffs" and RefreshBuffsPage then RefreshBuffsPage() end
        if key == "alerts" and UpdateAlertsPage then UpdateAlertsPage() 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("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(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] = "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
    -- ============================================================
    local home = CreateFrame("Frame", nil, content)
    home:SetAllPoints()
    pages.home = home

    local hero = Label(home, 24)
    hero:SetPoint("TOP", 0, -30)
    hero:SetText(("Asylum |cff%sCompagnon|r"):format(NS.ACCENT))

    local welcome = Label(home, 13, COLORS.muted)
    welcome:SetPoint("TOP", hero, "BOTTOM", 0, -8)
    welcome:SetText("Bienvenue dans l'addon de Test Asylum")

    local imgPanel = CreateFrame("Frame", nil, home)
    imgPanel:SetSize(460, 345)
    imgPanel:SetPoint("TOP", welcome, "BOTTOM", 0, -16)
    local ipbg = Solid(imgPanel)
    ipbg:SetAllPoints()
    ipbg:SetVertexColor(unpack(COLORS.field))
    Border(imgPanel, COLORS.line)

    -- message visible uniquement si l'image n'est pas trouvée
    local imgMissing = imgPanel:CreateFontString(nil, "BORDER")
    imgMissing:SetFont(FONT, 11, "")
    imgMissing:SetTextColor(unpack(COLORS.muted))
    imgMissing:SetPoint("CENTER")
    imgMissing:SetJustifyH("CENTER")
    imgMissing:SetText("Image d'accueil manquante :\nAsylumCompagnon\\media\\accueil.tga\n\n(un redémarrage complet du jeu est nécessaire\naprès l'ajout du fichier)")

    local img = imgPanel:CreateTexture(nil, "ARTWORK")
    img:SetPoint("TOPLEFT", 1, -1)
    img:SetPoint("BOTTOMRIGHT", -1, 1)
    img:SetTexture(HOME_IMAGE)
    img:SetTexCoord(0, 1, 0, 0.75) -- l'image occupe le haut du canevas 1024x1024

    -- ============================================================
    -- 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, "« 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, "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(("|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(("|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(("|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(toi)|r"):format(ClassColorStr(class), UnitName("player") or "?"))
            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] = "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 "# Impossible de générer le code pour l'instant.")
            dStatus:SetText(mp and mp.ts and ("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] = "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 "|cff22c55een ligne|r" or "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((("Code reçu %s"):format(RelTime(p.ts) or ""))
                    .. (stale and "  |cffeab308(plus récent dispo)|r" or ""))
            else
                SetCode("# Pas encore de code reçu pour ce personnage.\n"
                    .. "# Il doit être en ligne avec l'addon installé.\n"
                    .. "# 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("demande envoyée…")
            else
                dStatus:SetText("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("demande envoyée…")
            end
        end
    end

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

    refreshInvList = MakeCharList(inv, function(key) ShowInvDetail(key) end)

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

    local iback = MakeButton(invdetail, "« 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)

    -- ligne : or
    local goldRow = CreateFrame("Frame", nil, invdetail)
    goldRow:SetHeight(38)
    goldRow:SetPoint("TOPLEFT", 20, -100)
    goldRow:SetPoint("TOPRIGHT", -20, -100)
    local gbg = Solid(goldRow)
    gbg:SetAllPoints()
    gbg:SetVertexColor(unpack(COLORS.panel))
    Border(goldRow, COLORS.line)
    local goldIcon = goldRow:CreateTexture(nil, "ARTWORK")
    goldIcon:SetTexture(GOLD_ICON)
    goldIcon:SetSize(18, 18)
    goldIcon:SetPoint("LEFT", 10, 0)
    local goldLabel = Label(goldRow, 12)
    goldLabel:SetPoint("LEFT", goldIcon, "RIGHT", 10, 0)
    goldLabel:SetText("Pièces d'or")
    local goldValue = Label(goldRow, 13)
    goldValue:SetPoint("RIGHT", -12, 0)

    -- 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(gold, items)
            goldValue:SetText(gold and (FormatNumber(gold) .. " po") or "—")
            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, -(146 + r * 36))
                    if c > 0 then
                        row.count:SetText(FormatNumber(c))
                        row.count:SetTextColor(unpack(COLORS.text))
                    else
                        row.count:SetText("0")
                        row.count:SetTextColor(0.92, 0.28, 0.32, 1)
                    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(toi)|r"):format(ClassColorStr(class), UnitName("player") or "?"))
            local bits = { ClassName(class) }
            local lvl = (mp and mp.level) or myMeta.level
            if lvl and lvl > 0 then bits[#bits + 1] = "Niveau " .. lvl end
            iMeta:SetText(table.concat(bits, " · "))
            local shown = SetValues(mp and mp.gold, mp and mp.items)
            iStatus:SetText("en direct")
            if shown == 0 then
                iNote:SetText("Aucun consommable suivi possédé pour l'instant.")
            else
                iNote:SetText("Sacs + banque · |cffea48500|r = stock épuisé")
            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] = "Niveau " .. p.level end
            bits[#bits + 1] = p.online and "|cff22c55een ligne|r" or "hors ligne"
            iMeta:SetText(table.concat(bits, " · "))
            local shown = SetValues(p.gold, p.items)
            local seen = RelTime(math.max(p.lastSeen or 0, p.ts or 0))
            iStatus:SetText(seen and ("synchro " .. seen) or "")
            if p.gold == nil then
                iNote:SetText("Inventaire non reçu (version d'addon trop ancienne).")
            elseif shown == 0 then
                iNote:SetText("Aucun consommable suivi possédé.")
            else
                iNote:SetText("Dernière synchro · |cffea48500|r = stock épuisé")
            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("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("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)

    local kReset = MakeButton(kicks, "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(("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("|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")

    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)
    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("Préparation du groupe")

    local bRefresh = MakeButton(buffsPage, "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 CELL_OK   = "|TInterface\\RaidFrame\\ReadyCheck-Ready:13|t"
    local CELL_KO   = "|TInterface\\RaidFrame\\ReadyCheck-NotReady:13|t"
    local CELL_NA   = "|cff8b8fa3—|r"

    RefreshBuffsPage = function()
        if not (NS.Buffs and NS.Buffs.GetReport) then return end
        local rep = NS.Buffs.GetReport()
        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 = bHeaders[j]
            if not h then
                h = Label(bGrid, 10, COLORS.muted)
                bHeaders[j] = h
            end
            h:ClearAllPoints()
            h:SetPoint("TOPLEFT", nameW + (j - 1) * cellW, 0)
            h:SetWidth(cellW)
            h:SetJustifyH("CENTER")
            h:SetWordWrap(false)
            h:SetText(label)
            h:Show()
        end
        for j = nCols + 1, #bHeaders do bHeaders[j]:Hide() end

        for i, row in ipairs(rep.rows) do
            local y = -(20 + (i - 1) * 24)
            local nameFS = bNames[i]
            if not nameFS then
                nameFS = Label(bGrid, 12)
                bNames[i] = nameFS
                bCells[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(toi)|r" or ""))
            nameFS:Show()
            for j = 1, nCols do
                local c = bCells[i][j]
                if not c then
                    c = Label(bGrid, 12)
                    bCells[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, #bCells[i] do bCells[i][j]:Hide() end
        end
        for i = #rep.rows + 1, #bNames do
            bNames[i]:Hide()
            for _, c in ipairs(bCells[i]) do c:Hide() end
        end
    end

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

    -- ============================================================
    -- PAGE : ALERTES (choix des notifications)
    -- ============================================================
    local alerts = CreateFrame("Frame", nil, content)
    alerts:SetAllPoints()
    alerts:Hide()
    pages.alerts = alerts

    local aTitle = Label(alerts, 13)
    aTitle:SetPoint("TOPLEFT", 20, -18)
    aTitle:SetText("Alertes de stock épuisé")

    local function MakeAlertCheckbox(y, text, dbKey)
        local box = CreateFrame("Button", nil, alerts)
        box:SetSize(20, 20)
        box:SetPoint("TOPLEFT", 20, y)
        local bg2 = Solid(box)
        bg2:SetAllPoints()
        bg2: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(alerts, 12)
        lbl:SetPoint("LEFT", box, "RIGHT", 10, 0)
        lbl:SetText(text)
        box.Update = function()
            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
            box.Update()
        end)
        return box
    end

    local acbChat  = MakeAlertCheckbox(-52,  "Message dans le chat", "alertChat")
    local acbRaid  = MakeAlertCheckbox(-86,  "Alerte au centre de l'écran", "alertRaid")
    local acbSound = MakeAlertCheckbox(-120, "Alerte sonore", "alertSound")

    UpdateAlertsPage = function()
        acbChat.Update()
        acbRaid.Update()
        acbSound.Update()
    end

    -- 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

    -- ----- 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("Synchro active")
        else
            syncDot:SetVertexColor(unpack(COLORS.gray))
            syncLabel:SetText("Synchro indisponible")
        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
