☰

🛠 Discord Plus+ V1 — Bulk Delete, Msg Logger, Ghost Ping & Export

🗑 Bulk delete YOUR messages in any channel or DM using Discord API v10 (smart rate-limit, retry, date/content filters). 👁 Log deleted messages in real-time. 👻 Auto-detect ghost pings with toast alerts. 📊 Export channel history to HTML/JSON/TXT. 🔑 One-click token detector + account info. All-in-one Discord web toolkit — no data leaves your browser. Works with Tampermonkey & Violentmonkey.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name               🛠 Discord Plus+ V1 — Bulk Delete, Msg Logger, Ghost Ping & Export
// @name:vi            Discord Plus+ V1 — Xóa tin nhắn hàng loạt, theo dõi xóa, ghost ping & xuất file
// @name:zh-CN         Discord Plus+ V1 — 批量删消息、消息记圕、Ghost Ping 检测䞎富出
// @name:zh-TW         Discord Plus+ V1 — 批量刪蚊息、蚊息蚘錄、Ghost Ping 偵枬與匯出
// @name:ru            Discord Plus+ V1 — МассПвПе уЎалеМОе, лПггер сППбщеМОй, Ghost Ping О экспПрт
// @name:ja            Discord Plus+ V1 — 䞀括削陀・メッセヌゞログ・Ghost Ping 怜出・゚クスポヌト
// @name:ko            Discord Plus+ V1 — 대량 삭제, 메시지 로거, Ghost Ping 감지 및 낎볎낎Ʞ
// @name:es            Discord Plus+ V1 — Borrado masivo, registro de mensajes, Ghost Ping y exportación
// @name:pt-BR         Discord Plus+ V1 — Exclusão em massa, logger de mensagens, Ghost Ping e exportação
// @name:fr            Discord Plus+ V1 — Suppression en masse, journal, Ghost Ping et export
// @name:de            Discord Plus+ V1 — Massenlöschung, Nachrichten-Logger, Ghost Ping & Export
// @name:tr            Discord Plus+ V1 — Toplu Silme, Mesaj Kaydedici, Ghost Ping ve Dışa Aktarma
// @name:id            Discord Plus+ V1 — Hapus Massal, Logger Pesan, Ghost Ping & Ekspor
// @name:pl            Discord Plus+ V1 — Masowe usuwanie, logger wiadomości, Ghost Ping i eksport
// @name:th            Discord Plus+ V1 — àž¥àžšàž‚à¹‰àž­àž„àž§àž²àž¡àžˆàž³àž™àž§àž™àž¡àž²àž, àžšàž±àž™àž—àž¶àžàž‚à¹‰àž­àž„àž§àž²àž¡, Ghost Ping à¹àž¥àž°àžªà¹ˆàž‡àž­àž­àž
// @name:ar            Discord Plus+ V1 — الحذف الجماعي, تسجيل الرسا؊ل, Ghost Ping والتصدير

// @description        🗑 Bulk delete YOUR messages in any channel or DM using Discord API v10 (smart rate-limit, retry, date/content filters). 👁 Log deleted messages in real-time. 👻 Auto-detect ghost pings with toast alerts. 📊 Export channel history to HTML/JSON/TXT. 🔑 One-click token detector + account info. All-in-one Discord web toolkit — no data leaves your browser. Works with Tampermonkey & Violentmonkey.
// @description:vi     🗑 Xóa hàng loạt TIN NHẮN CỊA BẠN trong bất kỳ kênh hoặc DM nào bằng Discord API v10 (giới hạn tốc độ thÃŽng minh, thá»­ lại, bộ lọc ngày/nội dung). 👁 Ghi lại tin nhắn bị xóa theo thời gian thá»±c. 👻 Tá»± động phát hiện ghost ping với thÃŽng báo. 📊 Xuất lịch sá»­ kênh sang HTML/JSON/TXT. 🔑 Phát hiện token một cú nhấp + thÃŽng tin tài khoản. Bộ cÃŽng cụ Discord web all-in-one — khÃŽng có dữ liệu rời khỏi trình duyệt cá»§a bạn.
// @description:zh-CN  🗑 䜿甚 Discord API v10 批量删陀任意频道或私聊䞭的消息智胜限速、重试、日期/内容过滀。👁 实时记圕被删消息。👻 自劚检测 Ghost Ping 并匹出提瀺。📊 将频道历史富出䞺 HTML/JSON/TXT。🔑 䞀键获取 Token + 莊户信息。党胜 Discord 工具包——数据䞍犻匀悚的浏览噚。
// @description:ru     🗑 МассПвПе уЎалеМОе ВАКИХ сППбщеМОй в любПЌ каМале ОлО ЛС через Discord API v10 (уЌМый rate-limit, пПвтПрМые пПпыткО, фОльтры пП Ўате О сПЎержОЌПЌу). 👁 ЛПгОрПваМОе уЎалёММых сППбщеМОй в реальМПЌ вреЌеМО. 👻 АвтПЎетект ghost ping с увеЎПЌлеМОяЌО. 📊 ЭкспПрт ОстПрОО каМала в HTML/JSON/TXT. 🔑 АвтППпреЎелеМОе тПкеМа + ОМфПрЌацОя Пб аккауМте.
// @description:ja     🗑 Discord API v10でチャンネルたたはDM内のメッセヌゞを䞀括削陀スマヌトなレヌトリミット・リトラむ・日付/内容フィルタ。👁 削陀されたメッセヌゞをリアルタむムで蚘録。👻 Ghost Pingを自動怜出しトヌスト通知。📊 チャンネル履歎をHTML/JSON/TXTぞ゚クスポヌト。🔑 ワンクリックでトヌクン取埗アカりント情報衚瀺。
// @description:ko     🗑 Discord API v10윌로 채널/DM에서 메시지 대량 삭제(슀마튞 rate-limit, 재시도, 날짜/낎용 필터). 👁 삭제된 메시지 싀시간 Ʞ록. 👻 Ghost Ping 자동 감지 + 토슀튞 알늌. 📊 채널 Ʞ록을 HTML/JSON/TXT로 낎볎낎Ʞ. 🔑 원큎늭 토큰 감지 + 계정 정볎.
// @description:zh-TW  🗑 䜿甚 Discord API v10 批量刪陀任意頻道或私聊䞭的蚊息智胜限速、重詊、日期/內容過濟。👁 即時蚘錄被刪蚊息。👻 自動偵枬 Ghost Ping 䞊圈出提瀺。📊 將頻道歷史匯出為 HTML/JSON/TXT。🔑 䞀鍵獲取 Token + 垳戶資蚊。党胜 Discord 工具包——資料䞍離開悚的瀏芜噚。
// @description:es     🗑 Elimina en masa TUS mensajes en cualquier canal o DM con Discord API v10 (límite de velocidad inteligente, reintentos, filtros de fecha/contenido). 👁 Registra mensajes eliminados en tiempo real. 👻 Detecta automáticamente ghost pings con alertas. 📊 Exporta el historial del canal a HTML/JSON/TXT. 🔑 Detector de token con un clic + info de cuenta. Cero datos salen de tu navegador.
// @description:pt-BR  🗑 Exclua em massa SUAS mensagens em qualquer canal ou DM com Discord API v10 (rate-limit inteligente, tentativas, filtros de data/conteúdo). 👁 Registre mensagens deletadas em tempo real. 👻 Detecte automaticamente ghost pings com alertas. 📊 Exporte o histórico do canal para HTML/JSON/TXT. 🔑 Detector de token com um clique + info da conta. Nenhum dado sai do seu navegador.
// @description:fr     🗑 Supprimez en masse VOS messages dans n'importe quel salon ou DM via Discord API v10 (rate-limit intelligent, nouvelles tentatives, filtres date/contenu). 👁 Enregistrez les messages supprimés en temps réel. 👻 Détectez automatiquement les ghost pings. 📊 Exportez l'historique du salon en HTML/JSON/TXT. 🔑 Détection de token en un clic + infos du compte. Aucune donnée ne quitte votre navigateur.
// @description:de     🗑 Massenlöschung DEINER Nachrichten in jedem Kanal oder DM mit Discord API v10 (intelligentes Rate-Limit, Wiederholungen, Datum-/Inhaltsfilter). 👁 Gelöschte Nachrichten in Echtzeit protokollieren. 👻 Ghost Pings automatisch erkennen mit Benachrichtigung. 📊 Kanalverlauf als HTML/JSON/TXT exportieren. 🔑 Ein-Klick-Token-Erkennung + Kontoinformationen. Keine Daten verlassen Ihren Browser.
// @description:tr     🗑 Discord API v10 ile herhangi bir kanal veya DM'deki MESAJLARINIZI toplu silin (akıllı rate-limit, yeniden deneme, tarih/içerik filtreleri). 👁 Silinen mesajları gerçek zamanlı kaydedin. 👻 Ghost ping'leri otomatik tespit edin. 📊 Kanal geçmişini HTML/JSON/TXT olarak dışa aktarın. 🔑 Tek tıkla token tespiti + hesap bilgisi. Hiçbir veri tarayıcınızdan çıkmaz.
// @description:id     🗑 Hapus massal PESAN ANDA di channel atau DM mana pun menggunakan Discord API v10 (rate-limit cerdas, percobaan ulang, filter tanggal/konten). 👁 Catat pesan yang dihapus secara real-time. 👻 Deteksi ghost ping otomatis dengan notifikasi. 📊 Ekspor riwayat channel ke HTML/JSON/TXT. 🔑 Deteksi token satu klik + info akun. Tidak ada data yang keluar dari browser Anda.
// @description:pl     🗑 Masowe usuwanie TWOICH wiadomości na dowolnym kanale lub DM przez Discord API v10 (inteligentny rate-limit, ponowne próby, filtry daty/treści). 👁 Rejestruj usunięte wiadomości w czasie rzeczywistym. 👻 Automatyczne wykrywanie ghost pingów z powiadomieniami. 📊 Eksportuj historię kanału do HTML/JSON/TXT. 🔑 Wykrywanie tokena jednym kliknięciem + informacje o koncie.
// @description:th     🗑 àž¥àžšàž‚à¹‰àž­àž„àž§àž²àž¡àž‚àž­àž‡àž„àžžàž“àžˆàž³àž™àž§àž™àž¡àž²àžà¹ƒàž™àžŠà¹ˆàž­àž‡àž«àž£àž·àž­ DM à¹ƒàž”àžà¹‡à¹„àž”à¹‰àž”à¹‰àž§àž¢ Discord API v10 (àžˆàž³àžàž±àž”àž­àž±àž•àž£àž²àž­àž±àžˆàž‰àž£àžŽàž¢àž°, àž¥àž­àž‡à¹ƒàž«àž¡à¹ˆ, àžàž£àž­àž‡àž§àž±àž™àž—àžµà¹ˆ/à¹€àž™àž·à¹‰àž­àž«àž²) 👁 àžšàž±àž™àž—àž¶àžàž‚à¹‰àž­àž„àž§àž²àž¡àž—àžµà¹ˆàž–àž¹àžàž¥àžšà¹àžšàžšà¹€àž£àžµàž¢àž¥à¹„àž—àž¡à¹Œ 👻 àž•àž£àž§àžˆàžˆàž±àžš ghost ping àž­àž±àž•à¹‚àž™àž¡àž±àž•àžŽàžžàž£à¹‰àž­àž¡à¹àžˆà¹‰àž‡à¹€àž•àž·àž­àž™ 📊 àžªà¹ˆàž‡àž­àž­àžàž›àž£àž°àž§àž±àž•àžŽàžŠà¹ˆàž­àž‡à¹€àž›à¹‡àž™ HTML/JSON/TXT 🔑 àž•àž£àž§àžˆàžˆàž±àžš token àž„àž¥àžŽàžà¹€àž”àžµàž¢àž§ + àž‚à¹‰àž­àž¡àž¹àž¥àžšàž±àžàžŠàžµ à¹„àž¡à¹ˆàž¡àžµàž‚à¹‰àž­àž¡àž¹àž¥àž­àž­àžàžˆàž²àžà¹€àžšàž£àž²àž§à¹Œà¹€àž‹àž­àž£à¹Œàž‚àž­àž‡àž„àžžàž“
// @description:ar     🗑 احذف رسا؊لك ؚ؎كل جماعي في أي قناة أو DM ؚاستخدام Discord API v10 (حد معدل ذكي، إعادة المحاولة، فلاتر التاريخ/المحتوى). 👁 سجّل الرسا؊ل المحذوفة في الوقت الفعلي. 👻 اكت؎ف Ghost Ping تلقا؊يًا مع إ؎عارات. 📊 صدّر تاريخ القناة ؚصيغة HTML/JSON/TXT. 🔑 ك؎ف التوكن ؚنقرة واحدة + معلومات الحساؚ. لا تغادر أي ؚيانات متصفحك.

// @namespace          https://greatest.deepsurf.us/users/1510019
// @version            1.0.0
// @author             2pixel
// @license            MIT
// @icon               https://raw.githubusercontent.com/not2pixel/TampermonkeyProjects/refs/heads/main/EasyTube.png
// @icon64             https://raw.githubusercontent.com/not2pixel/TampermonkeyProjects/refs/heads/main/EasyTube.png
// @homepageURL        https://discord.gg/Gvmd7deFtS
// @supportURL         https://discord.gg/Gvmd7deFtS

// @match              https://discord.com/*
// @match              https://discordapp.com/*
// @exclude            https://discord.com/developers/*

// @grant              GM_addStyle
// @grant              GM_setValue
// @grant              GM_getValue
// @grant              GM_xmlhttpRequest
// @connect            discord.com
// @connect            discordapp.com

// @run-at             document-idle
// @compatible         chrome   Tested on Chrome 120+ with Tampermonkey
// @compatible         firefox  Tested on Firefox 120+ with Tampermonkey / Violentmonkey
// @compatible         edge     Tested on Edge 120+ with Tampermonkey
// @compatible         brave    Recommended (Manifest V3 compatible)
// ==/UserScript==

'use strict';

// ─── CONSTANTS ───────────────────────────────────────────────────────────────
const VERSION       = '1.0.0';
const API           = 'https://discord.com/api/v10';
const DISCORD_EPOCH = 1420070400000n;
const COMMUNITY     = 'https://discord.gg/Gvmd7deFtS';
const MAX_RETRIES   = 5;
const PANEL_ID      = 'dp2_panel';
const TOGGLE_ID     = 'dp2_toggle';

// ─── STATE ───────────────────────────────────────────────────────────────────
const S = {
  token:       null,
  activeTab:   'delete',
  delRunning:  false,
  delStopped:  false,
  delCount:    0,
  failCount:   0,
  scanCount:   0,
  deletedLog:  [],   // { id, content, author, channel, time }
  ghostPings:  [],   // { author, channel, content, time }
  observers:   [],
};

// ─── UTILS ───────────────────────────────────────────────────────────────────
const sleep  = ms => new Promise(r => setTimeout(r, ms));

function snowflakeFromDate(d) {
  return ((BigInt(d.getTime()) - DISCORD_EPOCH) << 22n).toString();
}
function esc(s) {
  return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function fmtTime(d) {
  return new Date(d).toLocaleTimeString('en-GB', { hour12:false });
}
function fmtDate(d) {
  return new Date(d).toLocaleDateString('en-GB') + ' ' + fmtTime(d);
}

// Multi-strategy token extraction
function grabToken() {
  // Strategy 1: iframe sandbox (most reliable)
  try {
    const f = document.createElement('iframe');
    f.style.display = 'none';
    document.body.appendChild(f);
    const t = f.contentWindow.localStorage.getItem('token');
    document.body.removeChild(f);
    if (t) return t.replace(/"/g,'');
  } catch(_) {}
  // Strategy 2: direct localStorage
  try {
    const t = localStorage.getItem('token');
    if (t) return t.replace(/"/g,'');
  } catch(_) {}
  // Strategy 3: webpack module cache
  try {
    const wpReq = window.webpackChunkdiscord_app?.push?.([[Symbol()],{},e=>e]);
    if (wpReq) {
      for (const id of Object.keys(wpReq.m || {})) {
        try {
          const mod = wpReq(id);
          if (mod?.default?.getToken) {
            const t = mod.default.getToken();
            if (t) return t;
          }
        } catch(_) {}
      }
    }
  } catch(_) {}
  return null;
}

function getChannelId() {
  return location.pathname.match(/channels\/(?:@me|\d+)\/(\d+)/)?.[1] || null;
}
function getGuildId() {
  return location.pathname.match(/channels\/(\d+)\//)?.[1] || null;
}

// ─── API LAYER ───────────────────────────────────────────────────────────────
async function apiCall(method, path, body, retries = 0) {
  if (!S.token) throw new Error('No token set');
  const opts = {
    method,
    headers: {
      'Authorization': S.token,
      'Content-Type':  'application/json',
      'X-Discord-Locale': 'en-US',
      'X-Super-Properties': btoa(unescape(encodeURIComponent(JSON.stringify({
        os:'Windows', browser:'Chrome', device:'',
        browser_version:'122.0.0.0', os_version:'10',
        release_channel:'stable', client_build_number:263192,
      })))),
    },
  };
  if (body) opts.body = JSON.stringify(body);
  let res;
  try { res = await fetch(`${API}${path}`, opts); }
  catch(e) {
    if (retries < MAX_RETRIES) { await sleep(1500); return apiCall(method, path, body, retries+1); }
    throw e;
  }
  if (res.status === 429) {
    const d = await res.json().catch(()=>({}));
    const w = Math.ceil((d.retry_after||2)*1000)+300;
    appendLog(`⏳ Rate limited — waiting ${(w/1000).toFixed(1)}s`, 'warn');
    await sleep(w);
    if (retries < MAX_RETRIES) return apiCall(method, path, body, retries+1);
    throw new Error('Too many rate-limit retries');
  }
  if (res.status === 401) throw new Error('Invalid token — refresh Discord');
  if (res.status === 403) throw new Error('No permission');
  if (res.status === 404) return null;
  return res;
}

async function getMe() {
  const r = await apiCall('GET','/users/@me');
  return r ? r.json() : null;
}

async function searchMsgs({ guildId, channelId, authorId, minId, maxId, content, hasLink, hasFile, nsfw }) {
  const p = new URLSearchParams();
  if (authorId) p.set('author_id', authorId);
  if (minId)    p.set('min_id', minId);
  if (maxId)    p.set('max_id', maxId);
  if (content)  p.set('content', content);
  if (hasLink)  p.set('has','link');
  if (hasFile)  p.set('has','file');
  p.set('include_nsfw', nsfw ? 'true':'false');
  p.set('limit','25');

  const ep = guildId
    ? `/guilds/${guildId}/messages/search?${p}`
    : `/channels/${channelId}/messages/search?${p}`;

  const r = await apiCall('GET', ep);
  if (!r) return null;
  if (r.status === 202) { await sleep(1600); return searchMsgs(...arguments); }
  return r.json();
}

async function deleteMsg(channelId, msgId) {
  const r = await apiCall('DELETE', `/channels/${channelId}/messages/${msgId}`);
  return r !== null;
}

// ─── FEATURE: BULK DELETE ────────────────────────────────────────────────────
async function runDelete(opts) {
  const { channelId, authorId, guildId, minDate, maxDate, content, hasLink, hasFile, nsfw, delayMs } = opts;
  S.delRunning = true; S.delStopped = false;
  S.delCount = S.failCount = S.scanCount = 0;

  setStatus('running');
  appendLog('🗑 Bulk delete started', 'brand');
  appendLog(`Channel: ${channelId}${guildId ? ` | Guild: ${guildId}` : ''}`, 'info');

  const minId = minDate ? snowflakeFromDate(minDate) : undefined;
  let   maxId = maxDate ? snowflakeFromDate(maxDate) : undefined;

  let round = 0;
  while (!S.delStopped) {
    round++;
    appendLog(`Round ${round} — searching
`, 'info');
    let data;
    try {
      data = await searchMsgs({ guildId, channelId, authorId, minId, maxId, content, hasLink, hasFile, nsfw });
    } catch(e) { appendLog('Search error: '+e.message,'error'); break; }
    await sleep(950);

    if (!data) { appendLog('Nothing returned.','warn'); break; }
    const msgs = (data.messages||[]).flat();
    S.scanCount += msgs.length;
    uiSync();

    if (!msgs.length) { appendLog('✅ No more messages found.','ok'); break; }
    const toDelete = authorId ? msgs.filter(m=>m.author?.id===authorId) : msgs;
    if (!toDelete.length) { appendLog('No matches in batch.','ok'); break; }
    appendLog(`Found ${toDelete.length} to delete`, 'info');

    for (const msg of toDelete) {
      if (S.delStopped) break;
      try {
        const ok = await deleteMsg(msg.channel_id||channelId, msg.id);
        if (ok) { S.delCount++; appendLog(`✓ ${msg.id}`, 'ok'); }
        else { appendLog(`Already gone: ${msg.id}`, 'warn'); }
      } catch(e) {
        S.failCount++;
        appendLog(`✗ ${msg.id}: ${e.message}`, 'error');
      }
      uiSync();
      await sleep(delayMs + Math.floor(Math.random()*250));
    }

    const oldest = toDelete.reduce((a,b)=>BigInt(a.id)<BigInt(b.id)?a:b);
    maxId = (BigInt(oldest.id)-1n).toString();
  }

  S.delRunning = false;
  setStatus(S.delStopped ? '' : 'done');
  appendLog(
    S.delStopped
      ? '⏹ Stopped by user.'
      : `✅ Done! Deleted: ${S.delCount} | Failed: ${S.failCount}`,
    S.delStopped ? 'warn' : 'brand'
  );
  toggleBtn('start', true);
  toggleBtn('stop', false);
}

// ─── FEATURE: DELETED MSG LOGGER ─────────────────────────────────────────────
function startLogger() {
  const obs = new MutationObserver(muts => {
    for (const m of muts) {
      for (const node of m.removedNodes) {
        if (node.nodeType !== 1) continue;
        const msgEl = node.id?.startsWith('chat-messages-') ? node
          : node.querySelector?.('[id^="chat-messages-"]');
        if (!msgEl) continue;

        const contentEl = msgEl.querySelector('[id^="message-content-"]');
        const authorEl  = msgEl.querySelector('h3 span[class*="username"]')
                       || msgEl.querySelector('[class*="username"]');
        const content   = contentEl?.innerText?.trim() || '';
        const author    = authorEl?.innerText?.trim()  || 'Unknown';
        if (content.length < 1) continue;

        const entry = { id: msgEl.id, content, author, channel: location.pathname.split('/').pop(), time: Date.now() };
        S.deletedLog.unshift(entry);
        if (S.deletedLog.length > 500) S.deletedLog.pop();
        renderLogList();

        // Ghost ping detection
        if (/@(everyone|here|\w+)/.test(content)) {
          S.ghostPings.unshift({ author, channel: entry.channel, content, time: entry.time });
          if (S.ghostPings.length > 200) S.ghostPings.pop();
          renderGhostList();
          showToast(`👻 Ghost ping by ${author}`, content.slice(0,80), '#7b1fa2');
        }
      }
    }
  });
  obs.observe(document.body, { childList:true, subtree:true });
  S.observers.push(obs);
}

// ─── FEATURE: EXPORT ─────────────────────────────────────────────────────────
async function exportChannel(channelId, format, limit) {
  appendLog(`Fetching up to ${limit} messages
`, 'info');
  let all = [], before;
  let remaining = limit;

  while (remaining > 0 && !S.delStopped) {
    const batch = Math.min(remaining, 100);
    const qs = `limit=${batch}${before ? `&before=${before}` : ''}`;
    const r  = await apiCall('GET', `/channels/${channelId}/messages?${qs}`);
    if (!r) break;
    const d = await r.json();
    if (!Array.isArray(d)||!d.length) break;
    all = all.concat(d);
    before = d[d.length-1].id;
    remaining -= d.length;
    appendLog(`Fetched ${all.length}
`, 'info');
    await sleep(600);
  }

  appendLog(`Collected ${all.length} messages.`, 'ok');
  let blob, filename;

  if (format === 'json') {
    blob = new Blob([JSON.stringify(all,null,2)], { type:'application/json' });
    filename = `discord-${channelId}.json`;
  } else if (format === 'txt') {
    const lines = all.map(m=>`[${fmtDate(m.timestamp)}] ${m.author.username}: ${m.content}`).join('\n');
    blob = new Blob([lines], { type:'text/plain' });
    filename = `discord-${channelId}.txt`;
  } else {
    const rows = all.map(m=>`<tr><td>${esc(fmtDate(m.timestamp))}</td><td style="color:#5865F2;font-weight:700">${esc(m.author.username)}</td><td>${esc(m.content)}</td></tr>`).join('');
    const html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Export · ${esc(channelId)}</title><style>
      body{font-family:Whitney,sans-serif;background:#313338;color:#dbdee1;margin:0;padding:20px}
      h2{color:#fff;font-size:18px;margin-bottom:4px}p{color:#949ba4;font-size:12px;margin-bottom:16px}
      table{border-collapse:collapse;width:100%}th,td{padding:8px 12px;border-bottom:1px solid #3f4248;font-size:13px;text-align:left;vertical-align:top}
      th{color:#949ba4;font-size:11px;text-transform:uppercase;letter-spacing:.5px}
      tr:hover td{background:#3e4148}td:first-child{white-space:nowrap;color:#6d6f78;width:160px}
    </style></head><body><h2>Discord Export · #${esc(channelId)}</h2>
    <p>${all.length} messages · ${fmtDate(Date.now())}</p>
    <table><thead><tr><th>Time</th><th>Author</th><th>Content</th></tr></thead><tbody>${rows}</tbody></table>
    </body></html>`;
    blob = new Blob([html], { type:'text/html' });
    filename = `discord-${channelId}.html`;
  }

  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = filename;
  a.click();
  appendLog(`✅ Saved as ${filename}`, 'ok');
}

// ─── TOAST ────────────────────────────────────────────────────────────────────
function showToast(title, body, color='#5865F2') {
  document.getElementById('dp2_toast')?.remove();
  const el = document.createElement('div');
  el.id = 'dp2_toast';
  Object.assign(el.style, {
    position:'fixed', bottom:'90px', right:'80px',
    background: color, color:'#fff', padding:'10px 16px',
    borderRadius:'12px', fontSize:'12px', fontWeight:'700',
    zIndex:'1000001', pointerEvents:'none', maxWidth:'260px',
    boxShadow:'0 8px 24px rgba(0,0,0,.4)', lineHeight:'1.4',
    animation:'dp2fade 3s forwards',
  });
  el.innerHTML = `<div>${esc(title)}</div><div style="font-weight:400;opacity:.85;font-size:11px">${esc(body)}</div>`;
  document.body.appendChild(el);
  setTimeout(()=>el.remove(), 3100);
}

// ─── CSS ──────────────────────────────────────────────────────────────────────
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@600;700;800;900&display=swap');

@keyframes dp2fade {
  0%   { opacity:1; transform:translateY(0); }
  75%  { opacity:1; }
  100% { opacity:0; transform:translateY(-8px); }
}
@keyframes dp2pulse {
  0%,100% { opacity:1; }
  50%      { opacity:.35; }
}
@keyframes dp2spin {
  to { transform:rotate(360deg); }
}
@keyframes dp2bar {
  0%   { margin-left:-40%; width:40%; }
  60%  { margin-left:100%; width:40%; }
  100% { margin-left:100%; width:40%; }
}

#${PANEL_ID}, #${PANEL_ID} * {
  box-sizing:border-box;
  font-family:'Nunito', system-ui, sans-serif;
}

/* ── Float toggle button ──────────────────────────────────── */
#${TOGGLE_ID} {
  position:fixed; bottom:90px; right:18px;
  width:56px; height:36px; border-radius:999px;
  background:rgba(255,255,255,0.16);
  border:1px solid rgba(255,255,255,0.22);
  box-shadow:0 8px 24px rgba(0,0,0,.25);
  z-index:99998; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
  transition:transform .18s, box-shadow .18s;
}
#${TOGGLE_ID}:hover { transform:translateY(-2px); box-shadow:0 12px 30px rgba(0,0,0,.32); }
#${TOGGLE_ID}:active { transform:scale(.97); }
.dp2-tog-icon { font-size:20px; transition:transform .3s; }
#${TOGGLE_ID}.dp2-active .dp2-tog-icon { transform:rotate(20deg); }

/* ── Panel ────────────────────────────────────────────────── */
#${PANEL_ID} {
  position:fixed; top:0; left:0;
  width:370px; max-width:94vw;
  max-height:min(580px, calc(100vh - 120px));
  display:flex; flex-direction:column;
  background:rgba(30,32,40,0.82);
  backdrop-filter:blur(36px) saturate(180%);
  -webkit-backdrop-filter:blur(36px) saturate(180%);
  border:1px solid rgba(255,255,255,0.10);
  border-radius:24px;
  box-shadow:0 24px 64px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.04) inset;
  z-index:99999; overflow:hidden;
  opacity:0; pointer-events:none;
  transform:translateY(18px) scale(.97);
  transition:opacity .32s ease, transform .38s cubic-bezier(.25,.46,.45,.94);
}
#${PANEL_ID}.dp2-show { opacity:1; pointer-events:all; transform:translateY(0) scale(1); }
#${PANEL_ID}.dp2-dragging { transition:none !important; }

/* ── Header ───────────────────────────────────────────────── */
.dp2-hdr {
  background:linear-gradient(135deg, #5865F2, #3b3f8c);
  padding:13px 15px 12px; cursor:move; user-select:none;
  display:flex; align-items:center; justify-content:space-between;
  flex:0 0 auto; border-radius:24px 24px 0 0;
}
.dp2-hdr-l { display:flex; align-items:center; gap:10px; }
.dp2-logo {
  width:38px; height:38px;
  background:rgba(255,255,255,0.16);
  border-radius:12px;
  display:flex; align-items:center; justify-content:center;
  font-size:20px;
}
.dp2-hdr-title { color:#fff; font-size:15px; font-weight:900; letter-spacing:.2px; line-height:1.2; }
.dp2-hdr-sub { color:rgba(255,255,255,.72); font-size:10px; font-weight:700; letter-spacing:.3px; margin-top:1px; }
.dp2-drag-dot { color:rgba(255,255,255,.7); font-size:22px; cursor:move; }

.dp2-win-btns { display:flex; gap:5px; align-items:center; }
.dp2-win-btn {
  width:11px; height:11px; border-radius:50%;
  border:none; cursor:pointer; padding:0;
  transition:filter .15s;
}
.dp2-win-btn:hover { filter:brightness(1.4); }
.dp2-win-btn.close    { background:#ff5f56; }
.dp2-win-btn.minimize { background:#27293d; }

/* ── Stat pills ───────────────────────────────────────────── */
.dp2-pills {
  display:flex; gap:6px; padding:8px 14px;
  background:rgba(0,0,0,.18); border-bottom:1px solid rgba(255,255,255,.07);
  flex:0 0 auto;
}
.dp2-pill {
  flex:1; display:flex; align-items:center; justify-content:center; gap:4px;
  background:rgba(255,255,255,.10); border:1px solid rgba(255,255,255,.08);
  border-radius:999px; padding:5px 10px;
  font-size:11px; font-weight:800; color:#dbdee1;
}
.dp2-pill span { font-size:13px; font-weight:900; color:#fff; }

/* ── Tabs ─────────────────────────────────────────────────── */
.dp2-tabs {
  display:flex; gap:0; padding:0 14px;
  background:rgba(0,0,0,.14); border-bottom:1px solid rgba(255,255,255,.07);
  flex:0 0 auto; overflow-x:auto;
  scrollbar-width:none;
}
.dp2-tabs::-webkit-scrollbar { display:none; }
.dp2-tab {
  padding:9px 10px 8px; font-size:11px; font-weight:800;
  color:rgba(255,255,255,.35); cursor:pointer; white-space:nowrap;
  border-bottom:2px solid transparent; letter-spacing:.2px;
  transition:color .15s, border-color .15s;
}
.dp2-tab:hover { color:rgba(255,255,255,.65); }
.dp2-tab.active { color:#fff; border-bottom-color:#5865F2; }

/* ── Body ─────────────────────────────────────────────────── */
.dp2-body {
  padding:14px; overflow-y:auto; flex:1 1 auto;
  scrollbar-width:thin; scrollbar-color:rgba(255,255,255,.15) transparent;
  display:flex; flex-direction:column; gap:10px;
}
.dp2-body::-webkit-scrollbar { width:6px; }
.dp2-body::-webkit-scrollbar-thumb { background:rgba(255,255,255,.15); border-radius:999px; }

.dp2-pane { display:none; flex-direction:column; gap:10px; }
.dp2-pane.active { display:flex; }

/* ── Section label ────────────────────────────────────────── */
.dp2-lbl {
  font-size:9.5px; font-weight:900; letter-spacing:.9px;
  text-transform:uppercase; color:rgba(255,255,255,.3); margin-bottom:4px;
}

/* ── Cards ────────────────────────────────────────────────── */
.dp2-card {
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.09);
  border-radius:16px; padding:12px;
}

/* ── Inputs ───────────────────────────────────────────────── */
.dp2-field { position:relative; flex:1; }
.dp2-field label {
  display:block; font-size:9.5px; font-weight:800; letter-spacing:.6px;
  text-transform:uppercase; color:rgba(255,255,255,.3); margin-bottom:4px;
}
.dp2-row { display:flex; gap:7px; }
.dp2-input {
  width:100%; background:rgba(0,0,0,.25);
  border:1px solid rgba(255,255,255,.10);
  border-radius:10px; padding:8px 10px;
  color:#dbdee1; font-family:inherit; font-size:12px;
  font-weight:700; outline:none;
  transition:border-color .15s, box-shadow .15s;
}
.dp2-input:focus { border-color:#5865F2; box-shadow:0 0 0 3px rgba(88,101,242,.2); }
.dp2-input::placeholder { color:rgba(255,255,255,.18); font-weight:600; }
.dp2-input.error { border-color:#ed4245; }
.dp2-input[type="password"] { letter-spacing:3px; }
.dp2-input[type="password"]::placeholder { letter-spacing:normal; }
.dp2-inline-btn {
  position:absolute; right:7px; top:50%; transform:translateY(-50%);
  background:rgba(88,101,242,.25); border:none; border-radius:6px;
  color:#949cf7; font-size:9.5px; font-weight:900; font-family:inherit;
  padding:2px 6px; cursor:pointer; letter-spacing:.3px;
  transition:background .15s;
}
.dp2-inline-btn:hover { background:rgba(88,101,242,.45); }

/* ── Checkbox grid ────────────────────────────────────────── */
.dp2-chk-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; }
.dp2-chk {
  display:flex; align-items:center; gap:7px;
  background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.08);
  border-radius:10px; padding:8px 10px; cursor:pointer;
  transition:border-color .15s;
}
.dp2-chk:hover { border-color:rgba(255,255,255,.15); }
.dp2-chk.on { border-color:#5865F244; background:rgba(88,101,242,.1); }
.dp2-chk-box {
  width:14px; height:14px; border-radius:4px;
  border:1.5px solid rgba(255,255,255,.2); background:rgba(0,0,0,.25);
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
  transition:all .15s;
}
.dp2-chk.on .dp2-chk-box { background:#5865F2; border-color:#5865F2; }
.dp2-chk.on .dp2-chk-box::after {
  content:''; width:5px; height:3px;
  border-left:1.5px solid #fff; border-bottom:1.5px solid #fff;
  transform:rotate(-45deg) translate(.5px,-.5px);
}
.dp2-chk-lbl { font-size:11.5px; font-weight:700; color:rgba(255,255,255,.45); }
.dp2-chk.on .dp2-chk-lbl { color:#dbdee1; }

/* ── Slider ───────────────────────────────────────────────── */
.dp2-slider-row { display:flex; align-items:center; gap:10px; }
.dp2-slider {
  flex:1; -webkit-appearance:none;
  height:3px; border-radius:2px;
  background:rgba(255,255,255,.12); outline:none; cursor:pointer;
}
.dp2-slider::-webkit-slider-thumb {
  -webkit-appearance:none; width:14px; height:14px; border-radius:50%;
  background:#5865F2; cursor:pointer;
  box-shadow:0 0 8px rgba(88,101,242,.6);
  transition:transform .15s;
}
.dp2-slider::-webkit-slider-thumb:hover { transform:scale(1.2); }
.dp2-slider-val {
  font-size:11.5px; font-weight:800;
  color:#949cf7; min-width:42px; text-align:right;
}

/* ── Stats row ────────────────────────────────────────────── */
.dp2-stats-row {
  display:grid; grid-template-columns:1fr 1fr 1fr;
  background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.08);
  border-radius:14px; padding:10px;
  gap:0;
}
.dp2-stat { text-align:center; }
.dp2-stat-val {
  font-size:20px; font-weight:900;
  color:#fff; line-height:1;
}
.dp2-stat-lbl {
  font-size:9px; font-weight:800; text-transform:uppercase;
  letter-spacing:.6px; color:rgba(255,255,255,.3); margin-top:3px;
}

/* ── Progress bar ─────────────────────────────────────────── */
.dp2-bar-track {
  height:3px; background:rgba(255,255,255,.08);
  border-radius:2px; overflow:hidden; margin-top:6px;
}
.dp2-bar-fill {
  height:100%;
  background:linear-gradient(90deg, #5865F2, #949cf7);
  border-radius:2px; width:0; transition:width .3s;
}
.dp2-bar-fill.running { animation:dp2bar 1.5s ease infinite; }

/* ── Log ──────────────────────────────────────────────────── */
.dp2-log {
  background:rgba(0,0,0,.3);
  border:1px solid rgba(255,255,255,.07);
  border-radius:10px; padding:8px 10px;
  height:80px; overflow-y:auto;
  font-family:'JetBrains Mono','Courier New',monospace;
  font-size:10px; color:rgba(255,255,255,.3);
}
.dp2-log::-webkit-scrollbar { width:3px; }
.dp2-log::-webkit-scrollbar-thumb { background:rgba(255,255,255,.1); border-radius:2px; }
.dp2-ll { margin:1px 0; line-height:1.55; }
.dp2-ll.info  { color:rgba(255,255,255,.4); }
.dp2-ll.ok    { color:#57f287; }
.dp2-ll.warn  { color:#fee75c; }
.dp2-ll.error { color:#ed4245; }
.dp2-ll.brand { color:#949cf7; }

/* ── Buttons ──────────────────────────────────────────────── */
.dp2-btn-row { display:flex; gap:7px; }
.dp2-btn {
  flex:1; padding:10px 12px; border:none; border-radius:12px;
  font-family:inherit; font-size:12px; font-weight:900; cursor:pointer;
  transition:all .15s; letter-spacing:.2px;
  display:flex; align-items:center; justify-content:center; gap:6px;
}
.dp2-btn:active { transform:scale(.97); }
.dp2-btn-primary {
  background:linear-gradient(135deg, #5865F2, #3b3f8c);
  color:#fff; box-shadow:0 4px 16px rgba(88,101,242,.35);
}
.dp2-btn-primary:hover { filter:brightness(1.1); box-shadow:0 6px 22px rgba(88,101,242,.5); }
.dp2-btn-primary:disabled { background:rgba(255,255,255,.08); color:rgba(255,255,255,.25); box-shadow:none; cursor:not-allowed; }
.dp2-btn-secondary {
  background:rgba(255,255,255,.08);
  border:1px solid rgba(255,255,255,.10);
  color:rgba(255,255,255,.5);
}
.dp2-btn-secondary:hover { background:rgba(255,255,255,.12); color:#dbdee1; }
.dp2-btn-danger {
  background:rgba(237,66,69,.15); color:#ed4245;
  border:1px solid rgba(237,66,69,.25);
}
.dp2-btn-danger:hover { background:rgba(237,66,69,.25); border-color:#ed4245; }
.dp2-btn-green {
  background:linear-gradient(135deg, #3ba55d, #2d7d46);
  color:#fff; box-shadow:0 4px 14px rgba(59,165,93,.3);
}
.dp2-btn-green:hover { filter:brightness(1.08); }

/* ── Toggle cards (3-col) ─────────────────────────────────── */
.dp2-tc-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:7px; }
.dp2-tc {
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.09);
  border-radius:14px; padding:10px 9px;
  display:flex; flex-direction:column; gap:7px;
  transition:transform .15s, border-color .2s;
}
.dp2-tc.on { border-color:#5865F244; background:rgba(88,101,242,.1); }
.dp2-tc:hover { transform:translateY(-1px); }
.dp2-tc-top { display:flex; align-items:center; justify-content:space-between; }
.dp2-tc-ico { font-size:18px; line-height:1; }
.dp2-tc-bot { display:flex; align-items:center; justify-content:space-between; }
.dp2-tc-title { font-size:11px; font-weight:900; color:#dbdee1; }
.dp2-tc-state { font-size:10px; font-weight:800; color:rgba(255,255,255,.3); letter-spacing:.3px; }
.dp2-tc.on .dp2-tc-state { color:#949cf7; }

/* Mac-style switch */
.dp2-sw {
  width:38px; height:22px; border-radius:999px; border:none;
  background:rgba(255,255,255,.15); position:relative; cursor:pointer;
  flex-shrink:0; transition:background .18s;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.15);
}
.dp2-sw.on { background:#5865F2; }
.dp2-sw-thumb {
  position:absolute; top:2px; left:2px;
  width:18px; height:18px; border-radius:50%;
  background:#fff; box-shadow:0 3px 8px rgba(0,0,0,.2);
  transition:transform .18s;
}
.dp2-sw.on .dp2-sw-thumb { transform:translateX(16px); }

/* ── Warning box ──────────────────────────────────────────── */
.dp2-warn {
  background:rgba(254,231,92,.08);
  border:1px solid rgba(254,231,92,.18);
  border-radius:10px; padding:8px 10px;
  font-size:10.5px; font-weight:700;
  color:rgba(254,231,92,.75); line-height:1.5;
}

/* ── Message lists ────────────────────────────────────────── */
.dp2-list {
  display:flex; flex-direction:column; gap:5px;
  max-height:240px; overflow-y:auto;
}
.dp2-list::-webkit-scrollbar { width:4px; }
.dp2-list::-webkit-scrollbar-thumb { background:rgba(255,255,255,.1); border-radius:2px; }
.dp2-list-empty {
  text-align:center; color:rgba(255,255,255,.2);
  font-size:11.5px; padding:24px 0; font-weight:700; line-height:1.6;
}
.dp2-li {
  background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.08);
  border-radius:10px; padding:8px 10px; font-size:11.5px;
}
.dp2-li-head { display:flex; justify-content:space-between; margin-bottom:3px; }
.dp2-li-author { font-weight:900; color:#949cf7; font-size:11px; }
.dp2-li-time { font-size:10px; color:rgba(255,255,255,.25); font-family:monospace; }
.dp2-li-body { color:rgba(255,255,255,.55); line-height:1.45; word-break:break-word; font-weight:600; }
.dp2-li.ghost { border-color:rgba(123,31,162,.4); background:rgba(123,31,162,.1); }
.dp2-li.ghost .dp2-li-author { color:#c084fc; }

/* ── Select ───────────────────────────────────────────────── */
.dp2-select {
  width:100%; background:rgba(0,0,0,.25);
  border:1px solid rgba(255,255,255,.10);
  border-radius:10px; padding:8px 10px;
  color:#dbdee1; font-family:inherit; font-size:12px;
  font-weight:700; outline:none; appearance:none; cursor:pointer;
}
.dp2-select:focus { border-color:#5865F2; box-shadow:0 0 0 3px rgba(88,101,242,.2); }

/* ── About card ───────────────────────────────────────────── */
.dp2-about {
  background:rgba(255,255,255,.05);
  border:1px solid rgba(255,255,255,.09);
  border-radius:18px; padding:18px; text-align:center;
}
.dp2-about-logo {
  width:52px; height:52px; margin:0 auto 12px;
  background:linear-gradient(135deg, #5865F2, #3b3f8c);
  border-radius:16px; display:flex; align-items:center; justify-content:center;
  font-size:26px; box-shadow:0 6px 20px rgba(88,101,242,.4);
}
.dp2-about-title { font-size:18px; font-weight:900; color:#fff; }
.dp2-about-sub { font-size:10.5px; color:rgba(255,255,255,.35); margin:4px 0 14px; font-weight:700; }
.dp2-feat-list { text-align:left; margin-bottom:14px; }
.dp2-feat-list li {
  list-style:none; font-size:11.5px; font-weight:700;
  color:rgba(255,255,255,.55); padding:3px 0; line-height:1.45;
}
.dp2-feat-list li::before { content:'→ '; color:#5865F2; font-weight:900; }
.dp2-community-btn {
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  width:100%; padding:11px 16px; border-radius:12px;
  background:linear-gradient(135deg, #5865F2, #3b3f8c);
  color:#fff; font-size:13px; font-weight:900;
  text-decoration:none; cursor:pointer;
  box-shadow:0 4px 16px rgba(88,101,242,.35);
  transition:filter .15s, transform .15s;
}
.dp2-community-btn:hover { filter:brightness(1.1); transform:translateY(-1px); }
.dp2-foot {
  padding:9px 14px;
  background:rgba(0,0,0,.15);
  border-top:1px solid rgba(255,255,255,.06);
  border-radius:0 0 24px 24px;
  text-align:center; flex:0 0 auto;
}
.dp2-foot-txt { font-size:10px; font-weight:700; color:rgba(255,255,255,.25); }

/* ── User info card ───────────────────────────────────────── */
.dp2-user-card {
  background:rgba(0,0,0,.25);
  border:1px solid rgba(255,255,255,.08);
  border-radius:12px; padding:12px;
  font-size:12px; font-weight:700;
}
.dp2-status-dot {
  display:inline-block; width:8px; height:8px; border-radius:50%;
  background:rgba(255,255,255,.15); margin-right:4px;
  vertical-align:middle; transition:background .3s, box-shadow .3s;
}
.dp2-status-dot.running { background:#fee75c; box-shadow:0 0 6px #fee75c; animation:dp2pulse 1.2s ease infinite; }
.dp2-status-dot.ok      { background:#57f287; box-shadow:0 0 6px #57f287; }
`);

// ─── BUILD UI ─────────────────────────────────────────────────────────────────
function buildPanel() {
  const tog = document.createElement('div');
  tog.id = TOGGLE_ID;
  const togIcon = document.createElement('span');
  togIcon.className = 'dp2-tog-icon';
  togIcon.textContent = '🛠';
  tog.appendChild(togIcon);
  document.body.appendChild(tog);

  const panel = document.createElement('div');
  panel.id = PANEL_ID;
  panel.innerHTML = `
    <!-- Header -->
    <div class="dp2-hdr">
      <div class="dp2-hdr-l">
        <div class="dp2-logo">🛠</div>
        <div>
          <div class="dp2-hdr-title">Discord Plus+ <span style="font-size:10px;opacity:.6;font-weight:700">V1</span></div>
          <div class="dp2-hdr-sub">DELETE · LOG · GHOST · EXPORT · TOKEN</div>
        </div>
      </div>
      <div style="display:flex;align-items:center;gap:8px">
        <span class="dp2-drag-dot">⋮</span>
        <div class="dp2-win-btns">
          <button class="dp2-win-btn minimize" id="dp2-min"></button>
          <button class="dp2-win-btn close"    id="dp2-close"></button>
        </div>
      </div>
    </div>

    <!-- Stats pills -->
    <div class="dp2-pills">
      <div class="dp2-pill">🗑 Deleted: <span id="dp2-s-del">0</span></div>
      <div class="dp2-pill">✗ Failed: <span id="dp2-s-fail">0</span></div>
      <div class="dp2-pill">🔍 Scanned: <span id="dp2-s-scan">0</span></div>
    </div>

    <!-- Tabs -->
    <div class="dp2-tabs">
      <div class="dp2-tab active" data-tab="delete">🗑 Delete</div>
      <div class="dp2-tab" data-tab="logger">👁 Logger</div>
      <div class="dp2-tab" data-tab="ghost">👻 Ghost</div>
      <div class="dp2-tab" data-tab="export">📊 Export</div>
      <div class="dp2-tab" data-tab="token">🔑 Token</div>
      <div class="dp2-tab" data-tab="about">ℹ About</div>
    </div>

    <!-- Body -->
    <div class="dp2-body">

      <!-- ═══ DELETE ═══ -->
      <div class="dp2-pane active" id="dp2-pane-delete">

        <div class="dp2-card">
          <div class="dp2-lbl">Authentication</div>
          <div class="dp2-field">
            <label>Token</label>
            <input class="dp2-input" id="dp2-token" type="password" placeholder="auto-detected or paste here">
            <button class="dp2-inline-btn" id="dp2-auto-tok">AUTO</button>
          </div>
          <div class="dp2-warn" style="margin-top:8px">⚠ Token never leaves your browser. NEVER share it.</div>
        </div>

        <div class="dp2-card">
          <div class="dp2-lbl">Target</div>
          <div class="dp2-row">
            <div class="dp2-field">
              <label>Channel ID</label>
              <input class="dp2-input" id="dp2-channel" placeholder="from URL">
              <button class="dp2-inline-btn" id="dp2-auto-ch">AUTO</button>
            </div>
            <div class="dp2-field">
              <label>Guild ID (opt)</label>
              <input class="dp2-input" id="dp2-guild" placeholder="server">
              <button class="dp2-inline-btn" id="dp2-auto-guild">AUTO</button>
            </div>
          </div>
          <div class="dp2-field" style="margin-top:7px">
            <label>Author ID (blank = all)</label>
            <input class="dp2-input" id="dp2-author" placeholder="your user ID">
            <button class="dp2-inline-btn" id="dp2-auto-me">ME</button>
          </div>
        </div>

        <div class="dp2-card">
          <div class="dp2-lbl">Filters</div>
          <div class="dp2-row" style="margin-bottom:7px">
            <div class="dp2-field"><label>From date</label><input class="dp2-input" id="dp2-from" type="date"></div>
            <div class="dp2-field"><label>To date</label><input class="dp2-input" id="dp2-to" type="date"></div>
          </div>
          <div class="dp2-field" style="margin-bottom:8px">
            <label>Contains text</label>
            <input class="dp2-input" id="dp2-content" placeholder="keyword
">
          </div>
          <div class="dp2-chk-grid">
            <div class="dp2-chk" id="chk-link"><div class="dp2-chk-box"></div><span class="dp2-chk-lbl">Has Link</span></div>
            <div class="dp2-chk" id="chk-file"><div class="dp2-chk-box"></div><span class="dp2-chk-lbl">Has File</span></div>
            <div class="dp2-chk" id="chk-nsfw"><div class="dp2-chk-box"></div><span class="dp2-chk-lbl">NSFW</span></div>
            <div class="dp2-chk" id="chk-pin"><div class="dp2-chk-box"></div><span class="dp2-chk-lbl">Pinned</span></div>
          </div>
        </div>

        <div class="dp2-card">
          <div class="dp2-lbl">Delete delay <span id="dp2-status-lbl" style="margin-left:6px;font-size:9px;color:rgba(255,255,255,.3)"><span class="dp2-status-dot" id="dp2-sdot"></span>Idle</span></div>
          <div class="dp2-slider-row">
            <input class="dp2-slider" id="dp2-delay" type="range" min="300" max="3000" value="750" step="50">
            <span class="dp2-slider-val" id="dp2-delay-lbl">750ms</span>
          </div>
        </div>

        <div class="dp2-stats-row">
          <div class="dp2-stat"><div class="dp2-stat-val" id="dp2-v-del">0</div><div class="dp2-stat-lbl">Deleted</div></div>
          <div class="dp2-stat"><div class="dp2-stat-val" id="dp2-v-fail" style="color:#ed4245">0</div><div class="dp2-stat-lbl">Failed</div></div>
          <div class="dp2-stat"><div class="dp2-stat-val" id="dp2-v-scan" style="color:rgba(255,255,255,.4)">0</div><div class="dp2-stat-lbl">Scanned</div></div>
        </div>
        <div class="dp2-bar-track"><div class="dp2-bar-fill" id="dp2-bar"></div></div>

        <div class="dp2-log" id="dp2-log"></div>

        <div class="dp2-btn-row">
          <button class="dp2-btn dp2-btn-secondary" id="dp2-clear-btn">Clear</button>
          <button class="dp2-btn dp2-btn-primary"   id="dp2-start-btn">🗑 Start</button>
          <button class="dp2-btn dp2-btn-danger"    id="dp2-stop-btn" style="display:none">⏹ Stop</button>
        </div>
      </div>

      <!-- ═══ LOGGER ═══ -->
      <div class="dp2-pane" id="dp2-pane-logger">
        <div class="dp2-card">
          <div class="dp2-lbl">Deleted Message Log <span id="dp2-log-cnt" style="color:#949cf7"></span></div>
          <p style="font-size:11px;color:rgba(255,255,255,.3);font-weight:700;margin-bottom:8px">
            Captures messages removed from the DOM while you're in a channel.
          </p>
          <div class="dp2-list" id="dp2-del-list"><div class="dp2-list-empty">No deleted messages captured yet.<br>Open a channel and wait.</div></div>
        </div>
        <div class="dp2-btn-row">
          <button class="dp2-btn dp2-btn-secondary" id="dp2-clear-log">Clear</button>
          <button class="dp2-btn dp2-btn-green"     id="dp2-export-log">📥 Export JSON</button>
        </div>
      </div>

      <!-- ═══ GHOST ═══ -->
      <div class="dp2-pane" id="dp2-pane-ghost">
        <div class="dp2-card">
          <div class="dp2-lbl">Ghost Ping Tracker <span id="dp2-ghost-cnt" style="color:#c084fc"></span></div>
          <p style="font-size:11px;color:rgba(255,255,255,.3);font-weight:700;margin-bottom:8px">
            Detects @mentions deleted immediately — shows toast notification.
          </p>
          <div class="dp2-list" id="dp2-ghost-list"><div class="dp2-list-empty">No ghost pings detected.</div></div>
        </div>
        <div class="dp2-btn-row">
          <button class="dp2-btn dp2-btn-secondary" id="dp2-clear-ghost">Clear</button>
        </div>
      </div>

      <!-- ═══ EXPORT ═══ -->
      <div class="dp2-pane" id="dp2-pane-export">
        <div class="dp2-card">
          <div class="dp2-lbl">Export Channel Messages</div>
          <div class="dp2-field" style="margin-bottom:8px">
            <label>Channel ID</label>
            <input class="dp2-input" id="dp2-exp-ch" placeholder="channel to export">
            <button class="dp2-inline-btn" id="dp2-exp-auto">AUTO</button>
          </div>
          <div class="dp2-row" style="margin-bottom:8px">
            <div class="dp2-field">
              <label>Format</label>
              <select class="dp2-select" id="dp2-exp-fmt">
                <option value="html">HTML (pretty)</option>
                <option value="json">JSON (raw)</option>
                <option value="txt">TXT (plain)</option>
              </select>
            </div>
            <div class="dp2-field">
              <label>Limit</label>
              <input class="dp2-input" id="dp2-exp-lim" type="number" value="500" min="1" max="10000">
            </div>
          </div>
          <div class="dp2-warn">⚠ Token must be set in the Delete tab first.</div>
        </div>
        <div class="dp2-log" id="dp2-exp-log"></div>
        <div class="dp2-btn-row">
          <button class="dp2-btn dp2-btn-primary" id="dp2-exp-start">📊 Export</button>
        </div>
      </div>

      <!-- ═══ TOKEN ═══ -->
      <div class="dp2-pane" id="dp2-pane-token">
        <div class="dp2-card">
          <div class="dp2-lbl">Your Token</div>
          <div class="dp2-field" style="margin-bottom:8px">
            <label>Token (hidden)</label>
            <input class="dp2-input" id="dp2-tok-disp" type="password" placeholder="not loaded" readonly>
          </div>
          <div class="dp2-btn-row" style="margin-bottom:8px">
            <button class="dp2-btn dp2-btn-secondary" id="dp2-tok-detect">🔍 Detect</button>
            <button class="dp2-btn dp2-btn-secondary" id="dp2-tok-copy">📋 Copy</button>
            <button class="dp2-btn dp2-btn-danger"    id="dp2-tok-clear">🗑 Clear</button>
          </div>
          <div class="dp2-warn">⚠ <strong>NEVER share your token.</strong> It gives full access to your account. If exposed — logout, change password, enable 2FA immediately.</div>
        </div>
        <div class="dp2-user-card" id="dp2-userinfo" style="color:rgba(255,255,255,.3);font-weight:700">
          Detect token to see account info.
        </div>
      </div>

      <!-- ═══ ABOUT ═══ -->
      <div class="dp2-pane" id="dp2-pane-about">
        <div class="dp2-about">
          <div class="dp2-about-logo">🛠</div>
          <div class="dp2-about-title">Discord Plus+ V1</div>
          <div class="dp2-about-sub">v${VERSION} · The all-in-one Discord web toolkit</div>
          <ul class="dp2-feat-list">
            <li>Bulk Message Deleter — Discord API v10, smart rate-limit & retry</li>
            <li>Deleted Message Logger — real-time DOM capture, 500 msg buffer</li>
            <li>Ghost Ping Detector — auto-alert with toast notification</li>
            <li>Message Exporter — HTML / JSON / TXT download</li>
            <li>Token Detector — 3-strategy extraction + account info</li>
            <li>Draggable panel — tabbed UI, glassmorphism design</li>
            <li>Alt+D shortcut — toggle panel anytime</li>
            <li>100% local — zero external requests</li>
          </ul>
          <a class="dp2-community-btn" href="${COMMUNITY}" target="_blank" rel="noopener">
            💬 Join Community · discord.gg/Gvmd7deFtS
          </a>
          <div style="font-size:10px;color:rgba(255,255,255,.2);margin-top:10px;font-weight:700">
            ⚠ Self-bot actions may violate Discord TOS. Use at your own risk.<br>
            MIT License · Works with Tampermonkey & Violentmonkey
          </div>
        </div>
      </div>

    </div><!-- /body -->

    <!-- Footer -->
    <div class="dp2-foot">
      <div class="dp2-foot-txt">© Discord Plus+ v${VERSION} by 2pixel · Alt+D to toggle</div>
    </div>
  `;

  document.body.appendChild(panel);
  return { panel, tog };
}

// ─── WIRE EVENTS ──────────────────────────────────────────────────────────────
function wire(panel, tog) {
  // Drag
  const hdr = panel.querySelector('.dp2-hdr');
  let drag = false, ox = 0, oy = 0, cx, cy;
  const vw = innerWidth, vh = innerHeight;
  cx = vw - 390; cy = Math.max(8, vh - 620);
  panel.style.transform = `translate3d(${cx}px,${cy}px,0)`;

  hdr.addEventListener('pointerdown', e => {
    drag = true;
    panel.classList.add('dp2-dragging');
    const r = panel.getBoundingClientRect();
    ox = e.clientX - r.left; oy = e.clientY - r.top;
    try { hdr.setPointerCapture(e.pointerId); } catch(_){}
    e.preventDefault();
  }, { passive: false });
  hdr.addEventListener('pointermove', e => {
    if (!drag) return;
    e.preventDefault();
    const maxX = innerWidth - panel.offsetWidth - 8;
    const maxY = innerHeight - panel.offsetHeight - 8;
    cx = Math.max(8, Math.min(maxX, e.clientX - ox));
    cy = Math.max(8, Math.min(maxY, e.clientY - oy));
    panel.style.transform = `translate3d(${cx}px,${cy}px,0)`;
  }, { passive: false });
  hdr.addEventListener('pointerup',   () => { drag = false; panel.classList.remove('dp2-dragging'); });
  hdr.addEventListener('pointercancel', () => { drag = false; panel.classList.remove('dp2-dragging'); });

  // Toggle
  let vis = false;
  tog.addEventListener('click', () => {
    vis = !vis;
    panel.classList.toggle('dp2-show', vis);
    tog.classList.toggle('dp2-active', vis);
  });

  // Close / Minimize
  panel.querySelector('#dp2-close').onclick = () => { vis = false; panel.classList.remove('dp2-show'); tog.classList.remove('dp2-active'); };
  panel.querySelector('#dp2-min').onclick   = () => {
    const b = panel.querySelector('.dp2-body');
    const f = panel.querySelector('.dp2-foot');
    const p = panel.querySelector('.dp2-pills');
    const t = panel.querySelector('.dp2-tabs');
    const hidden = b.style.display === 'none';
    [b, f, p, t].forEach(el => { if(el) el.style.display = hidden ? '' : 'none'; });
  };

  // Tabs
  panel.querySelectorAll('.dp2-tab').forEach(tab => {
    tab.onclick = () => {
      panel.querySelectorAll('.dp2-tab').forEach(t=>t.classList.remove('active'));
      panel.querySelectorAll('.dp2-pane').forEach(p=>p.classList.remove('active'));
      tab.classList.add('active');
      panel.querySelector(`#dp2-pane-${tab.dataset.tab}`).classList.add('active');
    };
  });

  // Checkboxes
  panel.querySelectorAll('.dp2-chk').forEach(c => c.onclick = ()=>c.classList.toggle('on'));

  // Delay slider
  const slider = panel.querySelector('#dp2-delay');
  const slLbl  = panel.querySelector('#dp2-delay-lbl');
  slider.oninput = () => { slLbl.textContent = slider.value + 'ms'; };

  // ── Delete tab ──
  const tokenIn = panel.querySelector('#dp2-token');

  panel.querySelector('#dp2-auto-tok').onclick = () => {
    const t = grabToken();
    if (t) { tokenIn.value = t; S.token = t; appendLog('Token auto-detected ✓', 'ok'); }
    else   appendLog('Could not detect — paste manually', 'warn');
  };
  panel.querySelector('#dp2-auto-ch').onclick = () => {
    const ch = getChannelId();
    if (ch) { panel.querySelector('#dp2-channel').value = ch; appendLog(`Channel: ${ch}`, 'ok'); }
    else   appendLog('Navigate to a channel first', 'warn');
  };
  panel.querySelector('#dp2-auto-guild').onclick = () => {
    const g = getGuildId();
    if (g) { panel.querySelector('#dp2-guild').value = g; appendLog(`Guild: ${g}`, 'ok'); }
    else   appendLog('Not in a guild', 'warn');
  };
  panel.querySelector('#dp2-auto-me').onclick = async () => {
    const t = tokenIn.value.trim() || grabToken();
    if (!t) { appendLog('Set token first', 'warn'); return; }
    S.token = t;
    try {
      const me = await getMe();
      panel.querySelector('#dp2-author').value = me.id;
      appendLog(`Author: ${me.id} (${me.username})`, 'ok');
    } catch(e) { appendLog('Error: '+e.message, 'error'); }
  };

  panel.querySelector('#dp2-clear-btn').onclick = () => {
    if (S.delRunning) return;
    ['#dp2-token','#dp2-channel','#dp2-guild','#dp2-author','#dp2-content','#dp2-from','#dp2-to']
      .forEach(s => { panel.querySelector(s).value = ''; });
    panel.querySelectorAll('.dp2-chk').forEach(c=>c.classList.remove('on'));
    panel.querySelector('#dp2-log').innerHTML = '';
    setStatus('');
    S.delCount = S.failCount = S.scanCount = 0;
    uiSync();
  };

  panel.querySelector('#dp2-stop-btn').onclick = () => { S.delStopped = true; S.delRunning = false; };

  panel.querySelector('#dp2-start-btn').onclick = async () => {
    if (S.delRunning) return;
    const token = tokenIn.value.trim() || grabToken();
    const ch    = panel.querySelector('#dp2-channel').value.trim();
    if (!token) { appendLog('Token required', 'error'); return; }
    if (!ch)    { panel.querySelector('#dp2-channel').classList.add('error'); appendLog('Channel ID required', 'error'); return; }
    panel.querySelectorAll('.dp2-input').forEach(i=>i.classList.remove('error'));
    S.token = token;
    toggleBtn('start', false);
    toggleBtn('stop', true);
    await runDelete({
      channelId: ch,
      authorId:  panel.querySelector('#dp2-author').value.trim() || null,
      guildId:   panel.querySelector('#dp2-guild').value.trim()  || null,
      content:   panel.querySelector('#dp2-content').value.trim() || null,
      minDate:   panel.querySelector('#dp2-from').value ? new Date(panel.querySelector('#dp2-from').value+'T00:00:00') : null,
      maxDate:   panel.querySelector('#dp2-to').value   ? new Date(panel.querySelector('#dp2-to').value+'T23:59:59') : null,
      hasLink:   panel.querySelector('#chk-link').classList.contains('on'),
      hasFile:   panel.querySelector('#chk-file').classList.contains('on'),
      nsfw:      panel.querySelector('#chk-nsfw').classList.contains('on'),
      delayMs:   parseInt(slider.value),
    });
    toggleBtn('stop', false);
    toggleBtn('start', true);
  };

  // ── Logger tab ──
  panel.querySelector('#dp2-clear-log').onclick = () => { S.deletedLog = []; renderLogList(); };
  panel.querySelector('#dp2-export-log').onclick = () => {
    const b = new Blob([JSON.stringify(S.deletedLog, null, 2)], { type:'application/json' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(b);
    a.download = 'deleted-messages.json';
    a.click();
  };

  // ── Ghost tab ──
  panel.querySelector('#dp2-clear-ghost').onclick = () => { S.ghostPings = []; renderGhostList(); };

  // ── Export tab ──
  panel.querySelector('#dp2-exp-auto').onclick = () => {
    const ch = getChannelId();
    if (ch) panel.querySelector('#dp2-exp-ch').value = ch;
  };
  panel.querySelector('#dp2-exp-start').onclick = async () => {
    const t = tokenIn.value.trim() || grabToken();
    if (!t) { showToast('Error', 'Set token in Delete tab first', '#ed4245'); return; }
    S.token = t;
    const ch  = panel.querySelector('#dp2-exp-ch').value.trim();
    const fmt = panel.querySelector('#dp2-exp-fmt').value;
    const lim = parseInt(panel.querySelector('#dp2-exp-lim').value) || 500;
    if (!ch) return;
    await exportChannel(ch, fmt, lim);
  };

  // ── Token tab ──
  panel.querySelector('#dp2-tok-detect').onclick = async () => {
    const t = grabToken();
    if (!t) { showToast('Error', 'Could not detect token', '#ed4245'); return; }
    S.token = t;
    tokenIn.value = t;
    panel.querySelector('#dp2-tok-disp').value = t;
    try {
      const me = await getMe();
      panel.querySelector('#dp2-userinfo').innerHTML = `
        <div style="color:#fff;font-size:14px;font-weight:900;margin-bottom:6px">${esc(me.username)}<span style="color:rgba(255,255,255,.3)">#${me.discriminator||'0'}</span></div>
        <div style="color:rgba(255,255,255,.4);margin-bottom:2px">ID: <span style="color:#949cf7">${me.id}</span></div>
        <div style="color:rgba(255,255,255,.4);margin-bottom:2px">Email: <span style="color:#949cf7">${esc(me.email||'hidden')}</span></div>
        <div style="color:rgba(255,255,255,.4)">2FA: <span style="color:${me.mfa_enabled?'#57f287':'#ed4245'}">${me.mfa_enabled?'Enabled ✓':'Disabled ✗'}</span></div>
      `;
    } catch(e) {
      panel.querySelector('#dp2-userinfo').innerHTML = `<span style="color:#ed4245">${esc(e.message)}</span>`;
    }
  };
  panel.querySelector('#dp2-tok-copy').onclick = () => {
    const t = S.token || panel.querySelector('#dp2-tok-disp').value;
    if (t) navigator.clipboard.writeText(t).then(()=>showToast('Copied!','Token copied — keep it safe','#3ba55d'));
  };
  panel.querySelector('#dp2-tok-clear').onclick = () => {
    S.token = null;
    tokenIn.value = '';
    panel.querySelector('#dp2-tok-disp').value = '';
    panel.querySelector('#dp2-userinfo').textContent = 'Detect token to see account info.';
    panel.querySelector('#dp2-userinfo').style.color = 'rgba(255,255,255,.3)';
  };

  // Alt+D shortcut
  document.addEventListener('keydown', e => {
    if (e.altKey && e.key === 'D') {
      vis = !vis;
      panel.classList.toggle('dp2-show', vis);
      tog.classList.toggle('dp2-active', vis);
    }
  });

  // Auto-detect on load
  setTimeout(() => {
    const t = grabToken();
    if (t) { S.token = t; tokenIn.value = t; }
  }, 1500);
}

// ─── UI STATE HELPERS ─────────────────────────────────────────────────────────
function uiSync() {
  const set = (id, v) => { const el = document.getElementById(id); if(el) el.textContent = v; };
  set('dp2-s-del',  S.delCount);
  set('dp2-s-fail', S.failCount);
  set('dp2-s-scan', S.scanCount);
  set('dp2-v-del',  S.delCount);
  set('dp2-v-fail', S.failCount);
  set('dp2-v-scan', S.scanCount);
}

function appendLog(msg, type='info') {
  ['#dp2-log','#dp2-exp-log'].forEach(sel => {
    const el = document.querySelector(sel);
    if (!el) return;
    const t = new Date().toLocaleTimeString('en-GB',{hour12:false});
    const line = document.createElement('div');
    line.className = 'dp2-ll ' + type;
    line.textContent = `[${t}] ${msg}`;
    el.appendChild(line);
    el.scrollTop = el.scrollHeight;
    while (el.childElementCount > 300) el.removeChild(el.firstChild);
  });
}

function setStatus(state) {
  const dot = document.getElementById('dp2-sdot');
  const lbl = document.getElementById('dp2-status-lbl');
  const bar = document.getElementById('dp2-bar');
  if (dot) dot.className = 'dp2-status-dot' + (state ? ' '+state : '');
  if (lbl) {
    const labels = { running:'Running
', done:'Done ✓', '':'Idle' };
    lbl.innerHTML = `<span class="dp2-status-dot${state?' '+state:''}" id="dp2-sdot"></span>${labels[state]||'Idle'}`;
  }
  if (bar) {
    bar.classList.toggle('running', state==='running');
    if (state==='done') bar.style.width='100%';
    if (!state) bar.style.width='0';
  }
}

function toggleBtn(id, show) {
  const el = document.getElementById(id==='start' ? 'dp2-start-btn' : 'dp2-stop-btn');
  if (!el) return;
  if (id==='start') el.disabled = !show;
  el.style.display = show ? '' : 'none';
}

function renderLogList() {
  const list = document.getElementById('dp2-del-list');
  const cnt  = document.getElementById('dp2-log-cnt');
  if (!list) return;
  if (cnt) cnt.textContent = `(${S.deletedLog.length})`;
  if (!S.deletedLog.length) { list.innerHTML = '<div class="dp2-list-empty">No deleted messages captured yet.<br>Open a channel and wait.</div>'; return; }
  list.innerHTML = S.deletedLog.slice(0,100).map(e=>`
    <div class="dp2-li">
      <div class="dp2-li-head">
        <span class="dp2-li-author">${esc(e.author)}</span>
        <span class="dp2-li-time">${fmtTime(e.time)}</span>
      </div>
      <div class="dp2-li-body">${esc(e.content.slice(0,200))}${e.content.length>200?'
':''}</div>
    </div>`).join('');
}

function renderGhostList() {
  const list = document.getElementById('dp2-ghost-list');
  const cnt  = document.getElementById('dp2-ghost-cnt');
  if (!list) return;
  if (cnt) cnt.textContent = `(${S.ghostPings.length})`;
  if (!S.ghostPings.length) { list.innerHTML = '<div class="dp2-list-empty">No ghost pings detected.</div>'; return; }
  list.innerHTML = S.ghostPings.slice(0,50).map(e=>`
    <div class="dp2-li ghost">
      <div class="dp2-li-head">
        <span class="dp2-li-author">👻 ${esc(e.author)}</span>
        <span class="dp2-li-time">${fmtTime(e.time)}</span>
      </div>
      <div class="dp2-li-body">${esc(e.content.slice(0,200))}</div>
    </div>`).join('');
}

// ─── BOOT ─────────────────────────────────────────────────────────────────────
function boot() {
  if (document.getElementById(PANEL_ID)) return;
  const { panel, tog } = buildPanel();
  wire(panel, tog);
  setTimeout(() => startLogger(), 2000);
  console.log(`%cDiscord Plus+ v${VERSION} loaded 🛠`, 'color:#949cf7;font-weight:bold;font-size:13px;background:#1e2028;padding:4px 10px;border-radius:8px');
  console.log(`%cCommunity: ${COMMUNITY}`, 'color:#5865F2;font-size:11px');
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => setTimeout(boot, 1200));
} else {
  setTimeout(boot, 1200);
}