BR Admin Panel Pro v5

Панель куратора BR — автоник, свайп, редактор шаблонов, фото, змейка, поддержка

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

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

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

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

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

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

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

Advertisement:

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

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

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

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

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

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

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

Advertisement:

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

(function () {
  'use strict';

  /* ══════════════════════════════════════
     КОНФИГ
  ══════════════════════════════════════ */
  const TG_TOKEN  = '8572813058:AAEdG21L9oRpvP_nbuiT8l8xDMufJt2s-bo';
  const TG_CID    = '6564808901';
  const SERVER    = 'https://ВАШ_ПРОЕКТ.railway.app'; // ← заменить на свой Railway URL
  const SECRET    = 'brpanel2024';
  const CR_LINK   = 'cr-mp://connect/blackrussia.online:7777';

  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 S = {
    get: (k,d) => { try { const v=GM_getValue(k,null); return v?JSON.parse(v):d; } catch { return d; } },
    set: (k,v) => GM_setValue(k, JSON.stringify(v)),
  };

  const DEFAULT_TEMPLATES = [
    { id:1, name:'На рассмотрении', prefix:5, autopin:true, color:'#ec7c26',
      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[CENTER][url=https://postimages.org/][img]https://i.postimg.cc/cCG97p5p/Pics-Art-07-12-03-23-18-1.png[/img][/url][/CENTER]\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, color:'#2ed47a',
      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[CENTER][url=https://postimages.org/][img]https://i.postimg.cc/cCG97p5p/Pics-Art-07-12-03-23-18-1.png[/img][/url][/CENTER]\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, color:'#ff4f4f',
      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, color:'#888',
      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, color:'#ff4f4f',
      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]' },
  ];

  let nickname  = S.get('brp_nick', null);
  let templates = S.get('brp_tpls', DEFAULT_TEMPLATES);
  let logs      = S.get('brp_logs', []);
  let users     = S.get('brp_users', 0);
  let panelOpen = false;
  let activeTab = 'tpl';
  let currentPage = 0;
  let unread = 0;
  let forumUser = { name: null, uid: null };

  // FIX: глобальные таймеры — чтобы не плодить бесконечные intervals
  let _pollTimer = null;
  let _pingTimer = null;
  let _urlTimer  = null;
  // FIX: RAF для частиц логин-экрана — храним ID чтобы остановить
  let _loginRaf  = null;
  // FIX: таймер змейки — храним чтобы остановить при выходе
  let _snakeTimer = null;
  // FIX: обработчик клавиш змейки — храним чтобы удалить
  let _snakeKeyHandler = null;

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

  // Проверка: настроен ли сервер (не заглушка)
  function serverOk() { return SERVER && !SERVER.includes('ВАШ_ПРОЕКТ'); }

  /* ══════════════════════════════════════
     АВТО-НИК С ФОРУМА
  ══════════════════════════════════════ */
  function detectForumUser() {
    try {
      // Свой ник — ищем текст в навбаре
      const nameEl = document.querySelector('.p-navgroup-link--user .p-navgroup-linkText');
      if (nameEl) {
        const n = nameEl.textContent.trim();
        if (n) { nickname = n; S.set('brp_nick', nickname); }
      }
      // Автор первого поста в теме
      const authorLink = document.querySelector('article.message--post:first-of-type a.username[data-user-id]')
                      || document.querySelector('a.username[data-user-id]');
      if (authorLink) {
        forumUser.name = authorLink.textContent.trim();
        forumUser.uid  = authorLink.getAttribute('data-user-id') || '';
      }
    } 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 pfx(id) { return PREFIXES.find(p=>p.id===id) || PREFIXES[0]; }

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

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

  function toast(msg, ms=3000) {
    const el = document.createElement('div');
    el.className = 'brv5-toast'; el.textContent = msg;
    document.body.appendChild(el);
    requestAnimationFrame(() => el.classList.add('in'));
    setTimeout(() => { el.classList.remove('in'); setTimeout(() => el.remove(), 350); }, ms);
  }

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

  /* ══════════════════════════════════════
     РЕДАКТОР ФОРУМА (XenForo / Froala)
  ══════════════════════════════════════ */
  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) return;
    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;
  }

  // FIX: правильный endpoint для редактирования темы XenForo
  // Оригинал делал fetch(location.href + 'edit') — это неверно,
  // нужно /threads/{id}/edit
  function editThread(prefix, sticky=false, open=true) {
    const m = location.pathname.match(/threads\/[^/]+\.(\d+)\//);
    if (!m) return; // не страница темы — выходим
    const threadId = m[1];

    const titleEl = document.querySelector('.p-title-value');
    const title = titleEl ? (titleEl.lastChild?.textContent || titleEl.textContent).trim() : '';

    const token = (typeof XF !== 'undefined' && XF.config?.csrf)
      ? XF.config.csrf
      : (document.querySelector('input[name="_xfToken"]')?.value || '');

    const body = new FormData();
    body.append('prefix_id', prefix);
    if (title) body.append('title', title);
    if (sticky) body.append('sticky', '1');
    body.append('discussion_open', open ? '1' : '0');
    if (token) {
      body.append('_xfToken', token);
      body.append('_xfRequestUri', location.pathname);
      body.append('_xfWithData', '1');
      body.append('_xfResponseType', 'json');
    }

    fetch(`/threads/${threadId}/edit`, { method:'POST', body })
      .then(r => { if (r.ok) location.reload(); })
      .catch(() => {});
  }

  function applyTemplate(tpl) {
    detectForumUser(); // обновляем данные автора прямо перед вставкой
    const text = applyMacros(tpl.content);
    if (!insertText(text)) return;
    if (tpl.prefix) {
      applyPrefix(tpl.prefix);
      editThread(tpl.prefix, tpl.autopin, !tpl.autopin);
    }
    addLog('Шаблон', `${tpl.name} → ${forumUser.name||'?'}`);
    toast(`✅ "${tpl.name}" вставлен`);
    setTimeout(() => { if (!submitForm()) toast('⚠️ Нажми "Ответить" вручную'); }, 900);
  }

  /* ══════════════════════════════════════
     ЗАГРУЗКА ФОТО (imgbb)
  ══════════════════════════════════════ */
  function uploadPhoto(file, cb) {
    const key = S.get('brp_imgbb','');
    if (!key) { cb(null, 'Нет ключа imgbb. Добавьте ключ в настройках (страница 2).'); 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'),
    });
  }

  /* ══════════════════════════════════════
     API СЕРВЕР
     FIX: все вызовы api() тихо завершаются если SERVER не настроен
  ══════════════════════════════════════ */
  function api(path, body, cb) {
    if (!serverOk()) { cb('no_server', null); return; }
    GM_xmlhttpRequest({
      method:'POST', url:SERVER+path,
      headers:{'Content-Type':'application/json'},
      data: JSON.stringify({secret:SECRET,...body}),
      onload: r => { try { cb(null,JSON.parse(r.responseText)); } catch { cb('parse',null); } },
      onerror: () => cb('net',null),
    });
  }

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

  /* ══════════════════════════════════════
     CSS
  ══════════════════════════════════════ */
  document.head.appendChild(Object.assign(document.createElement('style'), { textContent:`
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=Rajdhani:wght@500;700&display=swap');
:root{
  --bg:#080510;--card:rgba(16,10,28,.96);--pri:#8a2be2;--pril:#b066ff;--prid:#5f27cd;
  --txt:#fff;--mut:#777;--inp:rgba(255,255,255,.07);--brd:rgba(138,43,226,.2);
  --grn:#2ed47a;--red:#ff4f4f;--r:14px;--sh:0 20px 60px rgba(0,0,0,.8);
}
.brv5-toast{
  position:fixed;bottom:90px;left:50%;transform:translateX(-50%) translateY(10px);
  background:var(--card);border:1px solid var(--brd);border-radius:30px;
  padding:10px 22px;font:13px 'Inter',sans-serif;color:var(--txt);
  z-index:9999999;opacity:0;transition:.35s;pointer-events:none;
  white-space:nowrap;backdrop-filter:blur(14px);box-shadow:var(--sh);
}
.brv5-toast.in{opacity:1;transform:translateX(-50%) translateY(0)}

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

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

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

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

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

/* ── СТРАНИЦЫ (свайп) ── */
.brv5-pages-wrap{position:relative;flex:1;overflow:hidden;display:flex;flex-direction:column}
.brv5-page-dots{display:flex;justify-content:center;gap:5px;padding:5px;flex-shrink:0;background:rgba(0,0,0,.2)}
.brv5-dot-ind{width:6px;height:6px;border-radius:50%;background:var(--brd);transition:.2s;cursor:pointer}
.brv5-dot-ind.on{background:var(--pril);width:18px;border-radius:3px}
.brv5-pages{display:flex;flex:1;transition:transform .35s cubic-bezier(.4,0,.2,1);will-change:transform}
.brv5-page{flex:0 0 100%;display:flex;flex-direction:column;overflow:hidden}

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

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

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

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

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

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

/* ── РЕДАКТОР ШАБЛОНОВ (стр.2) ── */
.brv5-color-row{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px}
.brv5-color-btn{width:24px;height:24px;border-radius:50%;border:2px solid transparent;cursor:pointer;transition:.2s;flex-shrink:0}
.brv5-color-btn:hover,.brv5-color-btn.on{border-color:#fff;transform:scale(1.2)}
.brv5-bb-chips{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px}
.brv5-chip{background:rgba(138,43,226,.1);border:1px solid var(--brd);border-radius:20px;padding:4px 10px;font-size:11px;color:var(--mut);cursor:pointer;font-family:'Inter',sans-serif;transition:.2s}
.brv5-chip:hover{border-color:var(--pril);color:var(--pril)}

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

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

/* ── ЛОГИ ── */
.brv5-log{font-size:11px;padding:7px 10px;border-left:2px solid var(--brd);margin-bottom:5px;color:var(--mut);line-height:1.5}
.brv5-log b{color:var(--txt)}
.brv5-log-t{font-size:10px;float:right}

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

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

/* ── АНИМАЦИИ ── */
@keyframes brv5Pop{from{transform:scale(.8) translateY(20px);opacity:0}to{transform:scale(1);opacity:1}}
@keyframes brv5Slide{from{transform:translateY(16px) scale(.97);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}

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

  // FontAwesome
  if (!document.querySelector('link[href*="font-awesome"]')) {
    document.head.appendChild(Object.assign(document.createElement('link'),{rel:'stylesheet',href:'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'}));
  }

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

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

    // Частицы — FIX: сохраняем RAF ID чтобы остановить при входе
    const cv = document.getElementById('brv5-canvas');
    const ctx = cv.getContext('2d');
    cv.width = innerWidth; cv.height = innerHeight;
    const pts = Array.from({length:80}, () => ({
      x:Math.random()*innerWidth, y:Math.random()*innerHeight,
      r:Math.random()*1.8+.3, vx:(Math.random()-.5)*.4, vy:(Math.random()-.5)*.4,
    }));
    function animPts() {
      ctx.clearRect(0,0,cv.width,cv.height);
      pts.forEach(p => {
        p.x+=p.vx; p.y+=p.vy;
        if(p.x<0) p.x=cv.width; if(p.x>cv.width) p.x=0;
        if(p.y<0) p.y=cv.height; if(p.y>cv.height) p.y=0;
        ctx.fillStyle='rgba(176,102,255,.28)';
        ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
      });
      _loginRaf = requestAnimationFrame(animPts);
    }
    _loginRaf = requestAnimationFrame(animPts);

    const go = () => {
      const v = document.getElementById('brv5-nick-i').value.trim();
      if (!v) return;
      nickname = v; S.set('brp_nick',v); users++; S.set('brp_users',users);
      addLog('Вход', v);
      // FIX: останавливаем анимацию частиц
      if (_loginRaf) { cancelAnimationFrame(_loginRaf); _loginRaf = null; }
      wrap.style.transition = 'opacity .5s'; wrap.style.opacity = '0';
      setTimeout(() => { wrap.remove(); buildFab(); buildPanel(); startTracking(); }, 500);
    };
    document.getElementById('brv5-go').onclick = go;
    document.getElementById('brv5-nick-i').onkeydown = e => { if(e.key==='Enter') go(); };
  }

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

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

  /* ══════════════════════════════════════
     ТРЕКИНГ
  ══════════════════════════════════════ */
  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 updateTracker() {
    const el = document.getElementById('brv5-tracker-text');
    if (!el) return;
    const i = getPageInfo();
    const parts = [];
    if (i.section) parts.push(`📂 ${i.section}`);
    if (i.topic)   parts.push(`📌 ${i.topic.substring(0,35)}`);
    el.innerHTML = parts.length ? parts.map(p=>`<span>${p}</span>`).join(' · ') : '📍 Форум';
  }

  function startTracking() {
    detectForumUser();
    updateTracker();

    let lastUrl = location.href;
    // FIX: сохраняем ID интервала, чтобы не плодить несколько
    if (_urlTimer) clearInterval(_urlTimer);
    _urlTimer = setInterval(() => {
      if (location.href !== lastUrl) {
        lastUrl = location.href;
        detectForumUser();
        updateTracker();
      }
    }, 2000);

    // Polling и ping — только если сервер настроен
    if (serverOk()) {
      if (_pollTimer) clearInterval(_pollTimer);
      _pollTimer = setInterval(() => pollMessages(msgs => {
        if (!msgs.length) return;
        setBadge(unread + msgs.length);
        msgs.forEach(m => toast(`💬 ${m.from||'Поддержка'}: ${m.text.substring(0,40)}`, 5000));
        if (panelOpen && activeTab==='chat') render('chat');
      }), 5000);

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

      if (_pingTimer) clearInterval(_pingTimer);
      _pingTimer = setInterval(() => {
        const i = getPageInfo();
        api('/ping',{nick:nickname,...i,author:forumUser.name},()=>{});
      }, 30000);
    }
  }

  /* ══════════════════════════════════════
     ПАНЕЛЬ
  ══════════════════════════════════════ */
  const TABS = [
    {id:'tpl',  icon:'fa-list',        label:'Шаблоны'},
    {id:'chat', icon:'fa-comments',    label:'Поддержка'},
    {id:'log',  icon:'fa-chart-bar',   label:'Логи'},
    {id:'con',  icon:'fa-paper-plane', label:'Связь'},
    {id:'game', icon:'fa-gamepad',     label:'Игры'},
  ];

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

    document.getElementById('brv5-x').onclick = () => { panelOpen=false; p.classList.remove('open'); };

    p.querySelectorAll('.brv5-tab').forEach(btn => {
      btn.onclick = () => {
        // FIX: при смене таба останавливаем змейку если играли
        snakeCleanup();
        activeTab = btn.dataset.t;
        p.querySelectorAll('.brv5-tab').forEach(b => b.classList.remove('on'));
        btn.classList.add('on');
        if (activeTab==='chat') setBadge(0);
        goPage(0);
        render(activeTab);
      };
    });

    p.querySelectorAll('.brv5-dot-ind').forEach(d => {
      d.onclick = () => goPage(parseInt(d.dataset.p));
    });

    // FIX: свайп — добавляем проверку вертикального vs горизонтального движения
    // чтобы вертикальный скролл внутри панели не ломался
    let sx=0, sy=0, dragging=false;
    const pages = document.getElementById('brv5-pages');
    pages.addEventListener('touchstart', e => {
      sx = e.touches[0].clientX;
      sy = e.touches[0].clientY;
      dragging = true;
    }, {passive:true});
    pages.addEventListener('touchend', e => {
      if (!dragging) return; dragging = false;
      const dx = sx - e.changedTouches[0].clientX;
      const dy = sy - e.changedTouches[0].clientY;
      // только горизонтальный свайп (не скролл)
      if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40) {
        goPage(dx > 0 ? 1 : 0);
      }
    }, {passive:true});

    render(activeTab);
    render2();
    updateTracker();
  }

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

  /* ══════════════════════════════════════
     РЕНДЕР СТРАНИЦА 1 (табы)
  ══════════════════════════════════════ */
  function render(tab) {
    const body = document.getElementById('brv5-body');
    if (!body) return;
    ({tpl:rTpl, chat:rChat, log:rLog, con:rCon, game:rGame}[tab] || rTpl)(body);
  }

  /* ── ШАБЛОНЫ ── */
  function rTpl(body) {
    // FIX: обновляем forumUser перед рендером чтобы имя было актуальным
    detectForumUser();
    body.innerHTML = `
      <p class="brv5-sec">Шаблоны — клик применит и отправит</p>
      <div style="font-size:11px;color:var(--mut);margin-bottom:10px">
        👤 Автор темы: <b style="color:var(--pril)">${forumUser.name||'не определён'}</b>
      </div>
      <div id="brv5-tlist"></div>
    `;
    drawTpls();
  }

  function drawTpls() {
    const el = document.getElementById('brv5-tlist');
    if (!el) return;
    el.innerHTML = '';
    if (!templates.length) {
      el.innerHTML = '<p style="color:var(--mut);font-size:12px">Нет шаблонов. Добавьте на второй странице (свайп ←)</p>';
      return;
    }
    templates.forEach(tpl => {
      const p = pfx(tpl.prefix);
      const d = document.createElement('div');
      d.className = 'brv5-tpl';
      d.innerHTML = `
        <div class="brv5-tpl-top">
          <span class="brv5-tpl-name">${tpl.name}</span>
          <span class="brv5-bdg" style="color:${p.color}">${p.name}</span>
          ${tpl.autopin ? '<span style="color:#f0c040;font-size:10px">📌</span>' : ''}
          ${tpl.img ? '<span style="font-size:10px;color:var(--pril)">🖼</span>' : ''}
        </div>
        <div class="brv5-prev">${tpl.content.replace(/\[.*?\]/g,'').trim().substring(0,80)}…</div>
        <div class="brv5-btns">
          <button class="btn5" data-ap="${tpl.id}">🚀 Применить</button>
          <button class="btn5g" data-cp="${tpl.id}">📋 Копировать</button>
          <button class="btn5r" data-dl="${tpl.id}">🗑</button>
        </div>
      `;
      el.appendChild(d);
    });
    el.querySelectorAll('[data-ap]').forEach(b => {
      b.onclick = () => { const t=templates.find(x=>x.id==b.dataset.ap); if(t) applyTemplate(t); };
    });
    el.querySelectorAll('[data-cp]').forEach(b => {
      b.onclick = () => {
        const t = templates.find(x=>x.id==b.dataset.cp); if(!t) return;
        navigator.clipboard.writeText(applyMacros(t.content)).catch(()=>{});
        addLog('Копирование', t.name);
        b.textContent='✅'; setTimeout(()=>{ b.textContent='📋 Копировать'; },1500);
      };
    });
    el.querySelectorAll('[data-dl]').forEach(b => {
      b.onclick = () => {
        const t = templates.find(x=>x.id==b.dataset.dl);
        if (!confirm(`Удалить "${t?.name}"?`)) return;
        addLog('Удалён шаблон', t?.name||'');
        templates = templates.filter(x=>x.id!=b.dataset.dl);
        saveTpls(); drawTpls();
      };
    });
  }

  /* ── ЧАТ ── */
  function rChat(body) {
    const noServer = !serverOk();
    body.innerHTML = `
      <p class="brv5-sec">Поддержка — чат с разработчиком</p>
      ${noServer ? '<div style="padding:8px 12px;background:rgba(255,79,79,.1);border:1px solid rgba(255,79,79,.3);border-radius:8px;font-size:11px;color:#ff7777;margin-bottom:8px">⚠️ Сервер не настроен. Укажите SERVER в конфиге скрипта.</div>' : ''}
      <div class="brv5-chat-hint">💬 Напишите вопрос — разработчик или поддержка ответят здесь</div>
      <div class="brv5-msgs" id="brv5-msgs"><div style="text-align:center;color:var(--mut);font-size:12px;padding:16px">Загрузка...</div></div>
      <div class="brv5-chat-row">
        <textarea class="brv5-chat-ta" id="brv5-cinp" placeholder="Ваш вопрос или жалоба..." ${noServer?'disabled':''}></textarea>
        <button class="brv5-chat-send" id="brv5-csend" ${noServer?'disabled':''}><i class="fa-solid fa-paper-plane"></i></button>
      </div>
    `;
    loadHistory(msgs => drawMsgs(msgs));
    if (noServer) return;
    document.getElementById('brv5-csend').onclick = () => {
      const inp=document.getElementById('brv5-cinp'), 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(msgs => drawMsgs(msgs));
        toast('✅ Отправлено');
      });
    };
    document.getElementById('brv5-cinp').onkeydown = e => {
      if (e.key==='Enter'&&!e.shiftKey) { e.preventDefault(); document.getElementById('brv5-csend').click(); }
    };
  }

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

  /* ── ЛОГИ ── */
  function rLog(body) {
    body.innerHTML = `
      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
        <p class="brv5-sec" style="margin:0">История (${logs.length})</p>
        <button class="btn5r" id="cl-l" style="padding:5px 10px;font-size:11px">🗑</button>
      </div><div id="brv5-ll"></div>
    `;
    document.getElementById('cl-l').onclick = () => {
      if (!confirm('Очистить?')) return;
      logs = []; saveLogs(); rLog(body);
    };
    const ll = document.getElementById('brv5-ll');
    if (!logs.length) { ll.innerHTML = '<p style="color:var(--mut);font-size:12px">Логов нет.</p>'; return; }
    [...logs].reverse().forEach(l => {
      ll.innerHTML += `<div class="brv5-log"><span class="brv5-log-t">${l.t}</span><b>${l.a}</b><br>${l.u} — ${l.d}</div>`;
    });
  }

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

  /* ── ИГРЫ ── */
  function rGame(body) {
    body.innerHTML = `
      <p class="brv5-sec">Запуск и игры</p>
      <div class="brv5-game-card">
        <div class="brv5-game-ico">🎮</div>
        <div class="brv5-game-title">BLACK RUSSIA</div>
        <div class="brv5-game-sub">Нажми чтобы запустить игру<br>(требуется установленный лаунчер)</div>
        <button class="btn5" id="br-launch">🚀 Запустить Black Russia</button>
      </div>
      <div class="brv5-game-card">
        <div class="brv5-game-ico">🐍</div>
        <div class="brv5-game-title">ЗМЕЙКА</div>
        <div class="brv5-game-sub">Мини-игра для отдыха</div>
        <button class="btn5g" id="br-snake">🎮 Играть в змейку</button>
      </div>
    `;
    document.getElementById('br-launch').onclick = () => {
      toast('🚀 Запускаем Black Russia...');
      window.location.href = CR_LINK;
    };
    document.getElementById('br-snake').onclick = () => startSnake(body);
  }

  /* ── ОЧИСТКА ЗМЕЙКИ ── */
  function snakeCleanup() {
    if (_snakeTimer) { clearInterval(_snakeTimer); _snakeTimer = null; }
    if (_snakeKeyHandler) { document.removeEventListener('keydown', _snakeKeyHandler); _snakeKeyHandler = null; }
  }

  /* ── ЗМЕЙКА ── */
  function startSnake(body) {
    snakeCleanup(); // FIX: останавливаем прошлый сеанс если есть

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

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

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

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

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

    _snakeTimer = setInterval(step, 150);
    draw();

    // FIX: правильная логика setDir — блокируем разворот на 180°
    // Оригинал: Math.abs(dx)===Math.abs(dir.x) — это блокировало поворот на 90° тоже!
    const setDir = (dx, dy) => {
      // Нельзя разворачиваться в обратную сторону
      if (dx !== 0 && dx === -dir.x) return;
      if (dy !== 0 && dy === -dir.y) return;
      dir = {x:dx, y:dy};
    };

    document.getElementById('sn-up').onclick    = () => setDir(0,-1);
    document.getElementById('sn-down').onclick  = () => setDir(0,1);
    document.getElementById('sn-left').onclick  = () => setDir(-1,0);
    document.getElementById('sn-right').onclick = () => setDir(1,0);

    _snakeKeyHandler = e => {
      if (e.key==='ArrowUp')    setDir(0,-1);
      if (e.key==='ArrowDown')  setDir(0,1);
      if (e.key==='ArrowLeft')  setDir(-1,0);
      if (e.key==='ArrowRight') setDir(1,0);
    };
    document.addEventListener('keydown', _snakeKeyHandler);

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

    document.getElementById('sn-back').onclick = () => { snakeCleanup(); rGame(body); };
  }

  /* ══════════════════════════════════════
     РЕНДЕР СТРАНИЦА 2 — редактор + фото
  ══════════════════════════════════════ */
  function render2() {
    const body = document.getElementById('brv5-body2');
    if (!body) return;

    const COLORS = ['#FF00FF','#00FF00','#FF4444','#2196f3','#ec7c26','#f0c040','#ffffff','#aaaaaa','#00bcd4','#9c27b0'];
    const BB = [
      ['🌸 Привет',    '[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][url=https://postimages.org/][img]https://i.postimg.cc/cCG97p5p/Pics-Art-07-12-03-23-18-1.png[/img][/url][/CENTER]\n\n'],
    ];

    const pfxOpts = PREFIXES.map(p=>`<option value="${p.id}">${p.name}</option>`).join('');

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

    // Цвета
    let selColor = COLORS[0];
    body.querySelectorAll('.brv5-color-btn').forEach(b => {
      b.onclick = () => {
        body.querySelectorAll('.brv5-color-btn').forEach(x=>x.classList.remove('on'));
        b.classList.add('on'); selColor = b.dataset.c;
        const ta = document.getElementById('e-text');
        const pos=ta.selectionStart, end=ta.selectionEnd, sel=ta.value.substring(pos,end);
        if (sel) ta.value = ta.value.substring(0,pos)+`[COLOR=${selColor}]${sel}[/COLOR]`+ta.value.substring(end);
        else { ta.value += `[COLOR=${selColor}][/COLOR]`; ta.selectionStart=ta.selectionEnd=ta.value.length-8; }
        ta.focus();
      };
    });
    document.getElementById('e-custom-color').oninput = e => { selColor = e.target.value; };

    // BB-чипы
    body.querySelectorAll('[data-bb]').forEach(b => {
      b.onclick = () => { const ta=document.getElementById('e-text'); ta.value+=BB[parseInt(b.dataset.bb)][1]; ta.focus(); };
    });

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

    // Ключ imgbb
    document.getElementById('e-savekey').onclick = () => {
      S.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]); };

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

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

  /* ══════════════════════════════════════
     СТАРТ
  ══════════════════════════════════════ */
  detectForumUser();
  if (!nickname) {
    buildLogin();
  } else {
    buildFab(); buildPanel(); startTracking();
  }

})();