TBD Shoutbox Notifier

Detects @mentions and keywords in TorrentBD shoutbox and alerts you with sound, toast popups, custom notifications, and customizable highlights.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         TBD Shoutbox Notifier
// @version      1.1
// @description  Detects @mentions and keywords in TorrentBD shoutbox and alerts you with sound, toast popups, custom notifications, and customizable highlights.
// @author       Anik
// @namespace    https://aonexyz.vercel.app/
// @match        https://*.torrentbd.com/*
// @match        https://*.torrentbd.net/*
// @match        https://*.torrentbd.org/*
// @match        https://*.torrentbd.me/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      github.com
// @connect      raw.githubusercontent.com
// @connect      objects.githubusercontent.com
// @license      MIT
// @run-at       document-end
// @icon         https://iili.io/Bb6kpwb.png
// ==/UserScript==

(function () {
    'use strict';

    /* ═══════════════════════════════════════════════════════
       STORE
    ═══════════════════════════════════════════════════════ */
    const Store = {
        get: (k, d) => GM_getValue(k, d),
        set: (k, v) => GM_setValue(k, v),
    };

    /* ═══════════════════════════════════════════════════════
       CONFIG
       hlRowEnabled   → left border + bg tint on row    (default OFF, user toggles)
       hlMentionOn    → @username TEXT gets coloured     (default ON,  user can toggle off)
       hlColor        → colour for row border/bg
       mentionColor   → colour for username text highlight
    ═══════════════════════════════════════════════════════ */
    const Cfg = {
        username:        Store.get('v1_username',''),
        volume:          Store.get('v1_volume',0.65),
        hlColor:         Store.get('v1_hlColor','#4ade80'),
        mentionColor:    Store.get('v1_mentionColor','#60a5fa'),
        hlRowEnabled:    Store.get('v1_hlRow',true),
        hlMentionOn:     Store.get('v1_hlMention',true),
        keywords:        Store.get('v1_keywords',[]),
        popupMention:    Store.get('v1_popupMention', true),
        popupKeywords:   Store.get('v1_popupKeywords', true),
        // sound — stored as base64 data-URLs so they survive page reload
        customSoundB64:  Store.get('v1_customSoundB64', ''), // user-uploaded MP3
        customSoundName: Store.get('v1_customSoundName', ''), // filename of user-uploaded MP3
        defaultSoundUrl: Store.get('v1_defaultSound', 'https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-493469.mp3'),
        defaultSoundB64: Store.get('v1_defaultSoundB64', ''), // fetched+cached version of defaultSoundUrl
        darkMode:        Store.get('v1_darkMode', true),
        // keep old key aliases so existing save calls work
        get customSoundUrl() { return this.customSoundB64; },
        set customSoundUrl(v) { this.customSoundB64 = v; Store.set('v1_customSoundB64', v); },
        save(k, v) { this[k] = v; Store.set(`v1_${k}`, v); },
    };

    /* ═══════════════════════════════════════════════════════
       SEEN IDs
    ═══════════════════════════════════════════════════════ */
    const Seen = (() => {
        let list = Store.get('v1_seen', []);
        return {
            has:  id => list.includes(id),
            mark: id => {
                if (list.includes(id)) return;
                list.push(id);
                if (list.length > 500) list.shift();
                Store.set('v1_seen', list);
            },
        };
    })();

    /* ═══════════════════════════════════════════════════════
       USERNAME AUTO-DETECT
    ═══════════════════════════════════════════════════════ */
    function detectUser() {
        for (const el of document.querySelectorAll('.tbdrank')) {
            if (el.closest('#shoutbox-container')) continue;
            const t = el.firstChild;
            if (t?.nodeType === Node.TEXT_NODE) {
                const name = t.nodeValue.trim();
                if (name) return name;
            }
        }
        return '';
    }

    /* ═══════════════════════════════════════════════════════
       SOUND
       • Custom sound  → FileReader → base64 → GM_setValue → reload safe
       • Default sound → GM_xmlhttpRequest (bypasses CORS) → base64 → GM_setValue
       • AudioContext pre-decode → zero latency playback
    ═══════════════════════════════════════════════════════ */
    const Sound = (() => {
        let _ctx = null;
        let _buffer = null;

        function getCtx() {
            if (!_ctx) _ctx = new (window.AudioContext || window.webkitAudioContext)();
            return _ctx;
        }

        // Returns the active base64 data-URL (custom > cached default)
        function activeSrc() {
            if (Cfg.customSoundB64) return Cfg.customSoundB64;
            if (Cfg.defaultSoundB64) return Cfg.defaultSoundB64;
            return null;
        }

        // Decode a base64 data-URL into an AudioBuffer
        function _decodeB64(dataUrl) {
            return new Promise((resolve, reject) => {
                try {
                    const ctx = getCtx();
                    const b64 = dataUrl.split(',')[1];
                    const bin = atob(b64);
                    const bytes = new Uint8Array(bin.length);
                    for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
                    ctx.decodeAudioData(bytes.buffer.slice(0), resolve, reject);
                } catch(e) { reject(e); }
            });
        }

        // Fetch a remote URL via GM_xmlhttpRequest (bypasses CORS), save as base64
        function _fetchAndCache(url) {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    responseType: 'arraybuffer',
                    onload(res) {
                        try {
                            const bytes = new Uint8Array(res.response);
                            // Convert in chunks to avoid call stack overflow on large files
                            const CHUNK = 8192;
                            let bin = '';
                            for (let i = 0; i < bytes.length; i += CHUNK) {
                            bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
                            }
                            const b64 = 'data:audio/mpeg;base64,' + btoa(bin);
                            // Cache it so future reloads don't need to fetch again
                            Cfg.defaultSoundB64 = b64;
                            Store.set('v1_defaultSoundB64', b64);
                            resolve(b64);
                         } catch(e) { reject(e); }
                      },
                    onerror(e) { reject(e); },
                });
            });
        }

        async function _load() {
            try {
                let src = activeSrc();
                if (!src) {
                    // No cached base64 yet → fetch default sound now
                    src = await _fetchAndCache(Cfg.defaultSoundUrl);
                }
                _buffer = await _decodeB64(src);
            } catch(e) {
                _buffer = null;
            }
        }

        function preload() { _load(); }

        function reload() { _buffer = null; _load(); }

        async function play() {
            if (Cfg.volume < 0.01) return;
            if (_buffer) {
                try {
                    const ctx = getCtx();
                    // Resume AudioContext if suspended (browser policy)
                    if (ctx.state === 'suspended') {
                        try { await ctx.resume(); } catch(_) {}
                    }
                    const src = ctx.createBufferSource();
                    src.buffer = _buffer;
                    const gain = ctx.createGain();
                    gain.gain.value = Cfg.volume;
                    src.connect(gain);
                    gain.connect(ctx.destination);
                    src.start(0);
                    return;
                } catch(_) {}
            }
            // fallback — Audio element (works even when tab is hidden in most browsers)
            const fb = activeSrc() || Cfg.defaultSoundUrl;
            try {
                const a = new Audio(fb);
                a.volume = Math.min(1, Math.max(0, Cfg.volume));
                a.play().catch(() => {});
            } catch(_) {}
        }

        // Instant preview: stream via Audio() immediately, cache buffer in background
        function previewUrl(url) {
            if (Cfg.volume < 0.01) return;
            try {
                const a = new Audio(url);
                a.volume = Cfg.volume;
                a.play().catch(() => {});
            } catch(_) {}
            // also cache the new buffer in background for future notification plays
            _buffer = null;
            _load();
        }

        return { preload, reload, play, previewUrl };
    })();

    /* ═══════════════════════════════════════════════════════
       TOAST
    ═══════════════════════════════════════════════════════ */
    const Toasts = (() => {
        let wrap = null;

        function getWrap() {
            if (wrap) return wrap;
            wrap = document.createElement('div');
            wrap.id = 'tbn-toasts';
            document.body.appendChild(wrap);
            return wrap;
        }

        const ICONS = {
            mention: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10h5v-2h-5c-4.34 0-8-3.66-8-8s3.66-8 8-8 8 3.66 8 8v1.43c0 .79-.71 1.57-1.5 1.57s-1.5-.78-1.5-1.57V12c0-2.76-2.24-5-5-5s-5 2.24-5 5 2.24 5 5 5c1.38 0 2.64-.56 3.54-1.47.65.89 1.77 1.47 2.96 1.47 1.97 0 3.5-1.6 3.5-3.57V12c0-5.52-4.48-10-10-10zm0 13c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></svg>`,
            keyword: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>`,
        };

        function show(type) {
            if (type === 'mention' && !Cfg.popupMention) return; // skip if mention popup disabled
            if (type === 'keyword' && !Cfg.popupKeywords) return; // skip if keyword popup disabled
            const el = document.createElement('div');
            el.className = `tbn-toast tbn-toast-${type}`;
            const iconBg = type === 'mention' ? `${Cfg.mentionColor}22` : 'rgba(96,165,250,.12)';
            const iconClr = type === 'mention' ? Cfg.mentionColor : '#60a5fa';
            el.innerHTML = `
                <div class="tbn-t-icon" style="background:${iconBg};color:${iconClr};">${ICONS[type] || ICONS.keyword}</div>
                <div class="tbn-t-body">
                    <div class="tbn-t-title">${type === 'mention' ? 'You were mentioned' : 'Keyword matched'}</div>
                    <div class="tbn-t-msg">${type === 'mention' ? 'Your name was @tagged in the shoutbox' : 'A keyword was detected in the shoutbox'}</div>
                </div>
                <button class="tbn-t-close" aria-label="Dismiss">
                    <svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
                </button>`;

            getWrap().appendChild(el);
            requestAnimationFrame(() => requestAnimationFrame(() => el.classList.add('tbn-t-in')));

            const dismiss = () => {
                el.classList.remove('tbn-t-in');
                el.classList.add('tbn-t-out');
                setTimeout(() => el.remove(), 380);
            };
            el.querySelector('.tbn-t-close').addEventListener('click', dismiss);
            setTimeout(dismiss, 5000);
        }

        return { show };
    })();

    /* ═══════════════════════════════════════════════════════
       BROWSER NOTIFICATION  (works when tab is hidden/minimized)
    ═══════════════════════════════════════════════════════ */
    const BrowserNotify = (() => {
        // Request permission once on load
        function requestPermission() {
            if (!('Notification' in window)) return;
            if (Notification.permission === 'default') {
                Notification.requestPermission();
            }
        }

        function send(type) {
            if (!('Notification' in window)) return;
            if (Notification.permission !== 'granted') return;

            const title = type === 'mention' ? '💬 You were mentioned!' : '🔑 Keyword matched!';
            const body = type === 'mention'
                ? `Someone @tagged ${Cfg.username} in the shoutbox`
                : 'A keyword was detected in the shoutbox';

            try {
                const n = new Notification(title, {
                    body,
                    icon:   'https://www.torrentbd.com/favicon.ico',
                    badge:  'https://www.torrentbd.com/favicon.ico',
                    tag:    `tbn-${type}`, // replaces previous same-type notification
                    silent: true, // sound handled by Sound.play()
                });
                // Click on notification focuses the TBD tab
                n.onclick = () => {
                    window.focus();
                    n.close();
                };
                // Auto-close after 6s
                setTimeout(() => n.close(), 6000);
            } catch (_) {}
        }

        return { requestPermission, send };
    })();

    /* ═══════════════════════════════════════════════════════
       NOTIFY
    ═══════════════════════════════════════════════════════ */
    function notify(type) {
        Sound.play();
        Toasts.show(type);
        BrowserNotify.send(type);
    }

    /* ═══════════════════════════════════════════════════════
       APPLY HIGHLIGHT TO A SHOUT ROW
       1. hlMentionOn  → wrap matching username text with coloured <mark>
       2. hlRowEnabled → add left border + bg tint to the whole row
    ═══════════════════════════════════════════════════════ */
    function applyHighlight(shoutEl, user) {
        // ── 1. Username text highlight — check hlMentionOn ──
        if (Cfg.hlMentionOn) {
            const textEl = shoutEl.querySelector('.shout-text');
            if (textEl && !textEl.querySelector('.tbn-mark')) {
                const esc = user.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                const regex = new RegExp(`(@${esc}|${esc})`, 'gi');
                textEl.innerHTML = textEl.innerHTML.replace(
                    regex,
                    `<mark class="tbn-mark" style="color:${Cfg.mentionColor};background:${Cfg.mentionColor}22;border-radius:3px;padding:0 2px;font-weight:700;font-style:normal;">$1</mark>`
                );
            }
        }

        // ── 2. Row border + bg ──
        if (Cfg.hlRowEnabled) {
            shoutEl.style.setProperty('--tbn-c', Cfg.hlColor);
            shoutEl.classList.add('tbn-hl-row');
        }

        // ── 3. Flash ──
        shoutEl.classList.remove('tbn-flash');
        void shoutEl.offsetWidth;
        shoutEl.classList.add('tbn-flash');
    }

    /* ═══════════════════════════════════════════════════════
       DETECTION
    ═══════════════════════════════════════════════════════ */
    function inspect(el, silent = false) {
        if (!el?.id) return;
        const body = el.querySelector('.shout-text');
        if (!body) return;

        const bodyText = body.textContent || '';
        const user = Cfg.username.trim();
        if (!user) return;

        const done = Seen.has(el.id);
        const esc = user.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

        // Only check message body — rowText contains sender name and causes false positives
        const mentionPats = [
            new RegExp(`@${esc}`, 'i'),
            new RegExp(`@${esc}\\.`, 'i'),
        ];

        if (mentionPats.some(rx => rx.test(bodyText)) || bodyText.toLowerCase().includes('@' + user.toLowerCase())) {
            applyHighlight(el, user);
            if (!done) { if (!silent) notify('mention'); Seen.mark(el.id); }
            return;
        }

        // keyword detection
        for (const kw of Cfg.keywords.filter(k => k.trim())) {
            const kwEsc = kw.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            if (new RegExp(`\\b${kwEsc}\\b`, 'i').test(bodyText)) {
                // highlight matched keyword text — white + underline only (no row highlight)
                if (!body.querySelector('.tbn-kw-mark')) {
                    const kwRegex = new RegExp(`(\\b${kwEsc}\\b)`, 'gi');
                    body.innerHTML = body.innerHTML.replace(
                        kwRegex,
                        `<mark class="tbn-kw-mark">$1</mark>`
                    );
                }
                el.classList.remove('tbn-flash');
                void el.offsetWidth;
                el.classList.add('tbn-flash');
                if (!done) { if (!silent) notify('keyword'); Seen.mark(el.id); }
                return;
            }
        }
    }

    /* ═══════════════════════════════════════════════════════
       BUILD MODAL HTML
    ═══════════════════════════════════════════════════════ */
    function buildModal() {
        const modal = document.createElement('div');
        modal.id = 'tbn-modal';
        modal.style.display = 'none';

        modal.innerHTML = `
        <div id="tbn-backdrop"></div>
        <div id="tbn-panel" role="dialog" aria-label="Notifier Settings">

            <div id="tbn-hdr">
                <div id="tbn-hdr-l">
                    <div id="tbn-hdr-icon">
                        <svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
                    </div>
                    <div>
                        <h2>Notifier Settings</h2>
                        <p>Configure your alerts and triggers</p>
                    </div>
                </div>
                <button id="tbn-close" aria-label="Close">
                    <svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
                </button>
            </div>

            <div id="tbn-body">

                <!-- Username -->
                <div class="tbn-fg">
                    <label class="tbn-label">
                        <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/></svg>
                        Username
                    </label>
                    <div class="tbn-iw">
                        <input id="tbn-uname" type="text" placeholder="Detection username..." autocomplete="off" spellcheck="false">
                        <div id="tbn-uname-dot"></div>
                    </div>
                </div>

                <!-- Custom Theme -->
                <div class="tbn-fg">
                    <label class="tbn-label">
                        <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></svg>
                        Custom Theme
                    </label>

                    <!-- Row 1: two toggle cards -->
                    <div id="tbn-theme-grid">

                        <!-- Highlight Color (row border) — default OFF -->
                        <button class="tbn-tc" id="tog-hlrow">
                            <div class="tbn-tc-l">
                                <span class="tbn-tc-ico" id="ico-hlrow">
                                    <svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.66 7.93 12 2.27 6.34 7.93c-3.12 3.12-3.12 8.19 0 11.31C7.9 20.8 9.95 21.58 12 21.58c2.05 0 4.1-.78 5.66-2.34 3.12-3.12 3.12-8.19 0-11.31zM12 19.59c-1.6 0-3.11-.62-4.24-1.76C6.62 16.69 6 15.19 6 13.59c0-1.6.62-3.11 1.76-4.24L12 5.12v14.47z"/></svg>
                                </span>
                                <span class="tbn-tc-txt">Highlight Color</span>
                            </div>
                            <div class="tbn-dot" id="dot-hlrow"></div>
                        </button>

                        <!-- Highlight mention (username text) — default ON -->
                        <button class="tbn-tc" id="tog-hlmention">
                            <div class="tbn-tc-l">
                                <span class="tbn-tc-ico" id="ico-hlmention">
                                    <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10h5v-2h-5c-4.34 0-8-3.66-8-8s3.66-8 8-8 8 3.66 8 8v1.43c0 .79-.71 1.57-1.5 1.57s-1.5-.78-1.5-1.57V12c0-2.76-2.24-5-5-5s-5 2.24-5 5 2.24 5 5 5c1.38 0 2.64-.56 3.54-1.47.65.89 1.77 1.47 2.96 1.47 1.97 0 3.5-1.6 3.5-3.57V12c0-5.52-4.48-10-10-10zm0 13c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></svg>
                                </span>
                                <span class="tbn-tc-txt">Highlight mention</span>
                            </div>
                            <div class="tbn-dot" id="dot-hlmention"></div>
                        </button>

                    </div>

                    <!-- Row 2: Highlight Colour picker -->
                    <div class="tbn-cpick" id="cpick-hlrow">
                        <div class="tbn-cp-l">
                            <div class="tbn-swatch" id="sw-hlrow" title="Pick colour"></div>
                            <input type="color" id="in-hlrow">
                            <div>
                                <span class="tbn-cp-name">Pick Highlight Colour</span>
                                <span class="tbn-cp-hex" id="hex-hlrow">#4ADE80</span>
                            </div>
                        </div>
                        <div class="tbn-presets">
                            <button class="tbn-pre" data-target="hlrow" data-color="#4ade80" style="background:#4ade80;"></button>
                            <button class="tbn-pre" data-target="hlrow" data-color="#60a5fa" style="background:#60a5fa;"></button>
                            <button class="tbn-pre" data-target="hlrow" data-color="#f87171" style="background:#f87171;"></button>
                            <button class="tbn-pre" data-target="hlrow" data-color="#fb923c" style="background:#fb923c;"></button>
                            <button class="tbn-pre" data-target="hlrow" data-color="#a78bfa" style="background:#a78bfa;"></button>
                        </div>
                    </div>

                    <!-- Row 3: Highlight mention colour picker -->
                    <div class="tbn-cpick" id="cpick-mention">
                        <div class="tbn-cp-l">
                            <div class="tbn-swatch" id="sw-mention" title="Pick colour"></div>
                            <input type="color" id="in-mention">
                            <div>
                                <span class="tbn-cp-name">Pick Mention Colour</span>
                                <span class="tbn-cp-hex" id="hex-mention">#FB923C</span>
                            </div>
                        </div>
                        <div class="tbn-presets">
                            <button class="tbn-pre" data-target="mention" data-color="#4ade80" style="background:#4ade80;"></button>
                            <button class="tbn-pre" data-target="mention" data-color="#60a5fa" style="background:#60a5fa;"></button>
                            <button class="tbn-pre" data-target="mention" data-color="#f87171" style="background:#f87171;"></button>
                            <button class="tbn-pre" data-target="mention" data-color="#fb923c" style="background:#fb923c;"></button>
                            <button class="tbn-pre" data-target="mention" data-color="#a78bfa" style="background:#a78bfa;"></button>
                        </div>
                    </div>

                </div>

                <!-- Audio Volume -->
                <div class="tbn-fg">
                    <label class="tbn-label">
                        <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
                        Audio Preferences
                    </label>

                    <!-- 1. Default Sound button + popup board -->
                    <div style="position:relative;">
                        <div class="tbn-cpick tbn-sound-card" id="tbn-default-sound-btn" style="cursor:pointer;">
                            <div class="tbn-cp-l">
                                <div class="tbn-sound-icon">
                                    <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
                                </div>
                                <div>
                                    <span class="tbn-cp-name">Default Sound</span>
                                    <span class="tbn-cp-hex" id="tbn-default-sound-name">Standard Bell</span>
                                </div>
                            </div>
                            <svg viewBox="0 0 24 24" fill="currentColor" style="width:16px;height:16px;color:#6b7280;flex-shrink:0;"><path d="M7 10l5 5 5-5z"/></svg>
                        </div>

                        <!-- Notification Board popup -->
                        <div id="tbn-nb-popup" style="display:none;">
                            <div id="tbn-nb-header">
                                <span id="tbn-nb-title">NOTIFICATION BOARD</span>
                                <button id="tbn-nb-close">✕</button>
                            </div>
                            <div id="tbn-nb-grid">
                                <button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-493469.mp3" data-name="Minimalistic"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Minimalistic</button>
                                <button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-352705.mp3" data-name="Modern Chime"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Modern Chime</button>
                                <button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-372475.mp3" data-name="Digital Alert"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Digital Alert</button>
                                <button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-398649.mp3" data-name="Soft Pulse"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Soft Pulse</button>
                                <button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-352755.mp3" data-name="Standard Bell"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Standard Bell</button>
                                <button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-494238.mp3" data-name="Echo Ring"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Echo Ring</button>
                                <button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-494256.mp3" data-name="Crystal Clear"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Crystal Clear</button>
                                <button class="tbn-nb-btn" data-url="https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/notification-494544.mp3" data-name="Classic Pop"><svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-2-3.5l6-4.5-6-4.5v9z"/></svg> Classic Pop</button>
                            </div>
                        </div>
                    </div>

                    <!-- 2. Custom Sound -->
                    <div class="tbn-cpick tbn-sound-card" id="tbn-sound-card">
                        <div class="tbn-cp-l">
                            <div class="tbn-sound-icon">
                                <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
                            </div>
                            <div>
                                <span class="tbn-cp-name">Custom Sound</span>
                                <span class="tbn-cp-hex" id="tbn-sound-name">No file chosen</span>
                            </div>
                        </div>
                        <div style="display:flex;gap:7px;align-items:center;">
                            <button class="tbn-sound-btn" id="tbn-sound-pick" style="outline:none;">
                                Choose File
                                <input type="file" id="tbn-sound-file" accept=".mp3,audio/mp3,audio/mpeg" style="display:none;">
                            </button>
                            <button class="tbn-sound-remove" id="tbn-sound-remove" style="display:none;">✕</button>
                        </div>
                    </div>

                    <div id="tbn-vol-card">
                        <div id="tbn-vol-row">
                            <span id="tbn-vol-ico" class="tbn-vol-svg"></span>
                            <div id="tbn-vol-wrap">
                                <div id="tbn-vol-fill"></div>
                                <input type="range" id="tbn-vol-slider" min="0" max="1" step="0.001">
                                <div id="tbn-vol-thumb"></div>
                            </div>
                            <span id="tbn-vol-pct">65%</span>
                        </div>
                        <button id="tbn-test-sound">
                            Test Sound
                        </button>
                    </div>
                </div>

                <!-- Additional Keywords -->
                <div class="tbn-fg">
                    <div class="tbn-kw-hdr">
                        <label class="tbn-label" style="margin:0;">
                            <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l-5.5 9h11L12 2zm0 3.84L13.93 9h-3.87L12 5.84zM17.5 13c-2.49 0-4.5 2.01-4.5 4.5S15.01 22 17.5 22s4.5-2.01 4.5-4.5S19.99 13 17.5 13zm0 7c-1.38 0-2.5-1.12-2.5-2.5S16.12 15 17.5 15s2.5 1.12 2.5 2.5S18.88 20 17.5 20zM3 21.5h8v-8H3v8zm2-6h4v4H5v-4z"/></svg>
                            Additional Keywords (One per line)
                        </label>
                        <button id="tbn-kw-reset">Reset</button>
                    </div>
                    <textarea id="tbn-kw" placeholder="Add keywords to be notified about..." rows="4" spellcheck="false"></textarea>
                </div>

                <!-- Extras -->
                <div class="tbn-fg">
                    <label class="tbn-label">
                        <svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14l-5-5 1.41-1.41L12 14.17l7.59-7.59L21 8l-9 9z"/></svg>
                        Extras
                    </label>

                    <!-- 1. Popup mention toggle -->
                    <button class="tbn-tc" id="tog-popup">
                        <div class="tbn-tc-l">
                            <span class="tbn-tc-ico" id="ico-popup">
                                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h2v3l4-3h10a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="10" r="2.4"/><path d="M14.4 10v1a2 2 0 0 0 4 0v-1a6.4 6.4 0 1 0-2.55 5.1"/></svg>
                            </span>
                            <span class="tbn-tc-txt">Popup mention</span>
                        </div>
                        <div class="tbn-dot" id="dot-popup"></div>
                    </button>

                    <!-- 2. Popup keywords toggle -->
                    <button class="tbn-tc" id="tog-popup-kw">
                        <div class="tbn-tc-l">
                            <span class="tbn-tc-ico" id="ico-popup-kw">
                                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h7l1 2.5 1-2.5h7a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/><circle cx="8.5" cy="10" r="2"/><path d="M10.5 10h5M13.5 9.2v1.6M15 9.2v1.6"/></svg>
                            </span>
                            <span class="tbn-tc-txt">Popup keywords</span>
                        </div>
                        <div class="tbn-dot" id="dot-popup-kw"></div>
                    </button>

                    <!-- Dark / Light mode -->
                    <div id="tbn-mode-grid">
                        <button class="tbn-mode-btn" id="btn-dark">
                            <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"/></svg>
                            Dark Mode
                        </button>
                        <button class="tbn-mode-btn" id="btn-light">
                            <svg viewBox="0 0 24 24" fill="currentColor"><path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm9-9.95h-2V3.5h2V.55zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 10.5v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19.5h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z"/></svg>
                            Light Mode
                        </button>
                    </div>
                </div>

            </div>

        <div id="tbn-footer">
         𝗗𝗲𝘃𝗲𝗹𝗼𝗽𝗲𝗱 𝗯𝘆
         <strong>
            <a href="https://aonexyz.vercel.app" target="_blank" style="text-decoration: none; color: inherit;">
         𝟰𝗡𝟭𝗞
      </a>
    </strong>
   </div>`;

        document.body.appendChild(modal);
        return modal;
    }

    /* ═══════════════════════════════════════════════════════
       WIRE MODAL
    ═══════════════════════════════════════════════════════ */
    function wireModal(modal) {
        const Q = s => modal.querySelector(s);

        const backdrop = Q('#tbn-backdrop');
        const closeBtn = Q('#tbn-close');
        const unameIn = Q('#tbn-uname');
        const unameDot = Q('#tbn-uname-dot');

        // Highlight Color (row border)
        const togHlRow = Q('#tog-hlrow');
        const dotHlRow = Q('#dot-hlrow');
        const icoHlRow = Q('#ico-hlrow');
        const swHlRow = Q('#sw-hlrow');
        const inHlRow = Q('#in-hlrow');
        const hexHlRow = Q('#hex-hlrow');

        // Highlight mention (text)
        const togHlMention = Q('#tog-hlmention');
        const dotHlMention = Q('#dot-hlmention');
        const icoHlMention = Q('#ico-hlmention');
        const swMention = Q('#sw-mention');
        const inMention = Q('#in-mention');
        const hexMention = Q('#hex-mention');

        const kwTA = Q('#tbn-kw');
        const kwReset = Q('#tbn-kw-reset');
        const volSlider = Q('#tbn-vol-slider');
        const volFill = Q('#tbn-vol-fill');
        const volThumb = Q('#tbn-vol-thumb');
        const volPct = Q('#tbn-vol-pct');
        const volIco = Q('#tbn-vol-ico');
        const testSound = Q('#tbn-test-sound');

        // SVG paths for each volume level
        const VOL_ICONS = {
            mute: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="20" height="20">
                <path d="M11 5L6 9H2v6h4l5 4V5z"/>
                <line x1="23" y1="9" x2="17" y2="15"/>
                <line x1="17" y1="9" x2="23" y2="15"/>
            </svg>`,
            high: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="20" height="20">
                <path d="M11 5L6 9H2v6h4l5 4V5z"/>
                <path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
                <path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
            </svg>`,
        };

        function getVolIcon(pct) {
            return pct === 0 ? VOL_ICONS.mute : VOL_ICONS.high;
        }
        function setHlColor(c) {
            Cfg.save('hlColor', c);
            swHlRow.style.background = c;
            swHlRow.style.boxShadow = `0 0 12px ${c}55`;
            inHlRow.value = c;
            hexHlRow.textContent = c.toUpperCase();
            icoHlRow.style.color = Cfg.hlRowEnabled ? c : '#6b7280';
        }

        function setMentionColor(c) {
            Cfg.save('mentionColor', c);
            swMention.style.background = c;
            swMention.style.boxShadow = `0 0 12px ${c}55`;
            inMention.value = c;
            hexMention.textContent = c.toUpperCase();
            icoHlMention.style.color = Cfg.hlMentionOn ? c : '#6b7280';
        }

        /* ── toggle sync ── */
        function syncHlRow() {
            const on = Cfg.hlRowEnabled;
            togHlRow.classList.toggle('tbn-tc-off', !on);
            dotHlRow.style.background = on ? Cfg.hlColor : 'transparent';
            dotHlRow.style.borderColor = on ? Cfg.hlColor : '#374151';
            icoHlRow.style.color = on ? Cfg.hlColor : '#6b7280';
        }

        function syncHlMention() {
            const on = Cfg.hlMentionOn;
            togHlMention.classList.toggle('tbn-tc-off', !on);
            dotHlMention.style.background = on ? Cfg.mentionColor : 'transparent';
            dotHlMention.style.borderColor = on ? Cfg.mentionColor : '#374151';
            icoHlMention.style.color = on ? Cfg.mentionColor : '#6b7280';
        }

        function setVol(v) {
            // v is 0.0–1.0 (matches React's volume state)
            Cfg.save('volume', v);
            volSlider.value = v;
            const pct = Math.round(v * 100);
            volFill.style.width = pct + '%';
            volPct.textContent = pct + '%';
            // Move visual capsule thumb — left: calc(v*100% - 14px) for 28px wide thumb
            volThumb.style.left = `calc(${pct}% - 14px)`;
            // Dynamic volume SVG icon
            volIco.innerHTML = getVolIcon(pct);
        }



        /* ── events ── */
        backdrop.addEventListener('click', closeModal);
        closeBtn.addEventListener('click', closeModal);
        document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });

        unameIn.addEventListener('input', () => {
            Cfg.save('username', unameIn.value.trim());
            unameDot.className = Cfg.username ? 'tbn-dot-g' : 'tbn-dot-gr';
        });

        togHlRow.addEventListener('click', () => {
            Cfg.hlRowEnabled = !Cfg.hlRowEnabled;
            Store.set('v1_hlRow', Cfg.hlRowEnabled);
            syncHlRow();
        });

        togHlMention.addEventListener('click', () => {
            Cfg.hlMentionOn = !Cfg.hlMentionOn;
            Store.set('v1_hlMention', Cfg.hlMentionOn);
            syncHlMention();
        });

        swHlRow.addEventListener('click', () => inHlRow.click());
        inHlRow.addEventListener('input', () => { setHlColor(inHlRow.value); syncHlRow(); });

        swMention.addEventListener('click', () => inMention.click());
        inMention.addEventListener('input', () => { setMentionColor(inMention.value); syncHlMention(); });

        modal.querySelectorAll('.tbn-pre').forEach(btn => {
            btn.addEventListener('click', () => {
                const c = btn.dataset.color;
                if (btn.dataset.target === 'hlrow') { setHlColor(c); syncHlRow(); }
                else { setMentionColor(c); syncHlMention(); }
            });

            // hover → live preview on swatch + hex + toggle dot + icon
            btn.addEventListener('mouseenter', () => {
                const c = btn.dataset.color;
                if (btn.dataset.target === 'hlrow') {
                    swHlRow.style.background = c;
                    swHlRow.style.boxShadow = `0 0 12px ${c}55`;
                    hexHlRow.textContent = c.toUpperCase();
                    dotHlRow.style.background = c;
                    dotHlRow.style.borderColor = c;
                    icoHlRow.style.color = c;
                } else {
                    swMention.style.background = c;
                    swMention.style.boxShadow = `0 0 12px ${c}55`;
                    hexMention.textContent = c.toUpperCase();
                    dotHlMention.style.background = c;
                    dotHlMention.style.borderColor = c;
                    icoHlMention.style.color = c;
                }
            });

            // mouse leave → restore saved colour
            btn.addEventListener('mouseleave', () => {
                if (btn.dataset.target === 'hlrow') { setHlColor(Cfg.hlColor); syncHlRow(); }
                else { setMentionColor(Cfg.mentionColor); syncHlMention(); }
            });
        });

        kwTA.addEventListener('input', () => {
            Cfg.save('keywords', kwTA.value.split('\n').map(k => k.trim()).filter(Boolean));
        });
        kwReset.addEventListener('click', () => { kwTA.value = ''; Cfg.save('keywords', []); });

        volSlider.addEventListener('input', () => setVol(parseFloat(volSlider.value)));
        testSound.addEventListener('click', () => { ripple(testSound); Sound.play(); });

        // ── Extras ──
        const togPopup = Q('#tog-popup');
        const dotPopup = Q('#dot-popup');
        const icoPopup = Q('#ico-popup');
        const togPopupKw = Q('#tog-popup-kw');
        const dotPopupKw = Q('#dot-popup-kw');
        const icoPopupKw = Q('#ico-popup-kw');
        const soundPick = Q('#tbn-sound-pick');
        const soundFile = Q('#tbn-sound-file');
        const soundName = Q('#tbn-sound-name');
        const btnDark = Q('#btn-dark');
        const btnLight = Q('#btn-light');

        function syncPopup() {
            const on = Cfg.popupMention;
            togPopup.classList.toggle('tbn-tc-off', !on);
            dotPopup.style.background = on ? '#60a5fa' : 'transparent';
            dotPopup.style.borderColor = on ? '#60a5fa' : '#374151';
            icoPopup.style.color = on ? '#60a5fa' : '#6b7280';
        }

        function syncPopupKw() {
            const on = Cfg.popupKeywords;
            togPopupKw.classList.toggle('tbn-tc-off', !on);
            dotPopupKw.style.background = on ? '#60a5fa' : 'transparent';
            dotPopupKw.style.borderColor = on ? '#60a5fa' : '#374151';
            icoPopupKw.style.color = on ? '#60a5fa' : '#6b7280';
        }

        function syncMode() {
            const dark = Cfg.darkMode;
            btnDark.classList.toggle('tbn-mode-active', dark);
            btnLight.classList.toggle('tbn-mode-active', !dark);
            applyTheme(dark);
        }

        togPopup.addEventListener('click', () => {
            Cfg.save('popupMention', !Cfg.popupMention);
            syncPopup();
        });

        togPopupKw.addEventListener('click', () => {
            Cfg.save('popupKeywords', !Cfg.popupKeywords);
            syncPopupKw();
        });

        soundPick.addEventListener('click', () => { soundFile.click(); soundPick.blur(); });
        soundFile.addEventListener('change', () => {
            const file = soundFile.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = e => {
                // save base64 data-URL directly to GM storage → survives page reload
                Cfg.customSoundB64 = e.target.result;
                Store.set('v1_customSoundB64', e.target.result);
                // save filename so it persists after page reload
                Cfg.customSoundName = file.name;
                Store.set('v1_customSoundName', file.name);
                soundName.textContent = file.name;
                soundRemove.style.display = 'flex';
                soundPick.style.background = 'rgba(96,165,250,.28)';
                soundPick.style.borderColor = 'rgba(96,165,250,.5)';
                Sound.reload();
                Sound.play();
                syncNBButtons();
            };
            reader.readAsDataURL(file);
            soundFile.value = '';
            soundPick.blur();
        });

        const soundRemove = Q('#tbn-sound-remove');
        soundRemove.style.display = Cfg.customSoundB64 ? 'flex' : 'none';
        if (Cfg.customSoundB64) {
            soundPick.style.background = 'rgba(96,165,250,.28)';
            soundPick.style.borderColor = 'rgba(96,165,250,.5)';
            soundName.textContent = '(Custom sound loaded)';
        }
        soundRemove.addEventListener('click', () => {
            Cfg.customSoundB64 = '';
            Store.set('v1_customSoundB64', '');
            Cfg.customSoundName = '';
            Store.set('v1_customSoundName', '');
            soundName.textContent = 'No file chosen';
            soundFile.value = '';
            soundRemove.style.display = 'none';
            soundPick.style.background = '';
            soundPick.style.borderColor = '';
            Sound.reload();
            syncNBButtons();
        });

        // ── Notification Board popup ──
        const nbBtn = Q('#tbn-default-sound-btn');
        const nbPopup = Q('#tbn-nb-popup');
        const nbClose = Q('#tbn-nb-close');
        const defaultSoundName = Q('#tbn-default-sound-name');

        function closeNB() {
            nbPopup.classList.add('tbn-nb-closing');
            setTimeout(() => {
                nbPopup.style.display = 'none';
                nbPopup.classList.remove('tbn-nb-closing');
            }, 220);
        }

        nbBtn.addEventListener('click', () => {
            if (nbPopup.style.display === 'none') {
                nbPopup.classList.remove('tbn-nb-closing');
                nbPopup.style.display = 'block';
            } else {
                closeNB();
            }
        });
        nbClose.addEventListener('click', closeNB);

        function syncNBButtons() {
            Q('#tbn-nb-grid').querySelectorAll('.tbn-nb-btn').forEach(btn => {
                const isActive = btn.dataset.url === Cfg.defaultSoundUrl && !Cfg.customSoundB64;
                btn.classList.toggle('tbn-nb-active', isActive);
                btn.querySelector('svg').style.color = isActive ? '#60a5fa' : '';
                // Re-apply inline styles immediately so switching sounds updates colors right away
                if (Cfg.darkMode) {
                    btn.style.background = isActive ? 'rgba(96,165,250,.15)' : 'rgba(255,255,255,.04)';
                    btn.style.borderColor = isActive ? 'rgba(30,58,138,.75)' : 'rgba(255,255,255,.08)';
                    btn.style.color = isActive ? '#60a5fa' : '#d1d5db';
                } else {
                    btn.style.background = isActive ? 'rgba(96,165,250,.18)' : 'rgba(255,255,255,.8)';
                    btn.style.borderColor = isActive ? 'rgba(96,165,250,.5)' : 'rgba(30,58,138,.4)';
                    btn.style.color = isActive ? '#1d4ed8' : '#111827';
                }
            });
            const activeBtn = Q('#tbn-nb-grid .tbn-nb-active');
            defaultSoundName.textContent = activeBtn ? activeBtn.dataset.name : 'Minimalistic';

        }

        Q('#tbn-nb-grid').querySelectorAll('.tbn-nb-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const url = btn.dataset.url;
                // Save new default URL and clear old cached base64 so it gets re-fetched
                Cfg.defaultSoundUrl = url;
                Store.set('v1_defaultSound', url);
                Cfg.defaultSoundB64 = '';
                Store.set('v1_defaultSoundB64', '');
                // Clear custom sound
                Cfg.customSoundB64 = '';
                Store.set('v1_customSoundB64', '');
                soundFile.value = '';
                soundRemove.style.display = 'none';
                soundPick.style.background = '';
                soundPick.style.borderColor = '';
                soundName.textContent = 'No file chosen';
                Sound.previewUrl(url);
                syncNBButtons();
                closeNB();
            });
        });

        function syncPresets() { syncNBButtons(); }

        btnDark.addEventListener('click', () => { Cfg.save('darkMode', true); syncMode(); });
        btnLight.addEventListener('click', () => { Cfg.save('darkMode', false); syncMode(); });

        function syncAll() {
            const det = detectUser();
            if (det && !Cfg.username) Cfg.save('username', det);
            unameIn.value = Cfg.username;
            unameDot.className = Cfg.username ? 'tbn-dot-g' : 'tbn-dot-gr';
            setHlColor(Cfg.hlColor);
            setMentionColor(Cfg.mentionColor);
            syncHlRow();
            syncHlMention();
            setVol(Cfg.volume);
            kwTA.value = Cfg.keywords.join('\n');
            syncPopup();
            syncPopupKw();
            syncMode();
            soundName.textContent = Cfg.customSoundB64
                ? (Cfg.customSoundName || '(Custom sound loaded)')
                : 'No file chosen';
            if (soundRemove) soundRemove.style.display = Cfg.customSoundB64 ? 'flex' : 'none';
            if (Cfg.customSoundB64) {
                soundPick.style.background = 'rgba(96,165,250,.28)';
                soundPick.style.borderColor = 'rgba(96,165,250,.5)';
            } else {
                soundPick.style.background = '';
                soundPick.style.borderColor = '';
            }
            syncNBButtons();
        }

        return { syncAll };
    }

    /* ── Dark/Light theme apply ── */
    function applyTheme(dark) {
        const panel = document.getElementById('tbn-panel');
        if (!panel) return;

        if (dark) {
            // ── Dark mode — blue-tinted dark bg ──
            panel.style.background = 'rgba(10,14,30,.92)';
            panel.style.border = '2px solid rgba(96,165,250,.12)';
            panel.style.color = '#e5e7eb';
            panel.querySelectorAll('.tbn-label').forEach(el => el.style.color = '#6b7280');
            panel.querySelectorAll('.tbn-tc-txt').forEach(el => el.style.color = '#d1d5db');
            panel.querySelectorAll('.tbn-cp-name').forEach(el => el.style.color = '#e5e7eb');
            panel.querySelectorAll('.tbn-cp-hex').forEach(el => el.style.color = '#6b7280');
            panel.querySelectorAll('#tbn-hdr h2').forEach(el => el.style.color = '#fff');
            panel.querySelectorAll('#tbn-hdr p').forEach(el => el.style.color = '#6b7280');
            panel.querySelectorAll('#tbn-vol-pct').forEach(el => el.style.color = '#9ca3af');
            panel.querySelectorAll('#tbn-test-sound').forEach(el => { el.style.color = '#d1d5db'; el.style.background = 'rgba(255,255,255,.05)'; el.style.borderColor = 'rgba(255,255,255,.07)'; });
            panel.querySelectorAll('.tbn-tc').forEach(el => { el.style.background = 'rgba(96,165,250,.06)'; el.style.borderColor = 'rgba(96,165,250,.18)'; });
            panel.querySelectorAll('.tbn-cpick').forEach(el => { el.style.background = 'rgba(96,165,250,.04)'; el.style.borderColor = 'rgba(96,165,250,.12)'; });
            panel.querySelectorAll('.tbn-mode-btn').forEach(el => {
            const isActive = el.classList.contains('tbn-mode-active');
            el.style.color = isActive ? '#60a5fa' : '#6b7280';
            el.style.background = isActive ? 'rgba(96,165,250,.18)' : 'rgba(255,255,255,.05)';
            el.style.borderColor = isActive ? 'rgba(30,58,138,.75)' : 'rgba(255,255,255,.1)';
            });
            panel.querySelectorAll('#tbn-uname').forEach(el => { el.style.color = '#e5e7eb'; el.style.background = 'rgba(255,255,255,.05)'; el.style.borderColor = 'rgba(255,255,255,.1)'; });
            panel.querySelectorAll('#tbn-kw').forEach(el => { el.style.color = '#e5e7eb'; el.style.background = 'rgba(255,255,255,.04)'; el.style.borderColor = 'rgba(255,255,255,.1)'; });
            panel.querySelectorAll('#tbn-hdr').forEach(el => el.style.borderBottomColor = 'rgba(255,255,255,.05)');
            panel.querySelectorAll('#tbn-footer').forEach(el => { el.style.color = '#4b5563'; el.style.borderTopColor = 'rgba(255,255,255,.05)'; });
            panel.querySelectorAll('#tbn-vol-card').forEach(el => { el.style.background = 'rgba(255,255,255,.05)'; el.style.borderColor = 'rgba(255,255,255,.08)'; });
            panel.querySelectorAll('#tbn-kw-reset').forEach(el => { el.style.background = '#b91c1c'; el.style.color = '#fff'; });
            panel.querySelectorAll('.tbn-tc-ico').forEach(el => el.style.opacity = '1');
            // Notification Board popup — dark mode: restore dark bg
            panel.querySelectorAll('#tbn-nb-popup').forEach(el => {
                el.style.background = 'rgba(12,17,35,.96)';
                el.style.borderColor = 'rgba(30,58,138,.4)';
                el.style.boxShadow = '0 20px 60px rgba(0,0,0,.6)';
            });
            panel.querySelectorAll('#tbn-nb-title').forEach(el => el.style.color = '#6b7280');
            panel.querySelectorAll('#tbn-nb-close').forEach(el => { el.style.color = '#6b7280'; });
            panel.querySelectorAll('.tbn-nb-btn:not(.tbn-nb-active)').forEach(el => {
                el.style.background = 'rgba(255,255,255,.04)';
                el.style.borderColor = 'rgba(255,255,255,.08)';
                el.style.color = '#d1d5db';
            });
            panel.querySelectorAll('.tbn-nb-btn.tbn-nb-active').forEach(el => {
                el.style.background = 'rgba(96,165,250,.15)';
                el.style.borderColor = 'rgba(30,58,138,.75)';
                el.style.color = '#60a5fa';
            });
        } else {
            // ── Light mode — clean white, all borders visible ──
                        panel.style.background = 'rgba(235,242,255,.98)';
            panel.style.border = '1px solid rgba(30,58,138,.55)';
            panel.style.color = '#111827';
            panel.querySelectorAll('.tbn-label').forEach(el => el.style.color = '#374151');
            panel.querySelectorAll('.tbn-tc-txt').forEach(el => el.style.color = '#111827');
            panel.querySelectorAll('.tbn-cp-name').forEach(el => el.style.color = '#111827');
            panel.querySelectorAll('.tbn-cp-hex').forEach(el => el.style.color = '#374151');
            panel.querySelectorAll('#tbn-hdr h2').forEach(el => el.style.color = '#111827');
            panel.querySelectorAll('#tbn-hdr p').forEach(el => el.style.color = '#374151');
            panel.querySelectorAll('#tbn-vol-pct').forEach(el => el.style.color = '#374151');
            panel.querySelectorAll('#tbn-test-sound').forEach(el => { el.style.color = '#111827'; el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.85)'; });
            panel.querySelectorAll('.tbn-tc').forEach(el => { el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.85)'; });
            panel.querySelectorAll('.tbn-cpick').forEach(el => { el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.8)'; });
            panel.querySelectorAll('.tbn-mode-btn').forEach(el => { const isActive = el.classList.contains('tbn-mode-active'); el.style.color = isActive ? '#1d4ed8' : '#374151'; el.style.background = isActive ? 'rgba(96,165,250,.18)' : 'rgba(235,242,255,.9)'; el.style.borderColor = isActive ? 'rgba(30,58,138,.95)' : 'rgba(30,58,138,.85)'; });
            panel.querySelectorAll('#tbn-uname').forEach(el => { el.style.color = '#111827'; el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.85)'; });
            panel.querySelectorAll('#tbn-kw').forEach(el => { el.style.color = '#111827'; el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.85)'; });
            panel.querySelectorAll('#tbn-hdr').forEach(el => el.style.borderBottomColor = 'rgba(30,58,138,.5)');
            panel.querySelectorAll('#tbn-footer').forEach(el => { el.style.color = '#374151'; el.style.borderTopColor = 'rgba(30,58,138,.5)'; });
            panel.querySelectorAll('#tbn-vol-card').forEach(el => { el.style.background = 'rgba(235,242,255,.9)'; el.style.borderColor = 'rgba(30,58,138,.8)'; });
            panel.querySelectorAll('#tbn-kw-reset').forEach(el => { el.style.background = '#dc2626'; el.style.color = '#fff'; });
            panel.querySelectorAll('.tbn-tc-ico').forEach(el => el.style.opacity = '1');
            // Notification Board popup — light mode: white+blueish
            panel.querySelectorAll('#tbn-nb-popup').forEach(el => {
                el.style.background = 'rgba(235,242,255,.99)';
                el.style.borderColor = 'rgba(30,58,138,.85)';
                el.style.boxShadow = '0 20px 60px rgba(96,165,250,.15)';
            });
            panel.querySelectorAll('#tbn-nb-title').forEach(el => el.style.color = '#374151');
            panel.querySelectorAll('#tbn-nb-close').forEach(el => { el.style.color = '#374151'; });
            panel.querySelectorAll('.tbn-nb-btn:not(.tbn-nb-active)').forEach(el => {
                el.style.background = 'rgba(255,255,255,.8)';
                el.style.borderColor = 'rgba(30,58,138,.75)';
                el.style.color = '#111827';
            });
            panel.querySelectorAll('.tbn-nb-btn.tbn-nb-active').forEach(el => {
                el.style.background = 'rgba(96,165,250,.18)';
                el.style.borderColor = 'rgba(30,58,138,.9)';
                el.style.color = '#1d4ed8';
            });
        }
    }
    function ripple(el) {
        const r = document.createElement('span');
        r.className = 'tbn-ripple';
        el.appendChild(r);
        setTimeout(() => r.remove(), 550);
    }

    let _syncAll = null;

    function openModal() {
        const m = document.getElementById('tbn-modal');
        m.style.display = 'flex';
        requestAnimationFrame(() => requestAnimationFrame(() => m.classList.add('tbn-modal-in')));
        _syncAll?.();
    }

    function closeModal() {
        const m = document.getElementById('tbn-modal');
        if (!m) return;
        m.classList.remove('tbn-modal-in');
        m.classList.add('tbn-modal-out');
        setTimeout(() => { m.style.display = 'none'; m.classList.remove('tbn-modal-out'); }, 300);
    }

    function addTrigger() {
        const btn = document.createElement('button');
        btn.id = 'tbn-trigger';
        btn.title = 'Shoutbox Notifier Settings';
        btn.innerHTML = `<svg viewBox="0 0 24 24" fill="currentColor">
        <path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
    </svg>`;
        btn.addEventListener('click', e => { e.stopPropagation(); openModal(); });
        const anchor = document.querySelector('#shoutbox-container .content-title h6.left');
        if (anchor) {
            anchor.style.cssText += 'display:flex;align-items:center;gap:6px;';
            anchor.appendChild(btn);
        }
    }

    /* ═══════════════════════════════════════════════════════
       OBSERVER
    ═══════════════════════════════════════════════════════ */
    function startObserver() {
        const box = document.getElementById('shouts-container');
        if (!box) { setTimeout(startObserver, 500); return; }
        // Old shouts — apply highlight only, do not notify
        box.querySelectorAll('.shout-item').forEach(el => inspect(el, true));
        new MutationObserver(muts => {
            for (const m of muts)
                m.addedNodes.forEach(n => {
                    if (n.nodeType === 1 && n.classList.contains('shout-item')) inspect(n);
                });
        }).observe(box, { childList: true });
    }

    /* ═══════════════════════════════════════════════════════
       STYLES
    ═══════════════════════════════════════════════════════ */
    GM_addStyle(`
    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');

    /* ── username text mark (Highlight mention) ── */
    .tbn-mark {
        border-radius: 3px !important;
        padding: 0 2px !important;
        font-weight: 700 !important;
        font-style: normal !important;
        transition: color .3s, background .3s !important;
    }

    /* ── keyword text mark ── */
    .tbn-kw-mark {
        background: transparent !important;
        color: rgba(255, 255, 255, 0.9) !important;
        font-weight: 600 !important;
        font-style: normal !important;
        text-decoration: underline !important;
        text-decoration-color: rgba(255, 255, 255, 0.45) !important;
        text-underline-offset: 2px !important;
        border-radius: 0 !important;
        padding: 0 !important;
    }

    /* ── row highlight (Highlight Color) ── */
    .tbn-hl-row {
        border-left: 3px solid var(--tbn-c, #4ade80) !important;
        background: linear-gradient(90deg, color-mix(in srgb, var(--tbn-c,#4ade80) 10%, transparent) 0%, transparent 70%) !important;
        padding-left: 10px !important;
        border-radius: 4px !important;
        transition: background .4s ease, border-color .4s ease !important;
    }

    /* ── flash ── */
    @keyframes tbn-flash-kf { 0%{opacity:1}35%{opacity:.35}70%{opacity:1}100%{opacity:1} }
    .tbn-flash { animation: tbn-flash-kf .55s ease; }

    /* ── toast container ── */
    #tbn-toasts {
        position:fixed; bottom:24px; right:24px; z-index:2147483647;
        display:flex; flex-direction:column; gap:12px; pointer-events:none;
    }

    /* ── toast ── */
    .tbn-toast {
        pointer-events:auto;
        display:flex; align-items:center; gap:16px;
        padding:14px 16px; min-width:270px; max-width:340px;
        background:rgba(17,24,39,.94);
        backdrop-filter:blur(20px) saturate(1.3);
        border:1px solid rgba(255,255,255,.1); border-radius:20px;
        box-shadow:0 20px 60px rgba(0,0,0,.55);
        font-family:'Inter',-apple-system,sans-serif;
        opacity:0; transform:translateX(50px) scale(.9);
        transition:opacity .32s ease, transform .38s cubic-bezier(.34,1.4,.64,1);
    }
    .tbn-toast.tbn-t-in  { opacity:1; transform:translateX(0) scale(1); }
    .tbn-toast.tbn-t-out { opacity:0; transform:translateX(40px) scale(.9); transition:opacity .3s ease, transform .28s ease; }
    .tbn-t-icon { width:38px;height:38px;border-radius:12px;flex-shrink:0;display:flex;align-items:center;justify-content:center; }
    .tbn-t-icon svg { width:19px;height:19px;display:block; }
    .tbn-toast-mention .tbn-t-icon { background:rgba(251,146,60,.12); color:#fb923c; }
    .tbn-toast-keyword .tbn-t-icon { background:rgba(96,165,250,.12);  color:#60a5fa; }
    .tbn-t-body { flex:1; }
    .tbn-t-title { font-size:13.5px;font-weight:700;color:#fff;letter-spacing:-.01em; }
    .tbn-t-msg   { font-size:11.5px;color:#6b7280;margin-top:2px; }
    .tbn-t-close { background:none;border:none;cursor:pointer;color:#374151;padding:4px;border-radius:8px;display:flex;align-items:center;transition:color .15s,background .15s; }
    .tbn-t-close:hover { color:#9ca3af;background:rgba(255,255,255,.08); }
    .tbn-t-close svg { width:15px;height:15px;display:block; }

    /* ── trigger ── */
    #tbn-trigger {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 26px;
        height: 26px;
        padding: 0;
        background: transparent;
        border: 1px solid transparent;
        border-radius: 10px;
        cursor: pointer;
        color: #6b7280;
        transition: color .18s, background .22s, border-color .22s, transform .8s cubic-bezier(.34,1.56,.64,1);
    }
    #tbn-trigger:hover {
        color: #ffffff;
        background: rgba(255, 255, 255, 0.12);
        border-color: rgba(255, 255, 255, 0.08);
        transform: rotate(69deg);
    }
    #tbn-trigger svg {
        width: 20px;
        height: 20px;
        display: block;
    }

    /* ── modal overlay ── */
    #tbn-modal {
        position:fixed;inset:0;z-index:2147483646;
        display:flex;align-items:center;justify-content:center;padding:16px;
        font-family:'Inter',-apple-system,sans-serif;
        -webkit-font-smoothing:antialiased;
    }
    #tbn-backdrop {
        position:absolute;inset:0;
        background:rgba(0,0,0,.76);backdrop-filter:blur(6px);
        opacity:0;transition:opacity .28s ease;
    }
    #tbn-modal.tbn-modal-in  #tbn-backdrop { opacity:1; }
    #tbn-modal.tbn-modal-out #tbn-backdrop { opacity:0; }

    /* ── panel ── */
    #tbn-panel {
        position:relative;z-index:1;
        width:100%;max-width:520px;max-height:578px;overflow-y:auto;
        background:rgba(15,18,28,.88);
        backdrop-filter:blur(20px) saturate(1.3);
        border:1px solid rgba(255,255,255,.07);border-radius:28px;
        box-shadow:0 32px 80px rgba(0,0,0,.7), 0 1px 0 rgba(255,255,255,.05) inset;
        scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.1) transparent;
        color:#e5e7eb;
        opacity:0;transform:translateY(22px) scale(.97);
        transition:opacity .3s ease, transform .36s cubic-bezier(.22,1,.36,1);
    }
    #tbn-panel::-webkit-scrollbar { width:6px; }
    #tbn-panel::-webkit-scrollbar-thumb { background:rgba(255,255,255,.1);border-radius:999px; }
    #tbn-modal.tbn-modal-in  #tbn-panel { opacity:1;transform:translateY(0) scale(1); }
    #tbn-modal.tbn-modal-out #tbn-panel { opacity:0;transform:translateY(10px) scale(.97);transition:opacity .25s ease,transform .25s ease; }

    /* header */
    #tbn-hdr { display:flex;align-items:center;justify-content:space-between;padding:22px 22px 18px;border-bottom:1px solid rgba(255,255,255,.05); }
    #tbn-hdr-l { display:flex;align-items:center;gap:13px; }
    #tbn-hdr-icon {
        width:42px;height:42px;border-radius:13px;flex-shrink:0;
        background:transparent;border:none;
        display:flex;align-items:center;justify-content:center;color:#60a5fa;
        cursor:default;
    }
    #tbn-hdr-icon:hover {
        transform:none;
        box-shadow:none;
    }
     @keyframes tbn-spin-slow { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
       #tbn-hdr-icon svg {
    width:45px;height:45px;display:block;
    animation:tbn-spin-slow 10s linear infinite;
    animation-play-state:running;
    transition:animation-play-state 0s;
    }
      #tbn-hdr-icon:hover svg {
    animation-play-state:paused;
    }
    #tbn-hdr h2 { margin:0;font-size:17px;font-weight:700;color:#fff;letter-spacing:-.025em; }
    #tbn-hdr p  { margin:3px 0 0;font-size:12px;color:#6b7280; }
    #tbn-close {
        width:30px;height:30px;border-radius:50%;background:rgba(255,255,255,.07);border:none;
        cursor:pointer;display:flex;align-items:center;justify-content:center;color:#6b7280;
        transition:background .15s,color .15s;flex-shrink:0;
    }
    #tbn-close:hover { background:rgba(255,255,255,.13);color:#e5e7eb; }
    #tbn-close svg { width:15px;height:15px;display:block; }

    /* body */
    #tbn-body { padding:22px;display:flex;flex-direction:column;gap:22px; }

    /* field group */
    .tbn-fg { display:flex;flex-direction:column;gap:10px; }
    .tbn-label { display:flex;align-items:center;gap:7px;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.08em; }
    .tbn-label svg { width:13px;height:13px;display:block; }

    /* username input */
    .tbn-iw {
        position:relative;
        transition:transform .2s ease, box-shadow .2s ease;
    }
    .tbn-iw:hover {
        transform:translateY(-2px);
        box-shadow:0 6px 20px rgba(0,0,0,.3);
        border-radius:13px;
    }
    #tbn-uname {
        width:100%;padding:11px 38px 11px 15px;
        background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:13px;
        color:#e5e7eb;font-size:14px;font-family:'JetBrains Mono',monospace;
        outline:none;box-sizing:border-box;
        transition:border-color .18s,box-shadow .18s;
    }
    #tbn-uname:focus { border-color:rgba(96,165,250,.5);box-shadow:0 0 0 3px rgba(96,165,250,.1); }
    #tbn-uname::placeholder { color:#374151; }
    #tbn-uname-dot { position:absolute;right:13px;top:50%;transform:translateY(-50%);width:8px;height:8px;border-radius:50%; }
    .tbn-dot-g  { background:#4ade80;box-shadow:0 0 8px #4ade8099; }
    .tbn-dot-gr { background:#374151; }

    /* theme grid */
    #tbn-theme-grid { display:grid;grid-template-columns:1fr 1fr;gap:10px; }

    /* toggle card — same bg always, only border/icon/dot change on ON/OFF */
   .tbn-tc {
    display:flex;align-items:center;justify-content:space-between;
    padding:14px 14px;
    background:rgba(255,255,255,.05);
    border:1px solid rgba(255,255,255,.1);
    border-radius:16px;
    cursor:pointer;font-family:'Inter',sans-serif;
    transition:border-color .22s, opacity .18s, transform .2s ease, box-shadow .2s ease;
    text-align:left;
    }
    .tbn-tc:hover {
    transform:translateY(-2px);
    box-shadow:0 6px 20px rgba(0,0,0,.3);
    }

    .tbn-tc:focus,
    .tbn-tc:active {
    outline:none;
    box-shadow:none;
    background:rgba(255,255,255,.05) !important;
    }

    .tbn-tc:focus-visible {
    outline:none;
    }
    /* ON state — slightly brighter border, no colour fill */
    .tbn-tc:not(.tbn-tc-off) {
        border-color:rgba(255,255,255,.2);
        opacity:1;
        background:rgba(255,255,255,.05);
    }
    .tbn-tc-l { display:flex;align-items:center;gap:9px; }
    .tbn-tc-ico { width:17px;height:17px;display:flex;align-items:center;justify-content:center;transition:color .2s;flex-shrink:0; }
    .tbn-tc-ico svg { width:17px;height:17px;display:block; }
    .tbn-tc-txt { font-size:13px;font-weight:600;color:#d1d5db; }
    .tbn-dot {
        width:14px;height:14px;border-radius:50%;flex-shrink:0;
        border:2px solid #374151;
        transition:background .2s,border-color .2s,box-shadow .2s;
    }

    /* colour picker card */
    .tbn-cpick {
        display:flex;align-items:center;justify-content:space-between;gap:12px;
        padding:14px 15px;
        background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:16px;
        transition:transform .2s ease, box-shadow .2s ease;
        position:relative;
    }
    .tbn-cpick:hover {
        transform:translateY(-2px);
        box-shadow:0 6px 20px rgba(0,0,0,.3);
    }
    .tbn-cp-l { display:flex;align-items:center;gap:11px;flex:1;min-width:0; }
    .tbn-swatch {
        width:48px;height:48px;border-radius:14px;flex-shrink:0;
        border:2px solid rgba(255,255,255,.1);cursor:pointer;
        transition:transform .2s ease,box-shadow .2s ease;
    }
    .tbn-swatch:hover { transform:translateY(-2px) scale(1.05); box-shadow:0 6px 18px rgba(0,0,0,.3); }
    /* colour picker popup — appears on the left side of the screen */
    input[type=color] {
        position:fixed;
        left:0;
        top:50%;
        width:1px; height:1px;
        opacity:0; border:none; padding:0; cursor:pointer;
        z-index:2147483647;
    }
    .tbn-cp-name { display:block;font-size:13.5px;font-weight:700;color:#e5e7eb; }
    .tbn-cp-hex  { display:block;font-size:11px;color:#6b7280;font-family:'JetBrains Mono',monospace;margin-top:2px; }
    .tbn-presets { display:flex;gap:7px;flex-shrink:0;align-items:center; }
    .tbn-pre {
        width:24px;height:24px;border-radius:50%;
        border:2px solid rgba(255,255,255,.2);cursor:pointer;
        transition:transform .2s ease, box-shadow .2s ease;
    }
    .tbn-pre:hover { transform:translateY(-2px) scale(1.25); box-shadow:0 4px 12px rgba(0,0,0,.3); }

    /* keywords */
    .tbn-kw-hdr { display:flex;align-items:center;justify-content:space-between; }
    #tbn-kw-reset {
        font-size:12px;font-weight:700;color:#fff;
        background:#b91c1c;border:none;border-radius:8px;
        padding:5px 12px;cursor:pointer;font-family:'Inter',sans-serif;
        transition:background .15s, transform .2s ease, box-shadow .2s ease;
    }
    #tbn-kw-reset:hover { background:#dc2626; transform:translateY(-2px); box-shadow:0 4px 14px rgba(185,28,28,.4); }
    #tbn-kw {
    width:100%;resize:vertical;min-height:96px;
    padding:13px 15px;
    background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:13px;
    color:#e5e7eb;font-size:13px;font-family:'JetBrains Mono',monospace;
    outline:none;box-sizing:border-box;line-height:1.65;
    transition:border-color .18s, transform .2s ease, box-shadow .2s ease;
    }
    #tbn-kw:hover {
    transform:translateY(-2px);
    box-shadow:0 6px 20px rgba(0,0,0,.3);
    }
    #tbn-kw:focus { border-color:rgba(96,165,250,.4);box-shadow:0 0 0 3px rgba(96,165,250,.08); }
    #tbn-kw::placeholder { color:#374151; }

    /* volume */
    #tbn-vol-card {
        padding:20px;
        background:rgba(255,255,255,.05);
        border:1px solid rgba(255,255,255,.08);
        border-radius:18px;
        display:flex;flex-direction:column;gap:18px;
    }
    #tbn-vol-row { display:flex;align-items:center;gap:16px; }
    .tbn-vol-svg {
        display:flex; align-items:center; justify-content:center;
        width:20px; height:20px; flex-shrink:0; color:#60a5fa;
        transition:color .2s ease;
    }
    .tbn-vol-svg svg { display:block; }

    /* track wrapper */
    #tbn-vol-wrap {
        flex:1; height:8px;
        background:rgba(255,255,255,.08);
        border-radius:9999px;
        position:relative;
    }

    /* blue filled portion — smooth transition */
    #tbn-vol-fill {
        position:absolute; left:0; top:0; height:100%;
        background:linear-gradient(90deg, #2563eb, #3b82f6, #60a5fa);
        border-radius:9999px;
        pointer-events:none;
        transition:width 40ms ease-out;
    }

    /* transparent range input — sits on top, captures drag */
    #tbn-vol-slider {
        -webkit-appearance:none; appearance:none;
        position:absolute; inset:0;
        width:100%; height:100%;
        opacity:0; cursor:pointer;
        z-index:20; margin:0; padding:0;
    }

    /* visual capsule thumb — smooth movement */
    #tbn-vol-thumb {
        position:absolute;
        top:50%; transform:translateY(-50%);
        width:28px; height:14px;
        background:#ffffff;
        border-radius:9999px;
        box-shadow:0 0 0 2px rgba(59,130,246,.35), 0 2px 8px rgba(0,0,0,.4), 0 0 14px rgba(255,255,255,.18);
        pointer-events:none;
        z-index:10;
        transition:left 40ms ease-out, box-shadow .2s ease;
    }
    /* glow on drag — we can't detect :active on the invisible input,
       so we keep a nice resting glow always */

    #tbn-vol-pct {
        font-size:12px; font-weight:700;
        color:#9ca3af;
        font-family:'JetBrains Mono',monospace;
        min-width:36px; text-align:right;
    }

    /* Test Sound */
    #tbn-test-sound {
        width:100%; padding:12px;
        background:rgba(255,255,255,.05);
        border:1px solid rgba(255,255,255,.07);
        border-radius:13px;
        color:#d1d5db; font-size:13px; font-weight:700;
        display:flex; align-items:center; justify-content:center; gap:8px;
        cursor:pointer; font-family:'Inter',sans-serif;
        transition:background .18s, border-color .18s, color .15s, transform .2s ease, box-shadow .2s ease;
        position:relative; overflow:hidden;
    }
    #tbn-test-sound:hover  { background:rgba(255,255,255,.1); border-color:rgba(255,255,255,.14); color:#fff; transform:translateY(-2px); box-shadow:0 6px 20px rgba(0,0,0,.3); }
    #tbn-test-sound:active { transform:scale(.98); }
    #tbn-test-sound svg { width:13px;height:13px;display:block;fill:currentColor; }

    /* footer */
    #tbn-footer { text-align:center;padding:16px 22px 20px;border-top:1px solid rgba(255,255,255,.05);font-size:13px;color:#94a3b8; }
    #tbn-footer strong { color:#60a5fa;font-weight:700; }

    /* ── Extras ── */
    /* Sound card */
    .tbn-sound-card { cursor:default; }
    .tbn-sound-icon {
        width:42px;height:42px;border-radius:12px;flex-shrink:0;
        background:rgba(96,165,250,.1);border:1px solid rgba(96,165,250,.15);
        display:flex;align-items:center;justify-content:center;color:#60a5fa;
    }
    .tbn-sound-icon svg { width:22px;height:22px;display:block; }
    .tbn-sound-btn {
        padding:8px 16px;
        background:rgba(96,165,250,.15);border:1px solid rgba(96,165,250,.25);
        border-radius:10px;color:#60a5fa;font-size:12px;font-weight:700;
        cursor:pointer;font-family:'Inter',sans-serif;white-space:nowrap;
        outline:none;
        transition:background .15s,transform .2s ease,box-shadow .2s ease;
    }
    .tbn-sound-btn:focus { outline:none; box-shadow:none; }
    .tbn-sound-btn:hover { background:rgba(96,165,250,.25);transform:translateY(-2px);box-shadow:0 4px 14px rgba(96,165,250,.2); }
    .tbn-sound-remove {
        width:28px;height:28px;border-radius:8px;
        background:rgba(239,68,68,.15);border:1px solid rgba(239,68,68,.25);
        color:#f87171;font-size:13px;font-weight:700;
        cursor:pointer;display:none;align-items:center;justify-content:center;
        outline:none;
        transition:background .15s,transform .2s ease,box-shadow .2s ease;
    }
    .tbn-sound-remove:hover { background:rgba(239,68,68,.3);transform:translateY(-2px);box-shadow:0 4px 12px rgba(239,68,68,.2); }

    /* Default sound presets */
    #tbn-sound-presets {
        display:flex; flex-direction:column; gap:6px;
    }
    .tbn-sound-preset {
        width:100%; padding:9px 13px;
        background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08);
        border-radius:10px;
        color:#d1d5db; font-size:13px; font-weight:500; text-align:left;
        cursor:pointer; font-family:'Inter',sans-serif;
        transition:background .15s, border-color .15s, transform .2s ease, box-shadow .2s ease;
        outline:none;
    }
    .tbn-sound-preset:hover { background:rgba(96,165,250,.1); border-color:rgba(96,165,250,.25); transform:translateY(-1px); box-shadow:0 4px 12px rgba(0,0,0,.2); }
    .tbn-sound-preset.tbn-preset-active { background:rgba(96,165,250,.18); border-color:rgba(96,165,250,.4); color:#60a5fa; font-weight:700; }

    /* ── Notification Board popup ── */
    #tbn-nb-popup {
        position:absolute; left:0; right:0; top:calc(100% + 8px);
        background:rgba(12,17,35,.96);
        backdrop-filter:blur(20px) saturate(1.3);
        border:1px solid rgba(96,165,250,.2);
        border-radius:16px;
        padding:16px;
        z-index:999;
        box-shadow:0 20px 60px rgba(0,0,0,.6);
        animation:tbn-nb-in .28s cubic-bezier(.22,1,.36,1);
    }
    @keyframes tbn-nb-in { from { opacity:0; transform:translateY(-10px) scale(.97); } to { opacity:1; transform:translateY(0) scale(1); } }
    @keyframes tbn-nb-out { from { opacity:1; transform:translateY(0) scale(1); } to { opacity:0; transform:translateY(-8px) scale(.97); } }
    #tbn-nb-popup.tbn-nb-closing { animation:tbn-nb-out .22s ease forwards; }
    #tbn-nb-header {
        display:flex; align-items:center; justify-content:space-between;
        margin-bottom:14px;
    }
    #tbn-nb-title {
        font-size:11px; font-weight:700; color:#6b7280;
        text-transform:uppercase; letter-spacing:.08em;
    }
    #tbn-nb-close {
        background:none; border:none; cursor:pointer;
        color:#6b7280; font-size:14px; padding:2px 6px;
        border-radius:6px; outline:none;
        transition:color .15s, background .15s;
    }
    #tbn-nb-close:hover { color:#e5e7eb; background:rgba(255,255,255,.08); }
    #tbn-nb-grid {
        display:grid; grid-template-columns:1fr 1fr; gap:8px;
    }
    .tbn-nb-btn {
        display:flex; align-items:center; gap:8px;
        padding:11px 13px;
        background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08);
        border-radius:12px;
        color:#d1d5db; font-size:13px; font-weight:500; text-align:left;
        cursor:pointer; font-family:'Inter',sans-serif;
        transition:background .15s, border-color .15s, transform .18s ease, box-shadow .18s ease;
        outline:none;
    }
    .tbn-nb-btn svg { width:13px; height:13px; display:block; flex-shrink:0; color:#6b7280; transition:color .15s; }
    .tbn-nb-btn:hover { background:rgba(96,165,250,.1); border-color:rgba(96,165,250,.25); transform:translateY(-2px); box-shadow:0 4px 14px rgba(0,0,0,.3); }
    .tbn-nb-btn.tbn-nb-active { background:rgba(96,165,250,.15); border-color:rgba(96,165,250,.4); color:#60a5fa; font-weight:700; }
    .tbn-nb-btn.tbn-nb-active svg { color:#60a5fa; }

    /* Dark/Light mode grid */
    #tbn-mode-grid { display:grid;grid-template-columns:1fr 1fr;gap:10px; }
    .tbn-mode-btn {
        display:flex;align-items:center;justify-content:center;gap:8px;
        padding:12px;border-radius:14px;
        background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);
        color:#6b7280;font-size:13px;font-weight:600;
        cursor:pointer;font-family:'Inter',sans-serif;
        transition:background .18s,border-color .18s,color .18s,transform .2s ease,box-shadow .2s ease;
    }
    .tbn-mode-btn svg { width:16px;height:16px;display:block; }
    .tbn-mode-btn:hover { transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.3); }
    .tbn-mode-btn.tbn-mode-active {
        background:rgba(96,165,250,.18);border-color:rgba(96,165,250,.4);
        color:#60a5fa;
    }

    /* ripple */
    .tbn-ripple {
        position:absolute;width:8px;height:8px;border-radius:50%;
        background:rgba(255,255,255,.18);
        top:50%;left:50%;transform:translate(-50%,-50%) scale(0);
        animation:tbn-ripple-kf .52s ease-out forwards;pointer-events:none;
    }
    @keyframes tbn-ripple-kf { to { transform:translate(-50%,-50%) scale(32);opacity:0; } }
    `);

    /* ═══════════════════════════════════════════════════════
       BOOT
    ═══════════════════════════════════════════════════════ */
    window.addEventListener('load', () => {
        BrowserNotify.requestPermission();
        Sound.preload();
        const det = detectUser();
        if (det && !Cfg.username) Cfg.save('username', det);

        const modal = buildModal();
        const { syncAll } = wireModal(modal);
        _syncAll = syncAll;

        applyTheme(Cfg.darkMode);
        addTrigger();
        startObserver();
    });

})();