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