BR Admin Panel Pro v5

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

Advertisement:

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();