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