PageSourceSearch

https://pupwife.com/projects/chatroom/scripts/settings.js

js pupwife.com collected 2026-09-30 15:06:54 UTC 115,848 bytes, 2,523 lines download raw bytes

1/**
2 * Settings System (Discord/Vencord-inspired)
3 * Manages user settings, themes, and preferences
4 */
5
6window.ChatroomSettings = (function() {
7    'use strict';
8
9    function emitDevLog() {
10        if (typeof window !== 'undefined' && typeof window.devLog === 'function') {
11            window.devLog.apply(window, arguments);
12        }
13    }
14
15    let settingsRef = null;
16    let currentUsername = null;
17    let settingsBooted = false;
18    let usernamePollId = null;
19    let usernamePollTimeoutId = null;
20    const PALETTE_THEMES = ['default', 'midnight', 'forest', 'sunset', 'banana', 'ocean', 'rose', 'sakura', 'purple'];
21
22    const CHAT_PALETTE_OPTIONS = [
23        { value: 'default', label: 'Default' },
24        { value: 'midnight', label: 'Midnight' },
25        { value: 'forest', label: 'Forest' },
26        { value: 'sunset', label: 'Sunset' },
27        { value: 'banana', label: 'Banana' },
28        { value: 'ocean', label: 'Ocean' },
29        { value: 'rose', label: 'Rose' },
30        { value: 'sakura', label: 'Sakura' },
31        { value: 'purple', label: 'Purple (Legacy)' }
32    ];
33
34    const CHAT_SETTINGS_TAB_DEFS = [
35        { id: 'appearance', label: 'Appearance', icon: 'fa-solid fa-palette' },
36        { id: 'messages', label: 'Messages', icon: 'fa-solid fa-comments' },
37        { id: 'notifications', label: 'Notifications', icon: 'fa-solid fa-bell' },
38        { id: 'voice', label: 'Voice & Chat', icon: 'fa-solid fa-microphone' },
39        { id: 'discord', label: 'Discord', icon: 'fa-brands fa-discord' },
40        { id: 'advanced', label: 'Advanced', icon: 'fa-solid fa-sliders' },
41        { id: 'moderation', label: 'Moderation', icon: 'fa-solid fa-shield-halved', modOnly: true }
42    ];
43
44    let activeChatSettingsTab = 'appearance';
45
46    let settings = {
47        theme: 'default',
48        colorMode: 'dark',
49        messageGrouping: true,
50        showTimestamps: true,
51        showAvatars: true,
52        fontSize: 'medium',
53        emojiSize: 'medium',
54        animations: true,
55        soundEffects: false,
56        notifications: false,
57        desktopNotifications: false,
58        enterToSend: true,
59        muteOnJoinVoice: false,
60        playJoinLeaveSounds: true,
61        autoScrollNewMessages: true,
62        showLinkPreviews: true,
63        showOnlineStatus: true,
64        confirmBeforeLeavingVoice: true,
65        useLanyardStatus: false,
66        discordUserId: ''
67    };
68
69    /**
70     * Initialize settings system
71     */
72    function init() {
73        if (settingsBooted) {
74            applySettings();
75            return true;
76        }
77        settingsBooted = true;
78
79        if (typeof firebase === 'undefined' || !firebase.database) {
80            console.error('Firebase not available for settings');
81            loadLocalSettings();
82            return false;
83        }
84
85        settingsRef = firebase.database().ref('chatroom/settings');
86
87        // Wait for username
88        usernamePollId = setInterval(() => {
89            if (window.__sharedUsername) {
90                currentUsername = window.__sharedUsername.get();
91                if (currentUsername) {
92                    clearInterval(usernamePollId);
93                    usernamePollId = null;
94                    loadSettings();
95                    setupUI();
96                }
97            }
98        }, 100);
99
100        usernamePollTimeoutId = setTimeout(() => {
101            usernamePollTimeoutId = null;
102            if (usernamePollId) {
103                clearInterval(usernamePollId);
104                usernamePollId = null;
105            }
106            if (!currentUsername) {
107                currentUsername = 'guest_' + Date.now();
108                loadLocalSettings();
109                setupUI();
110            }
111        }, 5000);
112
113        return true;
114    }
115
116    /**
117     * Load settings from Firebase
118     */
119    function loadSettings() {
120        // First load from localStorage (fast)
121        loadLocalSettings();
122        
123        // Then sync from Firebase (if available)
124        if (!settingsRef || !currentUsername) {
125            return;
126        }
127
128        settingsRef.child(currentUsername).once('value', (snapshot) => {
129            if (snapshot.exists()) {
130                const saved = snapshot.val();
131                Object.assign(settings, saved);
132                // Update localStorage with Firebase data
133                try {
134                    localStorage.setItem('chatroom_settings', JSON.stringify(settings));
135                } catch (e) {
136                    console.error('Error saving settings to localStorage:', e);
137                }
138            }
139            hydrateThemeFromSite();
140            syncDiscordLinkToProfile();
141            applySettings();
142        });
143    }
144
145    function migrateThemeSettings() {
146        if (settings.theme === 'dark') {
147            settings.theme = 'default';
148            settings.colorMode = 'dark';
149        } else if (settings.theme === 'light') {
150            settings.theme = 'default';
151            settings.colorMode = 'light';
152        }
153        if (!settings.colorMode) {
154            settings.colorMode = 'dark';
155        }
156        if (!PALETTE_THEMES.includes(settings.theme)) {
157            settings.theme = 'default';
158        }
159    }
160
161    /** Sitewide palette + light/dark are owned by SiteThemePalettes / SiteThemeToggle. */
162    function hydrateThemeFromSite() {
163        migrateThemeSettings();
164        if (window.SiteThemePalettes && typeof window.SiteThemePalettes.readMergedThemeSettings === 'function') {
165            const siteTheme = window.SiteThemePalettes.readMergedThemeSettings();
166            settings.theme = siteTheme.palette;
167            settings.colorMode = siteTheme.colorMode;
168            return;
169        }
170        syncColorModeFromSiteSettings();
171    }
172
173    function pushThemeToSite(source, patch) {
174        const src = source || 'chatroom-settings';
175        const palette = patch && patch.palette !== undefined ? patch.palette : settings.theme;
176        const colorMode = patch && patch.colorMode !== undefined
177            ? (patch.colorMode === 'light' ? 'light' : 'dark')
178            : (settings.colorMode === 'light' ? 'light' : 'dark');
179        if (window.SiteThemeToggle) {
180            if ((!patch || patch.palette !== undefined) && typeof window.SiteThemeToggle.setPalette === 'function') {
181                window.SiteThemeToggle.setPalette(palette, src);
182            }
183            if ((!patch || patch.colorMode !== undefined) && typeof window.SiteThemeToggle.setColorMode === 'function') {
184                window.SiteThemeToggle.setColorMode(colorMode === 'dark', src);
185            }
186            return;
187        }
188        if (window.SiteThemePalettes && typeof window.SiteThemePalettes.writeMergedThemeSettings === 'function') {
189            const writePatch = {};
190            if (!patch || patch.palette !== undefined) writePatch.palette = palette;
191            if (!patch || patch.colorMode !== undefined) writePatch.colorMode = colorMode;
192            if (Object.keys(writePatch).length === 0) {
193                writePatch.palette = palette;
194                writePatch.colorMode = colorMode;
195            }
196            window.SiteThemePalettes.writeMergedThemeSettings(writePatch);
197        }
198    }
199
200    function syncColorModeFromSiteSettings() {
201        try {
202            const raw = localStorage.getItem('puppywifey_settings');
203            if (!raw) return;
204            const site = JSON.parse(raw);
205            if (typeof site.darkMode === 'boolean') {
206                settings.colorMode = site.darkMode ? 'dark' : 'light';
207            }
208        } catch (e) { /* ignore */ }
209    }
210
211    /**
212     * Load settings from localStorage
213     */
214    function loadLocalSettings() {
215        try {
216            const saved = localStorage.getItem('chatroom_settings');
217            if (saved) {
218                Object.assign(settings, JSON.parse(saved));
219            }
220        } catch (e) {
221            console.error('Error loading local settings:', e);
222        }
223        hydrateThemeFromSite();
224        applySettings();
225    }
226
227    /**
228     * Save settings
229     */
230    function saveSettings() {
231        // Save to localStorage first (immediate)
232        try {
233            localStorage.setItem('chatroom_settings', JSON.stringify(settings));
234        } catch (e) {
235            console.error('Error saving local settings:', e);
236        }
237
238        // Then save to Firebase (async)
239        if (settingsRef && currentUsername) {
240            settingsRef.child(currentUsername).set(settings).catch(error => {
241                console.error('Error saving settings to Firebase:', error);
242            });
243        }
244
245        syncDiscordLinkToProfile();
246        applySettings();
247    }
248
249    function syncDiscordLinkToProfile() {
250        if (!window.ChatroomProfilePicture || !window.ChatroomProfilePicture.setDiscordLink) return;
251        const id = String(settings.discordUserId || '').replace(/\D/g, '');
252        const enabled = settings.useLanyardStatus === true && id.length >= 17;
253        window.ChatroomProfilePicture.setDiscordLink(enabled ? id : '', enabled);
254    }
255
256    function getChatroomPageWrap() {
257        if (window.ChatroomSpa && typeof window.ChatroomSpa.getPageWrap === 'function') {
258            return window.ChatroomSpa.getPageWrap();
259        }
260        if (window.CitrusRouteScope && typeof window.CitrusRouteScope.isCitrusHost === 'function' &&
261            window.CitrusRouteScope.isCitrusHost()) {
262            var scoped = document.querySelector(
263                '.citrus-window__content.page-chatroom #page-wrap, ' +
264                '.citrus-fullscreen__content.page-chatroom #page-wrap, ' +
265                '.citrus-window__content #page-wrap[data-chatroom-layout], ' +
266                '.citrus-fullscreen__content #page-wrap[data-chatroom-layout], ' +
267                '.citrus-window__content [data-citrus-app-root]#page-wrap, ' +
268                '.citrus-fullscreen__content [data-citrus-app-root]#page-wrap'
269            );
270            if (scoped) return scoped;
271        }
272        return document.getElementById('page-wrap');
273    }
274
275    function getCitrusChatContainer() {
276        return document.querySelector(
277            '.citrus-window__content.page-chatroom, .citrus-fullscreen__content.page-chatroom, ' +
278            '.citrus-window__content #page-wrap[data-chatroom-layout], ' +
279            '.citrus-fullscreen__content #page-wrap[data-chatroom-layout]'
280        );
281    }
282
283    function hasMountedCitrusChatShell() {
284        if (window.ChatroomSpa && typeof window.ChatroomSpa.hasMountedCitrusChat === 'function') {
285            return window.ChatroomSpa.hasMountedCitrusChat();
286        }
287        return !!getCitrusChatContainer();
288    }
289
290    /**
291     * Apply settings to UI
292     */
293    function getChatroomThemeRoot() {
294        return getChatroomPageWrap() || document.documentElement;
295    }
296
297    function isChatroomRouteActive() {
298        if (window.CitrusRouteScope && typeof window.CitrusRouteScope.isCitrusHost === 'function' &&
299            window.CitrusRouteScope.isCitrusHost()) {
300            if (typeof window.CitrusRouteScope.hasClass === 'function' &&
301                window.CitrusRouteScope.hasClass('page-chatroom')) {
302                return true;
303            }
304            var wrap = getChatroomPageWrap();
305            return !!(wrap && isChatroomWrapInCitrusWindow(wrap));
306        }
307        return !!(document.body && document.body.classList.contains('page-chatroom'));
308    }
309
310    function isChatroomWrapInCitrusWindow(wrap) {
311        if (!wrap || !wrap.closest) return false;
312        return !!wrap.closest(
313            '.citrus-window__content.page-chatroom, .citrus-fullscreen__content.page-chatroom, ' +
314            '.citrus-window__content, .citrus-fullscreen__content'
315        );
316    }
317
318    function clearChatroomThemeAttrsFromShell() {
319        var wrap = getChatroomPageWrap();
320        var targets = [wrap, document.body, document.documentElement];
321        var attrs = [
322            'data-chatroom-theme',
323            'data-chatroom-color-mode',
324            'data-chatroom-font-size',
325            'data-chatroom-emoji-size'
326        ];
327        var classes = [
328            'animations',
329            'message-grouping',
330            'show-timestamps',
331            'show-avatars',
332            'show-link-previews',
333            'show-online-status',
334            'hide-online-status'
335        ];
336        var i;
337        var j;
338        for (i = 0; i < targets.length; i++) {
339            var el = targets[i];
340            if (!el) continue;
341            for (j = 0; j < attrs.length; j++) {
342                el.removeAttribute(attrs[j]);
343            }
344            for (j = 0; j < classes.length; j++) {
345                el.classList.remove(classes[j]);
346            }
347        }
348        if (document.body) {
349            document.body.style.fontSize = '';
350            document.body.style.fontFamily = '';
351        }
352    }
353
354    function applySettings(opts) {
355        opts = opts || {};
356        hydrateThemeFromSite();
357        const theme = PALETTE_THEMES.includes(settings.theme) ? settings.theme : 'default';
358        const colorMode = settings.colorMode === 'light' ? 'light' : 'dark';
359        settings.theme = theme;
360        settings.colorMode = colorMode;
361
362        var routeActive = isChatroomRouteActive();
363        if (!routeActive) {
364            var activeWrap = getChatroomPageWrap();
365            routeActive = isChatroomWrapInCitrusWindow(activeWrap);
366        }
367        if (!routeActive && getCitrusChatContainer()) {
368            routeActive = true;
369        }
370        if (!routeActive && hasMountedCitrusChatShell()) {
371            routeActive = true;
372        }
373
374        if (!routeActive) {
375            if (window.ChatroomSpa && typeof window.ChatroomSpa.clearChatroomTheme === 'function') {
376                window.ChatroomSpa.clearChatroomTheme(null, { force: true });
377            } else {
378                clearChatroomThemeAttrsFromShell();
379            }
380            if (typeof window.refreshSidebarNavChrome === 'function') {
381                window.refreshSidebarNavChrome();
382            } else if (typeof window.ensureSidebarNavStyles === 'function') {
383                window.ensureSidebarNavStyles();
384            }
385            document.dispatchEvent(new CustomEvent('chatroom-messages-layout-refresh'));
386            emitDevLog('✅ Settings applied (chatroom theme skipped — not on chatroom route)');
387            return;
388        }
389
390        var themeRoot = getChatroomThemeRoot();
391        themeRoot.setAttribute('data-chatroom-theme', theme);
392        themeRoot.setAttribute('data-chatroom-color-mode', colorMode);
393        if (document.body && document.body.classList.contains('page-citrusos')) {
394            document.body.setAttribute('data-chatroom-theme', theme);
395            document.body.setAttribute('data-chatroom-color-mode', colorMode);
396        }
397
398        if (window.ChatroomSpa && typeof window.ChatroomSpa.scheduleChatroomThemeRecovery === 'function') {
399            window.ChatroomSpa.scheduleChatroomThemeRecovery();
400            if (typeof window.ChatroomSpa.refreshChatroomScrollLayout === 'function') {
401                window.ChatroomSpa.refreshChatroomScrollLayout();
402            }
403        } else if (window.ChatroomSpa && typeof window.ChatroomSpa.scheduleChatroomThemeApply === 'function') {
404            window.ChatroomSpa.scheduleChatroomThemeApply();
405            if (typeof window.ChatroomSpa.refreshChatroomScrollLayout === 'function') {
406                window.ChatroomSpa.refreshChatroomScrollLayout();
407            }
408        } else if (window.ChatroomSpa && typeof window.ChatroomSpa.applyChatroomTheme === 'function') {
409            window.ChatroomSpa.applyChatroomTheme();
410            if (typeof window.ChatroomSpa.refreshChatroomScrollLayout === 'function') {
411                window.ChatroomSpa.refreshChatroomScrollLayout();
412            }
413        }
414
415        // Apply message grouping and other toggles (scoped to chatroom shell)
416        themeRoot.classList.toggle('message-grouping', settings.messageGrouping !== false);
417        themeRoot.classList.toggle('show-timestamps', settings.showTimestamps === true);
418        themeRoot.classList.toggle('show-avatars', settings.showAvatars === true);
419        themeRoot.classList.toggle('animations', settings.animations === true);
420
421        // Apply font size
422        if (settings.fontSize) {
423            themeRoot.setAttribute('data-chatroom-font-size', settings.fontSize);
424        } else {
425            themeRoot.setAttribute('data-chatroom-font-size', 'medium');
426        }
427
428        // Apply emoji size
429        if (settings.emojiSize) {
430            themeRoot.setAttribute('data-chatroom-emoji-size', settings.emojiSize);
431        } else {
432            themeRoot.setAttribute('data-chatroom-emoji-size', 'medium');
433        }
434
435        // Toggles for chatroom behavior (used by other scripts)
436        themeRoot.classList.toggle('show-link-previews', settings.showLinkPreviews === true);
437        themeRoot.classList.toggle('show-online-status', settings.showOnlineStatus !== false);
438        themeRoot.classList.toggle('hide-online-status', settings.showOnlineStatus === false);
439
440        if (window.ChatroomSelectionStyle && typeof window.ChatroomSelectionStyle.scheduleReinject === 'function') {
441            window.ChatroomSelectionStyle.scheduleReinject();
442        } else if (window.ChatroomSelectionStyle && typeof window.ChatroomSelectionStyle.inject === 'function') {
443            window.ChatroomSelectionStyle.inject();
444        }
445
446        document.dispatchEvent(new CustomEvent('chatroom-messages-layout-refresh'));
447
448        emitDevLog('✅ Settings applied:', {
449            theme: settings.theme,
450            colorMode: settings.colorMode,
451            fontSize: settings.fontSize,
452        });
453    }
454
455    function isCitrusOsHost() {
456        return !!(document.body && document.body.classList.contains('page-citrusos'));
457    }
458
459    function isSettingsInAuxWindow() {
460        const modal = document.getElementById('settings-modal');
461        return !!(modal && modal.closest('.chatroom-citrus-aux-host'));
462    }
463
464    function ensureCitrusSettingsCss() {
465        if (!isCitrusOsHost()) return;
466        if (document.querySelector('link[data-citrus-chat-settings-css]')) return;
467        ['/css/citrusSettings.css', '/css/citrusChatroomSettings.css'].forEach(function (href) {
468            if (document.querySelector('link[href="' + href + '"]')) return;
469            const link = document.createElement('link');
470            link.rel = 'stylesheet';
471            link.href = href;
472            link.setAttribute('data-citrus-chat-settings-css', '1');
473            document.head.appendChild(link);
474        });
475    }
476
477    function getCategoryPanel(category) {
478        if (isCitrusOsHost()) {
479            return document.querySelector('[data-chatroom-settings-panel="' + category + '"]');
480        }
481        return document.getElementById('chatroom-settings-panel');
482    }
483
484    function setActiveChatSettingsTab(category) {
485        if (!isCitrusOsHost()) return;
486        activeChatSettingsTab = category || 'appearance';
487        document.querySelectorAll('[data-chatroom-settings-tab]').forEach(function (btn) {
488            const on = btn.getAttribute('data-chatroom-settings-tab') === activeChatSettingsTab;
489            btn.classList.toggle('is-active', on);
490            btn.setAttribute('aria-selected', on ? 'true' : 'false');
491        });
492        document.querySelectorAll('[data-chatroom-settings-panel]').forEach(function (panelEl) {
493            const on = panelEl.getAttribute('data-chatroom-settings-panel') === activeChatSettingsTab;
494            panelEl.classList.toggle('is-active', on);
495            panelEl.hidden = !on;
496        });
497    }
498
499    function syncModerationTabVisibility() {
500        const isModerator = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator();
501        const legacyBtn = document.getElementById('moderation-category-btn');
502        if (legacyBtn) {
503            legacyBtn.style.display = isModerator ? 'block' : 'none';
504        }
505        const citrusBtn = document.querySelector('[data-chatroom-settings-tab="moderation"]');
506        if (citrusBtn) {
507            citrusBtn.hidden = !isModerator;
508        }
509    }
510
511    function settingsFa(icon) {
512        return '<i class="fa-solid fa-' + icon + ' fa-width-auto settings-row__icon" aria-hidden="true"></i>';
513    }
514
515    function escapeSettingsHtml(text) {
516        if (text == null) return '';
517        const div = document.createElement('div');
518        div.textContent = String(text);
519        return div.innerHTML;
520    }
521
522    function createChatroomSettingsSection(title, faIcon) {
523        const section = document.createElement('section');
524        section.className = 'settings-section chatroom-settings-section' +
525            (isCitrusOsHost() ? ' settings-section--compact' : '');
526        const heading = document.createElement('h3');
527        heading.className = 'settings-section__title';
528        heading.innerHTML = settingsFa(faIcon) + ' <span>' + escapeSettingsHtml(title) + '</span>';
529        section.appendChild(heading);
530        return section;
531    }
532
533    function createChatroomToggle(opts) {
534        const wrap = document.createElement('div');
535        wrap.className = 'chatroom-settings-toggle-wrap';
536        const label = document.createElement('label');
537        label.className = 'settings-row settings-row--toggle';
538        label.innerHTML =
539            '<span class="settings-row__label">' +
540            settingsFa(opts.icon) +
541            '<span>' + escapeSettingsHtml(opts.label) + '</span></span>' +
542            '<span class="settings-switch">' +
543            '<input type="checkbox" id="' + opts.id + '" class="settings-switch__input"' +
544            (opts.checked ? ' checked' : '') + '>' +
545            '<span class="settings-switch__track" aria-hidden="true"></span></span>';
546        label.querySelector('input').addEventListener('change', function (e) {
547            opts.onChange(e.target.checked);
548        });
549        wrap.appendChild(label);
550        if (opts.hint) {
551            const hint = document.createElement('p');
552            hint.className = 'settings-section__hint chatroom-settings-hint';
553            hint.textContent = opts.hint;
554            wrap.appendChild(hint);
555        }
556        return wrap;
557    }
558
559    function createChatroomSelectField(opts) {
560        const field = document.createElement('div');
561        field.className = 'settings-field chatroom-settings-field';
562        const label = document.createElement('span');
563        label.className = 'settings-field__label';
564        label.innerHTML = settingsFa(opts.icon) + ' <span>
564' + escapeSettingsHtml(opts.label) + '</span>';
565        field.appendChild(label);
566        const select = document.createElement('select');
567        select.id = opts.id;
568        select.className = 'settings-select chatroom-settings-select';
569        opts.options.forEach(function (opt) {
570            const option = document.createElement('option');
571            option.value = opt.value;
572            option.textContent = opt.label;
573            if (opt.value === opts.value) option.selected = true;
574            select.appendChild(option);
575        });
576        select.addEventListener('change', function () {
577            opts.onChange(select.value);
578        });
579        field.appendChild(select);
580        if (opts.hint) {
581            const hint = document.createElement('p');
582            hint.className = 'settings-section__hint chatroom-settings-hint';
583            hint.textContent = opts.hint;
584            field.appendChild(hint);
585        }
586        return field;
587    }
588
589    function createVoiceVolumeSlider(opts) {
590        const wrap = document.createElement('div');
591        wrap.className = 'voice-settings-volume';
592        const label = document.createElement('label');
593        label.className = 'voice-settings-volume__label';
594        label.setAttribute('for', opts.id);
595        label.textContent = opts.label;
596        wrap.appendChild(label);
597        const slider = document.createElement('input');
598        slider.type = 'range';
599        slider.id = opts.id;
600        slider.className = 'voice-settings-volume__slider';
601        slider.min = '0';
602        slider.max = '200';
603        slider.step = '1';
604        slider.value = String(opts.value);
605        slider.addEventListener('input', function () {
606            opts.onInput(Number(slider.value));
607        });
608        wrap.appendChild(slider);
609        return wrap;
610    }
611
612    let voiceDeviceSettingsRoot = null;
613    let voiceDeviceListsLoaded = false;
614
615    async function refreshVoiceDeviceSelects() {
616        const VC = window.ChatroomVoiceChat;
617        if (!VC || !voiceDeviceSettingsRoot) return;
618
619        const inputSelect = voiceDeviceSettingsRoot.querySelector('#voice-setting-input-device');
620        const outputSelect = voiceDeviceSettingsRoot.querySelector('#voice-setting-output-device');
621        if (!inputSelect || !outputSelect) return;
622
623        if (VC.ensureDeviceLabels && !voiceDeviceListsLoaded) {
624            await VC.ensureDeviceLabels();
625        }
626
627        let deviceLists = { inputs: [], outputs: [] };
628        if (VC.enumerateVoiceDevices) {
629            deviceLists = await VC.enumerateVoiceDevices();
630        }
631
632        const saved = VC.getVoiceDeviceSettings ? VC.getVoiceDeviceSettings() : {
633            inputDeviceId: '',
634            outputDeviceId: '',
635            inputVolume: 100,
636            outputVolume: 100
637        };
638
639        function fillSelect(select, items, savedId, defaultLabel) {
640            const previous = select.value;
641            select.innerHTML = '';
642            const defaultOpt = document.createElement('option');
643            defaultOpt.value = '';
644            defaultOpt.textContent = defaultLabel;
645            select.appendChild(defaultOpt);
646            items.forEach(function (item) {
647                const opt = document.createElement('option');
648                opt.value = item.deviceId;
649                opt.textContent = item.label;
650                if (item.deviceId === savedId) opt.selected = true;
651                select.appendChild(opt);
652            });
653            if (previous) {
654                for (let i = 0; i < select.options.length; i++) {
655                    if (select.options[i].value === previous) {
656                        select.value = previous;
657                        break;
658                    }
659                }
660            }
661        }
662
663        fillSelect(inputSelect, deviceLists.inputs, saved.inputDeviceId, 'Default microphone');
664        fillSelect(outputSelect, deviceLists.outputs, saved.outputDeviceId, 'Default speaker');
665        voiceDeviceListsLoaded = true;
666    }
667
668    function buildVoiceDeviceSettingsRoot() {
669        const VC = window.ChatroomVoiceChat;
670        const saved = VC && VC.getVoiceDeviceSettings ? VC.getVoiceDeviceSettings() : {
671            inputDeviceId: '',
672            outputDeviceId: '',
673            inputVolume: 100,
674            outputVolume: 100
675        };
676
677        const devicesSection = document.createElement('div');
678        devicesSection.className = 'voice-settings-devices';
679
680        const grid = document.createElement('div');
681        grid.className = 'voice-settings-devices__grid';
682
683        const inputCol = document.createElement('div');
684        inputCol.className = 'voice-settings-devices__col';
685        const outputCol = document.createElement('div');
686        outputCol.className = 'voice-settings-devices__col';
687
688        const inputSelect = document.createElement('select');
689        inputSelect.id = 'voice-setting-input-device';
690        inputSelect.className = 'settings-select chatroom-settings-select';
691        inputSelect.innerHTML = '<option value="">Default microphone</option>';
692
693        const outputSelect = document.createElement('select');
694        outputSelect.id = 'voice-setting-output-device';
695        outputSelect.className = 'settings-select chatroom-settings-select';
696        outputSelect.innerHTML = '<option value="">Default speaker</option>';
697
698        inputCol.innerHTML = '<span class="voice-settings-devices__heading">Microphone</span>';
699        inputCol.appendChild(inputSelect);
700        outputCol.innerHTML = '<span class="voice-settings-devices__heading">
700Speaker</span>';
701        outputCol.appendChild(outputSelect);
702
703        grid.appendChild(inputCol);
704        grid.appendChild(outputCol);
705        devicesSection.appendChild(grid);
706
707        const volumeGrid = document.createElement('div');
708        volumeGrid.className = 'voice-settings-devices__grid voice-settings-devices__grid--volumes';
709        volumeGrid.appendChild(createVoiceVolumeSlider({
710            id: 'voice-setting-input-volume',
711            label: 'Input volume',
712            value: saved.inputVolume,
713            onInput: function (val) {
714                if (VC && VC.setInputVolume) VC.setInputVolume(val);
715            }
716        }));
717        volumeGrid.appendChild(createVoiceVolumeSlider({
718            id: 'voice-setting-output-volume',
719            label: 'Output volume',
720            value: saved.outputVolume,
721            onInput: function (val) {
722                if (VC && VC.setOutputVolume) VC.setOutputVolume(val);
723            }
724        }));
725        devicesSection.appendChild(volumeGrid);
726
727        const micTestRow = document.createElement('div');
728        micTestRow.className = 'voice-settings-mic-test';
729        const micTestBtn = document.createElement('button');
730        micTestBtn.type = 'button';
731        micTestBtn.id = 'voice-setting-mic-test-btn';
732        micTestBtn.className = 'voice-settings-mic-test__btn';
733        micTestBtn.textContent = 'Mic Test';
734        const micMeter = document.createElement('div');
735        micMeter.className = 'voice-settings-mic-test__meter';
736        micMeter.setAttribute('aria-hidden', 'true');
737        for (let i = 0; i < 32; i++) {
738            const bar = document.createElement('span');
739            bar.className = 'voice-settings-mic-test__bar';
740            micMeter.appendChild(bar);
741        }
742        micTestRow.appendChild(micTestBtn);
743        micTestRow.appendChild(micMeter);
744        devicesSection.appendChild(micTestRow);
745
746        inputSelect.addEventListener('change', async function () {
747            if (VC && VC.applyInputDevice) {
748                const wasTesting = VC.isMicTestRunning && VC.isMicTestRunning();
749                try {
750                    await VC.applyInputDevice(inputSelect.value);
751                    if (wasTesting) {
752                        micTestBtn.textContent = 'Stop Test';
753                        micTestBtn.classList.add('is-active');
754                    }
755                } catch (err) {
756                    console.error('Could not switch microphone:', err);
757                    alert('Could not switch microphone. Check permissions and try again.');
758                    if (wasTesting) {
759                        micTestBtn.textContent = 'Mic Test';
760                        micTestBtn.classList.remove('is-active');
761                    }
762                }
763            }
764        });
765
766        outputSelect.addEventListener('change', async function () {
767            if (VC && VC.applyOutputDevice) {
768                try {
769                    await VC.applyOutputDevice(outputSelect.value);
770                } catch (err) {
771                    console.error('Could not switch speaker:', err);
772                }
773            }
774        });
775
776        if (typeof HTMLMediaElement.prototype.setSinkId !== 'function') {
777            outputSelect.disabled = true;
778            outputCol.title = 'Speaker selection is not supported in this browser.';
779        }
780
781        const meterBars = micMeter.querySelectorAll('.voice-settings-mic-test__bar');
782        micTestBtn.addEventListener('click', async function () {
783            if (!VC || !VC.startMicTest) return;
784            if (VC.isMicTestRunning && VC.isMicTestRunning()) {
785                VC.stopMicTest();
786                micTestBtn.textContent = 'Mic Test';
787                micTestBtn.classList.remove('is-active');
788                meterBars.forEach(function (bar) {
789                    bar.style.height = '4px';
790                    bar.classList.remove('is-active');
791                });
792                return;
793            }
794            try {
795                micTestBtn.textContent = 'Stop Test';
796                micTestBtn.classList.add('is-active');
797                await VC.startMicTest(function (level) {
798                    const activeBars = Math.round(level * meterBars.length);
799                    meterBars.forEach(function (bar, index) {
800                        if (index < activeBars) {
801                            bar.style.height = (4 + Math.round(level * 28)) + 'px';
802                            bar.classList.add('is-active');
803                        } else {
804                            bar.style.height = '4px';
805                            bar.classList.remove('is-active');
806                        }
807                    });
808                });
809            } catch (err) {
810                console.error('Mic test failed:', err);
811                alert('Could not start mic test. Check microphone permissions.');
812                micTestBtn.textContent = 'Mic Test';
813                micTestBtn.classList.remove('is-active');
814            }
815        });
816
817        return devicesSection;
818    }
819
820    function appendVoiceDeviceSettings(voiceSection) {
821        if (!voiceDeviceSettingsRoot) {
822            voiceDeviceSettingsRoot = buildVoiceDeviceSettingsRoot();
823        }
824        voiceSection.appendChild(voiceDeviceSettingsRoot);
825        void refreshVoiceDeviceSelects();
826    }
827
828    function bindSidebarCategoryIcons() {
829        const map = {
830            appearance: ['palette', 'Appearance'],
831            messages: ['comments', 'Messages'],
832            notifications: ['bell', 'Notifications'],
833            voice: ['microphone', 'Voice & Chat'],
834            discord: ['tower-broadcast', 'Discord Status'],
835            advanced: ['sliders', 'Advanced'],
836            moderation: ['shield-halved', 'Moderation']
837        };
838        document.querySelectorAll('.settings-category[data-category]').forEach(function (btn) {
839            const entry = map[btn.dataset.category] || ['gear', btn.dataset.category];
840            btn.innerHTML = settingsFa(entry[0]) + ' <span>' + escapeSettingsHtml(entry[1]) + '</span>';
841            btn.classList.add('chatroom-settings-category');
842        });
843    }
844
845    function dispatchColorModeSync() {
846        document.dispatchEvent(new CustomEvent('pupwife-color-mode', {
847            detail: {
848                colorMode: settings.colorMode,
849                darkMode: settings.colorMode === 'dark',
850                source: 'chatroom-settings'
851            }
852        }));
853    }
854
855    function setColorMode(mode, options) {
856        const opts = options || {};
857        settings.colorMode = mode === 'light' ? 'light' : 'dark';
858        pushThemeToSite('chatroom-settings', { colorMode: settings.colorMode });
859        saveSettings();
860        const toggle = document.getElementById('setting-color-mode');
861        if (toggle) {
862            toggle.checked = settings.colorMode === 'dark';
863        }
864        if (!opts.skipDispatch) {
865            dispatchColorModeSync();
866        }
867    }
868
869    function getColorMode() {
870        return settings.colorMode === 'light' ? 'light' : 'dark';
871    }
872
873    /**
874     * Setup settings UI
875     */
876    function setupUI() {
877        const settingsBtn = document.getElementById('settings-btn');
878        const settingsModal = document.getElementById('settings-modal');
879
880        if (settingsBtn) {
881            settingsBtn.addEventListener('click', () => {
882                showSettings();
883            });
884        }
885
886        // Close settings on outside click
887        if (settingsModal) {
888            settingsModal.addEventListener('click', (e) => {
889                // Only close if clicking directly on the modal backdrop, not on content
890                if (e.target === settingsModal || e.target.classList.contains('settings-modal')) {
891                    hideSettings();
892                }
893            });
894        }
895    }
896
897    /**
898     * Rebuild settings shell when an older cached modal is missing newer categories.
899     */
900    function ensureSettingsModal() {
901        let settingsModal = document.getElementById('settings-modal');
902        const wantsCitrus = isCitrusOsHost();
903        const hasCitrusShell = !!(settingsModal && settingsModal.classList.contains('chatroom-settings--citrus'));
904        const hasDiscordCategory = !!document.querySelector('.settings-category[data-category="discord"]');
905        const needsRebuild = settingsModal && (
906            (wantsCitrus && !hasCitrusShell) ||
907            (!wantsCitrus && hasCitrusShell) ||
908            (!wantsCitrus && !hasDiscordCategory)
909        );
910        if (needsRebuild) {
911            settingsModal.remove();
912            settingsModal = null;
913        }
914        if (!settingsModal) {
915            createSettingsModal();
916            settingsModal = document.getElementById('settings-modal');
917        }
918        return settingsModal;
919    }
920
921    /**
922     * Show settings panel
923     */
924    function showSettings() {
925        ensureCitrusSettingsCss();
926        const settingsModal = ensureSettingsModal();
927        if (settingsModal) {
928            if (isCitrusOsHost()) {
929                settingsModal.hidden = false;
930                settingsModal.style.display = 'flex';
931                setActiveChatSettingsTab(activeChatSettingsTab);
932                showCategory(activeChatSettingsTab);
933            } else {
934                settingsModal.style.display = 'flex';
935            }
936            populateSettingsForm();
937        }
938
939        const colorToggle = document.getElementById('setting-color-mode');
940        if (colorToggle) {
941            colorToggle.checked = getColorMode() === 'dark';
942        }
943
944        syncModerationTabVisibility();
945
946        if (!isCitrusOsHost()) {
947            document.body.classList.add('settings-open');
948            const mainContent = document.querySelector('.main-content-area');
949            if (mainContent) {
950                mainContent.classList.add('settings-open');
951            }
952            const sidebar = document.querySelector('.sidebar');
953            if (sidebar) {
954                sidebar.classList.add('settings-open');
955            }
956        }
957        
958        // Re-attach close button handler in case modal was reused
959        // Use event delegation on the modal - get the modal again (might be different instance)
960        const modalForClose = document.getElementById('settings-modal');
961        if (modalForClose) {
962            // Add delegation handler directly (don't clone, just add listener)
963            modalForClose.addEventListener('click', function modalClickHandler(e) {
964                const closeBtn = document.getElementById('chatroom-settings-close');
965                if (closeBtn && (e.target === closeBtn || closeBtn.contains(e.target))) {
966                    e.preventDefault();
967                    e.stopPropagation();
968                    e.stopImmediatePropagation();
969                    emitDevLog('Close button clicked via delegation (showSettings)');
970                    hideSettings();
971                    return false;
972                }
973            }, true);
974            
975            // Also attach directly to button
976            setTimeout(() => {
977                const closeBtn = document.getElementById('chatroom-settings-close');
978                if (closeBtn) {
979                    closeBtn.addEventListener('click', function directHandler(e) {
980                        e.preventDefault();
981                        e.stopPropagation();
982                        e.stopImmediatePropagation();
983                        emitDevLog('Close button clicked directly (showSettings)');
984                        hideSettings();
985                        return false;
986                    }, true);
987                }
988            }, 0);
989        }
990        
991        // Add Escape key handler
992        const escapeHandler = (e) => {
993            if (e.key === 'Escape') {
994                hideSettings();
995                document.removeEventListener('keydown', escapeHandler);
996            }
997        };
998        document.addEventListener('keydown', escapeHandler);
999    }
1000
1001    /**
1002     * Hide settings panel
1003     */
1004    function hideSettings() {
1005        if (isCitrusOsHost() && isSettingsInAuxWindow()) {
1006            if (window.ChatroomCitrusMenus && typeof window.ChatroomCitrusMenus.closeAuxPanel === 'function') {
1007                window.ChatroomCitrusMenus.closeAuxPanel('settings');
1008                return;
1009            }
1010        }
1011
1012        const settingsModal = document.getElementById('settings-modal');
1013        if (settingsModal) {
1014            settingsModal.style.display = 'none';
1015            settingsModal.hidden = true;
1016        }
1017
1018        document.body.classList.remove('settings-open');
1019        const mainContent = document.querySelector('.main-content-area');
1020        if (mainContent) {
1021            mainContent.classList.remove('settings-open');
1022        }
1023        const sidebar = document.querySelector('.sidebar');
1024        if (sidebar) {
1025            sidebar.classList.remove('settings-open');
1026        }
1027    }
1028
1029    /**
1030     * Create settings modal
1031     */
1032    function createSettingsModal() {
1033        if (isCitrusOsHost()) {
1034            createCitrusSettingsModal();
1035            return;
1036        }
1037        createLegacySettingsModal();
1038    }
1039
1040    function createCitrusSettingsModal() {
1041        const modal = document.createElement('div');
1042        modal.id = 'settings-modal';
1043        modal.className = 'settings-modal chatroom-settings--citrus';
1044        modal.hidden = true;
1045
1046        const shell = document.createElement('div');
1047        shell.className = 'citrus-settings-window chatroom-settings-shell';
1048
1049        const tabsRoot = document.createElement('div');
1050        tabsRoot.className = 'citrus-settings-tabs';
1051        tabsRoot.setAttribute('data-chatroom-settings-tabs', '1');
1052
1053        const tabsBody = document.createElement('div');
1054        tabsBody.className = 'citrus-settings-tabs__body';
1055
1056        const nav = document.createElement('nav');
1057        nav.className = 'citrus-settings-tabs__nav';
1058        nav.setAttribute('role', 'tablist');
1059        nav.setAttribute('aria-label', 'Chat settings categories');
1060
1061        const panelsWrap = document.createElement('div');
1062        panelsWrap.className = 'citrus-settings-tabs__panels';
1063
1064        CHAT_SETTINGS_TAB_DEFS.forEach(function (def, index) {
1065            const btnId = 'chatroom-settings-tab-' + def.id;
1066            const panelId = 'chatroom-settings-panel-' + def.id;
1067
1068            const btn = document.createElement('button');
1069            btn.type = 'button';
1070            btn.className = 'citrus-settings-tabs__tab' + (index === 0 ? ' is-active' : '');
1071            btn.id = btnId;
1072            btn.setAttribute('data-chatroom-settings-tab', def.id);
1073            btn.setAttribute('role', 'tab');
1074            btn.setAttribute('aria-controls', panelId);
1075            btn.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
1076            if (def.modOnly) {
1077                btn.hidden = true;
1078            }
1079            btn.innerHTML =
1080                '<i class="' + def.icon + ' citrus-settings-tabs__tab-icon" aria-hidden="true"></i>' +
1081                '<span class="citrus-settings-tabs__tab-label">' + escapeSettingsHtml(def.label) + '</span>';
1082            btn.addEventListener('click', function () {
1083                showCategory(def.id);
1084            });
1085
1086            const tabPanel = document.createElement('div');
1087            tabPanel.className = 'citrus-settings-tabs__panel' + (index === 0 ? ' is-active' : '');
1088            tabPanel.id = panelId;
1089            tabPanel.setAttribute('data-chatroom-settings-panel', def.id);
1090            tabPanel.setAttribute('role', 'tabpanel');
1091            tabPanel.setAttribute('aria-labelledby', btnId);
1092            tabPanel.hidden = index !== 0;
1093
1094            nav.appendChild(btn);
1095            panelsWrap.appendChild(tabPanel);
1096        });
1097
1098        tabsBody.appendChild(nav);
1099        tabsBody.appendChild(panelsWrap);
1100        tabsRoot.appendChild(tabsBody);
1101        shell.appendChild(tabsRoot);
1102
1103        const scope = document.createElement('div');
1104        scope.className = 'citrus-window--settings citrus-window--settings-host-proxy';
1105        scope.appendChild(shell);
1106        modal.appendChild(scope);
1107        document.body.appendChild(modal);
1108
1109        modal.addEventListener('keydown', function (e) {
1110            if (e.key === 'Escape') {
1111                hideSettings();
1112            }
1113        });
1114
1115        syncModerationTabVisibility();
1116        showCategory('appearance');
1117    }
1118
1119    function createLegacySettingsModal() {
1120        const modal = document.createElement('div');
1121        modal.id = 'settings-modal';
1122        modal.className = 'settings-modal';
1123        modal.innerHTML = `
1124            <div class="settings-content">
1125                <div class="settings-header">
1126                    <h2>${window.ChatroomIcons ? window.ChatroomIcons.icon('gear') + ' ' : ''}Settings</h2>
1127                    <button class="settings-close cr-icon-btn" id="chatroom-settings-close" type="button" aria-label="Close settings" onclick="if(window.ChatroomSettings){window.ChatroomSettings.hideSettings();} return false;"></button>
1128                </div>
1129                <div class="settings-body">
1130                    <div class="settings-sidebar">
1131                        <button class="settings-category active" data-category="appearance">${window.ChatroomIcons ? window.ChatroomIcons.icon('palette') + ' ' : ''}Appearance</button>
1132                        <button class="settings-category" data-category="messages">${window.ChatroomIcons ? window.ChatroomIcons.icon('message') + ' ' : ''}Messages</button>
1133                        <button class="settings-category" data-category="notifications">${window.ChatroomIcons ? window.ChatroomIcons.icon('bell') + ' ' : ''}Notifications</button>
1134                        <button class="settings-category" data-category="voice">${window.ChatroomIcons ? window.ChatroomIcons.icon('microphone') + ' ' : ''}Voice & Chat</button>
1135                        <button class="settings-category" data-category="discord">${window.ChatroomIcons ? '<i class="fa-brands fa-discord fa-width-auto" aria-hidden="true"></i> ' : ''}Discord Status</button>
1136                        <button class="settings-category" data-category="advanced">${window.ChatroomIcons ? window.ChatroomIcons.icon('gear') + ' ' : ''}Advanced</button>
1137                        <button class="settings-category" data-category="moderation" id="moderation-category-btn" style="display: none;">${window.ChatroomIcons ? window.ChatroomIcons.icon('crown') + ' ' : ''}Moderation</button>
1138                    </div>
1139                    <div class="settings-panel" id="chatroom-settings-panel">
1140                        <!-- Settings content will be populated here -->
1141                    </div>
1142                </div>
1143            </div>
1144        `;
1145        document.body.appendChild(modal);
1146
1147        var chatCloseBtn = document.getElementById('chatroom-settings-close');
1148        if (chatCloseBtn && window.ChatroomIcons) {
1149            window.ChatroomIcons.set(chatCloseBtn, 'xmark');
1150        }
1151
1152        // Use event delegation on the modal itself to catch close button clicks
1153        modal.addEventListener('click', function modalClickHandler(e) {
1154            // Check if the click is on the close button or its children
1155            const closeBtn = document.getElementById('chatroom-settings-close');
1156            if (closeBtn && (e.target === closeBtn || closeBtn.contains(e.target))) {
1157                e.preventDefault();
1158                e.stopPropagation();
1159                e.stopImmediatePropagation();
1160                emitDevLog('Close button clicked via delegation');
1161                hideSettings();
1162                return false;
1163            }
1164        }, true); // Use capture phase
1165        
1166        // Also attach directly to the button as backup
1167        setTimeout(() => {
1168            const closeBtn = document.getElementById('chatroom-settings-close');
1169            if (closeBtn) {
1170                closeBtn.addEventListener('click', function directCloseHandler(e) {
1171                    e.preventDefault();
1172                    e.stopPropagation();
1173                    e.stopImmediatePropagation();
1174                    emitDevLog('Close button clicked directly');
1175                    hideSettings();
1176                    return false;
1177                }, true);
1178                
1179                closeBtn.addEventListener('mousedown', function mousedownHandler(e) {
1180                    e.preventDefault();
1181                    e.stopPropagation();
1182                    emitDevLog('Close button mousedown');
1183                    hideSettings();
1184                    return false;
1185                });
1186            }
1187        }, 0);
1188
1189        // Category buttons
1190        document.querySelectorAll('.settings-category').forEach(btn => {
1191            btn.addEventListener('click', () => {
1192                document.querySelectorAll('.settings-category').forEach(b => b.classList.remove('active'));
1193                btn.classList.add('active');
1194                showCategory(btn.dataset.category);
1195            });
1196        });
1197
1198        showCategory('appearance');
1199        bindSidebarCategoryIcons();
1200        populateSettingsForm();
1201        
1202        // Show/hide moderation category based on moderator status
1203        const moderationBtn = document.getElementById('moderation-category-btn');
1204        if (moderationBtn) {
1205            const isModerator = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator();
1206            moderationBtn.style.display = isModerator ? 'block' : 'none';
1207        }
1208        
1209        // Add Escape key handler
1210        const escapeHandler = (e) => {
1211            if (e.key === 'Escape') {
1212                hideSettings();
1213                document.removeEventListener('keydown', escapeHandler);
1214            }
1215        };
1216        document.addEventListener('keydown', escapeHandler);
1217    }
1218
1219    /**
1220     * Show settings category
1221     */
1222    function showCategory(category) {
1223        if (isCitrusOsHost()) {
1224            setActiveChatSettingsTab(category);
1225        } else {
1226            document.querySelectorAll('.settings-category').forEach(function (btn) {
1227                btn.classList.toggle('active', btn.dataset.category === category);
1228            });
1229        }
1230
1231        const panel = getCategoryPanel(category);
1232        if (!panel) return;
1233
1234        let html = '';
1235
1236        switch (category) {
1237            case 'appearance': {
1238                panel.innerHTML = '';
1239                const appearanceSection = createChatroomSettingsSection('Appearance', 'palette');
1240                appearanceSection.appendChild(createChatroomSelectField({
1241                    id: 'setting-theme',
1242                    icon: 'swatchbook',
1243                    label: 'Color palette',
1244                    value: settings.theme,
1245                    options: CHAT_PALETTE_OPTIONS,
1246                    onChange: function (val) {
1247                        settings.theme = val;
1248                        pushThemeToSite('chatroom-settings', { palette: val });
1249                        saveSettings();
1250                    }
1251                }));
1252
1253                const colorModeCard = document.createElement('div');
1254                colorModeCard.className = 'settings-card chatroom-settings-theme-card';
1255                colorModeCard.innerHTML =
1256                    '<p class="settings-card__label">' + settingsFa('circle-half-stroke') + ' Light / dark</p>' +
1257                    '<div class="settings-theme-card' + (settings.colorMode === 'dark' ? ' is-dark' : '') + '" id="chatroom-color-mode-card">' +
1258                    '<i class="fa-solid fa-sun settings-theme-card__icon settings-theme-card__icon--sun" aria-hidden="true"></i>' +
1259                    '<label class="settings-switch" title="Toggle light or dark mode">' +
1260                    '<input type="checkbox" id="setting-color-mode" class="settings-switch__input"' +
1261                    (settings.colorMode === 'dark' ? ' checked' : '') + '>' +
1262                    '<span class="settings-switch__track" aria-hidden="true"></span></label>' +
1263                    '<i class="fa-solid fa-moon settings-theme-card__icon settings-theme-card__icon--moon" aria-hidden="true"></i></div>';
1264                appearanceSection.appendChild(colorModeCard);
1265                const colorModeInput = colorModeCard.querySelector('#setting-color-mode');
1266                const colorModeCardEl = colorModeCard.querySelector('#chatroom-color-mode-card');
1267                if (colorModeInput) {
1268                    colorModeInput.addEventListener('change', function (e) {
1269                        setColorMode(e.target.checked ? 'dark' : 'light');
1270                        if (colorModeCardEl) {
1271                            colorModeCardEl.classList.toggle('is-dark', e.target.checked);
1272                        }
1273                    });
1274                }
1275
1276                appearanceSection.appendChild(createChatroomSelectField({
1277                    id: 'setting-font-size',
1278                    icon: 'text-height',
1279                    label: 'Font size',
1280                    value: settings.fontSize,
1281                    options: [
1282                        { value: 'small', label: 'Small' },
1283                        { value: 'medium', label: 'Medium' },
1284                        { value: 'large', label: 'Large' }
1285                    ],
1286                    onChange: function (val) {
1287                        settings.fontSize = val;
1288                        saveSettings();
1289                    }
1290                }));
1291                appearanceSection.appendChild(createChatroomSelectField({
1292                    id: 'setting-emoji-size',
1293                    icon: 'face-smile',
1294                    label: 'Emoji size',
1295                    value: settings.emojiSize,
1296                    options: [
1297                        { value: 'small', label: 'Small' },
1298                        { value: 'medium', label: 'Medium' },
1299                        { value: 'large', label: 'Large' }
1300                    ],
1301                    onChange: function (val) {
1302                        settings.emojiSize = val;
1303                        saveSettings();
1304                    }
1305                }));
1306                appearanceSection.appendChild(createChatroomToggle({
1307                    id: 'setting-animations',
1308                    icon: 'wand-magic-sparkles',
1309                    label: 'Enable animations',
1310                    checked: settings.animations === true,
1311                    onChange: function (val) {
1312                        settings.animations = val;
1313                        saveSettings();
1314                    }
1315                }));
1316                panel.appendChild(appearanceSection);
1317                return;
1318            }
1319
1320            case 'messages': {
1321                panel.innerHTML = '';
1322                const messagesSection = createChatroomSettingsSection('Messages', 'comments');
1323                messagesSection.appendChild(createChatroomToggle({
1324                    id: 'setting-message-grouping',
1325                    icon: 'layer-group',
1326                    label: 'Group consecutive messages',
1327                    checked: settings.messageGrouping === true,
1328                    hint: 'Hides repeat avatars and names when the same person sends back-to-back messages.',
1329                    onChange: function (val) {
1330                        settings.messageGrouping = val;
1331                        saveSettings();
1332                    }
1333                }));
1334                messagesSection.appendChild(createChatroomToggle({
1335                    id: 'setting-show-timestamps',
1336                    icon: 'clock',
1337                    label: 'Show timestamps',
1338                    checked: settings.showTimestamps === true,
1339                    onChange: function (val) {
1340                        settings.showTimestamps = val;
1341                        saveSettings();
1342                    }
1343                }));
1344                messagesSection.appendChild(createChatroomToggle({
1345                    id: 'setting-show-avatars',
1346                    icon: 'circle-user',
1347                    label: 'Show avatars',
1348                    checked: settings.showAvatars === true,
1349                    onChange: function (val) {
1350                        settings.showAvatars = val;
1351                        saveSettings();
1352                    }
1353                }));
1354                messagesSection.appendChild(createChatroomToggle({
1355                    id: 'setting-enter-to-send',
1356                    icon: 'keyboard',
1357                    label: 'Enter to send',
1358                    checked: settings.enterToSend !== false,
1359                    hint: 'On: Enter sends, Shift+Enter adds a line. Off: Shift+Enter sends.',
1360                    onChange: function (val) {
1361                        settings.enterToSend = val;
1362                        saveSettings();
1363                    }
1364                }));
1365                messagesSection.appendChild(createChatroomToggle({
1366                    id: 'setting-auto-scroll',
1367                    icon: 'arrow-down',
1368                    label: 'Auto-scroll to new messages',
1369                    checked: settings.autoScrollNewMessages !== false,
1370                    onChange: function (val) {
1371                        settings.autoScrollNewMessages = val;
1372                        saveSettings();
1373                    }
1374                }));
1375                messagesSection.appendChild(createChatroomToggle({
1376                    id: 'setting-link-previews',
1377                    icon: 'link',
1378                    label: 'Show link previews',
1379                    checked: settings.showLinkPreviews === true,
1380                    onChange: function (val) {
1381                        settings.showLinkPreviews = val;
1382                        saveSettings();
1383                    }
1384                }));
1385                panel.appendChild(messagesSection);
1386                return;
1387            }
1388
1389            case 'voice': {
1390                panel.innerHTML = '';
1391                const voiceSection = createChatroomSettingsSection('Voice & chat', 'microphone');
1392                voiceSection.appendChild(createChatroomToggle({
1393                    id: 'setting-mute-on-join',
1394                    icon: 'microphone-slash',
1395                    label: 'Mute mic when joining voice',
1396                    checked: settings.muteOnJoinVoice === true,
1397                    hint: 'Start muted when you join a voice channel.',
1398                    onChange: function (val) {
1399                        settings.muteOnJoinVoice = val;
1400                        saveSettings();
1401                    }
1402                }));
1403                voiceSection.appendChild(createChatroomToggle({
1404                    id: 'setting-join-leave-sounds',
1405                    icon: 'volume-high',
1406                    label: 'Join / leave sounds',
1407                    checked: settings.playJoinLeaveSounds !== false,
1408                    hint: 'Play a sound when someone joins or leaves voice.',
1409                    onChange: function (val) {
1410                        settings.playJoinLeaveSounds = val;
1411                        saveSettings();
1412                    }
1413                }));
1414                voiceSection.appendChild(createChatroomToggle({
1415                    id: 'setting-confirm-leave-voice',
1416                    icon: 'circle-question',
1417                    label: 'Confirm before leaving voice',
1418                    checked: settings.confirmBeforeLeavingVoice !== false,
1419                    onChange: function (val) {
1420                        settings.confirmBeforeLeavingVoice = val;
1421                        saveSettings();
1422                    }
1423                }));
1424                voiceSection.appendChild(createChatroomToggle({
1425                    id: 'setting-online-status',
1426                    icon: 'signal',
1427                    label: 'Show online status',
1428                    checked: settings.showOnlineStatus !== false,
1429                    onChange: function (val) {
1430                        settings.showOnlineStatus = val;
1431                        saveSettings();
1432                    }
1433                }));
1434                panel.appendChild(voiceSection);
1435                const deviceSection = createChatroomSettingsSection('Audio devices', 'headphones');
1436                panel.appendChild(deviceSection);
1437                appendVoiceDeviceSettings(deviceSection);
1438                return;
1439            }
1440
1441            case 'notifications': {
1442                panel.innerHTML = '';
1443                const notifSection = createChatroomSettingsSection('Notifications', 'bell');
1444                notifSection.appendChild(createChatroomToggle({
1445                    id: 'setting-notifications',
1446                    icon: 'bell',
1447                    label: 'Enable notifications',
1448                    checked: settings.notifications === true,
1449                    onChange: function (val) {
1450                        settings.notifications = val;
1451                        saveSettings();
1452                        if (window.ChatroomNotifications && window.ChatroomNotifications.updateSettings) {
1453                            window.ChatroomNotifications.updateSettings(val);
1454                        }
1455                    }
1456                }));
1457                notifSection.appendChild(createChatroomToggle({
1458                    id: 'setting-desktop-notifications',
1459                    icon: 'desktop',
1460                    label: 'Desktop notifications',
1461                    checked: settings.desktopNotifications === true,
1462                    hint: 'System notifications when tabbed out or on other pages.',
1463                    onChange: async function (val) {
1464                        if (val && typeof Notification !== 'undefined' && Notification.permission === 'default') {
1465                            const permission = await Notification.requestPermission();
1466                            if (permission !== 'granted') {
1467                                settings.desktopNotifications = false;
1468                                const el = document.getElementById('setting-desktop-notifications');
1469                                if (el) el.checked = false;
1470                                return;
1471                            }
1472                        }
1473                        settings.desktopNotifications = !!val;
1474                        saveSettings();
1475                        if (window.ChatroomNotifications && window.ChatroomNotifications.updateDesktopSetting) {
1476                            window.ChatroomNotifications.updateDesktopSetting(settings.desktopNotifications);
1477                        }
1478                    }
1479                }));
1480                notifSection.appendChild(createChatroomToggle({
1481                    id: 'setting-sound-effects',
1482                    icon: 'volume-high',
1483                    label: 'Sound effects',
1484                    checked: settings.soundEffects === true,
1485                    onChange: function (val) {
1486                        settings.soundEffects = val;
1487                        saveSettings();
1488                    }
1489                }));
1490                panel.appendChild(notifSection);
1491                return;
1492            }
1493
1494            case 'discord': {
1495                panel.innerHTML = '';
1496                const discordSection = createChatroomSettingsSection('Discord status (Lanyard)', 'tower-broadcast');
1497                const help = document.createElement('p');
1498                help.className = 'settings-discord-help';
1499                help.innerHTML =
1500                    'Show your live Discord custom status and activity in the chatroom. ' +
1501                    'You must join the <a href="https://discord.gg/lanyard" target="_blank" rel="noopener noreferrer">Lanyard Discord server</a> ' +
1502                    'and keep a visible status on Discord. ' +
1503                    '<a href="https://discord.com/developers/applications" target="_blank" rel="noopener noreferrer">Developer mode</a>: right-click your profile, Copy User ID.';
1504                discordSection.appendChild(help);
1505
1506                discordSection.appendChild(createChatroomToggle({
1507                    id: 'setting-use-lanyard',
1508                    icon: 'tower-broadcast',
1509                    label: 'Use Discord status in chatroom',
1510                    checked: settings.useLanyardStatus === true,
1511                    hint: 'Replaces your chatroom status line with Lanyard when connected.',
1512                    onChange: function (val) {
1513                        settings.useLanyardStatus = val;
1514                        saveSettings();
1515                    }
1516                }));
1517
1518                const idWrap = document.createElement('div');
1519                idWrap.className = 'settings-card chatroom-settings-discord-id';
1520                idWrap.innerHTML =
1521                    '<label class="settings-card__label" for="setting-discord-user-id">' +
1522                    settingsFa('id-card') + ' Discord user ID</label>';
1523                const idInput = document.createElement('input');
1524                idInput.type = 'text';
1525                idInput.id = 'setting-discord-user-id';
1526                idInput.className = 'chatroom-input';
1527                idInput.inputMode = 'numeric';
1528                idInput.autocomplete = 'off';
1529                idInput.placeholder = 'e.g. 905482133257224232';
1530                idInput.value = settings.discordUserId || '';
1531                idInput.addEventListener('change', function () {
1532                    settings.discordUserId = idInput.value.replace(/\D/g, '');
1533                    idInput.value = settings.discordUserId;
1534                    saveSettings();
1535                });
1536                idWrap.appendChild(idInput);
1537
1538                const previewBtn = document.createElement('button');
1539                previewBtn.type = 'button';
1540                previewBtn.className = 'settings-mod-btn settings-mod-btn--primary';
1541                previewBtn.textContent = 'Test connection';
1542                const previewOut = document.createElement('p');
1543                previewOut.className = 'settings-discord-preview';
1544                previewOut.setAttribute('aria-live', 'polite');
1545                previewBtn.addEventListener('click', function () {
1546                    const id = idInput.value.replace(/\D/g, '');
1547                    if (id.length < 17) {
1548                        previewOut.textContent = 'Enter a valid Discord user ID (17 to 20 digits).';
1549                        return;
1550                    }
1551                    previewOut.textContent = 'Checking Lanyard…';
1552                    if (window.ChatroomLanyard && window.ChatroomLanyard.previewDiscordId) {
1553                        window.ChatroomLanyard.previewDiscordId(id).then(function (res) {
1554                            previewOut.textContent = res.message || (res.ok ? 'Connected.' : 'No data.');
1555                        });
1556                    } else {
1557                        previewOut.textContent = 'Lanyard module not loaded yet.';
1558                    }
1559                });
1560                idWrap.appendChild(previewBtn);
1561                idWrap.appendChild(previewOut);
1562                discordSection.appendChild(idWrap);
1563                panel.appendChild(discordSection);
1564                return;
1565            }
1566
1567            case 'advanced':
1568                panel.innerHTML = '<h3>' + (window.ChatroomIcons ? window.ChatroomIcons.icon('gear') + ' ' : '') + 'Advanced</h3>';
1569                
1570                // Message Cleanup Section
1571                const cleanupItem = document.createElement('div');
1572                cleanupItem.className = 'setting-item';
1573                cleanupItem.style.marginTop = 'var(--gap)';
1574                cleanupItem.style.paddingTop = 'var(--gap)';
1575                cleanupItem.style.borderTop = 'var(--border-thickness) solid var(--border)';
1576                
1577                cleanupItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">🗑️ Message Cleanup</label>';
1578                
1579                const cleanupDesc = document.createElement('small');
1580                cleanupDesc.textContent = 'Automatically delete old messages and images to save Firebase storage. Messages older than the selected period will be permanently deleted.';
1581                cleanupDesc.style.display = 'block';
1582                cleanupDesc.style.marginTop = '4px';
1583                cleanupDesc.style.marginBottom = '12px';
1584                cleanupItem.appendChild(cleanupDesc);
1585                
1586                // Retention period selector
1587                const retentionLabel = document.createElement('label');
1588                retentionLabel.textContent = 'Keep messages for:';
1589                retentionLabel.style.display = 'block';
1590                retentionLabel.style.marginBottom = '8px';
1591                cleanupItem.appendChild(retentionLabel);
1592                
1593                const retentionSelect = document.createElement('select');
1594                retentionSelect.id = 'retention-days-select';
1595                retentionSelect.className = 'setting-item select';
1596                retentionSelect.style.width = '100%';
1597                retentionSelect.style.padding = '8px';
1598                retentionSelect.style.background = 'var(--bg-2)';
1599                retentionSelect.style.border = 'var(--border-thickness) solid var(--border)';
1600                retentionSelect.style.color = 'var(--text-2)';
1601                retentionSelect.style.fontFamily = 'var(--font)';
1602                retentionSelect.style.fontSize = '14px';
1603                retentionSelect.style.marginBottom = '12px';
1604                
1605                const retentionOptions = [
1606                    { value: 3, text: '3 days' },
1607                    { value: 7, text: '1 week (recommended)' },
1608                    { value: 14, text: '2 weeks' },
1609                    { value: 30, text: '1 month' },
1610                    { value: 60, text: '2 months' },
1611                    { value: 90, text: '3 months' }
1612                ];
1613                
1614                const currentRetention = window.ChatroomMessageCleanup 
1615                    ? window.ChatroomMessageCleanup.getRetentionDays() 
1616                    : 7;
1617                
1618                retentionOptions.forEach(opt => {
1619                    const option = document.createElement('option');
1620                    option.value = opt.value;
1621                    option.textContent = opt.text;
1622                    if (opt.value === currentRetention) {
1623                        option.selected = true;
1624                    }
1625                    retentionSelect.appendChild(option);
1626                });
1627                
1628                retentionSelect.addEventListener('change', () => {
1629                    const days = parseInt(retentionSelect.value, 10);
1630                    if (window.ChatroomMessageCleanup) {
1631                        window.ChatroomMessageCleanup.setRetentionDays(days);
1632                        // Show feedback
1633                        const feedback = document.createElement('div');
1634                        feedback.textContent = `✓ Retention period set to ${days} day${days !== 1 ? 's' : ''}`;
1635                        feedback.style.color = 'var(--green-2)';
1636                        feedback.style.fontSize = '12px';
1637                        feedback.style.marginTop = '4px';
1638                        cleanupItem.appendChild(feedback);
1639                        setTimeout(() => {
1640                            feedback.remove();
1641                        }, 3000);
1642                    }
1643                });
1644                
1645                cleanupItem.appendChild(retentionSelect);
1646                
1647                // Manual cleanup button
1648                const cleanupBtn = document.createElement('button');
1649                cleanupBtn.textContent = 'Run Cleanup Now';
1650                cleanupBtn.style.padding = '8px 16px';
1651                cleanupBtn.style.background = 'var(--accent-2)';
1652                cleanupBtn.style.color = 'var(--text-0)';
1653                cleanupBtn.style.border = 'var(--border-thickness) solid var(--accent-3)';
1654                cleanupBtn.style.cursor = 'pointer';
1655                cleanupBtn.style.fontFamily = 'var(--font)';
1656                cleanupBtn.style.fontSize = '13px';
1657                cleanupBtn.style.transition = 'all 0.2s ease';
1658                cleanupBtn.style.marginRight = '8px';
1659                cleanupBtn.addEventListener('click', () => {
1660                    if (window.ChatroomMessageCleanup) {
1661                        cleanupBtn.disabled = true;
1662                        cleanupBtn.textContent = 'Cleaning up...';
1663                        window.ChatroomMessageCleanup.manualCleanup();
1664                        setTimeout(() => {
1665                            cleanupBtn.disabled = false;
1666                            cleanupBtn.textContent = 'Run Cleanup Now';
1667                        }, 5000);
1668                    }
1669                });
1670                cleanupBtn.addEventListener('mouseenter', () => {
1671                    if (!cleanupBtn.disabled) {
1672                        cleanupBtn.style.background = 'var(--accent-3)';
1673                    }
1674                });
1675                cleanupBtn.addEventListener('mouseleave', () => {
1676                    if (!cleanupBtn.disabled) {
1677                        cleanupBtn.style.background = 'var(--accent-2)';
1678                    }
1679                });
1680                cleanupItem.appendChild(cleanupBtn);
1681                
1682                // Status display
1683                const statusDiv = document.createElement('div');
1684                statusDiv.id = 'cleanup-status';
1685                statusDiv.style.marginTop = '8px';
1686                statusDiv.style.fontSize = '12px';
1687                statusDiv.style.color = 'var(--text-4)';
1688                if (window.ChatroomMessageCleanup) {
1689                    const status = window.ChatroomMessageCleanup.getStatus();
1690                    if (status.lastCleanupTime) {
1691                        const lastCleanup = new Date(status.lastCleanupTime);
1692                        statusDiv.textContent = `Last cleanup: ${lastCleanup.toLocaleString()}`;
1693                    } else {
1694                        statusDiv.textContent = 'Cleanup runs automatically every hour';
1695                    }
1696                }
1697                cleanupItem.appendChild(statusDiv);
1698                
1699                panel.appendChild(cleanupItem);
1700                
1701                const resetItem = document.createElement('div');
1702                resetItem.className = 'setting-item';
1703                const resetBtn = window.ChatroomButton ? window.ChatroomButton.create({
1704                    text: 'Reset All Settings',
1705                    variant: 'danger',
1706                    onClick: () => {
1707                        if (confirm('Are you sure you want to reset all settings to default?')) {
1708                            settings = {
1709                                theme: 'default',
1710                                colorMode: 'dark',
1711                                messageGrouping: true,
1712                                showTimestamps: true,
1713                                showAvatars: true,
1714                                fontSize: 'medium',
1715                                emojiSize: 'medium',
1716                                animations: true,
1717                                soundEffects: false,
1718                                notifications: false,
1719                                desktopNotifications: false,
1720                                enterToSend: true,
1721                                muteOnJoinVoice: false,
1722                                playJoinLeaveSounds: true,
1723                                autoScrollNewMessages: true,
1724                                showLinkPreviews: false,
1725                                showOnlineStatus: true,
1726                                confirmBeforeLeavingVoice: true
1727                            };
1728                            saveSettings();
1729                            showCategory(category); // Refresh current category
1730                        }
1731                    }
1732                }) : document.createElement('button');
1733                if (!window.ChatroomButton) {
1734                    resetBtn.className = 'setting-reset-btn';
1735                    resetBtn.textContent = 'Reset All Settings';
1736                    resetBtn.id = 'reset-settings-btn';
1737                }
1738                resetItem.appendChild(resetBtn);
1739                panel.appendChild(resetItem);
1740                
1741                return;
1742                break;
1743
1744            case 'moderation':
1745                // Only show if user is moderator
1746                const isModerator = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator();
1747                if (!isModerator) {
1748                    panel.innerHTML = '<h3>👑 Moderation</h3><p>You do not have moderator privileges.</p>';
1749                    return;
1750                }
1751                
1752                panel.innerHTML = '<h3>👑 Moderation</h3>';
1753                
1754                // Delete Message section
1755                const deleteMessageItem = document.createElement('div');
1756                deleteMessageItem.className = 'setting-item';
1757                deleteMessageItem.style.marginBottom = 'var(--gap)';
1758                deleteMessageItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">🗑️ Delete Messages</label>';
1759                
1760                const deleteMessageDesc = document.createElement('small');
1761                deleteMessageDesc.textContent = 'Right-click a message (or use ⋯ on hover) → Delete. You can delete your own messages; moderators can delete any message.';
1762                deleteMessageDesc.style.display = 'block';
1763                deleteMessageDesc.style.marginTop = '4px';
1764                deleteMessageDesc.style.color = 'var(--text-4)';
1765                deleteMessageItem.appendChild(deleteMessageDesc);
1766                panel.appendChild(deleteMessageItem);
1767                
1768                // Create Channel section
1769                const createChannelItem = document.createElement('div');
1770                createChannelItem.className = 'setting-item';
1771                createChannelItem.style.marginBottom = 'var(--gap)';
1772                createChannelItem.style.paddingTop = 'var(--gap)';
1773                createChannelItem.style.borderTop = 'var(--border-thickness) solid var(--border)';
1774                createChannelItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">➕ Create Channel</label>';
1775                
1776                const createChannelDesc = document.createElement('small');
1777                createChannelDesc.textContent = 'Create a new text channel with a custom name. Channel names can contain letters, numbers, underscores, and hyphens.';
1778                createChannelDesc.style.display = 'block';
1779                createChannelDesc.style.marginTop = '4px';
1780                createChannelDesc.style.marginBottom = '12px';
1781                createChannelDesc.style.color = 'var(--text-4)';
1782                createChannelItem.appendChild(createChannelDesc);
1783                
1784                // Channel name input
1785                const channelNameLabel = document.createElement('label');
1786                channelNameLabel.textContent = 'Channel Name:';
1787                channelNameLabel.style.display = 'block';
1788                channelNameLabel.style.marginBottom = '8px';
1789                createChannelItem.appendChild(channelNameLabel);
1790                
1791                const channelNameInput = document.createElement('input');
1792                channelNameInput.type = 'text';
1793                channelNameInput.id = 'mod-create-channel-name';
1794                channelNameInput.placeholder = 'e.g., general, random, announcements';
1795                channelNameInput.style.width = '100%';
1796                channelNameInput.style.padding = '8px';
1797                channelNameInput.style.background = 'var(--bg-2)';
1798                channelNameInput.style.border = 'var(--border-thickness) solid var(--border)';
1799                channelNameInput.style.color = 'var(--text-2)';
1800                channelNameInput.style.fontFamily = 'var(--font)';
1801                channelNameInput.style.fontSize = '14px';
1802                channelNameInput.style.marginBottom = '12px';
1803                channelNameInput.addEventListener('keydown', (e) => {
1804                    if (e.key === 'Enter') {
1805                        e.preventDefault();
1806                        const createBtn = document.getElementById('mod-create-channel-btn');
1807                        if (createBtn) createBtn.click();
1808                    }
1809                });
1810                createChannelItem.appendChild(channelNameInput);
1811                
1812                // Channel topic input (optional)
1813                const channelTopicLabel = document.createElement('label');
1814                channelTopicLabel.textContent = 'Channel Topic (optional):';
1815                channelTopicLabel.style.display = 'block';
1816                channelTopicLabel.style.marginBottom = '8px';
1817                createChannelItem.appendChild(channelTopicLabel);
1818                
1819                const channelTopicInput = document.createElement('input');
1820                channelTopicInput.type = 'text';
1821                channelTopicInput.id = 'mod-create-channel-topic';
1822                channelTopicInput.placeholder = 'e.g., General discussion';
1823                channelTopicInput.style.width = '100%';
1824                channelTopicInput.style.padding = '8px';
1825                channelTopicInput.style.background = 'var(--bg-2)';
1826                channelTopicInput.style.border = 'var(--border-thickness) solid var(--border)';
1827                channelTopicInput.style.color = 'var(--text-2)';
1828                channelTopicInput.style.fontFamily = 'var(--font)';
1829                channelTopicInput.style.fontSize = '14px';
1830                channelTopicInput.style.marginBottom = '12px';
1831                channelTopicInput.addEventListener('keydown', (e) => {
1832                    if (e.key === 'Enter') {
1833                        e.preventDefault();
1834                        const createBtn = document.getElementById('mod-create-channel-btn');
1835                        if (createBtn) createBtn.click();
1836                    }
1837                });
1838                createChannelItem.appendChild(channelTopicInput);
1839                
1840                // Create channel button
1841                const createChannelBtn = document.createElement('button');
1842                createChannelBtn.id = 'mod-create-channel-btn';
1843                createChannelBtn.className = 'settings-mod-btn settings-mod-btn--primary';
1844                createChannelBtn.textContent = 'Create Channel';
1845                createChannelBtn.addEventListener('click', () => {
1846                    const name = channelNameInput.value.trim();
1847                    const topic = channelTopicInput.value.trim();
1848                    
1849                    if (!name) {
1850                        alert('Please enter a channel name.');
1851                        return;
1852                    }
1853                    
1854                    // Validate channel name (same as in chatroom.js)
1855                    const cleanName = name.toLowerCase().replace(/[^a-z0-9_-]/g, '');
1856                    if (cleanName.length < 1 || cleanName.length > 50) {
1857                        alert('Channel name must be 1-50 characters (letters, numbers, _, - only)');
1858                        return;
1859                    }
1860                    
1861                    // Check if channel already exists
1862                    if (firebase && firebase.database) {
1863                        const channelsRef = firebase.database().ref(`chatroom/channels/${cleanName}/metadata`);
1864                        channelsRef.once('value', (snapshot) => {
1865                            if (snapshot.exists()) {
1866                                alert('A channel with this name already exists.');
1867                                return;
1868                            }
1869                            
1870                            // Create the channel
1871                            const currentUsername = window.__sharedUsername ? window.__sharedUsername.get() : 'system';
1872                            channelsRef.set({
1873                                name: name,
1874                                topic: topic || '',
1875                                created: Date.now(),
1876                                createdBy: currentUsername
1877                            }).then(() => {
1878                                // Add new channel to the global order
1879                                const orderRef = firebase.database().ref('chatroom/channelOrder');
1880                                orderRef.once('value', (snapshot) => {
1881                                    let currentOrder = [];
1882                                    if (snapshot.exists()) {
1883                                        currentOrder = snapshot.val() || [];
1884                                    }
1885                                    // Add new channel to the end of the order
1886                                    if (!currentOrder.includes(cleanName)) {
1887                                        currentOrder.push(cleanName);
1888                                        orderRef.set(currentOrder).catch(err => {
1889                                            console.error('Error updating channel order:', err);
1890                                        });
1891                                    }
1892                                });
1893                                
1894                                // Clear inputs
1895                                channelNameInput.value = '';
1896                                channelTopicInput.value = '';
1897                                
1898                                // Show success message
1899                                const successMsg = document.createElement('div');
1900                                successMsg.textContent = `✓ Channel "${name}" created successfully!`;
1901                                successMsg.style.color = 'var(--green-2)';
1902                                successMsg.style.fontSize = '12px';
1903                                successMsg.style.marginTop = '8px';
1904                                createChannelItem.appendChild(successMsg);
1905                                setTimeout(() => {
1906                                    successMsg.remove();
1907                                }, 3000);
1908                                
1909                                // Switch to the new channel
1910                                if (window.Chatroom && window.Chatroom.loadChannel) {
1911                                    setTimeout(() => {
1912                                        window.Chatroom.loadChannel(cleanName);
1913                                    }, 500);
1914                                }
1915                            }).catch(error => {
1916                                console.error('Error creating channel:', error);
1917                                alert('Error creating channel. You may not have permission.');
1918                            });
1919                        });
1920                    }
1921                });
1922                createChannelItem.appendChild(createChannelBtn);
1923                
1924                panel.appendChild(createChannelItem);
1925                
1926                // Delete Channel section
1927                const deleteChannelItem = document.createElement('div');
1928                deleteChannelItem.className = 'setting-item';
1929                deleteChannelItem.style.marginBottom = 'var(--gap)';
1930                deleteChannelItem.style.paddingTop = 'var(--gap)';
1931                deleteChannelItem.style.borderTop = 'var(--border-thickness) solid var(--border)';
1932                deleteChannelItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">📁 Delete Channels</label>';
1933                
1934                const deleteChannelDesc = document.createElement('small');
1935                deleteChannelDesc.textContent = 'Delete channels that are no longer needed. The general channel cannot be deleted.';
1936                deleteChannelDesc.style.display = 'block';
1937                deleteChannelDesc.style.marginTop = '4px';
1938                deleteChannelDesc.style.marginBottom = '12px';
1939                deleteChannelDesc.style.color = 'var(--text-4)';
1940                deleteChannelItem.appendChild(deleteChannelDesc);
1941                
1942                // Channel selector
1943                const channelLabel = document.createElement('label');
1944                channelLabel.textContent = 'Select channel to delete:';
1945                channelLabel.style.display = 'block';
1946                channelLabel.style.marginBottom = '8px';
1947                deleteChannelItem.appendChild(channelLabel);
1948                
1949                const channelSelect = document.createElement('select');
1950                channelSelect.id = 'mod-delete-channel-select';
1951                channelSelect.style.width = '100%';
1952                channelSelect.style.padding = '8px';
1953                channelSelect.style.background = 'var(--bg-2)';
1954                channelSelect.style.border = 'var(--border-thickness) solid var(--border)';
1955                channelSelect.style.color = 'var(--text-2)';
1956                channelSelect.style.fontFamily = 'var(--font)';
1957                channelSelect.style.fontSize = '14px';
1958                channelSelect.style.marginBottom = '12px';
1959                
1960                // Populate channels
1961                if (firebase && firebase.database) {
1962                    const channelsRef = firebase.database().ref('chatroom/channels');
1963                    channelsRef.once('value', (snapshot) => {
1964                        const channels = snapshot.val();
1965                        if (channels) {
1966                            const defaultOption = document.createElement('option');
1967                            defaultOption.value = '';
1968                            defaultOption.textContent = '-- Select a channel --';
1969                            channelSelect.appendChild(defaultOption);
1970                            
1971                            Object.keys(channels).forEach(channelId => {
1972                                if (channelId === 'general') return; // Skip general
1973                                const metadata = channels[channelId].metadata;
1974                                if (metadata) {
1975                                    const option = document.createElement('option');
1976                                    option.value = channelId;
1977                                    option.textContent = `# ${metadata.name || channelId}`;
1978                                    channelSelect.appendChild(option);
1979                                }
1980                            });
1981                        }
1982                    });
1983                }
1984                
1985                deleteChannelItem.appendChild(channelSelect);
1986                
1987                // Delete channel button
1988                const deleteChannelBtn = document.createElement('button');
1989                deleteChannelBtn.className = 'settings-mod-btn settings-mod-btn--danger';
1990                deleteChannelBtn.textContent = 'Delete Channel';
1991                deleteChannelBtn.addEventListener('click', () => {
1992                    const channelId = channelSelect.value;
1993                    if (!channelId) {
1994                        alert('Please select a channel to delete.');
1995                        return;
1996                    }
1997                    if (window.ChatroomModerator && window.ChatroomModerator.deleteChannel) {
1998                        window.ChatroomModerator.deleteChannel(channelId);
1999                        // Refresh channel list after deletion
2000                        setTimeout(() => {
2001                            if (firebase && firebase.database) {
2002                                const channelsRef = firebase.database().ref('chatroom/channels');
2003                                channelsRef.once('value', (snapshot) => {
2004                                    const channels = snapshot.val();
2005                                    channelSelect.innerHTML = '<option value="">-- Select a channel --</option>';
2006                                    if (channels) {
2007                                        Object.keys(channels).forEac
2007h(chId => {
2008                                            if (chId === 'general') return;
2009                                            const metadata = channels[chId].metadata;
2010                                            if (metadata) {
2011                                                const option = document.createElement('option');
2012                                                option.value = chId;
2013                                                option.textContent = `# ${metadata.name || chId}`;
2014                                                channelSelect.appendChild(option);
2015                                            }
2016                                        });
2017                                    }
2018                                });
2019                            }
2020                        }, 1000);
2021                    }
2022                });
2023                deleteChannelItem.appendChild(deleteChannelBtn);
2024                
2025                panel.appendChild(deleteChannelItem);
2026                
2027                // Restore Default Channels section
2028                const restoreChannelsItem = document.createElement('div');
2029                restoreChannelsItem.className = 'setting-item';
2030                restoreChannelsItem.style.marginBottom = 'var(--gap)';
2031                restoreChannelsItem.style.paddingTop = 'var(--gap)';
2032                restoreChannelsItem.style.borderTop = 'var(--border-thickness) solid var(--border)';
2033                restoreChannelsItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">🔧 Restore Default Channels</label>';
2034                
2035                const restoreDesc = document.createElement('small');
2036                restoreDesc.textContent = 'Restore the default channels (general, whimsical-chatting) if they are missing.';
2037                restoreDesc.style.display = 'block';
2038                restoreDesc.style.marginTop = '4px';
2039                restoreDesc.style.marginBottom = '12px';
2040                restoreDesc.style.color = 'var(--text-4)';
2041                restoreChannelsItem.appendChild(restoreDesc);
2042                
2043                const restoreBtn = document.createElement('button');
2044                restoreBtn.textContent = 'Restore Default Channels';
2045                restoreBtn.style.padding = '8px 16px';
2046                restoreBtn.style.background = 'var(--accent-2)';
2047                restoreBtn.style.color = 'var(--text-0)';
2048                restoreBtn.style.border = 'var(--border-thickness) solid var(--accent-3)';
2049                restoreBtn.style.cursor = 'pointer';
2050                restoreBtn.style.fontFamily = 'var(--font)';
2051                restoreBtn.style.fontSize = '13px';
2052                restoreBtn.style.transition = 'all 0.2s ease';
2053                restoreBtn.addEventListener('click', () => {
2054                    if (!firebase || !firebase.database) {
2055                        alert('Firebase not available.');
2056                        return;
2057                    }
2058                    
2059                    restoreBtn.disabled = true;
2060                    restoreBtn.textContent = 'Restoring...';
2061                    
2062                    const channelsRef = firebase.database().ref('chatroom/channels');
2063                    const orderRef = firebase.database().ref('chatroom/channelOrder');
2064                    
2065                    // Restore general channel
2066                    const generalPromise = channelsRef.child('general').child('metadata').once('value').then(snapshot => {
2067                        if (!snapshot.exists()) {
2068                            return channelsRef.child('general').child('metadata').set({
2069                                name: 'general',
2070                                topic: 'Welcome to puppy\'s chatroom!',
2071                                created: Date.now(),
2072                                createdBy: 'system'
2073                            });
2074                        }
2075                    });
2076                    
2077                    // Restore whimsical-chatting channel
2078                    const whimsicalPromise = channelsRef.child('whimsical-chatting').child('metadata').once('value').then(snapshot => {
2079                        if (!snapshot.exists()) {
2080                            return channelsRef.child('whimsical-chatting').child('metadata').set({
2081                                name: 'whimsical chatting',
2082                                topic: 'Text chat for Whimsy Zone voice channel',
2083                                created: Date.now(),
2084                                createdBy: 'system',
2085                                type: 'voice-text'
2086                            });
2087                        }
2088                    });
2089                    
2090                    Promise.all([generalPromise, whimsicalPromise]).then(() => {
2091                        // Update channel order to include these channels
2092                        orderRef.once('value', (snapshot) => {
2093                            let currentOrder = [];
2094                            if (snapshot.exists()) {
2095                                currentOrder = snapshot.val() || [];
2096                            }
2097                            
2098                            // Add channels if they're not in the order
2099                            if (!currentOrder.includes('general')) {
2100                                currentOrder.unshift('general'); // Add at the beginning
2101                            }
2102                            if (!currentOrder.includes('whimsical-chatting')) {
2103                                currentOrder.push('whimsical-chatting'); // Add at the end
2104                            }
2105                            
2106                            return orderRef.set(currentOrder);
2107                        }).then(() => {
2108                            restoreBtn.disabled = false;
2109                            restoreBtn.textContent = '✓ Channels Restored!';
2110                            restoreBtn.style.background = 'var(--green-2)';
2111                            
2112                            setTimeout(() => {
2113                                restoreBtn.textContent = 'Restore Default Channels';
2114                                restoreBtn.style.background = 'var(--accent-2)';
2115                            }, 3000);
2116                            
2117                            // Refresh channels
2118                            if (window.Chatroom && window.Chatroom.setupChannels) {
2119                                setTimeout(() => {
2120                                    window.Chatroom.setupChannels();
2121                                }, 1000);
2122                            } else {
2123                                // Fallback: reload page
2124                                setTimeout(() => {
2125                                    window.location.reload();
2126                                }, 2000);
2127                            }
2128                        }).catch(err => {
2129                            console.error('Error updating channel order:', err);
2130                            restoreBtn.disabled = false;
2131                            restoreBtn.textContent = 'Restore Default Channels';
2132                            alert('Channels restored but error updating order. Please refresh the page.');
2133                        });
2134                    }).catch(err => {
2135                        console.error('Error restoring channels:', err);
2136                        restoreBtn.disabled = false;
2137                        restoreBtn.textContent = 'Restore Default Channels';
2138                        alert('Error restoring channels: ' + err.message);
2139                    });
2140                });
2141                restoreBtn.addEventListener('mouseenter', () => {
2142                    if (!restoreBtn.disabled) {
2143                        restoreBtn.style.background = 'var(--accent-3)';
2144                    }
2145                });
2146                restoreBtn.addEventListener('mouseleave', () => {
2147                    if (!restoreBtn.disabled && restoreBtn.textContent === 'Restore Default Channels') {
2148                        restoreBtn.style.background = 'var(--accent-2)';
2149                    }
2150                });
2151                restoreChannelsItem.appendChild(restoreBtn);
2152                panel.appendChild(restoreChannelsItem);
2153
2154                // Moderation log
2155                const modLogItem = document.createElement('div');
2156                modLogItem.className = 'setting-item';
2157                modLogItem.style.marginBottom = 'var(--gap)';
2158                modLogItem.style.paddingTop = 'var(--gap)';
2159                modLogItem.style.borderTop = 'var(--border-thickness) solid var(--border)';
2160                modLogItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">📋 Moderation log</label>';
2161
2162                const modLogDesc = document.createElement('small');
2163                modLogDesc.textContent = 'Recent auto-mutes, strikes, IP bans, and message deletes. Use this to debug false positives.';
2164                modLogDesc.style.display = 'block';
2165                modLogDesc.style.marginTop = '4px';
2166                modLogDesc.style.marginBottom = '12px';
2167                modLogDesc.style.color = 'var(--text-4)';
2168                modLogItem.appendChild(modLogDesc);
2169
2170                const modLogToolbar = document.createElement('div');
2171                modLogToolbar.style.display = 'flex';
2172                modLogToolbar.style.gap = '8px';
2173                modLogToolbar.style.marginBottom = '12px';
2174
2175                const modLogRefreshBtn = document.createElement('button');
2176                modLogRefreshBtn.type = 'button';
2177                modLogRefreshBtn.className = 'settings-mod-btn';
2178                modLogRefreshBtn.textContent = 'Refresh log';
2179                modLogToolbar.appendChild(modLogRefreshBtn);
2180                modLogItem.appendChild(modLogToolbar);
2181
2182                const modLogList = document.createElement('div');
2183                modLogList.className = 'settings-mod-log-list';
2184                modLogList.style.maxHeight = '320px';
2185                modLogList.style.overflowY = 'auto';
2186                modLogList.style.border = 'var(--border-thickness) solid var(--border)';
2187                modLogList.style.borderRadius = '8px';
2188                modLogList.style.background = 'var(--bg-2)';
2189                modLogList.style.padding = '8px';
2190                modLogList.textContent = 'Loading…';
2191                modLogItem.appendChild(modLogList);
2192
2193                function renderModerationLogRows(entries) {
2194                    modLogList.innerHTML = '';
2195                    if (!entries || !entries.length) {
2196                        modLogList.textContent = 'No moderation events logged yet.';
2197                        return;
2198                    }
2199                    var logApi = window.ChatroomModerationLog;
2200                    entries.forEach(function (entry) {
2201                        var row = document.createElement('div');
2202                        row.className = 'settings-mod-log-row';
2203                        row.style.padding = '10px 8px';
2204                        row.style.borderBottom = '1px solid var(--border)';
2205                        row.style.fontSize = '12px';
2206                        row.style.lineHeight = '1.45';
2207                        row.style.color = 'var(--text-3)';
2208
2209                        var typeColors = {
2210                            strike: '#e65100',
2211                            timeout_block: '#f57c00',
2212                            ip_ban: '#c62828',
2213                            message_delete: '#5c6bc0',
2214                            channel_delete: '#7b1fa2',
2215                            admin_clear: '#2e7d32'
2216                        };
2217                        var typeLabel = logApi ? logApi.formatTypeLabel(entry.type) : entry.type;
2218                        var when = logApi ? logApi.formatTimestamp(entry.timestamp) : '';
2219                        var detail = logApi ? logApi.describeEntry(entry) : '';
2220
2221                        row.innerHTML =
2222                            '<div style="display:flex;justify-content:space-between;gap:8px;margin-bottom:4px;">' +
2223                                '<strong style="color:' + (typeColors[entry.type] || 'var(--text-2)') + ';">' + typeLabel + '</strong>' +
2224                                '<span style="color:var(--text-4);white-space:nowrap;">' + when + '</span>' +
2225                            '</div>' +
2226                            '<div><strong style="color:var(--text-2);">' + (entry.username || entry.actor || 'unknown') + '</strong></div>' +
2227                            '<div style="margin-top:4px;">' + detail + '</div>';
2228                        modLogList.appendChild(row);
2229                    });
2230                }
2231
2232                function loadModerationLog() {
2233                    modLogList.textContent = 'Loading…';
2234                    if (!window.ChatroomModerationLog) {
2235                        modLogList.textContent = 'Moderation log module not loaded.';
2236                        return;
2237                    }
2238                    window.ChatroomModerationLog.fetchRecent(80).then(renderModerationLogRows);
2239                }
2240
2241                modLogRefreshBtn.addEventListener('click', loadModerationLog);
2242                loadModerationLog();
2243                panel.appendChild(modLogItem);
2244                
2245                // Channel Reordering section
2246                const reorderItem = document.createElement('div');
2247                reorderItem.className = 'setting-item';
2248                reorderItem.style.marginBottom = 'var(--gap)';
2249                reorderItem.style.paddingTop = 'var(--gap)';
2250                reorderItem.style.borderTop = 'var(--border-thickness) solid var(--border)';
2251                reorderItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">📋 Reorder Channels</label>';
2252                
2253                const reorderDesc = document.createElement('small');
2254                reorderDesc.textContent = 'Drag and drop text channels in the sidebar to reorder them. The order is synced globally for all users.';
2255                reorderDesc.style.display = 'block';
2256                reorderDesc.style.marginTop = '4px';
2257                reorderDesc.style.color = 'var(--text-4)';
2258                reorderItem.appendChild(reorderDesc);
2259                panel.appendChild(reorderItem);
2260                
2261                return;
2262                break;
2263        }
2264
2265        // Fallback for old HTML-based approach
2266        if (html) {
2267            panel.innerHTML = html;
2268            populateSettingsForm();
2269            attachSettingsHandlers();
2270        }
2271    }
2272
2273    /**
2274     * Helper: Create select element (fallback)
2275     */
2276    function createSelect(options, value, id, onChange) {
2277        const select = document.createElement('select');
2278        select.id = id;
2279        options.forEach(opt => {
2280            const option = document.createElement('option');
2281            option.value = opt;
2282            option.textContent = opt.charAt(0).toUpperCase() + opt.slice(1);
2283            if (opt === value) option.selected = true;
2284            select.appendChild(option);
2285        });
2286        if (onChange) {
2287            select.addEventListener('change', (e) =>
2287 onChange(e.target.value));
2288        }
2289        return select;
2290    }
2291
2292    /**
2293     * Helper: Create checkbox (fallback)
2294     */
2295    function createCheckbox(label, checked, id, onChange) {
2296        const labelEl = document.createElement('label');
2297        const input = document.createElement('input');
2298        input.type = 'checkbox';
2299        input.id = id;
2300        input.checked = checked;
2301        if (onChange) {
2302            input.addEventListener('change', (e) => onChange(e.target.checked));
2303        }
2304        labelEl.appendChild(input);
2305        const span = document.createElement('span');
2306        span.textContent = label;
2307        labelEl.appendChild(span);
2308        return labelEl;
2309    }
2310
2311    /**
2312     * Helper: Create textarea (fallback)
2313     */
2314    function createTextarea(placeholder, value, id, rows, onChange) {
2315        const textarea = document.createElement('textarea');
2316        textarea.id = id;
2317        textarea.placeholder = placeholder;
2318        textarea.value = value;
2319        textarea.rows = rows;
2320        if (onChange) {
2321            let timeout;
2322            textarea.addEventListener('input', (e) => {
2323                clearTimeout(timeout);
2324                timeout = setTimeout(() => onChange(e.target.value), 500);
2325            });
2326        }
2327        return textarea;
2328    }
2329
2330    /**
2331     * Populate settings form with current values
2332     */
2333    function populateSettingsForm() {
2334        hydrateThemeFromSite();
2335        syncThemeControlsInForm();
2336        const fontSizeSelect = document.getElementById('setting-font-size');
2337        if (fontSizeSelect) fontSizeSelect.value = settings.fontSize;
2338        const emojiSizeSelect = document.getElementById('setting-emoji-size');
2339        if (emojiSizeSelect) emojiSizeSelect.value = settings.emojiSize;
2340        document.getElementById('setting-animations') && (document.getElementById('setting-animations').checked = settings.animations);
2341        document.getElementById('setting-message-grouping') && (document.getElementById('setting-message-grouping').checked = settings.messageGrouping);
2342        document.getElementById('setting-show-timestamps') && (document.getElementById('setting-show-timestamps').checked = settings.showTimestamps);
2343        document.getElementById('setting-show-avatars') && (document.getElementById('setting-show-avatars').checked = settings.showAvatars);
2344        document.getElementById('setting-notifications') && (document.getElementById('setting-notifications').checked = settings.notifications);
2345        document.getElementById('setting-sound-effects') && (document.getElementById('setting-sound-effects').checked = settings.soundEffects);
2346    }
2347
2348    /**
2349     * Attach event handlers to settings inputs
2350     */
2351    function attachSettingsHandlers() {
2352        // Theme
2353        const themeSelect = document.getElementById('setting-theme');
2354        if (themeSelect) {
2355            themeSelect.value = settings.theme;
2356            themeSelect.addEventListener('change', (e) => {
2357                settings.theme = e.target.value;
2358                saveSettings();
2359            });
2360        }
2361
2362        // Font size
2363        const fontSizeSelect = document.getElementById('setting-font-size');
2364        if (fontSizeSelect) {
2365            fontSizeSelect.value = settings.fontSize;
2366            fontSizeSelect.addEventListener('change', (e) => {
2367                settings.fontSize = e.target.value;
2368                saveSettings();
2369            });
2370        }
2371
2372        // Emoji size
2373        const emojiSizeSelect = document.getElementById('setting-emoji-size');
2374        if (emojiSizeSelect) {
2375            emojiSizeSelect.value = settings.emojiSize;
2376            emojiSizeSelect.addEventListener('change', (e) => {
2377                settings.emojiSize = e.target.value;
2378                saveSettings();
2379            });
2380        }
2381
2382        // Checkboxes
2383        ['animations', 'message-grouping', 'show-timestamps', 'show-avatars', 'notifications', 'sound-effects'].forEach(id => {
2384            const checkbox = document.getElementById(`setting-${id}`);
2385            if (checkbox) {
2386                const key = id.replace(/-/g, '').replace('setting', '');
2387                checkbox.checked = settings[key] || false;
2388                checkbox.addEventListener('change', (e) => {
2389                    settings[key] = e.target.checked;
2390                    saveSettings();
2391                });
2392            }
2393        });
2394
2395        // Custom CSS - handled by Apply button in showCategory
2396
2397        // Reset button
2398        const resetBtn = document.getElementById('reset-settings-btn');
2399        if (resetBtn) {
2400            resetBtn.addEventListener('click', () => {
2401                if (confirm('Are you sure you want to reset all settings to default?')) {
2402                    settings = {
2403                        theme: 'default',
2404                        colorMode: 'dark',
2405                        messageGrouping: true,
2406                        showTimestamps: true,
2407                        showAvatars: true,
2408                        fontSize: 'medium',
2409                        emojiSize: 'medium',
2410                        animations: true,
2411                        soundEffects: false,
2412                        notifications: false,
2413                        desktopNotifications: false,
2414                        enterToSend: true,
2415                        muteOnJoinVoice: false,
2416                        playJoinLeaveSounds: true,
2417                        autoScrollNewMessages: true,
2418                        showLinkPreviews: true,
2419                        showOnlineStatus: true,
2420                        confirmBeforeLeavingVoice: true
2421                    };
2422                    saveSettings();
2423                    populateSettingsForm();
2424                }
2425            });
2426        }
2427    }
2428
2429    /**
2430     * Get setting value
2431     */
2432    function getSetting(key) {
2433        return settings[key];
2434    }
2435
2436    /**
2437     * Set setting value
2438     */
2439    function setSetting(key, value) {
2440        settings[key] = value;
2441        saveSettings();
2442    }
2443
2444    function syncChatroomThemeFromSiteEvent(detail) {
2445        if (!detail) return;
2446        if (detail.palette) {
2447            settings.theme = PALETTE_THEMES.includes(detail.palette) ? detail.palette : 'default';
2448        }
2449        if (detail.colorMode === 'light' || detail.colorMode === 'dark') {
2450            settings.colorMode = detail.colorMode;
2451        } else if (typeof detail.darkMode === 'boolean') {
2452            settings.colorMode = detail.darkMode ? 'dark' : 'light';
2453        }
2454    }
2455
2456    function persistThemeToLocalStorage() {
2457        try {
2458            localStorage.setItem('chatroom_settings', JSON.stringify(settings));
2459        } catch (e) {
2460            console.error('Error saving local settings:', e);
2461        }
2462    }
2463
2464    function syncThemeControlsInForm() {
2465        const theme = PALETTE_THEMES.includes(settings.theme) ? settings.theme : 'default';
2466        settings.theme = theme;
2467        const themeSelect = document.getElementById('setting-theme');
2468        if (themeSelect && themeSelect.querySelector('option[value="' + theme + '"]')) {
2469            themeSelect.value = theme;
2470        }
2471        const colorToggle = document.getElementById('setting-color-mode');
2472        if (colorToggle) {
2473            colorToggle.checked = settings.colorMode === 'dark';
2474        }
2475        const colorModeCardEl = document.getElementById('chatroom-color-mode-card');
2476        if (colorModeCardEl) {
2477            colorModeCardEl.classList.toggle('is-dark', settings.colorMode === 'dark');
2478        }
2479    }
2480
2481    function syncThemeFromSite(source, detail) {
2482        if (source === 'chatroom-settings') return;
2483        if (detail) {
2484            syncChatroomThemeFromSiteEvent(detail);
2485        } else {
2486            hydrateThemeFromSite();
2487        }
2488        syncThemeControlsInForm();
2489        persistThemeToLocalStorage();
2490        if (isChatroomRouteActive()) {
2491            applySettings();
2492        }
2493    }
2494
2495    document.addEventListener('pupwife-color-mode', function (e) {
2496        if (!e.detail) return;
2497        syncThemeFromSite(e.detail.source, e.detail);
2498    });
2499
2500    document.addEventListener('pupwife-site-theme', function (e) {
2501        if (!e.detail || !e.detail.palette) return;
2502        syncThemeFromSite(e.detail.source, e.detail);
2503    });
2504
2505    // Initialize on load
2506    if (document.readyState === 'loading') {
2507        document.addEventListener('DOMContentLoaded', init);
2508    } else {
2509        init();
2510    }
2511
2512    return {
2513        getSetting,
2514        setSetting,
2515        getColorMode,
2516        setColorMode,
2517        showSettings,
2518        hideSettings,
2519        init,
2520        applySettings,
2521        isRouteActive: isChatroomRouteActive
2522    };
2523})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.