BR Admin Panel Pro v6

Панель куратора BR — оптимизировано: единый таймер-менеджер, делегирование событий, без утечек

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         BR Admin Panel Pro v6
// @namespace    https://vk.ru/club237051164
// @version      6.2.0
// @description  Панель куратора BR — оптимизировано: единый таймер-менеджер, делегирование событий, без утечек
// @author       Akzholch1k | vk.ru/brunoverona
// @match        https://forum.blackrussia.online/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      api.telegram.org
// @connect      api.imgbb.com
// @connect      *
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  /* ══════════════════════════════════════════════════════════
     ПОЧЕМУ ЭТА ВЕРСИЯ БЫСТРЕЕ v5 — коротко, для будущих правок:

     1. TimerRegistry — единственное место, где живут setInterval/
        setTimeout. Раньше startTracking() плодил 3 независимых
        таймера при каждом вызове без сохранения ID; если функция
        вызывалась дважды (а на XenForo с pushState-навигацией это
        случается), таймеры удваивались молча. Теперь re-init убивает
        старые перед стартом новых — физически невозможно продублировать.

     2. Одна точка правды для "какая страница сейчас" — PAGE_STATE,
        обновляется по событию popstate/pushState, а не polling'ом
        location.href каждые 2 секунды в отдельном интервале.

     3. Делегирование кликов на уровне панели (один слушатель на
        #brv6-panel) вместо querySelectorAll(...).forEach(навесить
        обработчик) при каждой перерисовке таба. Раньше каждый вызов
        drawTpls() на N шаблонов вешал минимум 3N новых замыканий,
        и это не считая слушателей от предыдущего рендера — они не
        снимались явно, просто DOM-узлы с этими обработчиками
        выкидывались сборщиком мусора (это не утечка памяти в строгом
        смысле, но это N лишних функций-аллокаций на каждый клик
        по табу шаблонов, и это то, что ощущается как лаг при
        20-30 шаблонах на телефоне).

     4. Змейка: таймер — свойство объекта игры, а не переменная
        модуля. Explicit .destroy() при уходе со страницы игры,
        вызывается из ОДНОГО места (page router), а не через
        addEventListener(once) навешанный на все табы при каждом
        запуске игры.

     5. detectForumUser() кешируется на PAGE_STATE.url — вызывается
        реально только когда URL изменился, не на каждый /ping.
  ══════════════════════════════════════════════════════════ */

  /* ──────────────────────────────────────────────────────────
     TimerRegistry
     Единственное место в скрипте, которое трогает setInterval/
     setTimeout напрямую. Каждый таймер именован; повторная
     регистрация того же имени сначала снимает старый. init() —
     единственная точка входа, вызывается ровно один раз за
     жизнь скрипта на странице.
  ────────────────────────────────────────────────────────── */
  const Timers = (() => {
    const handles = new Map(); // name -> interval/timeout id
    const kinds   = new Map(); // name -> 'interval' | 'timeout'

    function clear(name) {
      if (!handles.has(name)) return;
      const id = handles.get(name);
      (kinds.get(name) === 'interval' ? clearInterval : clearTimeout)(id);
      handles.delete(name);
      kinds.delete(name);
    }

    function every(name, ms, fn) {
      clear(name);
      handles.set(name, setInterval(fn, ms));
      kinds.set(name, 'interval');
    }

    function after(name, ms, fn) {
      clear(name);
      handles.set(name, setTimeout(() => { handles.delete(name); kinds.delete(name); fn(); }, ms));
      kinds.set(name, 'timeout');
    }

    function clearAll() {
      for (const name of [...handles.keys()]) clear(name);
    }

    return { every, after, clear, clearAll };
  })();

  /* ══════════════════════════════════════════════════════════
     КОНФИГ
  ══════════════════════════════════════════════════════════ */
  // ─── НАСТРОЙКА ───
  // TG_TOKEN/TG_CID используются только для прямых уведомлений из скрипта.
  // Чат поддержки идёт через SERVER — туда же поставьте тот же SECRET
  // (переменная окружения SECRET в Railway).
  const TG_TOKEN  = '8814296690:AAEdmT5mi6gQFzvXaO450nNOFEZ6tlJTBAk';          // токен от @BotFather
  const TG_CID    = '6564808901';             // ваш numeric id (@userinfobot)
  const SERVER    = 'https://web-web-production-55258.up.railway.app';  // URL после деплоя
  const SECRET    = 'AdminForum';             // == SECRET на сервере
  const CR_LINK   = 'cr-mp://connect/blackrussia.online:7777';

  const PING_INTERVAL_MS   = 30000;
  const POLL_INTERVAL_MS   = 6000;   // было 5000 в v5 — на телефоне 6с не ощущается медленнее, а нагрузка на бесплатный Railway ниже
  const PRESENCE_DEBOUNCE_MS = 800;  // см. PageWatcher ниже

  const PREFIXES = [
    { id:0,  name:'— Без префикса',      color:'#666' },
    { id:2,  name:'📌 Закреплено',        color:'#f0c040' },
    { id:4,  name:'❌ Отказано',          color:'#ff4f4f' },
    { id:5,  name:'⏳ На рассмотрении',   color:'#ec7c26' },
    { id:6,  name:'✅ Решено',            color:'#2ed47a' },
    { id:7,  name:'🔒 Закрыто',          color:'#888' },
    { id:8,  name:'✔️ Одобрено',         color:'#4caf50' },
    { id:9,  name:'👁 Рассмотрено',      color:'#2196f3' },
    { id:10, name:'📢 Команде проекта',   color:'#9c27b0' },
    { id:12, name:'🏛 ГА',               color:'#ff9800' },
    { id:13, name:'🔧 Тех. специалисту', color:'#00bcd4' },
    { id:14, name:'⌛ Ожидание',         color:'#607d8b' },
  ];
  const prefixById = new Map(PREFIXES.map(p => [p.id, p]));

  const DEFAULT_TEMPLATES = [
    { id:1, name:'На рассмотрении', prefix:5, autopin:true,
      content:'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша заявка принята и находится на рассмотрении.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n\n[B][CENTER][FONT=times new roman][COLOR=#ec7c26][ICODE]На рассмотрении.[/ICODE][/COLOR][/CENTER][/FONT][/B]' },
    { id:2, name:'Одобрено', prefix:8, autopin:true,
      content:'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n[B][CENTER][FONT=times new roman][COLOR=#00FF00][ICODE]Ваша заявка одобрена. Поздравляем![/ICODE][/COLOR][/CENTER][/FONT][/B]\n\n[B][CENTER][FONT=times new roman][COLOR=#00FF00][ICODE]Одобрено.[/ICODE][/COLOR][/CENTER][/FONT][/B]' },
    { id:3, name:'Отказано', prefix:4, autopin:true,
      content:'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n[B][CENTER][FONT=times new roman][COLOR=#FF4444][ICODE]К сожалению, ваша заявка отклонена.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n\n[B][CENTER][FONT=times new roman][COLOR=#FF4444][ICODE]Отказано.[/ICODE][/COLOR][/CENTER][/FONT][/B]' },
    { id:4, name:'Закрыто', prefix:7, autopin:false,
      content:'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n[B][CENTER][FONT=times new roman][COLOR=#aaaaaa][ICODE]Тема закрыта куратором.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n\n[B][CENTER][FONT=times new roman][COLOR=#aaaaaa][ICODE]Закрыто.[/ICODE][/COLOR][/CENTER][/FONT][/B]' },
    { id:5, name:'Нет доказательств', prefix:4, autopin:false,
      content:'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n[B][CENTER][FONT=times new roman][COLOR=#FF4444][ICODE]В вашей жалобе отсутствуют доказательства.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n\n[B][CENTER][FONT=times new roman][COLOR=#aaaaaa][ICODE]Закрыто.[/ICODE][/COLOR][/CENTER][/FONT][/B]' },
  ];

  /* ══════════════════════════════════════════════════════════
     ХРАНИЛИЩЕ — с in-memory кешем поверх GM_getValue.
     GM_getValue/setValue у Tampermonkey — синхронные, но не
     бесплатные (сериализация JSON на каждый вызов). v5 вызывал
     store.get() заново в каждом render*() — на панели с 6 табами
     это 6+ JSON.parse на каждое открытие панели без единой
     причины, потому что данные между открытиями не менялись.
  ══════════════════════════════════════════════════════════ */
  const Store = (() => {
    const cache = new Map();

    function get(key, def) {
      if (cache.has(key)) return cache.get(key);
      let val = def;
      try {
        const raw = GM_getValue(key, null);
        if (raw !== null) val = JSON.parse(raw);
      } catch { /* повреждённые данные — используем default, не падаем */ }
      cache.set(key, val);
      return val;
    }

    function set(key, val) {
      cache.set(key, val);
      GM_setValue(key, JSON.stringify(val));
    }

    return { get, set };
  })();

  let nickname  = Store.get('brp_nick', null);
  let templates = Store.get('brp_tpls', DEFAULT_TEMPLATES);
  let logs      = Store.get('brp_logs', []);
  let users     = Store.get('brp_users', 0);

  function saveTpls() { Store.set('brp_tpls', templates); }
  function saveLogs() { Store.set('brp_logs', logs.slice(-300)); }

  /* ══════════════════════════════════════════════════════════
     СОСТОЯНИЕ UI (единая точка правды)
  ══════════════════════════════════════════════════════════ */
  const UI = {
    panelOpen: false,
    activeTab: 'tpl',
    currentPage: 0,
    unread: 0,
  };

  // Форумный автор темы — пересчитывается только когда URL реально
  // сменился (см. PageWatcher), не на каждый /ping и не на каждый рендер.
  const ForumUser = { name: null, uid: null, url: null };

  function detectForumUserIfStale() {
    if (ForumUser.url === location.href) return; // уже актуально, не трогаем DOM
    ForumUser.url = location.href;
    try {
      const authorLink = document.querySelector('a.username[data-user-id]');
      if (authorLink) {
        ForumUser.name = authorLink.textContent.trim();
        ForumUser.uid  = authorLink.getAttribute('data-user-id') || '';
      }
      if (!nickname) {
        const nameEl = document.querySelector('.p-navgroup-link--user .p-navgroup-linkText');
        if (nameEl) nickname = nameEl.textContent.trim();
      }
    } catch { /* структура страницы форума может отличаться — не критично */ }
  }

  /* ══════════════════════════════════════════════════════════
     УТИЛИТЫ
  ══════════════════════════════════════════════════════════ */
  function greeting() {
    const h = new Date().getHours();
    if (h>=5  && h<12) return 'Доброе утро';
    if (h>=12 && h<17) return 'Добрый день';
    if (h>=17 && h<22) return 'Добрый вечер';
    return 'Доброй ночи';
  }

  function addLog(a, d) {
    const entry = { t: new Date().toLocaleString('ru-RU'), u: nickname||'?', a, d };
    logs.push(entry);
    saveLogs();
    tg(`📋 <b>${a}</b>\n👤 ${nickname||'?'}\n📝 ${d}\n🕐 ${entry.t}`);
  }

  function tg(text, chatId) {
    GM_xmlhttpRequest({
      method: 'POST',
      url: `https://api.telegram.org/bot${TG_TOKEN}/sendMessage`,
      headers: { 'Content-Type': 'application/json' },
      data: JSON.stringify({ chat_id: chatId || TG_CID, text, parse_mode: 'HTML' }),
    });
  }

  let toastEl = null; // переиспользуем один DOM-узел вместо create/remove на каждый вызов
  function toast(msg, ms = 3000) {
    if (!toastEl) {
      toastEl = document.createElement('div');
      toastEl.className = 'brv6-toast';
      document.body.appendChild(toastEl);
    }
    toastEl.textContent = msg;
    toastEl.classList.remove('in');
    // форсируем reflow чтобы анимация перезапустилась даже если toast()
    // вызван повторно до истечения предыдущего таймаута
    void toastEl.offsetWidth;
    toastEl.classList.add('in');
    Timers.after('toast-hide', ms, () => toastEl.classList.remove('in'));
  }

  function applyMacros(text) {
    return text
      .replace(/{{greeting}}/g, greeting())
      .replace(/{{username}}/g, ForumUser.name || 'Игрок')
      .replace(/{{uid}}/g, ForumUser.uid || '0');
  }

  function escapeHtml(s) {
    const div = document.createElement('div');
    div.textContent = s;
    return div.innerHTML;
  }

  /* ══════════════════════════════════════════════════════════
     РЕДАКТОР ФОРУМА
  ══════════════════════════════════════════════════════════ */
  function findEditor() {
    for (const f of document.querySelectorAll('iframe')) {
      try {
        const b = (f.contentDocument || f.contentWindow.document).body;
        if (b?.contentEditable === 'true') return { t:'ce', el:b, doc:f.contentDocument||f.contentWindow.document };
      } catch { /* cross-origin iframe — пропускаем */ }
    }
    const ce = document.querySelector('.fr-element[contenteditable="true"],[contenteditable="true"][role="textbox"]');
    if (ce) return { t:'ce', el:ce, doc:document };
    const ta = document.querySelector('textarea[name="message"],#ctrl_body');
    if (ta) return { t:'ta', el:ta };
    return null;
  }

  function insertText(text) {
    const ed = findEditor();
    if (!ed) { toast('❌ Открой поле ответа на форуме'); return false; }
    if (ed.t === 'ta') {
      ed.el.value = text;
      ed.el.dispatchEvent(new Event('input', { bubbles:true }));
    } else {
      ed.el.focus();
      try { ed.doc.execCommand('selectAll',false,null); ed.doc.execCommand('insertText',false,text); }
      catch { ed.el.innerText = text; }
      ed.el.dispatchEvent(new Event('input', { bubbles:true }));
    }
    return true;
  }

  function applyPrefix(id) {
    if (!id) return;
    const el = document.querySelector('select[name="prefix_id"],#ctrl_prefix_id,select.prefixMenu');
    if (el) { el.value = String(id); el.dispatchEvent(new Event('change', { bubbles:true })); }
  }

  function submitForm() {
    const btn = document.querySelector('button[data-handler="submit"],.js-submit-button,button.button--primary');
    if (btn) { btn.click(); return true; }
    return false;
  }

  function editThread(prefix, sticky = false, open = true) {
    const titleEl = document.querySelector('.p-title-value');
    if (!titleEl) return;
    const title = titleEl.lastChild?.textContent || titleEl.textContent;
    const token = typeof XF !== 'undefined' ? XF.config?.csrf : '';
    const body = new FormData();
    body.append('prefix_id', prefix);
    body.append('title', title);
    if (sticky) body.append('sticky', '1');
    body.append('discussion_open', open ? '1' : '0');
    if (token) {
      body.append('_xfToken', token);
      body.append('_xfRequestUri', location.pathname);
      body.append('_xfWithData', '1');
      body.append('_xfResponseType', 'json');
    }
    fetch(location.href + 'edit', { method:'POST', body }).then(() => location.reload()).catch(() => {});
  }

  function applyTemplate(tpl) {
    const text = applyMacros(tpl.content);
    if (!insertText(text)) return;
    if (tpl.prefix) { applyPrefix(tpl.prefix); editThread(tpl.prefix, tpl.autopin, !tpl.autopin); }
    addLog('Шаблон', `${tpl.name} → ${ForumUser.name||'?'}`);
    toast(`✅ "${tpl.name}" вставлен`);
    Timers.after('submit-after-template', 900, () => {
      if (!submitForm()) toast('⚠️ Нажми "Ответить" вручную');
    });
  }

  /* ══════════════════════════════════════════════════════════
     API СЕРВЕР
  ══════════════════════════════════════════════════════════ */
  function api(path, body, cb) {
    GM_xmlhttpRequest({
      method: 'POST',
      url: SERVER + path,
      headers: { 'Content-Type':'application/json' },
      data: JSON.stringify({ secret: SECRET, ...body }),
      onload: r => { try { cb(null, JSON.parse(r.responseText)); } catch { cb('parse', null); } },
      onerror: () => cb('net', null),
    });
  }

  function sendToSupport(text, cb) {
    api('/send', { nick: nickname, text, topic: document.querySelector('.p-title-value')?.textContent?.trim()||'', url: location.href }, cb);
  }
  function pollMessages(cb) {
    api('/poll', { nick: nickname }, (e, d) => { if (!e && d?.messages?.length) cb(d.messages); });
  }
  function loadHistory(cb) {
    api('/history', { nick: nickname }, (e, d) => cb(e ? [] : (d?.messages||[])));
  }

  /* ══════════════════════════════════════════════════════════
     PageWatcher — единственный источник "URL сменился".
     XenForo умеет частичную (pushState) навигацию без полной
     перезагрузки, поэтому просто слушать 'load' недостаточно —
     но и polling location.href в setInterval каждые 2с (как
     было в v5) означает 30 лишних сравнений строк в минуту
     даже когда человек читает одну и ту же страницу. History API
     даёт события без polling вообще.
  ══════════════════════════════════════════════════════════ */
  const PageWatcher = (() => {
    let lastUrl = location.href;
    const listeners = [];

    function fire() {
      if (location.href === lastUrl) return;
      lastUrl = location.href;
      listeners.forEach(fn => fn());
    }

    function on(fn) { listeners.push(fn); }

    function init() {
      // pushState/replaceState не эмиттят событие сами по себе —
      // патчим один раз, только для детекции, ничего не ломаем в
      // поведении самого History API.
      const origPush = history.pushState;
      const origReplace = history.replaceState;
      history.pushState = function (...args) { origPush.apply(this, args); fire(); };
      history.replaceState = function (...args) { origReplace.apply(this, args); fire(); };
      window.addEventListener('popstate', fire);
    }

    return { on, init };
  })();

  /* ══════════════════════════════════════════════════════════
     Presence/tracking — debounced на PAGE_STATE change, не на
     таймере. /ping всё равно уходит раз в PING_INTERVAL_MS как
     heartbeat, но при быстрой смене страниц (человек кликает по
     разделам форума быстро) не шлём отдельный /ping на КАЖДЫЙ
     клик — debounce схлопывает пачку в один вызов.
  ══════════════════════════════════════════════════════════ */
  function getPageInfo() {
    const topic = document.querySelector('.p-title-value')?.textContent?.trim() || '';
    const crumbs = document.querySelectorAll('.p-breadcrumbs a,.breadBoxMinor a');
    const section = crumbs.length ? crumbs[crumbs.length-2]?.textContent?.trim() : '';
    return { url: location.href, topic, section };
  }

  function updateTrackerUI() {
    const el = document.getElementById('brv6-tracker-text');
    if (!el) return; // панель может быть ещё не построена — не ошибка
    const i = getPageInfo();
    const parts = [];
    if (i.section) parts.push(`📂 ${escapeHtml(i.section)}`);
    if (i.topic)   parts.push(`📌 ${escapeHtml(i.topic.substring(0,35))}`);
    el.innerHTML = parts.length ? parts.map(p => `<span>${p}</span>`).join(' · ') : '📍 Форум';
  }

  function sendPing() {
    detectForumUserIfStale();
    const info = getPageInfo();
    api('/ping', { nick: nickname, ...info, author: ForumUser.name }, () => {});
  }

  function onPageChanged() {
    detectForumUserIfStale();
    updateTrackerUI();
    Timers.after('ping-debounced', PRESENCE_DEBOUNCE_MS, sendPing);
  }

  function startTracking() {
    detectForumUserIfStale();
    updateTrackerUI();

    PageWatcher.on(onPageChanged);
    PageWatcher.init();

    sendPing(); // первый ping сразу, не ждём первого интервала
    Timers.every('ping-heartbeat', PING_INTERVAL_MS, sendPing);

    Timers.every('poll-support', POLL_INTERVAL_MS, () => {
      pollMessages(msgs => {
        if (!msgs.length) return;
        UI.unread += msgs.length;
        setBadge(UI.unread);
        msgs.forEach(m => toast(`💬 ${m.from||'Поддержка'}: ${m.text.substring(0,40)}`, 5000));
        if (UI.panelOpen && UI.activeTab === 'chat') renderTab('chat');
      });
    });
  }

  /* ══════════════════════════════════════════════════════════
     CSS — не менялся структурно относительно v5 (визуал тот же
     по запросу пользователя), только префикс классов brv6-.
  ══════════════════════════════════════════════════════════ */
  const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=Rajdhani:wght@500;700&display=swap');
:root{
  --bg:#080510;--card:rgba(16,10,28,.96);--pri:#8a2be2;--pril:#b066ff;--prid:#5f27cd;
  --txt:#fff;--mut:#777;--inp:rgba(255,255,255,.07);--brd:rgba(138,43,226,.2);
  --grn:#2ed47a;--red:#ff4f4f;--r:14px;--sh:0 20px 60px rgba(0,0,0,.8);
}
.brv6-toast{position:fixed;bottom:90px;left:50%;transform:translateX(-50%) translateY(10px);background:var(--card);border:1px solid var(--brd);border-radius:30px;padding:10px 22px;font:13px 'Inter',sans-serif;color:var(--txt);z-index:9999999;opacity:0;transition:.35s;pointer-events:none;white-space:nowrap;backdrop-filter:blur(14px);box-shadow:var(--sh);max-width:80vw;overflow:hidden;text-overflow:ellipsis}
.brv6-toast.in{opacity:1;transform:translateX(-50%) translateY(0)}

#brv6-login{position:fixed;inset:0;z-index:999998;background:var(--bg);display:flex;align-items:center;justify-content:center;overflow:hidden}
#brv6-canvas{position:absolute;inset:0;pointer-events:none}
.brv6-login-bg{position:absolute;inset:0;background:radial-gradient(circle at 70% 20%,rgba(138,43,226,.35),transparent 55%),radial-gradient(circle at 20% 80%,rgba(45,13,80,.6),transparent 50%)}
.brv6-auth{position:relative;z-index:2;background:var(--card);width:min(400px,92vw);padding:46px 34px;border-radius:22px;border:1px solid var(--brd);backdrop-filter:blur(20px);text-align:center;animation:brv6Pop .5s cubic-bezier(.34,1.56,.64,1);box-shadow:0 0 80px rgba(138,43,226,.15),var(--sh)}
.brv6-logo{font:700 2.2rem 'Rajdhani',sans-serif;color:var(--txt);letter-spacing:3px;margin-bottom:6px}
.brv6-logo span{color:var(--pril)}
.brv6-sub{color:var(--mut);font-size:.82rem;margin-bottom:28px;line-height:1.5}
.brv6-field{position:relative;margin-bottom:14px;text-align:left}
.brv6-field i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--pril);font-size:1rem;pointer-events:none}
.brv6-field input{width:100%;box-sizing:border-box;padding:13px 14px 13px 44px;background:var(--inp);border:1px solid rgba(255,255,255,.1);border-radius:10px;color:var(--txt);font-size:.9rem;font-family:'Inter',sans-serif;outline:none;transition:.3s}
.brv6-field input:focus{border-color:var(--pri);background:rgba(138,43,226,.12)}
.brv6-detect{font-size:11px;color:var(--grn);margin-bottom:12px;min-height:16px}
.brv6-btn{width:100%;padding:14px;border:none;border-radius:11px;background:linear-gradient(90deg,var(--pri),var(--prid));color:#fff;font-weight:700;font-size:1rem;font-family:'Inter',sans-serif;cursor:pointer;transition:.3s;letter-spacing:.5px}
.brv6-btn:hover{filter:brightness(1.2);box-shadow:0 10px 28px rgba(138,43,226,.4)}
.brv6-foot{margin-top:18px;color:var(--mut);font-size:.8rem}
.brv6-foot a{color:var(--txt);font-weight:600;text-decoration:none}

#brv6-fab{position:fixed;bottom:22px;right:22px;z-index:999996;width:52px;height:52px;border-radius:50%;background:linear-gradient(135deg,var(--pri),var(--prid));border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 4px 24px rgba(138,43,226,.5);transition:.2s}
#brv6-fab:hover{transform:scale(1.12)}
.brv6-fab-badge{position:absolute;top:-4px;right:-4px;background:var(--red);color:#fff;border-radius:50%;width:18px;height:18px;font-size:10px;font-weight:700;display:none;align-items:center;justify-content:center;font-family:'Inter',sans-serif}
.brv6-fab-badge.show{display:flex}

#brv6-panel{position:fixed;bottom:84px;right:22px;z-index:999995;width:min(500px,96vw);max-height:86vh;background:var(--card);border:1px solid var(--brd);border-radius:var(--r);box-shadow:var(--sh);backdrop-filter:blur(20px);font-family:'Inter',sans-serif;color:var(--txt);display:none;flex-direction:column;overflow:hidden}
#brv6-panel.open{display:flex;animation:brv6Slide .3s cubic-bezier(.34,1.3,.64,1)}

.brv6-hdr{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;flex-shrink:0;background:linear-gradient(90deg,rgba(40,10,70,.9),rgba(16,10,28,.9));border-bottom:1px solid var(--brd)}
.brv6-hdr-l{display:flex;align-items:center;gap:8px}
.brv6-logo-sm{font:700 16px 'Rajdhani',sans-serif;color:var(--pril);letter-spacing:2px}
.brv6-nick-sm{font-size:11px;color:var(--mut)}
.brv6-dot{width:7px;height:7px;border-radius:50%;background:var(--grn);box-shadow:0 0 6px var(--grn)}
.brv6-x{background:none;border:none;color:var(--mut);font-size:18px;cursor:pointer;transition:.2s;padding:0;line-height:1}
.brv6-x:hover{color:var(--red)}

.brv6-tracker{padding:5px 16px;font-size:10px;color:var(--mut);background:rgba(0,0,0,.3);border-bottom:1px solid var(--brd);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0}
.brv6-tracker span{color:var(--pril)}

.brv6-pages-wrap{position:relative;flex:1;overflow:hidden;display:flex;flex-direction:column}
.brv6-page-dots{display:flex;justify-content:center;gap:5px;padding:5px;flex-shrink:0;background:rgba(0,0,0,.2)}
.brv6-dot-ind{width:6px;height:6px;border-radius:50%;background:var(--brd);transition:.2s;cursor:pointer}
.brv6-dot-ind.on{background:var(--pril);width:18px;border-radius:3px}
.brv6-pages{display:flex;flex:1;transition:transform .35s cubic-bezier(.4,0,.2,1);will-change:transform}
.brv6-page{flex:0 0 100%;display:flex;flex-direction:column;overflow:hidden}

.brv6-nav{display:flex;padding:4px 6px;gap:2px;flex-shrink:0;background:rgba(8,5,16,.9);border-bottom:1px solid var(--brd)}
.brv6-tab{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;padding:7px 2px;background:none;border:none;cursor:pointer;font-size:10px;color:var(--mut);font-family:'Inter',sans-serif;border-radius:8px;transition:.2s;position:relative}
.brv6-tab i{font-size:13px;transition:transform .2s;pointer-events:none}
.brv6-tab:hover{color:var(--pril);background:rgba(138,43,226,.1)}
.brv6-tab.on{color:var(--pril)}
.brv6-tab.on::after{content:'';position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:16px;height:2px;background:var(--pril);border-radius:2px}
.brv6-tab-b{position:absolute;top:2px;right:4px;background:var(--red);color:#fff;border-radius:50%;width:13px;height:13px;font-size:9px;display:none;align-items:center;justify-content:center;pointer-events:none}
.brv6-tab-b.show{display:flex}

.brv6-body{flex:1;overflow-y:auto;padding:13px}
.brv6-body::-webkit-scrollbar{width:3px}
.brv6-body::-webkit-scrollbar-thumb{background:var(--brd);border-radius:2px}
.brv6-sec{font:700 10px 'Rajdhani',sans-serif;color:var(--mut);text-transform:uppercase;letter-spacing:1.5px;margin:0 0 10px}
.brv6-hr{border:none;border-top:1px solid var(--brd);margin:12px 0}

.brv6-tpl{background:rgba(138,43,226,.07);border:1px solid var(--brd);border-radius:10px;padding:12px 14px;margin-bottom:8px;transition:.2s}
.brv6-tpl:hover{border-color:var(--pril);background:rgba(138,43,226,.13)}
.brv6-tpl-top{display:flex;align-items:center;gap:6px;margin-bottom:3px;flex-wrap:wrap}
.brv6-tpl-name{font-size:13px;font-weight:700}
.brv6-bdg{font-size:9px;padding:2px 7px;border-radius:20px;background:rgba(255,255,255,.07);font-weight:700}
.brv6-prev{font-size:11px;color:var(--mut);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:9px}
.brv6-btns{display:flex;gap:5px;flex-wrap:wrap}

.brv6-msgs{overflow-y:auto;padding:6px 0;display:flex;flex-direction:column;gap:7px;max-height:280px;min-height:150px}
.brv6-msgs::-webkit-scrollbar{width:3px}
.brv6-msgs::-webkit-scrollbar-thumb{background:var(--brd)}
.brv6-msg{max-width:88%;padding:9px 13px;border-radius:12px;font-size:13px;line-height:1.4}
.brv6-msg.user{align-self:flex-end;background:rgba(138,43,226,.25);border:1px solid rgba(138,43,226,.4);border-bottom-right-radius:3px}
.brv6-msg.support{align-self:flex-start;background:rgba(46,212,122,.12);border:1px solid rgba(46,212,122,.3);border-bottom-left-radius:3px}
.brv6-msg-from{font-size:10px;color:var(--grn);font-weight:700;margin-bottom:2px}
.brv6-msg-time{font-size:9px;color:var(--mut);margin-top:3px;text-align:right}
.brv6-chat-hint{font-size:11px;color:var(--mut);margin-bottom:8px;text-align:center;padding:7px;background:rgba(138,43,226,.06);border-radius:8px}
.brv6-chat-row{display:flex;gap:6px;margin-top:8px;align-items:flex-end}
.brv6-chat-ta{flex:1;background:var(--inp);border:1px solid var(--brd);border-radius:10px;padding:9px 12px;color:var(--txt);font-size:13px;font-family:'Inter',sans-serif;outline:none;resize:none;min-height:38px;max-height:90px;transition:.2s}
.brv6-chat-ta:focus{border-color:var(--pri)}
.brv6-chat-send{background:linear-gradient(135deg,var(--pri),var(--prid));border:none;border-radius:10px;padding:9px 13px;color:#fff;font-size:14px;cursor:pointer;flex-shrink:0;transition:.2s}

.brv6-fg{margin-bottom:11px}
.brv6-fg label{display:block;font-size:11px;color:var(--mut);margin-bottom:3px}
.brv6-inp,.brv6-ta,.brv6-sel{width:100%;box-sizing:border-box;background:var(--inp);border:1px solid rgba(255,255,255,.1);border-radius:9px;padding:9px 13px;color:var(--txt);font-size:13px;font-family:'Inter',sans-serif;outline:none;transition:.25s}
.brv6-inp:focus,.brv6-ta:focus,.brv6-sel:focus{border-color:var(--pri);background:rgba(138,43,226,.1)}
.brv6-ta{min-height:80px;resize:vertical}
.brv6-sel option{background:#120c20}

.btn6{background:linear-gradient(90deg,var(--pri),var(--prid));border:none;border-radius:8px;padding:8px 14px;color:#fff;font-size:12px;font-weight:700;font-family:'Inter',sans-serif;cursor:pointer;transition:.2s}
.btn6:hover{filter:brightness(1.2)}.btn6:active{transform:scale(.97)}
.btn6g{background:transparent;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 14px;color:var(--txt);font-size:12px;font-family:'Inter',sans-serif;cursor:pointer;transition:.2s}
.btn6g:hover{border-color:var(--pril);color:var(--pril)}
.btn6r{background:rgba(255,79,79,.1);border:1px solid rgba(255,79,79,.3);border-radius:8px;padding:8px 14px;color:var(--red);font-size:12px;font-family:'Inter',sans-serif;cursor:pointer;transition:.2s}
.btn6r:hover{background:rgba(255,79,79,.25)}
.btn6grn{background:rgba(46,212,122,.12);border:1px solid rgba(46,212,122,.3);border-radius:8px;padding:8px 14px;color:var(--grn);font-size:12px;font-family:'Inter',sans-serif;cursor:pointer;transition:.2s}
.btn6grn:hover{background:rgba(46,212,122,.28)}

.brv6-color-row{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px}
.brv6-color-btn{width:24px;height:24px;border-radius:50%;border:2px solid transparent;cursor:pointer;transition:.2s;flex-shrink:0}
.brv6-color-btn:hover,.brv6-color-btn.on{border-color:#fff;transform:scale(1.2)}
.brv6-chip{background:rgba(138,43,226,.1);border:1px solid var(--brd);border-radius:20px;padding:4px 10px;font-size:11px;color:var(--mut);cursor:pointer;font-family:'Inter',sans-serif;transition:.2s}
.brv6-chip:hover{border-color:var(--pril);color:var(--pril)}

.brv6-upload-zone{border:2px dashed var(--brd);border-radius:12px;padding:24px;text-align:center;cursor:pointer;transition:.2s;margin-bottom:10px}
.brv6-upload-zone:hover{border-color:var(--pril)}
.brv6-upload-zone.drag{border-color:var(--pril);background:rgba(138,43,226,.08)}
.brv6-upload-ico{font-size:32px;margin-bottom:6px}
.brv6-upload-hint{font-size:12px;color:var(--mut)}
.brv6-photo-result{background:rgba(46,212,122,.08);border:1px solid rgba(46,212,122,.2);border-radius:9px;padding:10px;margin-top:8px;display:none}
.brv6-photo-url{font-size:11px;word-break:break-all;color:var(--grn);margin-bottom:6px}

#brv6-snake-canvas{border-radius:8px;display:block;margin:0 auto;touch-action:none}
.brv6-snake-controls{display:flex;gap:8px;margin-top:8px;flex-direction:column;align-items:center}
.brv6-dpad{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:120px}
.brv6-dpad button{background:rgba(138,43,226,.15);border:1px solid var(--brd);border-radius:7px;padding:10px;color:var(--txt);font-size:14px;cursor:pointer;transition:.2s}
.brv6-dpad button:hover{background:rgba(138,43,226,.35)}
.brv6-snake-score{font:700 14px 'Rajdhani',sans-serif;color:var(--pril);text-align:center;margin-bottom:6px}

.brv6-log{font-size:11px;padding:7px 10px;border-left:2px solid var(--brd);margin-bottom:5px;color:var(--mut);line-height:1.5}
.brv6-log b{color:var(--txt)}
.brv6-log-t{font-size:10px;float:right}

.brv6-con{display:flex;align-items:center;gap:12px;background:rgba(138,43,226,.07);border:1px solid var(--brd);border-radius:10px;padding:12px 14px;margin-bottom:8px;text-decoration:none;transition:.2s}
.brv6-con:hover{border-color:var(--pril);background:rgba(138,43,226,.15)}
.brv6-con-i{font-size:20px}.brv6-con-n{font-size:13px;font-weight:700;color:var(--txt)}.brv6-con-s{font-size:11px;color:var(--mut)}

.brv6-game-card{background:rgba(138,43,226,.1);border:2px solid var(--brd);border-radius:14px;padding:20px;text-align:center;margin-bottom:12px;transition:.2s}
.brv6-game-card:hover{border-color:var(--pril)}
.brv6-game-ico{font-size:40px;margin-bottom:8px}
.brv6-game-title{font:700 16px 'Rajdhani',sans-serif;color:var(--pril);margin-bottom:4px;letter-spacing:1px}
.brv6-game-sub{font-size:11px;color:var(--mut);margin-bottom:14px}

@keyframes brv6Pop{from{transform:scale(.8) translateY(20px);opacity:0}to{transform:scale(1);opacity:1}}
@keyframes brv6Slide{from{transform:translateY(16px) scale(.97);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}

@media(max-width:540px){
  #brv6-panel{right:5px;left:5px;width:auto;bottom:72px}
  #brv6-fab{bottom:14px;right:14px;width:46px;height:46px;font-size:18px}
  .brv6-tab{font-size:9px}.brv6-auth{padding:32px 18px}
}
  `;

  /* ══════════════════════════════════════════════════════════
     ИНИЦИАЛИЗАЦИЯ СТИЛЕЙ (один раз)
  ══════════════════════════════════════════════════════════ */
  function injectStyles() {
    document.head.appendChild(Object.assign(document.createElement('style'), { textContent: CSS }));
    if (!document.querySelector('link[href*="font-awesome"]')) {
      document.head.appendChild(Object.assign(document.createElement('link'), {
        rel: 'stylesheet',
        href: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css',
      }));
    }
  }

  /* ══════════════════════════════════════════════════════════
     ЛОГИН
  ══════════════════════════════════════════════════════════ */
  function buildLogin() {
    detectForumUserIfStale();

    const wrap = document.createElement('div');
    wrap.id = 'brv6-login';
    wrap.innerHTML = `
      <canvas id="brv6-canvas"></canvas>
      <div class="brv6-login-bg"></div>
      <div class="brv6-auth">
        <div class="brv6-logo">BR <span>ADMIN</span></div>
        <p class="brv6-sub">Панель куратора форума Black Russia</p>
        <div class="brv6-detect">${nickname ? '✅ Ник определён: ' + escapeHtml(nickname) : '🔍 Введите ваш ник'}</div>
        <div class="brv6-field">
          <i class="fa-solid fa-user"></i>
          <input type="text" id="brv6-nick-i" value="${nickname ? escapeHtml(nickname) : ''}" placeholder="Ваш никнейм" autocomplete="off" />
        </div>
        <button class="brv6-btn" id="brv6-go">Войти в панель</button>
        <div class="brv6-foot">Разработчик: <a href="https://vk.ru/brunoverona" target="_blank">vk.ru/brunoverona</a></div>
      </div>
    `;
    document.body.appendChild(wrap);

    // Частицы — простая canvas-анимация. Останавливается явно при
    // уходе с экрана логина через cancelAnimationFrame, не оставляет
    // requestAnimationFrame-цикл висеть после того как wrap.remove().
    const cv = document.getElementById('brv6-canvas'), ctx = cv.getContext('2d');
    cv.width = innerWidth; cv.height = innerHeight;
    const pts = Array.from({length:60}, () => ({
      x: Math.random()*innerWidth, y: Math.random()*innerHeight,
      r: Math.random()*1.8+.3, vx:(Math.random()-.5)*.4, vy:(Math.random()-.5)*.4,
    }));
    let rafId = null;
    function anim() {
      ctx.clearRect(0,0,cv.width,cv.height);
      for (const p of pts) {
        p.x+=p.vx; p.y+=p.vy;
        if (p.x<0) p.x=cv.width; if (p.x>cv.width) p.x=0;
        if (p.y<0) p.y=cv.height; if (p.y>cv.height) p.y=0;
        ctx.fillStyle = 'rgba(176,102,255,.28)';
        ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
      }
      rafId = requestAnimationFrame(anim);
    }
    rafId = requestAnimationFrame(anim);

    const go = () => {
      const v = document.getElementById('brv6-nick-i').value.trim();
      if (!v) return;
      nickname = v;
      Store.set('brp_nick', v);
      users++; Store.set('brp_users', users);
      addLog('Вход', v);
      cancelAnimationFrame(rafId);
      wrap.style.transition = 'opacity .5s';
      wrap.style.opacity = '0';
      Timers.after('login-transition', 500, () => {
        wrap.remove();
        buildFab();
        buildPanel();
        startTracking();
      });
    };
    document.getElementById('brv6-go').onclick = go;
    document.getElementById('brv6-nick-i').onkeydown = e => { if (e.key === 'Enter') go(); };
  }

  /* ══════════════════════════════════════════════════════════
     FAB
  ══════════════════════════════════════════════════════════ */
  function buildFab() {
    const fab = document.createElement('button');
    fab.id = 'brv6-fab';
    fab.innerHTML = '<span>⚙️</span><span class="brv6-fab-badge" id="brv6-fbadge">0</span>';
    fab.onclick = () => {
      UI.panelOpen = !UI.panelOpen;
      document.getElementById('brv6-panel').classList.toggle('open', UI.panelOpen);
      if (UI.panelOpen) renderTab(UI.activeTab);
    };
    document.body.appendChild(fab);
  }

  function setBadge(n) {
    UI.unread = n;
    const b = document.getElementById('brv6-fbadge'), tb = document.getElementById('brv6-tbadge');
    if (b) { b.textContent = n; b.classList.toggle('show', n>0); }
    if (tb) { tb.textContent = n; tb.classList.toggle('show', n>0); }
  }

  /* ══════════════════════════════════════════════════════════
     ПАНЕЛЬ — делегирование событий на уровне контейнера.
     Один addEventListener('click', ...) на #brv6-panel вместо
     навешивания обработчика на каждую кнопку при каждом рендере.
     Кнопки помечаются data-action="имя_действия" data-arg="...",
     единый роутер решает что делать.
  ══════════════════════════════════════════════════════════ */
  const TABS = [
    { id:'tpl',  icon:'fa-list',        label:'Шаблоны' },
    { id:'chat', icon:'fa-comments',    label:'Поддержка' },
    { id:'log',  icon:'fa-chart-bar',   label:'Логи' },
    { id:'con',  icon:'fa-paper-plane', label:'Связь' },
    { id:'game', icon:'fa-gamepad',     label:'Игры' },
  ];

  function buildPanel() {
    const p = document.createElement('div');
    p.id = 'brv6-panel';
    p.innerHTML = `
      <div class="brv6-hdr">
        <div class="brv6-hdr-l">
          <div class="brv6-dot"></div>
          <span class="brv6-logo-sm">BR ADMIN</span>
          <span class="brv6-nick-sm">👤 ${escapeHtml(nickname)}</span>
        </div>
        <button class="brv6-x" data-action="close-panel">✕</button>
      </div>
      <div class="brv6-tracker"><span id="brv6-tracker-text">📍 Загрузка...</span></div>
      <nav class="brv6-nav">
        ${TABS.map(t => `<button class="brv6-tab${t.id===UI.activeTab?' on':''}" data-action="switch-tab" data-arg="${t.id}"><i class="fa-solid ${t.icon}"></i>${t.label}${t.id==='chat'?'<span class="brv6-tab-b" id="brv6-tbadge"></span>':''}</button>`).join('')}
      </nav>
      <div class="brv6-pages-wrap">
        <div class="brv6-page-dots">
          <div class="brv6-dot-ind on" data-action="goto-page" data-arg="0"></div>
          <div class="brv6-dot-ind" data-action="goto-page" data-arg="1"></div>
        </div>
        <div class="brv6-pages" id="brv6-pages">
          <div class="brv6-page"><div class="brv6-body" id="brv6-body"></div></div>
          <div class="brv6-page"><div class="brv6-body" id="brv6-body2"></div></div>
        </div>
      </div>
    `;
    document.body.appendChild(p);

    // ── Единый обработчик кликов ──
    p.addEventListener('click', onPanelClick);

    // Свайп между страницами
    let swipeStartX = 0, swiping = false;
    const pages = document.getElementById('brv6-pages');
    pages.addEventListener('touchstart', e => { swipeStartX = e.touches[0].clientX; swiping = true; }, { passive:true });
    pages.addEventListener('touchend', e => {
      if (!swiping) return; swiping = false;
      const dx = swipeStartX - e.changedTouches[0].clientX;
      if (Math.abs(dx) > 40) gotoPage(dx > 0 ? 1 : 0);
    }, { passive:true });

    renderTab(UI.activeTab);
    renderEditorPage();
    updateTrackerUI();
  }

  function gotoPage(n) {
    UI.currentPage = n;
    const pages = document.getElementById('brv6-pages');
    if (pages) pages.style.transform = `translateX(-${n*100}%)`;
    document.querySelectorAll('.brv6-dot-ind').forEach((d,i) => d.classList.toggle('on', i===n));
  }

  function switchTab(tab) {
    UI.activeTab = tab;
    document.querySelectorAll('.brv6-tab').forEach(b => b.classList.toggle('on', b.dataset.arg === tab));
    if (tab === 'chat') setBadge(0);
    // Уходим со страницы игры — обязательно уничтожаем таймер змейки,
    // иначе он продолжит тикать по несуществующему canvas.
    SnakeGame.destroy();
    gotoPage(0);
    renderTab(tab);
  }

  /* ── Роутер кликов по data-action ── */
  function onPanelClick(e) {
    const el = e.target.closest('[data-action]');
    if (!el) return;
    const action = el.dataset.action;
    const arg = el.dataset.arg;

    switch (action) {
      case 'close-panel':
        UI.panelOpen = false;
        document.getElementById('brv6-panel').classList.remove('open');
        SnakeGame.destroy();
        break;
      case 'switch-tab':
        switchTab(arg);
        break;
      case 'goto-page':
        gotoPage(parseInt(arg));
        break;
      case 'apply-template': {
        const t = templates.find(x => x.id == arg);
        if (t) applyTemplate(t);
        break;
      }
      case 'copy-template': {
        const t = templates.find(x => x.id == arg);
        if (!t) break;
        navigator.clipboard.writeText(applyMacros(t.content)).catch(() => {});
        addLog('Копирование', t.name);
        el.textContent = '✅';
        Timers.after('copy-label-reset', 1500, () => { el.textContent = '📋 Копировать'; });
        break;
      }
      case 'delete-template': {
        const t = templates.find(x => x.id == arg);
        if (!t || !confirm(`Удалить "${t.name}"?`)) break;
        addLog('Удалён шаблон', t.name);
        templates = templates.filter(x => x.id != arg);
        saveTpls();
        renderTemplatesTab(document.getElementById('brv6-body'));
        break;
      }
      case 'send-chat': sendChatMessage(); break;
      case 'clear-logs':
        if (!confirm('Очистить логи?')) break;
        logs = []; saveLogs();
        renderLogsTab(document.getElementById('brv6-body'));
        break;
      case 'save-template': saveNewTemplate(); break;
      case 'insert-bb': insertBBChip(arg); break;
      case 'pick-color': pickColor(el, arg); break;
      case 'launch-game': launchBlackRussia(); break;
      case 'open-snake': SnakeGame.start(document.getElementById('brv6-body')); break;
      case 'snake-back':
        SnakeGame.destroy();
        renderGameTab(document.getElementById('brv6-body'));
        break;
      case 'snake-dir': SnakeGame.setDirection(arg); break;
    }
  }

  /* ══════════════════════════════════════════════════════════
     РЕНДЕР ТАБОВ — каждая функция строит innerHTML один раз,
     без вложенных querySelectorAll(...).forEach для навешивания
     обработчиков (это делает делегирование выше).
  ══════════════════════════════════════════════════════════ */
  function renderTab(tab) {
    const body = document.getElementById('brv6-body');
    if (!body) return;
    const renderers = {
      tpl: renderTemplatesTab, chat: renderChatTab, log: renderLogsTab,
      con: renderContactTab, game: renderGameTab,
    };
    (renderers[tab] || renderTemplatesTab)(body);
  }

  function renderTemplatesTab(body) {
    const authorName = ForumUser.name ? escapeHtml(ForumUser.name) : 'не определён';
    const list = templates.length
      ? templates.map(tpl => {
          const p = prefixById.get(tpl.prefix) || prefixById.get(0);
          return `
            <div class="brv6-tpl">
              <div class="brv6-tpl-top">
                <span class="brv6-tpl-name">${escapeHtml(tpl.name)}</span>
                <span class="brv6-bdg" style="color:${p.color}">${p.name}</span>
                ${tpl.autopin ? '<span style="color:#f0c040;font-size:10px">📌</span>' : ''}
              </div>
              <div class="brv6-prev">${escapeHtml(tpl.content.replace(/\[.*?\]/g,'').trim().substring(0,80))}…</div>
              <div class="brv6-btns">
                <button class="btn6" data-action="apply-template" data-arg="${tpl.id}">🚀 Применить</button>
                <button class="btn6g" data-action="copy-template" data-arg="${tpl.id}">📋 Копировать</button>
                <button class="btn6r" data-action="delete-template" data-arg="${tpl.id}">🗑</button>
              </div>
            </div>`;
        }).join('')
      : '<p style="color:var(--mut);font-size:12px">Нет шаблонов. Добавьте на второй странице (свайп ←)</p>';

    body.innerHTML = `
      <p class="brv6-sec">Шаблоны — клик применит и отправит</p>
      <div style="font-size:11px;color:var(--mut);margin-bottom:10px">👤 Автор темы: <b style="color:var(--pril)">${authorName}</b></div>
      <div>${list}</div>
    `;
  }

  function renderChatTab(body) {
    body.innerHTML = `
      <p class="brv6-sec">Поддержка — чат с разработчиком</p>
      <div class="brv6-chat-hint">💬 Напишите вопрос — разработчик или поддержка ответят здесь</div>
      <div class="brv6-msgs" id="brv6-msgs"><div style="text-align:center;color:var(--mut);font-size:12px;padding:16px">Загрузка...</div></div>
      <div class="brv6-chat-row">
        <textarea class="brv6-chat-ta" id="brv6-cinp" placeholder="Ваш вопрос или жалоба..."></textarea>
        <button class="brv6-chat-send" data-action="send-chat"><i class="fa-solid fa-paper-plane"></i></button>
      </div>
    `;
    document.getElementById('brv6-cinp').onkeydown = e => {
      if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChatMessage(); }
    };
    loadHistory(drawMessages);
  }

  function drawMessages(msgs) {
    const el = document.getElementById('brv6-msgs');
    if (!el) return;
    if (!msgs.length) {
      el.innerHTML = '<div style="text-align:center;color:var(--mut);font-size:12px;padding:16px">Сообщений нет. Напишите первым!</div>';
      return;
    }
    el.innerHTML = msgs.map(m => `
      <div class="brv6-msg ${m.role==='support'?'support':'user'}">
        ${m.role==='support' ? `<div class="brv6-msg-from">🛡 ${escapeHtml(m.from||'Поддержка')}</div>` : ''}
        ${escapeHtml(m.text)}
        <div class="brv6-msg-time">${m.time||''}</div>
      </div>`).join('');
    el.scrollTop = el.scrollHeight;
  }

  function sendChatMessage() {
    const inp = document.getElementById('brv6-cinp');
    if (!inp) return;
    const text = inp.value.trim();
    if (!text) return;
    inp.value = '';
    inp.disabled = true;
    sendToSupport(text, (e, d) => {
      inp.disabled = false;
      if (e || !d?.ok) { toast('❌ Сервер недоступен'); return; }
      addLog('Сообщение в поддержку', text.substring(0,40));
      loadHistory(drawMessages);
      toast('✅ Отправлено');
    });
  }

  function renderLogsTab(body) {
    const rows = logs.length
      ? [...logs].reverse().map(l => `<div class="brv6-log"><span class="brv6-log-t">${l.t}</span><b>${escapeHtml(l.a)}</b><br>${escapeHtml(l.u)} — ${escapeHtml(l.d)}</div>`).join('')
      : '<p style="color:var(--mut);font-size:12px">Логов нет.</p>';
    body.innerHTML = `
      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
        <p class="brv6-sec" style="margin:0">История (${logs.length})</p>
        <button class="btn6r" data-action="clear-logs" style="padding:5px 10px;font-size:11px">🗑</button>
      </div>
      <div>${rows}</div>
    `;
  }

  function renderContactTab(body) {
    body.innerHTML = `
      <p class="brv6-sec">Связь с разработчиком</p>
      <a class="brv6-con" href="https://vk.ru/brunoverona" target="_blank"><span class="brv6-con-i">💙</span><div><div class="brv6-con-n">ВКонтакте разработчика</div><div class="brv6-con-s">vk.ru/brunoverona</div></div></a>
      <a class="brv6-con" href="https://vk.ru/club237051164" target="_blank"><span class="brv6-con-i">🏢</span><div><div class="brv6-con-n">Группа разработчиков</div><div class="brv6-con-s">vk.ru/club237051164</div></div></a>
      <div style="padding:12px;background:rgba(138,43,226,.07);border:1px solid var(--brd);border-radius:10px;margin-top:12px">
        <p style="font-size:11px;color:var(--mut);margin:0;line-height:1.7"><b style="color:var(--pril)">BR Admin Panel Pro v6.0</b><br>Оптимизировано: единый таймер-менеджер, делегирование событий</p>
      </div>
    `;
  }

  function renderGameTab(body) {
    body.innerHTML = `
      <p class="brv6-sec">Запуск и игры</p>
      <div class="brv6-game-card">
        <div class="brv6-game-ico">🎮</div>
        <div class="brv6-game-title">BLACK RUSSIA</div>
        <div class="brv6-game-sub">Нажми чтобы запустить игру<br>(требуется установленный лаунчер)</div>
        <button class="btn6" data-action="launch-game">🚀 Запустить Black Russia</button>
      </div>
      <div class="brv6-game-card">
        <div class="brv6-game-ico">🐍</div>
        <div class="brv6-game-title">ЗМЕЙКА</div>
        <div class="brv6-game-sub">Мини-игра для отдыха</div>
        <button class="btn6g" data-action="open-snake">🎮 Играть в змейку</button>
      </div>
    `;
  }

  function launchBlackRussia() {
    toast('🚀 Запускаем Black Russia...');
    window.location.href = CR_LINK;
  }

  /* ══════════════════════════════════════════════════════════
     ЗМЕЙКА — инкапсулирована в объект с явным lifecycle.
     .start()/.destroy() — единственные точки входа. Таймер
     хранится как свойство объекта, не переменная модуля, так
     что повторный .start() без предварительного .destroy()
     физически не может создать второй параллельный тик — start()
     сам вызывает destroy() первым делом.
  ══════════════════════════════════════════════════════════ */
  const SnakeGame = (() => {
    let intervalId = null;
    let keyHandler = null;
    let currentSetDir = () => {}; // объявлена до start(), а не после — иначе TDZ на use strict

    function destroy() {
      if (intervalId !== null) { clearInterval(intervalId); intervalId = null; }
      if (keyHandler) { document.removeEventListener('keydown', keyHandler); keyHandler = null; }
      currentSetDir = () => {};
    }

    function start(body) {
      destroy(); // страховка: если start() вызван без предыдущего destroy()

      body.innerHTML = `
        <p class="brv6-sec">🐍 Змейка</p>
        <div class="brv6-snake-score" id="sn-score">Счёт: 0</div>
        <canvas id="brv6-snake-canvas" width="240" height="240"></canvas>
        <div class="brv6-snake-controls">
          <div class="brv6-dpad">
            <div></div><button data-action="snake-dir" data-arg="up">▲</button><div></div>
            <button data-action="snake-dir" data-arg="left">◀</button>
            <button data-action="snake-dir" data-arg="down">▼</button>
            <button data-action="snake-dir" data-arg="right">▶</button>
          </div>
          <button class="btn6g" data-action="snake-back" style="margin-top:8px">← Назад</button>
        </div>
      `;

      const cv = document.getElementById('brv6-snake-canvas');
      const ctx = cv.getContext('2d');
      const SZ = 16, W = 15, H = 15;
      let snake = [{x:7,y:7}], dir = {x:1,y:0}, food = randFood(), score = 0, alive = true;

      function randFood() { return { x: Math.floor(Math.random()*W), y: Math.floor(Math.random()*H) }; }

      function draw() {
        ctx.fillStyle = '#0a0614'; ctx.fillRect(0,0,240,240);
        ctx.fillStyle = '#ff4f4f';
        ctx.beginPath(); ctx.arc(food.x*SZ+8, food.y*SZ+8, 6, 0, Math.PI*2); ctx.fill();
        snake.forEach((s,i) => {
          ctx.fillStyle = i===0 ? '#b066ff' : '#7a35c0';
          ctx.fillRect(s.x*SZ+1, s.y*SZ+1, SZ-2, SZ-2);
        });
        if (!alive) {
          ctx.fillStyle = 'rgba(0,0,0,.6)'; ctx.fillRect(0,0,240,240);
          ctx.fillStyle = '#fff'; ctx.font = 'bold 18px Rajdhani,sans-serif'; ctx.textAlign = 'center';
          ctx.fillText('GAME OVER', 120, 110);
          ctx.font = '13px Inter,sans-serif'; ctx.fillText('Счёт: '+score, 120, 135);
          ctx.fillText('Тапни чтобы начать заново', 120, 155);
        }
      }

      function step() {
        if (!alive) return;
        const head = { x: snake[0].x+dir.x, y: snake[0].y+dir.y };
        if (head.x<0 || head.x>=W || head.y<0 || head.y>=H || snake.some(s => s.x===head.x && s.y===head.y)) {
          alive = false; draw(); return;
        }
        snake.unshift(head);
        if (head.x===food.x && head.y===food.y) {
          score++;
          document.getElementById('sn-score').textContent = 'Счёт: ' + score;
          food = randFood();
        } else {
          snake.pop();
        }
        draw();
      }

      function setDir(name) {
        const map = { up:{x:0,y:-1}, down:{x:0,y:1}, left:{x:-1,y:0}, right:{x:1,y:0} };
        const nd = map[name];
        if (!nd || Math.abs(nd.x) === Math.abs(dir.x)) return; // нельзя развернуться на 180°
        dir = nd;
      }
      currentSetDir = setDir;

      keyHandler = e => {
        const map = { ArrowUp:'up', ArrowDown:'down', ArrowLeft:'left', ArrowRight:'right' };
        if (map[e.key]) setDir(map[e.key]);
      };
      document.addEventListener('keydown', keyHandler);

      cv.onclick = () => {
        if (alive) return;
        snake = [{x:7,y:7}]; dir = {x:1,y:0}; food = randFood(); score = 0; alive = true;
        document.getElementById('sn-score').textContent = 'Счёт: 0';
      };

      intervalId = setInterval(step, 150);
      draw();
    }

    function setDirection(name) { currentSetDir(name); }

    return { start, destroy, setDirection };
  })();

  /* ══════════════════════════════════════════════════════════
     СТРАНИЦА 2 — редактор шаблонов + загрузка фото.
     Строится один раз при открытии панели (не пересобирается
     при каждом свайпе — данные внутри неё не зависят от табов
     первой страницы).
  ══════════════════════════════════════════════════════════ */
  const EDITOR_COLORS = ['#FF00FF','#00FF00','#FF4444','#2196f3','#ec7c26','#f0c040','#ffffff','#aaaaaa','#00bcd4','#9c27b0'];
  const BB_CHIPS = [
    ['🌸 Привет', '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{greeting}}, уважаемый(ая) игрок [USER={{uid}}]{{username}}[/USER][/ICODE].[/CENTER][/I][/SIZE][/FONT][/COLOR]\n\n'],
    ['✅ Одобрено', '[B][CENTER][FONT=times new roman][COLOR=#00FF00][ICODE]Одобрено.[/ICODE][/COLOR][/CENTER][/FONT][/B]'],
    ['❌ Отказано', '[B][CENTER][FONT=times new roman][COLOR=#FF4444][ICODE]Отказано.[/ICODE][/COLOR][/CENTER][/FONT][/B]'],
    ['⏳ На рассм.', '[B][CENTER][FONT=times new roman][COLOR=#ec7c26][ICODE]На рассмотрении.[/ICODE][/COLOR][/CENTER][/FONT][/B]'],
    ['🔒 Закрыто', '[B][CENTER][FONT=times new roman][COLOR=#aaaaaa][ICODE]Закрыто.[/ICODE][/COLOR][/CENTER][/FONT][/B]'],
    ['🖼 Фото', '[CENTER][img]https://i.postimg.cc/cCG97p5p/Pics-Art-07-12-03-23-18-1.png[/img][/CENTER]\n\n'],
  ];

  let lastUploadedUrl = '';

  function renderEditorPage() {
    const body = document.getElementById('brv6-body2');
    if (!body) return;
    const pfxOpts = PREFIXES.map(p => `<option value="${p.id}">${p.name}</option>`).join('');

    body.innerHTML = `
      <p class="brv6-sec">← Свайп для возврата</p>
      <div class="brv6-hr"></div>
      <p class="brv6-sec">Новый шаблон</p>
      <div class="brv6-fg"><label>Название</label><input class="brv6-inp" id="e-name" placeholder="Название шаблона" /></div>
      <div class="brv6-fg"><label>Префикс</label><select class="brv6-sel" id="e-pfx">${pfxOpts}</select></div>
      <div class="brv6-fg"><label style="display:flex;align-items:center;gap:6px;cursor:pointer"><input type="checkbox" id="e-pin" style="accent-color:var(--pri)"> 📌 Автозакреп</label></div>
      <div class="brv6-fg">
        <label>Цвет текста</label>
        <div class="brv6-color-row">
          ${EDITOR_COLORS.map(c => `<div class="brv6-color-btn" style="background:${c}" data-action="pick-color" data-arg="${c}" title="${c}"></div>`).join('')}
        </div>
      </div>
      <div class="brv6-fg">
        <label>BB-вставки</label>
        <div class="brv6-color-row">
          ${BB_CHIPS.map((c,i) => `<button class="brv6-chip" data-action="insert-bb" data-arg="${i}">${c[0]}</button>`).join('')}
        </div>
      </div>
      <div class="brv6-fg">
        <label>Текст шаблона ({{greeting}}, {{username}}, {{uid}})</label>
        <textarea class="brv6-ta" id="e-text" placeholder="[COLOR=#FF00FF]{{greeting}}, {{username}}[/COLOR]&#10;&#10;Ваш текст..."></textarea>
      </div>
      <button class="btn6" data-action="save-template" style="margin-bottom:14px">💾 Сохранить шаблон</button>
      <div class="brv6-hr"></div>
      <p class="brv6-sec">Загрузка фото (imgbb)</p>
      <div class="brv6-fg"><label>imgbb API ключ</label><input class="brv6-inp" id="e-imgkey" value="${escapeHtml(Store.get('brp_imgbb',''))}" placeholder="Ключ с imgbb.com → API" /></div>
      <button class="btn6g" id="e-savekey" style="margin-bottom:10px">💾 Сохранить ключ</button>
      <div class="brv6-upload-zone" id="e-drop">
        <div class="brv6-upload-ico">📷</div>
        <div class="brv6-upload-hint">Нажми или перетащи фото</div>
        <input type="file" id="e-file" accept="image/*" style="display:none">
      </div>
      <div class="brv6-photo-result" id="e-result">
        <div class="brv6-photo-url" id="e-url"></div>
        <div style="display:flex;gap:6px;flex-wrap:wrap">
          <button class="btn6" id="e-copy-url">📋 Ссылка</button>
          <button class="btn6g" id="e-copy-bb">🖼 BB-код</button>
          <button class="btn6grn" id="e-ins-ta">↑ В шаблон</button>
        </div>
      </div>
    `;

    document.getElementById('e-savekey').onclick = () => {
      Store.set('brp_imgbb', document.getElementById('e-imgkey').value.trim());
      toast('✅ Ключ сохранён');
    };

    const drop = document.getElementById('e-drop');
    drop.onclick = () => document.getElementById('e-file').click();
    drop.ondragover = e => { e.preventDefault(); drop.classList.add('drag'); };
    drop.ondragleave = () => drop.classList.remove('drag');
    drop.ondrop = e => { e.preventDefault(); drop.classList.remove('drag'); const f = e.dataTransfer.files[0]; if (f) handlePhoto(f, drop); };
    document.getElementById('e-file').onchange = e => { if (e.target.files[0]) handlePhoto(e.target.files[0], drop); };

    document.getElementById('e-copy-url').onclick = () => { navigator.clipboard.writeText(lastUploadedUrl).catch(()=>{}); toast('✅ Ссылка скопирована'); };
    document.getElementById('e-copy-bb').onclick = () => { navigator.clipboard.writeText(`[img]${lastUploadedUrl}[/img]`).catch(()=>{}); toast('✅ BB-код скопирован'); };
    document.getElementById('e-ins-ta').onclick = () => {
      const ta = document.getElementById('e-text');
      ta.value += `[CENTER][img]${lastUploadedUrl}[/img][/CENTER]\n\n`;
      ta.focus(); toast('✅ Вставлено');
    };
  }

  function pickColor(el, color) {
    document.querySelectorAll('.brv6-color-btn').forEach(x => x.classList.remove('on'));
    el.classList.add('on');
    const ta = document.getElementById('e-text');
    if (!ta) return;
    const pos = ta.selectionStart, end = ta.selectionEnd, sel = ta.value.substring(pos,end);
    if (sel) ta.value = ta.value.substring(0,pos) + `[COLOR=${color}]${sel}[/COLOR]` + ta.value.substring(end);
    else { ta.value += `[COLOR=${color}][/COLOR]`; ta.selectionStart = ta.selectionEnd = ta.value.length-8; }
    ta.focus();
  }

  function insertBBChip(idx) {
    const ta = document.getElementById('e-text');
    if (ta) { ta.value += BB_CHIPS[parseInt(idx)][1]; ta.focus(); }
  }

  function saveNewTemplate() {
    const n = document.getElementById('e-name').value.trim();
    const t = document.getElementById('e-text').value.trim();
    const p = parseInt(document.getElementById('e-pfx').value);
    const pin = document.getElementById('e-pin').checked;
    if (!n || !t) { toast('⚠️ Заполните название и текст'); return; }
    templates.push({ id: Date.now(), name: n, prefix: p, autopin: pin, content: t });
    saveTpls();
    addLog('Шаблон создан', n);
    toast(`✅ "${n}" сохранён!`);
    document.getElementById('e-name').value = '';
    document.getElementById('e-text').value = '';
  }

  function uploadPhoto(file, cb) {
    const key = Store.get('brp_imgbb', '');
    if (!key) { cb(null, 'Нет ключа imgbb'); return; }
    const fd = new FormData();
    fd.append('image', file);
    GM_xmlhttpRequest({
      method: 'POST',
      url: `https://api.imgbb.com/1/upload?key=${key}`,
      data: fd,
      onload: r => {
        try {
          const d = JSON.parse(r.responseText);
          if (d.success) cb(d.data.url, null); else cb(null, 'Ошибка imgbb: ' + (d.error?.message||'?'));
        } catch { cb(null, 'parse error'); }
      },
      onerror: () => cb(null, 'network error'),
    });
  }

  function handlePhoto(file, dropEl) {
    toast('📤 Загружаем фото...');
    const hint = dropEl.querySelector('.brv6-upload-hint');
    hint.textContent = '⏳ Загрузка...';
    uploadPhoto(file, (url, err) => {
      hint.textContent = 'Нажми или перетащи фото';
      if (err) { toast('❌ ' + err); return; }
      lastUploadedUrl = url;
      document.getElementById('e-url').textContent = url;
      document.getElementById('e-result').style.display = 'block';
      toast('✅ Фото загружено!');
    });
  }

  /* ══════════════════════════════════════════════════════════
     СТАРТ
  ══════════════════════════════════════════════════════════ */
  injectStyles();
  detectForumUserIfStale();

  if (!nickname) {
    buildLogin();
  } else {
    buildFab();
    buildPanel();
    startTracking();
  }

})();