BR Admin Panel Pro v7

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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

})();