Smart Chat (Bonk.io)

Timestamps, mention highlighting, anti-spam, filters, inline image previews (click-to-zoom), revealable hidden messages, Bonk Mod Settings Core UI, and Discord-like :emoji_name: input replacement + picker (grid, categories, recents, pinned).

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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

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

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

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

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

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

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Smart Chat (Bonk.io)
// @namespace    https://greasyfork.org/users/1552147-ansonii-crypto
// @version      0.0.8
// @description  Timestamps, mention highlighting, anti-spam, filters, inline image previews (click-to-zoom), revealable hidden messages, Bonk Mod Settings Core UI, and Discord-like :emoji_name: input replacement + picker (grid, categories, recents, pinned).
// @match        https://bonk.io/gameframe-release.html
// @run-at       document-end
// @grant        none
// @license      N/A
// ==/UserScript==

(() => {
    'use strict';

    const STORAGE_KEY_PREFIX_V2 = 'bonk_smartchat_config_v2_';
    const STORAGE_KEY_PREFIX_V1 = 'bonk_smartchat_config_v1_';

    const DEFAULT_CONFIG = {
        enabled: true,
        showTimestamps: true,
        timestamp24h: true,
        highlightMentions: true,
        mentionKeywords: [],
        antiSpamEnabled: true,
        antiSpamDelayMs: 900,
        hideSystemMessages: false,
        wordBlacklist: [],
        mutedNames: [],
        highlightColor: '#00c8ff',

        emojiEnabled: true,
        emojiPickerEnabled: true,
        emojiReplaceOnType: true,

        emojiGridMode: false,
        emojiShowCategories: true,
        emojiRecentEnabled: true
    };

    function normalize(str) {
        return String(str || '').toLowerCase();
    }

    function normalizeEmojiName(name) {
        return String(name || '')
            .toLowerCase()
            .replace(/_/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();
    }

    const EMOJI_SHORTCODES = (() => {
        const entries = [
            ['grinning', '😀'], ['smiley', '😃'], ['smile', '😄'], ['grin', '😁'],
            ['laughing', '😆'], ['sweat smile', '😅'], ['joy', '😂'], ['rofl', '🤣'],
            ['relaxed', '☺️'], ['blush', '😊'], ['innocent', '😇'], ['wink', '😉'],
            ['slight smile', '🙂'], ['upside down', '🙃'], ['heart eyes', '😍'],
            ['kissing heart', '😘'], ['kissing', '😗'], ['kissing smile', '😙'],
            ['kissing closed eyes', '😚'], ['yum', '😋'], ['stuck out tongue', '😛'],
            ['stuck out tongue wink', '😜'], ['stuck out tongue closed eyes', '😝'],
            ['sunglasses', '😎'], ['thinking', '🤔'], ['neutral face', '😐'],
            ['expressionless', '😑'], ['no mouth', '😶'], ['smirk', '😏'],
            ['unamused', '😒'], ['roll eyes', '🙄'], ['grimacing', '😬'],
            ['lying face', '🤥'], ['relieved', '😌'], ['pensive', '😔'],
            ['sleepy', '😪'], ['drooling face', '🤤'], ['sleeping', '😴'],
            ['mask', '😷'], ['facepalm', '🤦'], ['shrug', '🤷'],
            ['disappointed', '😞'], ['worried', '😟'], ['cry', '😢'],
            ['sob', '😭'], ['angry', '😠'], ['rage', '😡'], ['triumph', '😤'],
            ['confounded', '😖'], ['persevere', '😣'], ['fearful', '😨'],
            ['cold sweat', '😰'], ['scream', '😱'], ['flushed', '😳'],
            ['zany face', '🤪'], ['dizzy face', '😵'], ['exploding head', '🤯'],
            ['poop', '💩'], ['clown', '🤡'], ['skull', '💀'], ['ghost', '👻'],
            ['robot', '🤖'], ['alien', '👽'],
            ['thumbs up', '👍'], ['thumbsup', '👍'], ['thumbs down', '👎'], ['thumbsdown', '👎'],
            ['ok hand', '👌'], ['clap', '👏'], ['pray', '🙏'], ['muscle', '💪'],
            ['wave', '👋'], ['v', '✌️'], ['raised hands', '🙌'], ['point right', '👉'],
            ['point left', '👈'], ['point up', '☝️'], ['point down', '👇'],
            ['heart', '❤️'], ['blue heart', '💙'], ['green heart', '💚'],
            ['yellow heart', '💛'], ['purple heart', '💜'], ['black heart', '🖤'],
            ['broken heart', '💔'], ['sparkling heart', '💖'], ['two hearts', '💕'],
            ['fire', '🔥'], ['sparkles', '✨'], ['star', '⭐'], ['boom', '💥'],
            ['100', '💯'], ['check', '✅'], ['x', '❌'], ['warning', '⚠️'],
            ['question', '❓'], ['exclamation', '❗'], ['rocket', '🚀'],
            ['eyes', '👀'], ['sweat', '💦'], ['zzz', '💤'], ['crown', '👑'],
            ['tada', '🎉'], ['confetti ball', '🎊'], ['gift', '🎁'],
            ['skull and crossbones', '☠️'],
            ['cat', '🐱'], ['dog', '🐶'], ['mouse', '🐭'], ['panda face', '🐼'],
            ['monkey', '🐵'], ['frog', '🐸'], ['unicorn', '🦄'],
            ['pizza', '🍕'], ['burger', '🍔'], ['fries', '🍟'],
            ['coffee', '☕'], ['tea', '🍵'], ['cake', '🍰'],
            ['weary', '😩'], ['tired face', '😫'], ['astonished', '😲'],
            ['frowning', '😦'], ['frowning face', '☹️'],
            ['open mouth', '😮'], ['hushed', '😯'], ['zipper mouth', '🤐'],
            ['nauseated face', '🤢'], ['vomiting face', '🤮'],
            ['sneezing face', '🤧'], ['money mouth', '🤑'],
            ['nerd', '🤓'], ['cowboy', '🤠'], ['pleading face', '🥺'],
            ['raised eyebrow', '🤨']
        ];

        const map = Object.create(null);
        for (const [name, emoji] of entries) map[normalizeEmojiName(name)] = emoji;
        return map;
    })();

    const EMOJI_ALL = Object.keys(EMOJI_SHORTCODES).map(name => ({
        name,
        emoji: EMOJI_SHORTCODES[name]
    }));

    const EMOJI_CATEGORIES = [
        { id: 'recent', label: '⏱️ Recent' },
        { id: 'smileys', label: '🙂 Smileys' },
        { id: 'hearts', label: '❤️ Hearts' },
        { id: 'hands', label: '✋ Hands' },
        { id: 'symbols', label: '✅ Symbols' },
        { id: 'food', label: '🍕 Food' },
        { id: 'animals', label: '🐱 Animals' }
    ];

    function categorizeEmoji(nameKey, emoji) {
        const n = nameKey;

        if (emoji === '❤️' || n.includes('heart')) return 'hearts';

        if (
            n.includes('thumb') || n.includes('clap') || n.includes('pray') || n.includes('muscle') ||
            n.includes('wave') || n === 'v' || n.includes('raised hands') || n.includes('point')
        ) return 'hands';

        if (
            n.includes('check') || n === 'x' || n.includes('warning') || n.includes('question') ||
            n.includes('exclamation') || n.includes('100') || n.includes('boom') || n.includes('sparkles') ||
            n.includes('star') || n.includes('rocket')
        ) return 'symbols';

        if (n.includes('pizza') || n.includes('burger') || n.includes('fries') || n.includes('coffee') || n.includes('tea') || n.includes('cake'))
            return 'food';

        if (n.includes('cat') || n.includes('dog') || n.includes('mouse') || n.includes('panda') || n.includes('monkey') || n.includes('frog') || n.includes('unicorn'))
            return 'animals';

        return 'smileys';
    }

    let storageKey = null;
    let config = { ...DEFAULT_CONFIG };

    let lastSendTime = 0;
    let myName = null;

    let imgPreviewBackdrop = null;
    let imgPreviewImg = null;
    let imgPreviewCaption = null;

    const emojiUiState = new WeakMap();

    let smartChatSettingsContainer = null;
    let smartChatRenderSettings = null;

    function isLoggedInAccount() {
        const lvlEl = document.getElementById('pretty_top_level');
        if (!lvlEl) return false;

        const lvlText = (lvlEl.textContent || '').trim().toLowerCase();
        if (!lvlText || lvlText === 'guest') return false;

        const nameEl = document.getElementById('pretty_top_name');
        const name = (nameEl ? nameEl.textContent : '').trim();
        return !!name;
    }

    function getAccountNameFromPrettyTopOrNull() {
        if (!isLoggedInAccount()) return null;
        const el = document.getElementById('pretty_top_name');
        const name = (el ? el.textContent : '').trim();
        if (!name) return null;
        return name.toLowerCase();
    }

    function getStorageKeyV2() {
        const acct = getAccountNameFromPrettyTopOrNull();
        if (!acct) return null;
        return STORAGE_KEY_PREFIX_V2 + acct;
    }

    function guessOldV1StorageKeys() {
        const keys = new Set();

        const prettyName = document.querySelector('#pretty_top_name');
        if (prettyName && prettyName.textContent.trim()) {
            keys.add(STORAGE_KEY_PREFIX_V1 + encodeURIComponent(prettyName.textContent.trim()));
        }

        const nameInput = document.querySelector('#newbonklobby_name_input');
        if (nameInput && nameInput.value && nameInput.value.trim()) {
            keys.add(STORAGE_KEY_PREFIX_V1 + encodeURIComponent(nameInput.value.trim()));
        }

        const stored = localStorage.getItem('bonk_name');
        if (stored && stored.trim()) keys.add(STORAGE_KEY_PREFIX_V1 + encodeURIComponent(stored.trim()));

        keys.add(STORAGE_KEY_PREFIX_V1 + 'default');
        return Array.from(keys);
    }

    function migrateV1ToV2IfNeeded() {
        try {
            if (!storageKey) return;
            const v2Raw = localStorage.getItem(storageKey);
            if (v2Raw) return;

            for (const k of guessOldV1StorageKeys()) {
                const raw = localStorage.getItem(k);
                if (raw) {
                    localStorage.setItem(storageKey, raw);
                    return;
                }
            }
        } catch {}
    }

    function loadConfig() {
        try {
            if (!storageKey) return { ...DEFAULT_CONFIG };
            const raw = localStorage.getItem(storageKey);
            if (!raw) return { ...DEFAULT_CONFIG };
            const parsed = JSON.parse(raw);
            return { ...DEFAULT_CONFIG, ...parsed };
        } catch {
            return { ...DEFAULT_CONFIG };
        }
    }

    function saveConfig() {
        try {
            if (!storageKey) return;
            localStorage.setItem(storageKey, JSON.stringify(config));
        } catch {}
    }

    function hexToRgb(hex) {
        if (!hex) return null;
        let s = hex.trim();
        if (s[0] === '#') s = s.slice(1);
        if (s.length === 3) s = s.split('').map(c => c + c).join('');
        if (s.length !== 6) return null;
        const r = parseInt(s.slice(0, 2), 16);
        const g = parseInt(s.slice(2, 4), 16);
        const b = parseInt(s.slice(4, 6), 16);
        if ([r, g, b].some(v => Number.isNaN(v))) return null;
        return { r, g, b };
    }

    function applyHighlightColorFromConfig() {
        const color = config.highlightColor || '#00c8ff';
        const rgb = hexToRgb(color) || { r: 0, g: 200, b: 255 };
        const bg = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.12)`;
        const root = document.documentElement;
        root.style.setProperty('--smartchat-highlight-color', color);
        root.style.setProperty('--smartchat-highlight-bg', bg);
    }

    function makeTimestamp() {
        const d = new Date();
        if (config.timestamp24h) {
            const h = String(d.getHours()).padStart(2, '0');
            const m = String(d.getMinutes()).padStart(2, '0');
            return `${h}:${m}`;
        }
        return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    }

    function containsAny(haystack, list) {
        const norm = normalize(haystack);
        return list.some(word => norm.includes(normalize(word)));
    }

    function waitForElement(selector, timeoutMs = 15000) {
        return new Promise(resolve => {
            const existing = document.querySelector(selector);
            if (existing) return resolve(existing);

            const observer = new MutationObserver(() => {
                const el = document.querySelector(selector);
                if (el) {
                    observer.disconnect();
                    resolve(el);
                }
            });
            observer.observe(document.documentElement || document.body, { childList: true, subtree: true });

            setTimeout(() => {
                observer.disconnect();
                resolve(null);
            }, timeoutMs);
        });
    }

    function extractFileNameFromUrl(url) {
        try {
            const noQuery = url.split('#')[0].split('?')[0];
            const parts = noQuery.split('/');
            return parts[parts.length - 1] || 'image';
        } catch {
            return 'image';
        }
    }

    function tryDetectMyName(ensureKeyword = false) {
        try {
            const prettyName = document.querySelector('#pretty_top_name');
            const detected = prettyName && prettyName.textContent.trim() ? prettyName.textContent.trim() : null;
            if (!detected) return;

            myName = detected;

            if (ensureKeyword && storageKey) {
                const alreadyHas = (config.mentionKeywords || []).some(k => normalize(k) === normalize(detected));
                if (!alreadyHas) {
                    config.mentionKeywords.push(detected);
                    saveConfig();
                }
            }
        } catch {}
    }

    function updateAccountKeyFromPrettyTop() {
        const newKey = getStorageKeyV2();
        if (newKey === storageKey) return;

        storageKey = newKey;

        if (!storageKey) {
            config = { ...DEFAULT_CONFIG };
            myName = null;
            applyHighlightColorFromConfig();
            if (typeof smartChatRenderSettings === 'function') smartChatRenderSettings();
            return;
        }

        migrateV1ToV2IfNeeded();
        config = loadConfig();
        applyHighlightColorFromConfig();
        tryDetectMyName(true);

        if (typeof smartChatRenderSettings === 'function') smartChatRenderSettings();
    }

    function getEmojiRecentStorageKey() {
        return (storageKey ? storageKey : 'bonk_smartchat_guest') + '_emoji_recent_v1';
    }

    function loadRecentEmojis() {
        try {
            if (!config.emojiRecentEnabled) return [];
            const raw = localStorage.getItem(getEmojiRecentStorageKey());
            const arr = raw ? JSON.parse(raw) : [];
            return Array.isArray(arr) ? arr.filter(x => typeof x === 'string').slice(0, 40) : [];
        } catch {
            return [];
        }
    }

    function saveRecentEmojis(list) {
        try {
            if (!config.emojiRecentEnabled) return;
            localStorage.setItem(getEmojiRecentStorageKey(), JSON.stringify(list.slice(0, 40)));
        } catch {}
    }

    function recordRecentEmoji(emoji) {
        if (!config.emojiRecentEnabled || !emoji) return;
        const list = loadRecentEmojis();
        const next = [emoji, ...list.filter(e => e !== emoji)].slice(0, 40);
        saveRecentEmojis(next);
    }

    function injectStyles() {
		const css = `
			:root {
				--smartchat-highlight-color:#00c8ff;
				--smartchat-highlight-bg:rgba(0,200,255,.12);

				--sc-font: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;

				--sc-text: rgba(0,0,0,.92);
				--sc-text-dim: rgba(0,0,0,.70);
				--sc-text-dimmer: rgba(0,0,0,.55);

				--sc-shadow: 0 18px 50px rgba(0,0,0,.55);
				--sc-accent: rgba(0,150,136,.85);

				--sc-light-text: rgba(0,0,0,.85);
				--sc-light-text-dim: rgba(0,0,0,.60);
			}

			.smartchat-msg { border-left:2px solid transparent; padding-left:6px; margin:1px 0; border-radius:6px; }
			.smartchat-timestamp { font-size:10px; opacity:.65; margin-right:6px; display:inline-block; font-variant-numeric: tabular-nums; }
			.smartchat-mention { background:var(--smartchat-highlight-bg); border-left-color:var(--smartchat-highlight-color); box-shadow: inset 0 0 0 1px rgba(255,255,255,.05); }
			.smartchat-muted { display:none !important; }

			.smartchat-antispam-flash { animation: smartchat-flash .35s ease-out; }
			@keyframes smartchat-flash { 0% { box-shadow:0 0 0 0 rgba(255,0,0,.70); } 100% { box-shadow:0 0 0 5px rgba(255,0,0,0); } }

			.smartchat-hidden-wrapper {
				font-size:11px; opacity:.95;
				padding:6px 8px; border-radius:8px;
				background: rgba(255,255,255,.06);
				border:1px solid rgba(255,255,255,.10);
			}
			.smartchat-hidden-label { opacity:.8; }
			.smartchat-hidden-toggle {
				cursor:pointer; margin-left:6px; text-decoration:none;
				color: rgba(255,255,255,.85);
				border-bottom: 1px dashed rgba(255,255,255,.35);
			}
			.smartchat-hidden-toggle:hover { color: rgba(255,255,255,.95); border-bottom-color: rgba(255,255,255,.6); }
			.smartchat-hidden-content { margin-top:6px; opacity:.95; }

			.smartchat-body { white-space: pre-wrap; }
			#ingamechatcontent .smartchat-body { color:#ffffff; }

			.smartchat-img-wrapper { display:block; margin-top:4px; }
			.smartchat-img-alt { display:block; font-size:11px; opacity:.75; margin-bottom:3px; font-family: var(--sc-font); }

			.smartchat-inline-img {
				display:block; max-width:140px; max-height:140px; cursor:pointer;
				border-radius:8px; box-shadow:0 8px 20px rgba(0,0,0,.35);
				outline:1px solid rgba(255,255,255,.10);
				transition: transform .12s ease, outline-color .12s ease, filter .12s ease;
			}
			.smartchat-inline-img:hover { transform: translateY(-1px); outline-color: rgba(255,255,255,.20); filter: brightness(1.03); }
			#ingamechatcontent .smartchat-inline-img { max-width:90px; max-height:56px; }
			.smartchat-row { margin-bottom:8px; }
			.smartchat-toggle-label {
				display:flex; align-items:center; gap:10px; cursor:pointer;
				font-size:12px; user-select:none; font-family: var(--sc-font);
				color: var(--sc-text);
			}
			.smartchat-toggle-input { display:none; }
			.smartchat-toggle-switch {
				position:relative; width:34px; height:18px; border-radius:999px;
				background: rgba(255,255,255,.10);
				box-shadow: inset 0 0 0 1px rgba(255,255,255,.14);
				transition: background .15s ease-out, box-shadow .15s ease-out;
				flex: 0 0 auto;
			}
			.smartchat-toggle-knob {
				position:absolute; top:2px; left:2px; width:14px; height:14px; border-radius:50%;
				background: rgba(255,255,255,.92);
				box-shadow: 0 3px 10px rgba(0,0,0,.35);
				transition: transform .15s ease-out;
			}
			.smartchat-toggle-input:checked + .smartchat-toggle-switch {
				background: var(--sc-accent);
				box-shadow: inset 0 0 0 1px rgba(0,0,0,.20);
			}
			.smartchat-toggle-input:checked + .smartchat-toggle-switch .smartchat-toggle-knob { transform: translateX(16px); }

			.smartchat-tag-label {
				font-size:11px; margin-bottom:4px; font-family: var(--sc-font);
				color: var(--sc-text-dim);
			}
			.smartchat-tag-input {
				display:flex; flex-wrap:wrap; gap:6px; padding:7px 8px; min-height:30px;
				background: rgba(255,255,255,.06);
				border-radius:10px; border:1px solid rgba(255,255,255,.12);
				box-shadow: inset 0 0 0 1px rgba(0,0,0,.15);
			}
			.smartchat-tag-list { display:flex; flex-wrap:wrap; gap:6px; }
			.smartchat-tag {
				display:inline-flex; align-items:center; gap:6px;
				padding:3px 10px; border-radius:999px;
				background: rgba(0,150,136,.85);
				color: rgba(0,0,0,.92);
				font-size:11px; font-weight:600;
				box-shadow: 0 6px 14px rgba(0,0,0,.25);
			}
			.smartchat-tag-remove { cursor:pointer; font-size:12px; opacity:.85; line-height:1; }
			.smartchat-tag-remove:hover { opacity:1; }
			.smartchat-tag-input-field {
				flex:1 1 90px; min-width:60px;
				border:none; outline:none; background: transparent;
				font-size:12px; color: var(--sc-text); font-family: var(--sc-font);
			}
			.smartchat-tag-input-field::placeholder { color: rgba(0,0,0,.45); }

			.smartchat-account-hint { margin-top:8px; font-size:11px; opacity:.75; font-family: var(--sc-font); color: var(--sc-text-dimmer); }
			.smartchat-guest-warning { margin-top:8px; font-size:11px; opacity:.92; font-family: var(--sc-font); color: rgba(255,204,102,.95); }

			#bonk_mods_settings_container .smartchat-toggle-label {
				color: var(--sc-light-text);
			}
			#bonk_mods_settings_container .smartchat-tag-label {
				color: var(--sc-light-text-dim);
			}
			#bonk_mods_settings_container .smartchat-account-hint {
				color: var(--sc-light-text-dim);
			}
			#bonk_mods_settings_container .smartchat-guest-warning {
				color: rgba(140,90,0,.92);
			}

			#bonk_mods_settings_container .smartchat-tag-input {
				background: rgba(255,255,255,.55);
				border: 1px solid rgba(0,0,0,.14);
				box-shadow: none;
			}
			#bonk_mods_settings_container .smartchat-tag-input-field {
				color: var(--sc-light-text);
			}
			#bonk_mods_settings_container .smartchat-tag-input-field::placeholder {
				color: rgba(0,0,0,.45);
			}
			#bonk_mods_settings_container .smartchat-tag {
				background: rgba(0,150,136,.88);
				color: rgba(255,255,255,.96);
				box-shadow: none;
			}

			#bonk_mods_settings_container .smartchat-toggle-switch {
				background: rgba(0,0,0,.08);
				box-shadow: inset 0 0 0 1px rgba(0,0,0,.16);
			}
			#bonk_mods_settings_container .smartchat-toggle-knob {
				background: rgba(255,255,255,.98);
				box-shadow: 0 1px 2px rgba(0,0,0,.22);
			}
			#bonk_mods_settings_container .smartchat-toggle-input:checked + .smartchat-toggle-switch {
				background: rgba(0,150,136,.95);
				box-shadow: inset 0 0 0 1px rgba(0,0,0,.12);
			}

			.smartchat-img-preview-backdrop {
				position:fixed; inset:0; background: rgba(0,0,0,.78);
				display:none; align-items:center; justify-content:center; z-index:99999;
				backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
			}
			.smartchat-img-preview-inner {
				max-width: min(92vw, 980px); max-height:90vh;
				display:flex; flex-direction:column; align-items:center; gap:10px;
				padding:12px; border-radius:14px;
				background: rgba(18,18,20,.65);
				border:1px solid rgba(255,255,255,.10);
				box-shadow: var(--sc-shadow);
			}
			.smartchat-img-preview-inner img {
				max-width:100%; max-height:78vh;
				border-radius:12px; box-shadow:0 18px 50px rgba(0,0,0,.65);
				outline:1px solid rgba(255,255,255,.12);
			}
			.smartchat-img-preview-caption {
				font-size:12px; color: rgba(255,255,255,.85); opacity:.95;
				font-family: var(--sc-font);
				max-width:80ch; text-align:center; word-break: break-word;
			}

			.smartchat-emoji-picker {
				position:fixed; z-index:100000;
				width: min(340px, 92vw);
				max-height: min(320px, 55vh);
				overflow:auto;
				background: linear-gradient(180deg, rgba(30,30,34,.94), rgba(18,18,20,.94));
				border:1px solid rgba(255,255,255,.14);
				border-radius:14px;
				box-shadow: var(--sc-shadow);
				padding:0;
				display:none;
				font-family: var(--sc-font);
				color: var(--sc-text);
				backdrop-filter: blur(8px);
				-webkit-backdrop-filter: blur(8px);
				overscroll-behavior: contain;

				opacity:0;
				transform: translateY(6px) scale(.98);
				transition: opacity .12s ease, transform .12s ease;
				will-change: opacity, transform;
				scrollbar-width: thin;
				scrollbar-color: rgba(255,255,255,.22) transparent;
			}
			.smartchat-emoji-picker.smartchat-open {
				opacity:1;
				transform: translateY(0) scale(1);
			}
			.smartchat-emoji-picker::-webkit-scrollbar { width:10px; }
			.smartchat-emoji-picker::-webkit-scrollbar-track { background: transparent; }
			.smartchat-emoji-picker::-webkit-scrollbar-thumb {
				background: rgba(255,255,255,.20);
				border-radius:999px;
				border:2px solid transparent;
				background-clip: padding-box;
			}
			.smartchat-emoji-picker::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.28); }

			.smartchat-emoji-header {
				position: sticky; top:0; z-index:2;
				padding: 10px 10px 10px 10px;
				background: rgba(18,18,20,.88);
				backdrop-filter: blur(8px);
				-webkit-backdrop-filter: blur(8px);
				border-bottom: 1px solid rgba(255,255,255,.10);
				border-top-left-radius: 14px;
				border-top-right-radius: 14px;
			}
			.smartchat-emoji-header-top { display:flex; align-items:center; justify-content:space-between; gap:10px; }
			.smartchat-emoji-title {
				font-size:11px; color: rgba(255,255,255,.70);
				white-space: nowrap; overflow:hidden; text-overflow: ellipsis;
			}
			.smartchat-emoji-actions { display:flex; gap:6px; flex: 0 0 auto; }
			.smartchat-emoji-chip {
				font-size:11px; padding:4px 8px; border-radius:999px;
				background: rgba(255,255,255,.07);
				border:1px solid rgba(255,255,255,.10);
				color: rgba(255,255,255,.85);
				cursor:pointer; user-select:none;
			}
			.smartchat-emoji-chip:hover { background: rgba(255,255,255,.10); }

			.smartchat-emoji-tabs { margin-top:10px; display:flex; gap:6px; flex-wrap: wrap; }
			.smartchat-emoji-tab {
				font-size:11px; padding:4px 8px; border-radius:999px;
				background: rgba(255,255,255,.06);
				border:1px solid rgba(255,255,255,.10);
				color: rgba(255,255,255,.78);
				cursor:pointer;
			}
			.smartchat-emoji-tab:hover { background: rgba(255,255,255,.09); }
			.smartchat-emoji-tab.smartchat-tab-active {
				background: rgba(0,150,136,.22);
				border-color: rgba(0,150,136,.35);
				color: rgba(255,255,255,.92);
			}

			.smartchat-emoji-list { padding: 8px; }

			.smartchat-emoji-item {
				display:flex; align-items:center; gap:10px;
				padding:8px 10px; border-radius:12px;
				cursor:pointer; user-select:none;
				font-size:13px; line-height:1.15;
				color: var(--sc-text);
				border:1px solid transparent;
				transition: background .12s ease, border-color .12s ease, transform .08s ease;
			}
			.smartchat-emoji-item:hover {
				background: rgba(255,255,255,.08);
				border-color: rgba(255,255,255,.10);
				transform: translateY(-1px);
			}
			.smartchat-emoji-item.smartchat-emoji-active {
				background: rgba(0,150,136,.22);
				border-color: rgba(0,150,136,.35);
				box-shadow: 0 10px 26px rgba(0,0,0,.28);
			}
			.smartchat-emoji-glyph {
				font-size:18px; width:26px; text-align:center;
				filter: drop-shadow(0 2px 6px rgba(0,0,0,.35));
			}
			.smartchat-emoji-name {
				opacity:.95; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
				font-variant-numeric: tabular-nums;
			}
			.smartchat-emoji-hint {
				opacity:.60; font-size:11px; margin-left:auto;
				padding:3px 8px; border-radius:999px;
				background: rgba(255,255,255,.07);
				border:1px solid rgba(255,255,255,.10);
			}

			.smartchat-emoji-list.smartchat-grid {
				display:grid;
				grid-template-columns: repeat(6, minmax(0, 1fr));
				gap:6px;
			}
			.smartchat-emoji-item.smartchat-grid-item {
				display:flex; flex-direction:column; align-items:center; justify-content:center;
				gap:4px; padding:10px 6px; text-align:center;
			}
			.smartchat-emoji-item.smartchat-grid-item .smartchat-emoji-name,
			.smartchat-emoji-item.smartchat-grid-item .smartchat-emoji-hint { display:none; }
			.smartchat-emoji-item.smartchat-grid-item .smartchat-emoji-glyph { width:auto; font-size:20px; }
		`;

        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    }

    function isSystemMessage(text) {
        const t = normalize(text);
        return (
            t.includes('joined the room') ||
            t.includes('left the room') ||
            t.includes("you\'re doing that too much") ||
            t.startsWith('server:') ||
            t.startsWith('[system]')
        );
    }

    function extractSenderName(msgEl) {
        const nameSpan = msgEl.querySelector('.ingamechatname');
        if (nameSpan) return nameSpan.textContent.replace(/:$/, '').trim();
        const text = msgEl.textContent || '';
        const idx = text.indexOf(':');
        if (idx > 0 && idx < 30) return text.slice(0, idx).trim();
        return null;
    }

    const BRACKET_IMG_RE = /\[(https?:\/\/[^\]\s]+?\.(?:png|jpe?g|gif|webp)[^\]]*)]/gi;

    function forceScrollBottom(scroller) {
        if (!scroller) return;
        try { scroller.scrollTop = scroller.scrollHeight; } catch {}
    }

    function getScrollContainerFrom(el) {
        let node = el;
        while (node && node !== document.body) {
            if (node.scrollHeight - node.clientHeight > 5) return node;
            node = node.parentElement;
        }
        return el;
    }

    function ensureImagePreviewOverlay() {
        if (imgPreviewBackdrop) return;

        const backdrop = document.createElement('div');
        backdrop.className = 'smartchat-img-preview-backdrop';

        const inner = document.createElement('div');
        inner.className = 'smartchat-img-preview-inner';

        const img = document.createElement('img');
        const caption = document.createElement('div');
        caption.className = 'smartchat-img-preview-caption';

        inner.appendChild(img);
        inner.appendChild(caption);
        backdrop.appendChild(inner);
        document.body.appendChild(backdrop);

        backdrop.addEventListener('click', () => closeImagePreview());
        inner.addEventListener('click', e => e.stopPropagation());

        imgPreviewBackdrop = backdrop;
        imgPreviewImg = img;
        imgPreviewCaption = caption;

        window.addEventListener('keydown', e => {
            if (e.key === 'Escape' && imgPreviewBackdrop && imgPreviewBackdrop.style.display === 'flex') closeImagePreview();
        });
    }

    function openImagePreview(url, alt) {
        ensureImagePreviewOverlay();
        imgPreviewImg.src = url;
        imgPreviewCaption.textContent = alt || '';
        imgPreviewBackdrop.style.display = 'flex';
    }

    function closeImagePreview() {
        if (!imgPreviewBackdrop) return;
        imgPreviewBackdrop.style.display = 'none';
        imgPreviewImg.src = '';
        imgPreviewCaption.textContent = '';
    }

    function createImageBlock(url, alt, scroller) {
        const wrapper = document.createElement('div');
        wrapper.className = 'smartchat-img-wrapper';

        const altSpan = document.createElement('span');
        altSpan.className = 'smartchat-img-alt';
        altSpan.textContent = `[${alt}]`;

        const img = document.createElement('img');
        img.className = 'smartchat-inline-img';
        img.src = url;

        img.addEventListener('load', () => forceScrollBottom(scroller));
        img.addEventListener('click', e => {
            e.stopPropagation();
            openImagePreview(url, alt);
        });

        wrapper.appendChild(altSpan);
        wrapper.appendChild(img);
        return wrapper;
    }

    function ensureBodyContainer(msgEl) {
        let body = msgEl.querySelector('.smartchat-body');
        if (body) return body;

        const nameSpan = msgEl.querySelector('.ingamechatname');
        body = document.createElement('span');
        body.className = 'smartchat-body';

        if (nameSpan) {
            let sibling = nameSpan.nextSibling;
            while (sibling) {
                const next = sibling.nextSibling;
                body.appendChild(sibling);
                sibling = next;
            }
            msgEl.appendChild(body);
        } else {
            while (msgEl.firstChild) body.appendChild(msgEl.firstChild);
            msgEl.appendChild(body);
        }
        return body;
    }

    function getBodyTextWithoutName(msgEl) {
        const body = ensureBodyContainer(msgEl);
        let text = (body.textContent || '').trim();

        const lobbyNameSpan = msgEl.querySelector('.newbonklobby_chat_msg_name');
        if (lobbyNameSpan && lobbyNameSpan.textContent) {
            const nameText = lobbyNameSpan.textContent;
            const idx = text.indexOf(nameText);
            if (idx !== -1) text = (text.slice(0, idx) + text.slice(idx + nameText.length)).trim();
        }

        const tsMatch = text.match(/^\[\d{1,2}:\d{2}\]\s*/);
        if (tsMatch) text = text.slice(tsMatch[0].length);

        return text;
    }

    function processBracketImages(msgEl, scroller) {
        const body = ensureBodyContainer(msgEl);
        const text = body.textContent;
        if (!text || !BRACKET_IMG_RE.test(text)) return;
        BRACKET_IMG_RE.lastIndex = 0;

        const frag = document.createDocumentFragment();
        let lastIndex = 0;
        let match;

        while ((match = BRACKET_IMG_RE.exec(text))) {
            const idx = match.index;
            const url = match[1];

            if (idx > lastIndex) frag.appendChild(document.createTextNode(text.slice(lastIndex, idx)));

            const alt = extractFileNameFromUrl(url);
            frag.appendChild(createImageBlock(url, alt, scroller));

            lastIndex = BRACKET_IMG_RE.lastIndex;
        }

        if (lastIndex < text.length) frag.appendChild(document.createTextNode(text.slice(lastIndex)));

        body.textContent = '';
        body.appendChild(frag);
    }

    function hideMessageWithToggle(msgEl, reason) {
        if (msgEl._smartHiddenToggleApplied) return;
        msgEl._smartHiddenToggleApplied = true;

        const body = ensureBodyContainer(msgEl);
        const originalHTML = body.innerHTML;

        const labelTextMap = {
            system: 'System message hidden',
            blacklist: 'Message hidden (blacklisted word)',
            muted: 'Message hidden (muted user)',
            default: 'Message hidden'
        };
        const labelText = labelTextMap[reason] || labelTextMap.default;

        const wrapper = document.createElement('div');
        wrapper.className = 'smartchat-hidden-wrapper';

        const label = document.createElement('span');
        label.className = 'smartchat-hidden-label';
        label.textContent = labelText;

        const toggle = document.createElement('a');
        toggle.className = 'smartchat-hidden-toggle';
        toggle.textContent = '[reveal]';

        const content = document.createElement('div');
        content.className = 'smartchat-hidden-content';
        content.style.display = 'none';
        content.innerHTML = originalHTML;

        toggle.addEventListener('click', e => {
            e.preventDefault();
            const visible = content.style.display !== 'none';
            content.style.display = visible ? 'none' : '';
            toggle.textContent = visible ? '[reveal]' : '[hide]';
        });

        wrapper.appendChild(label);
        wrapper.appendChild(toggle);
        wrapper.appendChild(content);

        body.innerHTML = '';
        body.appendChild(wrapper);
    }

    function enhanceMessage(msgEl, scroller) {
        if (!config.enabled || !msgEl || msgEl._smartChatProcessed) return;
        msgEl._smartChatProcessed = true;

        msgEl.classList.add('smartchat-msg');

        const text = (msgEl.textContent || '').trim();
        if (!text) return;

        const sender = extractSenderName(msgEl);
        const isSelf = sender && myName && normalize(sender) === normalize(myName);

        if (config.hideSystemMessages && isSystemMessage(text)) {
            hideMessageWithToggle(msgEl, 'system');
            forceScrollBottom(scroller);
            return;
        }

        if (config.wordBlacklist && config.wordBlacklist.length) {
            if (!isSelf && containsAny(text, config.wordBlacklist)) {
                hideMessageWithToggle(msgEl, 'blacklist');
                forceScrollBottom(scroller);
                return;
            }
        }

        if (config.mutedNames && config.mutedNames.length) {
            if (!isSelf && sender && config.mutedNames.some(n => normalize(n) === normalize(sender))) {
                hideMessageWithToggle(msgEl, 'muted');
                forceScrollBottom(scroller);
                return;
            }
        }

        if (config.showTimestamps && !msgEl.querySelector('.smartchat-timestamp')) {
            const tsSpan = document.createElement('span');
            tsSpan.className = 'smartchat-timestamp';
            tsSpan.textContent = `[${makeTimestamp()}]`;
            msgEl.insertBefore(tsSpan, msgEl.firstChild);
        }

        if (config.highlightMentions && config.mentionKeywords && config.mentionKeywords.length) {
            const bodyText = getBodyTextWithoutName(msgEl);
            if (containsAny(bodyText, config.mentionKeywords)) msgEl.classList.add('smartchat-mention');
        }

        processBracketImages(msgEl, scroller);
        setTimeout(() => forceScrollBottom(scroller), 0);
    }

    function observeChatContainer(contentEl) {
        if (!contentEl || contentEl._smartChatObserverAttached) return;
        contentEl._smartChatObserverAttached = true;

        const scroller = getScrollContainerFrom(contentEl);
        Array.from(contentEl.children).forEach(el => enhanceMessage(el, scroller));

        const obs = new MutationObserver(mutations => {
            if (!config.enabled) return;
            for (const m of mutations) {
                for (const node of m.addedNodes) {
                    if (node.nodeType === 1) enhanceMessage(node, scroller);
                }
            }
        });
        obs.observe(contentEl, { childList: true });
    }

    function getEmojiQueryAtCaret(input) {
        try {
            const v = input.value || '';
            const pos = input.selectionStart ?? v.length;

            if (pos > 0 && v[pos - 1] === ':') {
                const prevColon = v.lastIndexOf(':', pos - 2);
                if (prevColon !== -1) {
                    const between = v.slice(prevColon + 1, pos - 1);
                    if (between && /^[a-z0-9_ ]+$/i.test(between)) {
                        return null;
                    }
                }
            }

            const left = v.slice(0, pos);
            const lastColon = left.lastIndexOf(':');
            if (lastColon === -1) return null;

            const token = left.slice(lastColon + 1);
            if (token.includes(':')) return null;

            const beforeColon = left.slice(Math.max(0, lastColon - 6), lastColon + 1);
            if (/https?:\/:$/.test(beforeColon)) return null;

            if (/[\r\n]/.test(token)) return null;
            if (!/^[a-z0-9_ ]*$/i.test(token)) return null;

            return { start: lastColon, query: token };
        } catch {
            return null;
        }
    }

    function findEmojiMatches(queryRaw, limit = 20) {
        const q = normalizeEmojiName(queryRaw || '');
        if (!q) {
            const defaults = ['smile', 'joy', 'pensive', 'thinking', 'heart', 'thumbs up', 'fire', 'eyes', 'tada', '100'];
            return defaults
                .map(n => ({ name: n, emoji: EMOJI_SHORTCODES[normalizeEmojiName(n)] }))
                .filter(x => x.emoji)
                .slice(0, limit);
        }

        const out = [];
        for (const key in EMOJI_SHORTCODES) {
            if (key.includes(q)) {
                out.push({ name: key, emoji: EMOJI_SHORTCODES[key] });
                if (out.length >= limit) break;
            }
        }
        return out;
    }

    function buildMatchesForPicker(queryRaw, categoryId) {
        const q = normalizeEmojiName(queryRaw || '');

        if (categoryId === 'recent') {
            const recents = loadRecentEmojis();
            const reverseLookup = Object.create(null);
            for (const nameKey of Object.keys(EMOJI_SHORTCODES)) {
                reverseLookup[EMOJI_SHORTCODES[nameKey]] = nameKey;
            }

            const recentMatches = recents
            .map(e => {
                const nameKey = reverseLookup[e] || '';
                return nameKey ? { name: nameKey, emoji: e } : null;
            })
            .filter(Boolean);

            if (!q) {
                if (recentMatches.length) return recentMatches.slice(0, 60);
                return EMOJI_ALL.filter(m => categorizeEmoji(m.name, m.emoji) === 'smileys').slice(0, 80);
            }

            const filtered = recentMatches.filter(m => m.name.includes(q)).slice(0, 60);
            if (filtered.length) return filtered;

            return EMOJI_ALL.filter(m => m.name.includes(q)).slice(0, 80);
        }

        if (!q) {
            return EMOJI_ALL
                .filter(m => categorizeEmoji(m.name, m.emoji) === categoryId)
                .slice(0, 120);
        }

        return EMOJI_ALL
            .filter(m => m.name.includes(q))
            .filter(m => categorizeEmoji(m.name, m.emoji) === categoryId)
            .slice(0, 80);
    }

    function closeEmojiPicker(input) {
        const st = emojiUiState.get(input);
        if (!st) return;
        st.open = false;
        st.matches = [];
        st.activeIndex = 0;
        st.tokenStart = -1;
        st.lastQuery = '';
        if (st.pickerEl) {
            st.pickerEl.classList.remove('smartchat-open');
            st.pickerEl.style.display = 'none';
        }
    }

    function ensureEmojiPicker(input) {
        let st = emojiUiState.get(input);
        if (st && st.pickerEl) return st;

        const picker = document.createElement('div');
        picker.className = 'smartchat-emoji-picker';
        document.body.appendChild(picker);

        st = {
            pickerEl: picker,
            open: false,
            matches: [],
            activeIndex: 0,
            tokenStart: -1,
            lastQuery: '',
            activeCategory: (loadRecentEmojis().length ? 'recent' : 'smileys'),
            listEl: null
        };
        emojiUiState.set(input, st);

        picker.addEventListener('mousedown', (e) => {
            e.preventDefault();
            e.stopPropagation();
        }, true);

        picker.addEventListener('click', (e) => {
            e.stopPropagation();
        }, true);

        picker.addEventListener('wheel', (e) => {
            const atTop = picker.scrollTop <= 0;
            const atBottom = picker.scrollTop + picker.clientHeight >= picker.scrollHeight - 1;
            const goingUp = e.deltaY < 0;
            const goingDown = e.deltaY > 0;

            if ((atTop && goingUp) || (atBottom && goingDown)) {
                e.preventDefault();
            }
            e.stopPropagation();
        }, { passive: false });

        const onDocPointerDown = (e) => {
            if (!st.open) return;
            if (st.pickerEl && (st.pickerEl === e.target || st.pickerEl.contains(e.target))) return;
            if (e.target === input || input.contains(e.target)) return;
            closeEmojiPicker(input);
        };
        document.addEventListener('pointerdown', onDocPointerDown, true);

        window.addEventListener('scroll', () => {
            const st2 = emojiUiState.get(input);
            if (st2 && st2.open && st2.pickerEl) positionEmojiPicker(input, st2.pickerEl);
        }, true);

        window.addEventListener('resize', () => {
            const st2 = emojiUiState.get(input);
            if (st2 && st2.open && st2.pickerEl) positionEmojiPicker(input, st2.pickerEl);
        }, true);

        return st;
    }

    function positionEmojiPicker(input, pickerEl) {
        const r = input.getBoundingClientRect();
        const margin = 8;

        const prevDisplay = pickerEl.style.display;
        pickerEl.style.display = 'block';

        const pickerRect = pickerEl.getBoundingClientRect();
        const pickerH = pickerRect.height || pickerEl.offsetHeight || 0;
        const pickerW = pickerRect.width || pickerEl.offsetWidth || 180;

        let top = r.top - margin - pickerH;
        if (top < 6) top = r.bottom + margin;

        const maxTop = window.innerHeight - 6 - pickerH;
        if (top > maxTop) top = Math.max(6, maxTop);

        let left = r.left;
        if (left + pickerW > window.innerWidth - 6) left = window.innerWidth - 6 - pickerW;
        if (left < 6) left = 6;

        pickerEl.style.top = `${Math.round(top)}px`;
        pickerEl.style.left = `${Math.round(left)}px`;

        pickerEl.style.display = prevDisplay || 'block';
    }

    function scrollEmojiActiveIntoView(input) {
        const st = emojiUiState.get(input);
        if (!st || !st.open || !st.pickerEl) return;

        const picker = st.pickerEl;
        const list = st.listEl || picker.querySelector('.smartchat-emoji-list');
        if (!list) return;

        const items = list.querySelectorAll('.smartchat-emoji-item');
        const active = items[st.activeIndex];
        if (!active) return;

        const header = picker.querySelector('.smartchat-emoji-header');
        const headerH = header ? header.getBoundingClientRect().height : 0;

        const pickerRect = picker.getBoundingClientRect();
        const activeRect = active.getBoundingClientRect();

        const topVisible = pickerRect.top + headerH + 6;
        const bottomVisible = pickerRect.bottom - 6;

        if (activeRect.top < topVisible) {
            const delta = topVisible - activeRect.top;
            picker.scrollTop -= delta;
            return;
        }

        if (activeRect.bottom > bottomVisible) {
            const delta = activeRect.bottom - bottomVisible;
            picker.scrollTop += delta;
        }
    }

    function renderEmojiPicker(input) {
        const st = ensureEmojiPicker(input);
        const picker = st.pickerEl;

        picker.innerHTML = '';

        const header = document.createElement('div');
        header.className = 'smartchat-emoji-header';

        const topRow = document.createElement('div');
        topRow.className = 'smartchat-emoji-header-top';

        const title = document.createElement('div');
        title.className = 'smartchat-emoji-title';
        const qShown = (st.lastQuery || '').trim();
        title.textContent = qShown ? `Emoji — "${qShown}" (Enter to insert)` : 'Emoji — type :name (Enter to insert)';

        const actions = document.createElement('div');
        actions.className = 'smartchat-emoji-actions';

        const gridBtn = document.createElement('div');
        gridBtn.className = 'smartchat-emoji-chip';
        gridBtn.textContent = config.emojiGridMode ? 'Grid ✓' : 'Grid';
        gridBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            config.emojiGridMode = !config.emojiGridMode;
            saveConfig();
            renderEmojiPicker(input);
        });

        actions.appendChild(gridBtn);

        topRow.appendChild(title);
        topRow.appendChild(actions);
        header.appendChild(topRow);

        if (config.emojiShowCategories) {
            const tabs = document.createElement('div');
            tabs.className = 'smartchat-emoji-tabs';

            EMOJI_CATEGORIES.forEach(cat => {
                const tab = document.createElement('div');
                tab.className = 'smartchat-emoji-tab' + (st.activeCategory === cat.id ? ' smartchat-tab-active' : '');
                tab.textContent = cat.label;

                tab.addEventListener('pointerdown', (e) => {
                    e.preventDefault();
                    e.stopPropagation();

                    const st = ensureEmojiPicker(input);
                    st.activeCategory = cat.id;
                    st.activeIndex = 0;

                    const q = getEmojiQueryAtCaret(input);
                    const query = q ? q.query : '';

                    st.lastQuery = query;
                    st.matches = buildMatchesForPicker(query, st.activeCategory);

                    st.open = true;
                    st.pickerEl.style.display = 'block';
                    st.pickerEl.classList.add('smartchat-open');

                    renderEmojiPicker(input);
                    scrollEmojiActiveIntoView(input);
                    positionEmojiPicker(input, st.pickerEl);
                }, true);

                tabs.appendChild(tab);
            });

            header.appendChild(tabs);
        }

        picker.appendChild(header);

        const list = document.createElement('div');
        list.className = 'smartchat-emoji-list' + (config.emojiGridMode ? ' smartchat-grid' : '');
        st.listEl = list;
        picker.appendChild(list);

        const matches = st.matches || [];
        matches.forEach((m, idx) => {
            const row = document.createElement('div');
            const isActive = idx === st.activeIndex;

            row.className =
                'smartchat-emoji-item' +
                (isActive ? ' smartchat-emoji-active' : '') +
                (config.emojiGridMode ? ' smartchat-grid-item' : '');

            const glyph = document.createElement('div');
            glyph.className = 'smartchat-emoji-glyph';
            glyph.textContent = m.emoji;

            const name = document.createElement('div');
            name.className = 'smartchat-emoji-name';
            name.textContent = `:${m.name.replace(/ /g, '_')}:`;

            const hint = document.createElement('div');
            hint.className = 'smartchat-emoji-hint';
            hint.textContent = 'Enter';

            row.appendChild(glyph);
            if (!config.emojiGridMode) {
                row.appendChild(name);
                row.appendChild(hint);
            }

            row.addEventListener('mouseenter', () => {
                st.activeIndex = idx;
                renderEmojiPicker(input);
                scrollEmojiActiveIntoView(input);
                positionEmojiPicker(input, st.pickerEl);
            });

            row.addEventListener('mousedown', (e) => {
                e.preventDefault();
                e.stopPropagation();
                insertEmojiForCurrentToken(input, m.emoji);
            }, true);

            list.appendChild(row);
        });

        if (st && st.open && st.pickerEl) positionEmojiPicker(input, st.pickerEl);
    }

    function openEmojiPicker(input, matches, tokenStart, query) {
        const st = ensureEmojiPicker(input);
        st.matches = matches;
        st.activeIndex = 0;
        st.open = true;
        st.tokenStart = tokenStart;
        st.lastQuery = query;

        st.pickerEl.style.display = matches.length ? 'block' : 'none';
        if (!matches.length) {
            st.pickerEl.classList.remove('smartchat-open');
            return;
        }

        st.pickerEl.classList.add('smartchat-open');
        renderEmojiPicker(input);
        scrollEmojiActiveIntoView(input);
        positionEmojiPicker(input, st.pickerEl);
    }

    function insertEmojiForCurrentToken(input, emoji) {
        const st = ensureEmojiPicker(input);
        const v = input.value || '';
        const pos = input.selectionStart ?? v.length;

        const start = st.tokenStart >= 0 ? st.tokenStart : (() => {
            const q = getEmojiQueryAtCaret(input);
            return q ? q.start : -1;
        })();
        if (start < 0) return;

        let end = pos;
        if (v[end] === ':') end += 1;

        const before = v.slice(0, start);
        const after = v.slice(end);

        const needsSpace = after.length && !/^\s/.test(after) ? ' ' : '';
        input.value = before + emoji + needsSpace + after;

        const newPos = (before + emoji + needsSpace).length;
        try { input.setSelectionRange(newPos, newPos); } catch {}

        input.dispatchEvent(new Event('input', { bubbles: true }));

        recordRecentEmoji(emoji);
        closeEmojiPicker(input);
    }

    function replaceCompletedEmojiShortcodes(input) {
        const v = input.value || '';
        const re = /:([a-z0-9_ ]+):/gi;

        const caret = input.selectionStart ?? v.length;
        let changed = false;
        let deltaBeforeCaret = 0;

        const out = v.replace(re, (full, name, offset) => {
            const key = normalizeEmojiName(name);
            const emoji = EMOJI_SHORTCODES[key];
            if (!emoji) return full;

            changed = true;

            const endOfMatch = offset + full.length;
            if (endOfMatch <= caret) deltaBeforeCaret += (emoji.length - full.length);
            return emoji;
        });

        if (changed && out !== v) {
            input.value = out;
            const newCaret = Math.max(0, caret + deltaBeforeCaret);
            try { input.setSelectionRange(newCaret, newCaret); } catch {}
            input.dispatchEvent(new Event('input', { bubbles: true }));
        }
    }

    function attachEmojiToInput(input) {
        if (!input || input._smartChatEmojiAttached) return;
        input._smartChatEmojiAttached = true;

        ensureEmojiPicker(input);

        input.addEventListener('input', () => {
            if (!config.enabled || !config.emojiEnabled) return;

            if (config.emojiReplaceOnType) replaceCompletedEmojiShortcodes(input);

            const v = input.value || '';
            const pos = input.selectionStart ?? v.length;
            if (pos > 0 && v[pos - 1] === ':') {
                const prev = v.lastIndexOf(':', pos - 2);
                if (prev !== -1) {
                    const between = v.slice(prev + 1, pos - 1);
                    if (between && /^[a-z0-9_ ]+$/i.test(between)) {
                        closeEmojiPicker(input);
                        return;
                    }
                }
            }

            if (!config.emojiPickerEnabled) {
                closeEmojiPicker(input);
                return;
            }

            const q = getEmojiQueryAtCaret(input);

            if (!q) {
                closeEmojiPicker(input);
                return;
            }

            const st = ensureEmojiPicker(input);
            st.lastQuery = q.query;

            const matches = buildMatchesForPicker(q.query, st.activeCategory).slice(0, 40);
            if (!matches.length) {
                closeEmojiPicker(input);
                return;
            }

            openEmojiPicker(input, matches, q.start, q.query);
        }, true);

        input.addEventListener('keydown', (e) => {
            if (!config.enabled || !config.emojiEnabled || !config.emojiPickerEnabled) return;

            const st = emojiUiState.get(input);
            if (!st || !st.open || !st.matches.length) return;

            const isGrid = !!config.emojiGridMode;

            const GRID_COLS = 6;

            if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
                e.preventDefault();
                e.stopPropagation();

                const len = st.matches.length;
                if (!len) return;

                if (!isGrid) {
                    const dir = (e.key === 'ArrowDown') ? 1 : (e.key === 'ArrowUp' ? -1 : 0);
                    if (dir !== 0) {
                        st.activeIndex = Math.max(0, Math.min(len - 1, st.activeIndex + dir));
                        renderEmojiPicker(input);
                        scrollEmojiActiveIntoView(input);
                        positionEmojiPicker(input, st.pickerEl);
                    }
                    return;
                }

                let next = st.activeIndex;

                if (e.key === 'ArrowLeft') next = st.activeIndex - 1;
                if (e.key === 'ArrowRight') next = st.activeIndex + 1;
                if (e.key === 'ArrowUp') next = st.activeIndex - GRID_COLS;
                if (e.key === 'ArrowDown') next = st.activeIndex + GRID_COLS;

                if (next < 0) next = 0;
                if (next > len - 1) next = len - 1;

                st.activeIndex = next;
                renderEmojiPicker(input);
                scrollEmojiActiveIntoView(input);
                positionEmojiPicker(input, st.pickerEl);
                return;
            }

            if (e.key === 'Enter' || e.key === 'Tab') {
                e.preventDefault();
                e.stopPropagation();
                const m = st.matches[st.activeIndex] || st.matches[0];
                if (m) insertEmojiForCurrentToken(input, m.emoji);
                return;
            }

            if (e.key === 'Escape') {
                e.preventDefault();
                e.stopPropagation();
                closeEmojiPicker(input);
                return;
            }
        }, true);

        input.addEventListener('blur', () => {
            setTimeout(() => {
                const st = emojiUiState.get(input);
                if (!st || !st.open) return;

                if (config.emojiPinned) return;

                const ae = document.activeElement;
                const insidePicker = st.pickerEl && ae && st.pickerEl.contains(ae);
                const isInput = ae === input;

                if (!insidePicker && !isInput) closeEmojiPicker(input);
            }, 0);
        }, true);
    }

    function attachAntiSpamToInput(input) {
        if (!input || input._smartChatAntiSpamAttached) return;
        input._smartChatAntiSpamAttached = true;

        input.addEventListener('keydown', e => {
            if (!config.enabled || !config.antiSpamEnabled) return;
            if (e.key !== 'Enter' || e.shiftKey) return;

            const st = emojiUiState.get(input);
            if (!st || !st.open || !st.matches.length) return;

            const q = getEmojiQueryAtCaret(input);
            if (!q || !q.query) return;

            const now = performance.now();
            const diff = now - lastSendTime;
            if (diff < config.antiSpamDelayMs) {
                e.stopPropagation();
                e.preventDefault();
                input.classList.remove('smartchat-antispam-flash');
                void input.offsetWidth;
                input.classList.add('smartchat-antispam-flash');
                return;
            }
            lastSendTime = now;
        }, true);
    }

    function setupModSettings() {
        const g = window;
        if (!g.bonkMods || !g.bonkMods.addBlock) return;

        const bonkMods = g.bonkMods;

        bonkMods.registerMod({
            id: 'smartChat',
            name: 'Smart Chat',
            version: '0.0.8',
            author: 'SIoppy',
            description: 'Smart chat improvements + :emoji_name: input picker (grid/categories/recents/pinned). Per-account saved locally (no guest persistence).'
        });

        bonkMods.registerCategory({
            id: 'chat',
            label: 'Chat',
            order: 5
        });

        bonkMods.addBlock({
            id: 'smartChat_main',
            modId: 'smartChat',
            categoryId: 'chat',
            title: 'Smart Chat',
            order: 0,
            render(container) {
                smartChatSettingsContainer = container;

                const doRender = () => {
                    try {
                        container.innerHTML = '';

                        const root = document.createElement('div');
                        root.style.fontSize = '12px';
                        container.appendChild(root);

                        const mkRow = () => {
                            const div = document.createElement('div');
                            div.className = 'smartchat-row';
                            root.appendChild(div);
                            return div;
                        };

                        const mkCheckbox = (labelText, initial, onChange) => {
                            const row = mkRow();

                            const label = document.createElement('label');
                            label.className = 'smartchat-toggle-label';

                            const input = document.createElement('input');
                            input.type = 'checkbox';
                            input.className = 'smartchat-toggle-input';
                            input.checked = !!initial;

                            const switchSpan = document.createElement('span');
                            switchSpan.className = 'smartchat-toggle-switch';

                            const knob = document.createElement('span');
                            knob.className = 'smartchat-toggle-knob';
                            switchSpan.appendChild(knob);

                            const textSpan = document.createElement('span');
                            textSpan.textContent = labelText;

                            input.addEventListener('change', () => onChange(input.checked));

                            label.appendChild(input);
                            label.appendChild(switchSpan);
                            label.appendChild(textSpan);

                            row.appendChild(label);
                            return input;
                        };

                        const mkNumberInput = (title, initial, onChange) => {
                            const row = mkRow();
                            const label = document.createElement('div');
                            label.textContent = title;
                            label.style.fontSize = '11px';
                            label.style.marginBottom = '4px';
                            row.appendChild(label);

                            const input = document.createElement('input');
                            input.type = 'number';
                            input.value = String(initial || 0);
                            input.style.width = '90px';
                            input.style.fontSize = '11px';

                            input.addEventListener('change', () => {
                                const v = parseInt(input.value, 10);
                                if (!isNaN(v) && v > 0) onChange(v);
                            });

                            row.appendChild(input);
                            return input;
                        };

                        const mkColorInput = (title, initial, onChange) => {
                            const row = mkRow();
                            const label = document.createElement('div');
                            label.textContent = title;
                            label.style.fontSize = '11px';
                            label.style.marginBottom = '4px';
                            row.appendChild(label);

                            const input = document.createElement('input');
                            input.type = 'color';
                            input.value = initial || '#00c8ff';
                            input.style.width = '52px';
                            input.style.height = '22px';
                            input.style.padding = '0';
                            input.style.border = 'none';
                            input.style.background = 'transparent';

                            input.addEventListener('change', () => onChange(input.value));
                            row.appendChild(input);
                            return input;
                        };

                        const mkTagInput = (title, initialList, onChange) => {
                            const row = mkRow();

                            const label = document.createElement('div');
                            label.className = 'smartchat-tag-label';
                            label.textContent = title;
                            row.appendChild(label);

                            const wrapper = document.createElement('div');
                            wrapper.className = 'smartchat-tag-input';

                            const tagList = document.createElement('div');
                            tagList.className = 'smartchat-tag-list';

                            const input = document.createElement('input');
                            input.type = 'text';
                            input.className = 'smartchat-tag-input-field';
                            input.placeholder = 'Type and press Enter';

                            let tags = Array.isArray(initialList) ? [...initialList] : [];

                            const renderTags = () => {
                                tagList.innerHTML = '';
                                tags.forEach((tag, idx) => {
                                    const tagEl = document.createElement('span');
                                    tagEl.className = 'smartchat-tag';

                                    const textSpan = document.createElement('span');
                                    textSpan.textContent = tag;

                                    const remove = document.createElement('span');
                                    remove.className = 'smartchat-tag-remove';
                                    remove.textContent = '×';
                                    remove.addEventListener('click', () => {
                                        tags.splice(idx, 1);
                                        renderTags();
                                        onChange([...tags]);
                                    });

                                    tagEl.appendChild(textSpan);
                                    tagEl.appendChild(remove);
                                    tagList.appendChild(tagEl);
                                });
                            };

                            const commitInput = () => {
                                const value = input.value;
                                if (!value || !value.trim()) return;
                                const parts = value.split(',').map(s => s.trim()).filter(Boolean);
                                let changed = false;
                                parts.forEach(p => {
                                    if (p && !tags.includes(p)) {
                                        tags.push(p);
                                        changed = true;
                                    }
                                });
                                if (changed) {
                                    renderTags();
                                    onChange([...tags]);
                                }
                                input.value = '';
                            };

                            input.addEventListener('keydown', e => {
                                if (e.key === 'Enter' || e.key === ',') {
                                    e.preventDefault();
                                    commitInput();
                                }
                            });

                            input.addEventListener('blur', () => commitInput());

                            renderTags();

                            wrapper.appendChild(tagList);
                            wrapper.appendChild(input);
                            row.appendChild(wrapper);

                            return { getTags: () => [...tags] };
                        };

                        mkCheckbox('Enabled', config.enabled, val => { config.enabled = val; saveConfig(); });
                        mkCheckbox('Show timestamps', config.showTimestamps, val => { config.showTimestamps = val; saveConfig(); });
                        mkCheckbox('24h clock', config.timestamp24h, val => { config.timestamp24h = val; saveConfig(); });
                        mkCheckbox('Highlight mentions', config.highlightMentions, val => { config.highlightMentions = val; saveConfig(); });

                        mkColorInput('Highlight colour:', config.highlightColor || '#00c8ff', value => {
                            config.highlightColor = value;
                            saveConfig();
                            applyHighlightColorFromConfig();
                        });

                        mkCheckbox('Anti-spam (minimum delay)', config.antiSpamEnabled, val => { config.antiSpamEnabled = val; saveConfig(); });
                        mkCheckbox('Hide system / spam warnings (with reveal)', config.hideSystemMessages, val => { config.hideSystemMessages = val; saveConfig(); });

                        mkNumberInput('Anti-spam delay (ms):', config.antiSpamDelayMs, v => { config.antiSpamDelayMs = v; saveConfig(); });

                        mkTagInput('Mention keywords:', config.mentionKeywords || [], list => { config.mentionKeywords = list; saveConfig(); });
                        mkTagInput('Word blacklist:', config.wordBlacklist || [], list => { config.wordBlacklist = list; saveConfig(); });
                        mkTagInput('Muted names:', config.mutedNames || [], list => {
                            const filtered = myName ? list.filter(n => normalize(n) !== normalize(myName)) : list;
                            config.mutedNames = filtered;
                            saveConfig();
                        });

                        mkCheckbox('Emoji shortcodes (:name: → 😄)', config.emojiEnabled, val => { config.emojiEnabled = val; saveConfig(); });
                        mkCheckbox('Emoji picker while typing ":"', config.emojiPickerEnabled, val => { config.emojiPickerEnabled = val; saveConfig(); });
                        mkCheckbox('Replace completed :name: as you type', config.emojiReplaceOnType, val => { config.emojiReplaceOnType = val; saveConfig(); });

                        mkCheckbox('Emoji picker: grid mode', config.emojiGridMode, val => { config.emojiGridMode = val; saveConfig(); });
                        mkCheckbox('Emoji picker: show categories', config.emojiShowCategories, val => { config.emojiShowCategories = val; saveConfig(); });
                        mkCheckbox('Emoji picker: remember recents', config.emojiRecentEnabled, val => { config.emojiRecentEnabled = val; saveConfig(); });

                        if (!storageKey) {
                            const warn = document.createElement('div');
                            warn.className = 'smartchat-guest-warning';
                            warn.textContent = 'Guest mode: settings are temporary until you log in.';
                            root.appendChild(warn);
                        } else {
                            const hint = document.createElement('div');
                            hint.className = 'smartchat-account-hint';
                            hint.textContent = `Per-account storage: ${storageKey}`;
                            root.appendChild(hint);
                        }
                    } catch {
                        container.textContent = 'Smart Chat UI failed to load; see console for details.';
                    }
                };

                smartChatRenderSettings = doRender;
                doRender();
            }
        });
    }

    async function init() {
        injectStyles();

        await waitForElement('#pretty_top_level', 15000);
        await waitForElement('#pretty_top_name', 15000);

        storageKey = getStorageKeyV2();
        migrateV1ToV2IfNeeded();
        config = loadConfig();
        applyHighlightColorFromConfig();
        tryDetectMyName(true);

        const prettyNameEl = document.querySelector('#pretty_top_name');
        if (prettyNameEl) {
            const obs = new MutationObserver(() => updateAccountKeyFromPrettyTop());
            obs.observe(prettyNameEl, { childList: true, characterData: true, subtree: true });
        }

        const prettyTopLevelEl = document.querySelector('#pretty_top_level');
        if (prettyTopLevelEl) {
            const obs2 = new MutationObserver(() => updateAccountKeyFromPrettyTop());
            obs2.observe(prettyTopLevelEl, { childList: true, characterData: true, subtree: true });
        }

        updateAccountKeyFromPrettyTop();

        const lobbyChatContent = document.querySelector('#newbonklobby_chat_content');
        const lobbyInput = document.querySelector('#newbonklobby_chat_input');
        if (lobbyChatContent) observeChatContainer(lobbyChatContent);
        if (lobbyInput) {
            attachAntiSpamToInput(lobbyInput);
            attachEmojiToInput(lobbyInput);
        }

        const ingameContent = await waitForElement('#ingamechatcontent');
        const ingameInput = document.querySelector('#ingamechatinputtext');
        if (ingameContent) observeChatContainer(ingameContent);
        if (ingameInput) {
            attachAntiSpamToInput(ingameInput);
            attachEmojiToInput(ingameInput);
        }

        const globalObserver = new MutationObserver(() => {
            updateAccountKeyFromPrettyTop();

            const lc = document.querySelector('#newbonklobby_chat_content');
            if (lc) observeChatContainer(lc);

            const li = document.querySelector('#newbonklobby_chat_input');
            if (li) {
                attachAntiSpamToInput(li);
                attachEmojiToInput(li);
            }

            const gc = document.querySelector('#ingamechatcontent');
            if (gc) observeChatContainer(gc);

            const gi = document.querySelector('#ingamechatinputtext');
            if (gi) {
                attachAntiSpamToInput(gi);
                attachEmojiToInput(gi);
            }
        });
        globalObserver.observe(document.documentElement || document.body, { childList: true, subtree: true });

        if (window.bonkMods && window.bonkMods.addBlock) setupModSettings();
        else window.addEventListener('bonkModsReady', () => setupModSettings());
    }

    init();
})();