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).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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