local addonName, TOAST = ...

local GOLD = { 0.88, 0.75, 0.36 }
local TEAL = { 0, 0.92, 0.79 }  -- #00EAC9
local BORDER = { 1, 1, 1, 0.12 }
local SOFT_GREEN = { 0.55, 0.85, 0.55 }  -- delta to player above (catching up)
local SOFT_RED   = { 0.90, 0.45, 0.45 }  -- delta to player below (falling behind)
local DISCORD_INVITE = "https://discord.gg/tide-crew"
local FRESH_THRESHOLD = 3600
local ROW_H = 24

local CreateActionButton = TOAST.CreateActionButton
local LockEditBox = TOAST.LockEditBox
local CLASS_TOKEN = TOAST.CLASS_TOKEN
local ROLE_TEX = TOAST.ROLE_TEX
local ParseDiscordPrefix = TOAST.ParseDiscordPrefix

local W = 480
local PAD = 16
local CARD_GAP = 12
local CREW_ROW_H = 26
local MYCREW_HEADER_H = 30
local CREWPOPUP_HEADER_H = 44
local CREWPOPUP_BOTTOM_PAD = 8

-- Header layout: matches the leaderboard's header height and divider
-- placement style (same "+1" convention) so both windows read as the same
-- design language, instead of the dashboard's header feeling shorter/cramped.
local DASHBOARD_HEADER_HEIGHT = 100  -- == leaderboard's BANNER_H
local HEADER_DIVIDER_Y = -(DASHBOARD_HEADER_HEIGHT + 1)
local HEADER_DIVIDER_MEDALLION = 28
local HEADER_DIVIDER_GAP = 6

-- Card A starts far enough below the divider to fully clear the medallion
-- (half its size) plus the standard CARD_GAP breathing room — no overlap.
local CARD_A_TOP = HEADER_DIVIDER_Y - (HEADER_DIVIDER_MEDALLION / 2) - CARD_GAP
local CARD_A_HEIGHT = 170
local CARD_B_HEIGHT = 260
local CARD_C_HEIGHT = 100
local FOOTER_HEIGHT = 24
local FOOTER_GAP = 28  -- breathing room between Card C and the footer (unchanged from before)

-- Total dashboard height, derived from the header/card/footer chain above
-- instead of an independently eyeballed number.
local H = (-CARD_A_TOP) + CARD_A_HEIGHT + CARD_GAP + CARD_B_HEIGHT + CARD_GAP + CARD_C_HEIGHT
    + FOOTER_GAP + FOOTER_HEIGHT + PAD

local dash
local cardA
local prefixIcon, prefixLabel, discordNameFS, charIcon, charLine, statusLabel, progressLabel, keySharingLine
local statusBadges, progressBadges = {}, {}
local keysScroll, keysContent, emptyText
local keyRows = {}
local rankFS
local nextLabelFS, nextPrefixIcon, nextPrefixLabelFS, nextNameFS, nextDeltaFS, nextFallbackFS
local behindLabelFS, behindPrefixIcon, behindPrefixLabelFS, behindNameFS, behindDeltaFS
local pulseOnlineFS, pulseKeysFS, pulseMembersFS
local pulseOnlineBtn, pulseKeysBtn, pulseMembersBtn
local discordPopover
local myCrewCard
local myCrewRows = {}
local clickCatcher
local copyPopup, copyEditBox
local crewPopup, crewHeaderIcon, crewHeaderLabel, crewNameFS, crewRankFS
local crewPopupRows = {}

local function AddBorder(f, color)
    local top = f:CreateTexture(nil, "BORDER")
    top:SetColorTexture(color[1], color[2], color[3], color[4])
    top:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
    top:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
    top:SetHeight(1)

    local bottom = f:CreateTexture(nil, "BORDER")
    bottom:SetColorTexture(color[1], color[2], color[3], color[4])
    bottom:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)
    bottom:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
    bottom:SetHeight(1)

    local left = f:CreateTexture(nil, "BORDER")
    left:SetColorTexture(color[1], color[2], color[3], color[4])
    left:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
    left:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)
    left:SetWidth(1)

    local right = f:CreateTexture(nil, "BORDER")
    right:SetColorTexture(color[1], color[2], color[3], color[4])
    right:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
    right:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
    right:SetWidth(1)
end

local function CreateCard(parent, y, height, skipAccent)
    local card = CreateFrame("Frame", nil, parent)
    card:SetSize(W - PAD * 2, height)
    card:SetPoint("TOPLEFT", parent, "TOPLEFT", PAD, y)

    local bg = card:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(0.06, 0.08, 0.10, 0.50)

    AddBorder(card, BORDER)

    -- Card A skips this: the header's own anchor divider is the single
    -- strong gold separator above it, so a second gold line here would be
    -- a redundant duplicate right underneath it.
    -- Thinner/softer than the anchor divider — these are section accents,
    -- not primary dividers, so they shouldn't compete with it.
    if not skipAccent then
        local accent = card:CreateTexture(nil, "ARTWORK")
        accent:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 0.75)
        accent:SetPoint("TOPLEFT", card, "TOPLEFT", 0, 0)
        accent:SetPoint("TOPRIGHT", card, "TOPRIGHT", 0, 0)
        accent:SetHeight(1)
    end

    return card
end

local function CreateBadge(parent, text, isPositive, isAccent)
    local badge = CreateFrame("Frame", nil, parent)
    badge:SetHeight(15)

    -- Neutral subtle background for both states — badges are status labels,
    -- not primary actions, so no green/red tint.
    local bg = badge:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(1, 1, 1, 0.035)

    -- Very thin muted border; gold accent for the one highlighted badge (ranking)
    local border = badge:CreateTexture(nil, "BORDER")
    border:SetAllPoints()
    if isAccent then
        border:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 0.35)
    else
        border:SetColorTexture(1, 1, 1, 0.06)
    end

    local label = badge:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    label:SetPoint("CENTER")
    label:SetText(text)
    if isAccent then
        label:SetTextColor(GOLD[1], GOLD[2], GOLD[3])
    elseif isPositive then
        label:SetTextColor(1, 1, 1, 0.75)
    else
        label:SetTextColor(1, 1, 1, 0.35)
    end

    -- Auto-width
    badge:SetWidth(label:GetStringWidth() + 14)

    return badge
end

local function ClearBadgeSet(set)
    for _, b in ipairs(set) do
        b:Hide()
        b:SetParent(nil)
    end
    wipe(set)
end

local function BuildBadgeRow(parent, anchorTo, defs)
    local row = {}
    local prev
    for _, def in ipairs(defs) do
        local badge = CreateBadge(parent, def[1], def[2], def[3])
        if prev then
            badge:SetPoint("LEFT", prev, "RIGHT", 6, 0)
        else
            badge:SetPoint("TOPLEFT", anchorTo, "BOTTOMLEFT", 0, -6)
        end
        tinsert(row, badge)
        prev = badge
    end
    return row
end

local function GetMemberByName(name)
    for _, m in ipairs(TOAST.members) do
        if m.name == name then return m end
    end
    return nil
end

local function CountCrewByDiscordId(discord_id)
    local count = 0
    for _, m in ipairs(TOAST.members) do
        if m.discord_id == discord_id then count = count + 1 end
    end
    return count
end

local function PositionAtCursor(frame)
    local x, y = GetCursorPosition()
    local scale = UIParent:GetEffectiveScale()
    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x / scale, y / scale)
end

local function EnsureClickCatcher()
    if clickCatcher then return end
    clickCatcher = CreateFrame("Button", nil, UIParent)
    clickCatcher:SetAllPoints(UIParent)
    clickCatcher:SetFrameStrata("FULLSCREEN")
    clickCatcher:Hide()
end

local function ShowWithCatcher(frame)
    EnsureClickCatcher()
    clickCatcher:SetScript("OnClick", function()
        frame:Hide()
        clickCatcher:Hide()
    end)
    clickCatcher:Show()
    frame:Show()
end

local function HideWithCatcher(frame)
    frame:Hide()
    if clickCatcher then clickCatcher:Hide() end
end

-- ── Copy Name popup (TOAST.ShowCopyPopup) ──────────────────────────
local function EnsureCopyPopup()
    if copyPopup then return end
    copyPopup = CreateFrame("Frame", "TOASTCopyPopup", UIParent, "BackdropTemplate")
    Mixin(copyPopup, BackdropTemplateMixin)
    copyPopup:SetSize(220, 46)
    copyPopup:SetFrameStrata("TOOLTIP")
    copyPopup:SetClampedToScreen(true)
    copyPopup:EnableMouse(true)
    copyPopup:Hide()
    tinsert(UISpecialFrames, "TOASTCopyPopup")

    local bg = copyPopup:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(0.06, 0.08, 0.10, 1.0)
    AddBorder(copyPopup, BORDER)

    local accent = copyPopup:CreateTexture(nil, "ARTWORK")
    accent:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 1)
    accent:SetPoint("TOPLEFT", copyPopup, "TOPLEFT", 0, 0)
    accent:SetPoint("TOPRIGHT", copyPopup, "TOPRIGHT", 0, 0)
    accent:SetHeight(2)

    local hint = copyPopup:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    hint:SetPoint("TOP", copyPopup, "TOP", 0, -8)
    hint:SetTextColor(1, 1, 1, 0.45)
    hint:SetText("Ctrl+C to copy")

    copyEditBox = CreateFrame("EditBox", nil, copyPopup, "InputBoxTemplate")
    copyEditBox:SetSize(196, 22)
    copyEditBox:SetPoint("BOTTOM", copyPopup, "BOTTOM", 0, 8)

    copyPopup:SetScript("OnHide", function()
        if clickCatcher then clickCatcher:Hide() end
    end)
end

local copyPopupGen = 0

TOAST.ShowCopyPopup = function(text)
    if crewPopup and crewPopup:IsShown() then crewPopup:Hide() end
    EnsureCopyPopup()
    LockEditBox(copyEditBox, text)
    PositionAtCursor(copyPopup)
    ShowWithCatcher(copyPopup)
    copyEditBox:SetFocus()
    copyEditBox:HighlightText()

    copyPopupGen = copyPopupGen + 1
    local myGen = copyPopupGen
    C_Timer.After(3, function()
        if myGen == copyPopupGen and copyPopup:IsShown() then
            HideWithCatcher(copyPopup)
        end
    end)
end

-- ── View Crew popup (TOAST.ShowCrewPopup) ──────────────────────────
local function EnsureCrewPopup()
    if crewPopup then return end
    crewPopup = CreateFrame("Frame", "TOASTCrewPopup", UIParent, "BackdropTemplate")
    Mixin(crewPopup, BackdropTemplateMixin)
    crewPopup:SetWidth(420)
    crewPopup:SetFrameStrata("TOOLTIP")
    crewPopup:SetClampedToScreen(true)
    crewPopup:EnableMouse(true)
    crewPopup:Hide()
    tinsert(UISpecialFrames, "TOASTCrewPopup")

    local bg = crewPopup:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(0.06, 0.08, 0.10, 1.0)
    AddBorder(crewPopup, BORDER)

    local accent = crewPopup:CreateTexture(nil, "ARTWORK")
    accent:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 1)
    accent:SetPoint("TOPLEFT", crewPopup, "TOPLEFT", 0, 0)
    accent:SetPoint("TOPRIGHT", crewPopup, "TOPRIGHT", 0, 0)
    accent:SetHeight(2)

    crewHeaderIcon = crewPopup:CreateTexture(nil, "OVERLAY")
    crewHeaderIcon:SetSize(16, 16)
    crewHeaderIcon:SetPoint("TOPLEFT", crewPopup, "TOPLEFT", 12, -8)

    crewHeaderLabel = crewPopup:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    crewHeaderLabel:SetPoint("LEFT", crewHeaderIcon, "RIGHT", 4, 0)
    crewHeaderLabel:SetTextColor(0.8, 0.8, 0.8)

    crewNameFS = crewPopup:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    crewNameFS:SetPoint("LEFT", crewHeaderLabel, "RIGHT", 6, 0)
    crewNameFS:SetTextColor(1, 1, 1)

    crewRankFS = crewPopup:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    crewRankFS:SetPoint("TOPLEFT", crewHeaderIcon, "BOTTOMLEFT", -2, -6)

    crewPopup:SetScript("OnHide", function()
        if clickCatcher then clickCatcher:Hide() end
    end)
end

local function BuildCrewPopupRow(parent, entry, y)
    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(CREW_ROW_H)
    row:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, y)
    row:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, y)

    local roleTex = row:CreateTexture(nil, "OVERLAY")
    roleTex:SetSize(16, 16)
    roleTex:SetPoint("LEFT", row, "LEFT", 12, 0)
    roleTex:SetTexture(ROLE_TEX[entry.role] or "")

    local token = CLASS_TOKEN[entry.class_]
    local c = token and RAID_CLASS_COLORS[token]
    local nameFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    nameFS:SetPoint("LEFT", row, "LEFT", 32, 0)
    nameFS:SetWidth(90)
    nameFS:SetJustifyH("LEFT")
    if c then
        nameFS:SetTextColor(c.r, c.g, c.b)
    end
    nameFS:SetText(entry.name)

    local realmFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    realmFS:SetPoint("LEFT", row, "LEFT", 130, 0)
    realmFS:SetWidth(70)
    realmFS:SetJustifyH("LEFT")
    realmFS:SetTextColor(1, 1, 1, 0.5)
    realmFS:SetText(entry.realm)

    local keyFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    keyFS:SetPoint("RIGHT", row, "RIGHT", -12, 0)
    keyFS:SetWidth(70)
    keyFS:SetJustifyH("RIGHT")
    keyFS:SetTextColor(1, 1, 1, 1)
    local keyData = TOAST.keys and TOAST.keys[entry.name]
    keyFS:SetText(keyData and ("+" .. keyData.level .. " " .. keyData.short) or "\226\128\148")

    local resiFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    resiFS:SetPoint("RIGHT", keyFS, "LEFT", -8, 0)
    resiFS:SetWidth(50)
    resiFS:SetJustifyH("RIGHT")
    if entry.resi and entry.resi > 0 then
        resiFS:SetText(string.format("|cff999999Resi |r|cffffffff%d|r", entry.resi))
        resiFS:SetAlpha(1)
    else
        resiFS:SetText("|cff999999Resi |r|cffffffff0|r")
        resiFS:SetAlpha(0.35)
    end

    local rioFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    rioFS:SetPoint("RIGHT", resiFS, "LEFT", -8, 0)
    rioFS:SetWidth(70)
    rioFS:SetJustifyH("RIGHT")
    rioFS:SetText(string.format("|cff999999RIO |r|cffffffff%s|r",
        entry.rio and tostring(entry.rio) or "\226\128\148"))

    return row
end

TOAST.ShowCrewPopup = function(discord_id)
    if not discord_id then return end
    if copyPopup and copyPopup:IsShown() then copyPopup:Hide() end

    local crewEntries = {}
    for _, m in ipairs(TOAST.members) do
        if m.discord_id == discord_id then
            tinsert(crewEntries, m)
        end
    end
    if #crewEntries == 0 then return end

    EnsureCrewPopup()

    for _, row in ipairs(crewPopupRows) do
        row:Hide()
        row:SetParent(nil)
    end
    wipe(crewPopupRows)

    local first = crewEntries[1]
    local p, discordName = ParseDiscordPrefix(first.discord_nickname)
    if p then
        crewHeaderIcon:SetTexture(p.tex)
        crewHeaderIcon:Show()
        crewHeaderLabel:SetText(p.label)
        crewHeaderLabel:Show()
    else
        crewHeaderIcon:Hide()
        crewHeaderLabel:Hide()
    end
    crewNameFS:SetText(discordName)

    local rankColor = (first.rank and TOAST.RANK_COLORS[first.rank]) or GOLD
    crewRankFS:SetText(first.rank or "")
    crewRankFS:SetTextColor(rankColor[1], rankColor[2], rankColor[3])

    local count = math.min(#crewEntries, 3)
    for i = 1, count do
        local row = BuildCrewPopupRow(crewPopup, crewEntries[i], -(CREWPOPUP_HEADER_H + (i - 1) * CREW_ROW_H))
        tinsert(crewPopupRows, row)
    end

    crewPopup:SetHeight(CREWPOPUP_HEADER_H + count * CREW_ROW_H + CREWPOPUP_BOTTOM_PAD)

    PositionAtCursor(crewPopup)
    ShowWithCatcher(crewPopup)
end

local function RefreshStatus()
    local me = TOAST_DB.me
    if not me then
        prefixIcon:Hide()
        prefixLabel:Hide()
        discordNameFS:ClearAllPoints()
        discordNameFS:SetPoint("TOPLEFT", cardA, "TOPLEFT", 12, -10)
        discordNameFS:SetText("Welcome!")
        charIcon:Hide()
        charLine:SetText("")
        ClearBadgeSet(statusBadges)
        ClearBadgeSet(progressBadges)
        keySharingLine:SetText("")
        return
    end

    -- Discord identity: prefix icon + prefix label + discord name, zelfde als leaderboard
    local p, cleanName
    if me.discord then
        p, cleanName = ParseDiscordPrefix(me.discord)
    else
        cleanName = me.name
    end

    discordNameFS:ClearAllPoints()
    if p then
        prefixIcon:SetTexture(p.tex)
        prefixIcon:Show()
        prefixLabel:SetText(p.label)
        prefixLabel:Show()
        discordNameFS:SetPoint("LEFT", prefixLabel, "RIGHT", 6, 0)
    else
        prefixIcon:Hide()
        prefixLabel:Hide()
        discordNameFS:SetPoint("TOPLEFT", cardA, "TOPLEFT", 12, -10)
    end
    discordNameFS:SetText(cleanName)

    -- Character-Realm met class color + role icoon
    charIcon:Show()
    charIcon:SetTexture(ROLE_TEX[me.role] or "")
    local token = CLASS_TOKEN[me.class_]
    local c = token and RAID_CLASS_COLORS[token]
    local charRealm = me.name .. "-" .. me.realm
    if c then
        charLine:SetText(string.format("|cff%02x%02x%02x%s|r",
            math.floor(c.r * 255), math.floor(c.g * 255), math.floor(c.b * 255), charRealm))
    else
        charLine:SetText(charRealm)
    end

    -- Status badges
    local rosterStatus, rosterPositive
    if TOAST.me then
        rosterStatus, rosterPositive = "Recognized", true
    elseif TOAST_DB.me then
        rosterStatus, rosterPositive = "Known alt", true
    else
        rosterStatus, rosterPositive = "Not in roster", false
    end
    local communityPositive = TOAST.inCommunity and true or false
    local communityStatus = communityPositive and "In TIDE and Crew" or "Not in community"
    local myKeyData = TOAST.keys and TOAST.keys[UnitName("player")]
    local keySharePositive = myKeyData ~= nil
    local keyShareStatus = keySharePositive and "Key shared" or "No key detected"

    ClearBadgeSet(statusBadges)
    statusBadges = BuildBadgeRow(cardA, statusLabel, {
        { rosterStatus, rosterPositive },
        { communityStatus, communityPositive },
        { keyShareStatus, keySharePositive },
    })

    -- Progress badges
    local keyData = TOAST.keys and TOAST.keys[me.name]
    local keyStr = keyData and ("+" .. keyData.level .. " " .. keyData.short) or "No key"

    -- me.rank/rio/resi are only present when recognized via TOAST.leaderboard;
    -- members-only matches (alts not on the leaderboard) lack these fields.
    local rankStr = me.rank and string.format("#%d of %d", me.rank, #TOAST.leaderboard) or "Unranked"
    local rioStr  = me.rio  and ("RIO " .. me.rio)   or "RIO \226\128\148"
    local resiStr = me.resi and ("Resi " .. me.resi) or "Resi \226\128\148"

    ClearBadgeSet(progressBadges)
    progressBadges = BuildBadgeRow(cardA, progressLabel, {
        { rankStr, true, true },
        { rioStr, true },
        { resiStr, true },
        { keyStr, true },
    })

    -- Key sharing live status
    if myKeyData and myKeyData.lastSeen then
        local ageMin = math.floor((GetServerTime() - myKeyData.lastSeen) / 60)
        local lastBroadcastStr = ageMin < 1 and "now" or (ageMin .. "m ago")
        keySharingLine:SetText(string.format(
            "Key sharing: |cff00EAC9ON|r  \226\128\162  Last broadcast: |cff00EAC9%s|r", lastBroadcastStr))
    else
        keySharingLine:SetText("Key sharing: OFF")
    end
end

local function RefreshKeysList()
    for _, row in ipairs(keyRows) do
        row:Hide()
        row:SetParent(nil)
    end
    wipe(keyRows)

    local now = GetServerTime()
    local fresh = {}
    for _, entry in ipairs(TOAST.leaderboard) do
        local data = TOAST.keys and TOAST.keys[entry.name]
        if data then
            local recentlySeen = data.lastSeen and (now - data.lastSeen) < FRESH_THRESHOLD
            local onlineNow = TOAST._onlineMembers and TOAST._onlineMembers[entry.name]
            if recentlySeen or onlineNow then
                tinsert(fresh, { entry = entry, data = data })
            end
        end
    end
    table.sort(fresh, function(a, b)
        local aOnline = TOAST._onlineMembers and TOAST._onlineMembers[a.entry.name] or false
        local bOnline = TOAST._onlineMembers and TOAST._onlineMembers[b.entry.name] or false
        if aOnline ~= bOnline then
            return aOnline
        end
        return a.data.level > b.data.level
    end)

    keysContent:SetWidth(keysScroll:GetWidth())

    if #fresh == 0 then
        emptyText:Show()
    else
        emptyText:Hide()
        for i, item in ipairs(fresh) do
            local row = CreateFrame("Frame", nil, keysContent)
            row:SetSize(keysContent:GetWidth(), ROW_H)
            row:SetPoint("TOPLEFT", keysContent, "TOPLEFT", 0, -(i - 1) * ROW_H)

            local rowBg = row:CreateTexture(nil, "BACKGROUND")
            rowBg:SetAllPoints()
            rowBg:SetColorTexture(0, 0, 0, 0)
            row:EnableMouse(true)
            row:SetScript("OnEnter", function() rowBg:SetColorTexture(0.11, 0.13, 0.15, 0.50) end)
            row:SetScript("OnLeave", function() rowBg:SetColorTexture(0, 0, 0, 0) end)

            -- Discord identity (prefix icon + label + name), same language as
            -- the leaderboard's Discord column — this is "which SEAMEN person
            -- has a key", not a raw character list.
            local prefixIconTex = row:CreateTexture(nil, "OVERLAY")
            prefixIconTex:SetSize(14, 14)
            prefixIconTex:SetPoint("LEFT", row, "LEFT", 4, 0)

            local labelFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
            labelFS:SetPoint("LEFT", prefixIconTex, "RIGHT", 2, 0)
            labelFS:SetTextColor(0.8, 0.8, 0.8)

            local nameFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
            nameFS:SetPoint("LEFT", row, "LEFT", 48, 0)
            nameFS:SetWidth(140)
            nameFS:SetJustifyH("LEFT")
            nameFS:SetWordWrap(false)

            local discordNickname = item.entry.discord_nickname
            local p, discordName
            if discordNickname and discordNickname ~= "" then
                p, discordName = ParseDiscordPrefix(discordNickname)
            else
                discordName = item.entry.name
            end

            if p then
                prefixIconTex:SetTexture(p.tex)
                prefixIconTex:Show()
                labelFS:SetText(p.label)
                labelFS:Show()
            else
                prefixIconTex:Hide()
                labelFS:Hide()
            end

            -- Discord name is the main identity; character name rides along
            -- as a secondary tag, colored by class so it's visible at a glance.
            local charToken = CLASS_TOKEN[item.entry.class_]
            local charColor = charToken and RAID_CLASS_COLORS[charToken]
            local charColorHex = charColor
                and string.format("%02x%02x%02x",
                    math.floor(charColor.r * 255), math.floor(charColor.g * 255), math.floor(charColor.b * 255))
                or "999999"

            if discordName == item.entry.name then
                nameFS:SetText(string.format("|cffffffff%s|r", discordName))
            else
                nameFS:SetText(string.format("|cffffffff%s|r |cff%s(%s)|r", discordName, charColorHex, item.entry.name))
            end

            local roleTex = row:CreateTexture(nil, "OVERLAY")
            roleTex:SetTexture(ROLE_TEX[item.entry.role] or "")
            roleTex:SetSize(18, 18)
            roleTex:SetPoint("LEFT", row, "LEFT", 204, 0)

            local keyFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
            keyFS:SetPoint("LEFT", row, "LEFT", 226, 0)
            keyFS:SetText("+" .. item.data.level .. " " .. item.data.short)

            local dotFrame = CreateFrame("Frame", nil, row)
            dotFrame:SetSize(10, 10)
            dotFrame:SetPoint("RIGHT", row, "RIGHT", -4, 0)
            dotFrame:EnableMouse(true)

            local dot = dotFrame:CreateTexture(nil, "OVERLAY")
            dot:SetSize(6, 6)
            dot:SetPoint("CENTER")
            dot:SetColorTexture(TEAL[1], TEAL[2], TEAL[3], 1)

            dotFrame:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_TOP")
                GameTooltip:AddLine("Seen recently through TOAST", 1, 1, 1, true)
                GameTooltip:Show()
            end)
            dotFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)

            local isOnline = TOAST._onlineMembers and TOAST._onlineMembers[item.entry.name]
            local lastSeenFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
            lastSeenFS:SetWidth(60)
            lastSeenFS:SetJustifyH("RIGHT")
            lastSeenFS:SetPoint("RIGHT", dotFrame, "LEFT", -6, 0)
            if isOnline then
                lastSeenFS:SetText("now")
                lastSeenFS:SetTextColor(TEAL[1], TEAL[2], TEAL[3])
            else
                local age = now - item.data.lastSeen
                lastSeenFS:SetText(math.floor(age / 60) .. "m ago")
                if age < 300 then
                    lastSeenFS:SetTextColor(1, 1, 1, 0.6)
                else
                    lastSeenFS:SetTextColor(1, 1, 1, 0.3)
                end
            end

            row:SetScript("OnMouseUp", function(self, button)
                if button ~= "RightButton" then return end
                local target = item.entry.name .. "-" .. item.entry.realm:gsub(" ", "")
                local memberEntry = GetMemberByName(item.entry.name)
                MenuUtil.CreateContextMenu(self, function(ownerRegion, rootDescription)
                    if memberEntry and memberEntry.discord_id and CountCrewByDiscordId(memberEntry.discord_id) > 1 then
                        rootDescription:CreateButton("View Crew", function()
                            TOAST.ShowCrewPopup(memberEntry.discord_id)
                        end)
                    end
                    rootDescription:CreateButton("Copy Name", function()
                        TOAST.ShowCopyPopup(target)
                    end)
                    rootDescription:CreateButton("Whisper", function()
                        ChatFrame_OpenChat("/w " .. target .. " ", DEFAULT_CHAT_FRAME)
                    end)
                    rootDescription:CreateButton("Invite", function()
                        C_PartyInfo.InviteUnit(target)
                    end)
                end)
            end)

            tinsert(keyRows, row)
        end
    end
    keysContent:SetHeight(math.max(#fresh * ROW_H, 1))
end

local function BuildMyCrewRow(parent, entry, y)
    local isCurrent = entry.name == UnitName("player")

    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(CREW_ROW_H)
    row:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, y)
    row:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, y)
    row:EnableMouse(true)

    local bg = row:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()

    local function ResetBg()
        if isCurrent then
            bg:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 0.08)
        else
            bg:SetColorTexture(0, 0, 0, 0)
        end
    end
    ResetBg()

    if isCurrent then
        local accent = row:CreateTexture(nil, "ARTWORK")
        accent:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 1)
        accent:SetWidth(2)
        accent:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
        accent:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
    end

    row:SetScript("OnEnter", function() bg:SetColorTexture(0.11, 0.13, 0.15, 0.50) end)
    row:SetScript("OnLeave", ResetBg)

    local roleTex = row:CreateTexture(nil, "OVERLAY")
    roleTex:SetSize(16, 16)
    roleTex:SetPoint("LEFT", row, "LEFT", 12, 0)
    roleTex:SetTexture(ROLE_TEX[entry.role] or "")

    local token = CLASS_TOKEN[entry.class_]
    local c = token and RAID_CLASS_COLORS[token]
    local nameFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    nameFS:SetPoint("LEFT", roleTex, "RIGHT", 6, 0)
    if c then
        nameFS:SetTextColor(c.r, c.g, c.b)
    end
    nameFS:SetText(entry.name)

    local realmFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    realmFS:SetPoint("LEFT", nameFS, "RIGHT", 4, 0)
    realmFS:SetTextColor(1, 1, 1, 0.5)
    realmFS:SetText(entry.realm)

    local keyFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    keyFS:SetPoint("RIGHT", row, "RIGHT", -12, 0)
    keyFS:SetWidth(70)
    keyFS:SetJustifyH("RIGHT")
    keyFS:SetTextColor(1, 1, 1, 1)

    local resiFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    resiFS:SetPoint("RIGHT", keyFS, "LEFT", -8, 0)
    resiFS:SetWidth(50)
    resiFS:SetJustifyH("RIGHT")

    local rioFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    rioFS:SetPoint("RIGHT", resiFS, "LEFT", -8, 0)
    rioFS:SetWidth(70)
    rioFS:SetJustifyH("RIGHT")

    local function UpdateContent()
        rioFS:SetText(string.format("|cff999999RIO |r|cffffffff%s|r",
            entry.rio and tostring(entry.rio) or "\226\128\148"))

        if entry.resi and entry.resi > 0 then
            resiFS:SetText(string.format("|cff999999Resi |r|cffffffff%d|r", entry.resi))
            resiFS:SetAlpha(1)
        else
            resiFS:SetText("|cff999999Resi |r|cffffffff0|r")
            resiFS:SetAlpha(0.35)
        end

        local keyData = TOAST.keys and TOAST.keys[entry.name]
        keyFS:SetText(keyData and ("+" .. keyData.level .. " " .. keyData.short) or "\226\128\148")
    end
    UpdateContent()

    row:SetScript("OnMouseUp", function(self, button)
        if button ~= "RightButton" then return end
        local target = entry.name .. "-" .. entry.realm:gsub(" ", "")
        MenuUtil.CreateContextMenu(self, function(ownerRegion, rootDescription)
            rootDescription:CreateButton("Copy Name", function()
                TOAST.ShowCopyPopup(target)
            end)
            rootDescription:CreateButton("Invite", function()
                C_PartyInfo.InviteUnit(target)
            end)
            rootDescription:CreateButton("Whisper", function()
                ChatFrame_OpenChat("/w " .. target .. " ", DEFAULT_CHAT_FRAME)
            end)
        end)
    end)

    return UpdateContent
end

local function RefreshMyCrew()
    if not myCrewCard then return end
    local me = TOAST_DB.me
    if not me or not me.discord_id then
        myCrewCard:Hide()
        return
    end
    myCrewCard:Show()
    for _, update in ipairs(myCrewRows) do
        update()
    end
end

local function FormatDelta(diff)
    if diff > 0 then return "+" .. diff .. " RIO" end
    return diff .. " RIO"  -- Lua's tostring on a negative number already has the "-"
end

local function SetDeltaColor(fs, diff)
    if diff > 0 then
        fs:SetTextColor(SOFT_GREEN[1], SOFT_GREEN[2], SOFT_GREEN[3])
    elseif diff < 0 then
        fs:SetTextColor(SOFT_RED[1], SOFT_RED[2], SOFT_RED[3])
    else
        fs:SetTextColor(1, 1, 1, 0.6)
    end
end

local function SetProgressPrefix(iconTex, labelFS, discordNickname)
    local p = discordNickname and discordNickname ~= "" and (ParseDiscordPrefix(discordNickname)) or nil
    if p then
        iconTex:SetTexture(p.tex)
        iconTex:Show()
        labelFS:SetText(p.label)
        labelFS:Show()
    else
        iconTex:Hide()
        labelFS:Hide()
    end
end

local function RefreshLeaderboardPreview()
    local me = TOAST_DB.me
    if not me or not me.rank or #TOAST.leaderboard == 0 then
        rankFS:SetTextColor(1, 1, 1, 0.5)
        rankFS:SetText("Leaderboard data unavailable")
        nextLabelFS:Hide(); nextPrefixIcon:Hide(); nextPrefixLabelFS:Hide()
        nextNameFS:Hide(); nextDeltaFS:Hide(); nextFallbackFS:Hide()
        behindLabelFS:Hide(); behindPrefixIcon:Hide(); behindPrefixLabelFS:Hide()
        behindNameFS:Hide(); behindDeltaFS:Hide()
        return
    end

    rankFS:SetTextColor(GOLD[1], GOLD[2], GOLD[3])
    rankFS:SetText(string.format("#%d of %d", me.rank, #TOAST.leaderboard))

    local above = me.rank > 1 and TOAST.leaderboard[me.rank - 1] or nil
    local below = TOAST.leaderboard[me.rank + 1]

    if above then
        nextFallbackFS:Hide()
        nextLabelFS:Show(); nextNameFS:Show(); nextDeltaFS:Show()
        nextLabelFS:SetText("Next")
        SetProgressPrefix(nextPrefixIcon, nextPrefixLabelFS, above.discord_nickname)
        nextNameFS:SetText(above.name)
        local diff = me.rio - above.rio
        nextDeltaFS:SetText(FormatDelta(diff))
        SetDeltaColor(nextDeltaFS, diff)
    else
        nextLabelFS:Hide(); nextPrefixIcon:Hide(); nextPrefixLabelFS:Hide()
        nextNameFS:Hide(); nextDeltaFS:Hide()
        nextFallbackFS:SetText("Top of the crew")
        nextFallbackFS:Show()
    end

    if below then
        behindLabelFS:Show(); behindNameFS:Show(); behindDeltaFS:Show()
        behindLabelFS:SetText("Behind")
        SetProgressPrefix(behindPrefixIcon, behindPrefixLabelFS, below.discord_nickname)
        behindNameFS:SetText(below.name)
        local diff = me.rio - below.rio
        behindDeltaFS:SetText(FormatDelta(diff))
        SetDeltaColor(behindDeltaFS, diff)
    else
        behindLabelFS:Hide(); behindPrefixIcon:Hide(); behindPrefixLabelFS:Hide()
        behindNameFS:Hide(); behindDeltaFS:Hide()
    end
end

local function RefreshFooter()
    local now = GetServerTime()
    local online, known = 0, 0
    for _, data in pairs(TOAST.keys or {}) do
        known = known + 1
        if data.lastSeen and (now - data.lastSeen) < FRESH_THRESHOLD then
            online = online + 1
        end
    end

    pulseOnlineFS:SetText(online .. " crew online")
    pulseOnlineBtn:SetWidth(pulseOnlineFS:GetStringWidth())

    pulseKeysFS:SetText(known .. " keys spotted")
    pulseKeysBtn:SetWidth(pulseKeysFS:GetStringWidth())

    pulseMembersFS:SetText(#TOAST.leaderboard .. " members")
    pulseMembersBtn:SetWidth(pulseMembersFS:GetStringWidth())
end

local function Refresh()
    TOAST._onlineMembers = TOAST.GetOnlineMembers and TOAST.GetOnlineMembers() or nil
    RefreshStatus()
    RefreshMyCrew()
    RefreshKeysList()
    RefreshLeaderboardPreview()
    RefreshFooter()
end

local function BuildDashboard()
    local myCrewEntries = {}
    if TOAST_DB.me and TOAST_DB.me.discord_id then
        for _, m in ipairs(TOAST.members) do
            if m.discord_id == TOAST_DB.me.discord_id then
                tinsert(myCrewEntries, m)
            end
        end
    end
    local hasMyCrew = #myCrewEntries > 0
    local myCrewHeight = hasMyCrew and (MYCREW_HEADER_H + (#myCrewEntries * CREW_ROW_H)) or 0
    local myCrewDelta = hasMyCrew and (myCrewHeight + CARD_GAP) or 0

    dash = CreateFrame("Frame", "TOASTDashboard", UIParent, "BackdropTemplate")
    Mixin(dash, BackdropTemplateMixin)
    dash:SetSize(W, H + myCrewDelta)
    dash:SetPoint("CENTER")
    dash:SetMovable(true)
    dash:EnableMouse(true)
    dash:RegisterForDrag("LeftButton")
    dash:SetScript("OnDragStart", dash.StartMoving)
    dash:SetScript("OnDragStop", dash.StopMovingOrSizing)
    dash:SetFrameStrata("DIALOG")
    dash:Hide()
    tinsert(UISpecialFrames, "TOASTDashboard")

    local bg = dash:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(0.06, 0.08, 0.10, 1.0)

    AddBorder(dash, BORDER)

    -- Subtle outer frame border — shouldn't compete with the header divider.
    local accent = dash:CreateTexture(nil, "ARTWORK")
    accent:SetColorTexture(GOLD[1], GOLD[2], GOLD[3], 1)
    accent:SetPoint("TOPLEFT", dash, "TOPLEFT", 0, 0)
    accent:SetPoint("TOPRIGHT", dash, "TOPRIGHT", 0, 0)
    accent:SetHeight(1)

    -- Stormy harbor banner, header area only (dash has no separate header
    -- frame, so this is just sized to the top band instead of a child frame).
    -- Dark/empty on the left (behind the title), harbor detail fades in on
    -- the right — same asset design language as the leaderboard's tentacle
    -- banner, so no manual crop is needed.
    local headerBanner = dash:CreateTexture(nil, "BACKGROUND")
    headerBanner:SetTexCoord(0, 1, 0, 1)
    headerBanner:SetPoint("TOPLEFT", dash, "TOPLEFT", 0, 0)
    headerBanner:SetPoint("TOPRIGHT", dash, "TOPRIGHT", 0, 0)
    headerBanner:SetHeight(DASHBOARD_HEADER_HEIGHT)
    headerBanner:SetVertexColor(1, 1, 1, 0.48)

    -- Setting the texture at creation time can silently no-op on first
    -- login (frame not yet shown when the lazy-load PNG/TGA resolves).
    -- Re-applying on OnShow guarantees it's set once the frame is actually visible.
    dash:HookScript("OnShow", function()
        headerBanner:SetTexture("Interface\\AddOns\\TOAST\\media\\banners\\tentakel_2.tga")
    end)

    TOAST.CreateCloseButton(dash, function() dash:Hide() end)

    local logo = dash:CreateTexture(nil, "ARTWORK")
    logo:SetTexture("Interface\\AddOns\\TOAST\\media\\discord\\toast_128.tga")
    logo:SetSize(64, 64)
    logo:SetPoint("TOPLEFT", dash, "TOPLEFT", 12, -8)

    local titleFS = dash:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
    titleFS:SetPoint("TOPLEFT", logo, "TOPRIGHT", 10, -8)
    titleFS:SetTextColor(GOLD[1], GOLD[2], GOLD[3])
    titleFS:SetText("TOAST DASHBOARD")

    local versionFS = dash:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    versionFS:SetPoint("TOPLEFT", titleFS, "BOTTOMLEFT", 0, -4)
    versionFS:SetTextColor(1, 1, 1, 0.45)
    versionFS:SetText("v" .. TOAST.VERSION)

    -- CARD A: YOUR STATUS
    -- skipAccent: the header's anchor divider is the sole gold separator
    -- above this card now, so its own top gold line would be a duplicate.
    cardA = CreateCard(dash, CARD_A_TOP, CARD_A_HEIGHT, true)

    -- Gold divider + anchor medallion between the header banner and Card A,
    -- aligned to card width (not the outer frame) and restrained in size
    -- since the dashboard is card-based rather than a full-width table like
    -- the leaderboard. CARD_A_TOP already clears the medallion fully, so
    -- aboveFrame is just a defensive guard against future size changes.
    TOAST.CreateAnchorDivider(dash, PAD, W - PAD, HEADER_DIVIDER_Y, {
        medallionSize = HEADER_DIVIDER_MEDALLION,
        gap = HEADER_DIVIDER_GAP,
        lineAlpha = 0.85,  -- dashboard-only softening; leaderboard keeps its default 1.0
        aboveFrame = cardA,
    })

    -- Group 1: identity
    prefixIcon = cardA:CreateTexture(nil, "OVERLAY")
    prefixIcon:SetSize(18, 18)
    prefixIcon:SetPoint("TOPLEFT", cardA, "TOPLEFT", 12, -10)

    prefixLabel = cardA:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    prefixLabel:SetPoint("LEFT", prefixIcon, "RIGHT", 4, 0)
    prefixLabel:SetTextColor(0.8, 0.8, 0.8)

    discordNameFS = cardA:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    discordNameFS:SetPoint("LEFT", prefixLabel, "RIGHT", 6, 0)

    charIcon = cardA:CreateTexture(nil, "OVERLAY")
    charIcon:SetSize(16, 16)
    charIcon:SetPoint("TOPLEFT", prefixIcon, "BOTTOMLEFT", 0, -8)

    charLine = cardA:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    charLine:SetPoint("LEFT", charIcon, "RIGHT", 4, 0)

    -- Group 2: status badges
    statusLabel = cardA:CreateFontString(nil, "OVERLAY")
    statusLabel:SetFont("Fonts\\FRIZQT__.TTF", 7, "")
    statusLabel:SetPoint("TOPLEFT", charIcon, "BOTTOMLEFT", 0, -14)
    statusLabel:SetTextColor(1, 1, 1, 0.18)
    statusLabel:SetText("STATUS")

    -- Group 3: progress badges
    progressLabel = cardA:CreateFontString(nil, "OVERLAY")
    progressLabel:SetFont("Fonts\\FRIZQT__.TTF", 7, "")
    progressLabel:SetPoint("TOPLEFT", statusLabel, "BOTTOMLEFT", 0, -(6 + 15 + 12))
    progressLabel:SetTextColor(1, 1, 1, 0.18)
    progressLabel:SetText("PROGRESS")

    -- Footer: key sharing live status
    keySharingLine = cardA:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    keySharingLine:SetPoint("TOPLEFT", progressLabel, "BOTTOMLEFT", 0, -(6 + 15 + 12))
    keySharingLine:SetTextColor(0.6, 0.6, 0.6)

    -- CARD MYCREW: MY CREW (only if the player is discord-linked)
    local belowCardA = CARD_A_TOP - CARD_A_HEIGHT - CARD_GAP
    if hasMyCrew then
        myCrewCard = CreateCard(dash, belowCardA, myCrewHeight)

        local myCrewTitle = myCrewCard:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        myCrewTitle:SetPoint("TOPLEFT", myCrewCard, "TOPLEFT", 12, -10)
        myCrewTitle:SetTextColor(GOLD[1], GOLD[2], GOLD[3])
        myCrewTitle:SetText("My Crew")

        local myCrewHelper = myCrewCard:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        myCrewHelper:SetPoint("TOPRIGHT", myCrewCard, "TOPRIGHT", -12, -10)
        myCrewHelper:SetTextColor(1, 1, 1, 0.35)
        myCrewHelper:SetText("Managed in Discord /crew")

        wipe(myCrewRows)
        for i = 1, math.min(#myCrewEntries, 3) do
            local update = BuildMyCrewRow(myCrewCard, myCrewEntries[i], -(MYCREW_HEADER_H + (i - 1) * CREW_ROW_H))
            tinsert(myCrewRows, update)
        end
    end

    -- CARD B: CURRENT SEAMEN KEYS
    local cardB = CreateCard(dash, belowCardA - myCrewDelta, CARD_B_HEIGHT)
    local keysLabel = cardB:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    keysLabel:SetPoint("TOPLEFT", cardB, "TOPLEFT", 12, -10)
    keysLabel:SetTextColor(GOLD[1], GOLD[2], GOLD[3])
    keysLabel:SetText("Current SEAMEN Keys")

    emptyText = cardB:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    emptyText:SetPoint("TOPLEFT", keysLabel, "BOTTOMLEFT", 4, -10)
    emptyText:SetTextColor(1, 1, 1, 0.45)
    emptyText:SetText("No fresh keys right now")

    keysScroll = CreateFrame("ScrollFrame", "TOASTDashboardKeysSF", cardB)
    keysScroll:SetPoint("TOPLEFT", cardB, "TOPLEFT", 8, -34)
    keysScroll:SetPoint("BOTTOMRIGHT", cardB, "BOTTOMRIGHT", -28, 8)

    keysContent = CreateFrame("Frame", nil, keysScroll)
    keysContent:SetSize(1, 1)
    keysScroll:SetScrollChild(keysContent)

    TOAST.CreateCustomScrollbar(keysScroll, keysContent)

    -- CARD C: YOUR PROGRESS
    local cardC = CreateCard(dash, belowCardA - CARD_B_HEIGHT - CARD_GAP - myCrewDelta, CARD_C_HEIGHT)

    local progressTitle = cardC:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    progressTitle:SetPoint("TOPLEFT", cardC, "TOPLEFT", 12, -10)
    progressTitle:SetTextColor(GOLD[1], GOLD[2], GOLD[3])
    progressTitle:SetText("Your Progress")

    rankFS = cardC:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    rankFS:SetPoint("TOPLEFT", progressTitle, "BOTTOMLEFT", 0, -10)
    rankFS:SetTextColor(GOLD[1], GOLD[2], GOLD[3])

    -- Button aligned with the rank line (the actual progress content),
    -- not the card title, so it reads as part of the content block.
    local lbBtn = CreateActionButton(cardC, "LEADERBOARD", "gold", 120, 22)
    lbBtn:SetPoint("TOP", rankFS, "TOP", 0, -2)
    lbBtn:SetPoint("RIGHT", cardC, "RIGHT", -12, 0)
    lbBtn:SetScript("OnClick", function()
        dash:Hide()
        TOAST.commands["lb"]()
    end)

    -- Next row: label / player name / delta, fixed columns via cardC LEFT + x.
    nextLabelFS = cardC:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    nextLabelFS:SetPoint("TOPLEFT", rankFS, "BOTTOMLEFT", 0, -8)
    nextLabelFS:SetWidth(50)
    nextLabelFS:SetJustifyH("LEFT")
    nextLabelFS:SetTextColor(1, 1, 1, 0.5)

    nextPrefixIcon = cardC:CreateTexture(nil, "OVERLAY")
    nextPrefixIcon:SetSize(14, 14)
    nextPrefixIcon:SetPoint("TOP", nextLabelFS, "TOP", 0, 0)
    nextPrefixIcon:SetPoint("LEFT", cardC, "LEFT", 66, 0)

    nextPrefixLabelFS = cardC:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    nextPrefixLabelFS:SetPoint("TOP", nextLabelFS, "TOP", 0, 0)
    nextPrefixLabelFS:SetPoint("LEFT", nextPrefixIcon, "RIGHT", 2, 0)
    nextPrefixLabelFS:SetTextColor(0.8, 0.8, 0.8)

    nextNameFS = cardC:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    nextNameFS:SetPoint("TOP", nextLabelFS, "TOP", 0, 0)
    nextNameFS:SetPoint("LEFT", cardC, "LEFT", 112, 0)
    nextNameFS:SetWidth(90)
    nextNameFS:SetJustifyH("LEFT")
    nextNameFS:SetTextColor(1, 1, 1, 1)

    nextDeltaFS = cardC:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    nextDeltaFS:SetPoint("TOP", nextLabelFS, "TOP", 0, 0)
    nextDeltaFS:SetPoint("LEFT", cardC, "LEFT", 215, 0)
    nextDeltaFS:SetWidth(90)
    nextDeltaFS:SetJustifyH("LEFT")

    -- Fallback for "no player above" (rank #1) — replaces the whole row.
    nextFallbackFS = cardC:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    nextFallbackFS:SetPoint("TOP", nextLabelFS, "TOP", 0, 0)
    nextFallbackFS:SetPoint("LEFT", cardC, "LEFT", 12, 0)
    nextFallbackFS:SetTextColor(1, 1, 1, 0.5)
    nextFallbackFS:Hide()

    -- Behind row: same column layout, one row down.
    behindLabelFS = cardC:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    behindLabelFS:SetPoint("TOPLEFT", nextLabelFS, "BOTTOMLEFT", 0, -4)
    behindLabelFS:SetWidth(50)
    behindLabelFS:SetJustifyH("LEFT")
    behindLabelFS:SetTextColor(1, 1, 1, 0.5)

    behindPrefixIcon = cardC:CreateTexture(nil, "OVERLAY")
    behindPrefixIcon:SetSize(14, 14)
    behindPrefixIcon:SetPoint("TOP", behindLabelFS, "TOP", 0, 0)
    behindPrefixIcon:SetPoint("LEFT", cardC, "LEFT", 66, 0)

    behindPrefixLabelFS = cardC:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    behindPrefixLabelFS:SetPoint("TOP", behindLabelFS, "TOP", 0, 0)
    behindPrefixLabelFS:SetPoint("LEFT", behindPrefixIcon, "RIGHT", 2, 0)
    behindPrefixLabelFS:SetTextColor(0.8, 0.8, 0.8)

    behindNameFS = cardC:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    behindNameFS:SetPoint("TOP", behindLabelFS, "TOP", 0, 0)
    behindNameFS:SetPoint("LEFT", cardC, "LEFT", 112, 0)
    behindNameFS:SetWidth(90)
    behindNameFS:SetJustifyH("LEFT")
    behindNameFS:SetTextColor(1, 1, 1, 1)

    behindDeltaFS = cardC:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    behindDeltaFS:SetPoint("TOP", behindLabelFS, "TOP", 0, 0)
    behindDeltaFS:SetPoint("LEFT", cardC, "LEFT", 215, 0)
    behindDeltaFS:SetWidth(90)
    behindDeltaFS:SetJustifyH("LEFT")

    -- FOOTER: Community Pulse
    local footer = CreateFrame("Frame", nil, dash)
    footer:SetSize(W - PAD * 2, 24)
    footer:SetPoint("BOTTOM", dash, "BOTTOM", 0, PAD)

    local function CreatePulseHoverZone(fs, tooltipText)
        local btn = CreateFrame("Button", nil, footer)
        btn:SetPoint("LEFT", fs, "LEFT", 0, 0)
        btn:SetHeight(20)
        btn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_TOP")
            GameTooltip:AddLine(tooltipText, 1, 1, 1, true)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        return btn
    end

    pulseOnlineFS = footer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    pulseOnlineFS:SetPoint("LEFT", footer, "LEFT", 4, 0)
    pulseOnlineFS:SetTextColor(TEAL[1], TEAL[2], TEAL[3])

    local pulseBullet1 = footer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    pulseBullet1:SetPoint("LEFT", pulseOnlineFS, "RIGHT", 6, 0)
    pulseBullet1:SetTextColor(0.4, 0.4, 0.4)
    pulseBullet1:SetText("\226\128\162")

    pulseKeysFS = footer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    pulseKeysFS:SetPoint("LEFT", pulseBullet1, "RIGHT", 6, 0)
    pulseKeysFS:SetTextColor(TEAL[1], TEAL[2], TEAL[3])

    local pulseBullet2 = footer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    pulseBullet2:SetPoint("LEFT", pulseKeysFS, "RIGHT", 6, 0)
    pulseBullet2:SetTextColor(0.4, 0.4, 0.4)
    pulseBullet2:SetText("\226\128\162")

    pulseMembersFS = footer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    pulseMembersFS:SetPoint("LEFT", pulseBullet2, "RIGHT", 6, 0)
    pulseMembersFS:SetTextColor(TEAL[1], TEAL[2], TEAL[3])

    pulseOnlineBtn = CreatePulseHoverZone(pulseOnlineFS, "Players seen recently through TOAST")
    pulseKeysBtn = CreatePulseHoverZone(pulseKeysFS, "Keys stored in your local TOAST cache")
    pulseMembersBtn = CreatePulseHoverZone(pulseMembersFS, "Players on the SEAMEN leaderboard")

    local discordBtn = CreateFrame("Button", nil, footer)
    discordBtn:SetSize(20, 20)
    discordBtn:SetPoint("RIGHT", footer, "RIGHT", -4, 0)

    local discordIcon = discordBtn:CreateTexture(nil, "OVERLAY")
    discordIcon:SetAllPoints()
    discordIcon:SetTexture("Interface\\AddOns\\TOAST\\media\\discord\\discord.png")
    discordIcon:SetAlpha(0.8)

    discordBtn:SetScript("OnEnter", function() discordIcon:SetAlpha(1) end)
    discordBtn:SetScript("OnLeave", function() discordIcon:SetAlpha(0.8) end)
    discordBtn:SetScript("OnClick", function()
        if discordPopover:IsShown() then
            discordPopover:Hide()
        else
            discordPopover:Show()
        end
    end)

    discordPopover = CreateFrame("Frame", nil, dash, "BackdropTemplate")
    Mixin(discordPopover, BackdropTemplateMixin)
    discordPopover:SetSize(240, 40)
    discordPopover:SetPoint("BOTTOMRIGHT", discordBtn, "TOPRIGHT", 0, 6)
    discordPopover:SetFrameStrata("TOOLTIP")
    discordPopover:Hide()

    local popBg = discordPopover:CreateTexture(nil, "BACKGROUND")
    popBg:SetAllPoints()
    popBg:SetColorTexture(0.06, 0.08, 0.10, 1.0)
    AddBorder(discordPopover, BORDER)

    local discordEB = CreateFrame("EditBox", nil, discordPopover, "InputBoxTemplate")
    discordEB:SetSize(220, 22)
    discordEB:SetPoint("CENTER")
    LockEditBox(discordEB, DISCORD_INVITE)
end

TOAST.commands["dashboard"] = function()
    if not dash then BuildDashboard() end
    if dash:IsShown() then
        dash:Hide()
    else
        Refresh()
        dash:Show()
    end
end
