TBC Client (Lite)

A userscript with modded settings for the bonk collective server.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         TBC Client (Lite)
// @namespace    https://greasyfork.org/users/1552147-ansonii-crypto
// @version      1.3.2
// @description  A userscript with modded settings for the bonk collective server.
// @match        https://bonk.io/gameframe-release.html
// @run-at       document-start
// @grant        GM_xmlhttpRequest
// @connect      api.github.com
// @license      N/A
// ==/UserScript==

(() => {
  'use strict';

  function $(id) {
    return document.getElementById(id);
  }

  function waitForElement(id, cb, timeoutMs = 30000) {
    const start = Date.now();
    const int = setInterval(() => {
      const el = $(id);
      if (el) {
        clearInterval(int);
        cb(el);
      } else if (Date.now() - start > timeoutMs) {
        clearInterval(int);
      }
    }, 200);
  }

  (() => {
    const global = window;
    global.bonkMods = global.bonkMods || {};
    const bonkMods = global.bonkMods;

    bonkMods._categories = bonkMods._categories || {};
    bonkMods._blocks = bonkMods._blocks || [];
    bonkMods._mods = bonkMods._mods || {};

    const DEFAULT_CATEGORY_ID = 'general';
    const FALLBACK_MOD_ID = 'other';

    let currentCategoryId = null;
    let currentModId = 'all';

    bonkMods.registerMod = function registerMod(meta) {
      if (!meta || !meta.id) return;
      const id = meta.id;

      const existing = bonkMods._mods[id] || {};
      bonkMods._mods[id] = Object.assign(
        {
          id,
          name: id,
          version: '',
          author: '',
          description: '',
          homepage: '',
          devHint: '',
        },
        existing,
        meta
      );

      renderModDropdown();
      renderModInfo();
      renderCategories();
      renderBlocks();
    };

    bonkMods.registerCategory = function registerCategory(def) {
      if (!def || !def.id) return;
      if (!def.label) def.label = def.id;
      if (typeof def.order !== 'number') def.order = 100;

      if (!bonkMods._categories[def.id]) {
        bonkMods._categories[def.id] = { id: def.id, label: def.label, order: def.order };
      } else {
        Object.assign(bonkMods._categories[def.id], def);
      }

      renderCategories();
      renderBlocks();
    };

    bonkMods.addBlock = function addBlock(def) {
      if (!def || !def.id || typeof def.render !== 'function') return;

      if (!def.categoryId) def.categoryId = DEFAULT_CATEGORY_ID;
      if (!def.title) def.title = '';
      if (typeof def.order !== 'number') def.order = 100;
      if (!def.modId) def.modId = FALLBACK_MOD_ID;

      if (bonkMods._blocks.some((b) => b.id === def.id)) return;

      bonkMods._blocks.push(def);

      if (!bonkMods._categories[def.categoryId]) {
        bonkMods.registerCategory({
          id: def.categoryId,
          label: def.categoryId.charAt(0).toUpperCase() + def.categoryId.slice(1),
          order: 100,
        });
      }

      if (!bonkMods._mods[def.modId] && def.modId === FALLBACK_MOD_ID) {
        bonkMods.registerMod({
          id: FALLBACK_MOD_ID,
          name: 'Other Mods',
          description: 'Blocks from mods that did not register themselves.',
        });
      }

      renderModDropdown();
      renderCategories();
      renderBlocks();
    };

    bonkMods.addModdedBlock = function addModdedBlock(def) {
      if (!def) return;
      bonkMods.addBlock(Object.assign({ categoryId: DEFAULT_CATEGORY_ID }, def));
    };

    function setupSettingsShell(settingsContainer) {
      const topBar = $('settings_topBar');
      const closeBtn = $('settings_close');
      if (!topBar || !closeBtn) return;

      if ($('mod_tabs')) {
        renderModDropdown();
        renderModInfo();
        renderCategories();
        renderBlocks();
        return;
      }

      topBar.style.display = 'flex';
      topBar.style.alignItems = 'center';
      topBar.style.padding = '0 10px';
      topBar.style.boxSizing = 'border-box';

      const title = document.createElement('div');
      title.textContent = 'Settings';
      title.style.fontWeight = 'bold';
      title.style.marginRight = '12px';

      const tabs = document.createElement('div');
      tabs.id = 'mod_tabs';
      tabs.style.display = 'flex';
      tabs.style.gap = '6px';
      tabs.style.margin = '0 auto';
      tabs.innerHTML = `
        <div class="brownButton brownButton_classic buttonShadow mod_tab active" data-tab="generic">
          Generic
        </div>
        <div class="brownButton brownButton_classic buttonShadow mod_tab mod_tab_modded" data-tab="modded">
          Modded <span id="mod_tab_modname" style="font-weight:normal;opacity:0.7;"></span> ▾
        </div>
      `;

      closeBtn.style.position = 'static';
      closeBtn.style.marginLeft = 'auto';

      topBar.textContent = '';
      topBar.appendChild(title);
      topBar.appendChild(tabs);
      topBar.appendChild(closeBtn);

      if (!$('bonk_mod_core_css')) {
        const style = document.createElement('style');
        style.id = 'bonk_mod_core_css';
        style.textContent = `
          .mod_tab {
            padding: 4px 10px !important;
            font-size: 13px;
            line-height: normal;
            height: auto !important;
            opacity: 0.75;
            position: relative;
          }
          .mod_tab.active { opacity: 1; }

          #mod_dropdown {
            position: absolute;
            top: 100%;
            left: 0;
            margin-top: 4px;
            background: rgba(16, 27, 38, 0.98);
            border-radius: 6px;
            box-shadow: 0 8px 20px rgba(0,0,0,0.6);
            padding: 6px;
            min-width: 200px;
            z-index: 100000;
            display: none;
          }
          #mod_dropdown_title {
            font-size: 11px;
            text-transform: uppercase;
            opacity: .8;
            margin-bottom: 4px;
          }
          .mod_dropdown_item {
            font-size: 12px;
            padding: 4px 6px;
            border-radius: 4px;
            cursor: pointer;
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 6px;
          }
          .mod_dropdown_item small { opacity: .7; font-size: 10px; }
          .mod_dropdown_item:hover { background: rgba(255,255,255,0.08); }
          .mod_dropdown_item.active { background: rgba(121,85,248,0.4); }

          #mod_modded_settings {
            display: flex;
            flex-direction: column;
            height: 100%;
            box-sizing: border-box;
            margin-top: 4px;
          }
          #mod_modinfo {
            padding: 4px 7px 6px 7px;
            margin-bottom: 6px;
            border-radius: 4px;
            background: rgba(0,0,0,0.18);
            font-size: 11px;
          }
          #mod_modinfo_title { font-weight: bold; font-size: 12px; }
          #mod_modinfo_meta { opacity: .8; margin: 1px 0 3px 0; }
          #mod_modinfo_desc { opacity: .9; }
          #mod_modinfo_link a { color: #9fd4ff; text-decoration: underline; }

          #mod_cat_tabs {
            display: flex;
            gap: 6px;
            margin: 4px 0 6px 0;
            flex-wrap: wrap;
          }
          .mod_cat_tab {
            padding: 3px 9px !important;
            font-size: 12px;
            height: auto !important;
            cursor: pointer;
            opacity: .75;
          }
          .mod_cat_tab.active {
            opacity: 1;
            outline: 1px solid rgba(255,255,255,0.25);
          }

          #mod_blocks_scroll {
            position: relative;
            flex: 1 1 auto;
            overflow-y: auto;
            overflow-x: hidden;
            padding-right: 6px;
            border-radius: 4px;
            background: rgba(0,0,0,0.1);
            scrollbar-width: thin;
            scrollbar-color: #32485d rgba(0,0,0,0.25);
          }
          #mod_blocks_scroll::-webkit-scrollbar { width: 8px; }
          #mod_blocks_scroll::-webkit-scrollbar-track {
            background: rgba(0,0,0,0.25);
            border-radius: 4px;
          }
          #mod_blocks_scroll::-webkit-scrollbar-thumb {
            background: linear-gradient(#32485d, #182430);
            border-radius: 4px;
            border: 1px solid rgba(255,255,255,0.15);
          }
          #mod_blocks_scroll::-webkit-scrollbar-thumb:hover {
            background: linear-gradient(#3e566d, #1f3140);
          }

          .mod_block {
            padding: 8px 6px 10px 6px;
            border-bottom: 1px solid rgba(255,255,255,0.08);
          }
          .mod_block:first-child { border-top: 1px solid rgba(255,255,255,0.08); }
          .mod_block_title { font-weight: bold; font-size: 13px; }
          .mod_block_sub {
            font-size: 11px;
            opacity: .8;
            margin-top: 2px;
            margin-bottom: 6px;
          }

          .tbc_board {
            border-radius: 10px;
            padding: 10px 10px 9px 10px;
            background: linear-gradient(180deg, rgba(121,85,248,0.18), rgba(0,0,0,0.15));
            border: 1px solid rgba(255,255,255,0.12);
            box-shadow: 0 6px 16px rgba(0,0,0,0.35);
          }
          .tbc_board_header {
            display: flex;
            align-items: baseline;
            justify-content: space-between;
            gap: 10px;
            margin-bottom: 6px;
          }
          .tbc_board_title {
            font-weight: 800;
            font-size: 14px;
            letter-spacing: 0.2px;
          }
          .tbc_board_badge {
            font-size: 10px;
            opacity: 0.85;
            padding: 2px 6px;
            border-radius: 999px;
            border: 1px solid rgba(255,255,255,0.14);
            background: rgba(0,0,0,0.18);
            white-space: nowrap;
          }
          .tbc_board_section {
            margin-top: 8px;
            padding-top: 8px;
            border-top: 1px solid rgba(255,255,255,0.08);
          }
          .tbc_board_h {
            font-weight: 700;
            font-size: 12px;
            margin-bottom: 4px;
          }
          .tbc_board_p {
            font-size: 11px;
            opacity: 0.9;
            line-height: 1.35;
          }
          .tbc_board_list {
            margin: 6px 0 0 0;
            padding-left: 16px;
            font-size: 11px;
            opacity: 0.92;
            line-height: 1.35;
          }
          .tbc_board_link {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            margin-top: 6px;
            padding: 6px 8px;
            border-radius: 8px;
            border: 1px solid rgba(159,212,255,0.35);
            background: rgba(0,0,0,0.18);
            color: #9fd4ff;
            text-decoration: none;
            font-size: 11px;
          }
          .tbc_board_link:hover {
            background: rgba(159,212,255,0.08);
            border-color: rgba(159,212,255,0.55);
          }
          .tbc_board_note {
            margin-top: 6px;
            font-size: 10px;
            opacity: 0.75;
          }
        `;
        document.head.appendChild(style);
      }

      const genericWrap = document.createElement('div');
      genericWrap.id = 'mod_generic_settings';

      const moddedWrap = document.createElement('div');
      moddedWrap.id = 'mod_modded_settings';
      moddedWrap.style.display = 'none';
      moddedWrap.style.padding = '10px 10px 6px 10px';

      const modInfo = document.createElement('div');
      modInfo.id = 'mod_modinfo';
      modInfo.innerHTML = `
        <div id="mod_modinfo_title"></div>
        <div id="mod_modinfo_meta"></div>
        <div id="mod_modinfo_desc"></div>
        <div id="mod_modinfo_link"></div>
      `;

      const catTabs = document.createElement('div');
      catTabs.id = 'mod_cat_tabs';

      const blocksScroll = document.createElement('div');
      blocksScroll.id = 'mod_blocks_scroll';

      moddedWrap.appendChild(modInfo);
      moddedWrap.appendChild(catTabs);
      moddedWrap.appendChild(blocksScroll);

      [...settingsContainer.children].forEach((el) => {
        if (
          el.id !== 'settings_topBar' &&
          el.id !== 'settings_close' &&
          el.id !== 'settings_cancelButton' &&
          el.id !== 'settings_saveButton'
        ) {
          genericWrap.appendChild(el);
        }
      });

      settingsContainer.insertBefore(genericWrap, settingsContainer.children[1]);
      settingsContainer.insertBefore(moddedWrap, $('settings_cancelButton'));

      tabs.querySelectorAll('.mod_tab').forEach((tab) => {
        tab.addEventListener('click', (e) => {
          const which = tab.dataset.tab;

          tabs.querySelectorAll('.mod_tab').forEach((t) => t.classList.toggle('active', t === tab));

          const isModded = which === 'modded';
          genericWrap.style.display = isModded ? 'none' : 'block';
          moddedWrap.style.display = isModded ? 'flex' : 'none';

          if (isModded) {
                e.stopPropagation();
                hideModDropdown();
            } else {
                hideModDropdown();
            }
        });
      });

      const moddedTab = topBar.querySelector('.mod_tab_modded');
      const dropdown = document.createElement('div');
      dropdown.id = 'mod_dropdown';
      dropdown.innerHTML = `
        <div id="mod_dropdown_title">Select mod</div>
        <div id="mod_dropdown_list"></div>
      `;
      moddedTab.style.position = 'relative';
      moddedTab.appendChild(dropdown);

      document.addEventListener('click', (e) => {
        if (!dropdown.contains(e.target) && !moddedTab.contains(e.target)) hideModDropdown();
      });

      bonkMods.registerCategory({ id: DEFAULT_CATEGORY_ID, label: 'General', order: 0 });

      bonkMods._mods.all = {
        id: 'all',
        name: 'All Mods',
        description: 'Show settings for every registered mod.',
        version: '1.3.2',
        author: 'SIoppy',
        homepage: '',
      };

      bonkMods.registerMod({
        id: '__tbc',
        name: "The Bonk Collective (TBC)",
        version: '1.3.2',
        author: 'SIoppy',
        description: 'About this script, its purpose, and community links.',
        homepage: 'https://discord.gg/RUm7wZHrHu',
        devHint: 'Community-driven Bonk.io quality-of-life tools.',
      });

      bonkMods.registerCategory({ id: '__tbc_info', label: 'Information', order: 999 });

      bonkMods.addBlock({
        id: '__tbc_board',
        modId: '__tbc',
        categoryId: '__tbc_info',
        title: 'TBC Info Board',
        order: 0,
        render(container) {
          container.innerHTML = `
            <div class="tbc_board">
              <div class="tbc_board_header">
                <div class="tbc_board_title">The Bonk Collective (TBC)</div>
                <div class="tbc_board_badge">Script Info</div>
              </div>

              <div class="tbc_board_p">
                This userscript bundles a modded Settings UI plus community features (like Re:Color + preset colours) to make Bonk.io
                customization easier and more organized.
              </div>

              <div class="tbc_board_section">
                <div class="tbc_board_h">Intended purpose</div>
                <ul class="tbc_board_list">
                  <li>Provide a clean “Modded Settings” hub for scripts.</li>
                  <li>Ship curated QoL tools under one umbrella.</li>
                  <li>Keep settings grouped, consistent, and easy to manage.</li>
                </ul>
              </div>

              <div class="tbc_board_section">
                <div class="tbc_board_h">Discord connection</div>
                <div class="tbc_board_p">
                  TBC coordinates updates, feedback, and feature requests through Discord.
                </div>
                <a class="tbc_board_link" href="https://discord.gg/RUm7wZHrHu" target="_blank" rel="noopener">
                  <span style="font-weight:700;">Join the TBC Discord</span>
                  <span style="opacity:.75;">(invite)</span>
                </a>
                <div class="tbc_board_note">
                  Tip: If you’re in guest mode, some settings may be temporary.
                </div>
              </div>
            </div>
          `;
        },
      });

      global.dispatchEvent(new Event('bonkModsReady'));

      renderModDropdown();
      renderModInfo();
      renderCategories();
      renderBlocks();
    }

    function toggleModDropdown() {
      const dd = $('mod_dropdown');
      if (!dd) return;
      dd.style.display = dd.style.display === 'none' || dd.style.display === '' ? 'block' : 'none';
    }

    function hideModDropdown() {
      const dd = $('mod_dropdown');
      if (!dd) return;
      dd.style.display = 'none';
    }

    function renderModDropdown() {
      const listEl = $('mod_dropdown_list');
      const modNameSpan = $('mod_tab_modname');
      if (!listEl || !bonkMods._mods) return;

      if (!bonkMods._mods.all) {
        bonkMods._mods.all = { id: 'all', name: 'All Mods', description: '', version: '', author: '' };
      }

      if (!currentModId || !bonkMods._mods[currentModId]) currentModId = 'all';

      const modsArr = Object.values(bonkMods._mods);
      if (!modsArr.length) return;

      modsArr.sort((a, b) => {
        if (a.id === 'all') return -1;
        if (b.id === 'all') return 1;
        if ((a.name || '') === (b.name || '')) return a.id > b.id ? 1 : -1;
        return (a.name || '').localeCompare(b.name || '');
      });

      listEl.textContent = '';
      modsArr.forEach((mod) => {
        const item = document.createElement('div');
        item.className = `mod_dropdown_item${mod.id === currentModId ? ' active' : ''}`;
        item.dataset.modId = mod.id;
        item.innerHTML = `<span>${mod.name || mod.id}</span><small>${mod.version || ''}</small>`;

        item.addEventListener('click', (e) => {
          e.stopPropagation();
          currentModId = mod.id;
          renderModDropdown();
          renderModInfo();
          renderCategories();
          renderBlocks();
          hideModDropdown();
        });

        listEl.appendChild(item);
      });

      if (modNameSpan && bonkMods._mods[currentModId]) {
        const label = currentModId === 'all' ? '' : `(${bonkMods._mods[currentModId].name})`;
        modNameSpan.textContent = label;
      }
    }

    function renderModInfo() {
      const mod = bonkMods._mods[currentModId] || bonkMods._mods.all;

      const t = $('mod_modinfo_title');
      const m = $('mod_modinfo_meta');
      const d = $('mod_modinfo_desc');
      const l = $('mod_modinfo_link');
      if (!t || !m || !d || !l || !mod) return;

      t.textContent = mod.name || 'All Mods';
      m.textContent = [mod.version ? `v${mod.version}` : '', mod.author ? `by ${mod.author}` : '']
        .filter(Boolean)
        .join(' · ');

      d.textContent = '';
      const desc = document.createElement('div');
      desc.textContent = mod.description || '';
      d.appendChild(desc);

      if (mod.id === '__tbc' && mod.devHint) {
        const extra = document.createElement('div');
        extra.style.fontSize = '10px';
        extra.style.opacity = '0.8';
        extra.style.marginTop = '3px';
        extra.textContent = mod.devHint;
        d.appendChild(extra);
      }

      if (mod.homepage) {
        l.innerHTML = `<a href="${mod.homepage}" target="_blank" rel="noopener">Open page</a>`;
      } else {
        l.textContent = '';
      }
    }

    function renderCategories() {
      const catTabs = $('mod_cat_tabs');
      if (!catTabs) return;

      const blocks = bonkMods._blocks || [];
      const usedCatIds = new Set();

      blocks.forEach((b) => {
        if (currentModId === 'all' || b.modId === currentModId) usedCatIds.add(b.categoryId);
      });

      const allCats = Object.values(bonkMods._categories).filter((c) => usedCatIds.has(c.id));
      if (!allCats.length) {
        catTabs.textContent = '';
        currentCategoryId = null;
        const scroll = $('mod_blocks_scroll');
        if (scroll) scroll.textContent = '';
        return;
      }

      allCats.sort((a, b) => (a.order === b.order ? a.label.localeCompare(b.label) : a.order - b.order));

      if (!currentCategoryId || !usedCatIds.has(currentCategoryId)) currentCategoryId = allCats[0].id;

      catTabs.textContent = '';
      allCats.forEach((cat) => {
        const btn = document.createElement('div');
        btn.className = `brownButton brownButton_classic buttonShadow mod_cat_tab${
          cat.id === currentCategoryId ? ' active' : ''
        }`;
        btn.dataset.catId = cat.id;
        btn.textContent = cat.label;

        btn.addEventListener('click', () => {
          currentCategoryId = cat.id;
          renderCategories();
          renderBlocks();
        });

        catTabs.appendChild(btn);
      });
    }

    function renderBlocks() {
      const scroll = $('mod_blocks_scroll');
      if (!scroll) return;

      const blocks = bonkMods._blocks || [];
      scroll.textContent = '';
      scroll.scrollTop = 0;

      if (!blocks.length || !currentCategoryId) return;

      const arr = blocks
        .slice()
        .sort((a, b) => {
          const ca = bonkMods._categories[a.categoryId] || { order: 999 };
          const cb = bonkMods._categories[b.categoryId] || { order: 999 };
          if (ca.order !== cb.order) return ca.order - cb.order;
          if (a.order === b.order) return a.id > b.id ? 1 : -1;
          return a.order - b.order;
        });

      arr.forEach((def) => {
        if (def.categoryId !== currentCategoryId) return;
        if (currentModId !== 'all' && def.modId !== currentModId) return;

        const block = document.createElement('div');
        block.className = 'mod_block';
        block.dataset.blockId = def.id;

        if (def.title) {
          const titleEl = document.createElement('div');
          titleEl.className = 'mod_block_title';
          titleEl.textContent = def.title;
          block.appendChild(titleEl);
        }

        const content = document.createElement('div');
        block.appendChild(content);
        scroll.appendChild(block);

        try {
          def.render(content);
        } catch (e) {
          console.error('[BonkModSettingsCore] error rendering block', def.id, e);
          content.textContent = 'Error loading this mod block.';
        }
      });
    }

    waitForElement('settingsContainer', setupSettingsShell);
  })();

  (() => {
    let colorGroups = [];

    const STORAGE_KEY_PREFIX_V2 = 'bonk_recolor_groups_v2_';
    const STORAGE_KEY_PREFIX_V1 = 'bonk_mod_color_groups_';
    const DISPLAY_SETTINGS_KEY_PREFIX_V1 = 'bonk_recolor_display_toggles_v1_';

    let storageKey = null;
    let lastStorageKey = undefined;
    let observersInitialized = false;
    let activePanel = null;

    let selectedPlayer = null;
    let refreshRecolorSettingsUi = null;
    let recolorUiSyncLocked = false;
    let recolorSyncLockListenerInstalled = false;

    function createDefaultRecolorDisplaySettings() {
      return {
        playerNames: true,
        winnerBoard: false,
        ingameChatNames: false,
        lobbyChatNames: false,
        playerListNames: false,
        playerListBackboard: true,
      };
    }

    let recolorDisplaySettings = createDefaultRecolorDisplaySettings();

    const COLOR_PRESETS = [
      { id: 'blue', label: 'BLUE', color: '#448aff' },
      { id: 'red', label: 'RED', color: '#d32e2f' },
      { id: 'green', label: 'GREEN', color: '#177818' },
      { id: 'yellow', label: 'YELLOW', color: '#fff93d' },
      { id: 'ffa', label: 'FFA', color: '#1abc9c' },
      { id: 'orange', label: 'ORANGE', color: '#ff8a00' },
      { id: 'pink_purple', label: 'PINK-PURPLE', color: '#d35bff' },
      { id: 'turquoise', label: 'PINK', color: '#ff69b4' },
    ];

    function isPresetColor(hex) {
      const h = String(hex || '').trim().toLowerCase();
      return COLOR_PRESETS.some((p) => p.color.toLowerCase() === h);
    }

    function getRandomPresetColor() {
      const idx = Math.floor(Math.random() * COLOR_PRESETS.length);
      return COLOR_PRESETS[idx]?.color || '#ff0000';
    }

    let colorCache = new Map();
    window.addEventListener('recolorGroupsChanged', () => colorCache.clear());
    let sharedColorCache = new Map();
    let lastSharedColorSig = '';

    function getLobbyHostNameNormForRecolor() {
      const badges = Array.from(document.querySelectorAll('.newbonklobby_playerentry_host'));
      let fallbackName = '';
      for (const hostBadge of badges) {
        const src = String(hostBadge.getAttribute('src') || hostBadge.src || '').toLowerCase();
        const row = hostBadge.closest('.newbonklobby_playerentry');
        if (!row) continue;
        const nameEl = row.querySelector('.newbonklobby_playerentry_name');
        if (!nameEl) continue;
        const nameNorm = normalizeName(nameEl.textContent || '');
        if (!nameNorm) continue;
        if (!fallbackName) fallbackName = nameNorm;
        if (src && src.indexOf('host_5.png') !== -1) return nameNorm;
      }
      return fallbackName;
    }

    function isSelfLobbyHostForRecolor() {
      const selfNameEl = $('pretty_top_name');
      const selfNorm = normalizeName(selfNameEl ? selfNameEl.textContent : '');
      const hostNorm = getLobbyHostNameNormForRecolor();
      return !!selfNorm && !!hostNorm && selfNorm === hostNorm;
    }

    function rebuildSharedColorCacheIfNeeded() {
      const snapshot = Array.isArray(window.tbcSharedGroupsSnapshot) ? window.tbcSharedGroupsSnapshot : [];
      const sig = JSON.stringify(snapshot);
      if (sig === lastSharedColorSig) return;
      lastSharedColorSig = sig;
      sharedColorCache.clear();

      snapshot.forEach((g) => {
        if (!g || typeof g !== 'object') return;
        const color = String(g.color || '').trim();
        if (!color) return;
        const players = Array.isArray(g.players) ? g.players : [];
        players.forEach((p) => {
          const name = typeof p === 'string' ? p : (p && p.name ? p.name : '');
          const key = normalizeName(name);
          const canonKey = normalizeWinnerLookupName(name);
          const memberType = getGroupPlayerMemberType(p);
          const keyTyped = key ? `${key}|${getLookupTypeSuffix(memberType)}` : '';
          const canonTyped = canonKey ? `${canonKey}|${getLookupTypeSuffix(memberType)}` : '';
          if (keyTyped && !sharedColorCache.has(keyTyped)) sharedColorCache.set(keyTyped, color);
          if (canonTyped && !sharedColorCache.has(canonTyped)) sharedColorCache.set(canonTyped, color);
          if (memberType === 'account') {
            if (key && !sharedColorCache.has(key)) sharedColorCache.set(key, color);
            if (canonKey && !sharedColorCache.has(canonKey)) sharedColorCache.set(canonKey, color);
          }
        });
      });
    }

    function hasDisplayGroupsForNames() {
      rebuildSharedColorCacheIfNeeded();
      const hasShared = !!window.tbcRoomGroupsSyncActive;
      if (hasShared) return sharedColorCache.size > 0;
      return Array.isArray(colorGroups) && colorGroups.length > 0;
    }

    function getDisplayColorForName(name, opts = null) {
      const memberType = normalizeMemberType(opts && opts.memberType);
      rebuildSharedColorCacheIfNeeded();
      const hasShared = !!window.tbcRoomGroupsSyncActive;
      if (hasShared) {
        const key = normalizeName(name);
        const canonKey = normalizeWinnerLookupName(name);
        if (memberType !== 'any') {
          const typedKey = key ? `${key}|${getLookupTypeSuffix(memberType)}` : '';
          const typedCanon = canonKey ? `${canonKey}|${getLookupTypeSuffix(memberType)}` : '';
          if (typedKey && sharedColorCache.has(typedKey)) return sharedColorCache.get(typedKey) || null;
          if (typedCanon && sharedColorCache.has(typedCanon)) return sharedColorCache.get(typedCanon) || null;
          return null;
        }
        if (key && sharedColorCache.has(key)) return sharedColorCache.get(key) || null;
        if (canonKey && sharedColorCache.has(canonKey)) return sharedColorCache.get(canonKey) || null;
        const accountKey = key ? `${key}|account` : '';
        const accountCanon = canonKey ? `${canonKey}|account` : '';
        if (accountKey && sharedColorCache.has(accountKey)) return sharedColorCache.get(accountKey) || null;
        if (accountCanon && sharedColorCache.has(accountCanon)) return sharedColorCache.get(accountCanon) || null;
        const guestKey = key ? `${key}|guest` : '';
        const guestCanon = canonKey ? `${canonKey}|guest` : '';
        if (guestKey && sharedColorCache.has(guestKey)) return sharedColorCache.get(guestKey) || null;
        if (guestCanon && sharedColorCache.has(guestCanon)) return sharedColorCache.get(guestCanon) || null;
        return null;
      }
      return getColorForName(name, opts);
    }

    function canonicalizeWinnerName(nameText) {
      const raw = String(nameText || '')
        .replace(/[\u200B-\u200D\uFEFF]/g, '')
        .trim();
      if (!raw) return '';
      const parts = raw.split(/\s+/).filter(Boolean);
      if (parts.length >= 3 && parts.every((p) => p.length === 1)) return parts.join('');
      return raw;
    }

    function normalizeWinnerLookupName(nameText) {
      return normalizeName(canonicalizeWinnerName(nameText));
    }

    function getWinnerBoardColorForName(name, opts = null) {
      const hasSharedSync = !!window.tbcRoomGroupsSyncActive;
      const sharedColor = getDisplayColorForName(name, opts);
      if (sharedColor) return sharedColor;
      const canonical = canonicalizeWinnerName(name);
      if (canonical && canonical !== String(name || '')) {
        const sharedCanonical = getDisplayColorForName(canonical, opts);
        if (sharedCanonical) return sharedCanonical;
      }
      if (hasSharedSync) return null;
      const local = getColorForName(name, opts);
      if (local) return local;
      if (canonical && canonical !== String(name || '')) return getColorForName(canonical, opts);
      return null;
    }

    let lobbyScanQueued = false;
    let lobbyPlayerMenuOwnerNameNorm = '';
    let lobbyPlayerMenuPendingOwnerNameNorm = '';
    let lobbyPlayerMenuOwnerTrackingInstalled = false;
    const lobbyPersistentColorByName = new Map();
    const lobbyPersistentColorVersionByName = new Map();
    let lobbyPersistentColorEpoch = 1;
    let lobbyPlayerGroupsActionMenuEl = null;
    let lobbyPlayerGroupsTargetMenuEl = null;
    let lobbyPlayerGroupsPickerMenuEl = null;
    let lobbyPlayerGroupsPositionSyncInstalled = false;

    function scheduleLobbyScan() {
      if (lobbyScanQueued) return;
      lobbyScanQueued = true;
      const run = () => {
        lobbyScanQueued = false;
        applyLobbyNameColors(false);
      };
      if (typeof queueMicrotask === 'function') queueMicrotask(run);
      else Promise.resolve().then(run);
    }

    function ensureLobbyPlayerGroupsMenuStyles() {
      if ($('tbc_player_groups_menu_css')) return;
      const style = document.createElement('style');
      style.id = 'tbc_player_groups_menu_css';
      style.textContent = `
        .tbc_player_groups_target_menu {
          max-height: 220px;
          overflow-y: auto;
          overflow-x: hidden;
          scrollbar-width: thin;
          scrollbar-color: rgba(115, 78, 63, 0.92) rgba(170, 187, 196, 0.75);
        }
        .tbc_player_groups_action_menu .newbonklobby_playerentry_menu_button,
        .tbc_player_groups_target_menu .newbonklobby_playerentry_menu_button {
          width: 100%;
          box-sizing: border-box;
          white-space: nowrap;
          text-overflow: ellipsis;
          overflow: hidden;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-move-list="1"] {
          display: flex;
          flex-wrap: wrap;
          gap: 4px;
          align-content: flex-start;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-action="back"] {
          width: 100%;
          margin-bottom: 4px;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-target] {
          width: calc((100% - 8px) / 3);
          min-width: 0;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-target-create] {
          width: calc((100% - 8px) / 3);
          min-width: 0;
          border: 1px dashed rgba(255, 255, 255, 0.6);
          border-radius: 8px;
          background: transparent !important;
          color: rgba(255, 255, 255, 0.92) !important;
          text-shadow: none !important;
          font-weight: 700;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-target-create]:hover,
        .tbc_player_groups_target_menu [data-tbc-player-groups-target-create]:active {
          background: rgba(127, 97, 81, 0.55) !important;
          border-color: rgba(255, 255, 255, 0.78);
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-create-preset] {
          width: calc((100% - 8px) / 3);
          min-width: 0;
          color: transparent !important;
          text-shadow: none !important;
          background: var(--tbc-group-color, #7f6151) !important;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-create-preset][data-selected="1"] {
          box-shadow: inset 0 0 0 2px rgba(255,255,255,0.8), 0 0 0 1px rgba(60,40,32,0.85);
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-action="create-cancel"]:hover,
        .tbc_player_groups_target_menu [data-tbc-player-groups-action="create-cancel"]:active {
          background: #b75545 !important;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-target],
        .tbc_player_groups_target_menu [data-tbc-player-groups-target]:hover,
        .tbc_player_groups_target_menu [data-tbc-player-groups-target]:active {
          background: var(--tbc-group-color, #7f6151) !important;
          color: transparent !important;
          text-shadow: none !important;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-target]:hover {
          filter: brightness(1.08);
          box-shadow: inset 0 0 0 1px rgba(255,255,255,0.45);
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-move-list="1"]::-webkit-scrollbar {
          width: 11px;
          height: 11px;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-move-list="1"]::-webkit-scrollbar-track {
          background: rgba(170, 187, 196, 0.75);
          border-left: 1px solid rgba(78, 96, 106, 0.55);
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-move-list="1"]::-webkit-scrollbar-thumb {
          background: linear-gradient(180deg, #8f6756, #6f4a3e);
          border: 1px solid rgba(48, 34, 29, 0.9);
          border-radius: 7px;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-move-list="1"]::-webkit-scrollbar-thumb:hover {
          background: linear-gradient(180deg, #9f7562, #7b5345);
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-move-list="1"]::-webkit-scrollbar-button:single-button {
          background-color: rgba(170, 187, 196, 0.75);
          border-left: 1px solid rgba(78, 96, 106, 0.55);
          background-repeat: no-repeat;
          background-position: center;
          background-size: 7px 7px;
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-move-list="1"]::-webkit-scrollbar-button:single-button:vertical:decrement {
          background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='7' height='7' viewBox='0 0 7 7'><path d='M3.5 1 L6 4.5 H1 Z' fill='%235e473d'/></svg>");
        }
        .tbc_player_groups_target_menu [data-tbc-player-groups-move-list="1"]::-webkit-scrollbar-button:single-button:vertical:increment {
          background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='7' height='7' viewBox='0 0 7 7'><path d='M1 2.5 H6 L3.5 6 Z' fill='%235e473d'/></svg>");
        }
      `;
      document.head.appendChild(style);
    }

    function closeLobbyPlayerGroupsMenus() {
      wipeLobbyPlayerEntryMenus();
      if (lobbyPlayerGroupsPickerMenuEl && lobbyPlayerGroupsPickerMenuEl.parentNode) {
        lobbyPlayerGroupsPickerMenuEl.parentNode.removeChild(lobbyPlayerGroupsPickerMenuEl);
      }
      if (lobbyPlayerGroupsTargetMenuEl && lobbyPlayerGroupsTargetMenuEl.parentNode) {
        lobbyPlayerGroupsTargetMenuEl.parentNode.removeChild(lobbyPlayerGroupsTargetMenuEl);
      }
      if (lobbyPlayerGroupsActionMenuEl && lobbyPlayerGroupsActionMenuEl.parentNode) {
        lobbyPlayerGroupsActionMenuEl.parentNode.removeChild(lobbyPlayerGroupsActionMenuEl);
      }
      lobbyPlayerGroupsPickerMenuEl = null;
      lobbyPlayerGroupsTargetMenuEl = null;
      lobbyPlayerGroupsActionMenuEl = null;
      setLobbyGroupsActionMenuMasked(false);
    }

    function setLobbyMainPlayerMenuMasked(active) {
      const mainMenu = document.querySelector('.newbonklobby_playerentry_menu');
      if (!(mainMenu instanceof Element)) return;
      if (active) {
        if (!mainMenu.hasAttribute('data-tbc-orig-opacity')) {
          mainMenu.setAttribute('data-tbc-orig-opacity', String(mainMenu.style.opacity || ''));
        }
        if (!mainMenu.hasAttribute('data-tbc-orig-pointer-events')) {
          mainMenu.setAttribute('data-tbc-orig-pointer-events', String(mainMenu.style.pointerEvents || ''));
        }
        mainMenu.style.opacity = '0';
        mainMenu.style.pointerEvents = 'none';
        return;
      }

      const origOpacity = mainMenu.getAttribute('data-tbc-orig-opacity');
      const origPointer = mainMenu.getAttribute('data-tbc-orig-pointer-events');
      if (origOpacity !== null) {
        if (origOpacity) mainMenu.style.opacity = origOpacity;
        else mainMenu.style.removeProperty('opacity');
        mainMenu.removeAttribute('data-tbc-orig-opacity');
      }
      if (origPointer !== null) {
        if (origPointer) mainMenu.style.pointerEvents = origPointer;
        else mainMenu.style.removeProperty('pointer-events');
        mainMenu.removeAttribute('data-tbc-orig-pointer-events');
      }
    }

    function setLobbyGroupsActionMenuMasked(active) {
      const actionMenu = lobbyPlayerGroupsActionMenuEl;
      if (!(actionMenu instanceof Element)) return;
      if (active) {
        if (!actionMenu.hasAttribute('data-tbc-orig-opacity')) {
          actionMenu.setAttribute('data-tbc-orig-opacity', String(actionMenu.style.opacity || ''));
        }
        if (!actionMenu.hasAttribute('data-tbc-orig-pointer-events')) {
          actionMenu.setAttribute('data-tbc-orig-pointer-events', String(actionMenu.style.pointerEvents || ''));
        }
        actionMenu.style.opacity = '0';
        actionMenu.style.pointerEvents = 'none';
        return;
      }

      const origOpacity = actionMenu.getAttribute('data-tbc-orig-opacity');
      const origPointer = actionMenu.getAttribute('data-tbc-orig-pointer-events');
      if (origOpacity !== null) {
        if (origOpacity) actionMenu.style.opacity = origOpacity;
        else actionMenu.style.removeProperty('opacity');
        actionMenu.removeAttribute('data-tbc-orig-opacity');
      }
      if (origPointer !== null) {
        if (origPointer) actionMenu.style.pointerEvents = origPointer;
        else actionMenu.style.removeProperty('pointer-events');
        actionMenu.removeAttribute('data-tbc-orig-pointer-events');
      }
    }

    function wipeLobbyPlayerEntryMenus() {
      const mainMenu = document.querySelector('.newbonklobby_playerentry_menu');
      if (mainMenu && mainMenu.style) {
        mainMenu.style.display = 'none';
        mainMenu.style.visibility = 'hidden';
      }

      const submenu = document.querySelector('.newbonklobby_playerentry_menu_submenu');
      if (submenu && submenu.style) {
        submenu.style.display = 'none';
        submenu.style.visibility = 'hidden';
      }

      const highlighted = document.querySelector('.newbonklobby_playerentry_menuhighlighted');
      if (highlighted && highlighted.classList) highlighted.classList.remove('newbonklobby_playerentry_menuhighlighted');
    }

    function closeLobbyPlayerMenusAll() {
      closeLobbyPlayerGroupsMenus();
      wipeLobbyPlayerEntryMenus();
      lobbyPlayerMenuOwnerNameNorm = '';
      lobbyPlayerMenuPendingOwnerNameNorm = '';
    }

    function positionLobbyFloatingMenu(menuEl, left, top) {
      if (!menuEl || !(menuEl instanceof Element)) return;
      const vw = window.innerWidth || document.documentElement.clientWidth || 0;
      const vh = window.innerHeight || document.documentElement.clientHeight || 0;
      const pad = 6;
      let x = Math.round(left);
      let y = Math.round(top);
      const rect = menuEl.getBoundingClientRect();
      if (x + rect.width > vw - pad) x = Math.max(pad, vw - rect.width - pad);
      if (y + rect.height > vh - pad) y = Math.max(pad, vh - rect.height - pad);
      if (x < pad) x = pad;
      if (y < pad) y = pad;
      menuEl.style.left = `${x}px`;
      menuEl.style.top = `${y}px`;
    }

    function positionLobbyGroupsMenuLikeBonkDefault(menuEl, fallbackAnchorEl = null) {
      if (!menuEl || !(menuEl instanceof Element)) return;
      const nativeSub = Array.from(document.querySelectorAll('.newbonklobby_playerentry_menu_submenu')).find((el) => (
        el instanceof Element &&
        !el.hasAttribute('data-tbc-player-groups-action-menu') &&
        !el.hasAttribute('data-tbc-player-groups-target-menu')
      ));
      if (nativeSub) {
        const rect = nativeSub.getBoundingClientRect();
        menuEl.style.width = `${Math.max(1, Math.round(rect.width))}px`;
        menuEl.style.boxSizing = 'border-box';
        positionLobbyFloatingMenu(menuEl, rect.left, rect.top);
        return;
      }

      const mainMenu = document.querySelector('.newbonklobby_playerentry_menu');
      if (mainMenu instanceof Element) {
        const rect = mainMenu.getBoundingClientRect();
        menuEl.style.width = `${Math.max(1, Math.round(rect.width))}px`;
        menuEl.style.boxSizing = 'border-box';
        positionLobbyFloatingMenu(menuEl, rect.left, rect.top);
        return;
      }

      if (fallbackAnchorEl instanceof Element) {
        const rect = fallbackAnchorEl.getBoundingClientRect();
        menuEl.style.width = `${Math.max(1, Math.round(rect.width))}px`;
        menuEl.style.boxSizing = 'border-box';
        positionLobbyFloatingMenu(menuEl, rect.left, rect.top);
      }
    }

    function ensureLobbyPlayerGroupsPositionSync() {
      if (lobbyPlayerGroupsPositionSyncInstalled) return;
      lobbyPlayerGroupsPositionSyncInstalled = true;
      const sync = () => {
        if (!(lobbyPlayerGroupsActionMenuEl instanceof Element) || !lobbyPlayerGroupsActionMenuEl.isConnected) return;
        positionLobbyGroupsMenuLikeBonkDefault(lobbyPlayerGroupsActionMenuEl, null);
        if (lobbyPlayerGroupsPickerMenuEl instanceof Element && lobbyPlayerGroupsPickerMenuEl.isConnected) {
          positionLobbyGroupsMenuLikeBonkDefault(lobbyPlayerGroupsPickerMenuEl, lobbyPlayerGroupsActionMenuEl);
        }
      };
      window.addEventListener('resize', sync, { passive: true });
    }

    let recolorLobbyAccountInfoCache = null;
    let recolorLobbyAccountInfoCacheAt = 0;
    function getLobbyAccountInfoCached(maxAgeMs = 800) {
      const now = Date.now();
      if (recolorLobbyAccountInfoCache && (now - recolorLobbyAccountInfoCacheAt) <= Math.max(0, maxAgeMs)) {
        return recolorLobbyAccountInfoCache;
      }
      const accountSet = new Set();
      const guestSet = new Set();
      const levelMap = new Map();
      const rows = document.querySelectorAll(
        '#newbonklobby_playerbox .newbonklobby_playerentry, #newbonklobby_specbox .newbonklobby_playerentry'
      );
      rows.forEach((row) => {
        const nameEl = row.querySelector('.newbonklobby_playerentry_name');
        const lvlEl = row.querySelector('.newbonklobby_playerentry_level');
        const nameText = String((nameEl && nameEl.textContent) || '').trim();
        const lvlText = String((lvlEl && lvlEl.textContent) || '').trim().toLowerCase();
        const key = normalizeName(nameText);
        if (!key) return;
        const isGuest = lvlText === 'guest' || /\bguest\b/.test(lvlText);
        if (isGuest) {
          guestSet.add(key);
          levelMap.set(key, 'guest');
        } else if (lvlText) {
          accountSet.add(key);
          levelMap.set(key, 'level');
        } else if (!levelMap.has(key)) {
          levelMap.set(key, 'unknown');
        }
      });
      recolorLobbyAccountInfoCache = { accountSet, guestSet, levelMap };
      recolorLobbyAccountInfoCacheAt = now;
      return recolorLobbyAccountInfoCache;
    }

    function getLobbyPlayerMenuOwnerMeta() {
      const nameNorm = lobbyPlayerMenuOwnerNameNorm;
      if (!nameNorm) return null;
      const rows = document.querySelectorAll('.newbonklobby_playerentry');
      for (const row of rows) {
        const nameEl = row.querySelector('.newbonklobby_playerentry_name');
        const displayName = String((nameEl && nameEl.textContent) || '').trim();
        if (!displayName) continue;
        if (normalizeName(displayName) !== nameNorm) continue;

        const lvlEl = row.querySelector('.newbonklobby_playerentry_level');
        const lvlText = String((lvlEl && lvlEl.textContent) || '').trim().toLowerCase();
        const isGuest = lvlText === 'guest' || /\bguest\b/.test(lvlText);
        return {
          playerName: displayName,
          playerNameNorm: nameNorm,
          isGuest,
        };
      }
      const info = getLobbyAccountInfoCached(500);
      const lvlState = String((info && info.levelMap && info.levelMap.get(nameNorm)) || '');
      if (!lvlState) return null;
      return {
        playerName: lobbyPlayerMenuOwnerNameNorm,
        playerNameNorm: nameNorm,
        isGuest: lvlState === 'guest',
      };
    }

    function escapeLobbyMenuHtml(text) {
      return String(text || '')
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
    }

    function renderLobbyPlayerGroupsTargetMenu(anchorEl, playerName, mode, fromGroupId = '', memberType = 'any') {
      if (!(anchorEl instanceof Element)) return;
      const groups = Array.isArray(colorGroups) ? colorGroups.slice() : [];
      const fromGroup = fromGroupId ? (colorGroups.find((g) => g.id === fromGroupId) || null) : findGroupByPlayerName(playerName, memberType);
      const targets = mode === 'move'
        ? (fromGroup ? groups.filter((g) => g.id !== fromGroup.id) : groups)
        : groups;
      if (!targets.length) return;

      if (lobbyPlayerGroupsTargetMenuEl && lobbyPlayerGroupsTargetMenuEl.parentNode) {
        lobbyPlayerGroupsTargetMenuEl.parentNode.removeChild(lobbyPlayerGroupsTargetMenuEl);
      }
      ensureLobbyPlayerGroupsMenuStyles();
      const menuEl = document.createElement('div');
      menuEl.className = 'newbonklobby_playerentry_menu_submenu tbc_player_groups_target_menu';
      menuEl.setAttribute('data-tbc-player-groups-target-menu', '1');
      const tint = getPlayerListMenuGroupColor();
      setPlayerListMenuBackground(menuEl, tint);
      menuEl.innerHTML = targets
        .map((g) => (
          `<div class="newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow" data-tbc-player-groups-target="${escapeLobbyMenuHtml(g.id)}" title="${escapeLobbyMenuHtml(g.name)}" aria-label="${escapeLobbyMenuHtml(g.name)}" style="--tbc-group-color:${escapeLobbyMenuHtml(g.color || '#7f6151')};background:var(--tbc-group-color);color:transparent;text-shadow:none;">&nbsp;</div>`
        ))
        .join('');
      menuEl.addEventListener('mousedown', (e) => {
        e.stopPropagation();
      }, true);
      menuEl.addEventListener('click', (e) => {
        e.stopPropagation();
      }, true);
      menuEl.addEventListener('click', (e) => {
        const btn = e.target && e.target.closest ? e.target.closest('[data-tbc-player-groups-target]') : null;
        if (!btn) return;
        const groupId = String(btn.getAttribute('data-tbc-player-groups-target') || '').trim();
        if (!groupId) return;
        let res = null;
        if (mode === 'add') {
          res = addPlayerToGroup(groupId, playerName, { allowGuest: true, memberType });
        } else if (fromGroup) {
          res = movePlayerToGroup(fromGroup.id, groupId, playerName, memberType);
        } else {
          res = addPlayerToGroup(groupId, playerName, { allowGuest: true, memberType });
        }
        if (!res || !res.ok) {
          addLocalChatStatus(`[TBC] ${res && res.error ? res.error : 'Could not update groups.'}`, 'rgb(181, 48, 48)');
          return;
        }
        queueGroupsPanelActionAutoSync();
        closeLobbyPlayerGroupsMenus();
        scheduleLobbyScan();
      });

      document.body.appendChild(menuEl);
      const ar = anchorEl.getBoundingClientRect();
      positionLobbyFloatingMenu(menuEl, ar.right + 2, ar.top);
      lobbyPlayerGroupsTargetMenuEl = menuEl;
    }

    function renderLobbyPlayerGroupsActionMenu(anchorEl, playerName, playerIsGuest = false) {
      if (!(anchorEl instanceof Element)) return;
      ensureLobbyPlayerGroupsPositionSync();
      const defaultMemberType = playerIsGuest ? 'guest' : 'account';
      if (lobbyPlayerGroupsPickerMenuEl && lobbyPlayerGroupsPickerMenuEl.parentNode) {
        lobbyPlayerGroupsPickerMenuEl.parentNode.removeChild(lobbyPlayerGroupsPickerMenuEl);
      }
      lobbyPlayerGroupsPickerMenuEl = null;
      setLobbyGroupsActionMenuMasked(false);
      if (lobbyPlayerGroupsActionMenuEl && lobbyPlayerGroupsActionMenuEl.parentNode) {
        lobbyPlayerGroupsActionMenuEl.parentNode.removeChild(lobbyPlayerGroupsActionMenuEl);
      }
      if (lobbyPlayerGroupsTargetMenuEl && lobbyPlayerGroupsTargetMenuEl.parentNode) {
        lobbyPlayerGroupsTargetMenuEl.parentNode.removeChild(lobbyPlayerGroupsTargetMenuEl);
      }
      lobbyPlayerGroupsTargetMenuEl = null;

      ensureLobbyPlayerGroupsMenuStyles();

      const menuEl = document.createElement('div');
      menuEl.className = 'newbonklobby_playerentry_menu_submenu tbc_player_groups_action_menu';
      menuEl.setAttribute('data-tbc-player-groups-action-menu', '1');
      const tint = getPlayerListMenuGroupColor();
      setPlayerListMenuBackground(menuEl, tint);
      const resolveLivePlayerName = () => {
        const liveMeta = getLobbyPlayerMenuOwnerMeta();
        return String((liveMeta && liveMeta.playerName) || playerName || '').trim();
      };
      const removeLivePlayerFromGroups = () => {
        const liveMeta = getLobbyPlayerMenuOwnerMeta();
        const livePlayerName = resolveLivePlayerName();
        const liveMemberType = liveMeta && typeof liveMeta.isGuest === 'boolean'
          ? (liveMeta.isGuest ? 'guest' : 'account')
          : defaultMemberType;
        let removed = removePlayerFromAllGroups(livePlayerName, liveMemberType);
        if ((!removed || !removed.ok) && liveMeta && liveMeta.playerNameNorm) {
          removed = removePlayerFromAllGroups(liveMeta.playerNameNorm, liveMemberType);
        }
        if ((!removed || !removed.ok) && playerName) {
          removed = removePlayerFromAllGroups(playerName, defaultMemberType);
        }
        if (removed && removed.ok) {
          if (typeof queueGroupsPanelActionAutoSync === 'function') {
            queueGroupsPanelActionAutoSync();
          } else if (typeof broadcastSharedGroupsFromHost === 'function' && isSelfLobbyHost()) {
            broadcastSharedGroupsFromHost(true, { silent: true, skipNoop: true });
          }
        }
        scheduleLobbyScan();
        closeLobbyPlayerMenusAll();
      };
      const triggerGroupsAutoSyncSafe = () => {
        if (typeof queueGroupsPanelActionAutoSync === 'function') {
          queueGroupsPanelActionAutoSync();
          return;
        }
        if (typeof broadcastSharedGroupsFromHost === 'function' && isSelfLobbyHost()) {
          broadcastSharedGroupsFromHost(true, { silent: true, skipNoop: true });
        }
      };
      const renderActionsView = () => {
        const groups = Array.isArray(colorGroups) ? colorGroups.slice() : [];
        const currentGroup = findGroupByPlayerName(playerName, defaultMemberType);
        const inAnyGroup = isPlayerInAnyGroup(playerName, defaultMemberType);
        const addDisabled = inAnyGroup || groups.length < 1;
        const moveTargets = currentGroup ? groups.filter((g) => g.id !== currentGroup.id) : [];
        const moveDisabled = !inAnyGroup || moveTargets.length < 1;
        const removeDisabled = !inAnyGroup;
        menuEl.innerHTML = `
          <div class="newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow${addDisabled ? ' brownButtonDisabled' : ''}" data-tbc-player-groups-action="add">Add</div>
          <div class="newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow${moveDisabled ? ' brownButtonDisabled' : ''}" data-tbc-player-groups-action="move">Move</div>
          <div class="newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow${removeDisabled ? ' brownButtonDisabled' : ''}" data-tbc-player-groups-action="remove">Remove</div>
        `;
        const addBtn = menuEl.querySelector('[data-tbc-player-groups-action="add"]');
        const moveBtn = menuEl.querySelector('[data-tbc-player-groups-action="move"]');
        const removeBtn = menuEl.querySelector('[data-tbc-player-groups-action="remove"]');
        if (addBtn) {
          addBtn.addEventListener('mouseup', (e) => {
            e.preventDefault();
            e.stopPropagation();
            if (addBtn.classList.contains('brownButtonDisabled')) return;
            openTargetsPickerPanel('add');
          }, true);
        }
        if (moveBtn) {
          moveBtn.addEventListener('mouseup', (e) => {
            e.preventDefault();
            e.stopPropagation();
            if (moveBtn.classList.contains('brownButtonDisabled')) return;
            openTargetsPickerPanel('move');
          }, true);
        }
        if (removeBtn) {
          removeBtn.addEventListener('mouseup', (e) => {
            e.preventDefault();
            e.stopPropagation();
            wipeLobbyPlayerEntryMenus();
            if (removeBtn.classList.contains('brownButtonDisabled')) {
              closeLobbyPlayerMenusAll();
              return;
            }
            removeBtn.classList.add('brownButtonDisabled');
            if (moveBtn) moveBtn.classList.add('brownButtonDisabled');
            removeLivePlayerFromGroups();
          }, true);
        }
      };
      const refreshActionsView = () => {
        if (lobbyPlayerGroupsPickerMenuEl && lobbyPlayerGroupsPickerMenuEl.parentNode) {
          lobbyPlayerGroupsPickerMenuEl.parentNode.removeChild(lobbyPlayerGroupsPickerMenuEl);
        }
        lobbyPlayerGroupsPickerMenuEl = null;
        setLobbyGroupsActionMenuMasked(false);
        renderActionsView();
        requestAnimationFrame(() => {
          if (lobbyPlayerGroupsActionMenuEl === menuEl) renderActionsView();
        });
      };
      const openTargetsPickerPanel = (mode) => {
        const latestGroups = Array.isArray(colorGroups) ? colorGroups.slice() : [];
        const latestCurrentGroup = findGroupByPlayerName(playerName, defaultMemberType);
        const sourceGroupIdAtOpen = latestCurrentGroup ? String(latestCurrentGroup.id || '') : '';
        const targets = mode === 'move'
          ? (latestCurrentGroup ? latestGroups.filter((g) => g.id !== latestCurrentGroup.id) : [])
          : latestGroups;
        if (!targets.length) {
          refreshActionsView();
          return;
        }
        if (lobbyPlayerGroupsPickerMenuEl && lobbyPlayerGroupsPickerMenuEl.parentNode) {
          lobbyPlayerGroupsPickerMenuEl.parentNode.removeChild(lobbyPlayerGroupsPickerMenuEl);
        }
        lobbyPlayerGroupsPickerMenuEl = null;
        const pickerEl = document.createElement('div');
        pickerEl.id = 'tbc_groups_subsubpanel';
        pickerEl.className = 'newbonklobby_playerentry_menu_submenu tbc_player_groups_target_menu';
        pickerEl.setAttribute('data-tbc-player-groups-target-menu', '1');
        pickerEl.style.display = 'block';
        pickerEl.style.visibility = 'visible';
        pickerEl.style.zIndex = '2147483000';
        const pickerTint = getPlayerListMenuGroupColor();
        setPlayerListMenuBackground(pickerEl, pickerTint);
        const listMaxPx = 8 * 29;
        const listStyle = targets.length > 24
          ? `max-height:${listMaxPx}px;overflow-y:auto;overflow-x:hidden;`
          : 'overflow:visible;';
        pickerEl.innerHTML = `
          <div class="newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow" data-tbc-player-groups-action="back">Back</div>
          <div data-tbc-player-groups-move-list="1" style="${listStyle}">
            ${targets.map((g) => (
              `<div class="newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow" data-tbc-player-groups-target="${escapeLobbyMenuHtml(g.id)}" data-tbc-player-groups-target-mode="${escapeLobbyMenuHtml(mode)}" title="${escapeLobbyMenuHtml(g.name)}" aria-label="${escapeLobbyMenuHtml(g.name)}" style="--tbc-group-color:${escapeLobbyMenuHtml(g.color || '#7f6151')};background:var(--tbc-group-color);color:transparent;text-shadow:none;">&nbsp;</div>`
            )).join('')}
            <div class="newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow" data-tbc-player-groups-target-create="${escapeLobbyMenuHtml(mode)}" title="Create new group" aria-label="Create new group" style="background:rgba(127,97,81,0.45);color:#fff;font-weight:700;text-shadow:none;">+</div>
          </div>
        `;
        pickerEl.addEventListener('mousedown', (e) => {
          e.stopPropagation();
        }, true);
        pickerEl.addEventListener('click', (e) => {
          e.stopPropagation();
        }, true);
        const backBtn = pickerEl.querySelector('[data-tbc-player-groups-action="back"]');
        if (backBtn) {
          backBtn.addEventListener('mouseup', (e) => {
            e.preventDefault();
            e.stopPropagation();
            refreshActionsView();
          }, true);
        }
        const applyToTargetGroup = (targetGroupId, targetMode) => {
          if (!targetGroupId || !targetMode) return;
          const liveMeta = getLobbyPlayerMenuOwnerMeta();
          const liveMemberType = liveMeta && typeof liveMeta.isGuest === 'boolean'
            ? (liveMeta.isGuest ? 'guest' : 'account')
            : defaultMemberType;
          let res = null;
          if (targetMode === 'add') {
            const liveName = resolveLivePlayerName();
            res = addPlayerToGroup(targetGroupId, liveName || playerName, { allowGuest: true, memberType: liveMemberType });
          } else if (targetMode === 'move') {
            const liveName = resolveLivePlayerName();
            const effectiveName = liveName || playerName;
            const latestCurrentGroup = findGroupByPlayerName(effectiveName, liveMemberType);
            let fromGroupId = latestCurrentGroup ? String(latestCurrentGroup.id || '') : '';
            if (!fromGroupId && sourceGroupIdAtOpen) {
              const stillInSource = findPlayerInGroup(sourceGroupIdAtOpen, effectiveName, liveMemberType);
              if (stillInSource) fromGroupId = sourceGroupIdAtOpen;
            }
            if (!fromGroupId) {
              refreshActionsView();
              return;
            }
            res = movePlayerToGroup(fromGroupId, targetGroupId, effectiveName, liveMemberType);
          }
          if (!res || !res.ok) {
            addLocalChatStatus(`[TBC] ${res && res.error ? res.error : 'Could not update groups.'}`, 'rgb(181, 48, 48)');
            return;
          }
          triggerGroupsAutoSyncSafe();
          scheduleLobbyScan();
          closeLobbyPlayerMenusAll();
        };
        pickerEl.querySelectorAll('[data-tbc-player-groups-target]').forEach((targetBtn) => {
          targetBtn.addEventListener('mouseup', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const targetGroupId = String(targetBtn.getAttribute('data-tbc-player-groups-target') || '').trim();
            const targetMode = String(targetBtn.getAttribute('data-tbc-player-groups-target-mode') || '').trim();
            applyToTargetGroup(targetGroupId, targetMode);
          }, true);
        });
        const createBtn = pickerEl.querySelector('[data-tbc-player-groups-target-create]');
        if (createBtn) {
          createBtn.addEventListener('mouseup', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const targetMode = String(createBtn.getAttribute('data-tbc-player-groups-target-create') || '').trim();
            if (!targetMode) return;
            const defaultColor = (Array.isArray(COLOR_PRESETS) && COLOR_PRESETS[0] && COLOR_PRESETS[0].color)
              ? String(COLOR_PRESETS[0].color)
              : getRandomPresetColor();
            let selectedColor = defaultColor;
            const createPresetsHtml = (Array.isArray(COLOR_PRESETS) ? COLOR_PRESETS : [])
              .map((p) => {
                const clr = String((p && p.color) || '').trim() || '#7f6151';
                const selected = clr.toLowerCase() === selectedColor.toLowerCase();
                const label = String((p && p.label) || clr);
                return `<div class="newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow" data-tbc-player-groups-create-preset="${escapeLobbyMenuHtml(clr)}" data-selected="${selected ? '1' : '0'}" title="${escapeLobbyMenuHtml(label)}" aria-label="${escapeLobbyMenuHtml(label)}" style="--tbc-group-color:${escapeLobbyMenuHtml(clr)};">&nbsp;</div>`;
              })
              .join('');
            pickerEl.innerHTML = `
              <div style="display:flex;gap:4px;align-items:center;margin-bottom:4px;">
                <input type="text" data-tbc-player-groups-create-name placeholder="Group name" value="New Group" style="flex:1;min-width:0;height:24px;padding:0 6px;border:1px solid rgba(58,41,33,0.8);border-radius:6px;outline:none;background:rgba(255,255,255,0.95);color:#2e2019;box-sizing:border-box;">
              </div>
              <div style="display:flex;gap:4px;align-items:center;margin-bottom:4px;">
                <div class="newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow" data-tbc-player-groups-action="create-confirm" style="width:calc(50% - 2px);min-width:0;">Create</div>
                <div class="newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow" data-tbc-player-groups-action="create-cancel" style="width:calc(50% - 2px);min-width:0;">Cancel</div>
              </div>
              <div data-tbc-player-groups-create-presets="1" style="display:flex;flex-wrap:wrap;gap:4px;align-content:flex-start;">
                ${createPresetsHtml}
              </div>
            `;
            const cancelCreateBtn = pickerEl.querySelector('[data-tbc-player-groups-action="create-cancel"]');
            if (cancelCreateBtn) {
              cancelCreateBtn.addEventListener('mouseup', (ev) => {
                ev.preventDefault();
                ev.stopPropagation();
                openTargetsPickerPanel(targetMode);
              }, true);
            }
            const presetBtns = pickerEl.querySelectorAll('[data-tbc-player-groups-create-preset]');
            presetBtns.forEach((presetBtn) => {
              presetBtn.addEventListener('mouseup', (ev) => {
                ev.preventDefault();
                ev.stopPropagation();
                const clr = String(presetBtn.getAttribute('data-tbc-player-groups-create-preset') || '').trim();
                if (!clr) return;
                selectedColor = clr;
                presetBtns.forEach((el) => el.setAttribute('data-selected', el === presetBtn ? '1' : '0'));
              }, true);
            });
            const commitCreate = () => {
              const inputEl = pickerEl.querySelector('[data-tbc-player-groups-create-name]');
              const typed = String((inputEl && inputEl.value) || '').trim();
              const baseName = typed || 'New Group';
              const existing = new Set(
                (Array.isArray(colorGroups) ? colorGroups : [])
                  .map((g) => normalizeName(g && g.name))
                  .filter(Boolean)
              );
              let name = baseName;
              if (existing.has(normalizeName(name))) {
                let idx = 2;
                while (existing.has(normalizeName(`${baseName} ${idx}`))) idx += 1;
                name = `${baseName} ${idx}`;
              }
              const prevCount = Array.isArray(colorGroups) ? colorGroups.length : 0;
              addGroup(name, selectedColor);
              const createdGroup = Array.isArray(colorGroups) && colorGroups.length > prevCount ? colorGroups[colorGroups.length - 1] : null;
              const createdId = String((createdGroup && createdGroup.id) || '').trim();
              if (!createdId) {
                addLocalChatStatus('[TBC] Could not create a new group.', 'rgb(181, 48, 48)');
                return;
              }
              applyToTargetGroup(createdId, targetMode);
            };
            const createConfirmBtn = pickerEl.querySelector('[data-tbc-player-groups-action="create-confirm"]');
            if (createConfirmBtn) {
              createConfirmBtn.addEventListener('mouseup', (ev) => {
                ev.preventDefault();
                ev.stopPropagation();
                commitCreate();
              }, true);
            }
            const inputEl = pickerEl.querySelector('[data-tbc-player-groups-create-name]');
            if (inputEl) {
              inputEl.addEventListener('keydown', (ev) => {
                if (ev.key !== 'Enter') return;
                ev.preventDefault();
                ev.stopPropagation();
                commitCreate();
              }, true);
              inputEl.focus();
              inputEl.select();
            }
          }, true);
        }
        document.body.appendChild(pickerEl);
        positionLobbyGroupsMenuLikeBonkDefault(pickerEl, menuEl);
        lobbyPlayerGroupsPickerMenuEl = pickerEl;
        setLobbyGroupsActionMenuMasked(true);
      };
      renderActionsView();
      menuEl.addEventListener('mousedown', (e) => {
        e.stopPropagation();
      }, true);
      menuEl.addEventListener('click', (e) => {
        e.stopPropagation();
      }, true);

      document.body.appendChild(menuEl);
      positionLobbyGroupsMenuLikeBonkDefault(menuEl, anchorEl);
      lobbyPlayerGroupsActionMenuEl = menuEl;
      setLobbyMainPlayerMenuMasked(true);
    }

    function injectLobbyPlayerGroupsButton(menuEl) {
      if (!(menuEl instanceof Element)) return;
      if (!menuEl.classList || !menuEl.classList.contains('newbonklobby_playerentry_menu')) return;
      if (menuEl.querySelector('[data-tbc-player-groups-button]')) return;
      const meta = getLobbyPlayerMenuOwnerMeta();
      if (!meta || !meta.playerName) return;

      const btn = document.createElement('div');
      btn.className = 'newbonklobby_playerentry_menu_button brownButton brownButton_classic buttonShadow';
      btn.textContent = 'Groups';
      btn.setAttribute('data-tbc-player-groups-button', '1');
      btn.addEventListener('mousedown', (e) => {
        e.preventDefault();
        e.stopPropagation();
      }, true);
      btn.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        if (
          lobbyPlayerGroupsActionMenuEl &&
          lobbyPlayerGroupsActionMenuEl.parentNode &&
          lobbyPlayerGroupsActionMenuEl.getAttribute('data-tbc-player-groups-owner') === meta.playerNameNorm
        ) {
          closeLobbyPlayerGroupsMenus();
          return;
        }
        renderLobbyPlayerGroupsActionMenu(btn, meta.playerName, !!meta.isGuest);
        if (lobbyPlayerGroupsActionMenuEl) {
          lobbyPlayerGroupsActionMenuEl.setAttribute('data-tbc-player-groups-owner', meta.playerNameNorm);
        }
      });
      menuEl.appendChild(btn);
    }

    function setLobbyBlacklistButtonVisual(btn, active = false, disabled = false) {
      if (!(btn instanceof Element)) return;
      if (disabled) btn.classList.add('brownButtonDisabled');
      else btn.classList.remove('brownButtonDisabled');
      if (active) btn.style.setProperty('background', '#B75545', 'important');
      else btn.style.removeProperty('background');
    }

    function injectLobbyPlayerBlacklistButton(menuEl) {
      if (!(menuEl instanceof Element)) return;
      if (!menuEl.classList || !menuEl.classList.contains('newbonklobby_playerentry_menu')) return;

      const meta = getLobbyPlayerMenuOwnerMeta();
      if (!meta || !meta.playerName) return;
      const readSelfNameNorm = () => {
        const selfEl = document.getElementById('pretty_top_name');
        return selfEl ? normalizeName(selfEl.textContent || '') : '';
      };
      const playerNorm = meta.playerNameNorm || normalizeName(meta.playerName);
      const selfNorm = readSelfNameNorm();
      const isSelfTarget = !!selfNorm && !!playerNorm && selfNorm === playerNorm;

      const muteBtn = Array.from(menuEl.querySelectorAll('.newbonklobby_playerentry_menu_button'))
        .find((el) => /^\s*(mute|unmute)\s*$/i.test(String(el.textContent || '').trim()));
      if (isSelfTarget) {
        if (muteBtn instanceof Element) muteBtn.style.display = 'none';
        const existingSelfBtn = menuEl.querySelector('[data-tbc-player-blacklist-button]');
        if (existingSelfBtn && existingSelfBtn.parentNode) existingSelfBtn.parentNode.removeChild(existingSelfBtn);
        return;
      }

      if (menuEl.querySelector('[data-tbc-player-blacklist-button]')) return;
      const baseMuteWidth =
        muteBtn instanceof Element
          ? Math.round((muteBtn.getBoundingClientRect && muteBtn.getBoundingClientRect().width) || muteBtn.offsetWidth || 130)
          : 130;
      if (muteBtn instanceof Element) {
        muteBtn.style.removeProperty('display');
        muteBtn.style.display = 'inline-block';
        muteBtn.style.verticalAlign = 'top';
        muteBtn.style.marginRight = '4px';
      }

      const btn = document.createElement('div');
      btn.className = 'newbonklobby_playerentry_menu_button brownButton buttonShadow brownButton_classic';
      btn.textContent = 'Blacklist';
      btn.title = 'Blacklist';
      btn.setAttribute('aria-label', 'Blacklist');
      btn.setAttribute('data-tbc-player-blacklist-button', '1');
      btn.style.display = 'inline-block';
      btn.style.verticalAlign = 'top';
      const applySplitButtonWidths = () => {
        if (!(muteBtn instanceof Element)) return;
        menuEl.style.removeProperty('min-width');
        const menuWidth = Math.max(96, baseMuteWidth || 130);
        const gap = 4;
        let rightWidth = meta.isGuest ? 24 : 56;
        const minLeft = 54;
        if ((menuWidth - gap - rightWidth) < minLeft) {
          rightWidth = Math.max(24, menuWidth - gap - minLeft);
        }
        const leftWidth = Math.max(44, menuWidth - gap - rightWidth);
        muteBtn.style.width = `${leftWidth}px`;
        btn.style.width = `${rightWidth}px`;
      };
      const fitBlacklistLabel = () => {
        if (!(btn instanceof Element)) return;
        if (meta.isGuest) {
          btn.textContent = 'B';
          return;
        }
        btn.textContent = 'Blacklist';
        const tooLong = btn.scrollWidth > btn.clientWidth;
        if (tooLong) btn.textContent = 'B';
      };

      const refreshState = () => {
        const selfNorm = readSelfNameNorm();
        const disabledTarget = !!meta.isGuest || (!!selfNorm && !!playerNorm && selfNorm === playerNorm);
        const active = !disabledTarget &&
          typeof window.tbcIsNameBlacklisted === 'function' &&
          !!window.tbcIsNameBlacklisted(meta.playerName);
        setLobbyBlacklistButtonVisual(btn, active, disabledTarget);
      };
      refreshState();

      btn.addEventListener('mouseup', (e) => {
        e.preventDefault();
        e.stopPropagation();
        const selfNorm = readSelfNameNorm();
        const disabledTarget = !!meta.isGuest || (!!selfNorm && !!playerNorm && selfNorm === playerNorm);
        if (disabledTarget) return;
        if (typeof window.tbcToggleBlacklistName === 'function') {
          window.tbcToggleBlacklistName(meta.playerName);
        }
        refreshState();
      }, true);

      const groupsBtn = menuEl.querySelector('[data-tbc-player-groups-button]');
      if (muteBtn instanceof Element) muteBtn.insertAdjacentElement('afterend', btn);
      else if (groupsBtn && groupsBtn.parentNode === menuEl) menuEl.insertBefore(btn, groupsBtn);
      else menuEl.appendChild(btn);
      applySplitButtonWidths();
      fitBlacklistLabel();
      requestAnimationFrame(() => {
        applySplitButtonWidths();
        fitBlacklistLabel();
      });
    }

    function installLobbyPlayerMenuOwnerTracking() {
      if (lobbyPlayerMenuOwnerTrackingInstalled) return;
      lobbyPlayerMenuOwnerTrackingInstalled = true;
      const applyMenuTintNow = () => {
        const color = getPlayerListMenuGroupColor();
        document.querySelectorAll('.newbonklobby_playerentry_menu, .newbonklobby_playerentry_menu_submenu').forEach((menuEl) => {
          setPlayerListMenuBackground(menuEl, color);
        });
      };
      const promotePendingMenuOwner = () => {
        if (!lobbyPlayerMenuPendingOwnerNameNorm) return false;
        lobbyPlayerMenuOwnerNameNorm = lobbyPlayerMenuPendingOwnerNameNorm;
        lobbyPlayerMenuPendingOwnerNameNorm = '';
        return true;
      };
      document.addEventListener(
        'mousedown',
        (e) => {
          const target = e && e.target instanceof Element ? e.target : null;
          if (!target) return;
          const row = target.closest('.newbonklobby_playerentry');
          if (row) {
            closeLobbyPlayerGroupsMenus();
            const nameEl = row.querySelector('.newbonklobby_playerentry_name');
            const nm = normalizeName(nameEl ? nameEl.textContent : '');
            if (nm) lobbyPlayerMenuPendingOwnerNameNorm = nm;
            else {
              lobbyPlayerMenuPendingOwnerNameNorm = '';
              lobbyPlayerMenuOwnerNameNorm = '';
            }
            requestAnimationFrame(() => {
              scheduleLobbyScan();
            });
            return;
          }
          const inMenu = !!target.closest('.newbonklobby_playerentry_menu, .newbonklobby_playerentry_menu_submenu, [data-tbc-player-groups-button], [data-tbc-player-groups-action-menu], [data-tbc-player-groups-target-menu]');
          if (!inMenu && (lobbyPlayerMenuOwnerNameNorm || lobbyPlayerMenuPendingOwnerNameNorm)) {
            lobbyPlayerMenuPendingOwnerNameNorm = '';
            closeLobbyPlayerGroupsMenus();
          }
        },
        true
      );
      document.addEventListener(
        'click',
        (e) => {
          const target = e && e.target instanceof Element ? e.target : null;
          if (!target) return;
          const nativeSubBtn = target.closest('.newbonklobby_playerentry_menu_submenu .newbonklobby_playerentry_menu_button');
          if (
            !nativeSubBtn ||
            nativeSubBtn.classList.contains('brownButtonDisabled') ||
            nativeSubBtn.closest('[data-tbc-player-groups-action-menu]') ||
            nativeSubBtn.closest('[data-tbc-player-groups-target-menu]')
          ) {
            return;
          }
          setTimeout(() => {
            document.querySelectorAll('.newbonklobby_playerentry_menu_submenu').forEach((el) => {
              if (
                el instanceof Element &&
                !el.hasAttribute('data-tbc-player-groups-action-menu') &&
                !el.hasAttribute('data-tbc-player-groups-target-menu')
              ) {
                if (el.parentNode) el.parentNode.removeChild(el);
              }
            });
          }, 0);
        },
        true
      );

      waitForElement('newbonklobby', () => {
        const obs = new MutationObserver((mutations) => {
          for (const m of mutations) {
            if (m.type === 'childList' && m.removedNodes && m.removedNodes.length) {
              let removedAnyPlayerMenu = false;
              for (const node of m.removedNodes) {
                if (!(node instanceof Element)) continue;
                if (
                  node.classList &&
                  (
                    node.classList.contains('newbonklobby_playerentry_menu') ||
                    node.classList.contains('newbonklobby_playerentry_menu_submenu')
                  )
                ) {
                  removedAnyPlayerMenu = true;
                  break;
                }
                if (node.querySelector && node.querySelector('.newbonklobby_playerentry_menu, .newbonklobby_playerentry_menu_submenu')) {
                  removedAnyPlayerMenu = true;
                  break;
                }
              }
              if (removedAnyPlayerMenu && !document.querySelector('.newbonklobby_playerentry_menu, .newbonklobby_playerentry_menu_submenu')) {
                lobbyPlayerMenuOwnerNameNorm = '';
                lobbyPlayerMenuPendingOwnerNameNorm = '';
                closeLobbyPlayerGroupsMenus();
              }
            }

            if (
              m.type === 'attributes' &&
              m.target instanceof Element &&
              m.target.classList &&
              (
                m.target.classList.contains('newbonklobby_playerentry_menu') ||
                m.target.classList.contains('newbonklobby_playerentry_menu_submenu')
              )
            ) {
              if (promotePendingMenuOwner()) applyMenuTintNow();
              if (m.target.classList.contains('newbonklobby_playerentry_menu')) {
                injectLobbyPlayerGroupsButton(m.target);
                injectLobbyPlayerBlacklistButton(m.target);
              }
            }

            for (const node of m.addedNodes) {
              if (!(node instanceof Element)) continue;
              if (
                node.classList &&
                (
                  node.classList.contains('newbonklobby_playerentry_menu') ||
                  node.classList.contains('newbonklobby_playerentry_menu_submenu')
                )
              ) {
                promotePendingMenuOwner();
                setPlayerListMenuBackground(node, getPlayerListMenuGroupColor());
                injectLobbyPlayerGroupsButton(node);
                injectLobbyPlayerBlacklistButton(node);
              }
              node.querySelectorAll('.newbonklobby_playerentry_menu, .newbonklobby_playerentry_menu_submenu').forEach((menuEl) => {
                promotePendingMenuOwner();
                setPlayerListMenuBackground(menuEl, getPlayerListMenuGroupColor());
                injectLobbyPlayerGroupsButton(menuEl);
                injectLobbyPlayerBlacklistButton(menuEl);
              });
            }
          }
        });
        obs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
      });
    }

    function setElementNameColor(el, color) {
      if (!el) return;
      const desiredColor = color || '';
      if (desiredColor) {
        if (el.style.color !== desiredColor) el.style.setProperty('color', desiredColor, 'important');
      } else {
        if (el.style.color) el.style.removeProperty('color');
      }
      if (el.style.textShadow) el.style.removeProperty('text-shadow');
    }

    function setElementNameColorWithGrace(el, color, enabled = true, datasetKey = 'tbcNameNoColorSince', graceMs = 1200) {
      if (!el) return;
      if (!enabled) {
        if (el.dataset && datasetKey) delete el.dataset[datasetKey];
        setElementNameColor(el, null);
        return;
      }
      const desiredColor = String(color || '').trim();
      if (desiredColor) {
        if (el.dataset && datasetKey) delete el.dataset[datasetKey];
        setElementNameColor(el, desiredColor);
        return;
      }
      const hasInlineColor = !!(el.style && String(el.style.color || '').trim());
      if (hasInlineColor && el.dataset && datasetKey) {
        const now = Date.now();
        const missAt = parseInt(String(el.dataset[datasetKey] || '0'), 10) || 0;
        if (!missAt) {
          el.dataset[datasetKey] = String(now);
          return;
        }
        if ((now - missAt) < Math.max(0, graceMs)) return;
      }
      if (el.dataset && datasetKey) delete el.dataset[datasetKey];
      setElementNameColor(el, null);
    }

    function ensurePlayerListGroupBgStyles() {
      if ($('tbc_playerlist_group_bg_css')) return;
      const style = document.createElement('style');
      style.id = 'tbc_playerlist_group_bg_css';
      style.textContent = `
        .newbonklobby_playerentry.tbc_group_bg_row {
          transition: none !important;
        }
        .newbonklobby_playerentry.tbc_group_bg_row:hover {
          background-color: var(--tbc-player-bg-hover, transparent) !important;
          box-shadow: inset 0 0 0 1px rgba(255,255,255,0.22);
        }
      `;
      document.head.appendChild(style);
    }

    function setPlayerListEntryBackground(entry, color) {
      if (!entry) return;
      ensurePlayerListGroupBgStyles();
      if (color) {
        const merged = mergeColorWithLobbyBase(color, 0.25);
        if (!entry.classList.contains('tbc_group_bg_row')) entry.classList.add('tbc_group_bg_row');
        if (entry.style.getPropertyValue('--tbc-player-bg-base') !== merged) {
          entry.style.setProperty('--tbc-player-bg-base', merged);
        }
        if (entry.style.getPropertyValue('--tbc-player-bg-hover') !== merged) {
          entry.style.setProperty('--tbc-player-bg-hover', merged);
        }
        if (entry.style.getPropertyValue('background-color') !== 'var(--tbc-player-bg-base)') {
          entry.style.setProperty('background-color', 'var(--tbc-player-bg-base)', 'important');
        }
      } else if (
        entry.classList.contains('tbc_group_bg_row') ||
        entry.style.getPropertyValue('background-color') ||
        entry.style.getPropertyValue('--tbc-player-bg-base') ||
        entry.style.getPropertyValue('--tbc-player-bg-hover')
      ) {
        entry.classList.remove('tbc_group_bg_row');
        entry.style.removeProperty('background-color');
        entry.style.removeProperty('--tbc-player-bg-base');
        entry.style.removeProperty('--tbc-player-bg-hover');
      }
    }

    function setPlayerListMenuBackground(menuEl, color) {
      if (!menuEl) return;
      if (color) {
        menuEl.style.setProperty('background-color', mergeColorWithLobbyBase(color, 0.25), 'important');
      } else if (menuEl.style.backgroundColor) {
        menuEl.style.removeProperty('background-color');
      }
    }

    function mergeColorWithLobbyBase(inputColor, ratio = 0.1) {
      const parseHexToRgb = (hexText) => {
        let s = String(hexText || '').trim().toLowerCase();
        if (!s) return null;
        if (s.charAt(0) !== '#') s = `#${s}`;
        if (/^#[0-9a-f]{3}$/.test(s)) {
          const a = s.charAt(1), b = s.charAt(2), c = s.charAt(3);
          s = `#${a}${a}${b}${b}${c}${c}`;
        }
        if (!/^#[0-9a-f]{6}$/.test(s)) return null;
        return {
          r: parseInt(s.slice(1, 3), 16),
          g: parseInt(s.slice(3, 5), 16),
          b: parseInt(s.slice(5, 7), 16),
        };
      };

      const base = parseHexToRgb('#cfd8dc') || { r: 207, g: 216, b: 220 };
      const raw = String(inputColor || '').trim();
      let hex = null;
      if (/^\s*rgb/i.test(raw)) {
        const nums = raw.match(/-?\d+(\.\d+)?/g);
        if (nums && nums.length >= 3) {
          const r = Math.max(0, Math.min(255, Math.round(parseFloat(nums[0]) || 0)));
          const g = Math.max(0, Math.min(255, Math.round(parseFloat(nums[1]) || 0)));
          const b = Math.max(0, Math.min(255, Math.round(parseFloat(nums[2]) || 0)));
          hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
        }
      } else if (/^\s*hsv/i.test(raw)) {
        const nums = raw.match(/-?\d+(\.\d+)?/g);
        if (nums && nums.length >= 3) {
          let h = parseFloat(nums[0]);
          let s = parseFloat(nums[1]);
          let v = parseFloat(nums[2]);
          if (Number.isFinite(h) && Number.isFinite(s) && Number.isFinite(v)) {
            h = ((h % 360) + 360) % 360;
            s = Math.max(0, Math.min(1, s > 1 ? (s / 100) : s));
            v = Math.max(0, Math.min(1, v > 1 ? (v / 100) : v));
            const c = v * s;
            const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
            const m = v - c;
            let rp = 0, gp = 0, bp = 0;
            if (h < 60) { rp = c; gp = x; bp = 0; }
            else if (h < 120) { rp = x; gp = c; bp = 0; }
            else if (h < 180) { rp = 0; gp = c; bp = x; }
            else if (h < 240) { rp = 0; gp = x; bp = c; }
            else if (h < 300) { rp = x; gp = 0; bp = c; }
            else { rp = c; gp = 0; bp = x; }
            const r = Math.max(0, Math.min(255, Math.round((rp + m) * 255)));
            const g = Math.max(0, Math.min(255, Math.round((gp + m) * 255)));
            const b = Math.max(0, Math.min(255, Math.round((bp + m) * 255)));
            hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
          }
        }
      } else {
        hex = raw;
      }
      const tint = parseHexToRgb(hex) || base;
      const r = Math.max(0, Math.min(1, Number(ratio) || 0));
      const outR = Math.round((base.r * (1 - r)) + (tint.r * r));
      const outG = Math.round((base.g * (1 - r)) + (tint.g * r));
      const outB = Math.round((base.b * (1 - r)) + (tint.b * r));
      return `rgb(${outR}, ${outG}, ${outB})`;
    }

    function getPlayerListMenuGroupColor() {
      if (!recolorDisplaySettings.playerListBackboard) return null;
      if (!lobbyPlayerMenuOwnerNameNorm) return null;
      const meta = getLobbyPlayerMenuOwnerMeta();
      const memberType = meta && typeof meta.isGuest === 'boolean' ? (meta.isGuest ? 'guest' : 'account') : 'any';
      return resolveLobbyPlayerPersistentColor(lobbyPlayerMenuOwnerNameNorm, true, false, memberType);
    }

    function resolveLobbyPlayerPersistentColor(playerName, allowSticky = true, clearStickyWhenMissing = false, memberType = 'any') {
      const nm = normalizeName(playerName);
      if (!nm) return null;
      const key = `${nm}|${getLookupTypeSuffix(memberType)}`;
      const live = getDisplayColorForName(playerName, { memberType });
      if (live) {
        lobbyPersistentColorByName.set(key, live);
        return live;
      }
      if (allowSticky && lobbyPersistentColorByName.has(key)) return lobbyPersistentColorByName.get(key) || null;
      if (clearStickyWhenMissing) lobbyPersistentColorByName.delete(key);
      return null;
    }

    function getLobbyPersistentColorVersion(nameNorm) {
      if (!nameNorm) return 0;
      return lobbyPersistentColorVersionByName.get(nameNorm) || 0;
    }

    function invalidateLobbyPersistentColorsForPlayers(players, clearSharedCache = false) {
      const changed = [];
      const seen = new Set();
      const list = Array.isArray(players) ? players : [];
      list.forEach((name) => {
        const nm = normalizeName(name);
        if (!nm || seen.has(nm)) return;
        seen.add(nm);
        changed.push(nm);
        lobbyPersistentColorByName.delete(`${nm}|account`);
        lobbyPersistentColorByName.delete(`${nm}|guest`);
        lobbyPersistentColorByName.delete(`${nm}|any`);
        lobbyPersistentColorVersionByName.set(nm, getLobbyPersistentColorVersion(nm) + 1);
      });
      colorCache.clear();
      if (clearSharedCache) {
        lastSharedColorSig = '';
        sharedColorCache.clear();
      }
      return changed;
    }

    function refreshLobbyPlayerEntriesForNames(nameNorms, clearWhenNoColor = true) {
      const targets = Array.isArray(nameNorms) ? nameNorms.filter(Boolean) : [];
      if (!targets.length) return;
      const want = new Set(targets);
      document.querySelectorAll('.newbonklobby_playerentry').forEach((entry) => {
        const nameEl = entry.querySelector('.newbonklobby_playerentry_name');
        const nm = normalizeName(nameEl ? nameEl.textContent : '');
        if (!nm || !want.has(nm)) return;
        applyLobbyPlayerEntryColor(entry, clearWhenNoColor, false, true);
      });
    }

    function applyLobbyPlayerEntryColor(entry, clearWhenNoColor = true, allowSticky = true, clearStickyWhenMissing = false) {
      if (!entry || !(entry instanceof Element)) return;
      const nameEl = entry.querySelector('.newbonklobby_playerentry_name');
      const levelEl = entry.querySelector('.newbonklobby_playerentry_level');
      if (!nameEl || !levelEl) return;

      if (!recolorDisplaySettings.playerListNames && !recolorDisplaySettings.playerListBackboard) {
        setElementNameColor(nameEl, null);
        setPlayerListEntryBackground(entry, null);
        updateLobbyPlayerGuestGroupBadge(nameEl, false);
        return;
      }

      const levelText = (levelEl.textContent || '').trim().toLowerCase();
      const isLevelled = /^level\b/.test(levelText) || /^lv\b/.test(levelText) || /^lvl\b/.test(levelText);
      const memberType = /\bguest\b/.test(levelText) ? 'guest' : 'account';
      const playerName = (nameEl.textContent || '').trim();
      if (!playerName) {
        if (clearWhenNoColor) {
          setElementNameColor(nameEl, null);
          setPlayerListEntryBackground(entry, null);
        }
        updateLobbyPlayerGuestGroupBadge(nameEl, false);
        delete entry.dataset.tbcColorNameNorm;
        delete entry.dataset.tbcColorMemberType;
        delete entry.dataset.tbcColorEpoch;
        delete entry.dataset.tbcColorVersion;
        delete entry.dataset.tbcColorValue;
        return;
      }

      const nameNorm = normalizeName(playerName);
      const sourceEpoch = lobbyPersistentColorEpoch;
      const sourceVersion = getLobbyPersistentColorVersion(nameNorm);
      const cacheMemberKey = `${nameNorm}|${getLookupTypeSuffix(memberType)}`;
      const cachedNameNorm = String(entry.dataset.tbcColorNameNorm || '');
      const cachedEpoch = parseInt(String(entry.dataset.tbcColorEpoch || '0'), 10) || 0;
      const cachedVersion = parseInt(String(entry.dataset.tbcColorVersion || '0'), 10) || 0;
      const cachedType = String(entry.dataset.tbcColorMemberType || '');
      const canReuseRowColor = allowSticky && cachedNameNorm === nameNorm && cachedType === cacheMemberKey && cachedEpoch === sourceEpoch && cachedVersion === sourceVersion;
      let color = null;
      if (canReuseRowColor) {
        color = String(entry.dataset.tbcColorValue || '').trim() || null;
      } else {
        if (allowSticky && !clearStickyWhenMissing && lobbyPersistentColorByName.has(cacheMemberKey)) {
          color = lobbyPersistentColorByName.get(cacheMemberKey) || null;
        } else {
          color = resolveLobbyPlayerPersistentColor(playerName, allowSticky, clearStickyWhenMissing, memberType);
        }
        entry.dataset.tbcColorNameNorm = nameNorm;
        entry.dataset.tbcColorMemberType = cacheMemberKey;
        entry.dataset.tbcColorEpoch = String(sourceEpoch);
        entry.dataset.tbcColorVersion = String(sourceVersion);
        if (color) entry.dataset.tbcColorValue = color;
        else delete entry.dataset.tbcColorValue;
      }
      const immediateAuthoritativeRefresh = !allowSticky && !!clearStickyWhenMissing;
      const nameGraceMs = immediateAuthoritativeRefresh ? 0 : 1200;
      const backboardGraceMs = immediateAuthoritativeRefresh ? 0 : 1200;
      if (recolorDisplaySettings.playerListNames && isLevelled) {
        setElementNameColorWithGrace(nameEl, color, true, 'tbcPlayerListNameNoColorSince', nameGraceMs);
      } else {
        setElementNameColorWithGrace(nameEl, null, false, 'tbcPlayerListNameNoColorSince', 0);
      }

      if (recolorDisplaySettings.playerListBackboard) {
        const hasExistingBg =
          entry.classList.contains('tbc_group_bg_row') ||
          !!entry.style.getPropertyValue('--tbc-player-bg-base') ||
          !!entry.style.getPropertyValue('background-color');
        if (color) {
          delete entry.dataset.tbcNoColorSince;
          setPlayerListEntryBackground(entry, color);
        } else if (clearWhenNoColor) {
          if (hasExistingBg && backboardGraceMs > 0) {
            const now = Date.now();
            const missAt = parseInt(String(entry.dataset.tbcNoColorSince || '0'), 10) || 0;
            if (!missAt) {
              entry.dataset.tbcNoColorSince = String(now);
            } else if ((now - missAt) >= backboardGraceMs) {
              delete entry.dataset.tbcNoColorSince;
              setPlayerListEntryBackground(entry, null);
            }
          } else {
            delete entry.dataset.tbcNoColorSince;
            setPlayerListEntryBackground(entry, null);
          }
        } else {
          delete entry.dataset.tbcNoColorSince;
        }
      } else {
        delete entry.dataset.tbcNoColorSince;
        setPlayerListEntryBackground(entry, null);
      }

      const isGuest = memberType === 'guest';
      const showGuestGroupWarn = isGuest && (!!getDisplayColorForName(playerName, { memberType: 'guest' }) || isPlayerInAnyGroup(playerName, 'guest'));
      updateLobbyPlayerGuestGroupBadge(nameEl, showGuestGroupWarn);
    }

    function updateLobbyPlayerGuestGroupBadge(nameEl, show) {
      if (!(nameEl instanceof Element)) return;
      let badge = nameEl.querySelector(':scope > .tbc_guest_warn_badge_playerlist');
      if (!show) {
        if (badge && badge.parentNode) badge.parentNode.removeChild(badge);
        return;
      }
      if (!badge) {
        badge = document.createElement('span');
        badge.className = 'tbc_guest_warn_badge tbc_guest_warn_badge_playerlist';
        badge.title = 'Temporary guest member. Removed when guest/room/host state changes.';
        badge.setAttribute('aria-label', 'Temporary guest member');
        nameEl.appendChild(badge);
      }
    }

    function refreshLobbyPersistentColorsFromGroups() {
      lobbyPersistentColorEpoch += 1;
      lobbyPersistentColorByName.clear();
      const rows = document.querySelectorAll('.newbonklobby_playerentry');
      rows.forEach((row) => applyLobbyPlayerEntryColor(row, true, false, true));
      const menuColor = getPlayerListMenuGroupColor();
      const playerMenus = document.querySelectorAll('.newbonklobby_playerentry_menu, .newbonklobby_playerentry_menu_submenu');
      playerMenus.forEach((menuEl) => setPlayerListMenuBackground(menuEl, menuColor));
    }

    function applyLobbyNameColors(includePlayerList = true) {
      if (includePlayerList) {
        const playerEntries = document.querySelectorAll('.newbonklobby_playerentry');
        playerEntries.forEach((entry) => {
          applyLobbyPlayerEntryColor(entry, false, true, false);
        });
      }
      const menuColor = getPlayerListMenuGroupColor();
      const playerMenus = document.querySelectorAll('.newbonklobby_playerentry_menu, .newbonklobby_playerentry_menu_submenu');
      playerMenus.forEach((menuEl) => setPlayerListMenuBackground(menuEl, menuColor));

      const lobbyChatNames = document.querySelectorAll(
        '#newbonklobby_chat_content .newbonklobby_chat_msg_name'
      );

      lobbyChatNames.forEach((nameSpan) => {
        if (!recolorDisplaySettings.lobbyChatNames) {
          setElementNameColorWithGrace(nameSpan, null, false, 'tbcChatNameNoColorSince', 0);
          return;
        }
        const raw = (nameSpan.textContent || '').trim();
        const cleanName = extractChatName(raw);
        const color = getDisplayColorForName(cleanName);
        setElementNameColorWithGrace(nameSpan, color, true, 'tbcChatNameNoColorSince', 1400);
      });

      const inGameChatNames = document.querySelectorAll('#ingamechatcontent .ingamechatname');
      inGameChatNames.forEach((nameSpan) => {
        if (!recolorDisplaySettings.ingameChatNames) {
          setElementNameColorWithGrace(nameSpan, null, false, 'tbcChatNameNoColorSince', 0);
          return;
        }
        const raw = (nameSpan.textContent || '').trim();
        const cleanName = extractChatName(raw);
        const color = getDisplayColorForName(cleanName);
        setElementNameColorWithGrace(nameSpan, color, true, 'tbcChatNameNoColorSince', 1400);
      });

    }

    let winnerScanQueued = false;
    let winnerGroupedMode = false;
    let winnerSourceEntries = [];
    let lastGroupedWinnerWinSig = '';
    let groupedWinnerEndRetryTimer = null;

    function resetWinnerBoardTransientState() {
      winnerGroupedMode = false;
      winnerSourceEntries = [];
      lastGroupedWinnerWinSig = '';
      if (groupedWinnerEndRetryTimer) {
        clearTimeout(groupedWinnerEndRetryTimer);
        groupedWinnerEndRetryTimer = null;
      }
      const left = document.getElementById('ingamewinner_scores_left');
      if (left) {
        delete left.dataset.tbcWinnerGrouped;
      }
      clearWinnerHeadlineColor();
      if (typeof window.tbcInvalidatePointsPanel === 'function') {
        try { window.tbcInvalidatePointsPanel(); } catch {}
      }
      const pointsVisible =
        typeof window.tbcIsPointsPanelVisible === 'function' &&
        !!window.tbcIsPointsPanelVisible();
      if (pointsVisible && typeof window.tbcRenderPointsPanel === 'function') {
        try { window.tbcRenderPointsPanel(); } catch {}
      }
    }

    function resetPointsPanelScoreSnapshot() {
      if (typeof window.tbcResetPointsPanelCache === 'function') {
        try { window.tbcResetPointsPanelCache(); } catch {}
      } else if (typeof window.tbcInvalidatePointsPanel === 'function') {
        try { window.tbcInvalidatePointsPanel(); } catch {}
      }
      winnerSourceEntries = [];
      winnerGroupedMode = false;
      lastGroupedWinnerWinSig = '';
    }

    function scheduleWinnerScan() {
      if (winnerScanQueued) return;
      winnerScanQueued = true;
      requestAnimationFrame(() => {
        winnerScanQueued = false;
        applyWinnerNameColors();
        const pointsVisible =
          typeof window.tbcIsPointsPanelVisible === 'function' &&
          !!window.tbcIsPointsPanelVisible();
        if (pointsVisible && typeof window.tbcRenderPointsPanel === 'function') {
          try { window.tbcRenderPointsPanel(); } catch {}
        }
      });
    }

    function isTeamsOffForWinnerBoard() {
      const t1 = String((($('newbonklobby_teams_middletext') || {}).textContent || '')).trim().toLowerCase();
      const t2 = String((($('newbonklobby_teamsbutton') || {}).textContent || '')).trim().toLowerCase();
      return /\boff\b/.test(t1) || /\bffa\b/.test(t1) || /\boff\b/.test(t2) || /\bffa\b/.test(t2);
    }

    function getWinnerRoundsToWinCap() {
      const top = document.getElementById('ingamewinner_scores_top');
      const topText = String((top && (top.textContent || top.innerText)) || '');
      const topMatch = topText.match(/first\s*to\s*(\d+)\s*wins/i);
      if (topMatch && topMatch[1]) {
        const n = parseInt(topMatch[1], 10);
        if (Number.isFinite(n) && n > 0) return n;
      }
      const roundsInput = $('newbonklobby_roundsinput');
      const fallback = parseInt(String((roundsInput && roundsInput.value) || ''), 10);
      if (Number.isFinite(fallback) && fallback > 0) return fallback;
      return 0;
    }

    function getGroupedWinnerReachedCapEntry(entries) {
      const cap = getWinnerRoundsToWinCap();
      if (cap <= 0 || !Array.isArray(entries) || !entries.length) return null;
      const hit = entries.find((e) => (Number(e && e.score) || 0) >= cap);
      if (!hit) return null;
      return {
        name: String(hit.name || '').trim(),
        color: String(hit.color || '').trim(),
        score: Number(hit.score) || 0,
        cap,
      };
    }

    function applyGroupedWinnerHeadlineOverride(winEntry) {
      if (!winEntry || !winEntry.name) return;
      const top = document.getElementById('ingamewinner_top');
      const bottom = document.getElementById('ingamewinner_bottom');
      if (top) {
        top.textContent = String(winEntry.name);
        setElementNameColor(top, winEntry.color ? String(winEntry.color) : '');
      }
      if (bottom) bottom.textContent = 'WINS';
    }

    function clearWinnerHeadlineColor() {
      const top = document.getElementById('ingamewinner_top');
      if (!top) return;
      setElementNameColor(top, '');
    }

    function getWinnerHeadlineName() {
      const top = document.getElementById('ingamewinner_top');
      if (!top) return '';
      const raw = String((top.innerText || top.textContent) || '').replace(/\r/g, '');
      if (!raw) return '';
      const lines = raw
        .split('\n')
        .map((s) => s.trim())
        .filter(Boolean);
      if (!lines.length) return '';
      for (const line of lines) {
        const stripped = line.replace(/\b(scores?|wins?)\b/ig, '').replace(/:\s*$/, '').trim();
        if (stripped) return stripped;
      }
      return lines[0].replace(/:\s*$/, '').trim();
    }

    function applyWinnerHeadlineColor(resolveColor) {
      const top = document.getElementById('ingamewinner_top');
      if (!top) return;
      const winnerName = getWinnerHeadlineName();
      if (!winnerName || typeof resolveColor !== 'function') {
        clearWinnerHeadlineColor();
        return;
      }
      const color = resolveColor(winnerName);
      setElementNameColor(top, color ? String(color) : '');
    }

    function rewriteLatestScoresStatusToWins(winEntry) {
      if (!winEntry || !winEntry.name) return;
      const carrierSelectors = [
        '#ingamechatcontent .ingamechatstatus',
        '#ingamechatcontent .ingamechatmessage',
        '#ingamechatcontent .ingamechattext',
        '#newbonklobby_chat_content .newbonklobby_chat_status',
      ];
      const carriers = Array.from(document.querySelectorAll(carrierSelectors.join(', ')));
      for (let i = carriers.length - 1; i >= 0; i -= 1) {
        const el = carriers[i];
        const text = String((el && (el.textContent || el.innerText)) || '').trim();
        if (!text) continue;
        if (/^\*\s+.+\s+wins!?$/i.test(text)) return;
        if (!/^\*\s+.+\s+scores!?$/i.test(text)) continue;
        el.textContent = `* ${String(winEntry.name)} wins!`;
        return;
      }
    }

    function tryClickElement(el) {
      if (!(el instanceof Element)) return false;
      try {
        if (typeof PointerEvent !== 'undefined') {
          el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true, pointerType: 'mouse' }));
          el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, cancelable: true, pointerType: 'mouse' }));
        }
        el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
        el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
        el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
        if (typeof el.onclick === 'function') {
          try { el.onclick(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } catch {}
        }
        if (typeof el.click === 'function') el.click();
        return true;
      } catch {
        return false;
      }
    }

    function triggerPrettyTopExitHard() {
      const topExit = document.getElementById('pretty_top_exit');
      if (!(topExit instanceof Element)) return false;
      if (tryClickElement(topExit)) return true;
      try {
        const parent = topExit.parentElement || document;
        parent.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
      } catch {}
      return true;
    }

    function isVisibleActionElement(el) {
      if (!(el instanceof Element)) return false;
      const cs = window.getComputedStyle(el);
      return (
        cs.display !== 'none' &&
        cs.visibility !== 'hidden' &&
        cs.opacity !== '0' &&
        el.getClientRects().length > 0
      );
    }

    function isElementActuallyVisibleSafe(el) {
      if (!el || !(el instanceof Element)) return false;
      if (typeof isElementActuallyVisible === 'function') {
        try { return !!isElementActuallyVisible(el); } catch {}
      }
      const cs = window.getComputedStyle(el);
      return (
        cs.display !== 'none' &&
        cs.visibility !== 'hidden' &&
        cs.opacity !== '0' &&
        el.getClientRects().length > 0
      );
    }

    function attemptHostEndGameToLobby() {
      if (typeof isSelfLobbyHost === 'function' && !isSelfLobbyHost()) return false;
      const renderer = $('gamerenderer');
      if (!isElementActuallyVisibleSafe(renderer)) return false;

      const confirmSelectors = [
        '#leaveconfirmwindow_okbutton',
        '#leaveconfirmwindow_ok',
        '#leaveconfirmwindow_yesbutton',
        '#leaveconfirmwindow_yes',
        '#hostleaveconfirmwindow_okbutton',
        '#hostleaveconfirmwindow_ok',
        '#hostleaveconfirmwindow_yesbutton',
        '#hostleaveconfirmwindow_yes',
      ];
      for (const sel of confirmSelectors) {
        const btn = document.querySelector(sel);
        if (btn && isVisibleActionElement(btn) && tryClickElement(btn)) return true;
      }

      const topExitClicked = triggerPrettyTopExitHard();

      const specificSelectors = [
        '#ingamewinner_continuebutton',
        '#ingamewinner_continue',
        '#ingamewinner_lobbybutton',
        '#ingamewinner_backtolobby',
        '#ingamewinner_exitbutton',
        '#esc_lobbybutton',
        '#esc_leavebutton',
      ];
      for (const sel of specificSelectors) {
        const btn = document.querySelector(sel);
        if (btn && isVisibleActionElement(btn) && tryClickElement(btn)) return true;
      }

      const genericButtons = Array.from(document.querySelectorAll('button, .brownButton, .buttonShadow'))
        .filter(isVisibleActionElement);
      const textMatcher = /\b(return|lobby|leave|exit|quit|end game|back)\b/i;
      for (const btn of genericButtons) {
        const txt = String((btn.textContent || btn.innerText) || '').trim();
        if (!txt || !textMatcher.test(txt)) continue;
        if (tryClickElement(btn)) return true;
      }

      if (topExitClicked) return false;

      try {
        const evtDown = new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true, cancelable: true });
        const evtUp = new KeyboardEvent('keyup', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true, cancelable: true });
        document.dispatchEvent(evtDown);
        document.dispatchEvent(evtUp);
      } catch {}
      return false;
    }

    function scheduleHostEndGameToLobbyForGroupedWin(winEntry, groupedEntries) {
      if (!winEntry || !winEntry.name || !Array.isArray(groupedEntries) || !groupedEntries.length) return;
      const renderer = $('gamerenderer');
      if (!isElementActuallyVisibleSafe(renderer)) return;
      if (typeof isSelfLobbyHost === 'function' && !isSelfLobbyHost()) return;

      const sig = `${String(winEntry.name)}|${String(winEntry.cap)}|${groupedEntries.map((e) => `${normalizeName(e.name)}:${Number(e.score) || 0}`).join(',')}`;
      if (sig === lastGroupedWinnerWinSig) return;
      lastGroupedWinnerWinSig = sig;

      if (groupedWinnerEndRetryTimer) {
        clearTimeout(groupedWinnerEndRetryTimer);
        groupedWinnerEndRetryTimer = null;
      }

      let attempts = 0;
      const runAttempt = () => {
        groupedWinnerEndRetryTimer = null;
        const lobbyVisible = isElementActuallyVisibleSafe($('newbonklobby'));
        const rendererVisible = isElementActuallyVisibleSafe($('gamerenderer'));
        if (lobbyVisible || !rendererVisible) return;
        if (attemptHostEndGameToLobby()) return;
        attempts += 1;
        if (attempts < 8) {
          groupedWinnerEndRetryTimer = setTimeout(runAttempt, 300);
        }
      };
      groupedWinnerEndRetryTimer = setTimeout(runAttempt, 350);
    }

    function extractWinnerColumnLines(el) {
      if (!el || !(el instanceof Element)) return [];
      const normalizeLine = (txt) => String(txt || '').replace(/\s+/g, ' ').trim();
      const directChildren = Array.from(el.children || []);
      const directNonBreakChildren = directChildren.filter((child) => String((child && child.tagName) || '').toUpperCase() !== 'BR');
      if (directNonBreakChildren.length > 1) {
        const directLines = directNonBreakChildren
          .map((child) => normalizeLine((child && child.textContent) || ''))
          .filter(Boolean);
        if (directLines.length > 1) return directLines;
      }

      const html = String(el.innerHTML || '');
      if (/<br\s*\/?>/i.test(html)) {
        const fromHtml = html
          .split(/<br\s*\/?>/i)
          .map((part) => normalizeLine(String(part || '').replace(/<[^>]+>/g, ' ')))
          .filter(Boolean);
        if (fromHtml.length) return fromHtml;
      }

      const out = [];
      let cur = '';
      const flush = () => {
        const s = normalizeLine(cur);
        if (s) out.push(s);
        cur = '';
      };
      const nodes = Array.from(el.childNodes || []);
      if (nodes.length) {
        nodes.forEach((node) => {
          if (!node) return;
          const type = node.nodeType || 0;
          if (type === 3) {
            cur += String(node.nodeValue || '');
            return;
          }
          if (type !== 1) return;
          const tag = String((node.nodeName || '')).toUpperCase();
          if (tag === 'BR') {
            flush();
            return;
          }
          const isDirectChild = node.parentNode === el;
          if (isDirectChild && directNonBreakChildren.length > 1) {
            flush();
            cur += String((node.textContent || ''));
            flush();
            return;
          }
          cur += String((node.textContent || ''));
        });
        flush();
      }
      if (!out.length) {
        const txt = String((el.innerText || el.textContent) || '')
          .replace(/\r/g, '')
          .split('\n')
          .map((s) => s.trim())
          .filter(Boolean);
        return txt;
      }
      return out;
    }

    function splitCollapsedWinnerScoreToken(rawToken, expectedParts, cap) {
      const token = String(rawToken || '').replace(/[^\d-]/g, '');
      if (!token || !Number.isFinite(expectedParts) || expectedParts < 2) return [];
      let sign = 1;
      let body = token;
      if (body.startsWith('-')) {
        sign = -1;
        body = body.slice(1);
      }
      if (!body) return [];
      const maxCap = Number.isFinite(cap) && cap > 0 ? Math.max(0, Math.floor(cap)) : 99;
      const memo = new Map();
      const dfs = (idx, partIdx) => {
        if (partIdx === expectedParts) return idx === body.length ? [] : null;
        const key = `${idx}|${partIdx}`;
        if (memo.has(key)) return memo.get(key);
        const remainParts = expectedParts - partIdx;
        const remainChars = body.length - idx;
        let best = null;
        const maxLen = Math.max(1, remainChars - (remainParts - 1));
        for (let len = 1; len <= maxLen; len += 1) {
          const nextRemain = remainChars - len;
          const minNeeded = remainParts - 1;
          if (nextRemain < minNeeded) continue;
          const chunk = body.slice(idx, idx + len);
          const n = parseInt(chunk, 10);
          if (!Number.isFinite(n) || n > maxCap) continue;
          const tail = dfs(idx + len, partIdx + 1);
          if (tail) {
            best = [sign * n].concat(tail);
            break;
          }
        }
        memo.set(key, best);
        return best;
      };
      const solved = dfs(0, 0);
      if (solved && solved.length === expectedParts) return solved;
      if (maxCap <= 9 && body.length >= expectedParts) {
        const firstDigits = body
          .slice(0, expectedParts)
          .split('')
          .map((d) => sign * (parseInt(d, 10) || 0));
        if (firstDigits.length === expectedParts && firstDigits.every((n) => Number.isFinite(n) && Math.abs(n) <= maxCap)) {
          return firstDigits;
        }
      }
      return [];
    }

    function isLikelyKnownWinnerToken(nameText) {
      const nm = String(nameText || '').trim();
      if (!nm) return false;
      if (isLikelyTeamWinnerName(nm)) return true;
      if (isLikelyPlayerWinnerName(nm)) return true;
      const key = normalizeWinnerLookupName(nm);
      if (!key) return false;
      const shared = Array.isArray(window.tbcSharedGroupsSnapshot) ? window.tbcSharedGroupsSnapshot : [];
      for (const g of shared) {
        const gName = normalizeWinnerLookupName(g && g.name);
        if (gName && gName === key) return true;
      }
      if (Array.isArray(colorGroups)) {
        for (const g of colorGroups) {
          const gName = normalizeWinnerLookupName(g && g.name);
          if (gName && gName === key) return true;
        }
      }
      return false;
    }

    function expandCollapsedWinnerEntries(entries, rightEl) {
      if (!Array.isArray(entries) || entries.length !== 1) return entries;
      const row = entries[0] || {};
      const rowName = String(row.name || '').trim();
      if (!rowName || rowName.indexOf(':') === -1) return entries;

      const rawParts = rowName
        .split(':')
        .map((s) => String(s || '').trim())
        .filter(Boolean);
      if (rawParts.length < 2) return entries;
      const knownCount = rawParts.filter((p) => isLikelyKnownWinnerToken(p)).length;
      if (knownCount < 2) return entries;

      const cap = getWinnerRoundsToWinCap();
      let scores = [];
      const rightNums = rightEl
        ? (String((rightEl.innerText || rightEl.textContent) || '').match(/-?\d+/g) || [])
        : [];
      if (rightNums.length >= rawParts.length) {
        scores = rightNums
          .slice(0, rawParts.length)
          .map((n) => parseInt(String(n || ''), 10))
          .filter((n) => Number.isFinite(n));
      } else if (rightNums.length === 1) {
        scores = splitCollapsedWinnerScoreToken(rightNums[0], rawParts.length, cap);
        if (!scores.length) {
          const digits = String(rightNums[0] || '').replace(/[^\d]/g, '');
          if (digits.length === rawParts.length) {
            scores = digits.split('').map((d) => parseInt(d, 10));
          }
        }
      } else {
        scores = splitCollapsedWinnerScoreToken(String(Number(row.score) || 0), rawParts.length, cap);
      }
      if (!Array.isArray(scores) || scores.length !== rawParts.length) return entries;

      const rebuilt = rawParts
        .map((name, idx) => ({
          name: canonicalizeWinnerName(extractChatName(name)),
          score: Number(scores[idx]) || 0,
        }))
        .filter((e) => !!e.name);
      return rebuilt.length >= 2 ? rebuilt : entries;
    }

    function parseWinnerEntriesFromDom(leftEl, rightEl) {
      const splitNameSpans = leftEl ? Array.from(leftEl.querySelectorAll(':scope > .tbc_winner_line')) : [];
      const splitScoreSpans = rightEl ? Array.from(rightEl.querySelectorAll(':scope > .tbc_winner_score_line')) : [];
      if (splitNameSpans.length && splitScoreSpans.length) {
        const n = Math.min(splitNameSpans.length, splitScoreSpans.length);
        const out = [];
        for (let i = 0; i < n; i += 1) {
          const nameRaw = String(
            splitNameSpans[i].dataset.playerName ||
            splitNameSpans[i].textContent ||
            splitNameSpans[i].innerText ||
            ''
          ).replace(/:\s*$/, '');
          const nm = canonicalizeWinnerName(extractChatName(nameRaw));
          const scoreRaw = String(splitScoreSpans[i].textContent || splitScoreSpans[i].innerText || '');
          const score = parseInt(scoreRaw.replace(/[^\d-]/g, ''), 10);
          if (!nm || !Number.isFinite(score)) continue;
          out.push({ name: nm, score });
        }
        if (out.length) return expandCollapsedWinnerEntries(out, rightEl);
      }

      const leftLines = extractWinnerColumnLines(leftEl);
      const rightLines = extractWinnerColumnLines(rightEl);
      const out = [];
      if (leftLines.length && rightLines.length) {
        const lineCount = Math.min(leftLines.length, rightLines.length);
        for (let i = 0; i < lineCount; i += 1) {
          const nm = canonicalizeWinnerName(extractChatName(String(leftLines[i] || '').replace(/:\s*$/, '')));
          const score = parseInt(String(rightLines[i] || '').replace(/[^\d-]/g, ''), 10);
          if (!nm || !Number.isFinite(score)) continue;
          out.push({ name: nm, score });
        }
        if (out.length) return expandCollapsedWinnerEntries(out, rightEl);
      }
      if (leftLines.length && rightEl) {
        const rightNums = String((rightEl.innerText || rightEl.textContent) || '')
          .match(/-?\d+/g);
        if (rightNums && rightNums.length) {
          const n = Math.min(leftLines.length, rightNums.length);
          for (let i = 0; i < n; i += 1) {
            const nm = canonicalizeWinnerName(extractChatName(String(leftLines[i] || '').replace(/:\s*$/, '')));
            const score = parseInt(String(rightNums[i] || ''), 10);
            if (!nm || !Number.isFinite(score)) continue;
            out.push({ name: nm, score });
          }
          if (out.length) return expandCollapsedWinnerEntries(out, rightEl);
        }
      }
      for (const ln of leftLines) {
        const m = String(ln || '').match(/^(.*?):\s*(-?\d+)\s*$/);
        if (!m) continue;
        const nm = canonicalizeWinnerName(extractChatName(String(m[1] || '').trim()));
        const score = parseInt(String(m[2] || '0'), 10);
        if (!nm || !Number.isFinite(score)) continue;
        out.push({ name: nm, score });
      }
      return expandCollapsedWinnerEntries(out, rightEl);
    }

    function isLikelyTeamWinnerName(nameText) {
      const n = String(nameText || '').trim().toLowerCase();
      if (!n) return false;
      return (
        n === 'red team' ||
        n === 'blue team' ||
        n === 'green team' ||
        n === 'yellow team' ||
        n === 'free for all' ||
        n === 'spectators'
      );
    }

    function isLikelyPlayerWinnerName(nameText) {
      const n = normalizeWinnerLookupName(nameText);
      if (!n || isLikelyTeamWinnerName(nameText)) return false;
      const rows = document.querySelectorAll('.newbonklobby_playerentry_name');
      for (const el of rows) {
        const rowName = normalizeWinnerLookupName(el && el.textContent);
        if (rowName && rowName === n) return true;
      }
      return false;
    }

    function renderWinnerEntries(leftEl, rightEl, entries, colorFn = null) {
      leftEl.textContent = '';
      if (rightEl) rightEl.textContent = '';
      leftEl.dataset.tbcSplit = '1';
      entries.forEach((entry, idx) => {
        const line = document.createElement('span');
        line.className = 'tbc_winner_line';
        line.dataset.playerName = String(entry.name || '');
        line.textContent = `${String(entry.name || '')}:`;
        const c = colorFn ? colorFn(String(entry.name || ''), entry, idx) : null;
        setElementNameColor(line, c ? String(c) : '');
        leftEl.appendChild(line);
        leftEl.appendChild(document.createElement('br'));

        if (rightEl) {
          const scoreLine = document.createElement('span');
          scoreLine.className = 'tbc_winner_score_line';
          scoreLine.textContent = String(entry.score);
          rightEl.appendChild(scoreLine);
          rightEl.appendChild(document.createElement('br'));
        }
      });
    }

    function buildGroupedWinnerEntries(sourceEntries) {
      const shared = Array.isArray(window.tbcSharedGroupsSnapshot) ? window.tbcSharedGroupsSnapshot : [];
      const local = Array.isArray(colorGroups)
        ? colorGroups
            .map((g) => {
              if (!g || typeof g !== 'object') return null;
              const name = String(g.name || '').trim();
              const color = String(g.color || '').trim();
              const playersRaw = Array.isArray(g.players) ? g.players : [];
              const players = playersRaw
                .map((p) => {
                  if (typeof p === 'string') return { name: String(p || '').trim(), memberType: 'account' };
                  if (p && typeof p === 'object') {
                    const name = String(p.name || '').trim();
                    const memberType = normalizeMemberType(p.memberType || (p.tempGuest ? 'guest' : 'account'));
                    return { name, memberType: memberType === 'any' ? 'account' : memberType };
                  }
                  return null;
                })
                .filter((p) => p && p.name);
              if (!name) return null;
              return { name, color, players };
            })
            .filter(Boolean)
        : [];
      const selfNorm = normalizeName(typeof getSelfNameNorm === 'function' ? getSelfNameNorm() : '');
      const syncedHostNorm = normalizeName(typeof liveGroupsSyncHostNorm === 'string' ? liveGroupsSyncHostNorm : '');
      const hostBySyncedName = !!selfNorm && !!syncedHostNorm && selfNorm === syncedHostNorm;
      const hostByLobbyHost =
        typeof isSelfLobbyHost === 'function'
          ? !!isSelfLobbyHost()
          : (typeof isSelfLobbyHostForRecolor === 'function' ? !!isSelfLobbyHostForRecolor() : false);
      const hostLocalView =
        !!window.tbcRoomGroupsSyncActive &&
        local.length > 0 &&
        (hostBySyncedName || hostByLobbyHost);
      if (!Array.isArray(sourceEntries) || !sourceEntries.length) return null;
      const normalizeWinnerLoose = (nameText) => {
        return normalizeWinnerLookupName(nameText)
          .replace(/[i1|]/g, 'l')
          .replace(/0/g, 'o');
      };
      const levelStateByName = new Map();
      document.querySelectorAll('.newbonklobby_playerentry').forEach((row) => {
        const nameEl = row.querySelector('.newbonklobby_playerentry_name');
        const lvlEl = row.querySelector('.newbonklobby_playerentry_level');
        const nm = normalizeWinnerLookupName(String((nameEl && nameEl.textContent) || ''));
        const lvl = String((lvlEl && lvlEl.textContent) || '').trim().toLowerCase();
        if (!nm) return;
        const bucket = levelStateByName.get(nm) || { guest: 0, account: 0, unknown: 0 };
        if (/\bguest\b/.test(lvl)) bucket.guest += 1;
        else if (lvl) bucket.account += 1;
        else bucket.unknown += 1;
        levelStateByName.set(nm, bucket);
      });
      const normalizeColorLoose = (colorText) => String(colorText || '').trim().toLowerCase();
      const resolveRowMemberType = (winnerName) => {
        const key = normalizeWinnerLookupName(winnerName);
        const state = levelStateByName.get(key);
        if (!state) return 'any';
        if (state.account > 0 && state.guest === 0) return 'account';
        if (state.guest > 0 && state.account === 0) return 'guest';
        return 'any';
      };
      const buildFromSnapshot = (snapshot) => {
        if (!Array.isArray(snapshot) || !snapshot.length) return null;
        const playerToGroup = new Map();
        const loosePlayerToGroup = new Map();
        const groupsByColor = new Map();
        snapshot.forEach((g, idx) => {
          const gName = String((g && g.name) || '').trim();
          const gColor = String((g && g.color) || '').trim();
          const gToken = `g:${idx}`;
          const gColorKey = normalizeColorLoose(gColor);
          if (gColorKey && !groupsByColor.has(gColorKey)) groupsByColor.set(gColorKey, { name: gName, color: gColor, token: gToken });
          const players = Array.isArray(g && g.players) ? g.players : [];
          players.forEach((p) => {
            const pName = typeof p === 'string' ? p : String((p && p.name) || '');
            const pType = getLookupTypeSuffix(getGroupPlayerMemberType(p));
            const norm = normalizeWinnerLookupName(pName);
            if (!norm || !gName) return;
            const typedKey = `${norm}|${pType}`;
            playerToGroup.set(typedKey, { name: gName, color: gColor, token: gToken });
            const loose = normalizeWinnerLoose(pName);
            const typedLoose = loose ? `${loose}|${pType}` : '';
            if (typedLoose && !loosePlayerToGroup.has(typedLoose)) loosePlayerToGroup.set(typedLoose, { name: gName, color: gColor, token: gToken });
          });
        });
        const grouped = new Map();
        const matchedIndexes = new Set();
        let matched = 0;
        sourceEntries.forEach((row, rowIdx) => {
          const directNorm = normalizeWinnerLookupName(row.name);
          const rowType = resolveRowMemberType(row.name);
          const looseNorm = normalizeWinnerLoose(row.name);
          let grp = null;
          const typedTryOrder = rowType === 'any' ? ['account', 'guest'] : [rowType];
          for (const t of typedTryOrder) {
            const typedDirect = `${directNorm}|${t}`;
            grp = playerToGroup.get(typedDirect);
            if (grp) break;
            const typedLoose = `${looseNorm}|${t}`;
            grp = loosePlayerToGroup.get(typedLoose);
            if (grp) break;
          }
          if (!grp) {
            const mappedColor = normalizeColorLoose(getDisplayColorForName(row.name, { memberType: rowType }) || '');
            if (mappedColor) grp = groupsByColor.get(mappedColor) || null;
          }
          if (!grp) return;
          matched += 1;
          matchedIndexes.add(rowIdx);
          const key = String(grp.token || normalizeName(grp.name) || `${grp.name}|${grp.color || ''}`);
          const prev = grouped.get(key) || {
            name: grp.name,
            score: 0,
            color: grp.color || '',
            contributors: [],
            _contribIndex: new Map(),
            isUngroupedPlayer: false,
          };
          const add = Number(row.score) || 0;
          prev.score += add;
          if (!prev.color && grp.color) prev.color = grp.color;
          const contribName = String(row.name || '').trim();
          const contribType = resolveRowMemberType(contribName);
          const contribKey = `${normalizeWinnerLookupName(contribName) || normalizeName(contribName) || contribName.toLowerCase()}|${contribType}`;
          const existingContribIdx = prev._contribIndex.get(contribKey);
          if (existingContribIdx === undefined) {
            prev.contributors.push({ name: contribName, score: add });
            prev._contribIndex.set(contribKey, prev.contributors.length - 1);
          } else {
            prev.contributors[existingContribIdx].score += add;
          }
          grouped.set(key, prev);
        });
        if (!matched || !grouped.size) return null;
        const ungroupedRows = sourceEntries
          .map((row, rowIdx) => ({ row, rowIdx }))
          .filter(({ row, rowIdx }) => {
            if (!row || matchedIndexes.has(rowIdx)) return false;
            if (isLikelyTeamWinnerName(String(row.name || ''))) return false;
            return true;
          })
          .map(({ row }) => ({
            name: String((row && row.name) || '').trim(),
            score: Number((row && row.score) || 0),
            color: '',
            contributors: [],
            isUngroupedPlayer: true,
          }))
          .filter((row) => !!row.name);
        return {
          matched,
          rows: Array.from(grouped.values())
            .map((row) => {
              const contributors = Array.isArray(row.contributors)
                ? row.contributors
                    .slice()
                    .sort((a, b) => (Number(b.score) || 0) - (Number(a.score) || 0) || String(a.name || '').localeCompare(String(b.name || '')))
                : [];
              return {
                name: row.name,
                score: row.score,
                color: row.color || '',
                contributors,
                isUngroupedPlayer: false,
              };
            })
            .sort((a, b) => (b.score - a.score) || a.name.localeCompare(b.name))
            .concat(ungroupedRows),
        };
      };

      const candidateResults = [];
      const sharedResult = buildFromSnapshot(shared);
      if (sharedResult) candidateResults.push({ source: 'shared', ...sharedResult });
      if (hostLocalView) {
        const localResult = buildFromSnapshot(local);
        if (localResult) candidateResults.push({ source: 'local', ...localResult });
      }
      if (!candidateResults.length) return null;
      candidateResults.sort((a, b) => {
        if (b.matched !== a.matched) return b.matched - a.matched;
        if (a.source !== b.source) return a.source === 'shared' ? -1 : 1;
        return 0;
      });
      return candidateResults[0].rows;
    }

    function applyWinnerNameColors() {
      const left = document.getElementById('ingamewinner_scores_left');
      if (!left) return;
      const right = document.getElementById('ingamewinner_scores_right');

      const winnerBoardOn = !!recolorDisplaySettings.winnerBoard;
      const hasGroups = hasDisplayGroupsForNames();

      let parsed = parseWinnerEntriesFromDom(left, right);
      const hasPlayerLikeRows = parsed.some((e) => !isLikelyTeamWinnerName(e.name));
      const sharedSyncActive = !!window.tbcRoomGroupsSyncActive;
      const winnerBoardColorActive = winnerBoardOn || sharedSyncActive;
      const shouldGroup = sharedSyncActive && (isTeamsOffForWinnerBoard() || hasPlayerLikeRows);
      const splitLineCountBefore = left.querySelectorAll(':scope > .tbc_winner_line').length;
      if (
        !shouldGroup &&
        winnerBoardColorActive &&
        parsed.length &&
        (left.dataset.tbcSplit !== '1' || splitLineCountBefore === 0)
      ) {
        renderWinnerEntries(left, right, parsed, (nm) => getWinnerBoardColorForName(nm));
        parsed = parseWinnerEntriesFromDom(left, right);
      }
      const hasNativePlayerRows = parsed.some((e) => isLikelyPlayerWinnerName(e.name));
      const hasNonTeamRows = parsed.some((e) => !isLikelyTeamWinnerName(e.name));
      if (parsed.length && ((sharedSyncActive && hasNativePlayerRows) || (!sharedSyncActive && hasNonTeamRows))) {
        winnerSourceEntries = parsed.map((e) => ({ name: e.name, score: e.score }));
      }

      if (shouldGroup) {
        const source = winnerSourceEntries.length ? winnerSourceEntries : parsed;
        const grouped = buildGroupedWinnerEntries(source);
        if (grouped && grouped.length) {
          const groupedColorFn = (groupName, entry) => {
            if (entry && entry.isUngroupedPlayer) return '';
            if (entry && entry.color) return entry.color;
            return getDisplayColorForName(groupName) || '';
          };
          renderWinnerEntries(left, right, grouped, winnerBoardColorActive ? groupedColorFn : null);
          lastGroupedWinnerWinSig = '';
          if (winnerBoardColorActive) applyWinnerHeadlineColor(groupedColorFn);
          else clearWinnerHeadlineColor();
          left.dataset.tbcWinnerGrouped = '1';
          winnerGroupedMode = true;
          return;
        }
      }

      if (winnerGroupedMode) {
        const source = winnerSourceEntries.length ? winnerSourceEntries : parsed;
        if (source.length) {
          const colorFn = winnerBoardColorActive ? (nm) => getWinnerBoardColorForName(nm) : null;
          renderWinnerEntries(left, right, source, colorFn);
        }
        delete left.dataset.tbcWinnerGrouped;
        winnerGroupedMode = false;
      }
      lastGroupedWinnerWinSig = '';

      if (!winnerBoardColorActive) {
        if (left.dataset.tbcSplit === '1') {
          left.querySelectorAll(':scope > .tbc_winner_line').forEach((lineEl) => {
            setElementNameColor(lineEl, '');
          });
        }
        clearWinnerHeadlineColor();
        return;
      }

      if (left.dataset.tbcSplit === '1') {
        const existingLines = left.querySelectorAll(':scope > .tbc_winner_line');
        if (existingLines.length) {
          const lineColorByName = new Map();
          existingLines.forEach((lineEl) => {
            const nm = lineEl.dataset.playerName || '';
            const color = getWinnerBoardColorForName(nm);
            setElementNameColor(lineEl, color || '');
            const key = normalizeWinnerLookupName(nm);
            if (key && color) lineColorByName.set(key, color);
          });
          applyWinnerHeadlineColor((winnerName) => {
            const key = normalizeWinnerLookupName(winnerName);
            if (key && lineColorByName.has(key)) return lineColorByName.get(key) || null;
            return getWinnerBoardColorForName(winnerName);
          });
          return;
        }
      }
      if (parsed.length) {
        renderWinnerEntries(left, right, parsed, (nm) => getWinnerBoardColorForName(nm));
        const rebuiltLines = left.querySelectorAll(':scope > .tbc_winner_line');
        if (rebuiltLines.length) {
          const lineColorByName = new Map();
          rebuiltLines.forEach((lineEl) => {
            const nm = lineEl.dataset.playerName || '';
            const color = getWinnerBoardColorForName(nm);
            setElementNameColor(lineEl, color || '');
            const key = normalizeWinnerLookupName(nm);
            if (key && color) lineColorByName.set(key, color);
          });
          applyWinnerHeadlineColor((winnerName) => {
            const key = normalizeWinnerLookupName(winnerName);
            if (key && lineColorByName.has(key)) return lineColorByName.get(key) || null;
            return getWinnerBoardColorForName(winnerName);
          });
          return;
        }
      }
      applyWinnerHeadlineColor((winnerName) => {
        return getWinnerBoardColorForName(winnerName);
      });
    }

    function setupWinnerNameColorObservers() {
    const idsToWatch = [
        'ingamewinner_scores_left',
        'ingamewinner',
        'ingamewinner_container',
    ];

    idsToWatch.forEach((id) => {
        waitForElement(id, (el) => {
        const obs = new MutationObserver(() => scheduleWinnerScan());
        obs.observe(el, { childList: true, subtree: true, characterData: true });
        scheduleWinnerScan();
        });
    });

    window.addEventListener('recolorGroupsChanged', () => scheduleWinnerScan());
    window.addEventListener('tbcSharedGroupsChanged', () => {
      lastSharedColorSig = '';
      sharedColorCache.clear();
      scheduleWinnerScan();
    });
    window.addEventListener('tbcSharedSyncStateChanged', () => scheduleWinnerScan());

    scheduleWinnerScan();
    }

    setupWinnerNameColorObservers();


    function setupLobbyNameColorObservers() {
      installLobbyPlayerMenuOwnerTracking();
      const playerListIdsToWatch = [
        'newbonklobby_playerbox_elementcontainer',
        'newbonklobby_playerbox_leftelementcontainer',
        'newbonklobby_playerbox_rightelementcontainer',
        'newbonklobby_specbox_elementcontainer',
      ];
      const chatIdsToWatch = [
        'newbonklobby_chat_content',
        'ingamechatcontent',
      ];

      playerListIdsToWatch.forEach((id) => {
        waitForElement(id, (el) => {
          const obs = new MutationObserver((mutations) => {
            const touched = new Set();
            for (const m of mutations) {
              const collectFrom = (node) => {
                if (!(node instanceof Element)) return;
                if (node.classList && node.classList.contains('newbonklobby_playerentry')) {
                  touched.add(node);
                  return;
                }
                const row = node.closest ? node.closest('.newbonklobby_playerentry') : null;
                if (row) touched.add(row);
                if (node.querySelectorAll) {
                  node.querySelectorAll('.newbonklobby_playerentry').forEach((r) => touched.add(r));
                }
              };

              if (m.type === 'childList') {
                m.addedNodes.forEach((n) => collectFrom(n));
                if (m.target instanceof Element) {
                  const row = m.target.closest ? m.target.closest('.newbonklobby_playerentry') : null;
                  if (row) touched.add(row);
                }
              } else if (m.type === 'characterData') {
                const p = m.target && m.target.parentElement ? m.target.parentElement : null;
                collectFrom(p);
              }
            }

            touched.forEach((row) => applyLobbyPlayerEntryColor(row, false, true, false));

            const menuColor = getPlayerListMenuGroupColor();
            const playerMenus = document.querySelectorAll('.newbonklobby_playerentry_menu, .newbonklobby_playerentry_menu_submenu');
            playerMenus.forEach((menuEl) => setPlayerListMenuBackground(menuEl, menuColor));
          });
          obs.observe(el, { childList: true, subtree: true, characterData: true });
          const rows = el.querySelectorAll('.newbonklobby_playerentry');
          rows.forEach((row) => applyLobbyPlayerEntryColor(row, false, true, false));
        });
      });

      chatIdsToWatch.forEach((id) => {
        waitForElement(id, (el) => {
          const obs = new MutationObserver(() => scheduleLobbyScan());
          obs.observe(el, { childList: true, subtree: true, characterData: true });
          scheduleLobbyScan();
        });
      });

      waitForElement('newbonklobby', (el) => {
        const obs = new MutationObserver(() => scheduleLobbyScan());
        obs.observe(el, { attributes: true, attributeFilter: ['style', 'class'] });
      });

      const applyPlayerColorGroupUpdate = (players, clearSharedCache = false) => {
        const changed = invalidateLobbyPersistentColorsForPlayers(players, clearSharedCache);
        if (changed.length) refreshLobbyPlayerEntriesForNames(changed, true);
        scheduleLobbyScan();
      };

      window.addEventListener('recolorGroupsChanged', () => {
        scheduleLobbyScan();
      });
      window.addEventListener('tbcGroupMembershipChanged', (e) => {
        const players = e && e.detail && Array.isArray(e.detail.players) ? e.detail.players : [];
        applyPlayerColorGroupUpdate(players, false);
      });
      window.addEventListener('tbcGroupColorChanged', (e) => {
        const players = e && e.detail && Array.isArray(e.detail.players) ? e.detail.players : [];
        applyPlayerColorGroupUpdate(players, false);
      });
      window.addEventListener('tbcGroupDeleted', (e) => {
        const players = e && e.detail && Array.isArray(e.detail.players) ? e.detail.players : [];
        applyPlayerColorGroupUpdate(players, false);
      });
      window.addEventListener('tbcSharedGroupsChanged', (e) => {
        const players = e && e.detail && Array.isArray(e.detail.changedPlayers) ? e.detail.changedPlayers : [];
        if (!players.length || !!window.tbcRoomGroupsSyncActive) {
          refreshLobbyPersistentColorsFromGroups();
          applyLobbyNameColors(true);
          scheduleLobbyScan();
          return;
        }
        applyPlayerColorGroupUpdate(players, true);
      });
      window.addEventListener('tbcSharedSyncStateChanged', () => {
        refreshLobbyPersistentColorsFromGroups();
        applyLobbyNameColors(true);
        scheduleLobbyScan();
      });

      scheduleLobbyScan();
    }

    setupLobbyNameColorObservers();

    function extractChatName(rawText) {
      let s = String(rawText || '');
      s = s.replace(/[\s\u00A0\u2000-\u200B\u202F\u205F\u3000]+$/g, '');
      s = s.replace(/[:\uFF1A]+$/g, '');

      return s.trim();
    }

    function normalizeName(name) {
      return (name || '').replace(/\s+/g, ' ').trim().toLowerCase();
    }

    function namesEquivalent(a, b) {
      const aNorm = normalizeName(a);
      const bNorm = normalizeName(b);
      if (aNorm && bNorm && aNorm === bNorm) return true;
      const aCanon = normalizeWinnerLookupName(a);
      const bCanon = normalizeWinnerLookupName(b);
      return !!(aCanon && bCanon && aCanon === bCanon);
    }

    function normalizeMemberType(type) {
      const t = String(type || '').trim().toLowerCase();
      if (t === 'guest') return 'guest';
      if (t === 'account' || t === 'level' || t === 'player') return 'account';
      return 'any';
    }

    function getGroupPlayerMemberType(player) {
      if (player && typeof player === 'object') {
        const explicit = normalizeMemberType(player.memberType);
        if (explicit !== 'any') return explicit;
        if (player.tempGuest === true) return 'guest';
      }
      return 'account';
    }

    function memberIdentityMatches(player, playerName, memberType = 'any') {
      const targetType = normalizeMemberType(memberType);
      const pName = typeof player === 'string' ? player : String((player && player.name) || '');
      if (!namesEquivalent(pName, playerName)) return false;
      if (targetType === 'any') return true;
      return getGroupPlayerMemberType(player) === targetType;
    }

    function getLookupTypeSuffix(memberType) {
      const t = normalizeMemberType(memberType);
      return t === 'any' ? 'any' : t;
    }

    function clampInt(n, min, max) {
      const x = Number.isFinite(n) ? Math.trunc(n) : parseInt(String(n), 10);
      if (!Number.isFinite(x)) return min;
      return Math.max(min, Math.min(max, x));
    }

    function getPrettyTopAccountIdentity() {
      const nameEl = $('pretty_top_name');
      const lvlEl = $('pretty_top_level');
      const nameRaw = String(nameEl ? nameEl.textContent : '').trim();
      const lvlRaw = String(lvlEl ? lvlEl.textContent : '').trim();
      const nameNorm = normalizeName(nameRaw);
      const lvlNorm = lvlRaw.toLowerCase();
      if (!nameNorm || nameNorm === 'guest') return null;
      if (!lvlNorm || /\bguest\b/.test(lvlNorm)) return null;
      const looksLoggedIn = /^level\b/.test(lvlNorm) || /^lv\b/.test(lvlNorm) || /\d/.test(lvlNorm);
      if (!looksLoggedIn) return null;
      return { name: nameNorm, level: lvlNorm };
    }

    function isLoggedInAccount() {
      return !!getPrettyTopAccountIdentity();
    }

    function getAccountNameFromPrettyTopOrNull() {
      const ident = getPrettyTopAccountIdentity();
      return ident ? ident.name : null;
    }

    function getStorageKeyV2() {
      const acct = getAccountNameFromPrettyTopOrNull();
      if (!acct) return null;
      return STORAGE_KEY_PREFIX_V2 + acct;
    }

    function getDisplaySettingsStorageKeyV1() {
      const acct = getAccountNameFromPrettyTopOrNull();
      if (!acct) return null;
      return DISPLAY_SETTINGS_KEY_PREFIX_V1 + acct;
    }

    function guessOldV1StorageKeys() {
      const keys = new Set();

      const prettyName = $('pretty_top_name');
      if (prettyName && prettyName.textContent.trim()) {
        keys.add(STORAGE_KEY_PREFIX_V1 + normalizeName(prettyName.textContent.trim()));
      }

      const stored = localStorage.getItem('bonk_name');
      if (stored && stored.trim()) keys.add(STORAGE_KEY_PREFIX_V1 + normalizeName(stored.trim()));

      keys.add(STORAGE_KEY_PREFIX_V1 + 'default');
      return Array.from(keys);
    }

    function migrateV1ToV2IfNeeded() {
      try {
        if (!storageKey) return;

        const v2Raw = localStorage.getItem(storageKey);
        if (v2Raw) return;

        for (const k of guessOldV1StorageKeys()) {
          const raw = localStorage.getItem(k);
          if (!raw) continue;

          try {
            const parsed = JSON.parse(raw);
            if (Array.isArray(parsed)) {
              localStorage.setItem(storageKey, raw);
              return;
            }
          } catch {}
        }
      } catch {}
    }

    function hexToRgba(hex, alpha) {
      if (!hex) return `rgba(0,0,0,${alpha})`;

      let h = String(hex).replace('#', '');
      if (h.length === 3) h = h.split('').map((c) => c + c).join('');
      if (h.length !== 6) return `rgba(0,0,0,${alpha})`;

      const r = parseInt(h.slice(0, 2), 16) || 0;
      const g = parseInt(h.slice(2, 4), 16) || 0;
      const b = parseInt(h.slice(4, 6), 16) || 0;

      return `rgba(${r},${g},${b},${alpha})`;
    }

    function migrateAndNormalizeGroups(arr) {
      if (!Array.isArray(arr)) return [];

      return arr
        .map((g) => {
          if (!g || typeof g !== 'object') return null;

          const id = String(g.id || `cg_${Date.now()}_${Math.random().toString(16).slice(2)}`);
          const name = String(g.name || 'Group');

          const color = String(g.color || getRandomPresetColor());

          let players = [];
          if (Array.isArray(g.players)) {
            if (g.players.length && typeof g.players[0] === 'string') {
              players = g.players
                .map((p) => String(p || '').trim())
                .filter(Boolean)
                .map((p) => ({ name: p, memberType: 'account' }));
            } else {
              players = g.players
                .map((p) => {
                  if (!p) return null;
                  if (typeof p === 'string') return { name: p.trim(), memberType: 'account' };
                  if (typeof p === 'object') {
                    const nm = String(p.name || '').trim();
                    if (!nm) return null;
                    const memberType = normalizeMemberType(p.memberType || (p.tempGuest ? 'guest' : 'account'));
                    const tempGuest = !!p.tempGuest || memberType === 'guest';
                    return { name: nm, memberType: memberType === 'any' ? 'account' : memberType, tempGuest };
                  }
                  return null;
                })
                .filter(Boolean);
            }
          }

          return { id, name, color, players };
        })
        .filter(Boolean);
    }

    function normalizeRecolorDisplaySettings(data) {
      const defaults = createDefaultRecolorDisplaySettings();
      if (!data || typeof data !== 'object') return defaults;
      return {
        playerNames: typeof data.playerNames === 'boolean' ? data.playerNames : defaults.playerNames,
        winnerBoard: typeof data.winnerBoard === 'boolean' ? data.winnerBoard : defaults.winnerBoard,
        ingameChatNames: typeof data.ingameChatNames === 'boolean' ? data.ingameChatNames : defaults.ingameChatNames,
        lobbyChatNames: typeof data.lobbyChatNames === 'boolean' ? data.lobbyChatNames : defaults.lobbyChatNames,
        playerListNames: typeof data.playerListNames === 'boolean' ? data.playerListNames : defaults.playerListNames,
        playerListBackboard: typeof data.playerListBackboard === 'boolean' ? data.playerListBackboard : defaults.playerListBackboard,
      };
    }

    function loadGroups() {
      try {
        if (!storageKey) {
          colorGroups = [];
          return;
        }

        const raw = localStorage.getItem(storageKey);
        if (!raw) {
          colorGroups = [];
          return;
        }

        const data = JSON.parse(raw);
        colorGroups = migrateAndNormalizeGroups(data);
      } catch (e) {
        console.error('[Re:Color] Failed to load groups:', e);
        colorGroups = [];
      }
    }

    function saveGroups() {
      try {
        if (!storageKey) {
          window.dispatchEvent(new Event('recolorGroupsChanged'));
          updateStorageHintUI();
          return;
        }

        const persistable = (Array.isArray(colorGroups) ? colorGroups : []).map((g) => {
          const players = Array.isArray(g && g.players)
            ? g.players
                .filter((p) => !(p && typeof p === 'object' && p.tempGuest === true))
                .map((p) => {
                  if (typeof p === 'string') return { name: String(p || '').trim(), memberType: 'account' };
                  const name = String((p && p.name) || '').trim();
                  return { name, memberType: 'account' };
                })
                .filter((p) => p.name)
            : [];
          return {
            id: String((g && g.id) || `cg_${Date.now()}_${Math.random().toString(16).slice(2)}`),
            name: String((g && g.name) || 'Group'),
            color: String((g && g.color) || getRandomPresetColor()),
            players,
          };
        });
        localStorage.setItem(storageKey, JSON.stringify(persistable));
        window.dispatchEvent(new Event('recolorGroupsChanged'));
        updateStorageHintUI();
      } catch (e) {
        console.error('[Re:Color] Failed to save groups:', e);
      }
    }

    function loadRecolorDisplaySettings() {
      try {
        const key = getDisplaySettingsStorageKeyV1();
        if (!key) {
          recolorDisplaySettings = createDefaultRecolorDisplaySettings();
          return;
        }
        const raw = localStorage.getItem(key);
        if (!raw) {
          recolorDisplaySettings = createDefaultRecolorDisplaySettings();
          return;
        }
        const parsed = JSON.parse(raw);
        recolorDisplaySettings = normalizeRecolorDisplaySettings(parsed);
      } catch (e) {
        console.error('[Re:Color] Failed to load display settings:', e);
        recolorDisplaySettings = createDefaultRecolorDisplaySettings();
      }
    }

    function saveRecolorDisplaySettings() {
      try {
        const key = getDisplaySettingsStorageKeyV1();
        if (key) localStorage.setItem(key, JSON.stringify(recolorDisplaySettings));
      } catch (e) {
        console.error('[Re:Color] Failed to save display settings:', e);
      }
      window.dispatchEvent(new Event('recolorGroupsChanged'));
      applyLobbyNameColors();
      applyWinnerNameColors();
      if (typeof refreshRecolorSettingsUi === 'function') refreshRecolorSettingsUi();
    }

    function updateStorageHintUI() {
      const el = $('recolor_storage_hint');
      if (!el) return;

      if (storageKey) {
        el.style.color = '';
        el.style.opacity = '0.75';
        el.textContent = `Per-account storage: ${storageKey}`;
      } else {
        el.style.color = '#ffcc66';
        el.style.opacity = '0.9';
        el.textContent = 'Guest mode: settings are temporary until you log in.';
      }
    }

    function updateAccountStorageKey() {
      const newKey = getStorageKeyV2();
      if (newKey === lastStorageKey) return;

      lastStorageKey = newKey;
      storageKey = newKey;

      if (storageKey) migrateV1ToV2IfNeeded();
      loadGroups();
      loadRecolorDisplaySettings();
      lobbyPersistentColorEpoch += 1;
      lobbyPersistentColorByName.clear();
      lobbyPersistentColorVersionByName.clear();
      colorCache.clear();

      if ($('cg_groups_list')) renderGroupsUI();
      updateStorageHintUI();
      if (typeof refreshRecolorSettingsUi === 'function') refreshRecolorSettingsUi();
      applyLobbyNameColors();
      applyWinnerNameColors();
    }

    function ensureAccountObservers() {
      if (observersInitialized) return;
      observersInitialized = true;

      const attach = () => {
        const nameEl = $('pretty_top_name');
        const lvlEl = $('pretty_top_level');

        const obs = new MutationObserver(() => updateAccountStorageKey());
        if (nameEl) obs.observe(nameEl, { childList: true, characterData: true, subtree: true });
        if (lvlEl) obs.observe(lvlEl, { childList: true, characterData: true, subtree: true });
      };

      attach();
      updateAccountStorageKey();
    }

    function getColorForName(name, opts = null) {
      const memberType = normalizeMemberType(opts && opts.memberType);
      const key = normalizeName(name);
      const canonicalKey = normalizeWinnerLookupName(name);
      if (!key && !canonicalKey) return null;

      const cacheKey = `${canonicalKey || key}|${getLookupTypeSuffix(memberType)}`;
      if (cacheKey && colorCache.has(cacheKey)) return colorCache.get(cacheKey);

      let found = null;
      if (memberType === 'any') {
        for (const passType of ['account', 'guest']) {
          for (const g of colorGroups) {
            if ((g.players || []).some((p) => memberIdentityMatches(p, name, passType))) {
              found = g.color;
              break;
            }
          }
          if (found) break;
        }
      } else {
        for (const g of colorGroups) {
          if ((g.players || []).some((p) => memberIdentityMatches(p, name, memberType))) {
            found = g.color;
            break;
          }
        }
      }
      if (cacheKey) colorCache.set(cacheKey, found);
      return found;
    }

    function findGroupByPlayerName(playerName, memberType = 'any') {
      return colorGroups.find((g) => (g.players || []).some((p) => memberIdentityMatches(p, playerName, memberType))) || null;
    }

    function isPlayerInAnyGroup(playerName, memberType = 'any') {
      if (!normalizeName(playerName)) return false;
      return colorGroups.some((g) => (g.players || []).some((p) => memberIdentityMatches(p, playerName, memberType)));
    }

    function findPlayerInGroup(groupId, playerName, memberType = 'any') {
      const g = colorGroups.find((x) => x.id === groupId);
      if (!g) return null;
      return (g.players || []).find((p) => memberIdentityMatches(p, playerName, memberType)) || null;
    }

    function isTemporaryGuestGroupMemberPlayer(player) {
      return !!(player && typeof player === 'object' && player.tempGuest === true);
    }

    function isLobbyGuestName(name, lobbyInfo = null) {
      const nm = normalizeName(name);
      if (!nm) return false;
      const info = lobbyInfo || getLobbyAccountInfoCached(500);
      const guestSet = info && info.guestSet ? info.guestSet : new Set();
      const levelMap = info && info.levelMap ? info.levelMap : new Map();
      return guestSet.has(nm) || String(levelMap.get(nm) || '') === 'guest';
    }

    function shouldShowGuestWarningForGroupMember(player, lobbyInfo = null) {
      if (isTemporaryGuestGroupMemberPlayer(player)) return true;
      if (getGroupPlayerMemberType(player) === 'account') return false;
      const name = typeof player === 'string' ? player : String((player && player.name) || '');
      return isLobbyGuestName(name, lobbyInfo);
    }

    function purgeTemporaryGuestGroupMembers({ onlyMissingInLobby = false, lobbyInfo = null } = {}) {
      const info = lobbyInfo || getLobbyAccountInfoCached(500);
      const guestSet = info && info.guestSet ? info.guestSet : new Set();
      let removed = 0;
      let selectedCleared = false;
      colorGroups.forEach((g) => {
        const beforePlayers = Array.isArray(g && g.players) ? g.players : [];
        const nextPlayers = beforePlayers.filter((p) => {
          if (!isTemporaryGuestGroupMemberPlayer(p)) return true;
          if (onlyMissingInLobby) {
            const nm = normalizeName(p && p.name);
            if (nm && guestSet.has(nm)) return true;
          }
          removed += 1;
          if (selectedPlayer && memberIdentityMatches(selectedPlayer, p && p.name, 'guest')) selectedCleared = true;
          return false;
        });
        g.players = nextPlayers;
      });
      if (!removed) return 0;
      if (selectedCleared) selectedPlayer = null;
      saveGroups();
      renderGroupsUI();
      renderSelectedPlayerPanel();
      if (groupsPanelVisible) renderGroupsPanel();
      applyLobbyNameColors(true);
      return removed;
    }

    function addGroup(name = 'New Group', color = null) {
      const id = `cg_${Date.now()}_${Math.random().toString(16).slice(2)}`;

      const pickedColor = color ? String(color) : getRandomPresetColor();

      colorGroups.push({ id, name, color: pickedColor, players: [] });
      saveGroups();
      renderGroupsUI();
    }

    function deleteGroup(id) {
      const idx = colorGroups.findIndex((g) => g.id === id);
      if (idx === -1) return;
      const removedPlayers = (colorGroups[idx].players || []).map((p) => String((p && p.name) || '').trim()).filter(Boolean);
      colorGroups.splice(idx, 1);
      saveGroups();
      if (removedPlayers.length) {
        window.dispatchEvent(new CustomEvent('tbcGroupDeleted', {
          detail: { players: removedPlayers }
        }));
      }
      renderGroupsUI();
      renderSelectedPlayerPanel();
    }

    function renameGroup(id, newName) {
      const g = colorGroups.find((x) => x.id === id);
      if (!g) return;
      g.name = newName || g.name;
      saveGroups();
      renderGroupsUI();
      renderSelectedPlayerPanel();
    }

    function setGroupColor(id, newColor) {
      const g = colorGroups.find((x) => x.id === id);
      if (!g) return;
      g.color = String(newColor || g.color || '').trim();
      saveGroups();
      window.dispatchEvent(new CustomEvent('tbcGroupColorChanged', {
        detail: { players: (g.players || []).map((p) => String((p && p.name) || '').trim()).filter(Boolean) }
      }));
    }

    function addPlayerToGroup(groupId, playerName, opts = {}) {
      const name = (playerName || '').trim();
      if (!name) return { ok: false, error: 'Name cannot be empty.' };
      const requestedType = normalizeMemberType(opts.memberType);

      const existingGroup = findGroupByPlayerName(name, requestedType === 'any' ? 'account' : requestedType);
      if (existingGroup) {
        return { ok: false, error: `Player "${name}" is already in group "${existingGroup.name}".` };
      }

      const g = colorGroups.find((x) => x.id === groupId);
      if (!g) return { ok: false, error: 'Group not found.' };

      const info = getLobbyAccountInfoCached(500);
      const guestSet = info && info.guestSet ? info.guestSet : new Set();
      const isGuestNow = guestSet.has(normalizeName(name));
      const resolvedType = requestedType !== 'any'
        ? requestedType
        : ((opts && opts.allowGuest && isGuestNow) ? 'guest' : 'account');
      if (resolvedType === 'guest' && !opts.allowGuest) {
        return { ok: false, error: 'Guests can only be added from the player list.' };
      }
      g.players.push({ name, memberType: resolvedType, tempGuest: resolvedType === 'guest' });
      saveGroups();
      window.dispatchEvent(new CustomEvent('tbcGroupMembershipChanged', {
        detail: { players: [name], action: 'add' }
      }));
      renderGroupsUI();
      renderSelectedPlayerPanel();
      return { ok: true };
    }

    function removePlayerFromGroup(groupId, playerName, memberType = 'any') {
      const g = colorGroups.find((x) => x.id === groupId);
      if (!g) return;

      const beforeCount = (g.players || []).length;
      g.players = (g.players || []).filter((p) => !memberIdentityMatches(p, playerName, memberType));

      if (selectedPlayer && selectedPlayer.groupId === groupId && memberIdentityMatches(selectedPlayer, playerName, memberType)) {
        selectedPlayer = null;
      }

      saveGroups();
      if ((g.players || []).length !== beforeCount) {
        window.dispatchEvent(new CustomEvent('tbcGroupMembershipChanged', {
          detail: { players: [playerName], action: 'remove' }
        }));
      }
      renderGroupsUI();
      renderSelectedPlayerPanel();
    }

    function removePlayerFromAllGroups(playerName, memberType = 'any') {
      if (!normalizeName(playerName)) return { ok: false, removed: 0 };

      let removed = 0;
      colorGroups.forEach((g) => {
        const before = (g.players || []).length;
        g.players = (g.players || []).filter((p) => !memberIdentityMatches(p, playerName, memberType));
        removed += Math.max(0, before - (g.players || []).length);
      });
      if (!removed) return { ok: false, removed: 0 };

      if (selectedPlayer && memberIdentityMatches(selectedPlayer, playerName, memberType)) {
        selectedPlayer = null;
      }

      saveGroups();
      window.dispatchEvent(new CustomEvent('tbcGroupMembershipChanged', {
        detail: { players: [playerName], action: 'remove' }
      }));
      renderGroupsUI();
      renderSelectedPlayerPanel();
      return { ok: true, removed };
    }

    function movePlayerToGroup(fromGroupId, toGroupId, playerName, memberType = 'any') {
      if (fromGroupId === toGroupId) return { ok: false, error: 'Already in that group.' };

      const from = colorGroups.find((x) => x.id === fromGroupId);
      const to = colorGroups.find((x) => x.id === toGroupId);
      if (!from || !to) return { ok: false, error: 'Group not found.' };

      const existing = (to.players || []).some((p) => memberIdentityMatches(p, playerName, memberType));
      if (existing) return { ok: false, error: `Player "${playerName}" is already in that group.` };

      const p = (from.players || []).find((pp) => memberIdentityMatches(pp, playerName, memberType));
      if (!p) return { ok: false, error: 'Player not found.' };

      from.players = (from.players || []).filter((pp) => !memberIdentityMatches(pp, playerName, memberType));
      to.players.push({ name: p.name, memberType: getGroupPlayerMemberType(p), tempGuest: !!p.tempGuest });

      if (selectedPlayer && memberIdentityMatches(selectedPlayer, playerName, memberType)) selectedPlayer.groupId = toGroupId;

      saveGroups();
      window.dispatchEvent(new CustomEvent('tbcGroupMembershipChanged', {
        detail: { players: [p.name], action: 'move' }
      }));
      renderGroupsUI();
      renderSelectedPlayerPanel();
      return { ok: true };
    }

    function reorderGroupsToIndex(groupId, targetIndex) {
      const oldIndex = colorGroups.findIndex((g) => g.id === groupId);
      if (oldIndex === -1) return;

      if (targetIndex < 0) targetIndex = 0;
      if (targetIndex > colorGroups.length - 1) targetIndex = colorGroups.length - 1;
      if (oldIndex === targetIndex) return;

      const [moved] = colorGroups.splice(oldIndex, 1);
      colorGroups.splice(targetIndex, 0, moved);

      saveGroups();
      renderGroupsUI();
      renderSelectedPlayerPanel();
    }

    function closePanel() {
      if (activePanel && activePanel.parentNode) activePanel.parentNode.removeChild(activePanel);
      activePanel = null;
    }

    function openPanel(anchorEl, buildContent) {
      closePanel();

      const panel = document.createElement('div');
      panel.className = 'mod_ctx_panel';
      panel.addEventListener('click', (e) => e.stopPropagation());

      buildContent(panel);

      document.body.appendChild(panel);
      activePanel = panel;

      const rect = anchorEl.getBoundingClientRect();
      const panelRect = panel.getBoundingClientRect();

      let left = rect.left;
      let top = rect.bottom + 4;

      if (left + panelRect.width > window.innerWidth) left = window.innerWidth - panelRect.width - 8;
      if (top + panelRect.height > window.innerHeight) top = rect.top - panelRect.height - 4;

      panel.style.left = `${left}px`;
      panel.style.top = `${top}px`;
    }

    document.addEventListener('click', () => closePanel());

    function ensureRecolorStyles() {
      if ($('recolor_css')) return;

      const style = document.createElement('style');
      style.id = 'recolor_css';
      style.textContent = `
        #cg_groups_outer {
          margin-top: 8px;
          overflow-x: auto;
          overflow-y: hidden;
          padding-bottom: 4px;
          box-sizing: border-box;
        }

        #cg_groups_list {
          display: flex;
          flex-direction: row;
          gap: 10px;
          min-height: 180px;
        }

        .cg_group {
          position: relative;
          border: 1px solid rgba(0,0,0,0.4);
          border-radius: 6px;
          padding: 8px;
          background: rgba(0,0,0,0.15);
          box-shadow: 0 2px 4px rgba(0,0,0,0.25);
          display: flex;
          flex-direction: column;
          cursor: default;
          flex: 0 0 33%;
          max-width: 33%;
          box-sizing: border-box;
          height: 220px;
        }

        .cg_group_header {
          display: flex;
          align-items: center;
          margin-bottom: 6px;
          padding: 2px 4px;
          border-radius: 4px;
          background: rgba(0,0,0,0.2);
        }

        .cg_group_handle {
          width: 16px;
          height: 16px;
          margin-right: 6px;
          display: flex;
          align-items: center;
          justify-content: center;
          cursor: grab;
          opacity: 0.8;
          font-size: 10px;
          user-select: none;
        }

        .cg_group_title {
          flex: 1;
          font-weight: bold;
          font-size: 13px;
          text-align: left;
          overflow:hidden;
          text-overflow:ellipsis;
          white-space:nowrap;
        }

        .cg_group_menu {
          cursor: pointer;
          opacity: 0.8;
          padding: 2px 6px;
          border-radius: 4px;
        }
        .cg_group_menu:hover { background: rgba(0,0,0,0.2); }

        .cg_group_meta {
          display: flex;
          flex-direction: column;
          gap: 4px;
          font-size: 11px;
          opacity: .9;
          margin: 2px 0 6px 0;
          padding: 6px 6px;
          border-radius: 6px;
          background: rgba(0,0,0,0.12);
          border: 1px solid rgba(255,255,255,0.08);
        }

        .cg_group_meta_line {
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 8px;
          width: 100%;
        }

        .cg_group_meta_line_right {
          justify-content: flex-end;
          font-family: monospace;
          opacity: .85;
        }

        .cg_group_players {
          margin: 0 0 4px 0;
          max-height: 110px;
          overflow-y: auto;
          padding-right: 4px;
          flex: 1 1 auto;
        }

        .cg_player_row {
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 8px;
          padding: 3px 6px;
          border-radius: 6px;
          background: rgba(0,0,0,0.15);
          margin-bottom: 3px;
          font-size: 12px;
          cursor: pointer;
        }
        .cg_player_row:nth-child(even) { background: rgba(0,0,0,0.28); }
        .cg_player_row:last-child { margin-bottom: 0; }
        .cg_player_row.selected { outline: 1px solid rgba(121,85,248,0.6); background: rgba(121,85,248,0.12); }

        .cg_player_name {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          flex: 1 1 auto;
          display: inline-flex;
          align-items: center;
          gap: 4px;
        }

        .tbc_guest_warn_badge {
          display: inline-flex;
          align-items: center;
          justify-content: center;
          width: 13px;
          height: 13px;
          border-radius: 999px;
          background: #f1d34f;
          color: #3a2f08;
          border: 1px solid rgba(58, 47, 8, 0.35);
          font-size: 10px;
          font-weight: 800;
          line-height: 1;
          flex: 0 0 auto;
          user-select: none;
        }
        .tbc_guest_warn_badge::before {
          content: "i";
          transform: translateY(-0.25px);
        }
        .tbc_guest_warn_badge_playerlist {
          margin-left: 4px;
          vertical-align: middle;
        }

        .cg_player_right {
          display:flex;
          align-items:center;
          gap: 6px;
          flex: 0 0 auto;
        }

        .cg_player_menu {
          cursor: pointer;
          opacity: 0;
          transition: opacity 0.15s;
          padding: 1px 4px;
          border-radius: 4px;
        }
        .cg_player_row:hover .cg_player_menu { opacity: 1; }
        .cg_player_menu:hover { background: rgba(0,0,0,0.22); }

        .cg_color_bar {
        display:flex;
        align-items:center;
        justify-content: space-between;
        gap: 8px;
        margin-top: auto;
        padding: 6px 6px;
        border-radius: 8px;
        background: rgba(0,0,0,0.18);
        border: 1px solid rgba(255,255,255,0.10);
        }

        .cg_color_btn {
        display:inline-flex;
        align-items:center;
        gap: 8px;
        cursor: pointer;
        user-select: none;
        flex: 1 1 auto;
        min-width: 0;
        }

        .cg_color_preview {
        width: 18px;
        height: 18px;
        border-radius: 6px;
        border: 1px solid rgba(255,255,255,0.25);
        box-shadow: inset 0 0 0 1px rgba(0,0,0,0.25);
        flex: 0 0 auto;
        }

        .cg_color_hex {
        font-family: monospace;
        font-size: 11px;
        opacity: .88;
        padding: 2px 6px;
        border-radius: 999px;
        border: 1px solid rgba(255,255,255,0.12);
        background: rgba(0,0,0,0.18);
        white-space: nowrap;
        overflow:hidden;
        text-overflow: ellipsis;
        }

        .cg_color_edithint {
        font-size: 10px;
        opacity: .7;
        flex: 0 0 auto;
        }

        .cg_color_panel {
        width: 260px;
        }

        .cg_color_panel_top {
        display:flex;
        align-items:center;
        justify-content: space-between;
        gap: 10px;
        margin-bottom: 8px;
        }

        .cg_color_panel_preview {
        display:flex;
        align-items:center;
        gap: 8px;
        min-width: 0;
        }

        .cg_color_panel_previewbox {
        width: 22px;
        height: 22px;
        border-radius: 7px;
        border: 1px solid rgba(255,255,255,0.25);
        box-shadow: inset 0 0 0 1px rgba(0,0,0,0.25);
        flex: 0 0 auto;
        }

        .cg_color_panel_hex {
        font-family: monospace;
        font-size: 11px;
        opacity: .9;
        padding: 2px 8px;
        border-radius: 999px;
        border: 1px solid rgba(255,255,255,0.12);
        background: rgba(0,0,0,0.18);
        white-space: nowrap;
        overflow:hidden;
        text-overflow: ellipsis;
        }

        .cg_color_panel_body {
        display:grid;
        grid-template-columns: 1fr 1fr;
        gap: 10px;
        }

        .cg_color_panel_section {
        border: 1px solid rgba(255,255,255,0.10);
        background: rgba(0,0,0,0.16);
        border-radius: 8px;
        padding: 8px;
        }

        .cg_color_panel_section_title {
        font-size: 11px;
        font-weight: 800;
        opacity: .9;
        margin-bottom: 6px;
        }

        .cg_color_swatches {
        display:flex;
        flex-wrap: wrap;
        gap: 6px;
        }

        .cg_swatch {
        width: 18px;
        height: 18px;
        border-radius: 6px;
        border: 1px solid rgba(255,255,255,0.25);
        box-shadow: inset 0 0 0 1px rgba(0,0,0,0.25);
        cursor: pointer;
        flex: 0 0 auto;
        }

        .cg_swatch:hover { transform: translateY(-1px); }

        .cg_swatch.active {
        outline: 2px solid rgba(255,255,255,0.35);
        box-shadow: 0 0 0 2px rgba(121,85,248,0.35);
        }

        .cg_color_custom_row {
        display:flex;
        align-items:center;
        justify-content: space-between;
        gap: 8px;
        }

        .cg_color_custom_row input[type="color"]{
        border: none;
        padding: 0;
        width: 34px;
        height: 26px;
        cursor: pointer;
        background: transparent;
        }

        .cg_color_usebtn {
        padding: 4px 8px;
        border-radius: 8px;
        border: 1px solid rgba(255,255,255,0.18);
        background: rgba(255,255,255,0.06);
        cursor:pointer;
        font-size: 11px;
        white-space: nowrap;
        }
        .cg_color_usebtn:hover { background: rgba(255,255,255,0.12); }

        .cg_group.cg_group_add {
          border: 1px dashed rgba(255,255,255,0.5);
          background: transparent;
          align-items: center;
          justify-content: center;
          cursor: pointer;
        }

        .cg_group_add_inner { text-align: center; opacity: 0.9; }
        .cg_group_add_plus { font-size: 24px; line-height: 1; margin-bottom: 4px; }

        .cg_group_placeholder {
          flex: 0 0 33%;
          max-width: 33%;
          border: 2px dashed rgba(255,255,255,0.4);
          border-radius: 6px;
          background: rgba(255,255,255,0.04);
          height: 220px;
        }

        .cg_group_dragging {
          animation: cg_rock 0.25s ease-in-out infinite alternate;
          transform-origin: center center;
          box-shadow: 0 8px 22px rgba(0,0,0,0.7);
          cursor: grabbing !important;
        }

        @keyframes cg_rock {
          0% { transform: rotate(-1.5deg) translateY(-3px); }
          100% { transform: rotate(1.5deg) translateY(-3px); }
        }

        .mod_ctx_panel {
          position: fixed;
          background: rgba(25,25,25,0.96);
          border-radius: 6px;
          padding: 8px;
          box-shadow: 0 6px 18px rgba(0,0,0,0.6);
          z-index: 99999;
          min-width: 180px;
          font-size: 12px;
          color: #fff;
        }

        .mod_ctx_title { font-weight: bold; margin-bottom: 6px; }

        .mod_ctx_items {
          display: flex;
          flex-direction: column;
          gap: 4px;
          margin-top: 4px;
        }

        .mod_ctx_item {
          padding: 4px 6px;
          border-radius: 4px;
          cursor: pointer;
          white-space: nowrap;
        }
        .mod_ctx_item:hover { background: rgba(255,255,255,0.08); }

        .mod_ctx_input {
          width: 100%;
          box-sizing: border-box;
          border-radius: 6px;
          border: 1px solid rgba(255,255,255,0.15);
          background: rgba(0,0,0,0.2);
          color: #fff;
          padding: 6px 8px;
          margin-top: 4px;
          margin-bottom: 4px;
          outline: none;
          font-size: 12px;
        }

        .mod_ctx_buttons {
          display: flex;
          justify-content: flex-end;
          gap: 6px;
          margin-top: 6px;
        }

        .mod_ctx_button {
          padding: 3px 8px;
          border-radius: 6px;
          cursor: pointer;
          border: 1px solid rgba(255,255,255,0.2);
          background: rgba(255,255,255,0.05);
          font-size: 11px;
        }
        .mod_ctx_button:hover { background: rgba(255,255,255,0.12); }

        .mod_ctx_button_primary {
          border-color: rgba(121,85,248,0.8);
          background: rgba(121,85,248,0.5);
        }

        .mod_ctx_error {
          color: #ff6b6b;
          font-size: 11px;
          margin-top: 2px;
        }

        .cg_actions_row {
          margin-top: 8px;
          display:flex;
          align-items:center;
          justify-content:space-between;
          gap: 10px;
          flex-wrap: wrap;
        }

        .cg_action_btn {
          display:inline-flex;
          align-items:center;
          gap: 6px;
          padding: 6px 10px;
          border-radius: 10px;
          border: 1px solid rgba(255,255,255,0.14);
          background: rgba(0,0,0,0.16);
          cursor: pointer;
          font-size: 11px;
          opacity: .95;
        }
        .cg_action_btn:hover { background: rgba(255,255,255,0.08); }

        .cg_selected_panel {
          margin-top: 10px;
          padding: 10px 10px 9px 10px;
          border-radius: 10px;
          border: 1px solid rgba(255,255,255,0.12);
          background: rgba(0,0,0,0.14);
        }
        .cg_selected_top {
          display:flex;
          align-items:baseline;
          justify-content:space-between;
          gap: 10px;
          margin-bottom: 6px;
        }
        .cg_selected_name { font-weight: 800; font-size: 13px; }
        .cg_selected_badge {
          font-family: monospace;
          font-size: 11px;
          opacity: .9;
          padding: 2px 8px;
          border-radius: 999px;
          border: 1px solid rgba(255,255,255,0.14);
          background: rgba(0,0,0,0.18);
          white-space:nowrap;
        }
        .cg_selected_meta { font-size: 11px; opacity: .85; margin-bottom: 8px; }
        .cg_selected_controls { display:flex; align-items:center; gap: 8px; flex-wrap: wrap; }
        .cg_selected_controls input[type="number"]{
          width: 120px;
          border-radius: 8px;
          border: 1px solid rgba(255,255,255,0.14);
          background: rgba(0,0,0,0.22);
          color: #fff;
          padding: 6px 8px;
          font-size: 12px;
          outline: none;
        }

        .cg_info {
          position: relative;
          display: inline-flex;
          align-items: center;
          justify-content: center;
          width: 16px;
          height: 16px;
          border-radius: 999px;
          border: 1px solid rgba(255,255,255,0.22);
          background: rgba(0,0,0,0.18);
          color: rgba(255,255,255,0.92);
          font-size: 11px;
          font-weight: 800;
          line-height: 1;
          cursor: help;
          user-select: none;
          flex: 0 0 auto;
        }

        .cg_info::before {
          content: "i";
          transform: translateY(-0.5px);
        }

        .cg_info_tip {
          position: absolute;
          left: 50%;
          top: calc(100% + 6px);
          transform: translateX(-50%);
          min-width: 210px;
          max-width: 260px;
          padding: 7px 8px;
          border-radius: 8px;
          background: rgba(18, 22, 28, 0.98);
          border: 1px solid rgba(255,255,255,0.14);
          box-shadow: 0 10px 24px rgba(0,0,0,0.55);
          font-size: 11px;
          font-weight: 500;
          opacity: 0;
          pointer-events: none;
          transition: opacity 0.12s ease;
          z-index: 999999;
        }

        .cg_info_tip b { font-weight: 800; }
        .cg_info_tip small { opacity: 0.8; }

        .cg_info:hover .cg_info_tip {
          opacity: 1;
        }

        .cg_recolor_targets {
          margin-top: 8px;
          padding: 8px 10px;
          border-radius: 8px;
          border: 1px solid rgba(255,255,255,0.12);
          background: rgba(0,0,0,0.14);
        }

        .cg_recolor_targets_title {
          font-size: 12px;
          font-weight: 700;
          margin-bottom: 6px;
          opacity: 0.95;
        }

        .cg_recolor_targets_grid {
          display: grid;
          grid-template-columns: repeat(2, minmax(0, 1fr));
          gap: 6px 10px;
        }

        .cg_recolor_targets_grid .cg_recolor_toggle {
          min-height: 30px;
        }

        .cg_recolor_toggle.cg_recolor_toggle_full {
          grid-column: 1 / span 2;
        }

        .cg_recolor_toggle .cg_recolor_toggle_label {
          font-size: 11px;
          opacity: 0.95;
        }
      `;
      document.head.appendChild(style);
    }

    function updateGroupCardColors(card, color) {
      const header = card.querySelector('.cg_group_header');
      const light = hexToRgba(color, 0.18);
      const medium = hexToRgba(color, 0.32);
      if (header) header.style.background = `linear-gradient(90deg, ${light}, ${medium})`;
    }

    function sortPlayersForDisplay(players) {
      return (players || [])
        .slice()
        .sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), undefined, { sensitivity: 'base' }));
    }

    function renderSelectedPlayerPanel() {
      const host = $('cg_selected_panel_host');
      if (!host) return;

      host.textContent = '';
      if (!selectedPlayer) return;

      const g = colorGroups.find((x) => x.id === selectedPlayer.groupId);
      if (!g) return;

      const p = findPlayerInGroup(g.id, selectedPlayer.name, selectedPlayer.memberType || 'any');
      if (!p) return;

      const panel = document.createElement('div');
      panel.className = 'cg_selected_panel';

      const top = document.createElement('div');
      top.className = 'cg_selected_top';

      const nm = document.createElement('div');
      nm.className = 'cg_selected_name';
      nm.textContent = p.name;

      top.appendChild(nm);

      const meta = document.createElement('div');
      meta.className = 'cg_selected_meta';
      meta.textContent = `Group: ${g.name}`;

      panel.appendChild(top);
      panel.appendChild(meta);

      host.appendChild(panel);
    }

    function renderGroupsUI() {
      const list = $('cg_groups_list');
      if (!list) return;
      const lobbyInfo = getLobbyAccountInfoCached(500);

      list.innerHTML = '';

      colorGroups.forEach((group) => {
        const card = document.createElement('div');
        card.className = 'cg_group';
        card.dataset.groupId = group.id;

        const currentColor = String(group.color || '#ff0000');

        card.innerHTML = `
        <div class="cg_group_header">
            <div class="cg_group_handle" title="Drag to reorder">⋮⋮</div>
            <div class="cg_group_title" title="${group.name}">${group.name}</div>
            <div class="cg_group_menu" title="Group options">⋮</div>
        </div>

        <div class="cg_group_meta">
            <div class="cg_group_meta_line cg_group_meta_line_right">
            ${(group.players || []).length} players
            </div>
        </div>

        <div class="cg_group_players"></div>

        <!-- Color bar: click to open picker panel -->
        <div class="cg_color_bar">
            <div class="cg_color_btn" title="Click to choose a preset or custom colour">
            <div class="cg_color_preview" style="background:${currentColor};"></div>
            <div class="cg_color_hex">${currentColor.toLowerCase()}</div>
            </div>
            <div class="cg_color_edithint">Edit ▾</div>
        </div>
        `;

        const playersContainer = card.querySelector('.cg_group_players');

        const sortedPlayers = sortPlayersForDisplay(group.players);
        sortedPlayers.forEach((player) => {
          const row = document.createElement('div');
          row.className = 'cg_player_row';
          row.dataset.playerName = player.name;
          row.dataset.playerMemberType = getGroupPlayerMemberType(player);

          const isSelected =
            selectedPlayer &&
            selectedPlayer.groupId === group.id &&
            memberIdentityMatches(selectedPlayer, player.name, getGroupPlayerMemberType(player));
          if (isSelected) row.classList.add('selected');

          row.innerHTML = `
            <div class="cg_player_name" title="${player.name}">${player.name}</div>
            <div class="cg_player_right">
              <div class="cg_player_menu" title="Manage player">⋮</div>
            </div>
          `;

          const nameHost = row.querySelector('.cg_player_name');
          if (nameHost && shouldShowGuestWarningForGroupMember(player, lobbyInfo)) {
            const warn = document.createElement('span');
            warn.className = 'tbc_guest_warn_badge';
            warn.title = 'Temporary guest member. Removed when guest/room/host state changes.';
            warn.setAttribute('aria-label', 'Temporary guest member');
            nameHost.appendChild(warn);
          }

          row.addEventListener('click', (e) => {
            selectedPlayer = { groupId: group.id, name: player.name, memberType: getGroupPlayerMemberType(player) };
            renderGroupsUI();
            renderSelectedPlayerPanel();
          });

          playersContainer.appendChild(row);
        });

        updateGroupCardColors(card, group.color);
        list.appendChild(card);
      });

      const addCard = document.createElement('div');
      addCard.className = 'cg_group cg_group_add';
      addCard.innerHTML = `
        <div class="cg_group_add_inner">
          <div class="cg_group_add_plus">+</div>
          <div>Add new group</div>
        </div>
      `;
      list.appendChild(addCard);

      attachGroupEvents();
      updateStorageHintUI();
    }

    function attachGroupEvents() {
    const list = $('cg_groups_list');
    if (!list) return;

    const outer = $('cg_groups_outer');

    const addCard = list.querySelector('.cg_group_add');
    if (addCard) {
        addCard.addEventListener('click', (e) => {
        e.stopPropagation();
        addGroup('New Group');
        });
    }

    list.querySelectorAll('.cg_group').forEach((card) => {
        const groupId = card.dataset.groupId;
        if (!groupId) return;

        const handle = card.querySelector('.cg_group_handle');
        const titleEl = card.querySelector('.cg_group_title');
        const menuEl = card.querySelector('.cg_group_menu');
        const colorBtn = card.querySelector('.cg_color_btn');
        const previewBox = card.querySelector('.cg_color_preview');
        const hexChip = card.querySelector('.cg_color_hex');

        function applyColorToCard(newHex) {
        const val = String(newHex || '').trim();
        if (!val) return;

        setGroupColor(groupId, val);

        if (hexChip) hexChip.textContent = val.toLowerCase();
        if (previewBox) previewBox.style.background = val;

        updateGroupCardColors(card, val);
        }

        if (colorBtn) {
        colorBtn.addEventListener('click', (e) => {
            e.stopPropagation();

            const g = colorGroups.find((x) => x.id === groupId);
            const current = String((g && g.color) || '#ff0000');

            openPanel(colorBtn, (panel) => {
            const presetSwatches = COLOR_PRESETS.map((p) => {
                const active = p.color.toLowerCase() === current.toLowerCase();
                return `<div class="cg_swatch${active ? ' active' : ''}" data-preset="${p.id}" title="${p.label}" style="background:${p.color};"></div>`;
            }).join('');

            panel.innerHTML = `
                <div class="cg_color_panel">
                <div class="cg_color_panel_top">
                    <div class="cg_color_panel_preview">
                    <div class="cg_color_panel_previewbox" style="background:${current};"></div>
                    <div class="cg_color_panel_hex">${current.toLowerCase()}</div>
                    </div>
                </div>

                <div class="cg_color_panel_body">
                    <div class="cg_color_panel_section">
                    <div class="cg_color_panel_section_title">Presets</div>
                    <div class="cg_color_swatches">
                        ${presetSwatches}
                    </div>
                    </div>

                    <div class="cg_color_panel_section">
                    <div class="cg_color_panel_section_title">Custom</div>
                    <div class="cg_color_custom_row">
                        <input class="cg_custom_picker" type="color" value="${current}">
                        <div class="cg_color_usebtn">Use</div>
                    </div>
                    <div style="margin-top:6px;font-size:10px;opacity:.75;">
                        Pick a colour, then press “Use”.
                    </div>
                    </div>
                </div>
                </div>
            `;

            const preview = panel.querySelector('.cg_color_panel_previewbox');
            const hexEl = panel.querySelector('.cg_color_panel_hex');

            function setPreview(val) {
                if (preview) preview.style.background = val;
                if (hexEl) hexEl.textContent = val.toLowerCase();
            }

            panel.querySelectorAll('.cg_swatch').forEach((sw) => {
                sw.addEventListener('click', () => {
                const pid = sw.dataset.preset;
                const p = COLOR_PRESETS.find((pp) => pp.id === pid);
                if (!p) return;

                panel.querySelectorAll('.cg_swatch').forEach((s2) => s2.classList.remove('active'));
                sw.classList.add('active');

                setPreview(p.color);
                applyColorToCard(p.color);
                });
            });

            const customPicker = panel.querySelector('.cg_custom_picker');
            const useBtn = panel.querySelector('.cg_color_usebtn');

            if (customPicker) {
                customPicker.addEventListener('input', () => {
                panel.querySelectorAll('.cg_swatch').forEach((s2) => s2.classList.remove('active'));
                setPreview(customPicker.value);
                });
            }

            if (useBtn) {
                useBtn.addEventListener('click', () => {
                if (!customPicker) return;
                applyColorToCard(customPicker.value);
                closePanel();
                });
            }
            });
        });
        }

        let dragging = false;
        let dragOffsetX = 0;
        let dragOffsetY = 0;
        let placeholder = null;
        let startIndex = colorGroups.findIndex((g) => g.id === groupId);

        function onMouseMove(e) {
        if (!dragging) return;
        card.style.left = `${e.clientX - dragOffsetX}px`;
        card.style.top = `${e.clientY - dragOffsetY}px`;
        }

        function onMouseUp(e) {
        if (!dragging) return;
        dragging = false;

        document.removeEventListener('mousemove', onMouseMove);
        document.removeEventListener('mouseup', onMouseUp);

        const slots = colorGroups.length;
        let targetIndex = startIndex;

        if (slots > 0) {
            const rect = (outer || list).getBoundingClientRect();
            const scrollLeft = outer ? outer.scrollLeft : 0;
            const totalWidth = outer ? outer.scrollWidth : rect.width;

            let relX = (e.clientX - rect.left) + scrollLeft;
            relX = Math.max(0, Math.min(relX, totalWidth - 1));

            const slotWidth = totalWidth / slots;
            targetIndex = Math.floor(relX / slotWidth);
            targetIndex = Math.max(0, Math.min(targetIndex, slots - 1));
        }

        if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder);
        placeholder = null;

        if (card.parentNode) card.parentNode.removeChild(card);

        if (targetIndex !== startIndex) reorderGroupsToIndex(groupId, targetIndex);
        else renderGroupsUI();
        }

        function startDrag(e) {
        e.preventDefault();
        e.stopPropagation();
        if (dragging) return;

        dragging = true;
        startIndex = colorGroups.findIndex((g) => g.id === groupId);

        const rect = card.getBoundingClientRect();

        placeholder = document.createElement('div');
        placeholder.className = 'cg_group_placeholder';
        list.insertBefore(placeholder, card.nextSibling);

        card.classList.add('cg_group_dragging');
        card.style.position = 'absolute';
        card.style.width = `${rect.width}px`;
        card.style.height = `${rect.height}px`;
        card.style.left = `${rect.left}px`;
        card.style.top = `${rect.top}px`;
        card.style.pointerEvents = 'none';
        card.style.zIndex = '100000';

        document.body.appendChild(card);

        dragOffsetX = e.clientX - rect.left;
        dragOffsetY = e.clientY - rect.top;

        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup', onMouseUp);
        }

        if (handle) handle.addEventListener('mousedown', startDrag);

        if (titleEl) {
        titleEl.addEventListener('dblclick', (e) => {
            e.stopPropagation();
            openPanel(titleEl, (panel) => {
            const g = colorGroups.find((x) => x.id === groupId);
            const currentName = g ? g.name : '';

            panel.innerHTML = `
                <div class="mod_ctx_title">Rename group</div>
                <input class="mod_ctx_input" type="text" value="${currentName}">
                <div class="mod_ctx_buttons">
                <div class="mod_ctx_button mod_ctx_button_primary">Save</div>
                <div class="mod_ctx_button">Cancel</div>
                </div>
            `;

            const input = panel.querySelector('.mod_ctx_input');
            const btnSave = panel.querySelector('.mod_ctx_button_primary');
            const btnCancel = panel.querySelectorAll('.mod_ctx_button')[1];

            btnSave.addEventListener('click', () => {
                const value = input.value.trim();
                if (value) renameGroup(groupId, value);
                closePanel();
            });

            btnCancel.addEventListener('click', () => closePanel());

            input.focus();
            input.select();
            });
        });
        }

        if (menuEl) {
        menuEl.addEventListener('click', (e) => {
            e.stopPropagation();
            openPanel(menuEl, (panel) => {
            const g = colorGroups.find((x) => x.id === groupId);

            panel.innerHTML = `
                <div class="mod_ctx_title">Group: ${g ? g.name : ''}</div>
                <div class="mod_ctx_items">
                <div class="mod_ctx_item" data-action="add">Add player</div>
                <div class="mod_ctx_item" data-action="rename">Rename group</div>
                <div class="mod_ctx_item" data-action="delete" style="color:#ff6b6b;">Delete group</div>
                </div>
            `;

            panel.querySelectorAll('.mod_ctx_item').forEach((item) => {
                item.addEventListener('click', () => {
                const action = item.dataset.action;
                closePanel();

                if (action === 'add') {
                    openPanel(menuEl, (panel2) => {
                    panel2.innerHTML = `
                        <div class="mod_ctx_title">Add player</div>
                        <input class="mod_ctx_input" type="text" placeholder="Player name">
                        <div class="mod_ctx_error" style="display:none;"></div>
                        <div class="mod_ctx_buttons">
                        <div class="mod_ctx_button mod_ctx_button_primary">Add</div>
                        <div class="mod_ctx_button">Cancel</div>
                        </div>
                    `;

                    const input = panel2.querySelector('.mod_ctx_input');
                    const errEl = panel2.querySelector('.mod_ctx_error');
                    const btnAdd = panel2.querySelector('.mod_ctx_button_primary');
                    const btnCancel = panel2.querySelectorAll('.mod_ctx_button')[1];

                    btnAdd.addEventListener('click', () => {
                        errEl.style.display = 'none';
                        const res = addPlayerToGroup(groupId, input.value);
                        if (!res.ok) {
                        errEl.textContent = res.error || 'Error.';
                        errEl.style.display = 'block';
                        } else {
                        closePanel();
                        }
                    });

                    btnCancel.addEventListener('click', () => closePanel());
                    input.focus();
                    });
                }

                if (action === 'rename') {
                    const g2 = colorGroups.find((x) => x.id === groupId);
                    openPanel(menuEl, (panel2) => {
                    panel2.innerHTML = `
                        <div class="mod_ctx_title">Rename group</div>
                        <input class="mod_ctx_input" type="text" value="${g2 ? g2.name : ''}">
                        <div class="mod_ctx_buttons">
                        <div class="mod_ctx_button mod_ctx_button_primary">Save</div>
                        <div class="mod_ctx_button">Cancel</div>
                        </div>
                    `;

                    const input = panel2.querySelector('.mod_ctx_input');
                    const btnSave = panel2.querySelector('.mod_ctx_button_primary');
                    const btnCancel = panel2.querySelectorAll('.mod_ctx_button')[1];

                    btnSave.addEventListener('click', () => {
                        const value = input.value.trim();
                        if (value) renameGroup(groupId, value);
                        closePanel();
                    });

                    btnCancel.addEventListener('click', () => closePanel());

                    input.focus();
                    input.select();
                    });
                }

                if (action === 'delete') {
                    openPanel(menuEl, (panel2) => {
                    panel2.innerHTML = `
                        <div class="mod_ctx_title">Delete group?</div>
                        <div style="font-size:11px;opacity:0.8;margin-top:2px;">
                        This will remove the group and all its player assignments.
                        </div>
                        <div class="mod_ctx_buttons">
                        <div class="mod_ctx_button mod_ctx_button_primary"
                            style="background:rgba(255,65,65,0.7);border-color:rgba(255,65,65,0.9);">
                            Delete
                        </div>
                        <div class="mod_ctx_button">Cancel</div>
                        </div>
                    `;

                    const btnDelete = panel2.querySelector('.mod_ctx_button_primary');
                    const btnCancel = panel2.querySelectorAll('.mod_ctx_button')[1];

                    btnDelete.addEventListener('click', () => {
                        deleteGroup(groupId);
                        closePanel();
                    });

                    btnCancel.addEventListener('click', () => closePanel());
                    });
                }
                });
            });
            });
        });
        }

        card.querySelectorAll('.cg_player_row').forEach((row) => {
        const pname = row.dataset.playerName;
        const ptype = normalizeMemberType(row.dataset.playerMemberType || 'any');
        const menu = row.querySelector('.cg_player_menu');
        if (!pname) return;
        if (!menu) return;

        menu.addEventListener('click', (e) => {
            e.stopPropagation();
            openPanel(menu, (panel) => {

            panel.innerHTML = `
                <div class="mod_ctx_title">${pname}</div>
                <div class="mod_ctx_items">
                <div class="mod_ctx_item" data-action="move">Move to another group</div>
                <div class="mod_ctx_item" data-action="remove" style="color:#ff6b6b;">Remove from this group</div>
                </div>
            `;

            panel.querySelectorAll('.mod_ctx_item').forEach((item) => {
                item.addEventListener('click', () => {
                const action = item.dataset.action;
                closePanel();
                if (action === 'move') {
                    if (colorGroups.length < 2) return;

                    openPanel(menu, (panel2) => {
                    const itemsHtml = colorGroups
                        .filter((g) => g.id !== groupId)
                        .map((g) => `<div class="mod_ctx_item" data-target="${g.id}">${g.name}</div>`)
                        .join('');

                    panel2.innerHTML = `
                        <div class="mod_ctx_title">Move "${pname}" to:</div>
                        <div class="mod_ctx_error" style="display:none;"></div>
                        <div class="mod_ctx_items">
                        ${itemsHtml || '<div style="font-size:11px;opacity:0.8;">No other groups.</div>'}
                        </div>
                    `;

                    const errEl = panel2.querySelector('.mod_ctx_error');

                    panel2.querySelectorAll('.mod_ctx_item').forEach((it2) => {
                        it2.addEventListener('click', () => {
                        const targetId = it2.dataset.target;
                        const res = movePlayerToGroup(groupId, targetId, pname, ptype);

                        if (!res.ok) {
                            errEl.textContent = res.error || 'Error.';
                            errEl.style.display = 'block';
                        } else {
                            closePanel();
                        }
                        });
                    });
                    });
                }

                if (action === 'remove') removePlayerFromGroup(groupId, pname, ptype);
                });
            });
            });
        });
        });
    });
    }

    let recolorModRegistered = false;

    function initRecolorMod() {
      if (recolorModRegistered) return;

      const bonkMods = window.bonkMods;
      if (!bonkMods) return;

      recolorModRegistered = true;
      ensureRecolorStyles();

      window.recolorAPI = {
        getColorForName,
        getGroups: () => JSON.parse(JSON.stringify(colorGroups)),
      };

      bonkMods.registerMod({
        id: 'recolor',
        name: 'Re:Color',
        version: '1.3.2',
        author: 'SIoppy',
        description: `
            Colour groups for player names.
            Sort and manage players by name inside each group.
            `,
        devHint: 'Exposes window.recolorAPI.getColorForName(name).',
      });

      bonkMods.registerCategory({
        id: 'recolor_main',
        label: 'Colour Groups',
        order: 50,
      });

      bonkMods.addBlock({
        id: 'recolor_groups',
        modId: 'recolor',
        categoryId: 'recolor_main',
        title: 'Colour Groups',
        order: 0,
        render(container) {
          container.innerHTML = `
            <div class="mod_block_sub">
              Create groups of player names and assign them a colour.
              Players are listed alphabetically within each group.
            </div>

            <div id="recolor_storage_hint" style="margin-top:6px;font-size:11px;"></div>
            <div id="cg_sync_locked_notice" style="display:none;margin-top:6px;font-size:11px;color:#b53030;font-weight:700;">
              Shared groups sync is active. Colour Groups settings are locked until desync.
            </div>

            <div id="cg_lockable_root">
            <div class="cg_recolor_targets">
              <div class="cg_recolor_targets_title">Apply colours to</div>
              <div class="cg_recolor_targets_grid">
                <div class="tbc_toggle cg_recolor_toggle" id="cg_toggle_player_names"><div class="tbc_toggle_dot"></div><div class="cg_recolor_toggle_label">Player names (in-game)</div></div>
                <div class="tbc_toggle cg_recolor_toggle" id="cg_toggle_winner_board"><div class="tbc_toggle_dot"></div><div class="cg_recolor_toggle_label">Winner board</div></div>
                <div class="tbc_toggle cg_recolor_toggle" id="cg_toggle_lobby_chat_names"><div class="tbc_toggle_dot"></div><div class="cg_recolor_toggle_label">Lobby chat names</div></div>
                <div class="tbc_toggle cg_recolor_toggle" id="cg_toggle_ingame_chat_names"><div class="tbc_toggle_dot"></div><div class="cg_recolor_toggle_label">In-game chat names</div></div>
                <div class="tbc_toggle cg_recolor_toggle cg_recolor_toggle_full" id="cg_toggle_player_list_names"><div class="tbc_toggle_dot"></div><div class="cg_recolor_toggle_label">Player list names</div></div>
                <div class="tbc_toggle cg_recolor_toggle cg_recolor_toggle_full" id="cg_toggle_player_list_backboard"><div class="tbc_toggle_dot"></div><div class="cg_recolor_toggle_label">Player list backboard</div></div>
              </div>
            </div>

            <div id="cg_groups_outer">
              <div id="cg_groups_list"></div>
            </div>

            <div id="cg_selected_panel_host"></div>
            </div>
          `;

          renderGroupsUI();
          renderSelectedPlayerPanel();
          updateStorageHintUI();
          const applyRecolorSyncLockUi = () => {
            const lockable = $('cg_lockable_root');
            const notice = $('cg_sync_locked_notice');
            if (notice) notice.style.display = recolorUiSyncLocked ? '' : 'none';
            if (lockable) {
              lockable.style.opacity = recolorUiSyncLocked ? '0.5' : '1';
              lockable.style.pointerEvents = recolorUiSyncLocked ? 'none' : '';
            }
          };

          const toggleMap = [
            ['cg_toggle_player_names', 'playerNames'],
            ['cg_toggle_winner_board', 'winnerBoard'],
            ['cg_toggle_ingame_chat_names', 'ingameChatNames'],
            ['cg_toggle_lobby_chat_names', 'lobbyChatNames'],
            ['cg_toggle_player_list_names', 'playerListNames'],
            ['cg_toggle_player_list_backboard', 'playerListBackboard'],
          ];

          const renderRecolorSettingsToggles = () => {
            toggleMap.forEach(([id, key]) => {
              const el = $(id);
              if (!el) return;
              el.classList.toggle('on', !!recolorDisplaySettings[key]);
            });
          };

          toggleMap.forEach(([id, key]) => {
            const el = $(id);
            if (!el) return;
            el.addEventListener('click', () => {
              recolorDisplaySettings[key] = !recolorDisplaySettings[key];
              saveRecolorDisplaySettings();
            });
          });

          refreshRecolorSettingsUi = renderRecolorSettingsToggles;
          renderRecolorSettingsToggles();
          const isSelfHostForRecolorSyncLock = () => {
            if (typeof isSelfLobbyHost === 'function' && isSelfLobbyHost()) return true;
            if (typeof isSelfLobbyHostForRecolor === 'function' && isSelfLobbyHostForRecolor()) return true;
            const selfNorm = normalizeName((($('pretty_top_name') || {}).textContent || ''));
            if (!selfNorm) return false;
            const hostNormMain = typeof getLobbyHostNameNorm === 'function' ? normalizeName(getLobbyHostNameNorm()) : '';
            if (hostNormMain && selfNorm === hostNormMain) return true;
            const hostNormFallback = normalizeName(getLobbyHostNameNormForRecolor());
            return !!hostNormFallback && selfNorm === hostNormFallback;
          };
          const computeRecolorSyncLock = () => {
            if (isSelfHostForRecolorSyncLock()) return false;
            return !!window.tbcRoomGroupsSyncActive;
          };
          if (!recolorSyncLockListenerInstalled) {
            recolorSyncLockListenerInstalled = true;
            const refreshLock = () => {
              recolorUiSyncLocked = computeRecolorSyncLock();
              applyRecolorSyncLockUi();
            };
            window.addEventListener('tbcSharedSyncStateChanged', refreshLock);
            window.addEventListener('tbcSharedGroupsChanged', refreshLock);
          }
          recolorUiSyncLocked = computeRecolorSyncLock();
          applyRecolorSyncLockUi();
        },
      });

      setTimeout(() => {
        updateAccountStorageKey();
        updateStorageHintUI();
      }, 0);
    }

    if (window.bonkMods) initRecolorMod();
    window.addEventListener('bonkModsReady', initRecolorMod);

    const origFillText = CanvasRenderingContext2D.prototype.fillText;
    CanvasRenderingContext2D.prototype.fillText = function fillText(text, x, y) {
      if (!recolorDisplaySettings.playerNames) return origFillText.call(this, text, x, y);
      if (!hasDisplayGroupsForNames()) return origFillText.call(this, text, x, y);

      const color = getDisplayColorForName(String(text || ''));
      if (color) {
        const oldFill = this.fillStyle;
        this.fillStyle = color;
        origFillText.call(this, text, x, y);
        this.fillStyle = oldFill;
        return;
      }
      origFillText.call(this, text, x, y);
    };

  (() => {
    const CHAT_STORAGE_PREFIX_V2 = 'bonk_tbc_chat_v1_';
    const REPLAY_SYSTEM_COLOR = 'rgb(181, 48, 48)';
    const DEFAULT_SYSTEM_STATUS_COLOR = '#317dd7';
    const SYSTEM_COLOR_CATEGORIES = [
      { id: 'defaultSystem', label: 'Default system' },
      { id: 'portal', label: 'Portal messages' },
      { id: 'userJoin', label: 'User joined' },
      { id: 'userLeft', label: 'User left' },
      { id: 'kick', label: 'Kicked' },
      { id: 'ban', label: 'Banned' },
      { id: 'replay', label: 'Replay messages' },
      { id: 'hostTransfer', label: 'Host transfer' },
      { id: 'helpHint', label: 'Help hint / unknown cmd' },
      { id: 'customCommands', label: 'Custom commands' },
      { id: 'friend', label: 'Friend status' },
    ];
    const SYSTEM_COLOR_DEFAULT_HEX = {
      defaultSystem: '#317dd7',
      portal: '#44c0ff',
      userJoin: '#2e6f40',
      userLeft: '#cd1c18',
      kick: '#942222',
      ban: '#4a0404',
      replay: '#8c92ac',
      hostTransfer: '#317dd7',
      helpHint: '#317dd7',
      customCommands: '#b357d6',
      friend: '#4682b4',
    };
    const SYSTEM_COLOR_NEXT_FORMAT = { hex: 'rgb', rgb: 'hsv', hsv: 'hex' };

    function createDefaultChatState() {
      return {
        hideGuests: false,
        showSystemMessages: false,
        useCustomSystemMessageColors: false,
        ingameChatBackgrounds: false,
        hideIngameOthersUntilFadeDelay: false,
        blacklistUsers: [],
        systemMessageColors: createDefaultSystemMessageColors(),
        ingameChatLines: 4,
        ingameFadeDelaySec: 8
      };
    }

    function createDefaultSystemMessageColors() {
      const out = {};
      SYSTEM_COLOR_CATEGORIES.forEach((cat) => {
        out[cat.id] = {
          hex: String(SYSTEM_COLOR_DEFAULT_HEX[cat.id] || SYSTEM_COLOR_DEFAULT_HEX.defaultSystem).toLowerCase(),
          format: 'hex',
        };
      });
      return out;
    }

    let chatState = createDefaultChatState();

    let chatStorageKey = null;
    let lastChatStorageKey = undefined;
    let lastChatIdentity = undefined;
    let chatAccountObserver = null;
    let refreshChatSettingsUi = null;
    let ingameVisualRefreshQueued = false;
    let ingameChatFocusGuardsInstalled = false;
    let groupsPanelVisible = false;
    let pointsPanelVisible = false;
    let sharedHostGroupsSnapshot = [];
    let roomGroupsSyncActive = false;
    let groupsSyncChunksBySession = new Map();
    let groupsSyncTask = null;
    let groupsPanelAutoSyncTimer = null;
    let groupsSyncLastSig = '';
    let hostGroupsPanelSyncedRoomKey = '';
    let groupsRefreshSignalNextAllowedAt = 0;
    const GROUPS_SYNC_MAX_PAYLOAD_CHARS = 170;
    const GROUPS_SYNC_CHUNK_CHARS = 220;
    const GROUPS_SYNC_CHUNK_INTERVAL_MS = 7000;
    const ROOM_GROUPS_CACHE_PREFIX = 'bonk_tbc_room_groups_v1_';
    let lastRoomGroupsKey = '';
    let lastSharedBridgeSig = '';
    let lastSharedGroupsJoinNoticeKey = '';
    let lastObservedLobbyHostNorm = '';
    let liveGroupsSyncHostNorm = '';
    let lastHostTransferDesyncSig = '';
    let lastHostClosedRoomDesyncSig = '';
    let groupsRelayBusy = false;
    let pendingGroupsRelayTokenPull = '';
    let roomRelayPollTimer = null;
    let roomRelayPollInFlight = false;
    let roomRelayPollPausedByRenderer = false;
    let roomUiBothHiddenSince = 0;
    let lastGroupsSyncEligibility = null;
    let lastRendererVisibleForNonHostExitDesync = null;
    let lastRendererVisibleForPointsSnapshotReset = null;
    let pendingHostEventDesyncMessage = '';
    const roomRelayLastAppliedStampByScope = new Map();
    const GROUPS_SYNC_LIFECYCLE_STATUS_VISIBLE = false;
    const groupsPanelPlacement = { left: 12, top: 120, width: 248, maxHeight: 560 };
    const pointsPanelPlacement = { left: 270, top: 120, width: 300, maxHeight: 560 };
    let groupsPanelPinnedByUser = false;
    const groupsPanelDragState = { active: false, pointerId: null, offsetX: 0, offsetY: 0 };
    let pointsPanelPinnedByUser = false;
    const pointsPanelDragState = { active: false, pointerId: null, offsetX: 0, offsetY: 0 };
    let pointsPanelLastRenderSig = '';
    let pointsPanelCachedSourceEntries = [];
    let lastTempGuestPruneAt = 0;

    function setRoomGroupsSyncActive(active) {
      const next = !!active;
      if (roomGroupsSyncActive === next) return;
      roomGroupsSyncActive = next;
      window.tbcRoomGroupsSyncActive = next;
      window.dispatchEvent(new CustomEvent('tbcSharedSyncStateChanged', { detail: { active: next } }));
    }

    function applySharedGroupsDesyncNow(statusMessage = '') {
      pendingHostEventDesyncMessage = '';
      sharedHostGroupsSnapshot = [];
      groupsSyncChunksBySession = new Map();
      liveGroupsSyncHostNorm = '';
      setRoomGroupsSyncActive(false);
      purgeTemporaryGuestGroupMembers({ onlyMissingInLobby: false });
      syncSharedGroupsBridge();
      resetWinnerBoardTransientState();
      if (groupsPanelVisible) renderGroupsPanel();
      if (statusMessage) addGroupsLifecycleStatus(statusMessage);
    }

    function queueOrApplySharedGroupsDesync(statusMessage = '') {
      const rendererVisible = isElementActuallyVisible($('gamerenderer'));
      if (rendererVisible) {
        pendingHostEventDesyncMessage = String(statusMessage || '').trim();
        return false;
      }
      applySharedGroupsDesyncNow(statusMessage);
      return true;
    }

    function flushPendingSharedGroupsDesyncIfReady() {
      const msg = String(pendingHostEventDesyncMessage || '').trim();
      if (!msg) return false;
      const rendererVisible = isElementActuallyVisible($('gamerenderer'));
      if (rendererVisible) return false;
      applySharedGroupsDesyncNow(msg);
      return true;
    }

    function addGroupsLifecycleStatus(text, color = '#317dd7') {
      if (!GROUPS_SYNC_LIFECYCLE_STATUS_VISIBLE) return;
      addLocalChatStatus(text, color);
    }

    function escapeHtml(text) {
      return String(text || '')
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
    }

    function getLobbyHostNameNorm() {
      const badges = Array.from(document.querySelectorAll('.newbonklobby_playerentry_host'));
      for (const hostBadge of badges) {
        const src = String(hostBadge.getAttribute('src') || hostBadge.src || '').toLowerCase();
        const row = hostBadge.closest('.newbonklobby_playerentry');
        if (!row) continue;
        const nameEl = row.querySelector('.newbonklobby_playerentry_name');
        if (!nameEl) continue;
        const nameNorm = normalizeName(nameEl.textContent || '');
        if (!nameNorm) continue;
        if (src && /host_(?:[1-9]\d*)\.png/.test(src)) return nameNorm;
        const cs = window.getComputedStyle(hostBadge);
        const visible =
          !!cs &&
          cs.display !== 'none' &&
          cs.visibility !== 'hidden' &&
          cs.opacity !== '0' &&
          (
            (hostBadge.offsetWidth || 0) > 0 ||
            (hostBadge.offsetHeight || 0) > 0 ||
            (hostBadge.getClientRects ? hostBadge.getClientRects().length : 0) > 0
          );
        if (visible && src && src.indexOf('host_0.png') === -1) return nameNorm;
      }
      return '';
    }

    function isSelfLobbyHost() {
      const selfNorm = getSelfNameNorm();
      const hostNorm = getLobbyHostNameNorm();
      return !!selfNorm && !!hostNorm && selfNorm === hostNorm;
    }

    function canHostEditSharedGroupsPanel() {
      if (!isSelfLobbyHost()) return false;
      const hostNorm = getLobbyHostNameNorm();
      if (!hostNorm) return false;
      return hostGroupsPanelSyncedRoomKey === `h:${hostNorm}`;
    }

    function markHostGroupsPanelSyncedForCurrentRoom() {
      if (!isSelfLobbyHost()) return;
      const hostNorm = getLobbyHostNameNorm();
      if (!hostNorm) return;
      hostGroupsPanelSyncedRoomKey = `h:${hostNorm}`;
    }

    function normalizeSharedGroupSnapshot(raw) {
      if (!Array.isArray(raw)) return [];
      return raw
        .map((g) => {
          if (!g || typeof g !== 'object') return null;
          const name = String(g.name || 'Group').trim();
          const color = String(g.color || getRandomPresetColor()).trim() || getRandomPresetColor();
          const playersRaw = Array.isArray(g.players) ? g.players : [];
          const players = playersRaw
            .map((p) => {
              if (typeof p === 'string') return { name: String(p || '').trim(), memberType: 'account' };
              if (p && typeof p === 'object') {
                const name = String(p.name || '').trim();
                const memberType = normalizeMemberType(p.memberType || (p.tempGuest ? 'guest' : 'account'));
                return { name, memberType: memberType === 'any' ? 'account' : memberType };
              }
              return null;
            })
            .filter((p) => p && p.name);
          return { name, color, players };
        })
        .filter(Boolean);
    }

    function getLobbyRoomSyncKey() {
      const hostNorm = getLobbyHostNameNorm();
      const mapText = String((($('newbonklobby_maptext') || {}).textContent || '')).trim().toLowerCase();
      const modeText = String((($('newbonklobby_modetext') || {}).textContent || '')).trim().toLowerCase();
      const roundsText = String((($('newbonklobby_roundsinput') || {}).value || '')).trim().toLowerCase();
      const teamsText = String((($('newbonklobby_teams_middletext') || {}).textContent || '')).trim().toLowerCase();
      if (!hostNorm || !mapText || !modeText) return '';
      return `h:${hostNorm}|m:${mapText}|mode:${modeText}|r:${roundsText}|t:${teamsText}`;
    }

    function syncSharedGroupsBridge() {
      const safe = normalizeSharedGroupSnapshot(sharedHostGroupsSnapshot || []);
      const prev = Array.isArray(window.tbcSharedGroupsSnapshot) ? window.tbcSharedGroupsSnapshot : [];
      const mapByName = (groups) => {
        const out = new Map();
        (Array.isArray(groups) ? groups : []).forEach((g) => {
          if (!g || typeof g !== 'object') return;
          const color = String(g.color || '').trim();
          const players = Array.isArray(g.players) ? g.players : [];
          players.forEach((p) => {
            const pName = typeof p === 'string' ? p : (p && p.name ? p.name : '');
            const nm = normalizeName(pName);
            const pType = getGroupPlayerMemberType(p);
            const key = nm ? `${nm}|${getLookupTypeSuffix(pType)}` : '';
            if (!key || out.has(key)) return;
            out.set(key, color || '');
          });
        });
        return out;
      };
      const prevMap = mapByName(prev);
      const nextMap = mapByName(safe);
      const changedPlayersSet = new Set();
      const touched = new Set([...prevMap.keys(), ...nextMap.keys()]);
      touched.forEach((key) => {
        if ((prevMap.get(key) || '') !== (nextMap.get(key) || '')) {
          const nm = String(key || '').split('|')[0] || '';
          if (nm) changedPlayersSet.add(nm);
        }
      });
      const changedPlayers = Array.from(changedPlayersSet);
      const sig = JSON.stringify(
        safe.map((g) => ({
          name: g.name,
          color: g.color,
          players: (g.players || []).map((p) => [
            String((p && p.name) || '').trim(),
            getLookupTypeSuffix(getGroupPlayerMemberType(p)),
          ]),
        }))
      );
      if (sig === lastSharedBridgeSig) return;
      lastSharedBridgeSig = sig;
      window.tbcSharedGroupsSnapshot = safe;
      window.dispatchEvent(new CustomEvent('tbcSharedGroupsChanged', {
        detail: { changedPlayers }
      }));
    }

    function saveRoomGroupsCache(snapshot) {
      const roomKey = getLobbyRoomSyncKey();
      if (!roomKey) return;
      try {
        const payload = {
          at: Date.now(),
          groups: normalizeSharedGroupSnapshot(snapshot),
        };
        localStorage.setItem(ROOM_GROUPS_CACHE_PREFIX + roomKey, JSON.stringify(payload));
      } catch (e) {
        console.error('[TBC] Failed to save room groups cache', e);
      }
    }

    function loadRoomGroupsCacheForCurrentRoom() {
      const roomKey = getLobbyRoomSyncKey();
      if (!roomKey) return null;
      try {
        const raw = localStorage.getItem(ROOM_GROUPS_CACHE_PREFIX + roomKey);
        if (!raw) return null;
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== 'object') return null;
        return normalizeSharedGroupSnapshot(parsed.groups);
      } catch (e) {
        console.error('[TBC] Failed to load room groups cache', e);
        return null;
      }
    }

    function maybeLoadRoomGroupsCache() {
      const prevHostNorm = lastObservedLobbyHostNorm;
      const hostNorm = getLobbyHostNameNorm();
      lastObservedLobbyHostNorm = hostNorm || '';
      const rendererVisibleNow = isElementActuallyVisible($('gamerenderer'));
      const lobbyVisibleNow = isElementActuallyVisible($('newbonklobby'));

      const hostChanged = !!prevHostNorm && !!hostNorm && prevHostNorm !== hostNorm;
      if (hostChanged && (roomGroupsSyncActive || sharedHostGroupsSnapshot.length > 0)) {
        queueOrApplySharedGroupsDesync('[TBC] Host changed. Shared groups desynced until the new host syncs.');
      }

      const roomKey = getLobbyRoomSyncKey();
      if (roomKey === lastRoomGroupsKey) return;
      lastRoomGroupsKey = roomKey;
      resetWinnerBoardTransientState();

      if (!roomKey) {
        return;
      }
      if (isSelfLobbyHost()) {
        if (
          roomGroupsSyncActive &&
          !!liveGroupsSyncHostNorm &&
          !!hostNorm &&
          liveGroupsSyncHostNorm !== hostNorm
        ) {
          queueOrApplySharedGroupsDesync('[TBC] Host changed. Shared groups desynced until the new host syncs.');
        } else if (!roomGroupsSyncActive) {
          sharedHostGroupsSnapshot = [];
          liveGroupsSyncHostNorm = '';
          syncSharedGroupsBridge();
        }
        if (groupsPanelVisible) renderGroupsPanel();
        return;
      }

      const prevSnapshot = normalizeSharedGroupSnapshot(sharedHostGroupsSnapshot);
      const cached = loadRoomGroupsCacheForCurrentRoom();
      const deferSnapshotSwapDuringActiveRound =
        rendererVisibleNow &&
        !lobbyVisibleNow &&
        roomGroupsSyncActive &&
        prevSnapshot.length > 0 &&
        (!cached || !cached.length);
      if (deferSnapshotSwapDuringActiveRound) {
        sharedHostGroupsSnapshot = prevSnapshot;
        setRoomGroupsSyncActive(true);
        syncSharedGroupsBridge();
        if (groupsPanelVisible) renderGroupsPanel();
        return;
      }
      sharedHostGroupsSnapshot = cached || [];
      const hostForRoom = getLobbyHostNameNorm();
      const hostUnknown = !hostForRoom;
      const hostMatchesLiveSync = !!liveGroupsSyncHostNorm && !!hostForRoom && liveGroupsSyncHostNorm === hostForRoom;
      const canSafelyKeepPrevSnapshot =
        (!cached || !cached.length) &&
        roomGroupsSyncActive &&
        prevSnapshot.length > 0 &&
        hostMatchesLiveSync;
      if (canSafelyKeepPrevSnapshot) {
        sharedHostGroupsSnapshot = prevSnapshot;
        saveRoomGroupsCache(sharedHostGroupsSnapshot);
        setRoomGroupsSyncActive(true);
        syncSharedGroupsBridge();
        if (groupsPanelVisible) renderGroupsPanel();
        return;
      }
      const keepLiveSync =
        sharedHostGroupsSnapshot.length > 0 &&
        hostMatchesLiveSync;
      setRoomGroupsSyncActive(keepLiveSync);
      syncSharedGroupsBridge();
      if (sharedHostGroupsSnapshot.length && roomKey !== lastSharedGroupsJoinNoticeKey) {
        lastSharedGroupsJoinNoticeKey = roomKey;
        addGroupsLifecycleStatus('Room has synced groups...using host color groups');
      }
      if (groupsPanelVisible) renderGroupsPanel();
    }

    function maybeDesyncOnHostClosedRoomStatus(statusText) {
      const raw = String(statusText || '').replace(/\s+/g, ' ').trim();
      if (!raw) return;
      if (!/^\*\s+.+\s+has left the game and closed the room\.?$/i.test(raw)) return;
      const sig = normalizeName(raw);
      if (!sig || sig === lastHostClosedRoomDesyncSig) return;
      lastHostClosedRoomDesyncSig = sig;
      purgeTemporaryGuestGroupMembers({ onlyMissingInLobby: false });
      queueOrApplySharedGroupsDesync('[TBC] Host closed the room. Shared groups desynced.');
    }

    function getSharedGroupsSnapshotSig(groups) {
      const snapshot = normalizeSharedGroupSnapshot(groups);
      return JSON.stringify(snapshot.map((g) => [
        g.name,
        g.color,
        (g.players || []).map((p) => [
          String((p && p.name) || '').trim(),
          getLookupTypeSuffix(getGroupPlayerMemberType(p)),
        ]),
      ]));
    }

    function buildCompactFullSyncEnvelope(groups) {
      const snapshot = normalizeSharedGroupSnapshot(groups);
      return {
        m: 'f',
        g: snapshot.map((grp) => [
          String(grp.name || ''),
          String(grp.color || ''),
          (grp.players || [])
            .map((p) => {
              const n = String((p && p.name) || '').trim();
              if (!n) return null;
              const t = getLookupTypeSuffix(getGroupPlayerMemberType(p));
              return [n, t];
            })
            .filter(Boolean),
        ]),
      };
    }

    function buildDeltaSyncEnvelope(baseGroups, nextGroups) {
      const base = normalizeSharedGroupSnapshot(baseGroups);
      const next = normalizeSharedGroupSnapshot(nextGroups);
      const baseStructSig = JSON.stringify(base.map((g) => [g.name, g.color]));
      const nextStructSig = JSON.stringify(next.map((g) => [g.name, g.color]));
      if (baseStructSig !== nextStructSig) return null;

      const makePlayerMap = (groups) => {
        const out = new Map();
        groups.forEach((g, idx) => {
          (g.players || []).forEach((p) => {
            const displayName = String((p && p.name) || '').trim();
            const norm = normalizeName(displayName);
            const type = getLookupTypeSuffix(getGroupPlayerMemberType(p));
            const key = norm ? `${norm}|${type}` : '';
            if (!key || out.has(key)) return;
            out.set(key, { name: displayName, type, idx });
          });
        });
        return out;
      };

      const prevMap = makePlayerMap(base);
      const nextMap = makePlayerMap(next);
      const set = [];
      const del = [];

      nextMap.forEach((info, key) => {
        const prev = prevMap.get(key);
        if (!prev || prev.idx !== info.idx) set.push([info.name, info.type, info.idx]);
      });
      prevMap.forEach((info, key) => {
        if (!nextMap.has(key)) del.push([info.name, info.type]);
      });

      if (!set.length && !del.length) return null;
      return { m: 'd', s: nextStructSig, set, del };
    }

    function encodeSyncEnvelopeToB64(envelope) {
      const json = JSON.stringify(envelope || {});
      return btoa(unescape(encodeURIComponent(json)));
    }

    function decodeSyncEnvelopeFromB64(b64) {
      const json = decodeURIComponent(escape(atob(String(b64 || ''))));
      return JSON.parse(json);
    }

    function decodeCompactFullGroups(rows) {
      if (!Array.isArray(rows)) return [];
      return normalizeSharedGroupSnapshot(
        rows.map((row) => {
          const r = Array.isArray(row) ? row : [];
          const name = String(r[0] || '').trim() || 'Group';
          const color = String(r[1] || '').trim() || getRandomPresetColor();
          const players = Array.isArray(r[2]) ? r[2] : [];
          return {
            name,
            color,
            players: players
              .map((n) => {
                if (Array.isArray(n)) {
                  const nm = String(n[0] || '').trim();
                  const mt = normalizeMemberType(n[1] || 'account');
                  if (!nm) return null;
                  return { name: nm, memberType: mt === 'guest' ? 'guest' : 'account', tempGuest: mt === 'guest' };
                }
                const nm = String(n || '').trim();
                if (!nm) return null;
                return { name: nm, memberType: 'account' };
              })
              .filter((p) => p && p.name),
          };
        })
      );
    }

    function applyDeltaSyncEnvelope(baseGroups, envelope) {
      const base = normalizeSharedGroupSnapshot(baseGroups);
      const delta = envelope && typeof envelope === 'object' ? envelope : null;
      if (!delta || delta.m !== 'd') return null;

      const expectedSig = JSON.stringify(base.map((g) => [g.name, g.color]));
      if (String(delta.s || '') !== expectedSig) return null;

      const out = normalizeSharedGroupSnapshot(base);
      const removeByNormAndType = (normName, memberType) => {
        out.forEach((g) => {
          g.players = (g.players || []).filter((p) => {
            const nm = normalizeName((p && p.name) || '');
            const mt = getLookupTypeSuffix(getGroupPlayerMemberType(p));
            if (nm !== normName) return true;
            if (memberType === 'any') return false;
            return mt !== memberType;
          });
        });
      };

      (Array.isArray(delta.del) ? delta.del : []).forEach((n) => {
        const pair = Array.isArray(n) ? n : [n, 'account'];
        const norm = normalizeName(String(pair[0] || ''));
        const memberType = getLookupTypeSuffix(normalizeMemberType(pair[1] || 'account'));
        if (!norm) return;
        removeByNormAndType(norm, memberType);
      });

      (Array.isArray(delta.set) ? delta.set : []).forEach((pair) => {
        const row = Array.isArray(pair) ? pair : [];
        const displayName = String(row[0] || '').trim();
        const memberType = getLookupTypeSuffix(normalizeMemberType(row.length >= 3 ? row[1] : 'account'));
        const norm = normalizeName(displayName);
        const targetIdx = Math.floor(Number(row.length >= 3 ? row[2] : row[1]));
        if (!norm || !Number.isFinite(targetIdx) || targetIdx < 0 || targetIdx >= out.length) return;
        removeByNormAndType(norm, memberType);
        const target = out[targetIdx];
        if (!target) return;
        target.players = Array.isArray(target.players) ? target.players : [];
        target.players.push({ name: displayName, memberType: memberType === 'guest' ? 'guest' : 'account', tempGuest: memberType === 'guest' });
      });

      return normalizeSharedGroupSnapshot(out);
    }

    function encodeSharedGroupsPayload(groups) {
      return encodeSyncEnvelopeToB64(buildCompactFullSyncEnvelope(groups));
    }

    function decodeSharedGroupsPayload(b64, baseSnapshot = null) {
      const parsed = decodeSyncEnvelopeFromB64(b64);
      if (Array.isArray(parsed)) {
        return normalizeSharedGroupSnapshot(parsed);
      }
      if (!parsed || typeof parsed !== 'object') {
        throw new Error('Invalid shared-groups payload');
      }
      if (parsed.m === 'f') {
        return decodeCompactFullGroups(parsed.g);
      }
      if (parsed.m === 'd') {
        const applied = applyDeltaSyncEnvelope(baseSnapshot || [], parsed);
        if (applied) return applied;
        throw new Error('Delta payload cannot be applied to current base');
      }
      throw new Error('Unknown shared-groups payload mode');
    }

    const GROUPS_RELAY_GH_GISTS_API = 'https://api.github.com/gists';
    const GROUPS_RELAY_VERSION = 1;
    const GROUPS_RELAY_SHARED_GH_TOKEN = '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';
    const GROUPS_RELAY_FILE_NAME = `tbc_groups_relay_v${GROUPS_RELAY_VERSION}.json`;
    const GROUPS_RELAY_META_FILE_NAME = `tbc_groups_relay_meta_v${GROUPS_RELAY_VERSION}.json`;
    const GROUPS_RELAY_SCOPE_DESC_PREFIX = 'TBC Groups Relay Scope ';

    function getRoomRelayScopeKey() {
      const roomKey = String(getLobbyRoomSyncKey() || '').trim();
      if (!roomKey) return '';
      let h = 0x811c9dc5;
      for (let i = 0; i < roomKey.length; i += 1) {
        h ^= roomKey.charCodeAt(i);
        h = Math.imul(h, 0x01000193);
      }
      return `rk_${(h >>> 0).toString(16).padStart(8, '0')}`;
    }

    function relayHttpText(method, url, body = null, headersOverride = undefined) {
      return new Promise((resolve, reject) => {
        const requestHeaders = body == null
          ? undefined
          : (headersOverride === undefined ? { 'Content-Type': 'text/plain; charset=utf-8' } : headersOverride);
        const gmReq =
          (typeof GM_xmlhttpRequest === 'function' && GM_xmlhttpRequest) ||
          (typeof GM !== 'undefined' && GM && typeof GM.xmlHttpRequest === 'function' ? GM.xmlHttpRequest.bind(GM) : null);
        if (gmReq) {
          gmReq({
            method: String(method || 'GET').toUpperCase(),
            url: String(url || ''),
            anonymous: true,
            responseType: 'text',
            headers: requestHeaders,
            data: body == null ? undefined : String(body),
            onload: (res) => {
              const status = Number(res && res.status);
              if (status >= 200 && status < 300) resolve(String((res && res.responseText) || ''));
              else {
                const snippet = String((res && res.responseText) || '').replace(/\s+/g, ' ').trim().slice(0, 160);
                reject(new Error(`HTTP ${Number.isFinite(status) ? status : 'ERR'}${snippet ? ` - ${snippet}` : ''}`));
              }
            },
            onerror: () => reject(new Error('Network error')),
            ontimeout: () => reject(new Error('Request timeout')),
          });
          return;
        }
        fetch(String(url || ''), {
          method: String(method || 'GET').toUpperCase(),
          headers: requestHeaders,
          body: body == null ? undefined : String(body),
        })
          .then((res) => {
            return res.text().then((txt) => {
              if (!res.ok) {
                const snippet = String(txt || '').replace(/\s+/g, ' ').trim().slice(0, 160);
                throw new Error(`HTTP ${res.status}${snippet ? ` - ${snippet}` : ''}`);
              }
              return txt;
            });
          })
          .then((txt) => resolve(String(txt || '')))
          .catch((e) => reject(e));
      });
    }

    function bytesToBase64Url(bytes) {
      const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
      let bin = '';
      for (let i = 0; i < arr.length; i += 1) bin += String.fromCharCode(arr[i]);
      return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
    }

    function base64UrlToBytes(text) {
      const raw = String(text || '').replace(/-/g, '+').replace(/_/g, '/');
      const pad = raw.length % 4;
      const b64 = raw + (pad ? '='.repeat(4 - pad) : '');
      const bin = atob(b64);
      const out = new Uint8Array(bin.length);
      for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i) & 0xff;
      return out;
    }

    function parseGroupsRelayToken(input) {
      const raw = String(input || '').trim();
      if (!raw) return null;
      const fromGistUrl = raw.match(/gist\.github\.com\/[^/]+\/([A-Za-z0-9]+)(?:[#:]([A-Za-z0-9_-]+))?/i);
      if (fromGistUrl) {
        const id = String(fromGistUrl[1] || '').trim();
        const key = String(fromGistUrl[2] || '').trim();
        if (!id || !key) return null;
        return { provider: 'gh', id, key };
      }
      const plain = raw.match(/^([A-Za-z0-9_.-]+):([A-Za-z0-9_-]+)$/);
      if (plain) {
        const left = String(plain[1] || '').trim();
        const key = String(plain[2] || '').trim();
        if (!left || !key) return null;
        const dotted = left.match(/^([a-z]{2,4})\.(.+)$/i);
        if (dotted) {
          const provider = String(dotted[1] || '').toLowerCase();
          const id = String(dotted[2] || '').trim();
          if (!id) return null;
          return { provider, id, key };
        }
        return { provider: 'gh', id: left, key };
      }
      return null;
    }

    function getGroupsRelayGithubToken() {
      const sharedTok = String(GROUPS_RELAY_SHARED_GH_TOKEN || '').trim();
      return sharedTok || null;
    }

    function maybeDesyncOnHostTransferStatus(statusText) {
      const raw = String(statusText || '').replace(/\s+/g, ' ').trim();
      if (!raw) return;
      const transferA = /^\*\s+.+\s+has left the game and\s+.+\s+is now the game host\.?$/i;
      const transferB = /^\*\s+.+\s+has given host privileges to\s+.+,\s*who is now the game host\.?$/i;
      const transferC = /^\*\s+you are now the host of this game\.?$/i;
      const transferD = /^\*\s+you are now the game host\.?$/i;
      if (!transferA.test(raw) && !transferB.test(raw) && !transferC.test(raw) && !transferD.test(raw)) return;
      const sig = normalizeName(raw);
      if (!sig || sig === lastHostTransferDesyncSig) return;
      lastHostTransferDesyncSig = sig;
      purgeTemporaryGuestGroupMembers({ onlyMissingInLobby: false });
      queueOrApplySharedGroupsDesync('[TBC] Host changed. Shared groups desynced until the new host syncs.');
    }

    async function uploadGroupsRelayViaGithub(relayBody, keyToken, scopeKey = '') {
      const ghToken = getGroupsRelayGithubToken();
      if (!ghToken) throw new Error('GitHub relay token not configured.');
      const scope = String(scopeKey || '').trim();
      const description = scope
        ? `${GROUPS_RELAY_SCOPE_DESC_PREFIX}${scope} @ ${Date.now()}`
        : 'TBC Groups Relay (encrypted)';
      const meta = JSON.stringify({
        v: GROUPS_RELAY_VERSION,
        scope,
        key: String(keyToken || ''),
        at: Date.now(),
      });
      const payload = JSON.stringify({
        description,
        public: false,
        files: {
          [GROUPS_RELAY_FILE_NAME]: { content: String(relayBody || '') },
          [GROUPS_RELAY_META_FILE_NAME]: { content: meta },
        },
      });
      const resText = await relayHttpText('POST', GROUPS_RELAY_GH_GISTS_API, payload, {
        'Content-Type': 'application/json; charset=utf-8',
        Accept: 'application/vnd.github+json',
        Authorization: `token ${ghToken}`,
        'X-GitHub-Api-Version': '2022-11-28',
      });
      let parsed = null;
      try { parsed = JSON.parse(String(resText || '{}')); } catch {}
      const gistId = String((parsed && parsed.id) || '').trim();
      if (!gistId) throw new Error('GitHub gist response missing id.');
      return `gh.${gistId}:${keyToken}`;
    }

    async function encryptGroupsRelayPayload(snapshot) {
      if (!window.crypto || !crypto.subtle || typeof TextEncoder === 'undefined') {
        throw new Error('WebCrypto is unavailable in this browser.');
      }
      const env = {
        v: GROUPS_RELAY_VERSION,
        p: buildCompactFullSyncEnvelope(snapshot),
        t: Date.now(),
      };
      const plain = new TextEncoder().encode(JSON.stringify(env));
      const keyBytes = crypto.getRandomValues(new Uint8Array(32));
      const iv = crypto.getRandomValues(new Uint8Array(12));
      const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
      const cipherBuf = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plain);
      const cipher = new Uint8Array(cipherBuf);
      return {
        relayBody: JSON.stringify({
          v: GROUPS_RELAY_VERSION,
          a: 'A256GCM',
          iv: bytesToBase64Url(iv),
          ct: bytesToBase64Url(cipher),
        }),
        keyToken: bytesToBase64Url(keyBytes),
      };
    }

    async function decryptGroupsRelayPayload(relayText, keyToken) {
      if (!window.crypto || !crypto.subtle || typeof TextEncoder === 'undefined' || typeof TextDecoder === 'undefined') {
        throw new Error('WebCrypto is unavailable in this browser.');
      }
      const parsed = JSON.parse(String(relayText || '{}'));
      if (!parsed || parsed.a !== 'A256GCM' || !parsed.iv || !parsed.ct) {
        throw new Error('Invalid relay payload.');
      }
      const keyBytes = base64UrlToBytes(keyToken);
      const iv = base64UrlToBytes(parsed.iv);
      const ct = base64UrlToBytes(parsed.ct);
      const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']);
      const plainBuf = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
      const plainText = new TextDecoder().decode(new Uint8Array(plainBuf));
      const env = JSON.parse(plainText);
      if (!env || Number(env.v) !== GROUPS_RELAY_VERSION || !env.p) {
        throw new Error('Relay payload version mismatch.');
      }
      if (env.p.m === 'f') return decodeCompactFullGroups(env.p.g);
      throw new Error('Unsupported relay payload mode.');
    }

    function broadcastGroupsRelayTokenFromHost(tokenText) {
      if (!isSelfLobbyHost()) return false;
      const token = String(tokenText || '').trim();
      if (!token) return false;
      return sendLobbyChatMessage(buildGroupsSyncTransportMessage(`[TBCGRELAY|${token}]`));
    }

    function schedulePendingRelayTokenPull() {
      if (groupsRelayBusy) return;
      const token = String(pendingGroupsRelayTokenPull || '').trim();
      if (!token) return;
      pendingGroupsRelayTokenPull = '';
      setTimeout(() => {
        pullGroupsRelaySnapshot(token).catch(() => {});
      }, 75);
    }

    async function uploadGroupsRelaySnapshot(options = null) {
      const announceRelay = !!(options && options.announceRelay);
      const silent = !!(options && options.silent);
      const status = (text, color = '#317dd7') => {
        if (silent) return;
        addLocalChatStatus(text, color);
      };
      if (groupsRelayBusy) {
        status('[TBC] Relay is already in progress. Please wait.', 'rgb(181, 48, 48)');
        return false;
      }
      groupsRelayBusy = true;
      let succeeded = false;
      try {
        if (!isSelfLobbyHost()) {
          status('[TBC] Only the room host can push a groups relay.', 'rgb(181, 48, 48)');
          return false;
        }
        if (!canRunGroupsSyncNow()) {
          addSyncDisabledStatusNotice({ silent });
          return false;
        }
        if (!getGroupsRelayGithubToken()) {
          const ok = broadcastSharedGroupsFromHost(true, { silent: true });
          if (ok) {
            markHostGroupsPanelSyncedForCurrentRoom();
            if (groupsSyncTask) status('[TBC] Groups panel sync started (chat fallback).');
            else status('[TBC] Groups panel refresh sent (chat fallback).');
          } else {
            status('[TBC] Chat fallback sync failed.', 'rgb(181, 48, 48)');
          }
          succeeded = !!ok;
          return succeeded;
        }
        const snapshot = normalizeSharedGroupSnapshot(colorGroups);
        const { relayBody, keyToken } = await encryptGroupsRelayPayload(snapshot);
        const relayScope = getRoomRelayScopeKey();
        const token = await uploadGroupsRelayViaGithub(relayBody, keyToken, relayScope);
        status('[TBC] Relay uploaded via github gist.');
        status('[TBC] Relay uploaded.');
        let announced = true;
        if (announceRelay) {
          announced = broadcastGroupsRelayTokenFromHost(token);
          if (announced) status('[TBC] Relay token broadcasted. Clients should auto-pull.');
          else status('[TBC] Relay uploaded, but relay token broadcast failed.', 'rgb(181, 48, 48)');
        }
        succeeded = !announceRelay || !!announced;
        if (succeeded) {
          markHostGroupsPanelSyncedForCurrentRoom();
          sharedHostGroupsSnapshot = normalizeSharedGroupSnapshot(snapshot);
          saveRoomGroupsCache(sharedHostGroupsSnapshot);
          liveGroupsSyncHostNorm = getLobbyHostNameNorm() || '';
          setRoomGroupsSyncActive(true);
          syncSharedGroupsBridge();
          if (groupsPanelVisible) renderGroupsPanel();
        }
        try {
          if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
            await navigator.clipboard.writeText(String(token));
            status('[TBC] Relay token copied to clipboard.');
          }
        } catch {}
      } catch (e) {
        console.error('[TBC] Relay upload failed', e);
        status(`[TBC] Relay upload failed: ${e && e.message ? e.message : 'Unknown error'}`, 'rgb(181, 48, 48)');
        succeeded = false;
      } finally {
        groupsRelayBusy = false;
        schedulePendingRelayTokenPull();
      }
      return succeeded;
    }

    async function pullGroupsRelaySnapshot(tokenText) {
      const requestedToken = String(tokenText || '').trim();
      if (groupsRelayBusy) {
        if (requestedToken) pendingGroupsRelayTokenPull = requestedToken;
        return;
      }
      groupsRelayBusy = true;
      try {
        const token = parseGroupsRelayToken(requestedToken);
        if (!token) {
          addLocalChatStatus('[TBC] Invalid relay token.', 'rgb(181, 48, 48)');
          return;
        }
        const provider = String(token.provider || 'gh').toLowerCase();
        if (provider !== 'gh') throw new Error('Only GitHub relay tokens are supported now.');
        const gistUrl = `${GROUPS_RELAY_GH_GISTS_API}/${token.id}`;
        const pullHeaders = {
          Accept: 'application/vnd.github+json',
          'X-GitHub-Api-Version': '2022-11-28',
        };
        const ghToken = getGroupsRelayGithubToken();
        if (ghToken) pullHeaders.Authorization = `token ${ghToken}`;
        const raw = String(await relayHttpText('GET', gistUrl, null, pullHeaders) || '');
        let parsed = null;
        try { parsed = JSON.parse(raw); } catch {}
        const files = parsed && parsed.files && typeof parsed.files === 'object' ? parsed.files : null;
        if (!files) throw new Error('GitHub gist files missing.');
        const relayFile =
          files[GROUPS_RELAY_FILE_NAME] ||
          Object.values(files).find((f) => f && typeof f === 'object' && typeof f.content === 'string' && /relay/i.test(String(f.filename || '')));
        const relayText = String((relayFile && relayFile.content) || '');
        if (!relayText) throw new Error('GitHub gist content is empty.');
        const metaFile =
          files[GROUPS_RELAY_META_FILE_NAME] ||
          Object.values(files).find((f) => f && typeof f === 'object' && typeof f.content === 'string' && /meta/i.test(String(f.filename || '')));
        let metaKey = '';
        try {
          const parsedMeta = metaFile && typeof metaFile.content === 'string'
            ? JSON.parse(metaFile.content)
            : null;
          metaKey = String((parsedMeta && parsedMeta.key) || '').trim();
        } catch {}
        const useKey = String(token.key || metaKey || '').trim();
        if (!useKey) throw new Error('Relay decryption key missing.');
        const decoded = await decryptGroupsRelayPayload(relayText, useKey);
        sharedHostGroupsSnapshot = normalizeSharedGroupSnapshot(decoded);
        saveRoomGroupsCache(sharedHostGroupsSnapshot);
        liveGroupsSyncHostNorm = getLobbyHostNameNorm() || '';
        setRoomGroupsSyncActive(true);
        syncSharedGroupsBridge();
        if (groupsPanelVisible) renderGroupsPanel();
        addGroupsLifecycleStatus('Room has synced groups...using host color groups');
      } catch (e) {
        console.error('[TBC] Relay pull failed', e);
        addLocalChatStatus(`[TBC] Relay pull failed: ${e && e.message ? e.message : 'Unknown error'}`, 'rgb(181, 48, 48)');
      } finally {
        groupsRelayBusy = false;
        schedulePendingRelayTokenPull();
      }
    }

    async function pollRoomRelaySnapshotFallback() {
      if (roomRelayPollInFlight || groupsRelayBusy) return;
      const rendererVisible = isElementActuallyVisible($('gamerenderer'));
      const lobbyVisible = isElementActuallyVisible($('newbonklobby'));
      if (rendererVisible) {
        roomRelayPollPausedByRenderer = true;
        return;
      }
      if (!lobbyVisible) return;
      if (roomRelayPollPausedByRenderer) roomRelayPollPausedByRenderer = false;
      if (document.hidden) return;
      if (isSelfLobbyHost()) return;
      const scopeKey = getRoomRelayScopeKey();
      if (!scopeKey) return;
      const ghToken = getGroupsRelayGithubToken();
      if (!ghToken) return;

      roomRelayPollInFlight = true;
      try {
        const headers = {
          Accept: 'application/vnd.github+json',
          Authorization: `token ${ghToken}`,
          'X-GitHub-Api-Version': '2022-11-28',
        };
        const listRaw = String(await relayHttpText('GET', `${GROUPS_RELAY_GH_GISTS_API}?per_page=30`, null, headers) || '[]');
        let gists = [];
        try { gists = JSON.parse(listRaw); } catch {}
        if (!Array.isArray(gists) || !gists.length) return;
        const descNeedle = `${GROUPS_RELAY_SCOPE_DESC_PREFIX}${scopeKey}`;
        const target = gists.find((g) => {
          const d = String((g && g.description) || '');
          return d.indexOf(descNeedle) === 0;
        });
        if (!target || !target.id) return;

        const stamp = `${String(target.id || '')}|${String(target.updated_at || '')}`;
        if (roomRelayLastAppliedStampByScope.get(scopeKey) === stamp) return;

        const gistRaw = String(await relayHttpText('GET', `${GROUPS_RELAY_GH_GISTS_API}/${target.id}`, null, headers) || '');
        let gist = null;
        try { gist = JSON.parse(gistRaw); } catch {}
        const files = gist && gist.files && typeof gist.files === 'object' ? gist.files : null;
        if (!files) return;
        const relayFile =
          files[GROUPS_RELAY_FILE_NAME] ||
          Object.values(files).find((f) => f && typeof f === 'object' && typeof f.content === 'string' && /relay/i.test(String(f.filename || '')));
        const metaFile =
          files[GROUPS_RELAY_META_FILE_NAME] ||
          Object.values(files).find((f) => f && typeof f === 'object' && typeof f.content === 'string' && /meta/i.test(String(f.filename || '')));
        const relayText = String((relayFile && relayFile.content) || '');
        if (!relayText) return;
        let relayMeta = null;
        try {
          relayMeta = metaFile && typeof metaFile.content === 'string' ? JSON.parse(metaFile.content) : null;
        } catch {
          relayMeta = null;
        }
        const metaScope = String((relayMeta && relayMeta.scope) || '').trim();
        if (metaScope && metaScope !== scopeKey) return;
        const key = String((relayMeta && relayMeta.key) || '').trim();
        if (!key) return;

        const decoded = await decryptGroupsRelayPayload(relayText, key);
        const lobbyVisibleNow = isElementActuallyVisible($('newbonklobby'));
        const rendererVisibleNow = isElementActuallyVisible($('gamerenderer'));
        if (!lobbyVisibleNow || rendererVisibleNow) return;
        if (getRoomRelayScopeKey() !== scopeKey) return;

        sharedHostGroupsSnapshot = normalizeSharedGroupSnapshot(decoded);
        saveRoomGroupsCache(sharedHostGroupsSnapshot);
        liveGroupsSyncHostNorm = getLobbyHostNameNorm() || '';
        setRoomGroupsSyncActive(true);
        syncSharedGroupsBridge();
        if (groupsPanelVisible) renderGroupsPanel();
        roomRelayLastAppliedStampByScope.set(scopeKey, stamp);
      } catch (e) {
        console.error('[TBC] Relay fallback poll failed', e);
      } finally {
        roomRelayPollInFlight = false;
      }
    }

    function setupRoomRelayFallbackPolling() {
      if (roomRelayPollTimer) {
        clearInterval(roomRelayPollTimer);
        roomRelayPollTimer = null;
      }
    }

    function sendLobbyChatMessage(text) {
      const input =
        $('newbonklobby_chat_input') ||
        document.querySelector('#newbonklobby_chatbox input[type="text"], #newbonklobby_chatbox textarea');
      if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) return false;
      const rawText = String(text || '');
      const previousValue = String(input.value || '');
      const isTransportPayload =
        /\[TBCG(?:REF|CLR|RELAY)/i.test(rawText) ||
        rawText.indexOf('\u2063\u2064') !== -1 ||
        rawText.indexOf('\u2063') !== -1 ||
        rawText.indexOf('\u2064') !== -1;
      input.focus();
      input.value = rawText;
      input.dispatchEvent(new Event('input', { bubbles: true }));
      try {
        const keydownEvt = new KeyboardEvent('keydown', {
          key: 'Enter',
          code: 'Enter',
          keyCode: 13,
          which: 13,
          bubbles: true,
          cancelable: true,
        });
        const keyupEvt = new KeyboardEvent('keyup', {
          key: 'Enter',
          code: 'Enter',
          keyCode: 13,
          which: 13,
          bubbles: true,
          cancelable: true,
        });
        input.dispatchEvent(keydownEvt);
        input.dispatchEvent(keyupEvt);
      } catch {}
      if (isTransportPayload) {
        const restoreDraftIfNeeded = () => {
          if (!previousValue) return;
          const v = String(input.value || '');
          if (v) return;
          input.value = previousValue;
          input.dispatchEvent(new Event('input', { bubbles: true }));
        };
        const shouldClearArtifact = () => {
          if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) return false;
          const v = String(input.value || '');
          if (!v) return false;
          if (v === '.') return true;
          if (v.indexOf('\u2063') !== -1 || v.indexOf('\u2064') !== -1) return true;
          if (v.startsWith('.\u2063\u2064')) return true;
          return false;
        };
        let tries = 0;
        const maxTries = 8;
        const cleanup = () => {
          tries += 1;
          if (shouldClearArtifact()) {
            input.value = '';
            input.dispatchEvent(new Event('input', { bubbles: true }));
            restoreDraftIfNeeded();
            return;
          }
          if (tries < maxTries) setTimeout(cleanup, 90);
          else restoreDraftIfNeeded();
        };
        setTimeout(cleanup, 40);
      }
      return true;
    }

    function closeIngameChatInputByEnterTap(taps = 1) {
      const inGameVisible =
        typeof isElementActuallyVisible === 'function' &&
        !!isElementActuallyVisible($('gamerenderer'));
      if (!inGameVisible) return;
      const input = $('ingamechatinputtext');
      if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) return;
      const cs = window.getComputedStyle(input);
      if (!cs || cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return;
      if (
        (input.offsetWidth || 0) <= 0 &&
        (input.offsetHeight || 0) <= 0 &&
        (input.getClientRects ? input.getClientRects().length : 0) <= 0
      ) return;
      const count = Math.max(1, parseInt(String(taps || '1'), 10) || 1);
      const fireTap = () => {
        try {
          const keydownEvt = new KeyboardEvent('keydown', {
            key: 'Enter',
            code: 'Enter',
            keyCode: 13,
            which: 13,
            bubbles: true,
            cancelable: true,
          });
          const keyupEvt = new KeyboardEvent('keyup', {
            key: 'Enter',
            code: 'Enter',
            keyCode: 13,
            which: 13,
            bubbles: true,
            cancelable: true,
          });
          input.dispatchEvent(keydownEvt);
          input.dispatchEvent(keyupEvt);
        } catch {}
      };
      for (let i = 0; i < count; i += 1) {
        setTimeout(fireTap, 35 * i);
      }
    }

    function canRunGroupsSyncNow() {
      const lobbyVisible =
        typeof isElementActuallyVisible === 'function' &&
        !!isElementActuallyVisible($('newbonklobby'));
      const rendererVisible =
        typeof isElementActuallyVisible === 'function' &&
        !!isElementActuallyVisible($('gamerenderer'));
      return lobbyVisible && !rendererVisible;
    }

    function canRunGroupsDesyncNow() {
      const rendererVisible =
        typeof isElementActuallyVisible === 'function' &&
        !!isElementActuallyVisible($('gamerenderer'));
      return !rendererVisible;
    }

    function broadcastSharedGroupsFromHost(force = false, opts = null) {
      const silent = !!(opts && opts.silent);
      const skipNoop = !!(opts && opts.skipNoop);
      const useCarrierTransport = !!(opts && opts.transportMode === 'carrier');
      const buildTransport = useCarrierTransport
        ? buildGroupsSyncTransportCarrierMessage
        : buildGroupsSyncTransportMessage;
      if (!isSelfLobbyHost()) return false;
      if (!canRunGroupsSyncNow()) {
        addSyncDisabledStatusNotice({ silent });
        return false;
      }
      if (groupsSyncTask) {
        if (!silent) addLocalChatStatus('[TBC] Sync already in progress. Please wait.', 'rgb(181, 48, 48)');
        return false;
      }
      const now = Date.now();
      if (!force && now < groupsRefreshSignalNextAllowedAt) {
        if (!silent) {
          const sec = Math.ceil((groupsRefreshSignalNextAllowedAt - now) / 1000);
          addLocalChatStatus(`[TBC] Please wait ${sec}s before syncing again.`, 'rgb(181, 48, 48)');
        }
        return false;
      }
      let snapshot = [];
      try {
        snapshot = normalizeSharedGroupSnapshot(colorGroups);
      } catch (e) {
        console.error('[TBC] Failed to encode groups refresh payload', e);
        return false;
      }
      const snapshotSig = getSharedGroupsSnapshotSig(snapshot);

      if ((skipNoop || !force) && snapshotSig === groupsSyncLastSig) {
        if (!silent) addLocalChatStatus('[TBC] Groups already up to date.');
        return true;
      }
      groupsSyncLastSig = snapshotSig;
      const fullPayload = encodeSharedGroupsPayload(snapshot);
      let payload = fullPayload;
      const deltaEnvelope = buildDeltaSyncEnvelope(sharedHostGroupsSnapshot || [], snapshot);
      if (deltaEnvelope) {
        try {
          const deltaPayload = encodeSyncEnvelopeToB64(deltaEnvelope);
          if (deltaPayload.length > 0 && deltaPayload.length < fullPayload.length) {
            payload = deltaPayload;
          }
        } catch {}
      }

      if (payload.length > GROUPS_SYNC_MAX_PAYLOAD_CHARS) {
        const chunks = [];
        for (let i = 0; i < payload.length; i += GROUPS_SYNC_CHUNK_CHARS) {
          chunks.push(payload.slice(i, i + GROUPS_SYNC_CHUNK_CHARS));
        }
        if (!chunks.length) chunks.push('');
        const session = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
        const total = chunks.length;
        groupsRefreshSignalNextAllowedAt = now + (total * GROUPS_SYNC_CHUNK_INTERVAL_MS) + 1500;
        groupsSyncTask = { session, total, sent: 0 };
        if (groupsPanelVisible) renderGroupsPanel();

        const sendNext = () => {
          if (!groupsSyncTask || groupsSyncTask.session !== session) return;
          const idx = groupsSyncTask.sent;
          if (idx >= total) {
            groupsSyncTask = null;
            if (groupsPanelVisible) renderGroupsPanel();
            if (!silent) addLocalChatStatus('[TBC] Groups sync completed.');
            return;
          }
          const chunk = chunks[idx];
          const rawMsg = `[TBCGREF|${session}|${idx + 1}/${total}|${chunk}]`;
          const ok = sendLobbyChatMessage(buildTransport(rawMsg));
          if (!ok) {
            groupsSyncTask = null;
            if (groupsPanelVisible) renderGroupsPanel();
            if (!silent) addLocalChatStatus('[TBC] Sync failed: could not send chat payload.', 'rgb(181, 48, 48)');
            return;
          }
          groupsSyncTask.sent += 1;
          if (groupsPanelVisible) renderGroupsPanel();
          setTimeout(sendNext, GROUPS_SYNC_CHUNK_INTERVAL_MS);
        };

        setTimeout(sendNext, 0);
        if (!silent) {
          const secs = Math.max(1, Math.round(GROUPS_SYNC_CHUNK_INTERVAL_MS / 1000));
          addLocalChatStatus(`[TBC] Large payload detected. Syncing ${total} parts (1 part every ${secs}s)...`);
        }
        saveRoomGroupsCache(snapshot);
        sharedHostGroupsSnapshot = normalizeSharedGroupSnapshot(snapshot);
        liveGroupsSyncHostNorm = getLobbyHostNameNorm() || '';
        setRoomGroupsSyncActive(true);
        syncSharedGroupsBridge();
        return true;
      }

      groupsRefreshSignalNextAllowedAt = now + 1400;
      saveRoomGroupsCache(snapshot);
      const sent = sendLobbyChatMessage(buildTransport(`[TBCGREF|${payload}]`));
      if (sent) {
        sharedHostGroupsSnapshot = normalizeSharedGroupSnapshot(snapshot);
        liveGroupsSyncHostNorm = getLobbyHostNameNorm() || '';
        setRoomGroupsSyncActive(true);
        syncSharedGroupsBridge();
      }
      return sent;
    }

    function broadcastSharedGroupsDesyncFromHost() {
      if (!isSelfLobbyHost()) return false;
      if (!canRunGroupsDesyncNow()) {
        addDesyncDisabledStatusNotice();
        return false;
      }
      if (groupsSyncTask) return false;
      const ok = sendLobbyChatMessage(buildGroupsSyncTransportMessage('[TBCGCLR]'));
      if (!ok) return false;
      groupsSyncLastSig = '';
      sharedHostGroupsSnapshot = [];
      setRoomGroupsSyncActive(false);
      syncSharedGroupsBridge();
      if (groupsPanelVisible) renderGroupsPanel();
      return true;
    }

    function queueGroupsPanelActionAutoSync() {
      if (groupsPanelAutoSyncTimer) {
        clearTimeout(groupsPanelAutoSyncTimer);
        groupsPanelAutoSyncTimer = null;
      }
      return;
    }

    function getGroupsPanelLobbyData(sourceGroups = colorGroups) {
      const info = getLobbyAccountInfo();
      const accountSet = info && info.accountSet ? info.accountSet : new Set();
      const guestSet = info && info.guestSet ? info.guestSet : new Set();
      const canFilterLobbyPresence = accountSet.size > 0 || guestSet.size > 0;
      const groups = Array.isArray(sourceGroups) ? sourceGroups : [];
      return groups
        .map((g) => {
          const name = String((g && g.name) || '').trim();
          const color = String((g && g.color) || '').trim();
          const playersRaw = Array.isArray(g && g.players) ? g.players : [];
          const players = playersRaw
            .map((p) => {
              const pName = typeof p === 'string' ? String(p || '').trim() : String((p && p.name) || '').trim();
              if (!pName) return null;
              const pNorm = normalizeName(pName);
              const pType = getGroupPlayerMemberType(p);
              if (canFilterLobbyPresence) {
                if (pType === 'guest' && !guestSet.has(pNorm)) return null;
                if (pType === 'account' && !accountSet.has(pNorm)) return null;
                if (pType === 'any' && !accountSet.has(pNorm) && !guestSet.has(pNorm)) return null;
              }
              const guestWarn = shouldShowGuestWarningForGroupMember(p, info);
              return { name: pName, guestWarn, memberType: pType };
            })
            .filter(Boolean);
          return { name, color, players };
        })
        .filter((g) => (g.players || []).length > 0);
    }

    function getHostGroupByName(groupName) {
      const n = normalizeName(groupName);
      return colorGroups.find((g) => normalizeName(g.name) === n) || null;
    }

    function openGroupsPanelPlayerManager(anchorEl, groupName, playerName, memberType = 'any') {
      if (!canHostEditSharedGroupsPanel()) {
        addLocalChatStatus('[TBC] Sync your groups first to enable player editing in Shared Groups.', 'rgb(181, 48, 48)');
        return;
      }

      const fromGroup = getHostGroupByName(groupName);
      if (!fromGroup) {
        addLocalChatStatus('[TBC] Source group not found.', 'rgb(181, 48, 48)');
        return;
      }

      openPanel(anchorEl, (panel) => {
        const moveTargets = colorGroups
          .filter((g) => g.id !== fromGroup.id)
          .map((g) => `<div class="mod_ctx_item" data-tbc-groups-move="${escapeHtml(g.id)}">${escapeHtml(g.name)}</div>`)
          .join('');

        panel.innerHTML = `
          <div class="mod_ctx_title">${escapeHtml(playerName)}</div>
          <div style="font-size:11px;opacity:.8;margin-top:2px;">Current group: ${escapeHtml(fromGroup.name)}</div>
          <div class="mod_ctx_items" style="margin-top:8px;">
            <div class="mod_ctx_item" data-tbc-groups-open-move="1">Move player</div>
            <div id="tbc_groups_move_targets" style="display:none;margin-top:4px;">
              ${moveTargets || '<div style="font-size:11px;opacity:.75;padding:4px 6px;">No other groups available.</div>'}
            </div>
            <div class="mod_ctx_item" data-tbc-groups-remove="1" style="color:#ff6b6b;">Remove from group</div>
          </div>
        `;

        const moveToggle = panel.querySelector('[data-tbc-groups-open-move="1"]');
        const moveTargetsHost = panel.querySelector('#tbc_groups_move_targets');
        if (moveToggle && moveTargetsHost) {
          moveToggle.addEventListener('click', () => {
            const opening = moveTargetsHost.style.display === 'none';
            moveTargetsHost.style.display = opening ? '' : 'none';
            moveToggle.textContent = opening ? 'Move player (hide list)' : 'Move player';
          });
        }

        panel.querySelectorAll('[data-tbc-groups-move]').forEach((btn) => {
          btn.addEventListener('click', () => {
            const toId = String(btn.getAttribute('data-tbc-groups-move') || '');
            const res = movePlayerToGroup(fromGroup.id, toId, playerName, memberType);
            if (!res || !res.ok) {
              addLocalChatStatus(`[TBC] ${res && res.error ? res.error : 'Move failed.'}`, 'rgb(181, 48, 48)');
              return;
            }
            closePanel();
            renderGroupsPanel();
            queueGroupsPanelActionAutoSync();
          });
        });

        const removeBtn = panel.querySelector('[data-tbc-groups-remove="1"]');
        if (removeBtn) {
          removeBtn.addEventListener('click', () => {
            removePlayerFromGroup(fromGroup.id, playerName, memberType);
            closePanel();
            renderGroupsPanel();
            queueGroupsPanelActionAutoSync();
          });
        }
      });
    }

    function openGroupsPanelGroupManager(anchorEl, groupName) {
      if (!canHostEditSharedGroupsPanel()) {
        addLocalChatStatus('[TBC] Sync your groups first to enable editing in Shared Groups.', 'rgb(181, 48, 48)');
        return;
      }

      const group = getHostGroupByName(groupName);
      if (!group) {
        addLocalChatStatus('[TBC] Group not found.', 'rgb(181, 48, 48)');
        return;
      }

      openPanel(anchorEl, (panel) => {
        panel.innerHTML = `
          <div class="mod_ctx_title">Group: ${escapeHtml(group.name)}</div>
          <div class="mod_ctx_items">
            <div class="mod_ctx_item" data-tbc-groups-group-add="1">Add player</div>
            <div class="mod_ctx_item" data-tbc-groups-group-rename="1">Rename group</div>
            <div class="mod_ctx_item" data-tbc-groups-group-delete="1" style="color:#ff6b6b;">Delete group</div>
          </div>
        `;

        const addBtn = panel.querySelector('[data-tbc-groups-group-add="1"]');
        if (addBtn) {
          addBtn.addEventListener('click', () => {
            closePanel();
            openPanel(anchorEl, (panel2) => {
              panel2.innerHTML = `
                <div class="mod_ctx_title">Add player to ${escapeHtml(group.name)}</div>
                <input class="mod_ctx_input" type="text" placeholder="Player name">
                <div class="mod_ctx_error" style="display:none;"></div>
                <div class="mod_ctx_buttons">
                  <div class="mod_ctx_button mod_ctx_button_primary">Add</div>
                  <div class="mod_ctx_button">Cancel</div>
                </div>
              `;

              const input = panel2.querySelector('.mod_ctx_input');
              const errEl = panel2.querySelector('.mod_ctx_error');
              const confirmBtn = panel2.querySelector('.mod_ctx_button_primary');
              const cancelBtn = panel2.querySelectorAll('.mod_ctx_button')[1];

              if (confirmBtn) {
                confirmBtn.addEventListener('click', () => {
                  if (errEl) {
                    errEl.style.display = 'none';
                    errEl.textContent = '';
                  }
                  const res = addPlayerToGroup(group.id, input ? input.value : '');
                  if (!res || !res.ok) {
                    if (errEl) {
                      errEl.textContent = (res && res.error) ? res.error : 'Could not add player.';
                      errEl.style.display = 'block';
                    }
                    return;
                  }
                  closePanel();
                  renderGroupsPanel();
                  queueGroupsPanelActionAutoSync();
                });
              }

              if (cancelBtn) cancelBtn.addEventListener('click', () => closePanel());
              if (input) input.focus();
            });
          });
        }

        const renameBtn = panel.querySelector('[data-tbc-groups-group-rename="1"]');
        if (renameBtn) {
          renameBtn.addEventListener('click', () => {
            closePanel();
            openPanel(anchorEl, (panel2) => {
              panel2.innerHTML = `
                <div class="mod_ctx_title">Rename group</div>
                <input class="mod_ctx_input" type="text" value="${escapeHtml(group.name)}">
                <div class="mod_ctx_buttons">
                  <div class="mod_ctx_button mod_ctx_button_primary">Save</div>
                  <div class="mod_ctx_button">Cancel</div>
                </div>
              `;
              const input = panel2.querySelector('.mod_ctx_input');
              const saveBtn = panel2.querySelector('.mod_ctx_button_primary');
              const cancelBtn = panel2.querySelectorAll('.mod_ctx_button')[1];
              if (saveBtn) {
                saveBtn.addEventListener('click', () => {
                  const next = String((input && input.value) || '').trim();
                  if (!next) return;
                  renameGroup(group.id, next);
                  closePanel();
                  renderGroupsPanel();
                  queueGroupsPanelActionAutoSync();
                });
              }
              if (cancelBtn) cancelBtn.addEventListener('click', () => closePanel());
              if (input) {
                input.focus();
                input.select();
              }
            });
          });
        }

        const deleteBtn = panel.querySelector('[data-tbc-groups-group-delete="1"]');
        if (deleteBtn) {
          deleteBtn.addEventListener('click', () => {
            closePanel();
            openPanel(anchorEl, (panel2) => {
              panel2.innerHTML = `
                <div class="mod_ctx_title">Delete group?</div>
                <div style="font-size:11px;opacity:0.8;margin-top:2px;">This will remove the group and all player assignments.</div>
                <div class="mod_ctx_buttons">
                  <div class="mod_ctx_button mod_ctx_button_primary" style="background:rgba(255,65,65,0.7);border-color:rgba(255,65,65,0.9);">Delete</div>
                  <div class="mod_ctx_button">Cancel</div>
                </div>
              `;
              const confirmBtn = panel2.querySelector('.mod_ctx_button_primary');
              const cancelBtn = panel2.querySelectorAll('.mod_ctx_button')[1];
              if (confirmBtn) {
                confirmBtn.addEventListener('click', () => {
                  deleteGroup(group.id);
                  closePanel();
                  renderGroupsPanel();
                  queueGroupsPanelActionAutoSync();
                });
              }
              if (cancelBtn) cancelBtn.addEventListener('click', () => closePanel());
            });
          });
        }
      });
    }

    function getPanelPlacementState(kind) {
      return kind === 'points' ? pointsPanelPlacement : groupsPanelPlacement;
    }

    function clampPanelCoords(left, top, width, height) {
      const maxLeft = Math.max(8, window.innerWidth - width - 8);
      const maxTop = Math.max(8, window.innerHeight - height - 8);
      return {
        left: Math.min(maxLeft, Math.max(8, Math.round(left))),
        top: Math.min(maxTop, Math.max(8, Math.round(top))),
      };
    }

    function rectsOverlap(a, b, gap = 0) {
      if (!a || !b) return false;
      return (
        a.left < (b.right + gap) &&
        (a.right + gap) > b.left &&
        a.top < (b.bottom + gap) &&
        (a.bottom + gap) > b.top
      );
    }

    function getVisiblePanelRect(kind) {
      const id = kind === 'points' ? 'tbc_points_panel' : 'tbc_groups_panel';
      const panel = $(id);
      if (!panel || panel.style.display === 'none' || panel.getAttribute('aria-hidden') === 'true') return null;
      const r = panel.getBoundingClientRect();
      if (!Number.isFinite(r.width) || !Number.isFinite(r.height) || r.width < 20 || r.height < 20) return null;
      return {
        left: Math.round(r.left),
        top: Math.round(r.top),
        right: Math.round(r.right),
        bottom: Math.round(r.bottom),
        width: Math.round(r.width),
        height: Math.round(r.height),
      };
    }

    function getPanelSizeEstimate(kind) {
      const placement = getPanelPlacementState(kind);
      const fallbackWidth = kind === 'points' ? 300 : 248;
      const width = Math.max(kind === 'points' ? 240 : 200, Math.round(placement.width || fallbackWidth));
      const maxHeight = Math.max(220, Math.round(placement.maxHeight || 560));
      const panelRect = getVisiblePanelRect(kind);
      const height = panelRect
        ? Math.max(220, Math.round(panelRect.height))
        : Math.max(220, Math.min(maxHeight, window.innerHeight - 16));
      return { width, height };
    }

    function resolveDragPanelNoOverlap(kind, left, top, width, height) {
      const otherKind = kind === 'points' ? 'groups' : 'points';
      const obstacle = getVisiblePanelRect(otherKind);
      if (!obstacle) return clampPanelCoords(left, top, width, height);

      const desired = clampPanelCoords(left, top, width, height);
      const desiredRect = {
        left: desired.left,
        top: desired.top,
        right: desired.left + width,
        bottom: desired.top + height,
      };
      if (!rectsOverlap(desiredRect, obstacle, 2)) return desired;

      const gap = 10;
      const candidateSeeds = [
        { left: desired.left, top: obstacle.bottom + gap },
        { left: desired.left, top: obstacle.top - height - gap },
        { left: obstacle.right + gap, top: desired.top },
        { left: obstacle.left - width - gap, top: desired.top },
      ];

      let best = null;
      let bestDist = Infinity;
      candidateSeeds.forEach((seed) => {
        const clamped = clampPanelCoords(seed.left, seed.top, width, height);
        const rect = {
          left: clamped.left,
          top: clamped.top,
          right: clamped.left + width,
          bottom: clamped.top + height,
        };
        if (rectsOverlap(rect, obstacle, 2)) return;
        const dist = Math.abs(clamped.left - desired.left) + Math.abs(clamped.top - desired.top);
        if (dist < bestDist) {
          bestDist = dist;
          best = clamped;
        }
      });

      if (best) return best;
      return desired;
    }

    function applySlashPanelsSpawnLayout(primaryKind = 'groups', includePoints = false) {
      const showGroups = !!groupsPanelVisible;
      const showPoints = !!pointsPanelVisible && (includePoints || primaryKind === 'points');
      if (!showGroups && !showPoints) return;

      const gap = 10;
      const baseLeft = 12;
      const baseTop = 120;

      const placeKind = primaryKind === 'points' ? 'points' : 'groups';
      const secondaryKind = placeKind === 'groups' ? 'points' : 'groups';

      const placeOne = (kind, left, top) => {
        const size = getPanelSizeEstimate(kind);
        const clamped = clampPanelCoords(left, top, size.width, size.height);
        const placement = getPanelPlacementState(kind);
        placement.left = clamped.left;
        placement.top = clamped.top;
        placement.width = size.width;
        const maxAllowedHeight = Math.max(220, window.innerHeight - clamped.top - 8);
        placement.maxHeight = Math.max(220, Math.min(placement.maxHeight || size.height, maxAllowedHeight));
        if (kind === 'groups') groupsPanelPinnedByUser = true;
        else pointsPanelPinnedByUser = true;
        return { left: clamped.left, top: clamped.top, right: clamped.left + size.width, bottom: clamped.top + size.height, width: size.width, height: size.height };
      };

      const placeSecondaryAround = (kind, anchorRect) => {
        const size = getPanelSizeEstimate(kind);
        const belowTop = anchorRect.bottom + gap;
        const aboveTop = anchorRect.top - size.height - gap;
        const rightLeft = anchorRect.right + gap;
        if (belowTop + size.height + 8 <= window.innerHeight) {
          return placeOne(kind, anchorRect.left, belowTop);
        }
        if (aboveTop >= 8) {
          return placeOne(kind, anchorRect.left, aboveTop);
        }
        if (rightLeft + size.width + 8 <= window.innerWidth) {
          return placeOne(kind, rightLeft, anchorRect.top);
        }
        return placeOne(kind, baseLeft, baseTop);
      };

      const firstVisible = (placeKind === 'groups' ? showGroups : showPoints);
      let firstRect = null;
      if (firstVisible) {
        firstRect = placeOne(placeKind, baseLeft, baseTop);
      }

      const secondVisible =
        secondaryKind === 'groups' ? showGroups : showPoints;
      if (secondVisible) {
        if (!firstRect) {
          firstRect = placeOne(secondaryKind, baseLeft, baseTop);
        } else {
          placeSecondaryAround(secondaryKind, firstRect);
        }
      }

      if (groupsPanelVisible) positionGroupsPanel();
      if (pointsPanelVisible) positionPointsPanel();
    }

    function ensureGroupsPanelElement() {
      let panel = $('tbc_groups_panel');
      if (panel) return panel;
      const root = document.body;
      if (!root) return null;
      panel = document.createElement('div');
      panel.id = 'tbc_groups_panel';
      panel.style.display = 'none';
      panel.setAttribute('aria-hidden', 'true');
      root.appendChild(panel);
      panel.addEventListener('click', (e) => {
        e.stopPropagation();
        const target = e.target && e.target.closest ? e.target.closest('[data-tbc-groups-action]') : null;
        if (target) {
          const action = target.getAttribute('data-tbc-groups-action');
          if (action === 'close-panel') {
            setGroupsPanelVisible(false);
            return;
          }
          if (action === 'sync-now') {
            if (!isSelfLobbyHost()) {
              addLocalChatStatus('[TBC] Only the room host can trigger sync.', 'rgb(181, 48, 48)');
              return;
            }
            if (!canRunGroupsSyncNow()) {
              addSyncDisabledStatusNotice();
              return;
            }
            uploadGroupsRelaySnapshot({ announceRelay: true, silent: true })
              .then((ok) => {
                if (ok) {
                  addLocalChatStatus('* Groups are now synced.');
                  setTimeout(() => closeIngameChatInputByEnterTap(1), 70);
                } else {
                  addLocalChatStatus('[TBC] Sync failed. Groups were not shared.', 'rgb(181, 48, 48)');
                }
              })
              .catch(() => {
                addLocalChatStatus('[TBC] Sync failed. Groups were not shared.', 'rgb(181, 48, 48)');
              });
          } else if (action === 'desync-now') {
            if (!isSelfLobbyHost()) {
              addLocalChatStatus('[TBC] Only the room host can trigger desync.', 'rgb(181, 48, 48)');
              return;
            }
            if (!canRunGroupsDesyncNow()) {
              addDesyncDisabledStatusNotice();
              return;
            }
            if (!roomGroupsSyncActive) {
              addLocalChatStatus('[TBC] Sync first before desync.', 'rgb(181, 48, 48)');
              return;
            }
            const ok = broadcastSharedGroupsDesyncFromHost();
            if (ok) {
              addLocalChatStatus('* Groups are now desynced.');
              setTimeout(() => closeIngameChatInputByEnterTap(2), 70);
            }
            else addLocalChatStatus('[TBC] Could not send desync signal.', 'rgb(181, 48, 48)');
          }
        }

        const playerBtn = e.target && e.target.closest ? e.target.closest('[data-tbc-groups-player-action]') : null;
        if (playerBtn) {
          if (!canHostEditSharedGroupsPanel()) {
            addLocalChatStatus('[TBC] Sync your groups first to enable player editing in Shared Groups.', 'rgb(181, 48, 48)');
            return;
          }
          const playerName = String(playerBtn.getAttribute('data-tbc-groups-player') || '').trim();
          const groupName = String(playerBtn.getAttribute('data-tbc-groups-group') || '').trim();
          const memberType = normalizeMemberType(String(playerBtn.getAttribute('data-tbc-groups-member-type') || 'any'));
          if (!playerName || !groupName) return;
          openGroupsPanelPlayerManager(playerBtn, groupName, playerName, memberType);
        }

        const groupBtn = e.target && e.target.closest ? e.target.closest('[data-tbc-groups-group-action]') : null;
        if (groupBtn) {
          if (!canHostEditSharedGroupsPanel()) {
            addLocalChatStatus('[TBC] Sync your groups first to enable group editing.', 'rgb(181, 48, 48)');
            return;
          }
          const groupName = String(groupBtn.getAttribute('data-tbc-groups-group') || '').trim();
          if (!groupName) return;
          openGroupsPanelGroupManager(groupBtn, groupName);
        }
      });
      const onMove = () => {
        if (!groupsPanelVisible) return;
        positionGroupsPanel();
      };
      window.addEventListener('resize', onMove);
      window.addEventListener('scroll', onMove, true);
      panel.addEventListener('pointerdown', (e) => {
        const closeBtn = e.target && e.target.closest ? e.target.closest('[data-tbc-groups-action="close-panel"]') : null;
        if (closeBtn) return;
        const handle = e.target && e.target.closest ? e.target.closest('[data-tbc-groups-drag-handle="1"]') : null;
        if (!handle) return;
        if (e.pointerType === 'mouse' && e.button !== 0) return;
        const rect = panel.getBoundingClientRect();
        groupsPanelPinnedByUser = true;
        groupsPanelDragState.active = true;
        groupsPanelDragState.pointerId = e.pointerId;
        groupsPanelDragState.offsetX = e.clientX - rect.left;
        groupsPanelDragState.offsetY = e.clientY - rect.top;
        panel.classList.add('tbc_groups_panel_dragging');
        try {
          if (handle.setPointerCapture) handle.setPointerCapture(e.pointerId);
        } catch (_) {}
        e.preventDefault();
      });
      const endDrag = () => {
        if (!groupsPanelDragState.active) return;
        groupsPanelDragState.active = false;
        groupsPanelDragState.pointerId = null;
        panel.classList.remove('tbc_groups_panel_dragging');
      };
      window.addEventListener('pointermove', (e) => {
        if (!groupsPanelDragState.active) return;
        if (groupsPanelDragState.pointerId !== null && e.pointerId !== groupsPanelDragState.pointerId) return;
        const width = panel.offsetWidth || groupsPanelPlacement.width || 248;
        const height = panel.offsetHeight || 320;
        const maxLeft = Math.max(8, window.innerWidth - width - 8);
        const maxTop = Math.max(8, window.innerHeight - height - 8);
        const nextLeft = Math.min(maxLeft, Math.max(8, Math.round(e.clientX - groupsPanelDragState.offsetX)));
        const nextTop = Math.min(maxTop, Math.max(8, Math.round(e.clientY - groupsPanelDragState.offsetY)));
        const adjusted = resolveDragPanelNoOverlap('groups', nextLeft, nextTop, width, height);
        groupsPanelPlacement.left = adjusted.left;
        groupsPanelPlacement.top = adjusted.top;
        const maxAllowedHeight = Math.max(220, window.innerHeight - groupsPanelPlacement.top - 8);
        groupsPanelPlacement.maxHeight = Math.max(220, Math.min(groupsPanelPlacement.maxHeight || maxAllowedHeight, maxAllowedHeight));
        panel.style.left = `${groupsPanelPlacement.left}px`;
        panel.style.top = `${groupsPanelPlacement.top}px`;
        panel.style.maxHeight = `${groupsPanelPlacement.maxHeight}px`;
        e.preventDefault();
      });
      window.addEventListener('pointerup', endDrag);
      window.addEventListener('pointercancel', endDrag);
      return panel;
    }

    function positionGroupsPanel() {
      const panel = $('tbc_groups_panel');
      if (!panel) return;

      const lobby = $('newbonklobby');
      const playerbox = $('newbonklobby_playerbox');
      const settingsbox = $('newbonklobby_settingsbox');

      const lobbyRect = lobby ? lobby.getBoundingClientRect() : null;
      const playerRect = playerbox ? playerbox.getBoundingClientRect() : null;
      const settingsRect = settingsbox ? settingsbox.getBoundingClientRect() : null;

      const hasAnchorRect =
        !!lobbyRect &&
        Number.isFinite(lobbyRect.left) &&
        Number.isFinite(lobbyRect.top) &&
        lobbyRect.width > 10 &&
        lobbyRect.height > 10;

      if (hasAnchorRect && !groupsPanelPinnedByUser) {
        const desiredWidth = 248;
        const anchorTop = (playerRect && playerRect.height > 10) ? playerRect.top : lobbyRect.top + 72;
        const anchorBottom = (settingsRect && settingsRect.height > 10) ? settingsRect.bottom : (lobbyRect.bottom - 10);
        const desiredHeight = Math.max(240, Math.round(anchorBottom - anchorTop));
        const left = 12;
        const top = Math.max(8, Math.round(anchorTop));
        groupsPanelPlacement.left = left;
        groupsPanelPlacement.top = top;
        groupsPanelPlacement.width = desiredWidth;
        groupsPanelPlacement.maxHeight = Math.max(220, Math.min(desiredHeight, window.innerHeight - top - 8));
      }

      const safeWidth = Math.max(200, Number.isFinite(groupsPanelPlacement.width) ? Math.round(groupsPanelPlacement.width) : 248);
      groupsPanelPlacement.width = safeWidth;
      const panelRect = panel.getBoundingClientRect();
      const panelHeight = panelRect && panelRect.height > 10
        ? panelRect.height
        : Math.max(220, Math.min(Number.isFinite(groupsPanelPlacement.maxHeight) ? groupsPanelPlacement.maxHeight : 560, window.innerHeight - 16));
      const maxLeft = Math.max(8, window.innerWidth - safeWidth - 8);
      const maxTop = Math.max(8, window.innerHeight - panelHeight - 8);
      groupsPanelPlacement.left = Math.min(maxLeft, Math.max(8, Math.round(groupsPanelPlacement.left || 12)));
      groupsPanelPlacement.top = Math.min(maxTop, Math.max(8, Math.round(groupsPanelPlacement.top || 120)));
      const maxAllowedHeight = Math.max(220, window.innerHeight - groupsPanelPlacement.top - 8);
      groupsPanelPlacement.maxHeight = Math.max(220, Math.min(Number.isFinite(groupsPanelPlacement.maxHeight) ? groupsPanelPlacement.maxHeight : maxAllowedHeight, maxAllowedHeight));

      panel.style.left = `${groupsPanelPlacement.left}px`;
      panel.style.top = `${groupsPanelPlacement.top}px`;
      panel.style.width = `${groupsPanelPlacement.width}px`;
      panel.style.maxHeight = `${groupsPanelPlacement.maxHeight}px`;
    }

    function renderGroupsPanel() {
      const panel = ensureGroupsPanelElement();
      if (!panel) return;
      const hostView = isSelfLobbyHost();
      const hostCanEdit = canHostEditSharedGroupsPanel();
      const hostCanSyncNow = hostView && !groupsSyncTask && canRunGroupsSyncNow();
      const hostCanDesyncNow = hostView && !groupsSyncTask && roomGroupsSyncActive && canRunGroupsDesyncNow();
      const groups = hostView
        ? getGroupsPanelLobbyData(colorGroups)
        : getGroupsPanelLobbyData(sharedHostGroupsSnapshot);

      const groupsHtml = groups.length
        ? groups
            .map((g) => {
              const canManage = hostCanEdit;
              const players = g.players
                .map((p) => {
                  const pName = String((p && p.name) || '').trim();
                  const guestWarn = !!(p && p.guestWarn);
                  const memberType = normalizeMemberType(String((p && p.memberType) || 'any'));
                  const safeName = escapeHtml(pName);
                  const safeMemberType = escapeHtml(memberType);
                  const guestWarnHtml = guestWarn
                    ? '<span class="tbc_guest_warn_badge" title="Temporary guest member. Removed when guest/room/host state changes." aria-label="Temporary guest member"></span>'
                    : '';
                  const safeGroup = escapeHtml(g.name);
                  const hostBtns = hostView
                    ? `
                      <div class="tbc_groups_player_actions">
                        <button class="tbc_groups_player_btn" data-tbc-groups-player-action="manage" data-tbc-groups-player="${safeName}" data-tbc-groups-group="${safeGroup}" data-tbc-groups-member-type="${safeMemberType}" title="${canManage ? 'Manage player' : 'Sync first to enable editing'}">⋮</button>
                      </div>
                    `
                    : '';
                  return `
                    <div class="tbc_groups_player${hostView ? ' has-actions' : ''}"${hostView ? ` data-tbc-groups-player-row="1" data-tbc-groups-player="${safeName}" data-tbc-groups-group="${safeGroup}" data-tbc-groups-member-type="${safeMemberType}"` : ''}>
                      <span class="tbc_groups_player_name">${safeName}${guestWarnHtml}</span>
                      ${hostBtns}
                    </div>
                  `;
                })
                .join('');
              return `
                <div class="tbc_groups_card">
                  <div class="tbc_groups_card_top">
                    <span class="tbc_groups_dot" style="background:${escapeHtml(g.color)}"></span>
                    <span class="tbc_groups_name">${escapeHtml(g.name)}</span>
                    ${hostView ? `<button class="tbc_groups_group_btn" data-tbc-groups-group-action="manage" data-tbc-groups-group="${escapeHtml(g.name)}" title="${hostCanEdit ? 'Manage group' : 'Sync first to enable editing'}">⋮</button>` : ''}
                    <span class="tbc_groups_count">${g.players.length}</span>
                  </div>
                  <div class="tbc_groups_players">${players || '<span class="tbc_groups_empty">No players</span>'}</div>
                </div>
              `;
            })
            .join('')
        : `<div class="tbc_groups_empty">${roomGroupsSyncActive ? 'Host synced with no groups.' : 'No shared groups synced yet.'}</div>`;

      panel.innerHTML = `
        <div class="tbc_groups_window windowShadow">
          <div class="tbc_groups_top" data-tbc-groups-drag-handle="1">
            <span class="tbc_groups_drag_grip" aria-hidden="true">
              <span class="tbc_groups_drag_grip_dot"></span><span class="tbc_groups_drag_grip_dot"></span><span class="tbc_groups_drag_grip_dot"></span>
              <span class="tbc_groups_drag_grip_dot"></span><span class="tbc_groups_drag_grip_dot"></span><span class="tbc_groups_drag_grip_dot"></span>
              <span class="tbc_groups_drag_grip_dot"></span><span class="tbc_groups_drag_grip_dot"></span><span class="tbc_groups_drag_grip_dot"></span>
            </span>
            <span class="tbc_groups_top_title">Shared Groups</span>
            <button class="tbc_groups_close_btn" data-tbc-groups-action="close-panel" title="Close" aria-label="Close panel">x</button>
          </div>
          <div class="tbc_groups_body">
            <div class="tbc_groups_head">
              <div class="tbc_groups_sub">Showing host groups for players currently in lobby (including temporary guests).</div>
              <div class="tbc_groups_actions">
                <button class="tbc_groups_btn" data-tbc-groups-action="sync-now"${hostCanSyncNow ? '' : ' disabled'}>${groupsSyncTask ? `Syncing... ${groupsSyncTask.sent}/${groupsSyncTask.total}` : 'Sync'}</button>
                <button class="tbc_groups_btn" data-tbc-groups-action="desync-now"${hostCanDesyncNow ? '' : ' disabled'}>Desync</button>
              </div>
            </div>
            <div class="tbc_groups_hint">${hostView && !hostCanEdit ? 'Sync your groups once to enable editing in Shared Groups.' : 'Auto-updates on join/leave are local. Only manual Sync sends chat sync data.'}</div>
            <div class="tbc_groups_list">${groupsHtml}</div>
          </div>
        </div>
      `;
      const closeBtnEl = panel.querySelector('[data-tbc-groups-action="close-panel"]');
      const settingsCloseEl = $('settings_close');
      if (closeBtnEl && settingsCloseEl) {
        closeBtnEl.className = settingsCloseEl.className || closeBtnEl.className;
        closeBtnEl.classList.add('tbc_groups_close_btn');
      }
      positionGroupsPanel();
      panel.setAttribute('aria-hidden', groupsPanelVisible ? 'false' : 'true');
    }

    function setGroupsPanelVisible(visible) {
      groupsPanelVisible = !!visible;
      const panel = ensureGroupsPanelElement();
      if (!panel) return;
      if (!groupsPanelVisible) {
        panel.style.display = 'none';
        panel.setAttribute('aria-hidden', 'true');
        return;
      }
      renderGroupsPanel();
      panel.style.display = '';
      panel.setAttribute('aria-hidden', 'false');
    }

    function clonePointsEntries(entries) {
      return (Array.isArray(entries) ? entries : [])
        .map((e) => ({ name: String((e && e.name) || '').trim(), score: Number((e && e.score) || 0) }))
        .filter((e) => e.name);
    }

    function collectScoreEntriesFromStatusMessages() {
      const readRows = (host) => Array.from((host && host.children) ? host.children : [])
        .map((row) => {
          const txt = String((row && (row.textContent || row.innerText)) || '').replace(/\s+/g, ' ').trim();
          return txt;
        })
        .filter(Boolean);

      const ingameRows = readRows($('ingamechatcontent'));
      const lobbyRows = readRows($('newbonklobby_chat_content'));
      const rows = ingameRows.concat(lobbyRows);
      if (!rows.length) return [];

      const map = new Map();
      rows.forEach((raw) => {
        const text = String(raw || '').trim();
        if (!text) return;
        if (/^\*\s*game starting in\s+\d+/i.test(text)) {
          map.clear();
          return;
        }
        const scoreMatch = text.match(/^\*\s*(.+?)\s+scores!?$/i);
        if (!scoreMatch) return;
        const name = canonicalizeWinnerName(extractChatName(String(scoreMatch[1] || '').trim()));
        const norm = normalizeWinnerLookupName(name);
        if (!name || !norm) return;
        const prev = map.get(norm) || { name, score: 0 };
        prev.score += 1;
        map.set(norm, prev);
      });
      return Array.from(map.values())
        .map((e) => ({ name: String(e.name || '').trim(), score: Number(e.score) || 0 }))
        .filter((e) => e.name);
    }

    function getPointsPanelSourceEntries() {
      const rendererVisible = isElementActuallyVisibleSafe($('gamerenderer'));
      const winnerVisible = isElementActuallyVisibleSafe($('ingamewinner'));
      const canSampleLive = rendererVisible && winnerVisible;
      const roomSynced = !!window.tbcRoomGroupsSyncActive;

      if (canSampleLive) {
        const left = document.getElementById('ingamewinner_scores_left');
        const right = document.getElementById('ingamewinner_scores_right');
        const parsed = left ? parseWinnerEntriesFromDom(left, right) : [];
        const parsedHasScoreRows = Array.isArray(parsed) && parsed.some((e) => !isLikelyTeamWinnerName((e && e.name) || ''));
        let source = clonePointsEntries(winnerSourceEntries);
        if (!source.length || (!roomSynced && parsedHasScoreRows)) source = clonePointsEntries(parsed);
        if (roomSynced && !source.length) source = clonePointsEntries(parsed);
        const statusEntries = collectScoreEntriesFromStatusMessages();
        if (statusEntries.length) {
          const statusMap = new Map(statusEntries.map((e) => [normalizeWinnerLookupName(e.name), e]));
          if (source.length) {
            const seen = new Set();
            source = source.map((e) => {
              const key = normalizeWinnerLookupName(e.name);
              seen.add(key);
              const statusRow = statusMap.get(key);
              if (!statusRow) return e;
              return { name: e.name, score: Math.max(Number(e.score) || 0, Number(statusRow.score) || 0) };
            });
            statusEntries.forEach((e) => {
              const key = normalizeWinnerLookupName(e.name);
              if (!seen.has(key)) source.push({ name: e.name, score: Number(e.score) || 0 });
            });
          } else {
            source = clonePointsEntries(statusEntries);
          }
        }
        if (source.length) {
          pointsPanelCachedSourceEntries = source;
        }
      }

      return clonePointsEntries(pointsPanelCachedSourceEntries);
    }

    function getGroupedPointsEntries(sourceEntries) {
      const sharedSyncActive = !!window.tbcRoomGroupsSyncActive;
      if (!sharedSyncActive) return [];
      const grouped = buildGroupedWinnerEntries(sourceEntries);
      if (Array.isArray(grouped) && grouped.length) return grouped;

      const rows = Array.isArray(sourceEntries) ? sourceEntries : [];
      const nonTeamRows = rows.filter((r) => r && !isLikelyTeamWinnerName(String(r.name || '')));
      if (!nonTeamRows.length) return [];
      const snapshot = normalizeSharedGroupSnapshot(window.tbcSharedGroupsSnapshot || []);
      const buckets = new Map();
      snapshot.forEach((g) => {
        const key = normalizeWinnerLookupName(g && g.name);
        if (!key) return;
        if (!buckets.has(key)) buckets.set(key, []);
        buckets.get(key).push(g);
      });
      const pickedCount = new Map();
      const out = nonTeamRows.map((r, idx) => {
        const rowName = String((r && r.name) || '').trim();
        const key = normalizeWinnerLookupName(rowName);
        const bucket = key ? (buckets.get(key) || []) : [];
        const used = key ? (pickedCount.get(key) || 0) : 0;
        const picked = bucket[used] || null;
        if (key) pickedCount.set(key, used + 1);
        return {
          name: rowName || `Group ${idx + 1}`,
          score: Number((r && r.score) || 0),
          color: String((picked && picked.color) || ''),
          contributors: [],
        };
      });
      return out;
    }

    function buildPointsPlayersHtml(entries, colorResolver = null) {
      if (!entries.length) return '<div class="tbc_points_empty">No score data yet.</div>';
      return entries
        .map((entry) => {
          const name = String(entry.name || '').trim();
          const score = Number(entry.score) || 0;
          const color = colorResolver
            ? String(colorResolver(name, entry) || '')
            : (getWinnerBoardColorForName(name) || '');
          const dotStyle = color ? ` style="background:${escapeHtml(color)}"` : '';
          return `
            <div class="tbc_points_row">
              <span class="tbc_points_dot"${dotStyle}></span>
              <span class="tbc_points_name">${escapeHtml(name)}</span>
              <span class="tbc_points_score">${score}</span>
            </div>
          `;
        })
        .join('');
    }

    function buildPointsGroupsHtml(grouped) {
      if (!grouped.length) return '<div class="tbc_points_empty">No grouped score data yet.</div>';
      return grouped
        .map((group) => {
          const gName = String(group.name || '').trim() || 'Group';
          const gScore = Number(group.score) || 0;
          const gColor = String(group.color || '').trim();
          const dotStyle = gColor ? ` style="background:${escapeHtml(gColor)}"` : '';
          const contributors = Array.isArray(group.contributors) ? group.contributors : [];
          const contribHtml = contributors.length
            ? contributors
                .map((c) => {
                  const cName = String((c && c.name) || '').trim();
                  const cScore = Number((c && c.score) || 0);
                  return `
                    <div class="tbc_points_contrib_row">
                      <span class="tbc_points_contrib_name">${escapeHtml(cName)}</span>
                      <span class="tbc_points_contrib_score">${cScore}</span>
                    </div>
                  `;
                })
                .join('')
            : '<div class="tbc_points_empty">No contributors</div>';
          return `
            <div class="tbc_points_group_card">
              <div class="tbc_points_row">
                <span class="tbc_points_dot"${dotStyle}></span>
                <span class="tbc_points_name">${escapeHtml(gName)}</span>
                <span class="tbc_points_score">${gScore}</span>
              </div>
              <div class="tbc_points_contribs">${contribHtml}</div>
            </div>
          `;
        })
        .join('');
    }

    function ensurePointsPanelElement() {
      let panel = $('tbc_points_panel');
      if (panel) return panel;
      const root = document.body;
      if (!root) return null;
      panel = document.createElement('div');
      panel.id = 'tbc_points_panel';
      panel.style.display = 'none';
      panel.setAttribute('aria-hidden', 'true');
      root.appendChild(panel);
      panel.addEventListener('click', (e) => {
        e.stopPropagation();
        const target = e.target && e.target.closest ? e.target.closest('[data-tbc-points-action]') : null;
        if (!target) return;
        const action = target.getAttribute('data-tbc-points-action');
        if (action === 'close-panel') setPointsPanelVisible(false);
      });
      const onMove = () => {
        if (!pointsPanelVisible) return;
        positionPointsPanel();
      };
      window.addEventListener('resize', onMove);
      window.addEventListener('scroll', onMove, true);
      panel.addEventListener('pointerdown', (e) => {
        const closeBtn = e.target && e.target.closest ? e.target.closest('[data-tbc-points-action="close-panel"]') : null;
        if (closeBtn) return;
        const handle = e.target && e.target.closest ? e.target.closest('[data-tbc-points-drag-handle="1"]') : null;
        if (!handle) return;
        if (e.pointerType === 'mouse' && e.button !== 0) return;
        const rect = panel.getBoundingClientRect();
        pointsPanelPinnedByUser = true;
        pointsPanelDragState.active = true;
        pointsPanelDragState.pointerId = e.pointerId;
        pointsPanelDragState.offsetX = e.clientX - rect.left;
        pointsPanelDragState.offsetY = e.clientY - rect.top;
        panel.classList.add('tbc_points_panel_dragging');
        try {
          if (handle.setPointerCapture) handle.setPointerCapture(e.pointerId);
        } catch (_) {}
        e.preventDefault();
      });
      const endDrag = () => {
        if (!pointsPanelDragState.active) return;
        pointsPanelDragState.active = false;
        pointsPanelDragState.pointerId = null;
        panel.classList.remove('tbc_points_panel_dragging');
      };
      window.addEventListener('pointermove', (e) => {
        if (!pointsPanelDragState.active) return;
        if (pointsPanelDragState.pointerId !== null && e.pointerId !== pointsPanelDragState.pointerId) return;
        const width = panel.offsetWidth || pointsPanelPlacement.width || 300;
        const height = panel.offsetHeight || 320;
        const maxLeft = Math.max(8, window.innerWidth - width - 8);
        const maxTop = Math.max(8, window.innerHeight - height - 8);
        const nextLeft = Math.min(maxLeft, Math.max(8, Math.round(e.clientX - pointsPanelDragState.offsetX)));
        const nextTop = Math.min(maxTop, Math.max(8, Math.round(e.clientY - pointsPanelDragState.offsetY)));
        const adjusted = resolveDragPanelNoOverlap('points', nextLeft, nextTop, width, height);
        pointsPanelPlacement.left = adjusted.left;
        pointsPanelPlacement.top = adjusted.top;
        const maxAllowedHeight = Math.max(220, window.innerHeight - pointsPanelPlacement.top - 8);
        pointsPanelPlacement.maxHeight = Math.max(220, Math.min(pointsPanelPlacement.maxHeight || maxAllowedHeight, maxAllowedHeight));
        panel.style.left = `${pointsPanelPlacement.left}px`;
        panel.style.top = `${pointsPanelPlacement.top}px`;
        panel.style.maxHeight = `${pointsPanelPlacement.maxHeight}px`;
        e.preventDefault();
      });
      window.addEventListener('pointerup', endDrag);
      window.addEventListener('pointercancel', endDrag);
      return panel;
    }

    function positionPointsPanel() {
      const panel = $('tbc_points_panel');
      if (!panel) return;
      const lobby = $('newbonklobby');
      const playerbox = $('newbonklobby_playerbox');
      const settingsbox = $('newbonklobby_settingsbox');
      const lobbyRect = lobby ? lobby.getBoundingClientRect() : null;
      const playerRect = playerbox ? playerbox.getBoundingClientRect() : null;
      const settingsRect = settingsbox ? settingsbox.getBoundingClientRect() : null;
      const hasAnchorRect =
        !!lobbyRect &&
        Number.isFinite(lobbyRect.left) &&
        Number.isFinite(lobbyRect.top) &&
        lobbyRect.width > 10 &&
        lobbyRect.height > 10;

      if (hasAnchorRect && !pointsPanelPinnedByUser) {
        const desiredWidth = 300;
        const anchorTop = (playerRect && playerRect.height > 10) ? playerRect.top : lobbyRect.top + 72;
        const anchorBottom = (settingsRect && settingsRect.height > 10) ? settingsRect.bottom : (lobbyRect.bottom - 10);
        const desiredHeight = Math.max(240, Math.round(anchorBottom - anchorTop));
        pointsPanelPlacement.left = Math.max(8, Math.round((groupsPanelPlacement.left || 12) + (groupsPanelPlacement.width || 248) + 10));
        pointsPanelPlacement.top = Math.max(8, Math.round(anchorTop));
        pointsPanelPlacement.width = desiredWidth;
        pointsPanelPlacement.maxHeight = Math.max(220, Math.min(desiredHeight, window.innerHeight - pointsPanelPlacement.top - 8));
      }

      const safeWidth = Math.max(240, Number.isFinite(pointsPanelPlacement.width) ? Math.round(pointsPanelPlacement.width) : 300);
      pointsPanelPlacement.width = safeWidth;
      const panelRect = panel.getBoundingClientRect();
      const panelHeight = panelRect && panelRect.height > 10
        ? panelRect.height
        : Math.max(220, Math.min(Number.isFinite(pointsPanelPlacement.maxHeight) ? pointsPanelPlacement.maxHeight : 560, window.innerHeight - 16));
      const maxLeft = Math.max(8, window.innerWidth - safeWidth - 8);
      const maxTop = Math.max(8, window.innerHeight - panelHeight - 8);
      pointsPanelPlacement.left = Math.min(maxLeft, Math.max(8, Math.round(pointsPanelPlacement.left || 270)));
      pointsPanelPlacement.top = Math.min(maxTop, Math.max(8, Math.round(pointsPanelPlacement.top || 120)));
      const maxAllowedHeight = Math.max(220, window.innerHeight - pointsPanelPlacement.top - 8);
      pointsPanelPlacement.maxHeight = Math.max(220, Math.min(Number.isFinite(pointsPanelPlacement.maxHeight) ? pointsPanelPlacement.maxHeight : maxAllowedHeight, maxAllowedHeight));

      panel.style.left = `${pointsPanelPlacement.left}px`;
      panel.style.top = `${pointsPanelPlacement.top}px`;
      panel.style.width = `${pointsPanelPlacement.width}px`;
      panel.style.maxHeight = `${pointsPanelPlacement.maxHeight}px`;
    }

    function renderPointsPanel() {
      const panel = ensurePointsPanelElement();
      if (!panel) return;
      const sourceEntries = getPointsPanelSourceEntries()
        .slice()
        .sort((a, b) => (Number(b.score) || 0) - (Number(a.score) || 0) || String(a.name || '').localeCompare(String(b.name || '')));
      const roomSynced = !!window.tbcRoomGroupsSyncActive;
      const teamsOff = isTeamsOffForWinnerBoard();
      const hasTeamRows = sourceEntries.some((e) => isLikelyTeamWinnerName(e.name));
      const syncedSupportedNow = hasTeamRows ? false : teamsOff;
      const grouped = roomSynced && syncedSupportedNow ? getGroupedPointsEntries(sourceEntries) : [];
      const groupedOnly = grouped.filter((g) => !(g && g.isUngroupedPlayer));
      const ungroupedSyncedPlayers = grouped
        .filter((g) => !!(g && g.isUngroupedPlayer))
        .map((g) => ({
          name: String((g && g.name) || '').trim(),
          score: Number((g && g.score) || 0),
        }))
        .filter((e) => !!e.name);

      const modeKey = roomSynced ? (syncedSupportedNow ? 'synced' : 'syncedTeamsBlocked') : 'unsynced';
      const sig = JSON.stringify({
        m: modeKey,
        s: sourceEntries.map((e) => [e.name, e.score]),
        g: groupedOnly.map((g) => [g.name, g.score, g.color || '', (g.contributors || []).map((c) => [c.name, c.score])]),
        u: ungroupedSyncedPlayers.map((e) => [e.name, e.score]),
      });

      if (sig !== pointsPanelLastRenderSig) {
        let bodyHtml = '';
        if (roomSynced && !syncedSupportedNow) {
          bodyHtml = `
            <div class="tbc_points_warn">Won't work in teams mode. Group totals are only available in Teams Off / FFA.</div>
          `;
        } else if (roomSynced) {
          const syncedUngroupedColorFn = (name) => getDisplayColorForName(name) || '';
          const ungroupedHtml = ungroupedSyncedPlayers.length
            ? `
              <div class="tbc_points_section">
                <div class="tbc_points_section_title">Ungrouped Players</div>
                <div class="tbc_points_list">${buildPointsPlayersHtml(ungroupedSyncedPlayers, syncedUngroupedColorFn)}</div>
              </div>
            `
            : '';
          bodyHtml = `
            <div class="tbc_points_hint">Synced room: showing host shared-group totals and contributors.</div>
            <div class="tbc_points_section">
              <div class="tbc_points_section_title">Group Totals</div>
              <div class="tbc_points_list">${buildPointsGroupsHtml(groupedOnly)}</div>
            </div>
            ${ungroupedHtml}
          `;
        } else {
          bodyHtml = `
            <div class="tbc_points_hint">Unsynced room: showing player scoreboard snapshot.</div>
            <div class="tbc_points_section">
              <div class="tbc_points_section_title">Player Scores</div>
              <div class="tbc_points_list">${buildPointsPlayersHtml(sourceEntries)}</div>
            </div>
          `;
        }

        panel.innerHTML = `
          <div class="tbc_points_window windowShadow">
            <div class="tbc_points_top" data-tbc-points-drag-handle="1">
              <span class="tbc_points_drag_grip" aria-hidden="true">
                <span class="tbc_points_drag_grip_dot"></span><span class="tbc_points_drag_grip_dot"></span><span class="tbc_points_drag_grip_dot"></span>
                <span class="tbc_points_drag_grip_dot"></span><span class="tbc_points_drag_grip_dot"></span><span class="tbc_points_drag_grip_dot"></span>
                <span class="tbc_points_drag_grip_dot"></span><span class="tbc_points_drag_grip_dot"></span><span class="tbc_points_drag_grip_dot"></span>
              </span>
              <span class="tbc_points_top_title">Points</span>
              <button class="tbc_points_close_btn" data-tbc-points-action="close-panel" title="Close" aria-label="Close panel">x</button>
            </div>
            <div class="tbc_points_body">
              ${bodyHtml}
            </div>
          </div>
        `;
        const closeBtnEl = panel.querySelector('[data-tbc-points-action="close-panel"]');
        const settingsCloseEl = $('settings_close');
        if (closeBtnEl && settingsCloseEl) {
          closeBtnEl.className = settingsCloseEl.className || closeBtnEl.className;
          closeBtnEl.classList.add('tbc_points_close_btn');
        }
        pointsPanelLastRenderSig = sig;
      }

      positionPointsPanel();
      panel.setAttribute('aria-hidden', pointsPanelVisible ? 'false' : 'true');
    }

    function setPointsPanelVisible(visible) {
      pointsPanelVisible = !!visible;
      const panel = ensurePointsPanelElement();
      if (!panel) return;
      if (!pointsPanelVisible) {
        panel.style.display = 'none';
        panel.setAttribute('aria-hidden', 'true');
        return;
      }
      renderPointsPanel();
      panel.style.display = '';
      panel.setAttribute('aria-hidden', 'false');
    }

    window.tbcRenderPointsPanel = () => renderPointsPanel();
    window.tbcIsPointsPanelVisible = () => !!pointsPanelVisible;
    window.tbcInvalidatePointsPanel = () => {
      pointsPanelLastRenderSig = '';
    };
    window.tbcResetPointsPanelCache = () => {
      pointsPanelCachedSourceEntries = [];
      pointsPanelLastRenderSig = '';
    };

    function handleIncomingGroupsSyncChunk(senderName, msgText) {
      const relayRaw = String(msgText || '').trim();
      const relayMsgPipe = relayRaw.match(/^\[TBCGRELAY\|([^\]]+)\]$/i);
      const relayMsgBare = relayRaw.match(/^\[TBCGRELAY\]([A-Za-z0-9._:-]+)$/i);
      const relayTokenFromMsg = relayMsgPipe ? String(relayMsgPipe[1] || '').trim() : (relayMsgBare ? String(relayMsgBare[1] || '').trim() : '');
      if (relayTokenFromMsg) {
        const hostNorm = getLobbyHostNameNorm();
        const senderNorm = normalizeName(senderName);
        if (!hostNorm || senderNorm !== hostNorm) return false;
        const parsedRelay = parseGroupsRelayToken(relayTokenFromMsg);
        if (!parsedRelay) return true;
        if (!isSelfLobbyHost()) pullGroupsRelaySnapshot(relayTokenFromMsg);
        return true;
      }

      const clearMsg = String(msgText || '').trim().match(/^\[TBCGCLR(?:\|.*)?\]$/i);
      if (clearMsg) {
        const hostNorm = getLobbyHostNameNorm();
        const senderNorm = normalizeName(senderName);
        if (!hostNorm || senderNorm !== hostNorm) return false;
        sharedHostGroupsSnapshot = [];
        saveRoomGroupsCache([]);
        liveGroupsSyncHostNorm = '';
        setRoomGroupsSyncActive(false);
        syncSharedGroupsBridge();
        if (groupsPanelVisible) renderGroupsPanel();
        addGroupsLifecycleStatus('Host desynced shared groups...using your own color groups');
        return true;
      }

      const chunkMsg = String(msgText || '').trim().match(/^\[TBCGREF\|([a-z0-9_]+)\|(\d+)\/(\d+)\|([A-Za-z0-9+/=]*)\]$/i);
      if (chunkMsg) {
        const hostNorm = getLobbyHostNameNorm();
        const senderNorm = normalizeName(senderName);
        if (!hostNorm || senderNorm !== hostNorm) return false;

        const session = chunkMsg[1];
        const idx = Math.max(1, parseInt(chunkMsg[2], 10) || 1);
        const total = Math.max(1, parseInt(chunkMsg[3], 10) || 1);
        const chunk = chunkMsg[4] || '';

        let bag = groupsSyncChunksBySession.get(session);
        if (!bag) {
          bag = { total, chunks: new Array(total).fill(''), filled: 0, createdAt: Date.now() };
          groupsSyncChunksBySession.set(session, bag);
        }
        if (bag.total !== total) return true;
        if (!bag.chunks[idx - 1]) {
          bag.chunks[idx - 1] = chunk;
          bag.filled += 1;
        }

        for (const [sid, entry] of groupsSyncChunksBySession.entries()) {
          if ((Date.now() - entry.createdAt) > 60000) groupsSyncChunksBySession.delete(sid);
        }
        if (bag.filled < bag.total) return true;

        try {
          const payload = bag.chunks.join('');
          sharedHostGroupsSnapshot = decodeSharedGroupsPayload(payload, sharedHostGroupsSnapshot);
          saveRoomGroupsCache(sharedHostGroupsSnapshot);
          liveGroupsSyncHostNorm = hostNorm || '';
          setRoomGroupsSyncActive(true);
          syncSharedGroupsBridge();
          const roomKey = getLobbyRoomSyncKey();
          if (roomKey && sharedHostGroupsSnapshot.length && roomKey !== lastSharedGroupsJoinNoticeKey) {
            lastSharedGroupsJoinNoticeKey = roomKey;
            addGroupsLifecycleStatus('Room has synced groups...using host color groups');
          }
          renderGroupsPanel();
        } catch (e) {
          console.error('[TBC] Failed to decode chunked host groups payload', e);
        } finally {
          groupsSyncChunksBySession.delete(session);
        }
        return true;
      }

      const m = String(msgText || '').trim().match(/^\[TBCGREF\|([A-Za-z0-9+/=]+)\]$/);
      if (!m) return false;

      const hostNorm = getLobbyHostNameNorm();
      const senderNorm = normalizeName(senderName);
      if (!hostNorm || senderNorm !== hostNorm) return false;

      try {
        const payload = m[1];
        sharedHostGroupsSnapshot = decodeSharedGroupsPayload(payload, sharedHostGroupsSnapshot);
        saveRoomGroupsCache(sharedHostGroupsSnapshot);
        liveGroupsSyncHostNorm = hostNorm || '';
        setRoomGroupsSyncActive(true);
        syncSharedGroupsBridge();
        const roomKey = getLobbyRoomSyncKey();
        if (roomKey && sharedHostGroupsSnapshot.length && roomKey !== lastSharedGroupsJoinNoticeKey) {
          lastSharedGroupsJoinNoticeKey = roomKey;
          addGroupsLifecycleStatus('Room has synced groups...using host color groups');
        }
        renderGroupsPanel();
      } catch (e) {
        console.error('[TBC] Failed to decode host groups payload', e);
      }
      return true;
    }

    const GROUPS_SYNC_CHAT_CARRIER_VISIBLE = '.';
    const GROUPS_SYNC_CHAT_CARRIER_MARKER = '\u2063\u2064';

    function encodeGroupsSyncCarrierPayload(rawText) {
      const src = String(rawText || '');
      let bytes = null;
      try {
        if (typeof TextEncoder !== 'undefined') bytes = Array.from(new TextEncoder().encode(src));
      } catch {}
      if (!bytes) {
        const legacy = unescape(encodeURIComponent(src));
        bytes = Array.from(legacy).map((ch) => ch.charCodeAt(0) & 0xff);
      }
      let out = '';
      for (let i = 0; i < bytes.length; i += 1) {
        const b = bytes[i] & 0xff;
        out += String.fromCharCode(0xFE00 + ((b >> 4) & 0x0f));
        out += String.fromCharCode(0xFE00 + (b & 0x0f));
      }
      return out;
    }

    function decodeGroupsSyncCarrierPayload(encodedText) {
      const src = String(encodedText || '');
      if (!src || (src.length % 2) !== 0) return null;
      const bytes = [];
      for (let i = 0; i < src.length; i += 2) {
        const hi = src.charCodeAt(i) - 0xFE00;
        const lo = src.charCodeAt(i + 1) - 0xFE00;
        if (hi < 0 || hi > 15 || lo < 0 || lo > 15) return null;
        bytes.push((hi << 4) | lo);
      }
      try {
        if (typeof TextDecoder !== 'undefined') return new TextDecoder().decode(new Uint8Array(bytes));
      } catch {}
      try {
        return decodeURIComponent(escape(String.fromCharCode(...bytes)));
      } catch {
        return null;
      }
    }

    function buildGroupsSyncTransportMessage(rawMessage) {
      return String(rawMessage || '').trim();
    }

    function buildGroupsSyncTransportCarrierMessage(rawMessage) {
      const payload = encodeGroupsSyncCarrierPayload(String(rawMessage || ''));
      return `${GROUPS_SYNC_CHAT_CARRIER_VISIBLE}${GROUPS_SYNC_CHAT_CARRIER_MARKER}${payload}`;
    }

    function parseGroupsSyncTransportMessage(msgText) {
      const raw = String(msgText || '').trim();
      if (/^\[(TBCGREF\||TBCGCLR|TBCGRELAY\||TBCGRELAY\])/i.test(raw)) return raw;
      if (!raw.startsWith(GROUPS_SYNC_CHAT_CARRIER_VISIBLE + GROUPS_SYNC_CHAT_CARRIER_MARKER)) return null;
      const encoded = raw.slice((GROUPS_SYNC_CHAT_CARRIER_VISIBLE + GROUPS_SYNC_CHAT_CARRIER_MARKER).length);
      const decoded = decodeGroupsSyncCarrierPayload(encoded);
      if (!decoded || !/^\[(TBCGREF\||TBCGCLR|TBCGRELAY\||TBCGRELAY\])/i.test(decoded)) return null;
      return decoded;
    }

    function getChatAccountIdentity() {
      const lvlEl = $('pretty_top_level');
      const nameEl = $('pretty_top_name');
      if (!lvlEl || !nameEl) return null;

      const lvl = String(lvlEl.textContent || '').trim().toLowerCase();
      const nameRaw = String(nameEl.textContent || '').trim();
      const name = nameRaw.toLowerCase();

      if (!lvl || lvl === 'guest') return null;
      if (!name || name === 'guest') return null;

      const looksLoggedIn = /^level\b/.test(lvl) || /\d/.test(lvl);
      if (!looksLoggedIn) return null;

      return { level: lvl, name };
    }

    function getChatAccountNameOrNull() {
      const ident = getChatAccountIdentity();
      return ident ? ident.name : null;
    }

    function getChatStorageKeyV2() {
      const acct = getChatAccountNameOrNull();
      if (!acct) return null;
      return CHAT_STORAGE_PREFIX_V2 + acct;
    }

    function loadChatState() {
      try {
        chatState = createDefaultChatState();
        if (!chatStorageKey) return;
        const raw = localStorage.getItem(chatStorageKey);
        if (!raw) return;

        const data = JSON.parse(raw);
        if (!data || typeof data !== 'object') return;

        chatState.hideGuests = !!data.hideGuests;
        chatState.showSystemMessages = data.showSystemMessages === true;
        chatState.useCustomSystemMessageColors = data.useCustomSystemMessageColors === true;
        chatState.ingameChatBackgrounds = data.ingameChatBackgrounds === true;
        chatState.hideIngameOthersUntilFadeDelay = data.hideIngameOthersUntilFadeDelay === true;

        chatState.blacklistUsers = Array.isArray(data.blacklistUsers) ? data.blacklistUsers.map(s => String(s || '').trim()).filter(Boolean) : [];
        chatState.systemMessageColors = normalizeSystemMessageColors(data.systemMessageColors);

        chatState.ingameChatLines = clampIngameChatLines(
          data.ingameChatLines === undefined ? chatState.ingameChatLines : data.ingameChatLines
        );
        chatState.ingameFadeDelaySec = clampIngameFadeDelaySec(
          data.ingameFadeDelaySec === undefined ? chatState.ingameFadeDelaySec : data.ingameFadeDelaySec
        );
      } catch (e) {
        console.error('[TBC Chat] loadChatState failed', e);
      }
    }

    function normalizeSystemMessageColors(data) {
      const defaults = createDefaultSystemMessageColors();
      if (!data || typeof data !== 'object') return defaults;

      const legacyKickBanHex = parseColorTextToHex(
        data.kickBan && data.kickBan.hex ? data.kickBan.hex : '',
        data.kickBan && data.kickBan.format ? data.kickBan.format : 'hex'
      );
      const legacyKickBanFormat =
        data.kickBan && typeof data.kickBan.format === 'string' && SYSTEM_COLOR_NEXT_FORMAT[data.kickBan.format]
          ? data.kickBan.format
          : 'hex';

      SYSTEM_COLOR_CATEGORIES.forEach((cat) => {
        const raw = data[cat.id];
        const fallbackRaw =
          (cat.id === 'kick' || cat.id === 'ban') && !raw && legacyKickBanHex
            ? { hex: legacyKickBanHex, format: legacyKickBanFormat }
            : raw;
        const parsedHex = parseColorTextToHex(
          fallbackRaw && fallbackRaw.hex ? fallbackRaw.hex : '',
          fallbackRaw && fallbackRaw.format ? fallbackRaw.format : 'hex'
        );
        defaults[cat.id] = {
          hex: (parsedHex || SYSTEM_COLOR_DEFAULT_HEX[cat.id] || SYSTEM_COLOR_DEFAULT_HEX.defaultSystem).toLowerCase(),
          format:
            fallbackRaw && typeof fallbackRaw.format === 'string' && SYSTEM_COLOR_NEXT_FORMAT[fallbackRaw.format]
              ? fallbackRaw.format
              : 'hex',
        };
      });
      return defaults;
    }

    function saveChatState() {
        try {
            updateChatStorageKey();
            if (!chatStorageKey) {
            window.dispatchEvent(new Event('tbcChatSettingsChanged'));
            updateChatStorageHintUI();
            scheduleChatScan();
            return;
            }
            localStorage.setItem(chatStorageKey, JSON.stringify(chatState));
            window.dispatchEvent(new Event('tbcChatSettingsChanged'));
            updateChatStorageHintUI();
            scheduleChatScan();
        } catch (e) {
            console.error('[TBC Chat] saveChatState failed', e);
        }
    }

    function mutateChatState(mutator) {
      updateChatStorageKey(true);
      try {
        mutator(chatState);
      } catch (e) {
        console.error('[TBC Chat] mutateChatState failed', e);
        return;
      }
      saveChatState();
    }

    function updateChatStorageKey(force = false) {
      const ident = getChatAccountIdentity();
      const identityKey = ident ? `${ident.level}|${ident.name}` : 'guest';
      const k = ident ? (CHAT_STORAGE_PREFIX_V2 + ident.name) : null;

      if (!force && k === lastChatStorageKey && identityKey === lastChatIdentity) return;
      lastChatIdentity = identityKey;
      lastChatStorageKey = k;
      chatStorageKey = k;
      loadChatState();
      updateChatStorageHintUI();
      if (typeof refreshChatSettingsUi === 'function') refreshChatSettingsUi();
      applyIngameChatBackgroundSetting();
      applyIngameChatLines();
      if (chatState.showSystemMessages) restoreStashedIngameSystemRows();
      scheduleChatScan();
    }

    function ensureChatAccountWatchers() {
      if (chatAccountObserver) return;

      chatAccountObserver = new MutationObserver(() => updateChatStorageKey());
      waitForElement('pretty_top_name', (el) => {
        if (chatAccountObserver) chatAccountObserver.observe(el, { childList: true, subtree: true, characterData: true });
      });
      waitForElement('pretty_top_level', (el) => {
        if (chatAccountObserver) chatAccountObserver.observe(el, { childList: true, subtree: true, characterData: true });
      });

    }

    function updateChatStorageHintUI() {
      const el = $('tbc_chat_storage_hint');
      if (!el) return;

      if (chatStorageKey) {
        el.style.color = '';
        el.style.opacity = '0.75';
        el.textContent = `Per-account storage: ${chatStorageKey}`;
      } else {
        el.style.color = '#ffcc66';
        el.style.opacity = '0.9';
        el.textContent = 'Guest mode: settings are temporary until you log in.';
      }
    }

    function clampIngameChatLines(v) {
      const n = parseInt(String(v), 10);
      if (!Number.isFinite(n)) return 4;
      return Math.max(1, Math.min(10, n));
    }

    function clampIngameFadeDelaySec(v) {
      const n = parseFloat(String(v));
      if (!Number.isFinite(n)) return 8;
      return Math.max(1, Math.min(60, n));
    }

    function clamp255(v) {
      const n = parseFloat(String(v));
      if (!Number.isFinite(n)) return 0;
      return Math.max(0, Math.min(255, Math.round(n)));
    }

    function clamp01(v) {
      const n = parseFloat(String(v));
      if (!Number.isFinite(n)) return 0;
      return Math.max(0, Math.min(1, n));
    }

    function normalizeHexColor(input) {
      let s = String(input || '').trim().toLowerCase();
      if (!s) return null;
      if (s.charAt(0) !== '#') s = `#${s}`;
      if (/^#[0-9a-f]{3}$/.test(s)) {
        const a = s.charAt(1), b = s.charAt(2), c = s.charAt(3);
        return `#${a}${a}${b}${b}${c}${c}`;
      }
      if (/^#[0-9a-f]{6}$/.test(s)) return s;
      return null;
    }

    function hexToRgbObj(hex) {
      const h = normalizeHexColor(hex);
      if (!h) return null;
      return {
        r: parseInt(h.slice(1, 3), 16),
        g: parseInt(h.slice(3, 5), 16),
        b: parseInt(h.slice(5, 7), 16),
      };
    }

    function rgbObjToHex(r, g, b) {
      const rr = clamp255(r).toString(16).padStart(2, '0');
      const gg = clamp255(g).toString(16).padStart(2, '0');
      const bb = clamp255(b).toString(16).padStart(2, '0');
      return `#${rr}${gg}${bb}`;
    }

    function rgbToHsvObj(r, g, b) {
      const rn = clamp255(r) / 255;
      const gn = clamp255(g) / 255;
      const bn = clamp255(b) / 255;
      const max = Math.max(rn, gn, bn);
      const min = Math.min(rn, gn, bn);
      const d = max - min;
      let h = 0;
      if (d !== 0) {
        if (max === rn) h = ((gn - bn) / d) % 6;
        else if (max === gn) h = ((bn - rn) / d) + 2;
        else h = ((rn - gn) / d) + 4;
        h *= 60;
        if (h < 0) h += 360;
      }
      const s = max === 0 ? 0 : d / max;
      const v = max;
      return { h, s, v };
    }

    function hsvToRgbObj(h, s, v) {
      let hh = parseFloat(String(h));
      if (!Number.isFinite(hh)) hh = 0;
      hh = ((hh % 360) + 360) % 360;
      const ss = clamp01(s);
      const vv = clamp01(v);
      const c = vv * ss;
      const x = c * (1 - Math.abs(((hh / 60) % 2) - 1));
      const m = vv - c;
      let rp = 0, gp = 0, bp = 0;

      if (hh < 60) { rp = c; gp = x; bp = 0; }
      else if (hh < 120) { rp = x; gp = c; bp = 0; }
      else if (hh < 180) { rp = 0; gp = c; bp = x; }
      else if (hh < 240) { rp = 0; gp = x; bp = c; }
      else if (hh < 300) { rp = x; gp = 0; bp = c; }
      else { rp = c; gp = 0; bp = x; }

      return {
        r: clamp255((rp + m) * 255),
        g: clamp255((gp + m) * 255),
        b: clamp255((bp + m) * 255),
      };
    }

    function parseRgbText(text) {
      const nums = String(text || '').match(/-?\d+(\.\d+)?/g);
      if (!nums || nums.length < 3) return null;
      return { r: clamp255(nums[0]), g: clamp255(nums[1]), b: clamp255(nums[2]) };
    }

    function parseHsvText(text) {
      const nums = String(text || '').match(/-?\d+(\.\d+)?/g);
      if (!nums || nums.length < 3) return null;
      const h = parseFloat(nums[0]);
      const sRaw = parseFloat(nums[1]);
      const vRaw = parseFloat(nums[2]);
      if (!Number.isFinite(h) || !Number.isFinite(sRaw) || !Number.isFinite(vRaw)) return null;
      const s = clamp01(sRaw > 1 ? (sRaw / 100) : sRaw);
      const v = clamp01(vRaw > 1 ? (vRaw / 100) : vRaw);
      return { h, s, v };
    }

    function parseColorTextToHex(text, format) {
      const fmt = String(format || 'hex').toLowerCase();
      if (fmt === 'hex') return normalizeHexColor(text);
      if (fmt === 'rgb') {
        const rgb = parseRgbText(text);
        return rgb ? rgbObjToHex(rgb.r, rgb.g, rgb.b) : null;
      }
      if (fmt === 'hsv') {
        const hsv = parseHsvText(text);
        if (!hsv) return null;
        const rgb = hsvToRgbObj(hsv.h, hsv.s, hsv.v);
        return rgbObjToHex(rgb.r, rgb.g, rgb.b);
      }
      return normalizeHexColor(text);
    }

    function formatHexByMode(hex, format) {
      const h = normalizeHexColor(hex) || '#000000';
      const fmt = String(format || 'hex').toLowerCase();
      if (fmt === 'hex') return h;
      const rgb = hexToRgbObj(h) || { r: 0, g: 0, b: 0 };
      if (fmt === 'rgb') return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
      const hsv = rgbToHsvObj(rgb.r, rgb.g, rgb.b);
      return `hsv(${Math.round(hsv.h)}, ${Math.round(hsv.s * 100)}%, ${Math.round(hsv.v * 100)}%)`;
    }

    function classifySystemMessageCategory(text) {
      const raw = String(text || '');
      const s = normalizeStatusText(raw).replace(/^\*\s*/, '');
      if (!s) return 'defaultSystem';

      if (isReplayRecorderSystemText(s)) return 'replay';
      if (s.indexOf('portal') !== -1) return 'portal';
      if (
        s.indexOf('friend request') !== -1 ||
        s.indexOf('now friends') !== -1
      ) return 'friend';
      if (
        /\bhas given host privileges to\b/.test(s) ||
        /\bwho is now the game host\b/.test(s) ||
        /\byou are now the host of this game\b/.test(s) ||
        /\byou are now the game host\b/.test(s) ||
        /\bleft the game and .+ is now the game host\b/.test(s)
      ) return 'hostTransfer';
      if (/\bhas joined the game\b/.test(s)) return 'userJoin';
      if (
        /\bhas left the game\b/.test(s)
      ) return 'userLeft';
      if (/^\/[a-z]/.test(s)) {
        if (
          s.indexOf('/mapimg') !== -1 ||
          s.indexOf('/copymap') !== -1 ||
          s.indexOf('/groups') !== -1 ||
          s.indexOf('/panels') !== -1 ||
          s.indexOf('/points') !== -1 ||
          s.indexOf('/groupssync') !== -1 ||
          s.indexOf('/groupsrelay') !== -1 ||
          s.indexOf('/blacklist') !== -1
        ) return 'customCommands';
        return 'helpHint';
      }
      if (/^(?:\*\s*)?banned\b/.test(s)) return 'ban';
      if (/^(?:\*\s*)?kicked\b/.test(s)) return 'kick';
      if (
        s.indexOf('not recognised') !== -1 ||
        s.indexOf('accepted commands are listed above') !== -1 ||
        s.indexOf('accepted commands listed above') !== -1
      ) return 'helpHint';
      if (
        s.indexOf('/mapimg') !== -1 ||
        s.indexOf('/copymap') !== -1 ||
        s.indexOf('/groups') !== -1 ||
        s.indexOf('/panels') !== -1 ||
        s.indexOf('/points') !== -1 ||
        s.indexOf('/groupssync') !== -1 ||
        s.indexOf('/groupsrelay') !== -1 ||
        s.indexOf('/blacklist') !== -1 ||
        s.indexOf('[tbc]') !== -1 ||
        s.indexOf('shared groups panel') !== -1 ||
        s.indexOf('groups panel refresh sent') !== -1
      ) return 'customCommands';
      return 'defaultSystem';
    }

    function getSystemCategoryColorHex(categoryId) {
      const id = String(categoryId || 'defaultSystem');
      const map = chatState && chatState.systemMessageColors ? chatState.systemMessageColors : null;
      const hex = map && map[id] ? map[id].hex : null;
      return normalizeHexColor(hex) || SYSTEM_COLOR_DEFAULT_HEX[id] || SYSTEM_COLOR_DEFAULT_HEX.defaultSystem;
    }

    function getSystemColorForText(text) {
      const category = classifySystemMessageCategory(text);
      return {
        category,
        color: getSystemCategoryColorHex(category),
      };
    }

    function resolveIngameRow(node) {
      const row = resolveChatRow(node);
      if (!row || !row.isConnected) return null;
      const isInGame =
        (row.classList && row.classList.contains('ingamechatentry')) ||
        !!row.closest('#ingamechatcontent');
      return isInGame ? row : null;
    }

    function markIngameRowFadeBirth(node, force = false) {
      const row = resolveIngameRow(node);
      if (!row) return;
      if (!force && row.dataset.tbcFadeBornAt) return;
      row.dataset.tbcFadeBornAt = String(Date.now());
      delete row.dataset.tbcFadeBypassDelay;
      row.dataset.tbcFadeSig = String(row.textContent || '').trim();
    }

    function getVisibleIngameRowsForFade(host) {
      if (!host) return [];
      const out = [];
      const seen = new Set();
      const direct = Array.from(host.children || []);
      for (const child of direct) {
        const row = resolveIngameRow(child) || child;
        if (!(row instanceof Element)) continue;
        if (seen.has(row)) continue;
        seen.add(row);

        if (row.classList && row.classList.contains('tbc_chat_invisible')) continue;
        if (row.classList && row.classList.contains('tbc_fade_hidden')) continue;
        if ((row.offsetHeight || 0) <= 0 && (row.getClientRects ? row.getClientRects().length : 0) <= 0) continue;

        out.push(row);
      }
      return out;
    }

    function applyIngameRowOpacity(row, opacity) {
      if (!row || !(row instanceof Element)) return;
      const o = Math.max(0, Math.min(1, opacity));
      const prev = parseFloat(row.dataset.tbcLastOpacity || '');
      if (!Number.isFinite(prev) || Math.abs(prev - o) > 0.01) {
        row.style.opacity = String(o);
        row.dataset.tbcLastOpacity = String(o);
      }
      if (row.style.transition !== 'opacity 140ms linear') row.style.transition = 'opacity 140ms linear';
    }

    function setIngameRowFadeHidden(row, hidden) {
      if (!row || !(row instanceof Element)) return;
      row.classList.toggle('tbc_fade_hidden', !!hidden);
    }

    function resetIngameFadeRowsToVisible(host) {
      if (!host) return;
      const seen = new Set();
      const direct = Array.from(host.children || []);
      for (const child of direct) {
        const row = resolveIngameRow(child) || child;
        if (!(row instanceof Element)) continue;
        if (seen.has(row)) continue;
        seen.add(row);
        setIngameRowFadeHidden(row, false);
        applyIngameRowOpacity(row, 1);
      }
    }

    function isIngameChatInteracting() {
      const input = $('ingamechatinputtext');
      if (!input) return false;
      if (document.activeElement !== input) return false;

      const cs = window.getComputedStyle(input);
      if (!cs) return false;
      if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
      if (
        (input.offsetWidth || 0) <= 0 &&
        (input.offsetHeight || 0) <= 0 &&
        (input.getClientRects ? input.getClientRects().length : 0) <= 0
      ) {
        return false;
      }

      return true;
    }

    function updateIngameChatScrollFocusState(host, focused) {
      if (!host) return;
      const wasFocused = host.classList.contains('tbc_ingame_scroll_focus');
      const prevBottomOffset = host.scrollHeight - host.scrollTop;
      host.classList.toggle('tbc_ingame_scroll_focus', !!focused);
      if (focused && !wasFocused) {
        host.scrollTop = host.scrollHeight;
        requestAnimationFrame(() => {
          host.scrollTop = host.scrollHeight;
          requestAnimationFrame(() => {
            host.scrollTop = host.scrollHeight;
          });
        });
        return;
      }
      const nextTop = host.scrollHeight - prevBottomOffset;
      if (Number.isFinite(nextTop)) host.scrollTop = Math.max(0, nextTop);
    }

    function syncIngameChatScrollbarState(host) {
      if (!host) return;
      const focused = host.classList.contains('tbc_ingame_scroll_focus');
      const effectiveLines = Math.max(1, parseInt(host.getAttribute('data-tbc-effective-lines') || '1', 10) || 1);
      const visibleRows = getVisibleIngameRowsForFade(host).length;
      const hasOverflow = visibleRows > effectiveLines;
      const showScrollbar = focused && hasOverflow;
      host.classList.toggle('tbc_ingame_has_scrollbar', showScrollbar);
      host.style.overflowY = focused ? (showScrollbar ? 'scroll' : 'hidden') : 'hidden';
    }

    function applyIngameChatFade() {
      const host = $('ingamechatcontent');
      if (!host) return;
      if (!isElementActuallyVisible(host) && !isIngameChatInteracting()) return;

      const now = Date.now();
      const liveInteracting = isIngameChatInteracting();
      updateIngameChatScrollFocusState(host, liveInteracting);
      const delayMs = Math.round(clampIngameFadeDelaySec(chatState.ingameFadeDelaySec) * 1000);
      const fadeMs = 1400;
      const selfNorm = getSelfNameNorm();
      const hideOthersUntilDelay = !!chatState.hideIngameOthersUntilFadeDelay;

      if (liveInteracting) {
        resetIngameFadeRowsToVisible(host);
        const visibleWhileFocused = getVisibleIngameRowsForFade(host);
        visibleWhileFocused.forEach((row) => {
          row.dataset.tbcFadeKeepBornAtOnNextSigRefresh = '1';
        });
        syncIngameChatScrollbarState(host);
        wasIngameChatInteracting = true;
        return;
      }

      if (!chatState.showSystemMessages && wasIngameChatInteracting) pruneHiddenIngameSystemRows();

      const visible = getVisibleIngameRowsForFade(host);

      if (wasIngameChatInteracting) {
        visible.forEach((row) => {
          let bornAt = parseFloat(row.dataset.tbcFadeBornAt || '');
          if (!Number.isFinite(bornAt) || bornAt <= 0) {
            bornAt = now;
            row.dataset.tbcFadeBornAt = String(bornAt);
          }
          const age = now - bornAt;
          if (age >= delayMs) row.dataset.tbcFadeBypassDelay = '1';
          else delete row.dataset.tbcFadeBypassDelay;
          row.dataset.tbcFadeSig = String(row.textContent || '').trim();
          setIngameRowFadeHidden(row, false);
          applyIngameRowOpacity(row, 1);
        });
      }
      wasIngameChatInteracting = false;

      for (let i = 0; i < visible.length; i++) {
        const row = visible[i];
        const curSig = String(row.textContent || '').trim();
        if (row.dataset.tbcFadeSig !== curSig) {
          const keepBornAt = row.dataset.tbcFadeKeepBornAtOnNextSigRefresh === '1';
          row.dataset.tbcFadeSig = curSig;
          if (keepBornAt) {
            delete row.dataset.tbcFadeKeepBornAtOnNextSigRefresh;
          } else {
            row.dataset.tbcFadeBornAt = String(now);
            delete row.dataset.tbcFadeBypassDelay;
          }
        }

        let bornAt = parseFloat(row.dataset.tbcFadeBornAt || '');
        if (!Number.isFinite(bornAt) || bornAt <= 0) {
          bornAt = now;
          row.dataset.tbcFadeBornAt = String(bornAt);
          row.dataset.tbcFadeSig = curSig;
        }

        let age = now - bornAt;
        if (row.dataset.tbcFadeBypassDelay === '1') age += delayMs;

        if (hideOthersUntilDelay) {
          const msgType = String(row.dataset.tbcMsgType || '').toLowerCase();
          if (msgType === 'user') {
            const parts = getInGameMessagePartsFromRow(row);
            const senderNorm = normalizeName(parts.sender);
            const isSelfMsg = !!selfNorm && !!senderNorm && senderNorm === selfNorm;
            if (!isSelfMsg && age < delayMs) {
              setIngameRowFadeHidden(row, true);
              applyIngameRowOpacity(row, 0);
              continue;
            }
          }
        }

        let opacity = 1;
        if (age > delayMs) {
          opacity = 1 - ((age - delayMs) / fadeMs);
        }
        if (opacity <= 0.01) {
          setIngameRowFadeHidden(row, true);
          applyIngameRowOpacity(row, 0);
          continue;
        }
        setIngameRowFadeHidden(row, false);
        applyIngameRowOpacity(row, opacity);
      }
    }

    function scheduleIngameVisualRefresh() {
      if (ingameVisualRefreshQueued) return;
      ingameVisualRefreshQueued = true;
      requestAnimationFrame(() => {
        ingameVisualRefreshQueued = false;
        applyIngameChatLines();
        applyIngameChatFade();
      });
    }

    function applyIngameChatBackgroundSetting() {
      const host = $('ingamechatcontent');
      if (!host) return;
      host.classList.toggle('tbc_ingame_bg_on', !!chatState.ingameChatBackgrounds);
    }

    function applyIngameChatLines() {
      const host = $('ingamechatcontent');
      const chatBox = $('ingamechatbox');
      if (!host) return;

      const lines = clampIngameChatLines(chatState.ingameChatLines);
      const defaultLines = 4;

      function readPositivePx(el, attrName) {
        const v = parseFloat(el.getAttribute(attrName) || '');
        return Number.isFinite(v) && v > 0 ? v : 0;
      }

      let perLine =
        readPositivePx(host, 'data-tbc-per-line-height-px') ||
        readPositivePx(host, 'data-tbc-runtime-per-line-height-px');

      let hostBase =
        readPositivePx(host, 'data-tbc-base-chat-height-px') ||
        readPositivePx(host, 'data-tbc-runtime-base-chat-height-px');

      if (!perLine) {
        const hostLineH = parseFloat(window.getComputedStyle(host).lineHeight);
        if (Number.isFinite(hostLineH) && hostLineH > 0) perLine = hostLineH;
        else if (hostBase) perLine = hostBase / defaultLines;
        else perLine = 18;
        host.setAttribute('data-tbc-runtime-per-line-height-px', String(perLine));
      }

      if (!Number.isFinite(hostBase) || hostBase <= 0) {
        hostBase = perLine * defaultLines;
        host.setAttribute('data-tbc-runtime-base-chat-height-px', String(hostBase));
      }

      let effectiveLines = lines;
      if (host.classList.contains('tbc_ingame_scroll_focus')) {
        const rowCount = Array.from(host.children || []).filter((row) => {
          if (!(row instanceof Element)) return false;

          const msgType = String(row.dataset.tbcMsgType || '').toLowerCase();
          const isTaggedSystem = msgType === 'system';
          const isTaggedBlacklisted = row.dataset.tbcMsgBlacklisted === '1';

          if (isTaggedSystem && !chatState.showSystemMessages) return false;
          if (isTaggedBlacklisted) return false;
          if (row.classList.contains('tbc_chat_invisible')) return false;
          return true;
        }).length;
        if (rowCount > 0) effectiveLines = Math.min(lines, rowCount);
      }

      const hostPx = Math.max(Math.ceil(perLine), Math.round(perLine * effectiveLines));
      host.setAttribute('data-tbc-effective-lines', String(effectiveLines));
      host.style.height = `${hostPx}px`;
      host.style.maxHeight = `${hostPx}px`;

      if (chatBox) {
        let chromePx = readPositivePx(chatBox, 'data-tbc-runtime-chat-chrome-px');
        if (!chromePx) {
          const boxNow =
            parseFloat(window.getComputedStyle(chatBox).height) || chatBox.clientHeight || chatBox.offsetHeight || 0;
          const hostNow =
            parseFloat(window.getComputedStyle(host).height) || host.clientHeight || host.offsetHeight || 0;
          const attrBoxBase = readPositivePx(chatBox, 'data-tbc-base-chat-height-px');
          const attrHostBase = readPositivePx(host, 'data-tbc-base-chat-height-px');
          const attrDiff = attrBoxBase > 0 && attrHostBase > 0 ? (attrBoxBase - attrHostBase) : 0;
          chromePx = Math.max(16, boxNow - hostNow, attrDiff || 0, 24);
          chatBox.setAttribute('data-tbc-runtime-chat-chrome-px', String(chromePx));
        }

        const boxPx = Math.max(24, Math.floor(hostPx + chromePx));
        chatBox.style.height = `${boxPx}px`;
        chatBox.style.maxHeight = `${boxPx}px`;
      }

      const wasNearBottom = isContainerExactlyAtBottom(host);
      syncIngameChatScrollbarState(host);
      if (wasNearBottom) host.scrollTop = host.scrollHeight;
    }

    function ensureChatStyles() {
      if ($('tbc_chat_css')) return;

      const style = document.createElement('style');
      style.id = 'tbc_chat_css';
      style.textContent = `
        .tbc_row { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-top:8px; }
        .tbc_toggle {
          display:inline-flex; align-items:center; gap:8px;
          padding: 6px 10px; border-radius: 10px;
          border: 1px solid rgba(255,255,255,0.14);
          background: rgba(0,0,0,0.16);
          cursor:pointer; font-size: 11px; opacity: .95;
          user-select:none;
        }
        .tbc_toggle:hover { background: rgba(255,255,255,0.08); }
        .tbc_toggle_dot {
          width: 30px; height: 16px; border-radius: 999px;
          border: 1px solid rgba(255,255,255,0.16);
          background: rgba(0,0,0,0.25);
          position: relative; flex: 0 0 auto;
        }
        .tbc_toggle_dot::after{
          content:""; position:absolute; top:50%; left:1px;
          width: 12px; height: 12px; border-radius: 999px;
          background: rgba(255,255,255,0.75);
          transform: translateY(-50%);
          transition: transform 0.15s;
        }
        .tbc_toggle.on .tbc_toggle_dot {
          background: rgba(121,85,248,0.35);
          border-color: rgba(121,85,248,0.55);
        }
        .tbc_toggle.on .tbc_toggle_dot::after { transform: translate(14px, -50%); }

        .tbc_box {
          margin-top: 10px;
          padding: 10px;
          border-radius: 10px;
          border: 1px solid rgba(255,255,255,0.12);
          background: rgba(0,0,0,0.14);
        }
        .tbc_h { font-weight: 800; font-size: 12px; margin-bottom: 6px; }
        .tbc_p { font-size: 11px; opacity: .88; line-height: 1.35; margin-bottom: 6px; }

        .tbc_inputrow { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
        .tbc_input {
          width: 220px;
          border-radius: 8px;
          border: 1px solid rgba(255,255,255,0.14);
          background: rgba(0,0,0,0.22);
          color: #fff;
          padding: 6px 8px;
          font-size: 12px;
          outline: none;
        }
        .tbc_no_spin {
          appearance: textfield;
          -moz-appearance: textfield;
        }
        .tbc_no_spin::-webkit-outer-spin-button,
        .tbc_no_spin::-webkit-inner-spin-button {
          -webkit-appearance: none;
          margin: 0;
        }
        .tbc_btn {
          padding: 6px 10px;
          border-radius: 10px;
          border: 1px solid rgba(255,255,255,0.14);
          background: rgba(255,255,255,0.06);
          cursor:pointer;
          font-size: 11px;
          user-select:none;
        }
        .tbc_btn:hover { background: rgba(255,255,255,0.12); }
        .tbc_syscolor_head {
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 8px;
        }
        .tbc_syscolor_reset {
          width: 24px;
          height: 24px;
          display: inline-flex;
          align-items: center;
          justify-content: center;
          border-radius: 999px;
          border: 1px solid rgba(255,255,255,0.18);
          background: rgba(255,255,255,0.06);
          color: rgba(255,255,255,0.88);
          font-size: 12px;
          line-height: 1;
          cursor: pointer;
          user-select: none;
          transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
        }
        .tbc_syscolor_reset:hover {
          background: rgba(255, 76, 76, 0.16);
          border-color: rgba(255, 76, 76, 0.8);
          color: #ff4c4c;
        }

        .tbc_chips { display:flex; flex-wrap:wrap; gap:6px; margin-top: 8px; }
        .tbc_chip {
          display:inline-flex; align-items:center; gap:8px;
          padding: 4px 8px; border-radius: 999px;
          border: 1px solid rgba(255,255,255,0.14);
          background: rgba(0,0,0,0.18);
          font-size: 11px; opacity: .95;
        }
        .tbc_chip_x { cursor:pointer; opacity:.8; padding: 0 2px; }
        .tbc_chip_x:hover { opacity: 1; }

        .tbc_chat_hidden_notice{
            display: block;
            margin: 2px 0 !important;
            padding: 2px 4px !important;
            border-radius: 6px;
            background: rgba(0,0,0,0.16);
            border: 1px solid rgba(255,255,255,0.10);
            opacity: 0.88;
        }
        .tbc_chat_hidden_notice a {
            color: inherit;
            font-weight: 700;
            text-decoration: underline;
            cursor: pointer;
        }
        .tbc_chat_original{
            display: block;
        }
        .tbc_chat_invisible {
          display: none !important;
          height: 0 !important;
          margin: 0 !important;
          padding: 0 !important;
          border: 0 !important;
          opacity: 0 !important;
          overflow: hidden !important;
          pointer-events: none !important;
        }
        .tbc_fade_hidden {
          display: none !important;
          height: 0 !important;
          margin: 0 !important;
          padding: 0 !important;
          border: 0 !important;
          opacity: 0 !important;
          overflow: hidden !important;
          pointer-events: none !important;
        }
        .tbc_chat_hidectl a{
            font-weight: 700;
        }
        #tbc_groups_panel{
          position: fixed;
          z-index: 30;
          box-sizing: border-box;
          pointer-events: auto;
        }
        #adboxverticalleftCurse,
        #adboxverticalCurse,
        #adboxverticalleft,
        #adboxvertical {
          pointer-events: none !important;
        }
        .tbc_groups_window{
          height: 100%;
          display: flex;
          flex-direction: column;
          border-radius: 6px;
          overflow: hidden;
        }
        .tbc_groups_top{
          height: 30px;
          display:flex;
          align-items:center;
          gap:8px;
          padding: 0 10px;
          color: #fff;
          background: rgb(0, 160, 153);
          border-bottom: 1px solid rgba(0,0,0,0.25);
          flex: 0 0 auto;
          user-select: none;
          touch-action: none;
          cursor: grab;
        }
        #tbc_groups_panel.tbc_groups_panel_dragging .tbc_groups_top{
          cursor: grabbing;
        }
        .tbc_groups_drag_grip{
          width: 14px;
          height: 14px;
          display:grid;
          grid-template-columns: repeat(3, 1fr);
          grid-template-rows: repeat(3, 1fr);
          gap: 1px;
          align-items: center;
          justify-items: center;
          flex: 0 0 auto;
        }
        .tbc_groups_drag_grip_dot{
          display:block;
          width: 2px;
          height: 2px;
          border-radius: 999px;
          background: rgba(255,255,255,0.95);
        }
        .tbc_groups_top_title{
          font-size: 13px;
          font-weight: 700;
          line-height: 1;
          white-space: nowrap;
        }
        .tbc_groups_close_btn{
          margin-left:auto;
          flex: 0 0 auto;
          width: 22px;
          height: 22px;
          min-width: 22px;
          min-height: 22px;
          line-height: 20px;
          text-align: center;
          border-radius: 50%;
          border: 0;
          color: #fff;
          background: #7f6151;
          box-shadow: 0 2px 4px rgba(0,0,0,0.35);
          cursor: pointer;
          font-weight: 700;
          padding: 0;
        }
        .tbc_groups_body{
          background: #d4dde2;
          color: #1f2a33;
          padding: 8px;
          overflow-y: auto;
          overflow-x: hidden;
          flex: 1 1 auto;
          box-sizing: border-box;
        }
        .tbc_groups_head{
          display:flex;
          align-items:flex-start;
          justify-content:space-between;
          gap:8px;
        }
        .tbc_groups_sub{ font-size:11px; opacity:.9; margin-top:2px; font-weight:700; }
        .tbc_groups_actions{ display:flex; gap:6px; }
        .tbc_groups_btn{
          border: 1px solid rgba(0,0,0,0.25);
          background: rgb(127, 97, 81);
          color:#fff;
          padding: 2px 8px;
          border-radius: 6px;
          font-size: 11px;
          cursor: pointer;
        }
        .tbc_groups_btn:disabled{
          opacity: .5;
          cursor: default;
        }
        .tbc_groups_hint{ margin-top:6px; font-size:11px; opacity:.8; }
        .tbc_groups_list{ margin-top:8px; display:flex; flex-direction:column; gap:7px; }
        .tbc_groups_card{
          border: 1px solid rgba(0,0,0,0.16);
          border-radius: 6px;
          padding: 6px;
          background: rgba(255,255,255,0.55);
        }
        .tbc_groups_card_top{
          display:flex;
          align-items:center;
          gap:7px;
          font-size:12px;
        }
        .tbc_groups_dot{
          width:10px;
          height:10px;
          border-radius:999px;
          border:1px solid rgba(255,255,255,0.25);
          flex:0 0 auto;
        }
        .tbc_groups_name{ font-weight:700; }
        .tbc_groups_group_btn{
          margin-left:auto;
          width:20px;
          height:20px;
          border:1px solid rgba(0,0,0,0.25);
          border-radius:50%;
          background: transparent;
          color:#1f2a33;
          font-size:12px;
          line-height:1;
          cursor:pointer;
          padding:0;
          opacity:1;
          pointer-events:auto;
          transition: opacity 0.12s ease;
          display:flex;
          align-items:center;
          justify-content:center;
        }
        .tbc_groups_group_btn:hover{
          background: rgba(24, 43, 58, 0.16);
        }
        .tbc_groups_group_btn:disabled{
          opacity:.9;
          color: rgba(31,42,51,0.85);
          border-color: rgba(0,0,0,0.35);
          pointer-events:none;
        }
        .tbc_groups_count{
          opacity:.8;
          font-size:10px;
          border:1px solid rgba(0,0,0,0.18);
          border-radius:999px;
          padding:1px 6px;
        }
        .tbc_groups_players{
          display:grid;
          grid-template-columns: 1fr 1fr;
          gap:6px;
          margin-top:6px;
          min-width: 0;
        }
        .tbc_groups_player{
          position: relative;
          min-height: 26px;
          border:1px solid rgba(0,0,0,0.18);
          border-radius:6px;
          background: rgba(255,255,255,0.75);
          padding:4px 6px;
          display:flex;
          align-items:center;
          gap: 6px;
          box-sizing:border-box;
          min-width: 0;
        }
        .tbc_groups_player.has-actions{
          cursor: pointer;
        }
        .tbc_groups_player_name{
          font-size:10px;
          font-weight:600;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          display:inline-flex;
          align-items:center;
          gap:4px;
          flex: 1 1 auto;
          min-width: 0;
          box-sizing: border-box;
        }
        .tbc_groups_player.has-actions .tbc_groups_player_name{
          padding-right:0;
        }
        .tbc_groups_player_actions{
          position:static;
          margin-left:auto;
          display:flex;
          gap:3px;
          opacity:0;
          pointer-events:none;
          transition: opacity 0.12s ease;
          flex: 0 0 auto;
          z-index: 2;
        }
        .tbc_groups_player.has-actions .tbc_groups_player_actions{
          opacity:1;
          pointer-events:auto;
        }
        .tbc_groups_player:hover .tbc_groups_player_actions,
        .tbc_groups_player:focus-within .tbc_groups_player_actions{
          opacity:1;
          pointer-events:auto;
        }
        .tbc_groups_player_btn{
          width:20px;
          height:20px;
          border:1px solid rgba(0,0,0,0.25);
          border-radius:50%;
          background: transparent;
          color:#1f2a33;
          font-size:12px;
          line-height:1;
          cursor:pointer;
          padding:0;
          display:flex;
          align-items:center;
          justify-content:center;
        }
        .tbc_groups_player_btn:hover{
          background: rgba(24, 43, 58, 0.16);
        }
        .tbc_groups_empty{ font-size:11px; opacity:.7; }
        #tbc_points_panel{
          position: fixed;
          z-index: 30;
          box-sizing: border-box;
          pointer-events: auto;
        }
        .tbc_points_window{
          height: 100%;
          display: flex;
          flex-direction: column;
          border-radius: 6px;
          overflow: hidden;
        }
        .tbc_points_top{
          height: 30px;
          display:flex;
          align-items:center;
          gap:8px;
          padding: 0 10px;
          color: #fff;
          background: rgb(0, 160, 153);
          border-bottom: 1px solid rgba(0,0,0,0.25);
          flex: 0 0 auto;
          user-select: none;
          touch-action: none;
          cursor: grab;
        }
        #tbc_points_panel.tbc_points_panel_dragging .tbc_points_top{
          cursor: grabbing;
        }
        .tbc_points_drag_grip{
          width: 14px;
          height: 14px;
          display:grid;
          grid-template-columns: repeat(3, 1fr);
          grid-template-rows: repeat(3, 1fr);
          gap: 1px;
          align-items: center;
          justify-items: center;
          flex: 0 0 auto;
        }
        .tbc_points_drag_grip_dot{
          display:block;
          width: 2px;
          height: 2px;
          border-radius: 999px;
          background: rgba(255,255,255,0.95);
        }
        .tbc_points_top_title{
          font-size: 13px;
          font-weight: 700;
          line-height: 1;
          white-space: nowrap;
        }
        .tbc_points_close_btn{
          margin-left:auto;
          flex: 0 0 auto;
          width: 22px;
          height: 22px;
          min-width: 22px;
          min-height: 22px;
          line-height: 20px;
          text-align: center;
          border-radius: 50%;
          border: 0;
          color: #fff;
          background: #7f6151;
          box-shadow: 0 2px 4px rgba(0,0,0,0.35);
          cursor: pointer;
          font-weight: 700;
          padding: 0;
        }
        .tbc_points_body{
          background: #d4dde2;
          color: #1f2a33;
          padding: 8px;
          overflow-y: auto;
          overflow-x: hidden;
          flex: 1 1 auto;
          box-sizing: border-box;
          font-size: 11px;
        }
        .tbc_points_hint{
          opacity: .85;
          margin-bottom: 8px;
          font-size: 11px;
          font-weight: 600;
        }
        .tbc_points_section{
          border: 1px solid rgba(0,0,0,0.16);
          border-radius: 6px;
          background: rgba(255,255,255,0.55);
          padding: 6px;
          margin-bottom: 8px;
        }
        .tbc_points_section_title{
          font-size: 12px;
          font-weight: 700;
          margin-bottom: 5px;
          display: flex;
          align-items: center;
          gap: 6px;
        }
        .tbc_points_section_badge{
          font-size: 10px;
          opacity: .8;
          border: 1px solid rgba(0,0,0,0.2);
          border-radius: 999px;
          padding: 1px 6px;
        }
        .tbc_points_list{
          display: flex;
          flex-direction: column;
          gap: 5px;
        }
        .tbc_points_row{
          display: grid;
          grid-template-columns: 12px 1fr auto;
          align-items: center;
          gap: 6px;
          border: 1px solid rgba(0,0,0,0.14);
          border-radius: 6px;
          background: rgba(255,255,255,0.78);
          padding: 3px 6px;
        }
        .tbc_points_dot{
          width: 10px;
          height: 10px;
          border-radius: 999px;
          border: 1px solid rgba(0,0,0,0.18);
          background: rgba(0,0,0,0.12);
        }
        .tbc_points_name{
          font-weight: 700;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          min-width: 0;
        }
        .tbc_points_score{
          font-weight: 800;
          font-size: 12px;
        }
        .tbc_points_warn{
          border: 1px solid rgba(150, 35, 35, 0.45);
          border-radius: 6px;
          background: rgba(181, 48, 48, 0.16);
          color: rgb(150, 35, 35);
          padding: 8px;
          font-size: 11px;
          font-weight: 700;
        }
        .tbc_points_group_card{
          border: 1px solid rgba(0,0,0,0.14);
          border-radius: 6px;
          background: rgba(255,255,255,0.68);
          padding: 5px;
        }
        .tbc_points_contribs{
          margin-top: 4px;
          display: flex;
          flex-direction: column;
          gap: 3px;
          padding-left: 18px;
        }
        .tbc_points_contrib_row{
          display: grid;
          grid-template-columns: 1fr auto;
          gap: 6px;
          opacity: .92;
          font-size: 10px;
        }
        .tbc_points_contrib_name{
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          min-width: 0;
        }
        .tbc_points_contrib_score{
          font-weight: 700;
        }
        .tbc_points_empty{
          font-size: 11px;
          opacity: .75;
          padding: 2px 0;
        }
        #ingamechatcontent {
          display: flex;
          flex-direction: column;
          justify-content: flex-end;
          --tbc-ingame-line-height: 19px;
          line-height: var(--tbc-ingame-line-height);
          width: 100% !important;
          margin: 0 !important;
          padding: 0 !important;
          box-sizing: border-box;
          overflow-y: auto !important;
          scrollbar-gutter: auto;
        }
        #ingamechatcontent.tbc_ingame_scroll_focus {
          display: block;
          justify-content: initial;
          overflow-y: hidden !important;
          padding-right: 0 !important;
        }
        #ingamechatcontent.tbc_ingame_scroll_focus.tbc_ingame_has_scrollbar {
          overflow-y: scroll !important;
          padding-right: 2px !important;
        }
        #ingamechatcontent.tbc_ingame_scroll_focus.tbc_ingame_has_scrollbar:not(.tbc_ingame_bg_on) {
          scrollbar-color: rgba(210, 220, 235, 0.85) transparent;
        }
        #ingamechatcontent.tbc_ingame_scroll_focus.tbc_ingame_has_scrollbar:not(.tbc_ingame_bg_on)::-webkit-scrollbar {
          width: 9px;
        }
        #ingamechatcontent.tbc_ingame_scroll_focus.tbc_ingame_has_scrollbar:not(.tbc_ingame_bg_on)::-webkit-scrollbar-track {
          background: transparent;
          border-radius: 999px;
        }
        #ingamechatcontent.tbc_ingame_scroll_focus.tbc_ingame_has_scrollbar:not(.tbc_ingame_bg_on)::-webkit-scrollbar-thumb {
          background: rgba(210, 220, 235, 0.85);
          border-radius: 999px;
          border: 1px solid rgba(20, 30, 40, 0.35);
        }
        #ingamechatcontent.tbc_ingame_scroll_focus.tbc_ingame_has_scrollbar:not(.tbc_ingame_bg_on)::-webkit-scrollbar-thumb:hover {
          background: rgba(230, 238, 248, 0.92);
        }
        #ingamechatcontent > * {
          display: block;
          flex: 0 0 auto;
          width: 100%;
          box-sizing: border-box;
          margin: 0;
          min-height: var(--tbc-ingame-line-height);
          line-height: var(--tbc-ingame-line-height);
          padding: 0 4px;
          border-radius: 0;
          background: transparent;
          white-space: normal;
          overflow-wrap: anywhere;
          word-break: break-word;
          overflow: hidden;
          text-overflow: clip;
          contain: paint;
        }
        #ingamechatcontent.tbc_ingame_bg_on > * {
          background: rgba(0, 0, 0, 0.22);
        }
        #ingamechatcontent.tbc_ingame_scroll_focus.tbc_ingame_has_scrollbar > * {
          width: calc(100% - 5px);
        }
        #ingamechatcontent > * * {
          line-height: inherit !important;
          white-space: normal;
          overflow-wrap: anywhere;
          word-break: break-word;
        }
        #ingamechatcontent .ingamechatname,
        #ingamechatcontent .ingamechatmessage,
        #ingamechatcontent .ingamechattext,
        #ingamechatcontent .newbonklobby_chat_status {
          text-shadow: none !important;
        }
        #ingamechatcontent .newbonklobby_chat_status,
        #ingamechatcontent .ingamechatstatus {
          display: inline;
          white-space: normal !important;
          overflow-wrap: anywhere;
          word-break: break-word;
        }
        #ingamechatbox #ingamechatinputtext {
          display: block;
          width: 100% !important;
          margin: 0 !important;
          box-sizing: border-box;
        }
      `;
      document.head.appendChild(style);
    }

    function extractNameFromChatNameSpan(span) {
      const raw = (span && span.textContent ? span.textContent : '').trim();
      return extractChatName(raw);
    }

    function getLobbyPlayerIdentityPairs() {
      const pairs = [];
      const seen = new Set();
      const roots = [
        $('newbonklobby_playerbox_elementcontainer'),
        $('newbonklobby_playerbox_leftelementcontainer'),
        $('newbonklobby_playerbox_rightelementcontainer'),
        $('newbonklobby_specbox_elementcontainer'),
      ].filter(Boolean);

      const pushPair = (row) => {
        if (!row || !(row instanceof Element)) return;
        const nameEl = row.querySelector('.newbonklobby_playerentry_name');
        const levelEl = row.querySelector('.newbonklobby_playerentry_level');
        if (!nameEl || !levelEl) return;

        const nameText = String(nameEl.textContent || '').trim();
        const levelTextRaw = String(levelEl.textContent || '').trim();
        if (!nameText || !levelTextRaw) return;

        const dedupeKey = `${normalizeName(nameText)}|${levelTextRaw.toLowerCase()}`;
        if (seen.has(dedupeKey)) return;
        seen.add(dedupeKey);
        pairs.push({ nameText, levelTextRaw });
      };

      roots.forEach((root) => {
        root.querySelectorAll('.newbonklobby_playerentry').forEach((row) => pushPair(row));
      });

      if (!pairs.length) {
        document
          .querySelectorAll('#newbonklobby_playerbox .newbonklobby_playerentry, #newbonklobby_specbox .newbonklobby_playerentry')
          .forEach((row) => pushPair(row));
      }

      return pairs;
    }

    function getLobbyAccountInfo() {
      const accountSet = new Set();
      const guestSet = new Set();
      const levelMap = new Map();
      const pairs = getLobbyPlayerIdentityPairs();
      pairs.forEach(({ nameText, levelTextRaw }) => {
        const nm = String(nameText || '').trim();
        if (!nm) return;
        const key = normalizeName(nm);
        const levelText = String(levelTextRaw || '').trim().toLowerCase();
        const isGuest = levelText === 'guest';
        const isAccount = !!levelText && !isGuest;

        if (isGuest) guestSet.add(key);
        else if (isAccount) accountSet.add(key);

        if (isGuest) levelMap.set(key, 'guest');
        else if (isAccount) levelMap.set(key, 'level');
        else if (!levelMap.has(key)) levelMap.set(key, 'unknown');
      });
      return { accountSet, guestSet, levelMap };
    }

    let cachedLobbyAccountInfo = null;
    let cachedLobbyAccountInfoAt = 0;

    function getLobbyAccountInfoCached(maxAgeMs = 800) {
      const now = Date.now();
      if (cachedLobbyAccountInfo && (now - cachedLobbyAccountInfoAt) <= Math.max(0, maxAgeMs)) {
        return cachedLobbyAccountInfo;
      }
      cachedLobbyAccountInfo = getLobbyAccountInfo();
      cachedLobbyAccountInfoAt = now;
      return cachedLobbyAccountInfo;
    }

    function getSelfNameNorm() {
        const el = $('pretty_top_name');
        return el ? normalizeName(el.textContent || '') : '';
    }

    function getLobbyMessageText(msgContainer) {
        const txt = msgContainer.querySelector('.newbonklobby_chat_msg_txt');
        return (txt ? txt.textContent : msgContainer.textContent || '').trim();
    }

    function getInGameMessageText(row, nameSpan) {
        const textSpan = row.querySelector('.ingamechatmessage, .ingamechattext');
        if (textSpan) return (textSpan.textContent || '').trim();
        const full = (row.textContent || '').trim();
        const nameRaw = (nameSpan && nameSpan.textContent ? nameSpan.textContent : '').trim();
        return nameRaw ? full.replace(nameRaw, '').trim() : full;
    }

    function getLobbyMessagePartsFromRow(msgEl) {
        const nameSpan = msgEl.querySelector('.newbonklobby_chat_msg_name');
        const txtSpan = msgEl.querySelector('.newbonklobby_chat_msg_txt');
        let sender = nameSpan ? extractNameFromChatNameSpan(nameSpan) : '';
        let msgText = txtSpan ? (txtSpan.textContent || '').trim() : '';

        if (!sender || !msgText) {
            const full = (msgEl.textContent || '').trim();
            const idx = full.indexOf(':');
            const idxWide = full.indexOf(':');
            const splitAt = (idx >= 0 && idxWide >= 0) ? Math.min(idx, idxWide) : Math.max(idx, idxWide);
            if (splitAt > 0) {
                sender = sender || extractChatName(full.slice(0, splitAt));
                msgText = msgText || full.slice(splitAt + 1).trim();
            } else {
                msgText = msgText || full;
            }
        }

        return { sender, msgText, nameSpan };
    }

    function getInGameMessagePartsFromRow(row) {
        const nameSpan = row.querySelector('.ingamechatname');
        const sender = nameSpan ? extractNameFromChatNameSpan(nameSpan) : '';
        const msgText = getInGameMessageText(row, nameSpan);
        return { sender, msgText, nameSpan };
    }

    function isSystemStatusRow(row, isLobby, isInGame) {
      if (!row || !(row instanceof Element)) return false;
      if (hasSystemPrefix(row.textContent || '')) return true;

      if (isLobby) {
        if (row.querySelector('.newbonklobby_chat_status')) return true;
        if (isLobbyMapRequestRow(row)) return true;
        return false;
      }

      if (isInGame) {
        if (row.dataset && row.dataset.tbcMirroredStatus === '1') return true;
        if (row.querySelector('.newbonklobby_chat_status')) return true;
        if (!row.querySelector('.ingamechatname') && !!getIngameStatusTextFromRow(row)) return true;
      }

      return false;
    }

    function hasDup(list, value, normalizeFn) {
        const v = normalizeFn(value);
        return list.some((x) => normalizeFn(x) === v);
    }

    function messageIsBlacklisted(senderName) {
      const n = normalizeName(senderName);

      if (chatState.blacklistUsers.some((u) => normalizeName(u) === n)) return true;

      return false;
    }

    window.tbcIsNameBlacklisted = (name) => {
      return messageIsBlacklisted(name);
    };

    window.tbcToggleBlacklistName = (name) => {
      const raw = String(name || '').trim();
      if (!raw) return false;
      const n = normalizeName(raw);
      if (!n) return false;
      let changed = false;
      mutateChatState((state) => {
        const exists = state.blacklistUsers.some((x) => normalizeName(x) === n);
        if (exists) {
          const before = state.blacklistUsers.length;
          state.blacklistUsers = state.blacklistUsers.filter((x) => normalizeName(x) !== n);
          changed = state.blacklistUsers.length !== before;
          return;
        }
        state.blacklistUsers.push(raw);
        changed = true;
      });
      if (typeof refreshChatSettingsUi === 'function') refreshChatSettingsUi();
      scheduleChatScan();
      return changed;
    };

    function isPlayerSender(senderNorm, selfNorm, accountNameSet, guestNameSet, canVerify) {
      if (!senderNorm) return false;
      if (selfNorm && senderNorm === selfNorm) return true;
      if (senderNorm === 'guest') return true;
      if (accountNameSet.has(senderNorm) || guestNameSet.has(senderNorm)) return true;

      if (!canVerify) {
        if (
          senderNorm === 'system' ||
          senderNorm === 'server' ||
          senderNorm === 'announcement' ||
          senderNorm === 'announcer'
        ) {
          return false;
        }
        return true;
      }

      return false;
    }

    function unwrapHiddenIfPresent(msgEl) {
      if (!msgEl) return;
      const wrap = msgEl.querySelector(':scope > .tbc_chat_original');
      const placeholder = msgEl.querySelector(':scope > .tbc_chat_hidden_notice');
      if (wrap) {
        while (wrap.firstChild) msgEl.insertBefore(wrap.firstChild, wrap);
        wrap.remove();
      }
      if (placeholder) placeholder.remove();
      delete msgEl.dataset.tbcHiddenReady;
      delete msgEl.dataset.tbcUserRevealed;
    }

    function setBlacklistedPresentation(msgEl, isBlacklisted) {
        if (!msgEl) return;
        unwrapHiddenIfPresent(msgEl);
        if (isBlacklisted) msgEl.classList.add('tbc_chat_invisible');
        else msgEl.classList.remove('tbc_chat_invisible');
    }

    let chatScanQueued = false;
    let lastFullChatScanAt = 0;
    let chatTouchedQueued = false;
    const touchedChatRows = new Set();
    const messageRowsByAuthor = new Map();
    let slashCommandsInstalled = false;
    let lastSlashSig = '';
    let lastSlashAt = 0;
    let lastSlashSuppress = false;
    let lastUnknownSlashHelpAt = 0;
    let topAdClickthroughObserver = null;
    let lobbyStatusMirrorSeq = 0;
    let lobbyUserPinnedUp = false;
    let lobbyScrollContainerRef = null;
    let wasIngameChatInteracting = false;
    let stashedIngameSystemRows = [];
    let stashedIngameSystemSeq = 0;
    let replaySystemProtectionDisabled = false;
    let replayRendererWasVisibleOnce = false;
    const stashedReplayLobbyRows = new Map();

    function clearTransientChatCarryoverState() {
      touchedChatRows.clear();
      messageRowsByAuthor.clear();
      resetWinnerBoardTransientState();

      if (stashedIngameSystemRows.length) {
        stashedIngameSystemRows.forEach((entry) => {
          if (!entry || typeof entry !== 'object') return;
          const row = entry.row;
          const placeholder = entry.placeholder;
          if (placeholder instanceof Element && placeholder.parentElement) placeholder.remove();
          if (row instanceof Element) {
            delete row.dataset.tbcStashedSystem;
            delete row.dataset.tbcStashedSystemSeq;
            if (row.parentElement) row.remove();
          }
        });
        stashedIngameSystemRows = [];
      }

      if (stashedReplayLobbyRows.size) {
        for (const entry of stashedReplayLobbyRows.values()) {
          if (!entry || typeof entry !== 'object') continue;
          const row = entry.row;
          const placeholder = entry.placeholder;
          if (placeholder instanceof Element && placeholder.parentElement) placeholder.remove();
          if (row instanceof Element && row.parentElement) row.remove();
        }
        stashedReplayLobbyRows.clear();
      }
    }

    function scheduleChatScan() {
      if (chatScanQueued) return;
      chatScanQueued = true;
      requestAnimationFrame(() => {
        chatScanQueued = false;
        scanAndApplyChatRules();
      });
    }

    function forceTopAdOverlaysClickThrough() {
      try {
        const topWin = window.top;
        if (!topWin || !topWin.document) return;
        const doc = topWin.document;
        const ids = [
          'adboxverticalleftCurse',
          'adboxverticalCurse',
          'adboxverticalleft',
          'adboxvertical',
        ];
        ids.forEach((id) => {
          const el = doc.getElementById(id);
          if (!el || !(el instanceof topWin.Element)) return;
          el.style.setProperty('pointer-events', 'none', 'important');
        });
      } catch {}
    }

    function ensureTopAdClickthroughWatcher() {
      if (topAdClickthroughObserver) return;
      forceTopAdOverlaysClickThrough();
      try {
        const topWin = window.top;
        if (!topWin || !topWin.document) return;
        const root = topWin.document.body || topWin.document.documentElement;
        if (!root) return;
        topAdClickthroughObserver = new MutationObserver(() => forceTopAdOverlaysClickThrough());
        topAdClickthroughObserver.observe(root, {
          childList: true,
          subtree: true,
        });
      } catch {}
    }

    function getLobbyScrollContainer() {
      const host = $('newbonklobby_chat_content');
      if (!host) return null;

      const isScrollable = (el) => !!el && (el.scrollHeight - el.clientHeight) > 2;
      if (isScrollable(host)) return host;

      let cur = host.parentElement;
      while (cur && cur !== document.body) {
        if (isScrollable(cur)) return cur;
        cur = cur.parentElement;
      }

      return host;
    }

    function isContainerNearBottom(el, thresholdPx = 10) {
      if (!el) return false;
      return (el.scrollHeight - el.scrollTop - el.clientHeight) < thresholdPx;
    }

    function isContainerExactlyAtBottom(el) {
      if (!el) return false;
      return Math.abs((el.scrollHeight - el.clientHeight) - el.scrollTop) <= 1;
    }

    function updateLobbyPinnedStateFromContainer() {
      const scrollEl = lobbyScrollContainerRef || getLobbyScrollContainer();
      if (!scrollEl) return;
      lobbyUserPinnedUp = !isContainerNearBottom(scrollEl, 24);
    }

    function ensureLobbyScrollContainerTracking() {
      const scrollEl = getLobbyScrollContainer();
      if (!scrollEl) return null;
      if (lobbyScrollContainerRef === scrollEl) return scrollEl;

      if (lobbyScrollContainerRef) {
        lobbyScrollContainerRef.removeEventListener('scroll', updateLobbyPinnedStateFromContainer);
      }

      lobbyScrollContainerRef = scrollEl;
      updateLobbyPinnedStateFromContainer();
      lobbyScrollContainerRef.addEventListener('scroll', updateLobbyPinnedStateFromContainer, { passive: true });
      return scrollEl;
    }

    function maybeScrollLobbyToBottom(scrollEl, wasNearBottom = false) {
      if (!scrollEl) return;

      if (wasNearBottom) {
        scrollEl.scrollTop = scrollEl.scrollHeight;
        return;
      }

      updateLobbyPinnedStateFromContainer();
      if (lobbyUserPinnedUp) return;

      if (isContainerNearBottom(scrollEl, 10)) {
        scrollEl.scrollTop = scrollEl.scrollHeight;
      }
    }

    function maybeScrollIngameToBottom(host, wasNearBottom = false) {
      if (!host) return;
      if (wasNearBottom || isContainerExactlyAtBottom(host)) {
        host.scrollTop = host.scrollHeight;
      }
    }

    function normalizeStatusText(text) {
      let s = String(text || '').replace(/\s+/g, ' ').trim().toLowerCase();
      s = s.replace(/[.!?]+$/g, '');
      return s;
    }

    function isElementActuallyVisible(el) {
      if (!el || !(el instanceof Element)) return false;
      const cs = window.getComputedStyle(el);
      if (!cs) return false;
      if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
      if ((el.offsetWidth || 0) <= 0 && (el.offsetHeight || 0) <= 0) {
        if ((el.getClientRects ? el.getClientRects().length : 0) <= 0) return false;
      }
      return true;
    }

    function updateReplaySystemProtectionLatch() {
      if (replaySystemProtectionDisabled) return;
      const renderer = $('gamerenderer');
      if (!renderer) return;
      const lobby = $('newbonklobby');
      const rendererVisible = isElementActuallyVisible(renderer);
      const lobbyVisible = isElementActuallyVisible(lobby);
      if (rendererVisible) {
        replayRendererWasVisibleOnce = true;
        return;
      }
      if (replayRendererWasVisibleOnce && lobbyVisible) {
        replaySystemProtectionDisabled = true;
        if (!chatState.showSystemMessages) scheduleChatScan();
      }
    }

    function isReplayRecorderSystemText(normalizedCore) {
      return (
        normalizedCore === 'replay must be at least 5 seconds long' ||
        normalizedCore === 'the last 15 seconds have been recorded to the main menu' ||
        normalizedCore === 'please wait at least 15 seconds since the last replay' ||
        normalizedCore === 'no replays in football mode'
      );
    }

    function forceReplaySystemRowColor(row) {
      if (!row || !(row instanceof Element)) return;
      const replayColor = chatState.useCustomSystemMessageColors
        ? (getSystemCategoryColorHex('replay') || REPLAY_SYSTEM_COLOR)
        : REPLAY_SYSTEM_COLOR;
      const carriers = row.querySelectorAll(
        '.newbonklobby_chat_status, .ingamechatmessage, .ingamechattext, .ingamechatstatus'
      );
      if (carriers.length) {
        carriers.forEach((el) => {
          if (el.style.color !== replayColor) el.style.color = replayColor;
        });
      } else if (row.style.color !== replayColor) {
        row.style.color = replayColor;
      }
    }

    function stashIngameSystemRow(row) {
      row = resolveIngameRow(row);
      if (!row || !(row instanceof Element)) return false;
      if (row.dataset.tbcStashedSystem === '1') return true;

      stashedIngameSystemSeq += 1;
      row.dataset.tbcStashedSystem = '1';
      row.dataset.tbcStashedSystemSeq = String(stashedIngameSystemSeq);
      const host = $('ingamechatcontent');
      let placeholder = null;
      if (host && row.parentElement === host) {
        placeholder = document.createElement('div');
        placeholder.style.display = 'none';
        placeholder.dataset.tbcStashedSystemPlaceholder = '1';
        placeholder.dataset.tbcStashedSystemSeq = String(stashedIngameSystemSeq);
        host.insertBefore(placeholder, row);
      }
      stashedIngameSystemRows.push({
        row,
        placeholder,
        seq: stashedIngameSystemSeq,
      });
      row.remove();
      return true;
    }

    function restoreStashedIngameSystemRows() {
      if (!chatState.showSystemMessages) return;
      if (!stashedIngameSystemRows.length) return;

      const host = $('ingamechatcontent');
      if (!host) return;
      const wasNearBottom = isContainerExactlyAtBottom(host);

      const seen = new Set();
      const rows = stashedIngameSystemRows
        .filter((entry) => {
          const row = entry && entry.row;
          return row instanceof Element && !seen.has(row) && seen.add(row);
        })
        .sort((a, b) => (a.seq || 0) - (b.seq || 0));

      stashedIngameSystemRows = [];

      rows.forEach((entry) => {
        const row = entry.row;
        const placeholder = entry.placeholder;
        delete row.dataset.tbcStashedSystem;
        delete row.dataset.tbcStashedSystemSeq;
        if (placeholder instanceof Element && placeholder.parentElement === host) {
          host.insertBefore(row, placeholder);
          placeholder.remove();
        } else if (row.parentElement !== host) {
          const fallbackSeenAt = getIngameRowSeenAt(row) || Date.now();
          insertIngameRowBySeenAt(host, row, fallbackSeenAt);
        }
        addTouchedRow(row);
      });

      scheduleTouchedRowsFlush();
      applyIngameChatLines();
      applyIngameChatFade();
      maybeScrollIngameToBottom(host, wasNearBottom);
    }

    function pruneHiddenIngameSystemRows() {
      if (chatState.showSystemMessages) return;
      const host = $('ingamechatcontent');
      if (!host) return;

      const rows = Array.from(host.children || []);
      for (const raw of rows) {
        const row = resolveIngameRow(raw) || raw;
        if (!(row instanceof Element)) continue;
        if (row.dataset.tbcStashedSystem === '1') continue;

        const systemStatus = isSystemStatusRow(row, false, true) || isMapRequestStatusText(row.textContent || '');
        if (!systemStatus) continue;
        if (isProtectedIngameSystemStatus(row)) continue;

        const text = row.textContent || '';
        maybeDesyncOnHostClosedRoomStatus(text);
        maybeDesyncOnHostTransferStatus(text);
        row.dataset.tbcMsgType = 'system';
        row.dataset.tbcMsgBlacklisted = '0';
        row.dataset.tbcMsgGrouped = '0';
        unregisterRowFromAuthorIndex(row);
        unwrapHiddenIfPresent(row);
        row.classList.remove('tbc_chat_invisible');
        stashIngameSystemRow(row);
      }
    }

    function getIngameStatusTextFromRow(row) {
      if (!row || !(row instanceof Element)) return '';
      if (row.querySelector('.ingamechatname')) return '';

      const span = row.querySelector('.newbonklobby_chat_status, .ingamechatmessage, .ingamechattext, .ingamechatstatus');
      const text = String(span ? span.textContent : row.textContent || '').trim();
      if (!text) return '';
      const first = text.charAt(0);
      if (first !== '*' && first !== '/') return '';
      return text;
    }

    function hasSystemPrefix(text) {
      const s = String(text || '').trim();
      if (!s) return false;
      const first = s.charAt(0);
      return first === '*' || first === '/';
    }

    function ingameHasStatusText(text, limit = 20) {
      return !!findRecentIngameStatusRowByText(text, limit);
    }

    function findRecentIngameStatusRowByText(text, limit = 20) {
      const host = $('ingamechatcontent');
      if (!host) return null;
      const target = normalizeStatusText(text);
      if (!target) return null;

      const rows = Array.from(host.children || []).slice(-Math.max(1, limit));
      for (const row of rows) {
        if (normalizeStatusText(getIngameStatusTextFromRow(row)) === target) return row;
      }
      return null;
    }

    function findRecentLobbyStatusColorByText(text, limit = 120) {
      const target = normalizeStatusText(text);
      if (!target) return '';
      const statuses = Array.from(
        document.querySelectorAll('#newbonklobby_chat_content .newbonklobby_chat_status')
      ).slice(-Math.max(1, limit));
      for (let i = statuses.length - 1; i >= 0; i--) {
        const st = statuses[i];
        if (!(st instanceof Element)) continue;
        if (normalizeStatusText(st.textContent || '') !== target) continue;
        const c = String((st.style && st.style.color) || '').trim();
        if (c) return c;
      }
      return '';
    }

    function resolveSystemStatusColor(statusEl, text = '') {
      const configured = getSystemColorForText(text || (statusEl ? statusEl.textContent : ''));
      if (configured && configured.color) return configured.color;

      const inlineColor = String((statusEl && statusEl.style && statusEl.style.color) || '').trim();
      if (inlineColor) return inlineColor;

      let computedColor = '';
      if (statusEl instanceof Element) {
        computedColor = String(window.getComputedStyle(statusEl).color || '').trim();
      }
      if (computedColor && computedColor !== 'rgba(0, 0, 0, 0)') {
        return computedColor;
      }

      const recentColor = findRecentLobbyStatusColorByText(text, 120);
      if (recentColor) return recentColor;

      return DEFAULT_SYSTEM_STATUS_COLOR;
    }

    function clearConfiguredSystemColorFromRow(row) {
      if (!row || !(row instanceof Element)) return;
      const carriers = row.querySelectorAll('.newbonklobby_chat_status, .ingamechatmessage, .ingamechattext, .ingamechatstatus');
      if (carriers.length) {
        carriers.forEach((el) => {
          if (el.dataset && el.dataset.tbcSystemColorApplied === '1') {
            el.style.removeProperty('color');
            delete el.dataset.tbcSystemColorApplied;
          }
        });
      } else if (row.dataset && row.dataset.tbcSystemColorApplied === '1') {
        row.style.removeProperty('color');
        delete row.dataset.tbcSystemColorApplied;
      }
    }

    function applyConfiguredSystemColorToRow(row, text) {
      if (!row || !(row instanceof Element)) return;
      if (!chatState.useCustomSystemMessageColors) {
        clearConfiguredSystemColorFromRow(row);
        return;
      }
      const payload = getSystemColorForText(text || row.textContent || '');
      const color = payload && payload.color ? payload.color : DEFAULT_SYSTEM_STATUS_COLOR;
      const category = payload && payload.category ? payload.category : 'defaultSystem';
      row.dataset.tbcSystemCategory = category;

      const carriers = row.querySelectorAll('.newbonklobby_chat_status, .ingamechatmessage, .ingamechattext, .ingamechatstatus');
      if (carriers.length) {
        carriers.forEach((el) => {
          if (el.style.color !== color) el.style.color = color;
          if (el.dataset) el.dataset.tbcSystemColorApplied = '1';
        });
      } else if (row.style.color !== color) {
        row.style.color = color;
        row.dataset.tbcSystemColorApplied = '1';
      }
    }

    function refreshVisibleSystemMessageRows() {
      const selectors = ['#newbonklobby_chat_content > *', '#ingamechatcontent > *'];
      selectors.forEach((sel) => {
        const rows = Array.from(document.querySelectorAll(sel)).slice(-240);
        rows.forEach((row) => {
          if (!(row instanceof Element)) return;
          const isInGame =
            (row.classList && row.classList.contains('ingamechatentry')) ||
            !!row.closest('#ingamechatcontent');
          const isLobby =
            !isInGame &&
            (
              (row.classList && row.classList.contains('newbonklobby_chat_msg')) ||
              (row.parentElement && row.parentElement.id === 'newbonklobby_chat_content') ||
              !!row.querySelector(':scope > .newbonklobby_chat_status')
            );
          if (!isLobby && !isInGame) return;
          if (!isSystemStatusRow(row, isLobby, isInGame)) return;
          if (chatState.useCustomSystemMessageColors) applyConfiguredSystemColorToRow(row, row.textContent || '');
          else clearConfiguredSystemColorFromRow(row);
        });
      });
    }

    function syncNativeIngameSystemColorFromLobby(row) {
      if (!row || !(row instanceof Element)) return;
      if (row.dataset && row.dataset.tbcMirroredStatus === '1') return;

      const text = getIngameStatusTextFromRow(row);
      if (!text) return;
      applyConfiguredSystemColorToRow(row, text);
    }

    function hasRecentNativeIngameStatusText(text, limit = 30, withinMs = 500) {
      const host = $('ingamechatcontent');
      if (!host) return false;
      const target = normalizeStatusText(text);
      if (!target) return false;

      const rows = Array.from(host.children || []).slice(-Math.max(1, limit));
      for (let i = rows.length - 1; i >= 0; i--) {
        const row = rows[i];
        if (!(row instanceof Element)) continue;
        if (row.dataset && row.dataset.tbcMirroredStatus === '1') continue;
        const rowText = normalizeStatusText(getIngameStatusTextFromRow(row) || row.textContent || '');
        if (rowText !== target) continue;

        const t = parseInt(
          String(row.dataset.tbcFadeBornAt || row.dataset.tbcMirroredAt || ''),
          10
        );
        if (!Number.isFinite(t) || t <= 0) return true;
        if ((Date.now() - t) <= Math.max(0, withinMs)) return true;
      }
      return false;
    }

    function isJoinLeaveStatusText(text) {
      const s = String(text || '').toLowerCase();
      return /\bhas joined the game\b/.test(s) || /\bhas left the game\b/.test(s);
    }

    function isMapRequestStatusText(text) {
      const s = normalizeStatusText(String(text || '')).replace(/^\*\s*/, '');
      if (!s) return false;
      return /\brequests\b/.test(s) && /\bby\b/.test(s);
    }

    function isProtectedIngameSystemStatus(row) {
      if (!row || !(row instanceof Element)) return false;
      const s = String(row.textContent || '')
        .replace(/\s+/g, ' ')
        .trim()
        .toLowerCase();
      if (!s) return false;
      const core = s.replace(/^\*\s*/, '');
      const normalizedCore = normalizeStatusText(core);

      if (/^\*\s+.+\s+has joined the game\.?$/.test(s)) return true;
      if (/^\*\s+.+\s+has left the game\.?$/.test(s)) return true;
      if (/^\*\s+.+\s+has left the game and .+ is now the game host\.?$/.test(s)) return true;
      if (/^(?:\*\s*)?.+\s+has given host privileges to .+, who is now the game host\.?$/.test(s)) return true;
      if (
        /\byou are now the host of this game\b/.test(normalizedCore) ||
        /\byou are now the game host\b/.test(normalizedCore)
      ) return true;

      if (isReplayRecorderSystemText(normalizedCore)) return !replaySystemProtectionDisabled;
      if (core.includes('room name changed to')) return true;
      if (core.includes('a new password has been set for this room')) return true;
      if (core.includes('this room no longer requires a password to join')) return true;
      if (core.includes('room pass changed to') || core.includes('room password changed to')) return true;
      if (core.includes('room pass cleared') || core.includes('room password cleared')) return true;
      if (
        core.includes('/roomname') &&
        core.includes('must type') &&
        core.includes('new desired room name')
      ) return true;
      if (
        core.includes('/roompass') &&
        core.includes('must type') &&
        core.includes('new desired password')
      ) return true;
      if (
        core.includes('must be room host to use this command') ||
        core.includes('must be game host to use this command')
      ) return true;
      if (
        core.includes('you must be logged in') &&
        (core.includes('map must be a bonk 2 map') || core.includes('map must be a bonk2 map'))
      ) return true;
      if (
        core.includes('you cannot favourite bonk 1 map') ||
        core.includes('you cannot favorite bonk 1 map')
      ) return true;
      if (core.includes('map added to favourite') || core.includes('map added to favorite')) return true;
      if (core.includes('map removed from favourite') || core.includes('map removed from favorite')) return true;

      return false;
    }

    function isLobbyMapRequestRow(row) {
      if (!row || !(row instanceof Element)) return false;

      const hasMapSuggestBits =
        !!row.querySelector('.newbonklobby_mapsuggest_low') &&
        !!row.querySelector('.newbonklobby_mapsuggest_high');
      if (!hasMapSuggestBits) return false;

      const nameSpan = row.querySelector('.newbonklobby_chat_msg_name');
      const nameText = String(nameSpan ? nameSpan.textContent : '').trim();
      if (!/^\*/.test(nameText)) return false;

      return isMapRequestStatusText(row.textContent || '');
    }

    function hasRecentIngameSystemText(text, limit = 20, withinMs = 250) {
      const row = findRecentIngameStatusRowByText(text, limit);
      if (!row) return false;

      const t = parseInt(
        String(row.dataset.tbcMirroredAt || row.dataset.tbcFadeBornAt || ''),
        10
      );
      if (!Number.isFinite(t) || t <= 0) return false;
      return (Date.now() - t) <= Math.max(0, withinMs);
    }

    function isRepeatableMirroredSystemText(text) {
      const s = normalizeStatusText(String(text || '')).replace(/^\*\s*/, '');
      if (!s) return false;
      if (/\bgame start(?:s|ing)? in [1-3]\b/.test(s)) return true;
      if (s.includes("you're doing that too much") || s.includes('you are doing that too much')) return true;
      return false;
    }

    function lobbyHasStatusText(text, limit = 20) {
      const target = normalizeStatusText(text);
      if (!target) return false;

      const spans = Array.from(document.querySelectorAll('#newbonklobby_chat_content .newbonklobby_chat_status'))
        .slice(-Math.max(1, limit));
      return spans.some((sp) => normalizeStatusText(sp.textContent || '') === target);
    }

    function syncMirroredIngameStatusFromLobby(statusEl) {
      if (!statusEl) return false;
      const sourceStatusId = statusEl.dataset ? statusEl.dataset.tbcStatusId : '';
      if (!sourceStatusId) return false;

      const host = $('ingamechatcontent');
      if (!host) return false;
      const wasNearBottom = isContainerExactlyAtBottom(host);

      const mirrored = host.querySelector(`.ingamechatentry[data-tbc-source-status-id="${sourceStatusId}"]`);
      if (!mirrored) return false;

      const sourceRow =
        (statusEl.closest && statusEl.closest('#newbonklobby_chat_content > div')) ||
        statusEl.parentElement;
      if (!sourceRow) return false;

      const oldBornAt = mirrored.dataset.tbcFadeBornAt || '';
      const oldMirroredAt = mirrored.dataset.tbcMirroredAt || '';
      const resolvedColor = resolveSystemStatusColor(statusEl, statusEl.textContent || '');

      mirrored.innerHTML = sourceRow.innerHTML;
      mirrored.classList.add('ingamechatentry');
      mirrored.dataset.tbcMirroredStatus = '1';
      mirrored.dataset.tbcSourceStatusId = sourceStatusId;
      if (oldBornAt) mirrored.dataset.tbcFadeBornAt = oldBornAt;
      if (oldMirroredAt) mirrored.dataset.tbcMirroredAt = oldMirroredAt;

      const statusSpans = mirrored.querySelectorAll('.newbonklobby_chat_status');
      statusSpans.forEach((sp) => {
        sp.classList.add('ingamechatmessage');
        if (!sp.style.display) sp.style.display = 'inline';
        sp.style.color = resolvedColor;
      });

      addTouchedRow(mirrored);
      scheduleTouchedRowsFlush();
      applyIngameChatLines();
      applyIngameChatFade();
      maybeScrollIngameToBottom(host, wasNearBottom);
      return true;
    }

    function getIngameRowSeenAt(row) {
      if (!row || !(row instanceof Element)) return 0;
      const t = parseInt(
        String(
          row.dataset.tbcFadeBornAt ||
          row.dataset.tbcMirroredAt ||
          ((row.querySelector(':scope > .newbonklobby_chat_status') || {}).dataset || {}).tbcStatusSeenAt ||
          ''
        ),
        10
      );
      return Number.isFinite(t) && t > 0 ? t : 0;
    }

    function insertIngameRowBySeenAt(host, row, seenAt) {
      if (!host || !row || !(row instanceof Element)) return;
      const eventAt = Number.isFinite(seenAt) && seenAt > 0 ? seenAt : Date.now();

      const siblings = Array.from(host.children || []);
      let insertBeforeNode = null;
      for (const sib of siblings) {
        if (!(sib instanceof Element)) continue;
        if (sib === row) continue;
        const sibAt = getIngameRowSeenAt(sib);
        if (sibAt > 0 && sibAt > eventAt) {
          insertBeforeNode = sib;
          break;
        }
      }
      if (insertBeforeNode) host.insertBefore(row, insertBeforeNode);
      else host.appendChild(row);
    }

    function mirrorLobbyStatusToIngame(statusEl, opts = null) {
      if (!statusEl) return;
      if (statusEl.dataset.tbcMirroredFromIngame === '1') return;

      const host = $('ingamechatcontent');
      if (!host) return;
      const wasNearBottom = isContainerExactlyAtBottom(host);

      if (!statusEl.dataset.tbcStatusId) {
        lobbyStatusMirrorSeq += 1;
        statusEl.dataset.tbcStatusId = `st_${Date.now()}_${lobbyStatusMirrorSeq}`;
      }
      let sourceStatusId = statusEl.dataset.tbcStatusId;
      const existingMirroredRow = sourceStatusId
        ? host.querySelector(`.ingamechatentry[data-tbc-source-status-id="${sourceStatusId}"]`)
        : null;

      if (existingMirroredRow) {
        const curText = normalizeStatusText(statusEl.textContent || '');
        const prevText = normalizeStatusText(
          getIngameStatusTextFromRow(existingMirroredRow) || existingMirroredRow.textContent || ''
        );

        if (curText && prevText && curText !== prevText) {
          lobbyStatusMirrorSeq += 1;
          sourceStatusId = `st_${Date.now()}_${lobbyStatusMirrorSeq}`;
          statusEl.dataset.tbcStatusId = sourceStatusId;
          delete statusEl.dataset.tbcMirroredIngame;
        } else {
          statusEl.dataset.tbcMirroredIngame = '1';
          syncMirroredIngameStatusFromLobby(statusEl);
          return;
        }
      } else if (statusEl.dataset.tbcMirroredIngame === '1') {
        delete statusEl.dataset.tbcMirroredIngame;
      }

      const requireRecentMs = opts && Number.isFinite(opts.requireRecentMs) ? opts.requireRecentMs : 0;
      const seenAtRaw = statusEl.dataset.tbcStatusSeenAt || '';
      const seenAt = parseInt(seenAtRaw, 10);
      if (requireRecentMs > 0) {
        if (!Number.isFinite(seenAt) || seenAt <= 0) return;
        if ((Date.now() - seenAt) > requireRecentMs) return;
      }

      const text = String(statusEl.textContent || '').trim();
      if (!text) return;
      const resolvedColor = resolveSystemStatusColor(statusEl, text);

      if (hasRecentNativeIngameStatusText(text, 40, 700)) {
        statusEl.dataset.tbcMirroredIngame = '1';
        return;
      }

      const sourceRow =
        (statusEl.closest && statusEl.closest('#newbonklobby_chat_content > div')) ||
        statusEl.parentElement;

      const row = sourceRow ? sourceRow.cloneNode(true) : document.createElement('div');
      if (!sourceRow) {
        const msg = document.createElement('span');
        msg.className = 'newbonklobby_chat_status';
        msg.textContent = text;
        msg.style.color = resolvedColor;
        row.appendChild(msg);
      }

      row.classList.add('ingamechatentry');
      row.dataset.tbcMirroredStatus = '1';
      const eventSeenAt = Number.isFinite(seenAt) && seenAt > 0 ? seenAt : Date.now();
      row.dataset.tbcMirroredAt = String(eventSeenAt);
      if (sourceStatusId) row.dataset.tbcSourceStatusId = sourceStatusId;

      const statusSpans = row.querySelectorAll('.newbonklobby_chat_status');
      statusSpans.forEach((sp) => {
        sp.classList.add('ingamechatmessage');
        if (!sp.style.display) sp.style.display = 'inline';
        sp.style.color = resolvedColor;
      });

      row.dataset.tbcFadeBornAt = String(eventSeenAt);
      insertIngameRowBySeenAt(host, row, eventSeenAt);
      statusEl.dataset.tbcMirroredIngame = '1';

      addTouchedRow(row);
      scheduleTouchedRowsFlush();
      applyIngameChatLines();
      applyIngameChatFade();
      maybeScrollIngameToBottom(host, wasNearBottom);
    }

    function mirrorLobbyMapRequestRowToIngame(rowEl, opts = null) {
      if (!rowEl || !(rowEl instanceof Element)) return;
      if (!isLobbyMapRequestRow(rowEl)) return;
      if (rowEl.dataset.tbcMirroredFromIngame === '1') return;

      const host = $('ingamechatcontent');
      if (!host) return;
      const wasNearBottom = isContainerExactlyAtBottom(host);

      if (!rowEl.dataset.tbcStatusId) {
        lobbyStatusMirrorSeq += 1;
        rowEl.dataset.tbcStatusId = `st_${Date.now()}_${lobbyStatusMirrorSeq}`;
      }
      let sourceStatusId = rowEl.dataset.tbcStatusId;
      const existingMirroredRow = sourceStatusId
        ? host.querySelector(`.ingamechatentry[data-tbc-source-status-id="${sourceStatusId}"]`)
        : null;
      if (existingMirroredRow) {
        const curText = normalizeStatusText(rowEl.textContent || '');
        const prevText = normalizeStatusText(
          getIngameStatusTextFromRow(existingMirroredRow) || existingMirroredRow.textContent || ''
        );
        if (curText && prevText && curText !== prevText) {
          lobbyStatusMirrorSeq += 1;
          sourceStatusId = `st_${Date.now()}_${lobbyStatusMirrorSeq}`;
          rowEl.dataset.tbcStatusId = sourceStatusId;
          delete rowEl.dataset.tbcMirroredIngame;
        } else {
          rowEl.dataset.tbcMirroredIngame = '1';
          return;
        }
      } else if (rowEl.dataset.tbcMirroredIngame === '1') {
        delete rowEl.dataset.tbcMirroredIngame;
      }

      const requireRecentMs = opts && Number.isFinite(opts.requireRecentMs) ? opts.requireRecentMs : 0;
      const seenAtRaw = rowEl.dataset.tbcStatusSeenAt || '';
      const seenAt = parseInt(seenAtRaw, 10);
      if (requireRecentMs > 0) {
        if (!Number.isFinite(seenAt) || seenAt <= 0) return;
        if ((Date.now() - seenAt) > requireRecentMs) return;
      }

      const text = String(rowEl.textContent || '').trim();
      if (!text) return;

      if (hasRecentNativeIngameStatusText(text, 40, 700)) {
        rowEl.dataset.tbcMirroredIngame = '1';
        return;
      }

      const row = rowEl.cloneNode(true);
      row.classList.add('ingamechatentry');
      row.dataset.tbcMirroredStatus = '1';
      const eventSeenAt = Number.isFinite(seenAt) && seenAt > 0 ? seenAt : Date.now();
      row.dataset.tbcMirroredAt = String(eventSeenAt);
      if (sourceStatusId) row.dataset.tbcSourceStatusId = sourceStatusId;

      row.dataset.tbcFadeBornAt = String(eventSeenAt);
      insertIngameRowBySeenAt(host, row, eventSeenAt);
      rowEl.dataset.tbcMirroredIngame = '1';

      addTouchedRow(row);
      scheduleTouchedRowsFlush();
      applyIngameChatLines();
      applyIngameChatFade();
      maybeScrollIngameToBottom(host, wasNearBottom);
    }

    function syncLobbyStatusesToIngame(limit = 120) {
      const host = $('ingamechatcontent');
      if (!host) return;
      const bootstrapRecentMs = 2200;

      const statuses = Array.from(
        document.querySelectorAll('#newbonklobby_chat_content .newbonklobby_chat_status')
      ).slice(-Math.max(1, limit));

      statuses.forEach((st) => {
        if (!(st instanceof Element)) return;
        if (!st.dataset.tbcStatusSeenAt) st.dataset.tbcStatusSeenAt = String(Date.now());
        if (!st.dataset.tbcStatusId) {
          lobbyStatusMirrorSeq += 1;
          st.dataset.tbcStatusId = `st_${Date.now()}_${lobbyStatusMirrorSeq}`;
        }
        const statusTextNorm = normalizeStatusText(st.textContent || '').replace(/^\*\s*/, '');
        const forceHostProtectedMirror =
          /\bhas given host privileges to\b/.test(statusTextNorm) && /\bwho is now the game host\b/.test(statusTextNorm) ||
          /\byou are now the host of this game\b/.test(statusTextNorm) ||
          /\byou are now the game host\b/.test(statusTextNorm) ||
          /\bhas left the game and .+ is now the game host\b/.test(statusTextNorm);
        const forceReplayProtectedMirror = isReplayRecorderSystemText(statusTextNorm);

        mirrorLobbyStatusToIngame(st, {
          requireRecentMs: (forceHostProtectedMirror || forceReplayProtectedMirror) ? 0 : bootstrapRecentMs,
        });
      });

      const mapReqRows = Array.from(
        document.querySelectorAll('#newbonklobby_chat_content > *')
      )
        .filter((row) => row instanceof Element && isLobbyMapRequestRow(row))
        .slice(-Math.max(1, limit));

      mapReqRows.forEach((row) => {
        if (!row.dataset.tbcStatusSeenAt) row.dataset.tbcStatusSeenAt = String(Date.now());
        if (!row.dataset.tbcStatusId) {
          lobbyStatusMirrorSeq += 1;
          row.dataset.tbcStatusId = `st_${Date.now()}_${lobbyStatusMirrorSeq}`;
        }
        mirrorLobbyMapRequestRowToIngame(row, { requireRecentMs: bootstrapRecentMs });
      });
    }

    function syncReplayStatusLobbyRows() {
      const host = $('newbonklobby_chat_content');
      if (!host) return;
      const lobbyScrollEl = ensureLobbyScrollContainerTracking();
      const lobbyWasNearBottom = lobbyScrollEl ? isContainerNearBottom(lobbyScrollEl, 10) : false;

      function restoreStashedReplayLobbyRowsIfAny() {
        if (!stashedReplayLobbyRows.size) return;
        const entries = Array.from(stashedReplayLobbyRows.entries());
        entries.forEach(([sourceId, stashed]) => {
          if (!stashed || !(stashed.row instanceof Element)) {
            stashedReplayLobbyRows.delete(sourceId);
            return;
          }
          const ph = stashed.placeholder;
          if (ph instanceof Element && ph.parentElement === host) {
            host.insertBefore(stashed.row, ph);
            ph.remove();
            stashedReplayLobbyRows.delete(sourceId);
          }
        });
      }

      if (!chatState.showSystemMessages) {
        host.querySelectorAll('[data-tbc-replay-lobby="1"]').forEach((row) => {
          if (!(row instanceof Element)) return;
          const sourceId = String(row.dataset.tbcReplaySourceId || '');
          if (!sourceId) {
            row.remove();
            return;
          }
          if (!stashedReplayLobbyRows.has(sourceId)) {
            const ph = document.createElement('div');
            ph.dataset.tbcReplayLobbyPlaceholder = '1';
            ph.dataset.tbcReplaySourceId = sourceId;
            ph.style.display = 'none';
            host.insertBefore(ph, row);
            stashedReplayLobbyRows.set(sourceId, { row, placeholder: ph });
          }
          row.remove();
        });
        return;
      }

      restoreStashedReplayLobbyRowsIfAny();

      const ingameHost = $('ingamechatcontent');
      if (!ingameHost) return;

      function getLobbyRowSeenAt(row) {
        if (!row || !(row instanceof Element)) return 0;
        const st = row.querySelector(':scope > .newbonklobby_chat_status');
        if (!st) return 0;
        const t = parseInt(
          String((st.dataset && st.dataset.tbcStatusSeenAt) || row.dataset.tbcReplaySeenAt || ''),
          10
        );
        return Number.isFinite(t) && t > 0 ? t : 0;
      }

      const rows = Array.from(ingameHost.children || []);
      rows.forEach((rawRow) => {
        const rowEl = resolveIngameRow(rawRow) || rawRow;
        if (!(rowEl instanceof Element)) return;

        const text = getIngameStatusTextFromRow(rowEl);
        if (!text) return;

        const replayCore = normalizeStatusText(text).replace(/^\*\s*/, '');
        if (!isReplayRecorderSystemText(replayCore)) return;

        if (!rowEl.dataset.tbcReplayLobbySourceId) {
          lobbyStatusMirrorSeq += 1;
          rowEl.dataset.tbcReplayLobbySourceId = `rpl_${Date.now()}_${lobbyStatusMirrorSeq}`;
        }
        const sourceId = rowEl.dataset.tbcReplayLobbySourceId;
        if (!sourceId) return;

        const eventSeenAt = (() => {
          const t = parseInt(
            String(
              rowEl.dataset.tbcFadeBornAt ||
              rowEl.dataset.tbcMirroredAt ||
              rowEl.dataset.tbcStatusSeenAt ||
              ''
            ),
            10
          );
          return Number.isFinite(t) && t > 0 ? t : Date.now();
        })();

        let wrapper = host.querySelector(`[data-tbc-replay-source-id="${sourceId}"]`);
        const hadWrapper = wrapper instanceof Element;
        if (!hadWrapper) {
          const stashed = stashedReplayLobbyRows.get(sourceId);
          if (stashed && stashed.row instanceof Element) wrapper = stashed.row;
          else {
            wrapper = document.createElement('div');
            wrapper.dataset.tbcReplayLobby = '1';
            wrapper.dataset.tbcReplaySourceId = sourceId;
          }
        }
        wrapper.dataset.tbcReplayLobby = '1';
        wrapper.dataset.tbcReplaySourceId = sourceId;
        wrapper.dataset.tbcReplaySeenAt = String(eventSeenAt);

        let span = wrapper.querySelector(':scope > .newbonklobby_chat_status');
        if (!(span instanceof Element)) {
          span = document.createElement('span');
          span.className = 'newbonklobby_chat_status';
          wrapper.textContent = '';
          wrapper.appendChild(span);
        }

      const color = chatState.useCustomSystemMessageColors
        ? (getSystemCategoryColorHex('replay') || REPLAY_SYSTEM_COLOR)
        : REPLAY_SYSTEM_COLOR;
      span.textContent = text;
      if (color) span.style.color = color;
      else span.style.color = '';
        span.dataset.tbcStatusSeenAt = String(eventSeenAt);
        span.dataset.tbcMirroredFromIngame = '1';
        span.dataset.tbcSourceIngameId = sourceId;

        const children = Array.from(host.children || []);
        let insertBeforeNode = null;
        for (const child of children) {
          if (!(child instanceof Element)) continue;
          if (child === wrapper) continue;
          const childSeenAt = getLobbyRowSeenAt(child);
          if (childSeenAt > 0 && childSeenAt > eventSeenAt) {
            insertBeforeNode = child;
            break;
          }
        }
        const stashed = stashedReplayLobbyRows.get(sourceId);
        const placeholder =
          stashed && stashed.placeholder instanceof Element ? stashed.placeholder : null;

        if (placeholder && placeholder.parentElement === host) {
          host.insertBefore(wrapper, placeholder);
          placeholder.remove();
          stashedReplayLobbyRows.delete(sourceId);
        } else if (!hadWrapper || wrapper.parentElement !== host) {
          if (insertBeforeNode) host.insertBefore(wrapper, insertBeforeNode);
          else host.appendChild(wrapper);
        }
      });

      maybeScrollLobbyToBottom(lobbyScrollEl, lobbyWasNearBottom);
    }

    function mirrorIngameStatusToLobby(rowEl) {
      if (!rowEl || !(rowEl instanceof Element)) return;
      if (rowEl.dataset.tbcMirroredStatus === '1') return;

      const host = $('newbonklobby_chat_content');
      if (!host) return;
      const lobbyScrollEl = ensureLobbyScrollContainerTracking();
      const lobbyWasNearBottom = lobbyScrollEl ? isContainerNearBottom(lobbyScrollEl, 10) : false;

      const text = getIngameStatusTextFromRow(rowEl);
      if (!text) return;

      const replayCore = normalizeStatusText(text).replace(/^\*\s*/, '');
      const replayStatus = isReplayRecorderSystemText(replayCore);
      if (replayStatus) {
        syncReplayStatusLobbyRows();
        return;
      }

      if (lobbyHasStatusText(text, 20)) {
        rowEl.dataset.tbcMirroredLobby = '1';
        return;
      }

      if (!rowEl.dataset.tbcIngameStatusId) {
        lobbyStatusMirrorSeq += 1;
        rowEl.dataset.tbcIngameStatusId = `igst_${Date.now()}_${lobbyStatusMirrorSeq}`;
      }
      const srcId = rowEl.dataset.tbcIngameStatusId;
      if (srcId && host.querySelector(`.newbonklobby_chat_status[data-tbc-source-ingame-id="${srcId}"]`)) {
        rowEl.dataset.tbcMirroredLobby = '1';
        return;
      }

      const sourceSpan = rowEl.querySelector('.newbonklobby_chat_status, .ingamechatmessage, .ingamechattext, .ingamechatstatus');
      const color = sourceSpan && sourceSpan.style ? String(sourceSpan.style.color || '') : '';

      const wrapper = document.createElement('div');
      const span = document.createElement('span');
      span.className = 'newbonklobby_chat_status';
      span.textContent = text;
      if (color) span.style.color = color;
      span.dataset.tbcStatusSeenAt = String(Date.now());
      span.dataset.tbcMirroredFromIngame = '1';
      if (srcId) span.dataset.tbcSourceIngameId = srcId;
      wrapper.appendChild(span);
      host.appendChild(wrapper);
      rowEl.dataset.tbcMirroredLobby = '1';
      maybeScrollLobbyToBottom(lobbyScrollEl, lobbyWasNearBottom);
    }

    function addLocalChatStatus(text, color = '#317dd7') {
      const host = document.getElementById('newbonklobby_chat_content');
      if (!host) return null;
      const lobbyScrollEl = ensureLobbyScrollContainerTracking();
      const lobbyWasNearBottom = lobbyScrollEl ? isContainerNearBottom(lobbyScrollEl, 10) : false;

      const row = document.createElement('div');
      const span = document.createElement('span');
      span.className = 'newbonklobby_chat_status';
      span.style.color = color;
      span.textContent = String(text || '');
      span.dataset.tbcStatusSeenAt = String(Date.now());
      row.appendChild(span);
      host.appendChild(row);
      maybeScrollLobbyToBottom(lobbyScrollEl, lobbyWasNearBottom);
      return row;
    }

    function addSyncDisabledStatusNotice(options = null) {
      const silent = !!(options && options.silent);
      if (silent) return;
      const row = addLocalChatStatus('[TBC] Sync is disabled during a running game. Sync in lobby.', 'rgb(181, 48, 48)');
      const inGameVisible =
        typeof isElementActuallyVisible === 'function' &&
        !!isElementActuallyVisible($('gamerenderer'));
      if (!inGameVisible || !(row instanceof Element)) return;
      const statusEl = row.querySelector('.newbonklobby_chat_status');
      if (!statusEl) return;
      mirrorLobbyStatusToIngame(statusEl, { requireRecentMs: 15000 });
    }

    function addDesyncDisabledStatusNotice(options = null) {
      const silent = !!(options && options.silent);
      if (silent) return;
      const row = addLocalChatStatus('[TBC] Desync is disabled during a running game. Desync in lobby.', 'rgb(181, 48, 48)');
      const inGameVisible =
        typeof isElementActuallyVisible === 'function' &&
        !!isElementActuallyVisible($('gamerenderer'));
      if (!inGameVisible || !(row instanceof Element)) return;
      const statusEl = row.querySelector('.newbonklobby_chat_status');
      if (!statusEl) return;
      mirrorLobbyStatusToIngame(statusEl, { requireRecentMs: 15000 });
    }

    function appendSlashHelpStatusRows() {
      const host = document.getElementById('newbonklobby_chat_content');
      const lobbyScrollEl = ensureLobbyScrollContainerTracking();
      const lobbyWasNearBottom = lobbyScrollEl ? isContainerNearBottom(lobbyScrollEl, 10) : false;
      const lines = [
        '/mapimg - download current map thumbnail',
        '/copymap - copy current map thumbnail as PNG',
        '/groups - toggle shared groups panel',
        '/panels - toggle groups + points panels',
        '/points - toggle points panel',
        '/blacklist <player name> - blacklist an account name',
      ];
      if (!host) {
        lines.forEach((line) => addLocalChatStatus(line));
        return;
      }
      const rows = lines.map((text) => {
        const row = document.createElement('div');
        row.dataset.tbcCustomSlash = '1';
        const span = document.createElement('span');
        span.className = 'newbonklobby_chat_status';
        span.style.color = getSystemCategoryColorHex('customCommands');
        span.textContent = text;
        row.appendChild(span);
        return row;
      });

      const children = Array.from(host.children);
      let firstTrailingAsteriskRow = null;
      for (let i = children.length - 1; i >= 0; i--) {
        const status = children[i].querySelector(':scope > .newbonklobby_chat_status');
        const txt = status ? String(status.textContent || '').trim() : '';
        if (txt.startsWith('*')) {
          firstTrailingAsteriskRow = children[i];
          continue;
        }
        break;
      }

      rows.forEach((row) => host.insertBefore(row, firstTrailingAsteriskRow));
      maybeScrollLobbyToBottom(lobbyScrollEl, lobbyWasNearBottom);
    }

    function getMapThumbDataUrl() {
      const root = document.getElementById('newbonkgamecontainer') || document;
      const candidates = [
        root.querySelector('#isnewbonklobby_mapthumb_big'),
        root.querySelector('#newbonklobby_mapthumb_big'),
        root.querySelector('img#isnewbonklobby_mapthumb_big'),
        root.querySelector('img#newbonklobby_mapthumb_big'),
        root.querySelector('img[id*="mapthumb"][src^="data:image"]'),
        root.querySelector('img[src^="data:image"]'),
      ].filter(Boolean);

      for (const img of candidates) {
        const src = String(img.getAttribute('src') || img.src || '').trim();
        if (src && src.indexOf('data:image') === 0) return src;
      }
      return null;
    }

    function dataImageToPngBlob(dataUrl) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => {
          try {
            const w = img.naturalWidth || img.width || 0;
            const h = img.naturalHeight || img.height || 0;
            if (!w || !h) return reject(new Error('Invalid image size.'));

            const canvas = document.createElement('canvas');
            canvas.width = w;
            canvas.height = h;
            const ctx = canvas.getContext('2d');
            if (!ctx) return reject(new Error('Canvas context unavailable.'));

            ctx.drawImage(img, 0, 0);
            canvas.toBlob((blob) => {
              if (!blob) return reject(new Error('PNG conversion failed.'));
              resolve(blob);
            }, 'image/png');
          } catch (e) {
            reject(e);
          }
        };
        img.onerror = () => reject(new Error('Could not decode map image.'));
        img.src = dataUrl;
      });
    }

    function triggerBlobDownload(blob, filename) {
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 1500);
    }

    async function exportLobbyMapThumbAsPng() {
      const dataUrl = getMapThumbDataUrl();
      if (!dataUrl) {
        addLocalChatStatus('[TBC] Map image not found.', 'rgb(181, 48, 48)');
        return;
      }

      try {
        const blob = await dataImageToPngBlob(dataUrl);
        const ts = new Date();
        const stamp =
          ts.getFullYear().toString() +
          String(ts.getMonth() + 1).padStart(2, '0') +
          String(ts.getDate()).padStart(2, '0') + '_' +
          String(ts.getHours()).padStart(2, '0') +
          String(ts.getMinutes()).padStart(2, '0') +
          String(ts.getSeconds()).padStart(2, '0');
        triggerBlobDownload(blob, `bonk-map-${stamp}.png`);
        addLocalChatStatus('[TBC] Map image exported as PNG.');
      } catch (e) {
        console.error('[TBC] map export failed', e);
        addLocalChatStatus('[TBC] Map export failed.', 'rgb(181, 48, 48)');
      }
    }

    async function copyLobbyMapThumbAsPng() {
      const dataUrl = getMapThumbDataUrl();
      if (!dataUrl) {
        addLocalChatStatus('[TBC] Map image not found.', 'rgb(181, 48, 48)');
        return;
      }

      try {
        if (!navigator.clipboard || typeof navigator.clipboard.write !== 'function' || typeof ClipboardItem === 'undefined') {
          addLocalChatStatus('[TBC] Clipboard image copy not supported in this browser.', 'rgb(181, 48, 48)');
          return;
        }

        const blob = await dataImageToPngBlob(dataUrl);
        const item = new ClipboardItem({ 'image/png': blob });
        await navigator.clipboard.write([item]);
        addLocalChatStatus('[TBC] Map image copied to clipboard as PNG.');
      } catch (e) {
        console.error('[TBC] map copy failed', e);
        addLocalChatStatus('[TBC] Clipboard copy failed (browser permission or support issue).', 'rgb(181, 48, 48)');
      }
    }

    function maybeRunCustomSlashCommand(rawText) {
      const text = String(rawText || '').trim();
      if (!text || text.charAt(0) !== '/') return { suppress: false };

      const now = Date.now();
      const sig = text;
      if (sig === lastSlashSig && (now - lastSlashAt) < 450) {
        return { suppress: lastSlashSuppress };
      }
      lastSlashSig = sig;
      lastSlashAt = now;
      lastSlashSuppress = false;

      const parts = text.split(/\s+/).filter(Boolean);
      const cmd = (parts[0] || '').toLowerCase();
      if (cmd === '/') return { suppress: false };

      if (cmd === '/groups' || cmd === '/groupspanel') {
        const arg = String(parts[1] || '').toLowerCase();
        let nextVisible = groupsPanelVisible;
        if (arg === 'on' || arg === 'show' || arg === '1' || arg === 'true') nextVisible = true;
        else if (arg === 'off' || arg === 'hide' || arg === '0' || arg === 'false') nextVisible = false;
        else nextVisible = !groupsPanelVisible;
        setGroupsPanelVisible(nextVisible);
        if (nextVisible) applySlashPanelsSpawnLayout('groups', true);
        addLocalChatStatus(`[TBC] Shared groups panel ${groupsPanelVisible ? 'shown' : 'hidden'}.`);
        lastSlashSuppress = true;
        return { suppress: true };
      }

      if (cmd === '/panels') {
        const arg = String(parts[1] || '').toLowerCase();
        let showBoth = !(groupsPanelVisible && pointsPanelVisible);
        if (arg === 'on' || arg === 'show' || arg === '1' || arg === 'true') showBoth = true;
        else if (arg === 'off' || arg === 'hide' || arg === '0' || arg === 'false') showBoth = false;
        setGroupsPanelVisible(showBoth);
        setPointsPanelVisible(showBoth);
        if (showBoth) applySlashPanelsSpawnLayout('groups', true);
        addLocalChatStatus(`[TBC] Panels ${showBoth ? 'shown' : 'hidden'}.`);
        lastSlashSuppress = true;
        return { suppress: true };
      }

      if (cmd === '/points') {
        const arg = String(parts[1] || '').toLowerCase();
        let nextVisible = pointsPanelVisible;
        if (arg === 'on' || arg === 'show' || arg === '1' || arg === 'true') nextVisible = true;
        else if (arg === 'off' || arg === 'hide' || arg === '0' || arg === 'false') nextVisible = false;
        else nextVisible = !pointsPanelVisible;
        setPointsPanelVisible(nextVisible);
        if (nextVisible) {
          const primary = groupsPanelVisible ? 'groups' : 'points';
          applySlashPanelsSpawnLayout(primary, true);
        }
        addLocalChatStatus(`[TBC] Points panel ${pointsPanelVisible ? 'shown' : 'hidden'}.`);
        lastSlashSuppress = true;
        return { suppress: true };
      }

      if (cmd === '/groupssync') {
        if (!isSelfLobbyHost()) {
          addLocalChatStatus('[TBC] Only the room host can trigger sync.', 'rgb(181, 48, 48)');
        } else if (!canRunGroupsSyncNow()) {
          addSyncDisabledStatusNotice();
        } else {
          const ok = broadcastSharedGroupsFromHost(true);
          if (ok) {
            markHostGroupsPanelSyncedForCurrentRoom();
            if (groupsSyncTask) addLocalChatStatus('[TBC] Groups panel sync started.');
            else addLocalChatStatus('[TBC] Groups panel refresh sent.');
          }
        }
        lastSlashSuppress = true;
        return { suppress: true };
      }
      if (cmd === '/groupsrelay') return { suppress: true };
      if (cmd === '/mapimg') {
        exportLobbyMapThumbAsPng();
        lastSlashSuppress = true;
        return { suppress: true };
      }
      if (cmd === '/copymap') {
        copyLobbyMapThumbAsPng();
        lastSlashSuppress = true;
        return { suppress: true };
      }
      if (cmd === '/blacklist') {
        const firstSpace = text.indexOf(' ');
        const nameRaw = firstSpace === -1 ? '' : text.slice(firstSpace + 1);
        const name = String(nameRaw || '').trim();
        if (!name) {
          addLocalChatStatus('[TBC] Usage: /blacklist <player name>', 'rgb(181, 48, 48)');
          lastSlashSuppress = true;
          return { suppress: true };
        }

        const n = normalizeName(name);
        const selfNorm = getSelfNameNorm();
        if (!n || (selfNorm && n === selfNorm)) {
          addLocalChatStatus('[TBC] Cannot blacklist yourself.', 'rgb(181, 48, 48)');
          lastSlashSuppress = true;
          return { suppress: true };
        }

        const info = getLobbyAccountInfoCached(800);
        const accountSet = info && info.accountSet ? info.accountSet : new Set();
        const guestSet = info && info.guestSet ? info.guestSet : new Set();
        const levelMap = info && info.levelMap ? info.levelMap : new Map();
        const levelState = String(levelMap.get(n) || '');

        if (n === 'guest' || guestSet.has(n) || levelState === 'guest') {
          addLocalChatStatus('[TBC] /blacklist only accepts account names, not guests.', 'rgb(181, 48, 48)');
          lastSlashSuppress = true;
          return { suppress: true };
        }
        if (accountSet.size > 0 && !accountSet.has(n)) {
          addLocalChatStatus('[TBC] Account not found in lobby player list.', 'rgb(181, 48, 48)');
          lastSlashSuppress = true;
          return { suppress: true };
        }

        let changed = false;
        let removed = false;
        mutateChatState((state) => {
          const exists = state.blacklistUsers.some((x) => normalizeName(x) === n);
          if (exists) {
            const before = state.blacklistUsers.length;
            state.blacklistUsers = state.blacklistUsers.filter((x) => normalizeName(x) !== n);
            removed = state.blacklistUsers.length !== before;
            changed = removed;
            return;
          }
          state.blacklistUsers.push(name);
          changed = true;
        });
        if (removed) addLocalChatStatus(`[TBC] Removed from blacklist: ${name}`);
        else if (changed) addLocalChatStatus(`[TBC] Blacklisted account: ${name}`);
        else addLocalChatStatus(`[TBC] No blacklist changes for: ${name}`);
        if (typeof refreshChatSettingsUi === 'function') refreshChatSettingsUi();
        scheduleChatScan();
        lastSlashSuppress = true;
        return { suppress: true };
      }
      return { suppress: false };
    }

    function closeLobbyChatComposer(inputEl) {
      if (!(inputEl instanceof HTMLInputElement) && !(inputEl instanceof HTMLTextAreaElement)) return;

      inputEl.value = '';
      inputEl.dispatchEvent(new Event('input', { bubbles: true }));

      setTimeout(() => {
        try {
          const keydownEvt = new KeyboardEvent('keydown', {
            key: 'Enter',
            code: 'Enter',
            keyCode: 13,
            which: 13,
            bubbles: true,
            cancelable: true,
          });
          const keyupEvt = new KeyboardEvent('keyup', {
            key: 'Enter',
            code: 'Enter',
            keyCode: 13,
            which: 13,
            bubbles: true,
            cancelable: true,
          });

          inputEl.focus();
          inputEl.dispatchEvent(keydownEvt);
          inputEl.dispatchEvent(keyupEvt);
          if (typeof inputEl.blur === 'function') inputEl.blur();
        } catch {
          if (typeof inputEl.blur === 'function') inputEl.blur();
        }
      }, 0);
    }

    function setupCustomSlashCommands() {
      if (slashCommandsInstalled) return;
      slashCommandsInstalled = true;

      waitForElement('newbonklobby_chatbox', (chatBox) => {
        chatBox.addEventListener(
          'keydown',
          (e) => {
            if (e.key !== 'Enter') return;
            const target = e.target;
            if (!(target instanceof HTMLInputElement) && !(target instanceof HTMLTextAreaElement)) return;
            const result = maybeRunCustomSlashCommand(target.value);
            if (result && result.suppress) {
              e.preventDefault();
              e.stopPropagation();
              if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
              closeLobbyChatComposer(target);
            }
          },
          true
        );

        chatBox.addEventListener(
          'click',
          (e) => {
            const target = e.target && e.target.closest ? e.target.closest('button, .brownButton') : null;
            if (!target) return;

            const input = chatBox.querySelector('input[type="text"], textarea');
            if (!input) return;
            const result = maybeRunCustomSlashCommand(input.value);
            if (result && result.suppress) {
              e.preventDefault();
              e.stopPropagation();
              if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
              closeLobbyChatComposer(input);
            }
          },
          true
        );
      });

      waitForElement('ingamechatinputtext', (input) => {
        input.addEventListener(
          'keydown',
          (e) => {
            if (e.key !== 'Enter') return;
            const target = e.target;
            if (!(target instanceof HTMLInputElement) && !(target instanceof HTMLTextAreaElement)) return;
            const result = maybeRunCustomSlashCommand(target.value);
            if (result && result.suppress) {
              e.preventDefault();
              e.stopPropagation();
              if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
              target.value = '';
              target.dispatchEvent(new Event('input', { bubbles: true }));
              setTimeout(() => closeIngameChatInputByEnterTap(1), 35);
            }
          },
          true
        );
      });
    }

    function setupIngameChatFocusGuards() {
      if (ingameChatFocusGuardsInstalled) return;
      ingameChatFocusGuardsInstalled = true;

      let pendingRefocusUntil = 0;
      let hoveringIngameChat = false;
      let mousedownIngameChat = false;
      let ingameChatHostRef = null;

      const forceImmediateIngameChatSync = () => {
        const host = ingameChatHostRef || $('ingamechatcontent');
        if (!host) return;
        const focused = isIngameChatInteracting();
        updateIngameChatScrollFocusState(host, focused);
        applyIngameChatLines();
        applyIngameChatFade();
        if (focused) host.scrollTop = host.scrollHeight;
      };

      const ensureInputFocusFor = (input, ms) => {
        if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) return;
        const until = Date.now() + Math.max(0, ms || 0);
        pendingRefocusUntil = Math.max(pendingRefocusUntil, until);
        const tick = (triesLeft) => {
          if (Date.now() > pendingRefocusUntil) return;
          if (document.activeElement !== input) input.focus();
          if (triesLeft <= 0) return;
          setTimeout(() => tick(triesLeft - 1), 50);
        };
        tick(8);
      };

      waitForElement('ingamechatcontent', (host) => {
        ingameChatHostRef = host;
        const updateHover = (clientX, clientY) => {
          const rect = host.getBoundingClientRect();
          hoveringIngameChat =
            clientX >= rect.left &&
            clientX <= rect.right &&
            clientY >= rect.top &&
            clientY <= rect.bottom;
        };

        host.addEventListener('mouseenter', () => { hoveringIngameChat = true; }, true);
        host.addEventListener('mouseleave', () => { hoveringIngameChat = false; }, true);
        host.addEventListener('mousemove', (e) => updateHover(e.clientX, e.clientY), true);

        host.addEventListener(
          'mousedown',
          (e) => {
            const input = $('ingamechatinputtext');
            if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) return;
            if (document.activeElement !== input) return;
            if (e.button !== 0) return;

            mousedownIngameChat = true;
            ensureInputFocusFor(input, 1200);
            setTimeout(() => {
              if (Date.now() <= pendingRefocusUntil && document.activeElement !== input) input.focus();
              forceImmediateIngameChatSync();
            }, 0);
          },
          true
        );

        document.addEventListener(
          'mouseup',
          () => {
            mousedownIngameChat = false;
            pendingRefocusUntil = Date.now() + 100;
          },
          true
        );

        document.addEventListener(
          'wheel',
          (e) => {
            const rect = host.getBoundingClientRect();
            const pointerInsideHost =
              e.clientX >= rect.left &&
              e.clientX <= rect.right &&
              e.clientY >= rect.top &&
              e.clientY <= rect.bottom;
            const inputFocused = isIngameChatInteracting();
            if (!pointerInsideHost && !hoveringIngameChat && !inputFocused) return;

            const input = $('ingamechatinputtext');
            const hasInput = (input instanceof HTMLInputElement) || (input instanceof HTMLTextAreaElement);
            const canScroll = host.scrollHeight > host.clientHeight + 1;
            if (!canScroll) {
              e.preventDefault();
              e.stopPropagation();
              if (hasInput && document.activeElement !== input) input.focus();
              return;
            }

            const dir = e.deltaY > 0 ? 1 : (e.deltaY < 0 ? -1 : 0);
            if (dir !== 0) {
              const cs = window.getComputedStyle(host);
              const cssLine = parseFloat((cs && cs.getPropertyValue('--tbc-ingame-line-height')) || '');
              const attrLine = parseFloat(host.getAttribute('data-tbc-runtime-per-line-height-px') || '');
              const linePx = Math.max(1, Math.round((Number.isFinite(attrLine) && attrLine > 0) ? attrLine : ((Number.isFinite(cssLine) && cssLine > 0) ? cssLine : 19)));
              host.scrollTop += dir * linePx;
            }
            e.preventDefault();
            e.stopPropagation();
            if (hasInput && document.activeElement !== input) input.focus();
          },
          { passive: false, capture: true }
        );
      });

      waitForElement('ingamechatinputtext', (input) => {
        input.addEventListener(
          'focus',
          () => {
            forceImmediateIngameChatSync();
            requestAnimationFrame(() => forceImmediateIngameChatSync());
          },
          true
        );

        input.addEventListener(
          'click',
          () => {
            forceImmediateIngameChatSync();
          },
          true
        );

        input.addEventListener(
          'blur',
          () => {
            if (!mousedownIngameChat && Date.now() > pendingRefocusUntil) {
              setTimeout(() => forceImmediateIngameChatSync(), 0);
              return;
            }
            setTimeout(() => {
              if (mousedownIngameChat || Date.now() <= pendingRefocusUntil) ensureInputFocusFor(input, 300);
            }, 0);
          },
          true
        );
      });

      document.addEventListener(
        'keydown',
        (e) => {
          if (e.key !== 'Enter') return;
          setTimeout(() => {
            if (!isIngameChatInteracting()) return;
            forceImmediateIngameChatSync();
          }, 0);
        },
        true
      );
    }

    function resolveChatRow(node) {
      if (!node || !(node instanceof Element)) return null;

      if (node.classList && node.classList.contains('ingamechatentry')) return node;
      if (node.classList && node.classList.contains('newbonklobby_chat_msg')) return node;

      const inGameTree = node.closest('#ingamechatcontent');
      if (inGameTree) {
        let cur = node;
        while (cur && cur.parentElement !== inGameTree) cur = cur.parentElement;
        if (cur && cur.parentElement === inGameTree) return cur;
      }

      const inLobbyTree = node.closest('#newbonklobby_chat_content');
      if (inLobbyTree) {
        let cur = node;
        while (cur && cur.parentElement !== inLobbyTree) cur = cur.parentElement;
        if (cur && cur.parentElement === inLobbyTree) return cur;
      }

      return null;
    }
    function getAuthorSet(authorNorm, create = false) {
      if (!authorNorm) return null;
      let set = messageRowsByAuthor.get(authorNorm);
      if (!set && create) {
        set = new Set();
        messageRowsByAuthor.set(authorNorm, set);
      }
      return set || null;
    }

    function unregisterRowFromAuthorIndex(row) {
      if (!row || !(row instanceof Element)) return;
      const prevAuthor = String(row.dataset.tbcMsgAuthorNorm || '').trim();
      if (!prevAuthor) return;
      const set = getAuthorSet(prevAuthor, false);
      if (!set) return;
      set.delete(row);
      if (!set.size) messageRowsByAuthor.delete(prevAuthor);
    }

    function registerRowInAuthorIndex(row, authorNorm) {
      if (!row || !(row instanceof Element)) return;
      const nextAuthor = String(authorNorm || '').trim();
      const prevAuthor = String(row.dataset.tbcMsgAuthorNorm || '').trim();
      if (prevAuthor && prevAuthor !== nextAuthor) unregisterRowFromAuthorIndex(row);
      if (!nextAuthor) {
        delete row.dataset.tbcMsgAuthorNorm;
        return;
      }
      const set = getAuthorSet(nextAuthor, true);
      set.add(row);
      row.dataset.tbcMsgAuthorNorm = nextAuthor;
    }

    function pruneAuthorSet(authorNorm) {
      const set = getAuthorSet(authorNorm, false);
      if (!set) return [];
      const out = [];
      for (const row of set) {
        if (!(row instanceof Element) || !row.isConnected) {
          set.delete(row);
          continue;
        }
        out.push(row);
      }
      if (!set.size) messageRowsByAuthor.delete(authorNorm);
      return out;
    }

    function isGroupedColorEnabledForRow(row) {
      const inGame = !!(row && row.closest && row.closest('#ingamechatcontent'));
      return inGame ? !!recolorDisplaySettings.ingameChatNames : !!recolorDisplaySettings.lobbyChatNames;
    }

    function getNameSpanForRow(row) {
      if (!row || !(row instanceof Element)) return null;
      if (row.closest && row.closest('#ingamechatcontent')) {
        return row.querySelector('.ingamechatname');
      }
      return row.querySelector('.newbonklobby_chat_msg_name');
    }

    function applyTaggedNameColorForRow(row) {
      if (!row || !(row instanceof Element)) return;
      if (String(row.dataset.tbcMsgType || '') !== 'user') return;
      const nameSpan = getNameSpanForRow(row);
      if (!nameSpan) return;
      const raw = String(nameSpan.textContent || '').trim();
      const author = extractChatName(raw);
      const enabled = isGroupedColorEnabledForRow(row);
      const color = enabled && hasDisplayGroupsForNames() ? getDisplayColorForName(author) : null;
      setElementNameColor(nameSpan, color);
      row.dataset.tbcMsgGrouped = color ? '1' : '0';
    }

    function ensureAuthorRowsIndexed(authorNorm) {
      const key = normalizeName(authorNorm);
      if (!key) return;
      if (getAuthorSet(key, false) && getAuthorSet(key, false).size) return;

      const spans = Array.from(document.querySelectorAll(
        '#newbonklobby_chat_content .newbonklobby_chat_msg_name, #ingamechatcontent .ingamechatname'
      ));
      spans.forEach((nameSpan) => {
        const raw = String(nameSpan.textContent || '').trim();
        const nm = normalizeName(extractChatName(raw));
        if (!nm || nm !== key) return;
        const row = resolveChatRow(nameSpan);
        if (!row) return;
        row.dataset.tbcMsgType = 'user';
        registerRowInAuthorIndex(row, nm);
      });
    }

    function refreshRowsForAuthors(authorNames) {
      const list = Array.isArray(authorNames) ? authorNames : [authorNames];
      const keys = Array.from(new Set(list.map((x) => normalizeName(x)).filter(Boolean)));
      keys.forEach((key) => {
        ensureAuthorRowsIndexed(key);
        const rows = pruneAuthorSet(key);
        rows.forEach((row) => {
          row.dataset.tbcMsgBlacklisted = messageIsBlacklisted(key) ? '1' : '0';
          applyTaggedNameColorForRow(row);
        });
      });
    }

    function refreshAllIndexedRows() {
      const all = Array.from(messageRowsByAuthor.keys());
      if (!all.length) return;
      refreshRowsForAuthors(all);
    }

    function addTouchedRow(node) {
      if (!node || !(node instanceof Element) || !node.isConnected) return;
      const row = resolveChatRow(node);
      if (!row || !row.isConnected) return;
      touchedChatRows.add(row);
      markIngameRowFadeBirth(row);
    }

    function scheduleTouchedRowsFlush() {
      if (chatTouchedQueued) return;
      chatTouchedQueued = true;
      requestAnimationFrame(() => {
        chatTouchedQueued = false;
        if (!touchedChatRows.size) return;

        const selfNorm = getSelfNameNorm();
        const info = getLobbyAccountInfoCached(800);
        const accountNameSet = info.accountSet;
        const guestNameSet = info.guestSet;
        const lobbyLevelMap = info.levelMap;
        const canVerify = accountNameSet.size > 0 || guestNameSet.size > 0;
        const now = Date.now();
        if (canVerify && (now - lastTempGuestPruneAt) >= 900) {
          lastTempGuestPruneAt = now;
          purgeTemporaryGuestGroupMembers({ onlyMissingInLobby: true, lobbyInfo: info });
        }

        const rows = Array.from(touchedChatRows);
        touchedChatRows.clear();
        rows.forEach((row) => applyChatRulesToRow(row, selfNorm, accountNameSet, guestNameSet, canVerify, lobbyLevelMap));
        applyIngameChatFade();
      });
    }

    function applyChatRulesToRow(row, selfNorm, accountNameSet, guestNameSet, canVerify, lobbyLevelMap) {
      row = resolveChatRow(row);
      if (!row || !row.isConnected) return;

      const isInGame =
        (row.classList && row.classList.contains('ingamechatentry')) ||
        !!row.closest('#ingamechatcontent');

      const isLobby =
        !isInGame &&
        (
          (row.classList && row.classList.contains('newbonklobby_chat_msg')) ||
          (row.parentElement && row.parentElement.id === 'newbonklobby_chat_content') ||
          !!row.querySelector(':scope > .newbonklobby_chat_msg_name, :scope > .newbonklobby_chat_msg_txt')
        );

      if (!isLobby && !isInGame) return;
      const forcedSystemByText = isInGame && isMapRequestStatusText(row.textContent || '');
      const systemStatus = isSystemStatusRow(row, isLobby, isInGame) || forcedSystemByText;
      if (isInGame && systemStatus && !chatState.showSystemMessages && !isProtectedIngameSystemStatus(row)) {
        maybeDesyncOnHostClosedRoomStatus(row.textContent || '');
        maybeDesyncOnHostTransferStatus(row.textContent || '');
        row.dataset.tbcMsgType = 'system';
        row.dataset.tbcMsgBlacklisted = '0';
        row.dataset.tbcMsgGrouped = '0';
        unregisterRowFromAuthorIndex(row);
        unwrapHiddenIfPresent(row);
        row.classList.remove('tbc_chat_invisible');
        stashIngameSystemRow(row);
        return;
      }
      if (systemStatus) {
        maybeDesyncOnHostClosedRoomStatus(row.textContent || '');
        maybeDesyncOnHostTransferStatus(row.textContent || '');
        row.dataset.tbcMsgType = 'system';
        row.dataset.tbcMsgBlacklisted = '0';
        row.dataset.tbcMsgGrouped = '0';
        unregisterRowFromAuthorIndex(row);
        unwrapHiddenIfPresent(row);
        row.classList.remove('tbc_chat_invisible');
        if (isInGame) {
          syncNativeIngameSystemColorFromLobby(row);
          const replayCore = normalizeStatusText(getIngameStatusTextFromRow(row) || row.textContent || '').replace(/^\*\s*/, '');
          if (isReplayRecorderSystemText(replayCore)) forceReplaySystemRowColor(row);
        }
        applyConfiguredSystemColorToRow(row, row.textContent || '');
        return;
      }

      const parts = isLobby ? getLobbyMessagePartsFromRow(row) : getInGameMessagePartsFromRow(row);
      if (isLobby || isInGame) {
        const syncTextRaw = String(parts.msgText || '').trim();
        const syncText = parseGroupsSyncTransportMessage(syncTextRaw);
        if (syncText) {
          const syncSig = `${normalizeName(parts.sender)}|${syncText}`;
          if (row.dataset && row.dataset.tbcGroupsSyncHandledSig === syncSig) {
            row.classList.add('tbc_chat_invisible');
            return;
          }
          if (handleIncomingGroupsSyncChunk(parts.sender, syncText)) {
            if (row.dataset) row.dataset.tbcGroupsSyncHandledSig = syncSig;
            row.classList.add('tbc_chat_invisible');
            return;
          }
        }
      }
      const senderNorm = normalizeName(parts.sender);
      row.dataset.tbcMsgType = 'user';
      registerRowInAuthorIndex(row, senderNorm);
      const isSelf = !!selfNorm && senderNorm === selfNorm;
      const senderLobbyState = (lobbyLevelMap && senderNorm) ? String(lobbyLevelMap.get(senderNorm) || '') : '';
      const isGuestLike =
        senderNorm === 'guest' ||
        guestNameSet.has(senderNorm) ||
        senderLobbyState === 'guest';
      const rendererVisible = isElementActuallyVisible($('gamerenderer'));
      const hasLobbyElement = !!$('newbonklobby');
      const senderIdentityUnknown =
        !!senderNorm &&
        senderLobbyState !== 'guest' &&
        senderLobbyState !== 'level' &&
        !guestNameSet.has(senderNorm) &&
        !accountNameSet.has(senderNorm);
      const fallbackGuestHideActive =
        !!chatState.hideGuests &&
        rendererVisible &&
        hasLobbyElement &&
        senderIdentityUnknown;
      const fallbackTreatAsGuest =
        fallbackGuestHideActive &&
        !isSelf &&
        !!senderNorm &&
        senderNorm !== 'system' &&
        senderNorm !== 'server' &&
        senderNorm !== 'announcement' &&
        senderNorm !== 'announcer';

      if (!isSelf && chatState.hideGuests && (isGuestLike || fallbackTreatAsGuest)) {
        unwrapHiddenIfPresent(row);
        row.classList.add('tbc_chat_invisible');
        row.dataset.tbcMsgGuestFallback = fallbackTreatAsGuest ? '1' : '0';
        return;
      }

      row.dataset.tbcMsgGuestFallback = '0';
      if (!(chatState.hideGuests && !isSelf && isGuestLike)) row.classList.remove('tbc_chat_invisible');

      const canBlacklistSender = isPlayerSender(senderNorm, selfNorm, accountNameSet, guestNameSet, canVerify);
      const blacklisted = !isSelf && canBlacklistSender && messageIsBlacklisted(parts.sender);
      row.dataset.tbcMsgBlacklisted = blacklisted ? '1' : '0';
      setBlacklistedPresentation(row, blacklisted);

      if (parts.nameSpan) {
        const colorEnabled = isInGame ? !!recolorDisplaySettings.ingameChatNames : !!recolorDisplaySettings.lobbyChatNames;
        const nameColor = colorEnabled ? getDisplayColorForName(parts.sender) : null;
        setElementNameColorWithGrace(parts.nameSpan, nameColor, colorEnabled, 'tbcChatNameNoColorSince', 1400);
        row.dataset.tbcMsgGrouped = nameColor ? '1' : '0';
      } else {
        row.dataset.tbcMsgGrouped = '0';
      }

    }

    function scanAndApplyChatRules() {
        const now = Date.now();
        if ((now - lastFullChatScanAt) < 120) return;
        lastFullChatScanAt = now;
        updateReplaySystemProtectionLatch();
        const lobbyVisibleNow = isElementActuallyVisible($('newbonklobby'));
        const rendererVisibleNow = isElementActuallyVisible($('gamerenderer'));
        const syncEligibleNow = canRunGroupsSyncNow();
        if (syncEligibleNow !== lastGroupsSyncEligibility) {
          lastGroupsSyncEligibility = syncEligibleNow;
          if (groupsPanelVisible) renderGroupsPanel();
        }
        flushPendingSharedGroupsDesyncIfReady();
        if (
          lastRendererVisibleForNonHostExitDesync === true &&
          !rendererVisibleNow &&
          lobbyVisibleNow &&
          !isSelfLobbyHost() &&
          (roomGroupsSyncActive || sharedHostGroupsSnapshot.length > 0)
        ) {
          applySharedGroupsDesyncNow('[TBC] You left the game. Shared groups desynced.');
        }
        lastRendererVisibleForNonHostExitDesync = rendererVisibleNow;
        if (rendererVisibleNow && lastRendererVisibleForPointsSnapshotReset === false) {
          resetPointsPanelScoreSnapshot();
          if (pointsPanelVisible) renderPointsPanel();
        }
        lastRendererVisibleForPointsSnapshotReset = rendererVisibleNow;
        if (!document.hidden && !lobbyVisibleNow && !rendererVisibleNow) {
          if (!roomUiBothHiddenSince) roomUiBothHiddenSince = now;
          if ((now - roomUiBothHiddenSince) >= 1500) {
            clearTransientChatCarryoverState();
            if (roomGroupsSyncActive || sharedHostGroupsSnapshot.length > 0) {
              sharedHostGroupsSnapshot = [];
              groupsSyncChunksBySession = new Map();
              liveGroupsSyncHostNorm = '';
              setRoomGroupsSyncActive(false);
              syncSharedGroupsBridge();
              if (groupsPanelVisible) renderGroupsPanel();
            }
            purgeTemporaryGuestGroupMembers({ onlyMissingInLobby: false });
          }
        } else {
          roomUiBothHiddenSince = 0;
        }
        maybeLoadRoomGroupsCache();
        const lobbyScrollEl = ensureLobbyScrollContainerTracking();

        const lobbyBox = document.getElementById('newbonklobby_chat_content');
        const ingameBox = document.getElementById('ingamechatcontent');

        const lobbyWasNearBottom = lobbyScrollEl
        ? isContainerNearBottom(lobbyScrollEl, 10)
        : false;

        const ingameWasNearBottom = ingameBox
        ? (ingameBox.scrollHeight - ingameBox.scrollTop - ingameBox.clientHeight) < 10
        : false;

        const selfNorm = getSelfNameNorm();

        const info = getLobbyAccountInfoCached(800);
        const accountNameSet = info.accountSet;
        const guestNameSet = info.guestSet;
        const lobbyLevelMap = info.levelMap;
        const canVerify = accountNameSet.size > 0 || guestNameSet.size > 0;

        const lobbyRows = Array.from((lobbyBox && lobbyBox.children) ? lobbyBox.children : [])
            .map((el) => resolveChatRow(el))
            .filter(Boolean)
            .slice(-140);

        lobbyRows.forEach((row) => {
            applyChatRulesToRow(row, selfNorm, accountNameSet, guestNameSet, canVerify, lobbyLevelMap);
        });

        const inGameRows = Array.from((ingameBox && ingameBox.children) ? ingameBox.children : [])
            .map((el) => resolveChatRow(el))
            .filter(Boolean)
            .slice(-140);

        inGameRows.forEach((row) => {
            applyChatRulesToRow(row, selfNorm, accountNameSet, guestNameSet, canVerify, lobbyLevelMap);
        });

        maybeScrollLobbyToBottom(lobbyScrollEl, lobbyWasNearBottom);
        if (ingameBox && ingameWasNearBottom) ingameBox.scrollTop = ingameBox.scrollHeight;
        applyIngameChatFade();
        if (pointsPanelVisible) renderPointsPanel();
        syncReplayStatusLobbyRows();
    }

    function injectLobbyChatCog() {
      const chatBox = document.getElementById('newbonklobby_chatbox');
      if (!chatBox) return;

      const header = chatBox.querySelector('.newbonklobby_boxtop.newbonklobby_boxtop_classic');
      if (!header) return;

      if (!header.style.position) header.style.position = 'relative';
      if (header.dataset.tbcChatInit === '1') {
        return;
      }

      let btn = header.querySelector('#tbc_chat_cogbtn');
      if (!btn) {
        btn = document.createElement('button');
        btn.id = 'tbc_chat_cogbtn';
        btn.type = 'button';
        btn.setAttribute('aria-label', 'Chat settings');
        btn.title = 'Chat settings';
        btn.innerHTML = `
          <svg xmlns="http://www.w3.org/2000/svg"
               viewBox="0 0 24 24"
               width="18" height="18"
               fill="none"
               stroke="currentColor"
               stroke-width="1.8"
               stroke-linecap="round"
               stroke-linejoin="round"
               aria-label="Settings" role="img">
            <path d="M 10.358 2.136
                     A 10.000 10.000 0 0 1 13.642 2.136
                     L 13.603 5.083
                     A 7.100 7.100 0 0 1 15.757 5.976
                     L 17.814 3.864
                     A 10.000 10.000 0 0 1 20.136 6.186
                     L 18.024 8.243
                     A 7.100 7.100 0 0 1 18.917 10.397
                     L 21.864 10.358
                     A 10.000 10.000 0 0 1 21.864 13.642
                     L 18.917 13.603
                     A 7.100 7.100 0 0 1 18.024 15.757
                     L 20.136 17.814
                     A 10.000 10.000 0 0 1 17.814 20.136
                     L 15.757 18.024
                     A 7.100 7.100 0 0 1 13.603 18.917
                     L 13.642 21.864
                     A 10.000 10.000 0 0 1 10.358 21.864
                     L 10.397 18.917
                     A 7.100 7.100 0 0 1 8.243 18.024
                     L 6.186 20.136
                     A 10.000 10.000 0 0 1 3.864 17.814
                     L 5.976 15.757
                     A 7.100 7.100 0 0 1 5.083 13.603
                     L 2.136 13.642
                     A 10.000 10.000 0 0 1 2.136 10.358
                     L 5.083 10.397
                     A 7.100 7.100 0 0 1 5.976 8.243
                     L 3.864 6.186
                     A 10.000 10.000 0 0 1 6.186 3.864
                     L 8.243 5.976
                     A 7.100 7.100 0 0 1 10.397 5.083
                     L 10.358 2.136
                     Z" />
            <circle cx="12" cy="12" r="3.2" />
          </svg>
        `;
      }

      const getSettingsContainer = () => document.getElementById('settingsContainer');
      const getLobbyRoot = () => document.getElementById('newbonklobby');
      const overlayWindowIds = [
        'leaveconfirmwindowcontainer',
        'hostleaveconfirmwindowcontainer',
        'maploadwindowcontainer',
        'mapsearchwindowcontainer',
        'mapeditorwindowcontainer',
        'newbonklobby_votewindow_container'
      ];
      const getOverlayWindows = () =>
        overlayWindowIds
          .map((id) => document.getElementById(id))
          .filter(Boolean);
      const observedOverlays = new WeakSet();

      const syncCogVisibility = () => {
        const settingsContainer = getSettingsContainer();
        const lobbyRoot = getLobbyRoot();
        const lobbyVisible = (() => {
          if (!lobbyRoot) return false;
          const cs = window.getComputedStyle(lobbyRoot);
          return (
            cs.display !== 'none' &&
            cs.visibility !== 'hidden' &&
            cs.opacity !== '0' &&
            lobbyRoot.getClientRects().length > 0
          );
        })();
        if (!lobbyVisible) {
          btn.style.display = 'none';
          btn.style.opacity = '0';
          btn.style.pointerEvents = 'none';
          return;
        } else {
          btn.style.display = '';
        }

        const hasBlockingOverlay = getOverlayWindows().some((overlayEl) => {
          const cs = window.getComputedStyle(overlayEl);
          return (
            cs.display !== 'none' &&
            cs.visibility !== 'hidden' &&
            cs.opacity !== '0' &&
            overlayEl.getClientRects().length > 0
          );
        });

        if (hasBlockingOverlay) {
          btn.style.display = 'none';
          btn.style.opacity = '0';
          btn.style.pointerEvents = 'none';
          return;
        } else {
          btn.style.display = '';
        }

        const settingsVisible = (() => {
          if (!settingsContainer) return false;
          const cs = window.getComputedStyle(settingsContainer);
          return (
            cs.display !== 'none' &&
            cs.visibility !== 'hidden' &&
            cs.opacity !== '0' &&
            settingsContainer.getClientRects().length > 0
          );
        })();

        if (settingsVisible) {
          btn.style.display = 'none';
          btn.style.opacity = '0';
          btn.style.pointerEvents = 'none';
          return;
        }

        btn.style.display = '';
        btn.style.opacity = '1';
        btn.style.pointerEvents = '';
      };

      syncCogVisibility();

      const settingsElNow = getSettingsContainer();
      if (settingsElNow) {
        const visObs = new MutationObserver(() => syncCogVisibility());
        visObs.observe(settingsElNow, { attributes: true, attributeFilter: ['style', 'class'] });
      }

      const lobbyElNow = getLobbyRoot();
      if (lobbyElNow) {
        const lobbyObs = new MutationObserver(() => syncCogVisibility());
        lobbyObs.observe(lobbyElNow, { attributes: true, attributeFilter: ['style', 'class'] });
      }

      const bindOverlayObservers = () => {
        getOverlayWindows().forEach((overlayEl) => {
          if (observedOverlays.has(overlayEl)) return;
          const overlayObs = new MutationObserver(() => syncCogVisibility());
          overlayObs.observe(overlayEl, { attributes: true, attributeFilter: ['style', 'class'] });
          observedOverlays.add(overlayEl);
        });
      };
      bindOverlayObservers();
      const overlayRootObs = new MutationObserver(() => {
        bindOverlayObservers();
        syncCogVisibility();
      });
      overlayRootObs.observe(document.body, { childList: true, subtree: true });

      if (!btn.parentElement) header.appendChild(btn);

      if (!document.getElementById('tbc_chat_cog_css')) {
        const style = document.createElement('style');
        style.id = 'tbc_chat_cog_css';
        style.textContent = `
          #tbc_chat_cogbtn{
            position: absolute;
            right: 6px;
            top: 50%;
            transform: translateY(-50%);
            width: 26px;
            height: 26px;
            display: inline-flex;
            align-items: center;
            justify-content: center;

            background: rgba(0,0,0,0.18);
            border: 1px solid rgba(255,255,255,0.22);
            border-radius: 7px;

            padding: 0;
            margin: 0;
            cursor: pointer;
            color: #ffffff;
            z-index: 1;
          }
          #tbc_chat_cogbtn:hover{ background: rgba(255,255,255,0.10); }
          #tbc_chat_cogbtn:active{ transform: translateY(-50%) scale(0.98); }
          #tbc_chat_cogbtn svg{ display:block; }
        `;
        document.head.appendChild(style);
      }

      if (btn.dataset.tbcBound !== '1') {
        btn.dataset.tbcBound = '1';
        btn.addEventListener('click', (e) => {
          e.stopPropagation();

          const settingsBtn = document.getElementById('pretty_top_settings');
          if (settingsBtn) settingsBtn.click();

          setTimeout(() => {
            const moddedTab = document.querySelector('#mod_tabs .mod_tab_modded');
            if (moddedTab) moddedTab.click();

            setTimeout(() => {
              const chatCatBtn = Array.from(document.querySelectorAll('#mod_cat_tabs .mod_cat_tab'))
                .find((el) => (el.textContent || '').trim().toLowerCase() === 'chat');

              if (chatCatBtn) chatCatBtn.click();
            }, 50);
          }, 50);
        });
      }
      header.dataset.tbcChatInit = '1';
    }

    function setupChatObservers() {
      const idsToWatch = [
        'newbonklobby_chat_content',
        'ingamechatcontent',
        'newbonklobby_playerbox_elementcontainer',
        'newbonklobby_playerbox_leftelementcontainer',
        'newbonklobby_playerbox_rightelementcontainer',
        'newbonklobby_specbox_elementcontainer',
      ];

            idsToWatch.forEach((id) => {
        waitForElement(id, (el) => {
          const obs = new MutationObserver((mutations) => {
            if (id.indexOf('playerbox') !== -1 || id.indexOf('specbox') !== -1) {
              scheduleChatScan();
              return;
            }

            for (const m of mutations) {
              for (const node of m.removedNodes) {
                if (!(node instanceof Element)) continue;
                const row = resolveChatRow(node);
                if (row && row === node) {
                  unregisterRowFromAuthorIndex(row);
                }
                node
                  .querySelectorAll('.ingamechatentry, .newbonklobby_chat_msg')
                  .forEach((r) => unregisterRowFromAuthorIndex(r));
              }

              for (const node of m.addedNodes) {
                if (!(node instanceof Element)) continue;

                if (id === 'newbonklobby_chat_content') {
                  const statuses = [];
                  if (node.classList && node.classList.contains('newbonklobby_chat_status')) statuses.push(node);
                  node.querySelectorAll('.newbonklobby_chat_status').forEach((x) => statuses.push(x));
                  for (const st of statuses) {
                    if (!st.dataset.tbcStatusSeenAt) st.dataset.tbcStatusSeenAt = String(Date.now());
                    const statusRow = resolveChatRow(st);
                    if (statusRow && hasSystemPrefix(st.textContent || statusRow.textContent || '')) {
                      applyConfiguredSystemColorToRow(statusRow, st.textContent || statusRow.textContent || '');
                    }
                    mirrorLobbyStatusToIngame(st);
                    const txt = String(st.textContent || '').trim().toLowerCase();
                    if (txt.indexOf('not recognised') !== -1 && st.dataset.tbcUnknownHelpInjected !== '1') {
                      st.dataset.tbcUnknownHelpInjected = '1';
                      const now = Date.now();
                      if (now - lastUnknownSlashHelpAt > 120) {
                        lastUnknownSlashHelpAt = now;
                        setTimeout(() => appendSlashHelpStatusRows(), 0);
                      }
                      break;
                    }
                  }

                  const mapReqRows = [];
                  const rootRow = resolveChatRow(node);
                  if (rootRow && isLobbyMapRequestRow(rootRow)) mapReqRows.push(rootRow);
                  node.querySelectorAll('.newbonklobby_mapsuggest_low, .newbonklobby_mapsuggest_high').forEach((el2) => {
                    const r = resolveChatRow(el2);
                    if (!r || !isLobbyMapRequestRow(r)) return;
                    if (!mapReqRows.includes(r)) mapReqRows.push(r);
                  });
                  for (const r of mapReqRows) {
                    if (!r.dataset.tbcStatusSeenAt) r.dataset.tbcStatusSeenAt = String(Date.now());
                    mirrorLobbyMapRequestRowToIngame(r);
                  }

                  if (node.classList && node.classList.contains('newbonklobby_chat_msg')) addTouchedRow(node);
                  node.querySelectorAll('.newbonklobby_chat_msg').forEach((x) => addTouchedRow(x));
                  node.querySelectorAll('.newbonklobby_chat_msg_name').forEach((x) => {
                    addTouchedRow(x.closest('.newbonklobby_chat_msg') || x.parentElement);
                  });
                } else if (id === 'ingamechatcontent') {
                  const rowsToTouch = new Set();
                  const rootRow = resolveChatRow(node);
                  if (rootRow) rowsToTouch.add(rootRow);
                  node.querySelectorAll('.ingamechatentry, .ingamechatname, .ingamechatstatus, .ingamechatmessage, .ingamechattext').forEach((x) => {
                    const r = resolveChatRow(x);
                    if (r) rowsToTouch.add(r);
                  });
                  rowsToTouch.forEach((r) => {
                    addTouchedRow(r);
                    mirrorIngameStatusToLobby(r);
                  });
                }
              }
              if (id === 'newbonklobby_chat_content' && m.type === 'characterData') {
                const carrier = m.target && m.target.parentElement ? m.target.parentElement : null;
                const st = carrier && carrier.closest ? carrier.closest('.newbonklobby_chat_status') : null;
                if (st) {
                  if (!st.dataset.tbcStatusSeenAt) st.dataset.tbcStatusSeenAt = String(Date.now());
                  delete st.dataset.tbcMirroredIngame;
                  mirrorLobbyStatusToIngame(st);
                }

                const row = carrier && carrier.closest ? carrier.closest('#newbonklobby_chat_content > *') : null;
                if (row && isLobbyMapRequestRow(row)) {
                  if (!row.dataset.tbcStatusSeenAt) row.dataset.tbcStatusSeenAt = String(Date.now());
                  delete row.dataset.tbcMirroredIngame;
                  mirrorLobbyMapRequestRowToIngame(row);
                }
              }

              if (id === 'ingamechatcontent' && m.type === 'characterData') {
                const carrier = m.target && m.target.parentElement ? m.target.parentElement : null;
                const row = carrier ? resolveChatRow(carrier) : null;
                if (row) {
                  addTouchedRow(row);
                  mirrorIngameStatusToLobby(row);
                }
              }

              if (id === 'newbonklobby_chat_content' && m.type === 'attributes') {
                const targetEl = m.target instanceof Element ? m.target : null;
                if (!targetEl) continue;

                const st =
                  (targetEl.classList && targetEl.classList.contains('newbonklobby_chat_status') && targetEl) ||
                  (targetEl.closest ? targetEl.closest('.newbonklobby_chat_status') : null);
                if (st) {
                  if (!st.dataset.tbcStatusSeenAt) st.dataset.tbcStatusSeenAt = String(Date.now());
                  mirrorLobbyStatusToIngame(st);
                }

                const row = resolveChatRow(targetEl);
                if (row && isLobbyMapRequestRow(row)) {
                  if (!row.dataset.tbcStatusSeenAt) row.dataset.tbcStatusSeenAt = String(Date.now());
                  mirrorLobbyMapRequestRowToIngame(row);
                }
              }

              if (id === 'ingamechatcontent' && m.type === 'attributes') {
                const targetEl = m.target instanceof Element ? m.target : null;
                if (!targetEl) continue;
                const row = resolveChatRow(targetEl);
                if (row) {
                  addTouchedRow(row);
                  mirrorIngameStatusToLobby(row);
                }
              }

              if (id === 'newbonklobby_chat_content' && m.type === 'childList') {
                const targetEl = m.target instanceof Element ? m.target : null;
                if (!targetEl || targetEl.id === 'newbonklobby_chat_content') continue;
                const st =
                  (targetEl && targetEl.classList && targetEl.classList.contains('newbonklobby_chat_status') && targetEl) ||
                  (targetEl && targetEl.querySelector ? targetEl.querySelector(':scope > .newbonklobby_chat_status') : null);
                if (st) {
                  if (!st.dataset.tbcStatusSeenAt) st.dataset.tbcStatusSeenAt = String(Date.now());
                  mirrorLobbyStatusToIngame(st);
                }
              }

              if (id === 'ingamechatcontent' && m.type === 'childList') {
                const targetEl = m.target instanceof Element ? m.target : null;
                if (!targetEl) continue;
                const row = resolveChatRow(targetEl);
                if (row) {
                  addTouchedRow(row);
                  mirrorIngameStatusToLobby(row);
                }
              }
            }
            scheduleTouchedRowsFlush();
            if (id === 'ingamechatcontent') scheduleIngameVisualRefresh();
          });
          obs.observe(el, {
            childList: true,
            subtree: true,
            characterData: true
          });
          scheduleChatScan();
          if (id === 'ingamechatcontent') {
            applyIngameChatBackgroundSetting();
            syncLobbyStatusesToIngame(180);
            scheduleIngameVisualRefresh();
          }
          injectLobbyChatCog();
        });
      });

      const root = document.documentElement || document.body;
      if (root) {
        const rootObs = new MutationObserver(() => injectLobbyChatCog());
        rootObs.observe(root, { childList: true, subtree: true });
      }

      window.addEventListener('tbcChatSettingsChanged', () => {
        if (chatState.showSystemMessages) restoreStashedIngameSystemRows();
        refreshVisibleSystemMessageRows();
        syncReplayStatusLobbyRows();
        scheduleChatScan();
        scheduleIngameVisualRefresh();
      });
    }

    let chatModRegistered = false;

    function initChatMod() {
      if (chatModRegistered) return;
      const bonkMods = window.bonkMods;
      if (!bonkMods) return;

      chatModRegistered = true;
      ensureChatStyles();
      ensureGroupsPanelElement();
      ensurePointsPanelElement();

      bonkMods.registerMod({
        id: 'tbc_chat',
        name: 'Chat Tools',
        version: '1.3.2',
        author: 'SIoppy',
        description: 'Blacklisted-user hiding, optional guest filtering (not quickplay), and extended in-game chat history.',
      });

      bonkMods.registerCategory({
        id: 'chat_main',
        label: 'Chat',
        order: 60,
      });

      bonkMods.addBlock({
        id: 'tbc_chat_block',
        modId: 'tbc_chat',
        categoryId: 'chat_main',
        title: 'Chat',
        order: 0,
        render(container) {
          updateChatStorageKey();
          container.innerHTML = `
            <div class="mod_block_sub">
              Hide messages from blacklisted users and optionally hide guest messages.
              <br><small style="opacity:.8;">If verification data is unavailable mid-game, hide-guests temporarily hides non-self user messages until lobby/player data is available.</small>
            </div>

            <div id="tbc_chat_storage_hint" style="margin-top:6px;font-size:11px;"></div>

            <div class="tbc_row" style="margin-top:10px;">
              <div class="tbc_toggle" id="tbc_toggle_hideguests">
                <div class="tbc_toggle_dot"></div>
                <div>
                  <div style="font-weight:800;">Hide guest messages</div>
                  <div style="font-size:10px;opacity:.8;">
                    Only works when guests can be verified (not quickplay)
                  </div>
                </div>
              </div>

              <div class="tbc_toggle" id="tbc_toggle_showsystem">
                <div class="tbc_toggle_dot"></div>
                <div>
                  <div style="font-weight:800;">Show system messages</div>
                  <div style="font-size:10px;opacity:.8;">
                    Controls status lines like join/leave, command output, and countdown notices
                  </div>
                </div>
              </div>

              <div class="tbc_toggle" id="tbc_toggle_ingamechatbg">
                <div class="tbc_toggle_dot"></div>
                <div>
                  <div style="font-weight:800;">In-game chat backgrounds</div>
                  <div style="font-size:10px;opacity:.8;">
                    Toggle row background fill in in-game chat
                  </div>
                </div>
              </div>

              <div class="tbc_toggle" id="tbc_toggle_ingame_hide_others_delay">
                <div class="tbc_toggle_dot"></div>
                <div>
                  <div style="font-weight:800;">Hide others until fade delay</div>
                  <div style="font-size:10px;opacity:.8;">
                    In-game: only your own messages stay visible before fade delay
                  </div>
                </div>
              </div>
            </div>

            <div class="tbc_box">
              <div class="tbc_h">In-game chat size</div>
              <div class="tbc_p">Set how many lines are visible in the in-game chat box (default: 4, min: 1, max: 10).</div>
              <div class="tbc_inputrow">
                <input class="tbc_input" id="tbc_ingame_chat_lines" type="number" min="1" max="10" step="1" style="width:90px;" placeholder="1-10" title="Range: 1 to 10">
              </div>
              <div class="tbc_p" style="margin-top:8px;">
                Message fade delay (seconds). Messages fade oldest-first after this delay (default: 8, min: 1, max: 60).
              </div>
              <div class="tbc_inputrow">
                <input class="tbc_input" id="tbc_ingame_fade_delay" type="number" min="1" max="60" step="0.5" style="width:90px;" placeholder="8" title="Fade delay in seconds">
              </div>
            </div>
            <div class="tbc_box">
              <div class="tbc_h">Blacklist users</div>
              <div class="tbc_p">Messages from these usernames are always hidden from chat.</div>

              <div class="tbc_inputrow">
                <input class="tbc_input" id="tbc_bl_user" type="text" placeholder="username…">
                <div class="tbc_btn" id="tbc_bl_user_add">Add</div>
              </div>

              <div class="tbc_chips" id="tbc_bl_user_list"></div>
            </div>

            <div class="tbc_box">
              <div class="tbc_toggle" id="tbc_toggle_systemcolors" style="margin-bottom:8px;">
                <div class="tbc_toggle_dot"></div>
                <div style="font-weight:800;">Custom system message colours</div>
              </div>
              <div class="tbc_syscolor_head">
                <div class="tbc_h" style="margin-bottom:0;">System message colours</div>
                <div class="tbc_syscolor_reset" id="tbc_syscolor_reset" title="Reset all colours">↻</div>
              </div>
              <div class="tbc_p">Set colour per system-message category. Format button cycles HEX -> RGB -> HSV.</div>
              <div id="tbc_syscolor_list"></div>
            </div>
          `;

          updateChatStorageHintUI();

          const elHideGuests = $('tbc_toggle_hideguests');
          const elShowSystem = $('tbc_toggle_showsystem');
          const elIngameChatBg = $('tbc_toggle_ingamechatbg');
          const elHideOthersDelay = $('tbc_toggle_ingame_hide_others_delay');
          const elSystemColors = $('tbc_toggle_systemcolors');
          const elIngameLines = $('tbc_ingame_chat_lines');
          const elIngameFadeDelay = $('tbc_ingame_fade_delay');
          const elSystemColorList = $('tbc_syscolor_list');
          const elSystemColorReset = $('tbc_syscolor_reset');

          function renderToggles() {
            if (elHideGuests) elHideGuests.classList.toggle('on', !!chatState.hideGuests);
            if (elShowSystem) elShowSystem.classList.toggle('on', !!chatState.showSystemMessages);
            if (elIngameChatBg) elIngameChatBg.classList.toggle('on', !!chatState.ingameChatBackgrounds);
            if (elHideOthersDelay) elHideOthersDelay.classList.toggle('on', !!chatState.hideIngameOthersUntilFadeDelay);
            if (elSystemColors) elSystemColors.classList.toggle('on', !!chatState.useCustomSystemMessageColors);
            if (elSystemColorList) {
              const enabled = !!chatState.useCustomSystemMessageColors;
              elSystemColorList.style.opacity = enabled ? '1' : '0.55';
              elSystemColorList.style.pointerEvents = enabled ? '' : 'none';
            }
            if (elSystemColorReset) {
              const enabled = !!chatState.useCustomSystemMessageColors;
              elSystemColorReset.style.opacity = enabled ? '1' : '0.55';
              elSystemColorReset.style.pointerEvents = enabled ? '' : 'none';
            }
          }

          if (elHideGuests) {
            elHideGuests.addEventListener('click', () => {
              mutateChatState((state) => {
                state.hideGuests = !state.hideGuests;
              });
              renderToggles();
            });
          }

          if (elShowSystem) {
            elShowSystem.addEventListener('click', () => {
              mutateChatState((state) => {
                state.showSystemMessages = !state.showSystemMessages;
              });
              if (chatState.showSystemMessages) restoreStashedIngameSystemRows();
              renderToggles();
            });
          }

          if (elIngameChatBg) {
            elIngameChatBg.addEventListener('click', () => {
              mutateChatState((state) => {
                state.ingameChatBackgrounds = !state.ingameChatBackgrounds;
              });
              renderToggles();
              applyIngameChatBackgroundSetting();
            });
          }

          if (elHideOthersDelay) {
            elHideOthersDelay.addEventListener('click', () => {
              mutateChatState((state) => {
                state.hideIngameOthersUntilFadeDelay = !state.hideIngameOthersUntilFadeDelay;
              });
              renderToggles();
              scheduleIngameVisualRefresh();
            });
          }

          if (elSystemColors) {
            elSystemColors.addEventListener('click', () => {
              mutateChatState((state) => {
                state.useCustomSystemMessageColors = !state.useCustomSystemMessageColors;
              });
              renderToggles();
              refreshVisibleSystemMessageRows();
              scheduleChatScan();
            });
          }

          renderToggles();

          function renderIngameLines() {
            if (!elIngameLines) return;
            if (document.activeElement === elIngameLines) return;
            elIngameLines.value = String(clampIngameChatLines(chatState.ingameChatLines));
          }

          function renderIngameFadeDelay() {
            if (!elIngameFadeDelay) return;
            if (document.activeElement === elIngameFadeDelay) return;
            elIngameFadeDelay.value = String(clampIngameFadeDelaySec(chatState.ingameFadeDelaySec));
          }

          if (elIngameLines) {
            const saveIngameLines = () => {
              const val = clampIngameChatLines(elIngameLines.value);
              mutateChatState((state) => {
                state.ingameChatLines = val;
              });
              elIngameLines.value = String(val);
              scheduleIngameVisualRefresh();
            };

            elIngameLines.addEventListener('change', saveIngameLines);
            elIngameLines.addEventListener('blur', saveIngameLines);
          }

          if (elIngameFadeDelay) {
            const saveIngameFadeDelay = () => {
              const val = clampIngameFadeDelaySec(elIngameFadeDelay.value);
              mutateChatState((state) => {
                state.ingameFadeDelaySec = val;
              });
              elIngameFadeDelay.value = String(val);
              scheduleIngameVisualRefresh();
            };

            elIngameFadeDelay.addEventListener('change', saveIngameFadeDelay);
            elIngameFadeDelay.addEventListener('blur', saveIngameFadeDelay);
          }

          renderIngameLines();
          renderIngameFadeDelay();
          scheduleIngameVisualRefresh();
          function renderBLUsers() {
            const host = $('tbc_bl_user_list');
            if (!host) return;
            host.textContent = '';

            chatState.blacklistUsers.forEach((u) => {
              const chip = document.createElement('div');
              chip.className = 'tbc_chip';
              chip.innerHTML = `<span>${u}</span><span class="tbc_chip_x" title="Remove">×</span>`;
              chip.querySelector('.tbc_chip_x').addEventListener('click', () => {
                mutateChatState((state) => {
                  state.blacklistUsers = state.blacklistUsers.filter(
                    (x) => normalizeName(x) !== normalizeName(u)
                  );
                });
                renderBLUsers();
              });
              host.appendChild(chip);
            });
          }

          function renderSystemMessageColors() {
            const host = $('tbc_syscolor_list');
            if (!host) return;
            host.textContent = '';

            chatState.systemMessageColors = normalizeSystemMessageColors(chatState.systemMessageColors);
            const cfg = chatState.systemMessageColors;

            SYSTEM_COLOR_CATEGORIES.forEach((cat) => {
              const pref = cfg[cat.id] || { hex: SYSTEM_COLOR_DEFAULT_HEX[cat.id] || SYSTEM_COLOR_DEFAULT_HEX.defaultSystem, format: 'hex' };
              const mode = pref.format || 'hex';
              const nextMode = SYSTEM_COLOR_NEXT_FORMAT[mode] || 'hex';
              const baseHex = normalizeHexColor(pref.hex) || SYSTEM_COLOR_DEFAULT_HEX[cat.id] || SYSTEM_COLOR_DEFAULT_HEX.defaultSystem;
              const rgb = hexToRgbObj(baseHex) || { r: 0, g: 0, b: 0 };
              const hsv = rgbToHsvObj(rgb.r, rgb.g, rgb.b);
              const hexPlain = baseHex.replace('#', '');
              const hInt = Math.round(hsv.h);
              const sInt = Math.round(hsv.s * 100);
              const vInt = Math.round(hsv.v * 100);

              const row = document.createElement('div');
              row.className = 'tbc_inputrow';
              row.style.marginBottom = '8px';
              row.style.display = 'grid';
              row.style.gridTemplateColumns = '100px minmax(0, 1fr) 18px auto';
              row.style.gap = '4px';
              row.style.alignItems = 'center';
              row.style.minWidth = '0';
              row.style.width = '100%';

              let editorHtml = '';
              if (mode === 'hex') {
                editorHtml = `
                  <div style="display:flex;align-items:center;gap:3px;min-width:0;white-space:nowrap;overflow:hidden;">
                    <div style="font-family:monospace;opacity:.9;">#</div>
                    <input class="tbc_input" data-syscolor-hex="${cat.id}" type="text" value="${hexPlain}" style="width:96px;min-width:0;padding:4px 6px;">
                  </div>
                `;
              } else if (mode === 'rgb') {
                editorHtml = `
                  <div style="display:flex;align-items:center;gap:4px;flex-wrap:nowrap;white-space:nowrap;min-width:0;overflow:hidden;">
                    <div style="display:flex;align-items:center;gap:2px;"><span style="font-size:10px;opacity:.9;">R</span><input class="tbc_input tbc_no_spin" data-syscolor-r="${cat.id}" type="number" min="0" max="255" step="1" value="${rgb.r}" style="width:42px;min-width:42px;padding:4px 4px;"></div>
                    <div style="display:flex;align-items:center;gap:2px;"><span style="font-size:10px;opacity:.9;">G</span><input class="tbc_input tbc_no_spin" data-syscolor-g="${cat.id}" type="number" min="0" max="255" step="1" value="${rgb.g}" style="width:42px;min-width:42px;padding:4px 4px;"></div>
                    <div style="display:flex;align-items:center;gap:2px;"><span style="font-size:10px;opacity:.9;">B</span><input class="tbc_input tbc_no_spin" data-syscolor-b="${cat.id}" type="number" min="0" max="255" step="1" value="${rgb.b}" style="width:42px;min-width:42px;padding:4px 4px;"></div>
                  </div>
                `;
              } else {
                editorHtml = `
                  <div style="display:flex;align-items:center;gap:4px;flex-wrap:nowrap;white-space:nowrap;min-width:0;overflow:hidden;">
                    <div style="display:flex;align-items:center;gap:2px;"><span style="font-size:10px;opacity:.9;">H</span><input class="tbc_input tbc_no_spin" data-syscolor-h="${cat.id}" type="number" min="0" max="360" step="1" value="${hInt}" style="width:42px;min-width:42px;padding:4px 4px;"></div>
                    <div style="display:flex;align-items:center;gap:2px;"><span style="font-size:10px;opacity:.9;">S</span><input class="tbc_input tbc_no_spin" data-syscolor-s="${cat.id}" type="number" min="0" max="100" step="1" value="${sInt}" style="width:42px;min-width:42px;padding:4px 4px;"></div>
                    <div style="display:flex;align-items:center;gap:2px;"><span style="font-size:10px;opacity:.9;">V</span><input class="tbc_input tbc_no_spin" data-syscolor-v="${cat.id}" type="number" min="0" max="100" step="1" value="${vInt}" style="width:42px;min-width:42px;padding:4px 4px;"></div>
                  </div>
                `;
              }

              row.innerHTML = `
                <div style="font-size:11px;opacity:.92;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;">${cat.label}</div>
                ${editorHtml}
                <div data-syscolor-preview="${cat.id}" title="${baseHex}" style="width:14px;height:14px;border-radius:4px;border:1px solid rgba(255,255,255,0.28);background:${baseHex};box-shadow: inset 0 0 0 1px rgba(0,0,0,0.25);cursor:pointer;"></div>
                <div class="tbc_btn" data-syscolor-cycle="${cat.id}" title="Switch format" style="padding:4px 6px;white-space:nowrap;font-size:10px;">${mode.toUpperCase()}->${nextMode.toUpperCase()}</div>
              `;
              host.appendChild(row);
              const btn = row.querySelector(`[data-syscolor-cycle="${cat.id}"]`);
              const previewBtn = row.querySelector(`[data-syscolor-preview="${cat.id}"]`);


              const saveHexForCategory = (parsedHex, opts = null) => {
                if (!parsedHex) return;
                const rerender = !(opts && opts.rerender === false);
                mutateChatState((state) => {
                  state.systemMessageColors = normalizeSystemMessageColors(state.systemMessageColors);
                  state.systemMessageColors[cat.id].hex = parsedHex;
                  state.systemMessageColors[cat.id].format = mode;
                });
                if (rerender) renderSystemMessageColors();
                refreshVisibleSystemMessageRows();
                scheduleChatScan();
              };

              if (mode === 'hex') {
                const inpHex = row.querySelector(`[data-syscolor-hex="${cat.id}"]`);
                if (inpHex) {
                  const applyHex = (opts = null) => {
                    const parsedHex = parseColorTextToHex(`#${String(inpHex.value || '').trim()}`, 'hex');
                    if (!parsedHex) {
                      inpHex.value = hexPlain;
                      return;
                    }
                    saveHexForCategory(parsedHex, opts);
                  };
                  inpHex.addEventListener('input', () => applyHex({ rerender: false }));
                  inpHex.addEventListener('change', applyHex);
                  inpHex.addEventListener('blur', applyHex);
                }
              } else if (mode === 'rgb') {
                const inpR = row.querySelector(`[data-syscolor-r="${cat.id}"]`);
                const inpG = row.querySelector(`[data-syscolor-g="${cat.id}"]`);
                const inpB = row.querySelector(`[data-syscolor-b="${cat.id}"]`);
                const applyRgb = (opts = null) => {
                  if (!inpR || !inpG || !inpB) return;
                  const parsedHex = rgbObjToHex(inpR.value, inpG.value, inpB.value);
                  saveHexForCategory(parsedHex, opts);
                };
                if (inpR) { inpR.addEventListener('input', () => applyRgb({ rerender: false })); }
                if (inpG) { inpG.addEventListener('input', () => applyRgb({ rerender: false })); }
                if (inpB) { inpB.addEventListener('input', () => applyRgb({ rerender: false })); }
                if (inpR) { inpR.addEventListener('change', applyRgb); inpR.addEventListener('blur', applyRgb); }
                if (inpG) { inpG.addEventListener('change', applyRgb); inpG.addEventListener('blur', applyRgb); }
                if (inpB) { inpB.addEventListener('change', applyRgb); inpB.addEventListener('blur', applyRgb); }
              } else {
                const inpH = row.querySelector(`[data-syscolor-h="${cat.id}"]`);
                const inpS = row.querySelector(`[data-syscolor-s="${cat.id}"]`);
                const inpV = row.querySelector(`[data-syscolor-v="${cat.id}"]`);
                const applyHsv = (opts = null) => {
                  if (!inpH || !inpS || !inpV) return;
                  const rgbFromHsv = hsvToRgbObj(inpH.value, clamp01(parseFloat(String(inpS.value)) / 100), clamp01(parseFloat(String(inpV.value)) / 100));
                  const parsedHex = rgbObjToHex(rgbFromHsv.r, rgbFromHsv.g, rgbFromHsv.b);
                  saveHexForCategory(parsedHex, opts);
                };
                if (inpH) { inpH.addEventListener('input', () => applyHsv({ rerender: false })); }
                if (inpS) { inpS.addEventListener('input', () => applyHsv({ rerender: false })); }
                if (inpV) { inpV.addEventListener('input', () => applyHsv({ rerender: false })); }
                if (inpH) { inpH.addEventListener('change', applyHsv); inpH.addEventListener('blur', applyHsv); }
                if (inpS) { inpS.addEventListener('change', applyHsv); inpS.addEventListener('blur', applyHsv); }
                if (inpV) { inpV.addEventListener('change', applyHsv); inpV.addEventListener('blur', applyHsv); }
              }

              if (btn) {
                btn.addEventListener('click', () => {
                  const newMode = SYSTEM_COLOR_NEXT_FORMAT[mode] || 'hex';
                  mutateChatState((state) => {
                    state.systemMessageColors = normalizeSystemMessageColors(state.systemMessageColors);
                    state.systemMessageColors[cat.id].format = newMode;
                  });
                  renderSystemMessageColors();
                });
              }

              if (previewBtn) {
                previewBtn.addEventListener('click', (e) => {
                  e.stopPropagation();
                  const cfgNow = normalizeSystemMessageColors(chatState.systemMessageColors);
                  const currentHex =
                    normalizeHexColor((cfgNow[cat.id] && cfgNow[cat.id].hex) || '') ||
                    baseHex;

                  openPanel(previewBtn, (panel) => {
                    const presetSwatches = COLOR_PRESETS.map((p) => {
                      const active = p.color.toLowerCase() === currentHex.toLowerCase();
                      return `<div class="cg_swatch${active ? ' active' : ''}" data-preset="${p.id}" title="${p.label}" style="background:${p.color};"></div>`;
                    }).join('');

                    panel.innerHTML = `
                      <div class="cg_color_panel">
                        <div class="cg_color_panel_top">
                          <div class="cg_color_panel_preview">
                            <div class="cg_color_panel_previewbox" style="background:${currentHex};"></div>
                            <div class="cg_color_panel_hex">${currentHex.toLowerCase()}</div>
                          </div>
                        </div>
                        <div class="cg_color_panel_body">
                          <div class="cg_color_panel_section">
                            <div class="cg_color_panel_section_title">Presets</div>
                            <div class="cg_color_swatches">${presetSwatches}</div>
                          </div>
                          <div class="cg_color_panel_section">
                            <div class="cg_color_panel_section_title">Custom</div>
                            <div class="cg_color_custom_row">
                              <input class="cg_custom_picker" type="color" value="${currentHex}">
                              <div class="cg_color_usebtn">Use</div>
                            </div>
                            <div style="margin-top:6px;font-size:10px;opacity:.75;">Pick a colour, then press "Use".</div>
                          </div>
                        </div>
                      </div>
                    `;

                    const preview = panel.querySelector('.cg_color_panel_previewbox');
                    const hexEl = panel.querySelector('.cg_color_panel_hex');
                    const customPicker = panel.querySelector('.cg_custom_picker');
                    const useBtn = panel.querySelector('.cg_color_usebtn');

                    const setPreviewHex = (val) => {
                      if (preview) preview.style.background = val;
                      if (hexEl) hexEl.textContent = String(val || '').toLowerCase();
                    };

                    const applyHex = (hexVal) => {
                      const parsed = normalizeHexColor(hexVal);
                      if (!parsed) return;
                      mutateChatState((state) => {
                        state.systemMessageColors = normalizeSystemMessageColors(state.systemMessageColors);
                        state.systemMessageColors[cat.id].hex = parsed;
                      });
                      renderSystemMessageColors();
                      refreshVisibleSystemMessageRows();
                      scheduleChatScan();
                    };

                    panel.querySelectorAll('.cg_swatch').forEach((sw) => {
                      sw.addEventListener('click', () => {
                        const pid = sw.dataset.preset;
                        const p = COLOR_PRESETS.find((pp) => pp.id === pid);
                        if (!p) return;
                        panel.querySelectorAll('.cg_swatch').forEach((s2) => s2.classList.remove('active'));
                        sw.classList.add('active');
                        setPreviewHex(p.color);
                        applyHex(p.color);
                      });
                    });

                    if (customPicker) {
                      customPicker.addEventListener('input', () => {
                        panel.querySelectorAll('.cg_swatch').forEach((s2) => s2.classList.remove('active'));
                        setPreviewHex(customPicker.value);
                      });
                    }

                    if (useBtn) {
                      useBtn.addEventListener('click', () => {
                        if (!customPicker) return;
                        applyHex(customPicker.value);
                        closePanel();
                      });
                    }
                  });
                });
              }
            });
          }

          const addUser = $('tbc_bl_user_add');
          const syscolorResetBtn = $('tbc_syscolor_reset');
            if (addUser) addUser.addEventListener('click', () => {
                const inp = $('tbc_bl_user');
                const u = inp ? inp.value.trim() : '';
                if (!u) return;

                const selfNorm = getSelfNameNorm();
                const uNorm = normalizeName(u);

                if (selfNorm && uNorm === selfNorm) {
                    if (inp) inp.value = '';
                    return;
                }

                let changed = false;
                mutateChatState((state) => {
                  if (!state.blacklistUsers.some((x) => normalizeName(x) === uNorm)) {
                    state.blacklistUsers.push(u);
                    changed = true;
                  }
                });
                if (changed) renderBLUsers();
                if (inp) inp.value = '';
            });

          if (syscolorResetBtn) {
            syscolorResetBtn.addEventListener('click', () => {
              mutateChatState((state) => {
                const next = {};
                SYSTEM_COLOR_CATEGORIES.forEach((cat) => {
                  const curMode =
                    state.systemMessageColors &&
                    state.systemMessageColors[cat.id] &&
                    state.systemMessageColors[cat.id].format
                      ? state.systemMessageColors[cat.id].format
                      : 'hex';
                  next[cat.id] = {
                    hex: SYSTEM_COLOR_DEFAULT_HEX[cat.id] || SYSTEM_COLOR_DEFAULT_HEX.defaultSystem,
                    format: curMode,
                  };
                });
                state.systemMessageColors = next;
              });
              renderSystemMessageColors();
              refreshVisibleSystemMessageRows();
              scheduleChatScan();
            });
          }

          refreshChatSettingsUi = () => {
            updateChatStorageHintUI();
            renderToggles();
            renderIngameLines();
            renderIngameFadeDelay();
            renderSystemMessageColors();
            renderBLUsers();
          };

          refreshChatSettingsUi();
          renderBLUsers();
        },
      });

      updateChatStorageKey();
      setupChatObservers();
      setupCustomSlashCommands();
      setupIngameChatFocusGuards();
      ensureTopAdClickthroughWatcher();
      ensureChatAccountWatchers();

      scheduleChatScan();
      injectLobbyChatCog();
      scheduleIngameVisualRefresh();
      updateReplaySystemProtectionLatch();

      document.addEventListener('visibilitychange', () => {
        scheduleIngameVisualRefresh();
        updateReplaySystemProtectionLatch();
      });
      window.addEventListener('focus', () => {
        scheduleIngameVisualRefresh();
        updateReplaySystemProtectionLatch();
      });
      window.addEventListener('blur', () => updateReplaySystemProtectionLatch());
      window.addEventListener('resize', () => scheduleIngameVisualRefresh());

      waitForElement('gamerenderer', (el) => {
        const obs = new MutationObserver(() => updateReplaySystemProtectionLatch());
        obs.observe(el, { attributes: true, attributeFilter: ['style', 'class'] });
      });
      waitForElement('newbonklobby', (el) => {
        const obs = new MutationObserver(() => updateReplaySystemProtectionLatch());
        obs.observe(el, { attributes: true, attributeFilter: ['style', 'class'] });
      });

    }

    if (window.bonkMods) initChatMod();
    window.addEventListener('bonkModsReady', initChatMod);

    window.addEventListener('recolorGroupsChanged', () => {
      refreshAllIndexedRows();
      if (groupsPanelVisible) renderGroupsPanel();
      queueGroupsPanelActionAutoSync();
    });
    window.addEventListener('tbcGroupMembershipChanged', (e) => {
      const names = e && e.detail && Array.isArray(e.detail.players) ? e.detail.players : [];
      if (names.length) refreshRowsForAuthors(names);
      else refreshAllIndexedRows();
      if (groupsPanelVisible) renderGroupsPanel();
      queueGroupsPanelActionAutoSync();
    });
    window.addEventListener('tbcGroupColorChanged', (e) => {
      const names = e && e.detail && Array.isArray(e.detail.players) ? e.detail.players : [];
      if (names.length) refreshRowsForAuthors(names);
      else refreshAllIndexedRows();
    });
  })();

    waitForElement('pretty_top_level', () => {
      waitForElement('pretty_top_name', () => {
        ensureAccountObservers();
        updateAccountStorageKey();
      });
    });
  })();
})();