TBD Shoutbox Notifier

TorrentBD shoutbox @mention detection automatically detects your username and keywords, highlights your username and messages with a row border, and plays a sound alert with a premium UI.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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

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

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

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

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

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

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         TBD Shoutbox Notifier
// @version      1.0
// @description  TorrentBD shoutbox @mention detection automatically detects your username and keywords, highlights your username and messages with a row border, and plays a sound alert with a premium UI.
// @author       Anik
// @namespace    Anik
// @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
// @license      MIT
// @run-at       document-end
// @namespace    https://greasyfork.org/users/1597783
// ==/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',[]),
        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
    ═══════════════════════════════════════════════════════ */
    const Sound = {
        _url: 'https://github.com/aonexyz/TBD-SN-custom-sounds/raw/refs/heads/main/new-notification-010-352755.mp3',
        _audio: null,

        preload() {
            try {
                this._audio = new Audio(this._url);
                this._audio.load();
            } catch (_) {}
        },

        play() {
            if (Cfg.volume < 0.01) return;
            try {
                const a = this._audio || new Audio(this._url);
                a.volume = Cfg.volume;
                a.currentTime = 0;
                a.play().catch(() => {});
            } catch (_) {}
        },
    };

    /* ═══════════════════════════════════════════════════════
       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) {
            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) {
        if (!document.title.startsWith('(!) ')) document.title = '(!) ' + document.title;
        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 — 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, '\\$&');

        // শুধু message body চেক — rowText এ sender নাম থাকে, false positive হয়
        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 && !silent) { notify('mention'); Seen.mark(el.id); }
            if (!done) 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 && !silent) { notify('keyword'); Seen.mark(el.id); }
                if (!done) 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>𝖢𝗈𝗇𝖿𝗂𝗀𝗎𝗋𝖾 𝗒𝗈𝗎𝗋 𝖺𝗅𝖾𝗋𝗍𝗌 𝖺𝗇𝖽 𝗍𝗋𝗂𝗀𝗀𝖾𝗋𝗌</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>

                <!-- 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>

                <!-- 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 Volume
                    </label>
                    <div id="tbn-vol-card">
                        <div id="tbn-vol-row">
                            <span id="tbn-vol-ico" class="tbn-vol-svg"></span>
                            <!-- slider wrapper: relative container -->
                            <div id="tbn-vol-wrap">
                                <!-- blue fill bar -->
                                <div id="tbn-vol-fill"></div>
                                <!-- transparent range input on top (z-index:20) -->
                                <input type="range" id="tbn-vol-slider" min="0" max="1" step="0.001">
                                <!-- visual capsule thumb (z-index:10, pointer-events:none) -->
                                <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>

            </div>

        <div id="tbn-footer">
          Developed by
         <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);
        }

        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); // Cfg.volume is already 0.0–1.0
            kwTA.value = Cfg.keywords.join('\n');
        }

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

        return { syncAll };
    }

    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; }
        // পুরানো shouts — highlight করব কিন্তু 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:rgba(96,165,250,.1);border:1px solid rgba(96,165,250,.15);
        display:flex;align-items:center;justify-content:center;color:#60a5fa;
        cursor:default;
        transition:transform .2s ease, box-shadow .2s ease;
    }
    #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;
    }
    /* OFF state — dim + subtle border */
    .tbn-tc:not(.tbn-tc-off) {
    border-color:rgba(255,255,255,.2);
    opacity:1;
    background:rgba(255,255,255,.05);
    }
    /* ON state — slightly brighter border, no colour fill */
    .tbn-tc:not(.tbn-tc-off) {
        border-color:rgba(255,255,255,.2);
        opacity:1;
    }
    .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 near the swatch card */
    input[type=color] {
        position:absolute;
        left:0;
        top:0;
        margin-top:0;
        width:1px; height:1px;
        opacity:0; border:none; padding:0; cursor:pointer;
        z-index:10;
    }
    .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;
    }
    #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:#4b5563; }
    #tbn-footer strong { color:#9ca3af;font-weight:700; }

    /* 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;

        addTrigger();
        startObserver();
    });

})();