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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();