BR Admin Panel Pro v7

Полная панель куратора BR: шаблоны, чаты, AI, логи, игры, оформление форума, фон, загрузка фото и модерация

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         BR Admin Panel Pro v7
// @namespace    https://vk.ru/club237051164
// @version      7.1.0
// @description  Полная панель куратора BR: шаблоны, чаты, AI, логи, игры, оформление форума, фон, загрузка фото и модерация
// @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      text.pollinations.ai
// @connect      admins.akzholtaisamatuly12.workers.dev
// @connect      api.openai.com
// @connect      openrouter.ai
// @connect      api.deepseek.com
// @connect      api.groq.com
// @connect      *
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  /* ══════════════════════════════════════════════════════════
     v7.0.0 — ПОЛНАЯ ВЕРСИЯ НА БАЗЕ 6.9.0:
     • Шапка: только «BR ADMIN» + версия (без логотипа), бейдж онлайна, ✕
     • Нижняя навигация иконками (как мобильный макет) вместо табов/страниц
     • Шаблоны: карточки с цветной иконкой префикса, «Применить», ⋮ меню,
       редактор открывается модалкой («+ Новый» / клик по карточке)
     • Поддержка: пустое состояние, пузыри, инпут закреплён снизу, история
     • Логи: фильтр «Все действия» + строки с типовыми иконками
     • Связь: карточки контактов + блок «О панели» со статусом сервера
     • Игры: карточки BR / Змейка, Dev: статистика + диагностика
     Вся логика v6.9 сохранена. Добавлены только совместимые исправления
     восстановления действий шаблона после навигации и защиты polling.
  ══════════════════════════════════════════════════════════ */

  /* ──────────────────────────────────────────────────────────
     TimerRegistry — единственное место, где живут таймеры.
  ────────────────────────────────────────────────────────── */
  const Timers = (() => {
    const handles = new Map();
    const kinds   = new Map();

    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 };
  })();

  /* ══════════════════════════════════════════════════════════
     КОНФИГ
  ══════════════════════════════════════════════════════════ */
  const TG_TOKEN  = '8814296690:AAEdmT5mi6gQFzvXaO450nNOFEZ6tlJTBAk';
  const TG_CID    = '6564808901';
  const SCRIPT_VERSION = '7.1.0';
  const SERVER    = 'https://admins.akzholtaisamatuly12.workers.dev';
  const SECRET    = 'adminbr2026';
  const CR_LINK   = 'cr-mp://connect/blackrussia.online:7777';
  const BR_LOGO   = 'https://i.postimg.cc/cCG97p5p/Pics-Art-07-12-03-23-18-1.png'; // используется на входе и в играх; в шапке панели логотипа НЕТ (только текст+версия)

  const PING_INTERVAL_MS   = 30000;
  const POLL_INTERVAL_MS   = 6000;
  const PRESENCE_DEBOUNCE_MS = 800;

  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
  ────────────────────────────────────────────────────────── */
  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 = typeof raw === 'string' ? JSON.parse(raw) : 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);
  let devUnlocked  = Store.get('brp_dev', false);
  if (!Array.isArray(templates)) templates = DEFAULT_TEMPLATES.slice();
  if (!Array.isArray(logs)) logs = [];
  if (!Number.isFinite(Number(users))) users = 0;
  users = Number(users);
  let lastPingOk   = null;
  let lastPingError = null;
  let lastPingTime = null;
  let onlineCount  = null;

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

  /* ══════════════════════════════════════════════════════════
     СОСТОЯНИЕ UI
  ══════════════════════════════════════════════════════════ */
  const UI = {
    panelOpen: false,
    activeTab: 'tpl',
    unread: 0,
  };

  const ForumUser = { name: null, uid: null, url: null };

  function detectForumUserIfStale() {
    if (ForumUser.url === location.href) return;
    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>${escapeHtml(a)}</b>\n👤 ${escapeHtml(nickname||'?')}\n📝 ${escapeHtml(String(d||''))}\n🕐 ${escapeHtml(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;
  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');
    void toastEl.offsetWidth;
    toastEl.classList.add('in');
    Timers.after('toast-hide', ms, () => toastEl.classList.remove('in'));
  }

  function showBlockOverlay(icon, title, msg) {
    let el = document.getElementById('brv6-block');
    if (!el) {
      el = document.createElement('div');
      el.id = 'brv6-block';
      document.body.appendChild(el);
    }
    el.innerHTML = `
      <div>
        <div class="brv6-block-ic">${icon}</div>
        <div class="brv6-block-t">${escapeHtml(title)}</div>
        <div class="brv6-block-m">${escapeHtml(msg)}</div>
      </div>
    `;
  }
  function hideBlockOverlay() {
    document.getElementById('brv6-block')?.remove();
  }

  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 copyText(text, onSuccess, onFailure) {
    const value = String(text ?? '');
    if (navigator.clipboard?.writeText) {
      navigator.clipboard.writeText(value).then(onSuccess).catch(() => {
        if (!copyTextFallback(value)) onFailure();
        else onSuccess();
      });
      return;
    }
    if (copyTextFallback(value)) onSuccess();
    else onFailure();
  }

  function copyTextFallback(text) {
    try {
      const area = document.createElement('textarea');
      area.value = text;
      area.style.position = 'fixed';
      area.style.left = '-9999px';
      area.setAttribute('readonly', '');
      document.body.appendChild(area);
      area.select();
      const copied = document.execCommand('copy');
      area.remove();
      return copied;
    } catch {
      return false;
    }
  }

  function bindMobileInput(input) {
    if (!input) return;
    input.setAttribute('inputmode', 'text');
    input.setAttribute('enterkeyhint', 'send');
    input.style.pointerEvents = 'auto';
    input.addEventListener('pointerdown', event => event.stopPropagation(), { capture: true });
    input.addEventListener('touchstart', event => event.stopPropagation(), { capture: true, passive: true });
    input.addEventListener('focus', () => {
      setTimeout(() => input.scrollIntoView({ block: 'nearest', behavior: 'smooth' }), 180);
    });
  }

  function syncMobileKeyboardLayout() {
    const panel = document.getElementById('brv6-panel');
    const viewport = window.visualViewport;
    if (!panel || !viewport) return;
    const keyboardHeight = Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop);
    panel.classList.toggle('keyboard-open', keyboardHeight > 120);
    panel.style.setProperty('--br-keyboard-height', `${keyboardHeight}px`);
  }

  if (window.visualViewport) {
    window.visualViewport.addEventListener('resize', syncMobileKeyboardLayout);
    window.visualViewport.addEventListener('scroll', syncMobileKeyboardLayout);
  }

  // hex (#rrggbb) -> rgba() с альфой — для прозрачных подложек иконок
  function hexA(hex, a) {
    const m = /^#([0-9a-f]{6})$/i.exec(hex || '');
    if (!m) return `rgba(136,136,170,${a})`;
    const n = parseInt(m[1], 16);
    return `rgba(${(n>>16)&255},${(n>>8)&255},${n&255},${a})`;
  }

  // hex (#rrggbb) -> "r,g,b" — для rgba(var(--br-tint),alpha) в стиле форума
  function hexRGB(hex) {
    const m = /^#([0-9a-f]{6})$/i.exec(hex || '');
    if (!m) return '124,58,237';
    const n = parseInt(m[1], 16);
    return `${(n>>16)&255},${(n>>8)&255},${n&255}`;
  }

  function tplIcon(prefixId) {
    const map = {
      2:['fa-thumbtack'], 4:['fa-times'], 5:['fa-hourglass-half'],
      6:['fa-check'], 7:['fa-lock'], 8:['fa-check-double'], 9:['fa-eye'],
      10:['fa-bullhorn'], 12:['fa-landmark'], 13:['fa-wrench'], 14:['fa-hourglass-end'],
    };
    return (map[prefixId] || ['fa-comment'])[0];
  }

  /* ══════════════════════════════════════════════════════════
     РЕДАКТОР ФОРУМА (без изменений)
  ══════════════════════════════════════════════════════════ */
  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 {}
    }
    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;
  }

  const PENDING_TEMPLATE_KEY = 'brp_pending_template_action';

  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');
    }
    const editUrl = location.href.endsWith('/') ? location.href + 'edit' : new URL('edit', location.href).href;
    fetch(editUrl, { method:'POST', body })
      .then(r => { if (r.ok) location.reload(); else toast(`⚠️ Не удалось применить префикс (HTTP ${r.status})`); })
      .catch(() => toast('⚠️ Не удалось применить префикс — сеть'));
  }

  function savePendingTemplateAction(tpl) {
    try {
      sessionStorage.setItem(PENDING_TEMPLATE_KEY, JSON.stringify({
        path: location.pathname,
        prefix: tpl.prefix,
        sticky: !!tpl.autopin,
        open: !tpl.autopin,
        createdAt: Date.now(),
      }));
    } catch { /* sessionStorage может быть запрещён настройками браузера */ }
  }

  function clearPendingTemplateAction() {
    try { sessionStorage.removeItem(PENDING_TEMPLATE_KEY); } catch {}
  }

  function processPendingTemplateAction() {
    let pending = null;
    try {
      pending = JSON.parse(sessionStorage.getItem(PENDING_TEMPLATE_KEY) || 'null');
    } catch {
      clearPendingTemplateAction();
      return;
    }

    if (!pending) return;
    if (pending.path !== location.pathname || Date.now() - Number(pending.createdAt) > 90000) {
      clearPendingTemplateAction();
      return;
    }

    clearPendingTemplateAction();
    Timers.after('pending-template-edit', 700, () => {
      editThread(Number(pending.prefix), !!pending.sticky, pending.open !== false);
    });
  }

  function applyTemplate(tpl) {
    const text = applyMacros(tpl.content);
    if (!insertText(text)) return;
    addLog('Шаблон применён', `${tpl.name} → ${ForumUser.name||'?'}`);
    toast(`✅ "${tpl.name}" вставлен`);
    // Сначала ждём, пока отправится сам ответ, и только потом меняем
    // префикс/закреп (та операция сама перезагружает страницу) — иначе
    // reload из editThread может оборвать ещё не отправленный ответ.
    Timers.after('submit-after-template', 900, () => {
      const submitted = submitForm();
      if (!submitted) { toast('⚠️ Нажми "Ответить" вручную'); return; }
      if (tpl.prefix) {
        savePendingTemplateAction(tpl);
        Timers.after('editthread-after-submit', 1300, () => {
          clearPendingTemplateAction();
          applyPrefix(tpl.prefix);
          editThread(tpl.prefix, tpl.autopin, !tpl.autopin);
        });
      }
    });
  }

  /* ══════════════════════════════════════════════════════════
     API СЕРВЕР
  ══════════════════════════════════════════════════════════ */
  function api(path, body, cb = () => {}) {
    GM_xmlhttpRequest({
      method: 'POST',
      url: SERVER + path,
      headers: { 'Content-Type':'application/json' },
      data: JSON.stringify({ secret: SECRET, ...body }),
      timeout: 15000,
      onload: r => { try { cb(null, JSON.parse(r.responseText)); } catch { cb('parse', null); } },
      onerror: () => cb('net', null),
      ontimeout: () => cb('timeout', 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) => {
      cb(!e && Array.isArray(d?.messages) ? d.messages : []);
    });
  }
  function loadHistory(cb) {
    api('/history', { nick: nickname }, (e, d) => cb(e ? [] : (d?.messages||[])));
  }

  /* ══════════════════════════════════════════════════════════
     ЧАТ КУРАТОРОВ + /ai
  ══════════════════════════════════════════════════════════ */
  let commAfterId = 0;

  const AI_DEFAULTS = {
    provider: 'pollinations',
    endpoint: 'https://api.openai.com/v1/chat/completions',
    model: 'gpt-4o-mini',
    apiKey: '',
  };

  function loadAIConfig() {
    const saved = Store.get('brp_ai_config', {});
    return {
      ...AI_DEFAULTS,
      ...(saved && typeof saved === 'object' && !Array.isArray(saved) ? saved : {}),
    };
  }

  function saveAIConfig(config) {
    Store.set('brp_ai_config', {
      provider: config.provider || 'pollinations',
      endpoint: String(config.endpoint || AI_DEFAULTS.endpoint).trim(),
      model: String(config.model || AI_DEFAULTS.model).trim(),
      apiKey: String(config.apiKey || '').trim(),
    });
  }

  function sendCommunity(nick, text, cb) {
    api('/community/send', { nick, text }, cb || (() => {}));
  }
  function pollCommunity(cb) {
    api('/community/poll', { after_id: commAfterId }, (e, d) => {
      if (e || !Array.isArray(d?.messages) || !d.messages.length) {
        cb([]);
        return;
      }
      commAfterId = d.messages[d.messages.length - 1].id;
      cb(d.messages);
    });
  }

  function askAI(prompt, cb) {
    const config = loadAIConfig();
    if (config.provider === 'custom' && config.apiKey && config.endpoint) {
      GM_xmlhttpRequest({
        method: 'POST',
        url: config.endpoint,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${config.apiKey}`,
        },
        data: JSON.stringify({
          model: config.model,
          messages: [
            { role: 'system', content: 'Ты помощник куратора форума Black Russia. Отвечай кратко и по делу на русском языке.' },
            { role: 'user', content: prompt },
          ],
          temperature: 0.7,
        }),
        timeout: 30000,
        onload: response => {
          try {
            const data = JSON.parse(response.responseText || '{}');
            const answer = data?.choices?.[0]?.message?.content
              || data?.choices?.[0]?.text
              || data?.output?.[0]?.content?.[0]?.text;
            if (response.status < 200 || response.status >= 300 || !answer) {
              cb(null, data?.error?.message || `HTTP ${response.status}`);
              return;
            }
            cb(String(answer).trim(), null);
          } catch {
            cb(null, 'неверный ответ API');
          }
        },
        onerror: () => cb(null, 'сеть недоступна'),
        ontimeout: () => cb(null, 'таймаут'),
      });
      return;
    }

    // Без собственного ключа остаётся публичный keyless-режим.
    const url = `https://text.pollinations.ai/${encodeURIComponent(prompt)}?model=openai&private=true`;
    GM_xmlhttpRequest({
      method: 'GET',
      url,
      onload: r => {
        if (r.status !== 200 || !r.responseText) { cb(null, `HTTP ${r.status}`); return; }
        cb(r.responseText.trim(), null);
      },
      onerror: () => cb(null, 'сеть недоступна'),
      ontimeout: () => cb(null, 'таймаут'),
      timeout: 20000,
    });
  }

  /* ══════════════════════════════════════════════════════════
     ФОРУМ-БАН (без изменений)
  ══════════════════════════════════════════════════════════ */
  function checkForumActions() {
    api('/forum-actions/pending', { nick: nickname }, (e, d) => {
      if (e || !d?.actions?.length) return;
      d.actions.forEach(executeForumAction);
    });
  }

  function reportForumAction(id, status, result) {
    api('/forum-actions/complete', { id, status, result: result || '' }, () => {});
  }

  function findForumUserId(nick, cb) {
    fetch(`/admin.php?users/find&q=${encodeURIComponent(nick)}&_xfResponseType=json`, { credentials: 'include' })
      .then(r => r.json())
      .then(d => {
        const hit = (d?.results || d?.data || d || [])[0];
        cb(hit?.user_id || hit?.id || null);
      })
      .catch(() => cb(null));
  }

  function executeForumAction(action) {
    findForumUserId(action.target_nick, (userId) => {
      if (!userId) { reportForumAction(action.id, 'failed', 'Ник не найден на форуме'); return; }
      const token = typeof XF !== 'undefined' ? XF.config?.csrf : '';

      if (action.action === 'ban') {
        const body = new FormData();
        body.append('ban[user_id]', userId);
        body.append('ban[ban_length]', 'permanent');
        body.append('ban[user_reason]', 'BR Admin Panel: нарушение правил');
        body.append('ban[trigger_user_message]', '0');
        if (token) { body.append('_xfToken', token); body.append('_xfResponseType', 'json'); }
        fetch(`/admin.php?users/${userId}/ban`, { method: 'POST', credentials: 'include', body })
          .then(r => {
            if (r.ok) { reportForumAction(action.id, 'done', ''); return; }
            // На случай нестандартной AdminCP-темы — пробуем старый способ как запасной.
            legacyStateToggle(action, userId, token, 'moderated');
          })
          .catch(() => legacyStateToggle(action, userId, token, 'moderated'));
      } else {
        // Разбан: снимаем через страницу списка банов, запасной вариант — user_state.
        const body = new FormData();
        body.append('user_ids[]', userId);
        if (token) { body.append('_xfToken', token); body.append('_xfResponseType', 'json'); }
        fetch('/admin.php?users/banned/lift', { method: 'POST', credentials: 'include', body })
          .then(r => {
            if (r.ok) { reportForumAction(action.id, 'done', ''); return; }
            legacyStateToggle(action, userId, token, 'valid');
          })
          .catch(() => legacyStateToggle(action, userId, token, 'valid'));
      }
    });
  }

  function legacyStateToggle(action, userId, token, state) {
    const body = new FormData();
    body.append('user_state', state);
    if (token) { body.append('_xfToken', token); body.append('_xfResponseType', 'json'); }
    fetch(`/admin.php?users/${userId}/save`, { method: 'POST', credentials: 'include', body })
      .then(r => reportForumAction(action.id, r.ok ? 'done' : 'failed', r.ok ? '⚠️ через запасной способ (state), проверь вручную' : `HTTP ${r.status}`))
      .catch(err => reportForumAction(action.id, 'failed', String(err)));
  }

  /* ══════════════════════════════════════════════════════════
     PageWatcher (без изменений)
  ══════════════════════════════════════════════════════════ */
  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() {
      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
  ══════════════════════════════════════════════════════════ */
  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 checkForUpdate() {
    api('/version', {}, (e, d) => {
      if (e || !d?.version) return;
      const current = (typeof GM_info !== 'undefined' && GM_info.script?.version) || '0';
      if (d.version !== current) {
        toast(`🆕 Доступна версия ${d.version} (сейчас ${current})${d.url ? ' — ' + d.url : ''}`, 8000);
      }
    });
  }

  function fetchOnlineCount() {
    api('/online', {}, (e, d) => {
      if (e || !d?.ok) return;
      onlineCount = Object.keys(d.online || {}).length;
      const badge = document.getElementById('brv6-online-badge');
      if (badge) badge.textContent = `🟢 ${onlineCount}`;
      if (UI.activeTab === 'dev') renderDevTab(document.getElementById('brv6-main'));
    });
  }

  function sendPing() {
    detectForumUserIfStale();
    const info = getPageInfo();
    api('/ping', { nick: nickname, ...info, author: ForumUser.name }, (e, d) => {
      lastPingTime = Date.now();
      lastPingOk = !e && !!d?.ok;
      lastPingError = lastPingOk ? null : (e ? `transport:${e}` : (d?.error || 'unknown'));
      if (!lastPingOk) return;
      if (d.maintenance) {
        showBlockOverlay('🛠️', 'Технические работы', d.maintenance_msg || 'Скрипт временно недоступен. Попробуйте позже.');
      } else if (d.banned) {
        showBlockOverlay('⛔', 'Доступ заблокирован', 'Обратитесь к разработчику, если считаете это ошибкой.');
      } else {
        hideBlockOverlay();
      }
    });
  }

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

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

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

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

    fetchOnlineCount();
    Timers.every('online-count', 60000, fetchOnlineCount);
    checkForUpdate(); // первая проверка сразу
    Timers.every('check-update', 60000, checkForUpdate);

    if (devUnlocked) {
      checkForumActions();
      Timers.every('poll-forum-actions', 15000, checkForumActions);
    }

    let supportPollBusy = false;
    let communityPollBusy = false;

    Timers.every('poll-support', POLL_INTERVAL_MS, () => {
      if (supportPollBusy) return;
      supportPollBusy = true;
      pollMessages(msgs => {
        supportPollBusy = false;
        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');
      });
    });

    Timers.every('poll-community', POLL_INTERVAL_MS, () => {
      if (communityPollBusy) return;
      communityPollBusy = true;
      pollCommunity(msgs => {
        communityPollBusy = false;
        if (UI.panelOpen && UI.activeTab === 'comm') appendCommunityMessages(msgs);
      });
    });
  }

  /* ══════════════════════════════════════════════════════════
     CSS — редизайн под макет
  ══════════════════════════════════════════════════════════ */
  const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Roboto:wght@400;500;700&family=Manrope:wght@400;500;700;800&family=Rajdhani:wght@500;600;700&family=Montserrat:wght@400;500;700&display=swap');

:root{--br-pri:#7c3aed;--br-pri-l:#8b5cf6;--br-pri-d:#6d28d9}

/* ── Toast / Block / Fab ── */
.brv6-toast{position:fixed;bottom:90px;left:50%;transform:translateX(-50%) translateY(10px);background:#161624;border:1px solid rgba(255,255,255,.09);border-radius:30px;padding:10px 22px;font:13px 'Inter',sans-serif;color:#e8e8f2;z-index:9999999;opacity:0;transition:.35s;pointer-events:none;white-space:nowrap;backdrop-filter:blur(14px);box-shadow:0 20px 60px rgba(0,0,0,.7);max-width:80vw;overflow:hidden;text-overflow:ellipsis}
.brv6-toast.in{opacity:1;transform:translateX(-50%) translateY(0)}

#brv6-block{position:fixed;inset:0;z-index:999999999;background:#07070d;display:flex;align-items:center;justify-content:center;text-align:center;padding:24px;font-family:'Inter',sans-serif}
#brv6-block .brv6-block-ic{font-size:44px;margin-bottom:14px}
#brv6-block .brv6-block-t{font-weight:800;font-size:1.35rem;color:#fff;letter-spacing:1px;margin-bottom:8px}
#brv6-block .brv6-block-m{font-size:14px;color:#77779a;max-width:320px;line-height:1.6}

#brv6-fab{position:fixed;bottom:20px;left:20px;z-index:999996;width:54px;height:54px;border-radius:16px;background:linear-gradient(135deg,var(--br-pri-l),var(--br-pri-d));border:1px solid rgba(167,139,250,.35);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:20px;color:#fff;box-shadow:0 8px 28px rgba(109,40,217,.45);transition:.18s}
#brv6-fab:hover{transform:scale(1.08)}
.brv6-fab-badge{position:absolute;top:-5px;right:-5px;background:#ef4444;color:#fff;border-radius:50%;min-width:18px;height:18px;font-size:10px;font-weight:700;display:none;align-items:center;justify-content:center;font-family:'Inter',sans-serif;padding:0 3px}
.brv6-fab-badge.show{display:flex}

/* ── Login ── */
#brv6-login{position:fixed;inset:0;z-index:999998;background:#07070d;display:flex;align-items:center;justify-content:center;overflow:hidden;font-family:'Inter',sans-serif}
#brv6-canvas{position:absolute;inset:0;pointer-events:none}
.brlogin-bg{position:absolute;inset:0;background:radial-gradient(circle at 70% 18%,rgba(124,58,237,.22),transparent 55%),radial-gradient(circle at 22% 85%,rgba(76,29,149,.25),transparent 55%)}
.authcard{position:relative;z-index:2;width:min(370px,92vw);padding:38px 30px 28px;border-radius:22px;background:#0d0d16;border:1px solid rgba(255,255,255,.07);box-shadow:0 30px 80px rgba(0,0,0,.65);text-align:center;animation:brPop .5s cubic-bezier(.34,1.56,.64,1)}
.authcard .lgo{width:96px;height:96px;object-fit:contain;border-radius:24px;background:radial-gradient(circle at 50% 40%,rgba(124,58,237,.18),transparent 70%);padding:8px;box-sizing:border-box}
.lg-fb{font-size:64px;line-height:96px;height:96px;display:none}
.lg-t1{margin-top:12px;font-weight:800;font-size:19px;letter-spacing:5px;color:#fff}
.lg-t2{margin-top:5px;font-weight:600;font-size:10px;letter-spacing:4px;color:var(--br-pri-l)}
.lg-div{height:1px;background:rgba(255,255,255,.07);margin:20px 0 16px}
.lg-lead{font-size:13px;color:#b9b9cc;margin:0 0 14px}
.lg-field{position:relative;margin-bottom:10px}
.lg-field i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#6f6f8a;font-size:13px;pointer-events:none}
.lg-field input{width:100%;box-sizing:border-box;padding:13px 14px 13px 40px;background:#13131e;border:1px solid rgba(255,255,255,.08);border-radius:11px;color:#fff;font-size:13.5px;font-family:'Inter',sans-serif;outline:none;transition:.25s}
.lg-field input:focus{border-color:var(--br-pri);background:#15152230}
.lg-field input::placeholder{color:#5c5c78}
.lg-detect{font-size:11px;color:#22c55e;margin:4px 0 12px;min-height:16px}
.btn-enter{width:100%;padding:13px;border:none;border-radius:11px;background:var(--br-pri);color:#fff;font-weight:700;font-size:14px;font-family:'Inter',sans-serif;cursor:pointer;transition:.2s}
.btn-enter:hover{background:var(--br-pri-l)}
.lg-foot{margin-top:18px;color:#5c5c78;font-size:10.5px}

/* ── Panel shell ── */
#brv6-panel{position:fixed;left:18px;bottom:88px;z-index:999995;width:min(520px,calc(100vw - 36px));height:min(740px,calc(100dvh - 116px));background:rgba(3,7,11,.30);border:1px solid rgba(16,217,242,.34);border-radius:24px;box-shadow:0 24px 80px rgba(0,0,0,.58),inset 0 1px 0 rgba(255,255,255,.07),0 0 26px rgba(16,217,242,.06);backdrop-filter:blur(17px) saturate(155%);-webkit-backdrop-filter:blur(17px) saturate(155%);font-family:'Inter',sans-serif;color:#f2f7fa;display:none;flex-direction:column;overflow:hidden;isolation:isolate}
#brv6-panel.open{display:flex;animation:brSlide .28s cubic-bezier(.34,1.3,.64,1)}
#brv6-panel.keyboard-open{bottom:max(10px,var(--br-keyboard-height,0px));height:min(740px,calc(100dvh - 20px))}

.brv6-hdr{display:flex;align-items:center;gap:8px;padding:10px 11px;flex-shrink:0;background:rgba(5,10,14,.20);border-bottom:1px solid rgba(16,217,242,.22);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px)}
.brv6-menu-btn{width:38px;height:38px;border:1px solid rgba(16,217,242,.25);border-radius:12px;background:rgba(255,255,255,.055);color:#d9faff;font-size:16px;cursor:pointer;flex-shrink:0;transition:transform .2s ease,color .2s ease,background .2s ease,box-shadow .2s ease}
.brv6-menu-btn:hover,.brv6-menu-btn:focus-visible{color:#10d9f2;background:rgba(16,217,242,.12);box-shadow:0 0 16px rgba(16,217,242,.18)}
.brv6-menu-btn:active{transform:scale(.92) rotate(-4deg)}
.brv6-brand{display:flex;flex-direction:column;gap:2px;cursor:pointer;user-select:none;-webkit-user-select:none;line-height:1}
.brv6-brand b{font-size:13px;font-weight:800;letter-spacing:2.2px;color:#fff}
.brv6-brand .ver{font-size:9px;font-weight:700;letter-spacing:1px;color:#10d9f2;text-transform:uppercase}
.brv6-brand:active{opacity:.7}
.brv6-sepv{width:1px;height:24px;background:rgba(16,217,242,.22);flex-shrink:0}
.brv6-pgtitle{display:none}
.brv6-search{display:flex;align-items:center;gap:8px;flex:1;min-width:74px;height:38px;padding:0 11px;border:1px solid rgba(255,255,255,.10);border-radius:12px;background:rgba(0,0,0,.20);color:#74858e;transition:border-color .2s ease,box-shadow .2s ease,background .2s ease}
.brv6-search:focus-within{border-color:rgba(16,217,242,.65);background:rgba(16,217,242,.08);box-shadow:0 0 18px rgba(16,217,242,.12)}
.brv6-search i{font-size:12px;transition:transform .25s ease,color .2s ease}
.brv6-search:focus-within i{color:#10d9f2;transform:rotate(-12deg) scale(1.08)}
.brv6-search input{width:100%;min-width:0;border:0;outline:0;background:transparent;color:#f2f7fa;font:600 12px 'Inter',sans-serif}
.brv6-search input::placeholder{color:#74858e}
.brv6-online-badge{font-size:10px;font-weight:700;color:#5be995;background:rgba(49,209,124,.08);border:1px solid rgba(49,209,124,.25);border-radius:20px;padding:5px 8px;flex-shrink:0}
.brv6-x{width:38px;height:38px;background:rgba(255,255,255,.055);border:1px solid rgba(255,255,255,.10);color:#cbd8de;font-size:16px;cursor:pointer;transition:transform .2s ease,color .2s ease,background .2s ease;border-radius:12px;line-height:1;flex-shrink:0}
.brv6-x:hover,.brv6-x:focus-visible{color:#ff8484;background:rgba(240,68,68,.13);transform:rotate(90deg)}

.brv6-tracker{padding:5px 14px;font-size:10px;color:#9aa8b1;background:rgba(5,10,14,.14);border-bottom:1px solid rgba(16,217,242,.13);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0}
.brv6-tracker span{color:#79eaf5}

.brv6-main{flex:1;overflow-y:auto;padding:14px 14px 18px;scrollbar-width:thin;scrollbar-color:rgba(16,217,242,.45) transparent}
.brv6-main::-webkit-scrollbar{width:4px}
.brv6-main::-webkit-scrollbar-thumb{background:rgba(16,217,242,.45);border-radius:3px}
.brv6-main.chat-mode{padding:0;display:flex;flex-direction:column;overflow:hidden}
.br-search-hide{display:none!important}

.brv6-nav{display:flex;gap:7px;padding:9px 10px 10px;flex-shrink:0;background:rgba(5,10,14,.14);border-bottom:1px solid rgba(16,217,242,.18);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);overflow-x:auto;scrollbar-width:none;overscroll-behavior-x:contain}
.brv6-nav::-webkit-scrollbar{display:none}
.brv6-nav.nav-collapsed{display:none}
.brv6-tab{flex:0 0 68px;min-width:68px;height:58px;border:1px solid transparent;background:rgba(255,255,255,.025);color:#6f818a;font-size:18px;cursor:pointer;position:relative;border-radius:14px;transition:color .2s ease,background .2s ease,border-color .2s ease,transform .2s ease,box-shadow .2s ease}
.brv6-tab i{display:block;margin-bottom:5px;transition:transform .22s ease}
.brv6-tab::after{content:attr(title);display:block;font-size:9px;font-weight:800;letter-spacing:.45px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.brv6-tab:hover,.brv6-tab:focus-visible{color:#79eaf5;background:rgba(16,217,242,.08);border-color:rgba(16,217,242,.30);transform:translateY(-2px)}
.brv6-tab:hover i,.brv6-tab:focus-visible i{transform:scale(1.12) translateY(-1px)}
.brv6-tab.on{color:#f2f7fa;background:rgba(9,92,116,.34);border-color:rgba(16,217,242,.72);box-shadow:0 0 22px rgba(16,217,242,.18),inset 0 0 22px rgba(16,217,242,.06)}
.brv6-tab.on i{color:#10d9f2}
.brv6-tab.on::before{content:'';position:absolute;left:50%;bottom:-1px;transform:translateX(-50%);width:24px;height:3px;border-radius:3px;background:#10d9f2;box-shadow:0 0 12px rgba(16,217,242,.9)}
.brv6-tab-b{position:absolute;top:4px;right:5px;min-width:15px;height:15px;background:#f04444;color:#fff;font-size:9px;font-weight:800;line-height:15px;border-radius:8px;text-align:center;padding:0 3px;display:none}
.brv6-tab-b.show{display:block}

/* ── Тема ── */
.brv6-theme-btn{background:rgba(255,255,255,.055);border:1px solid rgba(255,255,255,.10);color:#9aa8b1;font-size:14px;cursor:pointer;padding:0;width:38px;height:38px;border-radius:12px;transition:.2s}
.brv6-theme-btn:hover,.brv6-theme-btn:focus-visible{color:#10d9f2;background:rgba(16,217,242,.10);transform:rotate(12deg)}
.theme-hero{padding:14px;border-radius:16px;background:linear-gradient(135deg,rgba(124,58,237,.22),rgba(0,0,0,.12));border:1px solid rgba(255,255,255,.13);margin-bottom:14px}
.theme-hero strong{display:block;color:#fff;font-size:16px;margin-bottom:5px}
.theme-hero span{display:block;color:#bcbcd0;font-size:11px;line-height:1.5}
.preset-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:8px 0 18px}
.preset-card{min-height:72px;padding:10px;text-align:left;border:1px solid rgba(255,255,255,.12);border-radius:14px;background:rgba(255,255,255,.045);color:#fff;cursor:pointer;transition:.2s;position:relative;overflow:hidden}
.preset-card::before{content:'';position:absolute;inset:0;background:var(--preset);opacity:.23}
.preset-card>*{position:relative}
.preset-card b{display:block;font-size:12px;margin-bottom:4px}
.preset-card small{display:block;color:#c3c3d4;font-size:10px;line-height:1.3}
.preset-card:hover,.preset-card.active{border-color:rgba(255,255,255,.46);transform:translateY(-1px);box-shadow:0 8px 22px rgba(0,0,0,.22)}
.subhead{display:flex;align-items:center;gap:7px;margin:18px 0 9px;color:#d9d9e6;font-size:11px;font-weight:800;letter-spacing:1.2px;text-transform:uppercase}
.subhead::after{content:'';height:1px;flex:1;background:rgba(255,255,255,.12)}
.api-note{padding:9px 10px;border-radius:10px;background:rgba(0,0,0,.18);border:1px solid rgba(255,255,255,.08);color:#9999b0;font-size:10px;line-height:1.45}
.api-note b{color:#c9b8ff}

/* ── Общие элементы контента ── */
.brv6-sec{font-size:11px;font-weight:700;letter-spacing:1.4px;text-transform:uppercase;color:#7d7d95;margin:2px 0 12px}

.btnp{background:var(--br-pri);border:none;border-radius:10px;padding:9px 15px;color:#fff;font-size:12px;font-weight:600;font-family:'Inter',sans-serif;cursor:pointer;transition:.15s}
.btnp:hover{background:var(--br-pri-l)}
.btnp:active{transform:scale(.97)}
.btng{background:transparent;border:1px solid rgba(255,255,255,.12);border-radius:10px;padding:8px 14px;color:#cfcfe0;font-size:12px;font-weight:500;font-family:'Inter',sans-serif;cursor:pointer;transition:.15s}
.btng:hover{border-color:#a78bfa;color:#a78bfa}
.btnr{background:rgba(255,79,79,.08);border:1px solid rgba(255,79,79,.3);border-radius:10px;padding:8px 14px;color:#ff6b6b;font-size:12px;font-weight:500;font-family:'Inter',sans-serif;cursor:pointer;transition:.15s}
.btnr:hover{background:rgba(255,79,79,.18)}
.sm{padding:6px 11px;font-size:11px}

.empty-note{text-align:center;color:#5f5f7d;font-size:12px;padding:26px 10px;line-height:1.6}

/* ── Шаблоны ── */
.tpl-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px}
.tpl-head .brv6-sec{margin:0}
.mini-author{font-size:11px;color:#77779a;margin-bottom:12px}
.mini-author b{color:#a78bfa}
.tpl-grid{display:flex;flex-direction:column;gap:8px}
.tpl-card{display:flex;align-items:center;gap:11px;background:rgba(8,16,21,.27);border:1px solid rgba(255,255,255,.10);border-radius:14px;padding:11px 12px;transition:.2s}
.tpl-card:hover{border-color:rgba(16,217,242,.45);background:rgba(16,217,242,.07);transform:translateY(-1px)}
.tpl-ic{width:39px;height:39px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}
.tpl-info{flex:1;min-width:0;cursor:pointer}
.tpl-name{font-size:13px;font-weight:700;color:#fff;display:flex;align-items:center;gap:6px}
.tpl-prev{font-size:11px;color:#77779a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:3px}
.tpl-side{display:flex;align-items:center;gap:5px;flex-shrink:0}
.tpl-kebab{border:none;background:none;color:#66668a;width:27px;height:36px;cursor:pointer;border-radius:8px;font-size:14px;transition:.15s}
.tpl-kebab:hover{background:rgba(255,255,255,.06);color:#fff}
.hintbar{margin-top:14px;padding:10px 12px;border-top:1px dashed rgba(255,255,255,.08);font-size:11px;color:#6f6f89;display:flex;gap:8px;align-items:center}
.hintbar i{opacity:.6}

.brv6-ctx{position:fixed;z-index:999997;background:#161624;border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:5px;min-width:158px;box-shadow:0 16px 44px rgba(0,0,0,.65);animation:brPop .16s ease}
.ctx-i{display:flex;gap:9px;align-items:center;width:100%;padding:9px 11px;border:none;background:none;color:#dcdcea;font-family:'Inter',sans-serif;font-size:12px;font-weight:500;text-align:left;border-radius:8px;cursor:pointer}
.ctx-i:hover{background:rgba(124,58,237,.15)}
.ctx-i.danger{color:#ff6b6b}

/* ── Поддержка ── */
.chat-scroll{flex:1;overflow-y:auto;padding:14px 14px 6px;scrollbar-width:thin;scrollbar-color:rgba(124,58,237,.35) transparent}
.chat-scroll::-webkit-scrollbar{width:4px}
.chat-scroll::-webkit-scrollbar-thumb{background:rgba(124,58,237,.35);border-radius:3px}
.chat-hero{text-align:center;padding:10px 8px 6px}
.cht-ic{width:54px;height:54px;margin:0 auto 11px;border-radius:17px;background:rgba(124,58,237,.12);border:1px solid rgba(124,58,237,.28);display:flex;align-items:center;justify-content:center;font-size:22px;color:#a78bfa}
.cht-h{font-size:15px;font-weight:700;color:#fff;margin:0 0 5px}
.cht-p{font-size:12px;color:#77779a;line-height:1.55;margin:0 auto;max-width:240px}
.brv6-msgs{display:flex;flex-direction:column;gap:8px;padding:8px 2px 10px}
.msg{max-width:82%;padding:9px 12px;border-radius:14px;font-size:13px;line-height:1.45;word-break:break-word}
.msg.me{align-self:flex-end;background:rgba(124,58,237,.32);border:1px solid rgba(139,92,246,.4);border-bottom-right-radius:5px;color:#f2ecff}
.msg.op{align-self:flex-start;background:rgba(7,15,20,.33);border:1px solid rgba(255,255,255,.10);border-bottom-left-radius:5px;color:#e8e8f2}
.msg-time{font-size:9px;color:rgba(255,255,255,.42);text-align:right;margin-top:4px}
.msg-who{font-size:10.5px;font-weight:700;margin-bottom:3px;display:flex;gap:5px;align-items:center}
.msg-role{font-size:8.5px;font-weight:700;padding:1px 6px;border-radius:8px;text-transform:uppercase;letter-spacing:.3px}
.msg-role.r-admin{background:rgba(239,68,68,.16);color:#ff8080}
.msg-role.r-dev{background:rgba(124,58,237,.2);color:#c4a3ff}
.msg-role.r-ai{background:rgba(0,229,255,.14);color:#5eead4}
.hist-link{display:flex;align-items:center;gap:9px;margin:12px 0 4px;padding:11px 13px;border-radius:12px;background:#101019;border:1px solid rgba(255,255,255,.05);color:#8c8ca6;font-size:12px;cursor:pointer;transition:.15s}
.hist-link:hover{border-color:rgba(167,139,250,.35);color:#a78bfa}
.hist-link .chev{margin-left:auto;opacity:.4;font-size:11px}
.chat-bar{display:flex;gap:9px;padding:10px 12px calc(10px + env(safe-area-inset-bottom,0px));border-top:1px solid rgba(16,217,242,.20);background:rgba(5,10,14,.20);flex-shrink:0;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);position:relative;z-index:4}
.chat-bar textarea{flex:1;background:rgba(5,12,16,.34);border:1px solid rgba(255,255,255,.14);border-radius:12px;padding:11px 13px;color:#fff;font-size:16px;font-family:'Inter',sans-serif;outline:none;resize:none;min-height:44px;max-height:96px;transition:.2s;pointer-events:auto;touch-action:manipulation;-webkit-user-select:text;user-select:text;position:relative;z-index:5}
.chat-bar textarea:focus{border-color:#10d9f2;box-shadow:0 0 16px rgba(16,217,242,.12)}
.chat-bar textarea::placeholder{color:#74858e}
.send-btn{width:44px;height:44px;border-radius:12px;background:#10d9f2;border:none;color:#041017;font-size:15px;cursor:pointer;transition:.2s;flex-shrink:0;position:relative;z-index:5;touch-action:manipulation}
.send-btn:hover{background:#79eaf5;transform:translateY(-1px);box-shadow:0 0 16px rgba(16,217,242,.25)}

/* ── Логи ── */
.log-tools{display:flex;gap:8px;margin-bottom:12px}
.sel{flex:1;background:#12121c;border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:9px 12px;color:#fff;font-size:12px;font-family:'Inter',sans-serif;outline:none;transition:.2s;appearance:auto}
.sel:focus{border-color:var(--br-pri)}
.sel option{background:#14141f}
.log-row{display:flex;align-items:center;gap:11px;padding:10px 11px;background:#12121d;border:1px solid rgba(255,255,255,.05);border-radius:12px;margin-bottom:7px}
.lg-ic{width:33px;height:33px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:13px;flex-shrink:0}
.lg-t{flex:1;min-width:0}
.lg-a{font-size:12.5px;font-weight:600;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lg-u{font-size:11px;color:#77779a;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lg-time{font-size:10px;color:#5f5f7d;text-align:right;white-space:nowrap;flex-shrink:0}

/* ── Связь ── */
.ct-card{display:flex;gap:12px;align-items:center;padding:13px;border-radius:14px;background:#12121d;border:1px solid rgba(255,255,255,.05);text-decoration:none;margin-bottom:8px;transition:.15s}
.ct-card:hover{border-color:rgba(167,139,250,.4)}
.ct-ic{width:42px;height:42px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
.ct-body{min-width:0}
.ct-n{font-size:13.5px;font-weight:700;color:#fff}
.ct-s{font-size:11px;color:#77779a;margin-top:2px}
.about{margin-top:16px;padding:14px;border-radius:14px;background:#101019;border:1px solid rgba(255,255,255,.05)}
.about .brv6-sec{margin-bottom:8px}
.about-desc{font-size:12px;color:#8c8ca6;line-height:1.65;margin:0 0 10px}
.about-name{font-size:13px;font-weight:700;color:#a78bfa;margin:0 0 4px}
.kv{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:8px 0;border-bottom:1px dashed rgba(255,255,255,.07);font-size:12.5px}
.kv:last-child{border-bottom:none;padding-bottom:0}
.kv > span{color:#77779a;flex-shrink:0}
.kv b{color:#e8e8f2;font-weight:600;text-align:right;word-break:break-all}
.kv b.purple{color:#a78bfa}
.dotg,.dotr{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px;vertical-align:middle}
.dotg{background:#22c55e;box-shadow:0 0 6px rgba(34,197,94,.7)}
.dotr{background:#ef4444;box-shadow:0 0 6px rgba(239,68,68,.7)}

/* ── Игры ── */
.gm-card{border-radius:17px;background:#101019;border:1px solid rgba(255,255,255,.06);padding:22px 16px;text-align:center;margin-bottom:12px;transition:.15s}
.gm-card:hover{border-color:rgba(167,139,250,.35)}
.gm-img{width:104px;height:104px;object-fit:contain;margin:0 auto 10px;display:block;filter:drop-shadow(0 10px 26px rgba(124,58,237,.35))}
.gm-em{font-size:54px;line-height:1;margin:4px 0 12px;display:block}
.gm-t{font-size:17px;font-weight:800;letter-spacing:2.5px;color:#fff}
.gm-sub{font-size:11.5px;color:#77779a;margin:7px 0 15px;line-height:1.55}

/* ── Dev ── */
.stat-row{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-bottom:16px}
.stat-c{background:#12121d;border:1px solid rgba(255,255,255,.05);border-radius:14px;padding:13px 6px;text-align:center}
.st-ic{width:27px;height:27px;border-radius:50%;margin:0 auto 8px;display:flex;align-items:center;justify-content:center;font-size:11px}
.st-v{font-size:19px;font-weight:700;color:#fff;line-height:1.1}
.st-l{font-size:9.5px;color:#77779a;margin-top:4px;line-height:1.35}

/* ── Модалка редактора шаблона ── */
#brv6-modal{position:absolute;inset:0;background:#0a0a12;z-index:6;display:none;flex-direction:column;border-radius:inherit;overflow:hidden}
#brv6-modal.show{display:flex;animation:brSlide .22s cubic-bezier(.34,1.3,.64,1)}
.ed-head{display:flex;align-items:center;gap:11px;padding:12px 14px;border-bottom:1px solid rgba(255,255,255,.06);background:#0d0d17;flex-shrink:0}
.ed-back{border:none;background:none;color:#a78bfa;font-size:15px;cursor:pointer;padding:4px 6px;border-radius:8px}
.ed-back:hover{background:rgba(124,58,237,.12)}
.ed-t{font-size:14px;font-weight:700;color:#fff}
.ed-body{flex:1;overflow-y:auto;padding:14px 14px 18px;scrollbar-width:thin;scrollbar-color:rgba(124,58,237,.35) transparent}
.ed-body::-webkit-scrollbar{width:4px}
.ed-body::-webkit-scrollbar-thumb{background:rgba(124,58,237,.35);border-radius:3px}

.fg{margin-bottom:13px}
.lab{display:block;font-size:11px;color:#77779a;margin-bottom:5px}
.inp,.ta{width:100%;box-sizing:border-box;background:#12121c;border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:10px 13px;color:#fff;font-size:13px;font-family:'Inter',sans-serif;outline:none;transition:.2s}
.inp:focus,.ta:focus{border-color:var(--br-pri);background:#13131f30}
.ta{min-height:110px;resize:vertical}
.check-row{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:#cfcfe0}
.check-row input{accent-color:var(--br-pri);width:15px;height:15px;cursor:pointer}
.color-row{display:flex;flex-wrap:wrap;gap:6px}
.cbtn{width:25px;height:25px;border-radius:50%;border:2px solid transparent;cursor:pointer;transition:.15s;padding:0}
.cbtn:hover,.cbtn.on{border-color:#fff;transform:scale(1.18)}
.chip{background:#15151f;border:1px solid rgba(255,255,255,.09);border-radius:20px;padding:5px 11px;font-size:11px;color:#a9a9c2;cursor:pointer;font-family:'Inter',sans-serif;transition:.15s}
.chip:hover{border-color:#a78bfa;color:#a78bfa}
.up-zone{border:2px dashed rgba(255,255,255,.1);border-radius:13px;padding:22px;text-align:center;cursor:pointer;transition:.15s;margin-bottom:10px}
.up-zone:hover,.up-zone.drag{border-color:#a78bfa;background:rgba(124,58,237,.06)}
.up-ico{font-size:30px;margin-bottom:6px}
.up-hint{font-size:12px;color:#77779a}
.ph-res{background:rgba(34,197,94,.06);border:1px solid rgba(34,197,94,.22);border-radius:11px;padding:11px;margin-top:8px;display:none}
.ph-url{font-size:11px;word-break:break-all;color:#4ade80;margin-bottom:8px}
.row-flex{display:flex;gap:6px;flex-wrap:wrap}

/* ── Змейка ── */
.sn-wrap{text-align:center}
#brv6-snake-canvas{border-radius:12px;display:block;margin:0 auto;border:1px solid rgba(255,255,255,.07);touch-action:none;background:#0b0b14}
.sn-score{font-size:14px;font-weight:700;color:#a78bfa;text-align:center;margin-bottom:10px}
.dpad{display:grid;grid-template-columns:repeat(3,1fr);gap:5px;width:132px;margin:12px auto 0}
.dpad button{background:#15151f;border:1px solid rgba(255,255,255,.08);border-radius:9px;padding:11px 0;color:#fff;font-size:14px;cursor:pointer;transition:.15s}
.dpad button:hover{background:rgba(124,58,237,.3);border-color:var(--br-pri)}
.back-row{text-align:center;margin-top:14px}

@keyframes brPop{from{transform:scale(.85);opacity:0}to{transform:scale(1);opacity:1}}
@keyframes brSlide{from{transform:translateY(14px) scale(.98);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}

@media(max-width:540px){
  #brv6-panel{left:8px;right:8px;bottom:80px;width:auto;height:calc(100vh - 100px);border-radius:20px}
  #brv6-fab{bottom:14px;left:14px;width:48px;height:48px;font-size:18px;border-radius:14px}
  .brv6-brand b{font-size:12px;letter-spacing:1.8px}
  .brv6-online-badge{display:none}
  .brv6-search{min-width:72px}
  .brv6-tab{flex-basis:64px;min-width:64px;height:56px}
}
@media(min-width:900px){
  #brv6-panel{width:460px}
}
`;

  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',
      }));
    }
  }

  /* ══════════════════════════════════════════════════════════
     ЛОГИН — под макет (логотип BR, BLACK RUSSIA / BR ADMIN PANEL)
  ══════════════════════════════════════════════════════════ */
  function buildLogin() {
    detectForumUserIfStale();

    const wrap = document.createElement('div');
    wrap.id = 'brv6-login';
    wrap.innerHTML = `
      <canvas id="brv6-canvas"></canvas>
      <div class="brlogin-bg"></div>
      <div class="authcard">
        <img class="lgo" src="${BR_LOGO}" alt="" onerror="this.style.display='none';this.nextElementSibling.style.display='block'">
        <div class="lg-fb">🛡️</div>
        <div class="lg-t1">BLACK RUSSIA</div>
        <div class="lg-t2">BR ADMIN PANEL</div>
        <div class="lg-div"></div>
        <p class="lg-lead">Вход в панель</p>
        <div class="lg-field">
          <i class="fa-solid fa-user"></i>
          <input type="text" id="brv6-nick-i" value="${nickname ? escapeHtml(nickname) : ''}" placeholder="Логин" autocomplete="off" />
        </div>
        <div class="lg-detect">${nickname ? '✅ Ник определён: ' + escapeHtml(nickname) : ''}</div>
        <button class="btn-enter" id="brv6-go">Войти</button>
        <div class="lg-foot">© 2026 BR Admin Panel</div>
      </div>
    `;
    document.body.appendChild(wrap);

    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(167,139,250,.26)';
        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.title = 'BR Admin Panel';
    fab.innerHTML = '<i class="fa-solid fa-shield-halved"></i><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);
      else { SnakeGame.destroy(); closeEditor(); closeCtx(); }
    };
    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); }
  }

  /* ══════════════════════════════════════════════════════════
     ПАНЕЛЬ — шапка (бренд+версия БЕЗ логотипа) / контент / нижняя навигация
  ══════════════════════════════════════════════════════════ */
  const TABS = [
    { id:'tpl',   icon:'fa-list-ul',           label:'Шаблоны',  title:'Шаблоны' },
    { id:'chat',  icon:'fa-comments',          label:'Поддержка',title:'Поддержка' },
    { id:'comm',  icon:'fa-users-rectangle',   label:'Чат',      title:'Чат кураторов' },
    { id:'logs',  icon:'fa-clock-rotate-left', label:'Логи',     title:'Логи' },
    { id:'con',   icon:'fa-paper-plane',       label:'Связь',    title:'Связь с разработчиком' },
    { id:'game',  icon:'fa-gamepad',           label:'Игры',     title:'Игры' },
  ];
  if (devUnlocked && !TABS.find(t => t.id === 'dev')) {
    TABS.push({ id:'dev', icon:'fa-code', label:'Dev', title:'Dev / Статистика' });
  }

  function updatePgTitle(tab) {
    const el = document.getElementById('brv6-pgtitle');
    const t = TABS.find(x => x.id === tab);
    if (el) el.textContent = t ? t.title : '';
  }

  function buildPanel() {
    const p = document.getElementById('brv6-panel');
    if (p) p.remove();

    const panel = document.createElement('div');
    panel.id = 'brv6-panel';
    panel.innerHTML = `
      <header class="brv6-hdr">
        <button class="brv6-menu-btn" data-action="toggle-panel-nav" aria-label="Показать или скрыть меню" title="Меню"><i class="fa-solid fa-bars"></i></button>
        <div class="brv6-brand" data-action="dev-gesture" title="BR Admin Panel Pro">
          <b>BR STYLE</b><span class="ver">ADMIN</span>
        </div>
        <div class="brv6-sepv"></div>
        <label class="brv6-search" title="Поиск по текущему разделу">
          <i class="fa-solid fa-magnifying-glass"></i>
          <input id="brv6-search" type="search" placeholder="Поиск настроек…" autocomplete="off" aria-label="Поиск по текущему разделу">
        </label>
        <span id="brv6-online-badge" class="brv6-online-badge" title="Кураторов онлайн">…</span>
        <button class="brv6-theme-btn" data-action="open-theme" aria-label="Тема" title="Цвет темы"><i class="fa-solid fa-palette"></i></button>
        <button class="brv6-x" data-action="close-panel" aria-label="Закрыть"><i class="fa-solid fa-xmark"></i></button>
      </header>
      <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}" title="${t.label}">
            <i class="fa-solid ${t.icon}"></i>${t.id==='chat'?'<span class="brv6-tab-b" id="brv6-tbadge">0</span>':''}
          </button>`).join('')}
      </nav>
      <main class="brv6-main" id="brv6-main"></main>
      <div id="brv6-modal"></div>
    `;
    document.body.appendChild(panel);
    panel.classList.toggle('open', UI.panelOpen);

    panel.addEventListener('click', onPanelClick);

    updatePgTitle(UI.activeTab);
    renderTab(UI.activeTab);
    updateTrackerUI();
    const search = document.getElementById('brv6-search');
    if (search) search.oninput = () => applyPanelSearch(search.value);
    syncMobileKeyboardLayout();
  }

  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);
    SnakeGame.destroy();
    closeEditor();
    closeCtx();
    updatePgTitle(tab);
    renderTab(tab);
  }

  function applyPanelSearch(query) {
    const main = document.getElementById('brv6-main');
    if (!main) return;
    const q = String(query || '').trim().toLowerCase();
    const selectors = '.tpl-card,.log-row,.ct-card,.gm-card,.stat-c,.about,.hist-link,.preset-card,.fg,.theme-hero,.empty-note';
    main.querySelectorAll(selectors).forEach(node => {
      const match = !q || node.textContent.toLowerCase().includes(q);
      node.classList.toggle('br-search-hide', !match);
    });
  }

  /* ── Тема / Стиль форума ── */
  function applyTheme(pri, pril) {
    document.documentElement.style.setProperty('--br-pri', pri);
    document.documentElement.style.setProperty('--br-pri-l', pril);
    document.documentElement.style.setProperty('--br-pri-d', pri);
  }
  function loadTheme() {
    applyForumSkin(loadForumSkin());
  }

  const VISUAL_PRESETS = {
    aurora: {
      label: 'Aurora Glass',
      note: 'Холодное стекло',
      primary: '#7c3aed',
      accent: '#22d3ee',
      btnStyle: 'glass',
      avaStyle: 'gradient',
      textTheme: 'cyberpunk',
      blur: 20,
      radius: 16,
    },
    crimson: {
      label: 'Crimson Night',
      note: 'Контрастный BR',
      primary: '#ef4444',
      accent: '#f97316',
      btnStyle: 'glow',
      avaStyle: 'glow',
      textTheme: 'cherry',
      blur: 17,
      radius: 13,
    },
    ocean: {
      label: 'Ocean Pulse',
      note: 'Синий неон',
      primary: '#0284c7',
      accent: '#22d3ee',
      btnStyle: 'glass',
      avaStyle: 'gradient',
      textTheme: 'ocean',
      blur: 18,
      radius: 18,
    },
    gold: {
      label: 'Gold Command',
      note: 'Тёплый премиум',
      primary: '#d97706',
      accent: '#facc15',
      btnStyle: 'def',
      avaStyle: 'glow',
      textTheme: 'gold',
      blur: 14,
      radius: 11,
    },
  };

  function applyVisualPreset(name) {
    const preset = VISUAL_PRESETS[name];
    if (!preset) return;
    const fields = {
      'st-primary': preset.primary,
      'st-accent': preset.accent,
      'st-btn': preset.btnStyle,
      'st-ava': preset.avaStyle,
      'st-text': preset.textTheme,
      'st-blur': preset.blur,
      'st-radius': preset.radius,
    };
    Object.entries(fields).forEach(([id, value]) => {
      const field = document.getElementById(id);
      if (!field) return;
      field.value = value;
      field.dispatchEvent(new Event('input', { bubbles: true }));
    });
    document.querySelectorAll('.preset-card').forEach(card => {
      card.classList.toggle('active', card.dataset.arg === name);
    });
    toast(`🎨 Пресет «${preset.label}» выбран`);
  }

  function closeTheme() {
    const m = document.getElementById('brv6-modal');
    if (m) { m.classList.remove('show'); m.innerHTML = ''; }
  }
  function openThemePicker() {
    const modal = document.getElementById('brv6-modal');
    if (!modal) return;
    const s = loadForumSkin();
    const fontOpts = ['Inter','Roboto','Manrope','Rajdhani','Montserrat']
      .map(f => `<option value="${f}"${s.font===f?' selected':''}>${f}</option>`).join('');
    const btnOpts = [['def','Обычный'],['glass','Стекло'],['glow','Свечение']]
      .map(([v,l]) => `<option value="${v}"${s.btnStyle===v?' selected':''}>${l}</option>`).join('');
    const avaOpts = [['def','Обычный'],['gradient','Градиент'],['glow','Свечение']]
      .map(([v,l]) => `<option value="${v}"${s.avaStyle===v?' selected':''}>${l}</option>`).join('');
    const textOpts = Object.keys(TEXT_THEMES)
      .map(k => `<option value="${k}"${s.textTheme===k?' selected':''}>${k}</option>`).join('');

    modal.innerHTML = `
      <div class="ed-head">
        <button class="ed-back" data-action="close-editor" aria-label="Назад"><i class="fa-solid fa-arrow-left"></i></button>
        <span class="ed-t">Стиль форума</span>
      </div>
     <div class="ed-body">
       <div class="theme-hero">
         <strong>Настрой стиль под себя</strong>
         <span>Пресет меняет цвета, стекло, кнопки, аватары и контраст. Фон анализируется отдельно при сохранении.</span>
       </div>
       <div class="subhead">Готовые стили</div>
       <div class="preset-grid">
         ${Object.entries(VISUAL_PRESETS).map(([key, preset]) => `
           <button class="preset-card" data-action="theme-preset" data-arg="${key}" style="--preset:linear-gradient(135deg,${preset.primary},${preset.accent})">
             <b>${preset.label}</b><small>${preset.note}</small>
           </button>`).join('')}
       </div>
       <div class="subhead">Цвет и фон</div>
        <div class="fg"><label class="lab">Основной цвет</label><input class="inp" id="st-primary" type="color" value="${s.primary}" style="height:42px;padding:4px"></div>
        <div class="fg"><label class="lab">Доп. цвет</label><input class="inp" id="st-accent" type="color" value="${s.accent}" style="height:42px;padding:4px"></div>
        <div class="fg"><label class="lab">Фон (ссылка на картинку, необязательно)</label><input class="inp" id="st-bg" value="${escapeHtml(s.bg)}" placeholder="https://..."></div>
        <div class="up-zone" id="st-bg-drop">
          <div class="up-ico">🖼️</div>
          <div class="up-hint">Или выбери фото с телефона</div>
          <input type="file" id="st-bg-file" accept="image/*" style="display:none">
        </div>
        <div class="fg"><label class="lab">Радиус углов: <span id="st-rad-v">${s.radius}</span>px</label><input id="st-radius" type="range" min="0" max="26" value="${s.radius}" style="width:100%"></div>
        <div class="fg"><label class="check-row"><input type="checkbox" id="st-blur-on" ${s.blurEnabled?'checked':''}> Размытие всплывающих окон</label></div>
        <div class="fg"><label class="lab">Сила размытия: <span id="st-blur-v">${s.blur}</span>px</label><input id="st-blur" type="range" min="0" max="30" value="${s.blur}" style="width:100%"></div>
        <div class="fg"><label class="lab">Шрифт</label><select class="sel" id="st-font">${fontOpts}</select></div>
        <div class="fg"><label class="lab">Стиль кнопок</label><select class="sel" id="st-btn">${btnOpts}</select></div>
        <div class="fg"><label class="lab">Стиль аватара</label><select class="sel" id="st-ava">${avaOpts}</select></div>
        <div class="fg"><label class="lab">Цветовая тема ников/цитат</label><select class="sel" id="st-text">${textOpts}</select></div>
       <div class="subhead">Своя AI API</div>
       <div class="fg"><label class="lab">Провайдер</label><select class="sel" id="ai-provider">
         <option value="pollinations"${loadAIConfig().provider === 'pollinations' ? ' selected' : ''}>Pollinations · без ключа</option>
         <option value="custom"${loadAIConfig().provider === 'custom' ? ' selected' : ''}>Своя OpenAI-совместимая API</option>
       </select></div>
       <div class="fg"><label class="lab">Endpoint</label><input class="inp" id="ai-endpoint" value="${escapeHtml(loadAIConfig().endpoint)}" placeholder="https://api.openai.com/v1/chat/completions"></div>
       <div class="fg"><label class="lab">Модель</label><input class="inp" id="ai-model" value="${escapeHtml(loadAIConfig().model)}" placeholder="gpt-4o-mini"></div>
       <div class="fg"><label class="lab">API-ключ <span style="color:#77779a">(хранится только локально)</span></label><input class="inp" id="ai-key" type="password" value="${escapeHtml(loadAIConfig().apiKey)}" placeholder="Ключ не отправляется в чат"></div>
       <div class="api-note"><b>/ai</b> будет использовать твою API при выборе «Своя…». Если ключ не указан, останется бесплатный режим Pollinations.</div>
        <button class="btnp" data-action="save-style" style="width:100%;padding:12px">💾 Применить</button>
      </div>
    `;
    modal.classList.add('show');
    document.getElementById('st-radius').oninput = e => { document.getElementById('st-rad-v').textContent = e.target.value; };
    document.getElementById('st-blur').oninput = e => { document.getElementById('st-blur-v').textContent = e.target.value; };

    const bgDrop = document.getElementById('st-bg-drop');
    bgDrop.onclick = () => document.getElementById('st-bg-file').click();
    document.getElementById('st-bg-file').onchange = e => {
      const f = e.target.files[0];
      if (!f) return;
      const img = new Image();
      img.onload = () => {
        const MAX_BYTES = 700 * 1024; // грубый лимит под GM-хранилище
        const attempts = [[1600, 0.85], [1100, 0.7], [750, 0.55]];
        let dataUrl = '';
        for (const [maxw, q] of attempts) {
          const cv = document.createElement('canvas');
          const scale = Math.min(1, maxw / img.width);
          cv.width = img.width * scale; cv.height = img.height * scale;
          cv.getContext('2d').drawImage(img, 0, 0, cv.width, cv.height);
          dataUrl = cv.toDataURL('image/webp', q);
          if (dataUrl.length * 0.75 <= MAX_BYTES) break; // base64 ~ 4/3 реального размера
        }
        URL.revokeObjectURL(img.src);
        if (dataUrl.length * 0.75 > MAX_BYTES) {
          toast('❌ Картинка слишком большая даже после сжатия — возьми другую или используй ссылку');
          return;
        }
        document.getElementById('st-bg').value = dataUrl;
         const luma = getImageLuma(img);
         if (Number.isFinite(luma)) {
           document.getElementById('st-bg').dataset.luma = String(luma);
         }
        toast('✅ Фон готов, жми «Применить»');
      };
      img.src = URL.createObjectURL(f);
    };
  }

  function isValidBgUrl(v) {
    if (!v) return true;
    return /^https:\/\//i.test(v) || /^data:image\//i.test(v);
  }

  function saveStyleFromModal() {
    const bgInput = document.getElementById('st-bg');
    const bg = bgInput.value.trim();
    if (!isValidBgUrl(bg)) {
      toast('❌ Фон должен быть ссылкой https:// или загруженным файлом');
      return;
    }
    const aiEndpoint = document.getElementById('ai-endpoint');
    const aiModel = document.getElementById('ai-model');
    const aiKey = document.getElementById('ai-key');
    const aiProvider = document.getElementById('ai-provider');
    if (aiEndpoint && aiModel && aiKey && aiProvider) {
      saveAIConfig({
        provider: aiProvider.value,
        endpoint: aiEndpoint.value,
        model: aiModel.value,
        apiKey: aiKey.value,
      });
    }
    const finish = luma => {
      const patch = {
        primary: document.getElementById('st-primary').value,
        accent: document.getElementById('st-accent').value,
        bg,
        bgLuma: Number.isFinite(luma) ? luma : 0.18,
        radius: parseInt(document.getElementById('st-radius').value),
        blurEnabled: document.getElementById('st-blur-on').checked,
        blur: parseInt(document.getElementById('st-blur').value),
        font: document.getElementById('st-font').value,
        btnStyle: document.getElementById('st-btn').value,
        avaStyle: document.getElementById('st-ava').value,
        textTheme: document.getElementById('st-text').value,
      };
      saveForumSkin(patch);
      toast('✅ Стиль адаптирован под фон и применён');
      closeTheme();
    };
    const savedLuma = parseFloat(bgInput.dataset.luma);
    if (bg && !Number.isFinite(savedLuma)) {
      toast('🔎 Анализируем светлость фона...');
      analyzeBackground(bg, finish);
    } else {
      finish(bg ? savedLuma : 0.18);
    }
  }

  /* ══════════════════════════════════════════════════════════
     СТИЛЬ ФОРУМА — перекраска самого форума (не только панели):
     фон, цвета, радиус, блюр, шрифт, стиль кнопок/аватаров, тема
     текста. Классы XenForo — открытая разметка, ничего чужого
     (токенов/бэкендов) тут нет, только CSS.
  ══════════════════════════════════════════════════════════ */
  const FORUM_SEL = {
    trans: '.block-container, .message, .structItem, .p-nav, .pageNav-page, .button, .button--primary, .inputGroup-text, input, textarea, select, .blockMessage, .bbCodeBlock, .widget-container, .p-header, .p-body, .p-body-header, .p-body-sidebar, .block-header, .block-footer, .block-formSection, .filterBar, .memberTooltip, .tooltip, .attachedFiles, .node-body, .offCanvasMenu-list',
    blur: '.menu-content, .offCanvasMenu-content, .overlay-container .overlay-content, .tooltip-content, .memberTooltip, .widget-container .widget, .fr-popup, .fr-dropdown-menu',
  };

  const TEXT_THEMES = {
    default:   { nick:'#a78bfa', quote:'#c4b5fd' },
    ocean:     { nick:'#00b0ff', quote:'#81d4fa' },
    forest:    { nick:'#76ff03', quote:'#c5e1a5' },
    gold:      { nick:'#ffd600', quote:'#ffe082' },
    cherry:    { nick:'#ff1744', quote:'#ffcdd2' },
    cyberpunk: { nick:'#00e5ff', quote:'#84ffff' },
    synthwave: { nick:'#f0abfc', quote:'#d8b4fe' },
    matrix:    { nick:'#00ff00', quote:'#b9f6ca' },
  };

  const STYLE_DEFAULTS = {
    bg: '', primary: '#7c3aed', accent: '#8b5cf6', radius: 12,
    blurEnabled: true, blur: 15, font: 'Inter',
    btnStyle: 'def', avaStyle: 'def', textTheme: 'default',
    bgLuma: 0.18,
  };

  function clamp(value, min, max) {
    return Math.min(max, Math.max(min, value));
  }

  function getImageLuma(image) {
    try {
      const canvas = document.createElement('canvas');
      canvas.width = 32;
      canvas.height = 32;
      const context = canvas.getContext('2d', { willReadFrequently: true });
      context.drawImage(image, 0, 0, 32, 32);
      const pixels = context.getImageData(0, 0, 32, 32).data;
      let total = 0;
      let weight = 0;
      for (let i = 0; i < pixels.length; i += 4) {
        const alpha = pixels[i + 3] / 255;
        if (!alpha) continue;
        const luma = (0.2126 * pixels[i] + 0.7152 * pixels[i + 1] + 0.0722 * pixels[i + 2]) / 255;
        total += luma * alpha;
        weight += alpha;
      }
      return weight ? clamp(total / weight, 0, 1) : null;
    } catch {
      return null;
    }
  }

  function analyzeBackground(source, callback) {
    if (!source) { callback(null); return; }
    const image = new Image();
    if (/^https:\/\//i.test(source)) image.crossOrigin = 'anonymous';
    image.onload = () => callback(getImageLuma(image));
    image.onerror = () => callback(null);
    image.src = source;
  }

  function forumSkinCSS(s) {
    const filter = s.blurEnabled ? `blur(${s.blur}px) saturate(160%)` : 'none';
    const hasBackground = Boolean(s.bg);
    const luma = Number.isFinite(Number(s.bgLuma)) ? clamp(Number(s.bgLuma), 0, 1) : 0.18;
    const overlay = hasBackground ? clamp(0.18 + luma * 0.38, 0.18, 0.56) : 0;
    const layerOne = hasBackground ? clamp(0.30 + luma * 0.35, 0.30, 0.70) : 0.50;
    const layerTwo = hasBackground ? clamp(0.40 + luma * 0.36, 0.40, 0.78) : 0.62;
    const textShadow = hasBackground ? '0 1px 3px rgba(0,0,0,.78)' : 'none';
    const btnRule = s.btnStyle === 'glass'
      ? `${FORUM_SEL.trans.split(',').filter(x=>x.includes('button')).join(',')}{background:linear-gradient(135deg,color-mix(in srgb,var(--br-primary) 35%,rgba(255,255,255,.12)),color-mix(in srgb,var(--br-accent) 20%,rgba(255,255,255,.05)))!important;backdrop-filter:blur(10px)!important;border:1px solid rgba(255,255,255,.2)!important;color:#fff!important}`
      : s.btnStyle === 'glow'
      ? `.button,.button--primary{background:rgba(var(--br-tint),.25)!important;border:1px solid var(--br-primary)!important;box-shadow:0 0 16px color-mix(in srgb,var(--br-primary) 70%,transparent)!important;color:#fff!important}`
      : `.button--primary{background:var(--br-primary)!important;border:none!important;color:#fff!important}`;
    const avaRule = s.avaStyle === 'glow'
      ? `.avatar{border-radius:50%!important;box-shadow:0 0 14px var(--br-primary)!important;border:2px solid var(--br-primary)!important}`
      : s.avaStyle === 'gradient'
      ? `.avatar{padding:3px!important;background:linear-gradient(45deg,var(--br-primary),var(--br-accent))!important;border-radius:50%!important}`
      : '';
    return `
:root{--br-bg:${hasBackground ? `url('${s.bg.replace(/'/g, "%27")}')` : 'none'};--br-primary:${s.primary};--br-accent:${s.accent};--br-rad:${s.radius}px;--br-filter:${filter};--br-tint:${hexRGB(s.primary)};--br-block-bg:14,14,20;--br-bg-overlay:${overlay};--br-l1:${layerOne};--br-l2:${layerTwo};--br-text-shadow:${textShadow}}
html{background-color:rgb(var(--br-block-bg))!important;background-image:linear-gradient(rgba(7,7,14,var(--br-bg-overlay)),rgba(7,7,14,var(--br-bg-overlay))),var(--br-bg)!important;background-position:center;background-size:cover;background-attachment:fixed;scrollbar-color:var(--br-primary) transparent;scrollbar-width:thin}
body{background:transparent!important;min-height:100vh;text-shadow:var(--br-text-shadow)}
body{font-family:'${s.font}',sans-serif!important}
.p-pageWrapper,.p-body-inner,.p-footer,.p-sectionLinks{background:rgba(var(--br-block-bg),var(--br-l1))!important;border-color:transparent!important}
${FORUM_SEL.trans}{background:rgba(var(--br-block-bg),var(--br-l2))!important;border-radius:var(--br-rad)!important;border-color:transparent!important;transition:.25s}
${FORUM_SEL.blur}{backdrop-filter:var(--br-filter)!important;-webkit-backdrop-filter:var(--br-filter)!important}
::-webkit-scrollbar-thumb{background:var(--br-primary)!important;border-radius:8px}
a.username{color:var(--br-nick,${(TEXT_THEMES[s.textTheme] || TEXT_THEMES.default).nick})!important}
.bbCodeBlock--quote{border-left-color:var(--br-primary)!important}
#brv6-panel{background:rgba(var(--br-block-bg),.24)!important}
#brv6-panel .brv6-hdr,#brv6-panel .brv6-tracker,#brv6-panel .brv6-nav,#brv6-panel .chat-bar{background:rgba(var(--br-block-bg),.16)!important}
#brv6-panel .tpl-card,#brv6-panel .log-row,#brv6-panel .ct-card,#brv6-panel .gm-card,#brv6-panel .about,#brv6-panel .hist-link{background:rgba(var(--br-block-bg),.18)!important}
#brv6-panel .msg.op{background:rgba(var(--br-block-bg),.26)!important}
${btnRule}
${avaRule}
`;
  }

  function applyForumSkin(s) {
    let el = document.getElementById('brv6-forum-skin');
    if (!el) { el = document.createElement('style'); el.id = 'brv6-forum-skin'; document.head.appendChild(el); }
    el.textContent = forumSkinCSS(s);
    applyTheme(s.primary, s.accent);
  }

  function loadForumSkin() {
    const saved = Store.get('brp_style', {});
    return {
      ...STYLE_DEFAULTS,
      ...(saved && typeof saved === 'object' && !Array.isArray(saved) ? saved : {}),
    };
  }

  function saveForumSkin(patch) {
    const s = { ...loadForumSkin(), ...patch };
    Store.set('brp_style', s);
    applyForumSkin(s);
    return s;
  }


  let ctxEl = null;
  function closeCtx() {
    if (ctxEl) { ctxEl.remove(); ctxEl = null; }
    document.removeEventListener('click', ctxOutside, { capture:true });
  }
  function ctxOutside(e) {
    if (ctxEl && !ctxEl.contains(e.target)) closeCtx();
  }
  function openTplMenu(anchorBtn, id) {
    closeCtx();
    const t = templates.find(x => x.id == id);
    if (!t) return;
    ctxEl = document.createElement('div');
    ctxEl.className = 'brv6-ctx';

    const mk = (icon, label, cls, fn) => {
      const b = document.createElement('button');
      b.className = 'ctx-i' + (cls ? ' ' + cls : '');
      b.innerHTML = `<i class="fas ${icon}"></i>${label}`;
      b.onclick = () => { closeCtx(); fn(); };
      return b;
    };

    ctxEl.appendChild(mk('fa-play', 'Применить', '', () => applyTemplate(t)));
    ctxEl.appendChild(mk('fa-pen-to-square', 'Редактировать', '', () => openEditor(id)));
    ctxEl.appendChild(mk('fa-copy', 'Копировать BB', '', () => {
      copyText(
        applyMacros(t.content),
        () => { addLog('Копирование', t.name); toast('📋 Скопировано'); },
        () => toast('❌ Не удалось скопировать — скопируй вручную')
      );
    }));
    ctxEl.appendChild(mk('fa-trash', 'Удалить', 'danger', () => {
      if (!confirm(`Удалить "${t.name}"?`)) return;
      addLog('Удалён шаблон', t.name);
      templates = templates.filter(x => x.id != id);
      saveTpls();
      if (UI.activeTab === 'tpl') renderTab('tpl');
    }));

    const r = anchorBtn.getBoundingClientRect();
    document.body.appendChild(ctxEl);
    const mw = ctxEl.offsetWidth, mh = ctxEl.offsetHeight;
    let top = r.bottom + 6, left = Math.min(r.right - mw, innerWidth - mw - 8);
    if (top + mh > innerHeight - 8) top = Math.max(8, r.top - mh - 6);
    ctxEl.style.top = top + 'px';
    ctxEl.style.left = Math.max(8, left) + 'px';
    setTimeout(() => document.addEventListener('click', ctxOutside, { capture:true }), 0);
  }

  /* ── Редактор шаблона (модалка внутри панели) ── */
  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 editingTplId = null;
  let lastUploadedUrl = '';

  function openEditor(id) {
    const modal = document.getElementById('brv6-modal');
    if (!modal) return;
    editingTplId = id != null ? id : null;
    const t = id != null ? templates.find(x => x.id == id) : null;
    const pfxOpts = PREFIXES.map(p => `<option value="${p.id}"${t && t.prefix === p.id ? ' selected':''}>${p.name}</option>`).join('');

    modal.innerHTML = `
      <div class="ed-head">
        <button class="ed-back" data-action="close-editor" aria-label="Назад"><i class="fa-solid fa-arrow-left"></i></button>
        <span class="ed-t">${t ? 'Редактирование шаблона' : 'Новый шаблон'}</span>
      </div>
      <div class="ed-body">
        <div class="fg"><label class="lab">Название</label><input class="inp" id="e-name" placeholder="Название шаблона" value="${t ? escapeHtml(t.name) : ''}" /></div>
        <div class="fg"><label class="lab">Префикс</label><select class="sel" id="e-pfx" style="flex:none">${pfxOpts}</select></div>
        <div class="fg"><label class="check-row"><input type="checkbox" id="e-pin" ${t && t.autopin ? 'checked':''}> 📌 Автозакреп</label></div>
        <div class="fg">
          <label class="lab">Цвет текста</label>
          <div class="color-row">
            ${EDITOR_COLORS.map(c => `<button class="cbtn" style="background:${c}" data-action="pick-color" data-arg="${c}" title="${c}"></button>`).join('')}
          </div>
        </div>
        <div class="fg">
          <label class="lab">BB-вставки</label>
          <div class="color-row">
            ${BB_CHIPS.map((c,i) => `<button class="chip" data-action="insert-bb" data-arg="${i}">${c[0]}</button>`).join('')}
          </div>
        </div>
        <div class="fg">
          <label class="lab">Текст шаблона (&#123;&#123;greeting&#125;&#125;, &#123;&#123;username&#125;&#125;, &#123;&#123;uid&#125;&#125;)</label>
          <textarea class="ta" id="e-text" placeholder="[COLOR=#FF00FF]{{greeting}}, {{username}}[/COLOR]&#10;&#10;Ваш текст...">${t ? escapeHtml(t.content) : ''}</textarea>
        </div>
        <button class="btnp" data-action="save-template" style="width:100%;padding:12px;margin-bottom:14px">💾 Сохранить шаблон</button>

        <div class="brv6-sec" style="margin-top:4px">Загрузка фото (imgbb)</div>
        <div class="fg"><label class="lab">imgbb API ключ</label><input class="inp" id="e-imgkey" value="${escapeHtml(Store.get('brp_imgbb',''))}" placeholder="Ключ с imgbb.com → API" /></div>
        <button class="btng sm" id="e-savekey" style="margin-bottom:10px">💾 Сохранить ключ</button>
        <div class="up-zone" id="e-drop">
          <div class="up-ico">📷</div>
          <div class="up-hint" id="e-hint">Нажми или перетащи фото</div>
          <input type="file" id="e-file" accept="image/*" style="display:none">
        </div>
        <div class="ph-res" id="e-result">
          <div class="ph-url" id="e-url"></div>
          <div class="row-flex">
            <button class="btnp sm" id="e-copy-url">📋 Ссылка</button>
            <button class="btng sm" id="e-copy-bb">🖼 BB-код</button>
            <button class="btng sm" id="e-ins-ta">↑ В шаблон</button>
          </div>
        </div>
      </div>
    `;
    modal.classList.add('show');

    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); };
    document.getElementById('e-file').onchange = e => { if (e.target.files[0]) handlePhoto(e.target.files[0]); };

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

    setTimeout(() => document.getElementById('e-name')?.focus(), 60);
  }

  function closeEditor() {
    const m = document.getElementById('brv6-modal');
    if (m) { m.classList.remove('show'); m.innerHTML = ''; }
    editingTplId = null;
  }

  function pickColor(el, color) {
    document.querySelectorAll('#brv6-modal .cbtn').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 tx = document.getElementById('e-text').value.trim();
    const pf = parseInt(document.getElementById('e-pfx').value);
    const pin = document.getElementById('e-pin').checked;
    if (!n || !tx) { toast('⚠️ Заполните название и текст'); return; }
    if (editingTplId != null) {
      const ex = templates.find(x => x.id == editingTplId);
      if (ex) Object.assign(ex, { name:n, prefix:pf, autopin:pin, content:tx });
      addLog('Шаблон изменён', n);
    } else {
      templates.push({ id: Date.now(), name: n, prefix: pf, autopin: pin, content: tx });
      addLog('Шаблон создан', n);
    }
    saveTpls();
    toast(`✅ "${n}" сохранён!`);
    closeEditor();
    if (UI.activeTab === 'tpl') renderTab('tpl');
  }

  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) {
    toast('📤 Загружаем фото...');
    const hint = document.getElementById('e-hint');
    if (hint) hint.textContent = '⏳ Загрузка...';
    uploadPhoto(file, (url, err) => {
      const hintNow = document.getElementById('e-hint');
      if (hintNow) hintNow.textContent = 'Нажми или перетащи фото';
      if (err) { toast('❌ ' + err); return; }
      lastUploadedUrl = url;
      const urlEl = document.getElementById('e-url');
      const resultEl = document.getElementById('e-result');
      if (!urlEl || !resultEl) return; // редактор закрыли, пока фото грузилось
      urlEl.textContent = url;
      resultEl.style.display = 'block';
      toast('✅ Фото загружено!');
    });
  }

  /* ── Роутер кликов по 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 'toggle-panel-nav': {
        const nav = document.querySelector('#brv6-panel .brv6-nav');
        const icon = el.querySelector('i');
        if (nav) nav.classList.toggle('nav-collapsed');
        if (icon) icon.classList.toggle('fa-bars');
        if (icon) icon.classList.toggle('fa-chevron-down');
        break;
      }
      case 'close-panel':
        UI.panelOpen = false;
        document.getElementById('brv6-panel').classList.remove('open');
        SnakeGame.destroy();
        closeEditor();
        closeCtx();
        closeTheme();
        break;
      case 'switch-tab':
        switchTab(arg);
        break;
      case 'dev-gesture':
        handleBrandTap();
        break;
      case 'open-theme':
        openThemePicker(el);
        break;
      case 'theme-preset':
        applyVisualPreset(arg);
        break;
      case 'apply-template': {
        const t = templates.find(x => x.id == arg);
        if (t) applyTemplate(t);
        break;
      }
      case 'tpl-menu':
        openTplMenu(el, arg);
        break;
      case 'open-editor':
        closeCtx();
        openEditor(null);
        break;
      case 'edit-template':
        closeCtx();
        openEditor(arg);
        break;
      case 'close-editor':
        closeEditor();
        break;
      case 'save-template':
        saveNewTemplate();
        break;
      case 'save-style':
        saveStyleFromModal();
        break;
      case 'insert-bb':
        insertBBChip(arg);
        break;
      case 'pick-color':
        pickColor(el, arg);
        break;
      case 'delete-menu-item':
        break;
      case 'send-chat':
        sendChatMessage();
        break;
      case 'send-community':
        sendCommunityMessage();
        break;
      case 'refresh-history':
        loadHistory(drawMessages);
        toast('🕘 История обновлена');
        break;
      case 'clear-logs':
        if (!confirm('Очистить логи?')) break;
        logs = []; saveLogs();
        if (UI.activeTab === 'logs') renderTab('logs');
        break;
      case 'launch-game':
        launchBlackRussia();
        break;
      case 'open-snake':
        SnakeGame.start(document.getElementById('brv6-main'));
        break;
      case 'snake-back':
        SnakeGame.destroy();
        updatePgTitle('game');
        renderGameTab(document.getElementById('brv6-main'));
        break;
      case 'snake-dir':
        SnakeGame.setDirection(arg);
        break;
    }
  }

  // 5 тапов по «BR ADMIN» в шапке → запрос dev-пароля
  let brandTaps = 0, brandTapReset = null;
  function handleBrandTap() {
    brandTaps++;
    clearTimeout(brandTapReset);
    brandTapReset = setTimeout(() => { brandTaps = 0; }, 2200);
    if (brandTaps < 5) return;
    brandTaps = 0;
    const pass = prompt('Пароль разработчика:');
    if (!pass) return;
    api('/dev/grant', { target: nickname, admin_pass: pass }, (e, d) => {
      if (e || !d?.ok) { toast('❌ Неверный пароль'); return; }
      devUnlocked = true;
      Store.set('brp_dev', true);
      toast('✅ Dev-доступ открыт');
      if (!TABS.find(t => t.id === 'dev')) TABS.push({ id:'dev', icon:'fa-code', label:'Dev', title:'Dev / Статистика' });
      checkForumActions();
      Timers.every('poll-forum-actions', 15000, checkForumActions);
      buildPanel(); // пересобираем панель — появится вкладка Dev
    });
  }

  /* ══════════════════════════════════════════════════════════
     РЕНДЕР ТАБОВ
  ══════════════════════════════════════════════════════════ */
  function renderTab(tab) {
    const main = document.getElementById('brv6-main');
    if (!main) return;
    main.classList.toggle('chat-mode', tab === 'chat' || tab === 'comm');
    const renderers = {
      tpl: renderTemplatesTab, chat: renderChatTab, comm: renderCommunityTab, logs: renderLogsTab,
      con: renderContactTab, game: renderGameTab, dev: renderDevTab,
    };
    (renderers[tab] || renderTemplatesTab)(main);
    const search = document.getElementById('brv6-search');
    if (search) applyPanelSearch(search.value);
  }

  /* ── ШАБЛОНЫ ── */
  function renderTemplatesTab(body) {
    const authorName = ForumUser.name ? escapeHtml(ForumUser.name) : 'не определён';
    const list = templates.length
      ? `<div class="tpl-grid">` + templates.map(tpl => {
          const p = prefixById.get(tpl.prefix) || prefixById.get(0);
          const prev = tpl.content.replace(/\[[^\]]*\]/g,'').replace(/\s+/g,' ').trim().substring(0,72);
          return `
            <div class="tpl-card">
              <div class="tpl-ic" style="background:${hexA(p.color,.14)};color:${p.color}" title="${p.name}">
                <i class="fas ${tplIcon(tpl.prefix)}"></i>
              </div>
              <div class="tpl-info" data-action="edit-template" data-arg="${tpl.id}" title="Клик — редактировать">
                <div class="tpl-name">${escapeHtml(tpl.name)}${tpl.autopin ? '<span style="font-size:11px">📌</span>' : ''}</div>
                <div class="tpl-prev">${escapeHtml(prev)}…</div>
              </div>
              <div class="tpl-side">
                <button class="btnp sm" data-action="apply-template" data-arg="${tpl.id}">Применить</button>
                <button class="tpl-kebab" data-action="tpl-menu" data-arg="${tpl.id}" aria-label="Меню"><i class="fas fa-ellipsis-vertical"></i></button>
              </div>
            </div>`;
        }).join('') + '</div>'
      : '<div class="empty-note">Нет шаблонов.<br>Нажми «+ Новый», чтобы создать первый.</div>';

    body.innerHTML = `
      <div class="tpl-head">
        <p class="brv6-sec">Шаблоны</p>
        <button class="btnp sm" data-action="open-editor">+ Новый шаблон</button>
      </div>
      <div class="mini-author">👤 Автор темы: <b>${authorName}</b></div>
      ${list}
      <div class="hintbar"><i class="far fa-hand-pointer"></i> Клик по шаблону — редактирование · ⋮ — действия</div>
    `;
  }

  /* ── ПОДДЕРЖКА ── */
  function renderChatTab(body) {
    body.innerHTML = `
      <div class="chat-scroll">
        <div class="chat-hero">
          <div class="cht-ic"><i class="fa-solid fa-comments"></i></div>
          <h3 class="cht-h">Поддержка</h3>
          <p class="cht-p">Напишите ваш вопрос, и мы ответим как можно скорее.<br>Команды: /help, /ask, /report question|bug, /rules</p>
        </div>
        <div class="brv6-msgs" id="brv6-msgs">
          <div class="empty-note" style="padding:12px">Загрузка…</div>
        </div>
        <div class="hist-link" data-action="refresh-history">
          <i class="far fa-clock"></i><span>История сообщений</span><i class="fas fa-chevron-right chev"></i>
        </div>
      </div>
      <div class="chat-bar">
        <textarea id="brv6-cinp" placeholder="Ваш вопрос или жалоба…" rows="1"></textarea>
        <button class="send-btn" data-action="send-chat" aria-label="Отправить"><i class="fa-solid fa-paper-plane"></i></button>
      </div>
    `;
    const input = document.getElementById('brv6-cinp');
    bindMobileInput(input);
    input.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 class="empty-note" style="padding:16px">Сообщений нет.<br>Напишите первым!</div>';
      return;
    }
    el.innerHTML = msgs.map(m => `
      <div class="msg ${m.role==='support'?'op':'me'}">
        ${escapeHtml(m.text)}
        <div class="msg-time">${escapeHtml(m.time||'')}</div>
      </div>`).join('');
    const sc = document.querySelector('.chat-scroll');
    if (sc) sc.scrollTop = sc.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 communityMsgHtml(m) {
    const mine = m.nick === nickname;
    const roleBadge = m.role === 'admin' ? '<span class="msg-role r-admin">Admin</span>'
      : m.role === 'dev' ? '<span class="msg-role r-dev">Dev</span>'
      : m.role === 'ai' ? '<span class="msg-role r-ai">ИИ</span>' : '';
    return `
      <div class="msg ${mine ? 'me' : 'op'}">
        ${mine ? '' : `<div class="msg-who">${escapeHtml(m.nick)}${roleBadge}</div>`}
        ${escapeHtml(m.text)}
        <div class="msg-time">${escapeHtml(m.time||'')}</div>
      </div>`;
  }

  function appendCommunityMessages(msgs) {
    const el = document.getElementById('brv6-comm-msgs');
    if (!el) return;
    el.querySelector('.empty-note')?.remove();
    el.insertAdjacentHTML('beforeend', msgs.map(communityMsgHtml).join(''));
    const sc = document.querySelector('#brv6-main .chat-scroll');
    if (sc) sc.scrollTop = sc.scrollHeight;
  }

  function renderCommunityTab(body) {
    body.innerHTML = `
      <div class="chat-scroll">
        <div class="chat-hero">
          <div class="cht-ic"><i class="fa-solid fa-users-rectangle"></i></div>
          <h3 class="cht-h">Чат кураторов</h3>
          <p class="cht-p">Общий чат для всех, кто открыл панель. Команда <b>/ai вопрос</b> — спросить нейросеть.</p>
        </div>
        <div class="brv6-msgs" id="brv6-comm-msgs">
          <div class="empty-note" style="padding:12px">Загрузка…</div>
        </div>
      </div>
      <div class="chat-bar">
        <textarea id="brv6-comm-inp" placeholder="Сообщение или /ai вопрос…" rows="1"></textarea>
        <button class="send-btn" data-action="send-community" aria-label="Отправить"><i class="fa-solid fa-paper-plane"></i></button>
      </div>
    `;
    const input = document.getElementById('brv6-comm-inp');
    bindMobileInput(input);
    input.onkeydown = e => {
      if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendCommunityMessage(); }
    };
    commAfterId = 0;
    api('/community/poll', { after_id: 0 }, (e, d) => {
      const el = document.getElementById('brv6-comm-msgs');
      if (!el) return;
      if (e || !d?.messages?.length) { el.innerHTML = '<div class="empty-note" style="padding:16px">Сообщений нет.<br>Напишите первым!</div>'; return; }
      commAfterId = d.messages[d.messages.length - 1].id;
      el.innerHTML = d.messages.map(communityMsgHtml).join('');
      const sc = document.querySelector('#brv6-main .chat-scroll');
      if (sc) sc.scrollTop = sc.scrollHeight;
    });
  }

  function sendCommunityMessage() {
    const inp = document.getElementById('brv6-comm-inp');
    if (!inp) return;
    const text = inp.value.trim();
    if (!text) return;
    inp.value = '';

    if (/^\/ai\s+/i.test(text)) {
      const prompt = text.replace(/^\/ai\s+/i, '').trim();
      sendCommunity(nickname, text);
      askAI(prompt, (answer, err) => {
        sendCommunity('🤖 ИИ', answer ? `💬 Запрос: ${prompt}\n\n${answer}` : `⚠️ Не удалось получить ответ (${err}). Запрос: ${prompt}`);
      });
      return;
    }

    sendCommunity(nickname, text);
  }


  let logFilter = '';

  function logIconOf(a) {
    const s = String(a || '');
    if (s.includes('Вход'))       return ['fa-right-to-bracket', '#4f8cff'];
    if (s.startsWith('Сообщение'))return ['fa-comment',           '#a78bfa'];
    if (s.includes('создан'))     return ['fa-plus',             '#8a8aa3'];
    if (s.includes('изменён'))    return ['fa-pen',              '#f0c040'];
    if (s.includes('примен'))     return ['fa-circle-check',     '#22c55e'];
    if (s.includes('Копир'))      return ['fa-copy',             '#8a8aa3'];
    if (s.includes('Удал'))       return ['fa-trash-can',        '#ff5c5c'];
    return ['fa-circle-dot', '#8a8aa3'];
  }

  function renderLogsTab(body) {
    const actions = [...new Set(logs.map(l => l.a).filter(Boolean))];
    if (!actions.includes(logFilter) && logFilter) logFilter = '';
    const rows = logs
      .filter(l => !logFilter || l.a === logFilter)
      .slice()
      .reverse();
    const listHtml = rows.length
      ? rows.map(l => {
          const [ic, col] = logIconOf(l.a);
          return `
            <div class="log-row">
              <div class="lg-ic" style="background:${hexA(col,.13)};color:${col}"><i class="fas ${ic}"></i></div>
              <div class="lg-t">
                <div class="lg-a">${escapeHtml(l.a)}</div>
                <div class="lg-u">${escapeHtml(l.u)}${l.d ? ' — ' + escapeHtml(l.d) : ''}</div>
              </div>
              <div class="lg-time">${escapeHtml(l.t)}</div>
            </div>`;
        }).join('')
      : '<div class="empty-note">Записей нет.</div>';

    body.innerHTML = `
      <p class="brv6-sec">Логи</p>
      <div class="log-tools">
        <select class="sel" id="log-filter">
          <option value="">Все действия</option>
          ${actions.map(a => `<option value="${escapeHtml(a)}"${a===logFilter?' selected':''}>${escapeHtml(a)}</option>`).join('')}
        </select>
        <button class="btnr sm" data-action="clear-logs" title="Очистить логи"><i class="far fa-trash-can"></i></button>
      </div>
      <div id="log-list">${listHtml}</div>
    `;
    document.getElementById('log-filter').onchange = e => {
      logFilter = e.target.value;
      renderLogsTab(body);
    };
  }

  /* ── СВЯЗЬ ── */
  function renderContactTab(body) {
    const serverHost = (() => { try { return new URL(SERVER).host; } catch { return SERVER; } })();
    const st = lastPingOk === null
      ? '<span class="dotr"></span>Проверка…'
      : lastPingOk
        ? '<span class="dotg"></span>Online'
        : '<span class="dotr"></span>Нет связи';

    body.innerHTML = `
      <p class="brv6-sec">Связь с разработчиком</p>
      <a class="ct-card" href="https://vk.ru/brunoverona" target="_blank">
        <div class="ct-ic" style="background:rgba(0,119,255,.14);color:#4da3ff"><i class="fa-brands fa-vk"></i></div>
        <div class="ct-body"><div class="ct-n">ВКонтакте</div><div class="ct-s">vk.ru/brunoverona</div></div>
      </a>
      <a class="ct-card" href="https://vk.ru/club237051164" target="_blank">
        <div class="ct-ic" style="background:rgba(41,169,235,.14);color:#53c0ff"><i class="fa-brands fa-telegram"></i></div>
        <div class="ct-body"><div class="ct-n">Группа разработчиков</div><div class="ct-s">vk.ru/club237051164</div></div>
      </a>

      <div class="about">
        <p class="brv6-sec">О панели</p>
        <p class="about-name">BR Admin Panel v${SCRIPT_VERSION}</p>
        <p class="about-desc">Современная панель управления для форума Black Russia.</p>
        <div class="kv"><span>Разработчик</span><b>Akzholch1k</b></div>
        <div class="kv"><span>Версия</span><b>${SCRIPT_VERSION}</b></div>
        <div class="kv"><span>Сервер</span><b class="purple">${escapeHtml(serverHost)}</b></div>
        <div class="kv"><span>Статус</span><b>${st}</b></div>
      </div>
    `;
  }

  /* ── ИГРЫ ── */
  function renderGameTab(body) {
    body.innerHTML = `
      <p class="brv6-sec">Игры</p>
      <div class="gm-card">
        <img class="gm-img" src="${BR_LOGO}" alt="" onerror="this.outerHTML='<span class=&quot;gm-em&quot;>🐯</span>'">
        <div class="gm-t">BLACK RUSSIA</div>
        <div class="gm-sub">Онлайн-игра про Россию<br>в твоём телефоне</div>
        <button class="btnp" data-action="launch-game">Открыть игру</button>
      </div>
      <div class="gm-card">
        <span class="gm-em">🐍</span>
        <div class="gm-t">ЗМЕЙКА</div>
        <div class="gm-sub">Мини-игра для отдыха</div>
        <button class="btnp" data-action="open-snake">Играть</button>
      </div>
    `;
  }

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

  /* ── DEV ── */
  function renderDevTab(body) {
    const ic = (emoji, col) => `<div class="st-ic" style="background:${hexA(col,.13)};color:${col}">${emoji}</div>`;
    const serverHost = (() => { try { return new URL(SERVER).host; } catch { return SERVER; } })();
    body.innerHTML = `
      <p class="brv6-sec">Dev / Статистика</p>
      <div class="stat-row">
        <div class="stat-c">${ic('●','#22c55e')}<div class="st-v">${onlineCount === null ? '…' : onlineCount}</div><div class="st-l">Онлайн сейчас</div></div>
        <div class="stat-c">${ic('<i class=\"fas fa-user\"></i>','#4f8cff')}<div class="st-v">${users}</div><div class="st-l">Пользователей</div></div>
        <div class="stat-c">${ic('<i class=\"fas fa-clipboard-list\"></i>','#8a8aa3')}<div class="st-v">${logs.length}</div><div class="st-l">Действий в логе</div></div>
      </div>

      <div class="about">
        <p class="brv6-sec">Диагностика</p>
        <div class="kv"><span>Ник</span><b>${escapeHtml(nickname || '—')}</b></div>
        <div class="kv"><span>Сервер</span><b class="purple">${escapeHtml(serverHost)}</b></div>
        <div class="kv"><span>Ключ (SECRET)</span><b>${SECRET.length} символов</b></div>
        <div class="kv"><span>Пинг</span><b>${lastPingTime ? new Date(lastPingTime).toLocaleTimeString('ru-RU') : 'ещё не было'}</b></div>
        <div class="kv"><span>Статус</span><b>${
          lastPingOk === null
            ? '<span class="dotr"></span>Ожидание'
            : lastPingOk
              ? '<span class="dotg"></span>Сервер отвечает'
              : `<span class="dotr"></span>Ошибка: ${escapeHtml(String(lastPingError))}`
        }</b></div>
      </div>
    `;
  }

  /* ══════════════════════════════════════════════════════════
     ЗМЕЙКА (lifecycle без изменений)
  ══════════════════════════════════════════════════════════ */
  const SnakeGame = (() => {
    let intervalId = null;
    let keyHandler = null;
    let currentSetDir = () => {};

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

    function start(body) {
      destroy();

      body.innerHTML = `
        <div class="sn-wrap">
          <p class="brv6-sec" style="text-align:left">🐍 Змейка</p>
          <div class="sn-score" id="sn-score">Счёт: 0</div>
          <canvas id="brv6-snake-canvas" width="264" height="264"></canvas>
          <div class="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>
          <div class="back-row"><button class="btng sm" data-action="snake-back">← Назад к играм</button></div>
        </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}, queuedDir = null, 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 = '#0b0b14'; ctx.fillRect(0,0,264,264);
        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 ? '#a78bfa' : '#7c3aed';
          ctx.fillRect(s.x*SZ+1, s.y*SZ+1, SZ-2, SZ-2);
        });
        if (!alive) {
          ctx.fillStyle = 'rgba(0,0,0,.65)'; ctx.fillRect(0,0,264,264);
          ctx.fillStyle = '#fff'; ctx.font = 'bold 18px Inter,sans-serif'; ctx.textAlign = 'center';
          ctx.fillText('GAME OVER', 132, 112);
          ctx.font = '13px Inter,sans-serif'; ctx.fillText('Счёт: '+score, 132, 138);
          ctx.fillStyle = '#a78bfa';
          ctx.fillText('Тапни чтобы начать заново', 132, 160);
        }
      }

      function step() {
        if (!alive) return;
        if (queuedDir) { dir = queuedDir; queuedDir = null; }
        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;
        queuedDir = 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}; queuedDir = null; food = randFood(); score = 0; alive = true;
        document.getElementById('sn-score').textContent = 'Счёт: 0';
      };

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

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

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

  /* ══════════════════════════════════════════════════════════
     ЗАЩИТА ОТ ДВОЙНОГО ЗАПУСКА — если скрипт включён дважды
     (две версии в Tampermonkey и т.п.), обе копии начинают
     конфликтовать за одни и те же DOM id. Ставим метку и через
     секунду проверяем, не появилась ли вторая.
  ══════════════════════════════════════════════════════════ */
  function checkInstanceConflict() {
    const marker = document.createElement('div');
    marker.className = 'brv6-instance-marker';
    marker.style.display = 'none';
    document.body.appendChild(marker);
    Timers.after('conflict-check', 1000, () => {
      if (document.querySelectorAll('.brv6-instance-marker').length > 1 && !document.getElementById('brv6-conflict')) {
        const m = document.createElement('div');
        m.id = 'brv6-conflict';
        m.style.cssText = 'position:fixed;inset:0;background:rgba(5,5,8,.9);backdrop-filter:blur(14px);z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:20px;font-family:Inter,sans-serif;text-align:center';
        m.innerHTML = `<div style="max-width:320px"><div style="font-size:40px;margin-bottom:12px">⚠️</div><div style="color:#fff;font-weight:800;font-size:16px;margin-bottom:8px">Скрипт запущен дважды</div><div style="color:#9a9ab0;font-size:13px;line-height:1.6">В Tampermonkey включено больше одной версии BR Admin Panel. Отключи старую версию (или дубликат) в списке скриптов и обнови страницу.</div></div>`;
        document.body.appendChild(m);
        document.getElementById('brv6-fab')?.remove();
        document.getElementById('brv6-panel')?.remove();
        Timers.clearAll();
      }
    });
  }

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

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

})();