Dreadcast Dynamic Messages V1

Messagerie dynamique

Versione datata 11/04/2025. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name           Dreadcast Dynamic Messages V1
// @namespace      http://tampermonkey.net/
// @version        1.0.1
// @description    Messagerie dynamique
// @author         Laïn
// @match          https://www.dreadcast.net/Main*
// @grant          GM_addStyle
// @grant          GM_xmlhttpRequest
// @grant          unsafeWindow
// @grant          GM_info
// ==/UserScript==


(function() {
    'use strict';


    // --- Global Variables & Constants ---
    let MY_NAME = null;
    const ACTIVE_CONVERSATIONS = {}; // Stores { customWindow, originalWindow, latestMessageId, oldestMessageId, allMessagesLoaded, isLoadingOlder, participants, hasUnreadNotification, muteTimerIntervalId }
    let openingMutedOverride = null;
    let openingMutedOverrideTimer = null;
    const INITIAL_LOAD_COUNT = 20;
    const LOAD_MORE_COUNT = 25;
    // Click simulation delays
    const REFIND_DELAY = 50;
    const UI_CLICK_DELAY = 50;
    const UI_WAIT_DELAY = 100;
    const WAIT_FOR_ELEMENT_TIMEOUT = 1500;
    const NOTIFICATION_SOUND_URL = 'https://opengameart.org/sites/default/files/audio_preview/GUI%20Sound%20Effects_031.mp3.ogg';
    const UNOPENED_NOTIFICATION_SOUND_URL = 'https://orangefreesounds.com/wp-content/uploads/2020/10/Simple-notification-alert.mp3';

    // --- Version Info ---
    const SCRIPT_VERSION = '0.9.9-global-mute'; // <<< UPDATED version number

    // --- UI Constants ---
    const MIN_WINDOW_WIDTH = 300;
    const MIN_WINDOW_HEIGHT = 200;
    const DEFAULT_THEME_COLOR = '#0b5a9c';
    const GLOBAL_THEME_STORAGE_KEY = 'dmm_theme_color_v1';
    const CONVERSATION_COLORS_STORAGE_KEY = 'dmm_conversation_colors_v2';
    // Notification Colors
    const UNREAD_NOTIFICATION_COLOR = '#cca300';
    const UNREAD_TEXT_COLOR = '#101010';
    const UNREAD_BORDER_COLOR = '#b0891a';
    // Sidebar Mute Highlight Color
    const SIDEBAR_MUTED_COLOR = '#ff6666'; // A noticeable red/pink

    // --- Global Sound Mute State --- <<< NEW >>>
    let isGloballyMuted = false;
    const GLOBAL_SOUND_MUTE_STORAGE_KEY = 'dmm_global_sound_mute_v1';
    const GLOBAL_MUTE_BUTTON_ID = 'dmm-global-mute-button';

    // --- Mute Constants & Storage (v3 - Timed Mutes + Selected Duration) ---
    const MUTED_CONVERSATIONS_STORAGE_KEY_V3 = 'dmm_muted_conversations_v3'; // New key for new format
    const MUTE_DURATIONS = { // milliseconds, null for forever, 0 for unmute
        UNMUTE: 0,
        TWO_MINUTES: 2 * 60 * 1000,
        FIFTEEN_MINUTES: 15 * 60 * 1000,
        ONE_HOUR: 60 * 60 * 1000,
        FOREVER: null
    };

    // --- Global Sound Mute Utilities --- <<< NEW >>>
    function loadGlobalMuteState() {
        try {
            const storedValue = localStorage.getItem(GLOBAL_SOUND_MUTE_STORAGE_KEY);
            isGloballyMuted = storedValue === 'true'; // localStorage stores strings
            // console.log(`DMM Global Mute: Initial state loaded: ${isGloballyMuted}`);
        } catch (e) {
            console.error("DMM Global Mute: Failed to load state from localStorage.", e);
            isGloballyMuted = false; // Default to unmuted on error
        }
    }

    function saveGlobalMuteState() {
        try {
            localStorage.setItem(GLOBAL_SOUND_MUTE_STORAGE_KEY, String(isGloballyMuted));
            // console.log(`DMM Global Mute: State saved: ${isGloballyMuted}`);
        } catch (e) {
            console.error("DMM Global Mute: Failed to save state to localStorage.", e);
        }
    }

    function updateGlobalMuteButtonAppearance() {
        const button = document.getElementById(GLOBAL_MUTE_BUTTON_ID);
        if (button) {
            if (isGloballyMuted) {
                button.textContent = '🔇'; // Muted icon
                button.title = 'Activer les sons du script DMM';
                button.style.textDecoration = 'line-through';
                button.style.opacity = '0.7';
                button.style.fontSize = '1.5em'; // 50% larger
            } else {
                button.textContent = '🔈'; // Unmuted icon
                button.title = 'Couper les sons du script DMM';
                button.style.textDecoration = 'none';
                button.style.opacity = '1';
                button.style.fontSize = '1.5em'; // 50% larger
            }
        }
    }

    async function createGlobalMuteButton() {
        try {
            const newsDiv = await waitForElement('.news', 5000); // Wait for the news div
            if (!newsDiv || document.getElementById(GLOBAL_MUTE_BUTTON_ID)) {
                if (!newsDiv) console.warn("DMM Global Mute: '.news' div not found to attach button.");
                return; // Don't add if already exists or target not found
            }

            const muteButton = document.createElement('span'); // Using span for inline flow
            muteButton.id = GLOBAL_MUTE_BUTTON_ID;
            muteButton.style.cursor = 'pointer';
            muteButton.style.marginLeft = '10px'; // Space after news link
            muteButton.style.fontSize = '1em'; // Adjust size if needed
            muteButton.style.verticalAlign = 'middle'; // Align with text
            muteButton.style.zIndex = '9999999999'; // Very high z-index as requested
            muteButton.style.position = 'relative'; // Needed for z-index to reliably apply vs static elements

            muteButton.addEventListener('click', () => {
                isGloballyMuted = !isGloballyMuted;
                saveGlobalMuteState();
                updateGlobalMuteButtonAppearance();
                console.log(`DMM Global Mute: Toggled to ${isGloballyMuted}`);
            });

            // Insert the button after the news div
            newsDiv.insertAdjacentElement('afterend', muteButton);

            // Set initial appearance based on loaded state
            updateGlobalMuteButtonAppearance();

            console.log("DMM Global Mute: Button created and attached.");

        } catch (error) {
            console.error("DMM Global Mute: Error creating or attaching global mute button:", error);
        }
    }
    // --- End Global Sound Mute Utilities ---


    function getMutedData() {
        try {
            // Prioritize V3, fallback to V2 for migration (optional, simpler to just use V3)
            let stored = localStorage.getItem(MUTED_CONVERSATIONS_STORAGE_KEY_V3);
            if (!stored) {
                 // Optional: Add migration logic here if needed from V2
                 // For simplicity, we'll just start fresh with V3
                 stored = '{}'; // Start with empty V3 data
                 // console.log("DMM Mute: Initializing V3 mute storage."); // Less verbose
            }

            const parsed = JSON.parse(stored);
            // Basic validation: ensure it's an object
            return (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) ? parsed : {};
        } catch (e) {
            console.error("DMM: Failed to parse muted conversation data (v3) from localStorage.", e);
            return {}; // Return empty object on error
        }
    }

    function saveMutedData(muteDataObject) {
        if (typeof muteDataObject !== 'object' || muteDataObject === null || Array.isArray(muteDataObject)) {
             console.error("DMM SaveMutedData: Attempted to save non-object:", muteDataObject);
             return;
        }
        try {
            localStorage.setItem(MUTED_CONVERSATIONS_STORAGE_KEY_V3, JSON.stringify(muteDataObject));
        } catch (e) {
            console.error("DMM: Failed to save muted conversation data (v3) to localStorage.", e);
        }
    }

    // Checks if a conversation is *currently* muted, cleans up expired entries AND updates sidebar UI
    function isConversationMuted(conversationId) {
        const idStr = String(conversationId);
        if (!idStr) return false;

        let mutedData = getMutedData();
        const entry = mutedData[idStr];
        let isCurrentlyMuted = false;
        let dataNeedsSaving = false;
        let needsSidebarUpdate = false; // Flag

        if (entry && typeof entry === 'object') {
            const endTime = entry.muteEndTime;
            if (endTime === null) { // Permanent mute
                isCurrentlyMuted = true;
            } else if (typeof endTime === 'number' && endTime > 0) {
                if (Date.now() < endTime) { // Timed mute still active
                    isCurrentlyMuted = true;
                } else {
                    // Mute expired! Clean up.
                    // console.log(`%cDMM Mute Check: Mute expired for ${idStr}. Removing entry.`, "color: gray;"); // Less verbose
                    delete mutedData[idStr];
                    dataNeedsSaving = true;
                    isCurrentlyMuted = false;
                    needsSidebarUpdate = true; // Trigger sidebar update on expiry
                }
            } else {
                // Invalid entry (missing endTime or invalid type), clean up
                console.warn(`DMM Mute Check: Found invalid mute entry format for ${idStr}. Removing.`, entry);
                delete mutedData[idStr];
                dataNeedsSaving = true;
                isCurrentlyMuted = false;
                needsSidebarUpdate = true; // Trigger sidebar update on cleanup
            }
        } else {
             // No entry or invalid entry type
             if (entry) { // If entry existed but was invalid type
                 console.warn(`DMM Mute Check: Found invalid mute entry type for ${idStr}. Removing.`, entry);
                 delete mutedData[idStr];
                 dataNeedsSaving = true;
                 needsSidebarUpdate = true; // Trigger sidebar update on cleanup
             }
             isCurrentlyMuted = false;
        }

        if (dataNeedsSaving) {
            saveMutedData(mutedData);
        }

        if (needsSidebarUpdate) { // NEW: Update sidebar if status changed due to expiry/cleanup
            updateSidebarMuteStatus(idStr);
        }

        return isCurrentlyMuted;
    }

    // Gets the current mute end time (null for permanent, 0 if not muted/expired, timestamp otherwise)
    function getConversationMuteEndTime(conversationId) {
         const idStr = String(conversationId);
         if (!idStr) return 0; // Treat as not muted

         let mutedData = getMutedData();
         const entry = mutedData[idStr];

         if (entry && typeof entry === 'object') {
             const endTime = entry.muteEndTime;
             if (endTime === null) {
                 return null; // Permanent
             } else if (typeof endTime === 'number' && endTime > 0) {
                 if (Date.now() < endTime) {
                     return endTime; // Active timed mute
                 } else {
                     // Expired, trigger potential cleanup and return 0
                      isConversationMuted(idStr); // Trigger cleanup side-effect (which now also updates sidebar)
                      return 0; // Treat as not muted
                 }
             }
         }
         // No valid entry or expired
         return 0; // Treat as not muted
    }

    // Gets the originally selected mute duration (used for checkmarks)
    function getConversationMuteSelectedDuration(conversationId) {
        const idStr = String(conversationId);
        if (!idStr) return MUTE_DURATIONS.UNMUTE; // Treat as unmuted

        const mutedData = getMutedData();
        const entry = mutedData[idStr];

        // First, check if it's *currently* muted at all
        if (!isConversationMuted(idStr)) {
            return MUTE_DURATIONS.UNMUTE; // Return Unmute duration if not currently muted
        }

        // If muted, retrieve the stored selected duration
        if (entry && typeof entry === 'object' && (typeof entry.selectedDuration === 'number' || entry.selectedDuration === null)) {
            return entry.selectedDuration;
        }

        // Fallback if data is somehow inconsistent (shouldn't happen with proper saving)
        console.warn(`DMM getSelectedDuration: Mute entry exists for ${idStr} but selectedDuration is missing/invalid.`, entry);
        // Try to infer based on endTime
        const endTime = entry?.muteEndTime;
        if (endTime === null) return MUTE_DURATIONS.FOREVER;
        // Cannot reliably infer timed duration, default to Unmute status display
        return MUTE_DURATIONS.UNMUTE;
    }


    // Sets mute status AND updates sidebar UI
    function setConversationMuted(conversationId, durationMs) {
        const idStr = String(conversationId);
        if (!idStr) return;

        let mutedData = getMutedData();
        let needsSave = false;
        const wasPreviouslyMuted = isConversationMuted(idStr); // Check *before* changing

        if (durationMs === MUTE_DURATIONS.UNMUTE) { // Unmute
            if (mutedData[idStr]) {
                delete mutedData[idStr];
                needsSave = true;
                console.log(`%cDMM Mute Set: Unmuted conversation ${idStr}.`, "color: green;");
            }
        } else if (durationMs === MUTE_DURATIONS.FOREVER) { // Mute forever
             if (!mutedData[idStr] || mutedData[idStr]?.muteEndTime !== null || mutedData[idStr]?.selectedDuration !== durationMs) {
                mutedData[idStr] = { muteEndTime: null, selectedDuration: MUTE_DURATIONS.FOREVER }; // Store duration
                needsSave = true;
                console.log(`%cDMM Mute Set: Muted conversation ${idStr} FOREVER.`, "color: orange;");
             }
        } else if (typeof durationMs === 'number' && durationMs > 0) { // Timed mute
            const endTime = Date.now() + durationMs;
            // Update only if end time or selected duration changes
            if (!mutedData[idStr] || mutedData[idStr]?.muteEndTime !== endTime || mutedData[idStr]?.selectedDuration !== durationMs) {
                mutedData[idStr] = { muteEndTime: endTime, selectedDuration: durationMs }; // Store duration
                needsSave = true;
                const durationMinutes = durationMs / (60 * 1000);
                console.log(`%cDMM Mute Set: Muted conversation ${idStr} for ${durationMinutes} minutes (until ${new Date(endTime).toLocaleTimeString()}).`, "color: orange;");
            }
        } else {
             console.warn(`DMM Mute Set: Invalid duration provided for ${idStr}:`, durationMs);
        }

        if (needsSave) {
             saveMutedData(mutedData);
        }

        const isNowMuted = isConversationMuted(idStr); // Check *after* changing

        // --- Trigger UI Updates ---
        // 1. Update DMM Window (Header, Menu Checkmarks/Timers, Theme)
        const convData = ACTIVE_CONVERSATIONS[idStr];
        if (convData?.customWindow && document.body.contains(convData.customWindow)) {
             updateHeaderMuteStatus(convData.customWindow, idStr);
             updateMuteOptionsUI(convData.customWindow, idStr); // <<< This now updates timer too
             applyCurrentTheme(convData.customWindow, idStr);
        }

        // 2. Update Sidebar List Item (if status changed)
        if (wasPreviouslyMuted !== isNowMuted || needsSave) { // Update if status flipped or if save happened (covers initial mute)
             updateSidebarMuteStatus(idStr);
        }
    }
    // --- End Mute Utilities (v3 + Sidebar Update Trigger) ---

    // --- Sidebar Mute UI Update Functions ---
    /**
     * Updates the visual style of a conversation item in the main message list (#liste_messages)
     * based on its current mute status.
     * @param {string} conversationId The ID of the conversation.
     */
    function updateSidebarMuteStatus(conversationId) {
        const listItem = document.getElementById(`message_${conversationId}`);
        if (!listItem) {
            // console.log(`DMM updateSidebarMuteStatus: LI #message_${conversationId} not found in DOM.`);
            return; // Element not visible (e.g., different folder) or doesn't exist
        }

        const titleElement = listItem.querySelector('.message_titre');
        if (!titleElement) {
            console.warn(`DMM updateSidebarMuteStatus: .message_titre not found within #message_${conversationId}`);
            return;
        }

        const currentlyMuted = isConversationMuted(conversationId); // Use the function that handles expiry checks

        if (currentlyMuted) {
            listItem.classList.add('dmm-muted-sidebar-item');
        } else {
            listItem.classList.remove('dmm-muted-sidebar-item');
        }
    }

    /**
     * Scans all visible message list items in the sidebar and updates their mute status highlighting.
     * Should be called on initial load and when the list content changes significantly.
     */
    function scanAndUpdateSidebarMutes() {
        const messageListItems = document.querySelectorAll('#liste_messages li.message[id^="message_"]');
        // console.log(`%cDMM scanAndUpdateSidebarMutes: Found ${messageListItems.length} items to check.`, "color: gray");
        messageListItems.forEach(item => {
            const conversationId = item.id.replace('message_', '');
            if (conversationId) {
                updateSidebarMuteStatus(conversationId);
            }
        });
    }
    // --- End Sidebar Mute UI Update Functions ---


    // --- Mute UI Update Functions ---
    /**
     * Updates the mute status display in the chat window header.
     * @param {HTMLElement} chatWindow - The custom chat window element.
     * @param {string} conversationId - The ID of the conversation.
     */
    function updateHeaderMuteStatus(chatWindow, conversationId) {
        if (!chatWindow || !document.body.contains(chatWindow)) return;

        const muteStatusDisplay = chatWindow.querySelector('.custom-chat-head .mute-status-display');
        if (!muteStatusDisplay) return;

        const endTime = getConversationMuteEndTime(conversationId); // null (forever), 0 (unmuted), or timestamp

        if (endTime === null) { // Permanent mute
            muteStatusDisplay.textContent = '🔈 Muted';
            muteStatusDisplay.style.display = 'inline-block';
            muteStatusDisplay.title = 'Cette conversation est muette de façon permanente.';
        } else if (endTime > 0) { // Timed mute active (endTime is a future timestamp)
            const now = Date.now();
            if (endTime > now) {
                const remainingSeconds = Math.round((endTime - now) / 1000);
                const remainingMinutes = Math.ceil(remainingSeconds / 60);

                if (remainingMinutes > 1) {
                    muteStatusDisplay.textContent = `🔈 ${remainingMinutes} min`;
                    muteStatusDisplay.title = `Muet pour encore ${remainingMinutes} minutes (jusqu'à ${new Date(endTime).toLocaleTimeString()}).`;
                } else if (remainingSeconds > 0) {
                    muteStatusDisplay.textContent = '🔈 <1 min';
                    muteStatusDisplay.title = `Muet pour moins d'une minute (jusqu'à ${new Date(endTime).toLocaleTimeString()}).`;
                } else {
                    // Should technically be caught by getConversationMuteEndTime returning 0, but safe fallback
                    muteStatusDisplay.style.display = 'none';
                    muteStatusDisplay.textContent = '';
                    muteStatusDisplay.title = '';
                }
                 muteStatusDisplay.style.display = 'inline-block';
            } else {
                // Mute just expired, hide display (isConversationMuted will handle cleanup later)
                muteStatusDisplay.style.display = 'none';
                muteStatusDisplay.textContent = '';
                muteStatusDisplay.title = '';
            }
        } else { // Not muted (endTime is 0 or invalid)
            muteStatusDisplay.style.display = 'none';
            muteStatusDisplay.textContent = '';
            muteStatusDisplay.title = '';
        }
    }

    /**
     * Updates the checkmarks, styles, and timer display for mute options in the menu.
     * @param {HTMLElement} chatWindow - The custom chat window element containing the menu.
     * @param {string} conversationId - The ID of the conversation.
     */
    function updateMuteOptionsUI(chatWindow, conversationId) { // <<< MODIFIED >>>
        if (!chatWindow || !document.body.contains(chatWindow)) return;

        const muteOptionsContainer = chatWindow.querySelector('.more-opts-menu .mute-options-container');
        if (!muteOptionsContainer) return;

        const currentEndTime = getConversationMuteEndTime(conversationId); // null (forever), 0 (unmuted), or timestamp
        const currentlySelectedDuration = getConversationMuteSelectedDuration(conversationId); // 0, null, or duration ms
        const isTimedMuteActive = typeof currentEndTime === 'number' && currentEndTime > 0;

        muteOptionsContainer.querySelectorAll('.mute-option-item').forEach(item => {
            const checkmark = item.querySelector('.checkmark');
            const textSpan = item.querySelector('.item-text'); // Get the text span
            if (!checkmark || !textSpan) return;

            const itemDurationStr = item.dataset.duration;
            let itemDuration;
            if (itemDurationStr === 'null') itemDuration = null;
            else itemDuration = parseInt(itemDurationStr, 10);

            // --- Restore Original Label ---
            // Store original label if not already stored
            if (!item.dataset.originalLabel) {
                item.dataset.originalLabel = textSpan.textContent;
            }
            // Always reset to original label before adding timer or checkmark styling
            textSpan.textContent = item.dataset.originalLabel;

            // --- Reset styles ---
            checkmark.style.display = 'none';
            item.style.fontWeight = 'normal'; // Reset font weight

            // --- Check if this item matches the currently active selection ---
            if (itemDuration === currentlySelectedDuration) {
                checkmark.style.display = 'inline'; // Show checkmark
                item.style.fontWeight = 'bold'; // Optional: make selected bold

                // --- Add Timer Display if this is the ACTIVE TIMED mute ---
                if (isTimedMuteActive && itemDuration === currentlySelectedDuration && typeof itemDuration === 'number' && itemDuration > 0) {
                     const remainingMs = currentEndTime - Date.now();
                     const formattedTime = formatRemainingTime(remainingMs);
                     if (formattedTime) {
                         // Append timer to the (restored) original label
                         textSpan.textContent += ` (${formattedTime})`;
                         item.title = `${item.dataset.originalLabel} (Fin: ${new Date(currentEndTime).toLocaleTimeString()})`; // Update title too
                     } else {
                         // Mute expired just now? Reset title
                         item.title = item.dataset.originalLabel;
                     }
                } else {
                     // Reset title if it's not the active timed mute
                     item.title = item.dataset.originalLabel;
                 }
            } else {
                // Reset title if it's not selected at all
                 item.title = item.dataset.originalLabel;
            }
        }); // --- End forEach item ---

        // Dim "Unmute" if already unmuted
        const unmuteItem = muteOptionsContainer.querySelector('.mute-option-item[data-duration="0"]');
        if (unmuteItem) {
            const isCurrentlyUnmuted = (currentEndTime === 0);
            unmuteItem.style.opacity = isCurrentlyUnmuted ? '0.6' : '1';
            unmuteItem.style.cursor = isCurrentlyUnmuted ? 'default' : 'pointer';

            // Ensure checkmark and bold are correctly applied if Unmute is the "selected" state
             if (isCurrentlyUnmuted && currentlySelectedDuration === MUTE_DURATIONS.UNMUTE) {
                 const unmuteCheckmark = unmuteItem.querySelector('.checkmark');
                 if (unmuteCheckmark) unmuteCheckmark.style.display = 'inline';
                 unmuteItem.style.fontWeight = 'bold'; // Also bold if selected
             }
        }
    } // --- End updateMuteOptionsUI ---
    // --- End Mute UI Update Functions ---


    // --- Observers ---
    let mainObserver = null; // Observes body for added original message windows
    let sidebarObserver = null; // Observes the #liste_messages content UL for changes
    let sidebarScanDebounceTimer = null; // Timer for debouncing sidebar scans

    // --- Utility Functions ---
    function getMyCharacterName() {
        const nameElement = document.getElementById('txt_pseudo');
        if (nameElement) return nameElement.textContent.trim();
        console.error("DMM: #txt_pseudo not found?");
        return null;
    }

    function waitForElement(selector, timeout = WAIT_FOR_ELEMENT_TIMEOUT, container = document) {
        return new Promise((resolve, reject) => {
            const startTime = Date.now();
            const interval = setInterval(() => {
                try {
                    const element = container.querySelector(selector);
                    // Check visibility more robustly
                    if (element && document.body.contains(element) && element.offsetParent !== null && getComputedStyle(element).visibility !== 'hidden' && getComputedStyle(element).display !== 'none') {
                        clearInterval(interval);
                        resolve(element);
                    } else if (Date.now() - startTime > timeout) {
                        clearInterval(interval);
                        reject(new Error(`Element ${selector} not found or not visible within ${timeout}ms`));
                    }
                } catch (e) {
                    clearInterval(interval);
                    console.error(`DMM waitForElement: Error during querySelector for "${selector}"`, e);
                    reject(new Error(`Error finding element ${selector}: ${e.message}`));
                }
            }, 50);
        });
    }

    /**
     * Formats remaining milliseconds into a user-friendly string.
     * @param {number} ms - Milliseconds remaining.
     * @returns {string} Formatted time string (e.g., "15 min left", "<1 min left").
     */
    function formatRemainingTime(ms) { // <<< NEW HELPER >>>
        if (ms <= 0) return ""; // No time left or invalid input

        const totalSeconds = Math.round(ms / 1000);
        const minutes = Math.floor(totalSeconds / 60);
        // const seconds = totalSeconds % 60; // Not using seconds for now

        if (minutes >= 1) {
            return `${minutes} min`;
        } else if (totalSeconds > 0) {
            return "<1 min";
        } else {
            return ""; // Should already be caught by ms <= 0, but safe fallback
        }
    }

    function bringWindowToFront(window) {
        // Get all DMM windows
        const allWindows = document.querySelectorAll('.custom-chat-window');
        let maxZ = 999999; // Base z-index

        // Find highest current z-index
        allWindows.forEach(w => {
            const z = parseInt(getComputedStyle(w).zIndex) || 0;
            maxZ = Math.max(maxZ, z);
        });

        // Set the clicked window higher than all others
        window.style.zIndex = (maxZ + 1).toString();
    }


    // --- Theme Color Management ---
    function getSavedGlobalThemeColor() {
        return localStorage.getItem(GLOBAL_THEME_STORAGE_KEY) || DEFAULT_THEME_COLOR;
    }
    function getConversationColors() {
        try {
            const stored = localStorage.getItem(CONVERSATION_COLORS_STORAGE_KEY);
            return stored ? JSON.parse(stored) : {};
        } catch (e) {
            console.error("DMM: Failed to parse conversation colors from localStorage.", e);
            return {};
        }
    }
    function saveConversationColors(allColors) {
        try {
            localStorage.setItem(CONVERSATION_COLORS_STORAGE_KEY, JSON.stringify(allColors));
        } catch (e) {
            console.error("DMM: Failed to save conversation colors to localStorage.", e);
        }
    }
    function getConversationSetting(conversationId) {
        const allColors = getConversationColors();
        const setting = allColors[conversationId];
        if (setting && typeof setting === 'object' && typeof setting.enabled === 'boolean' && typeof setting.color === 'string') {
            return setting;
        }
        return { enabled: false, color: DEFAULT_THEME_COLOR };
    }
    function setConversationSetting(conversationId, setting) {
        if (typeof setting !== 'object' || typeof setting.enabled !== 'boolean' || typeof setting.color !== 'string') {
            console.warn(`DMM: Invalid setting provided for conversation ${conversationId}:`, setting);
            return;
        }
        const allColors = getConversationColors();
        allColors[conversationId] = setting;
        saveConversationColors(allColors);
    }
    function applyCurrentTheme(chatWindow, conversationId) {
        if (!chatWindow || !conversationId) return;
        const specificSetting = getConversationSetting(conversationId);
        const globalColor = getSavedGlobalThemeColor();
        const effectiveColor = specificSetting.enabled ? specificSetting.color : globalColor;

        // Apply standard theme colors
        chatWindow.style.setProperty('--dmm-primary-color', effectiveColor);
        chatWindow.style.setProperty('--dmm-header-bg', `color-mix(in srgb, ${effectiveColor} 70%, #080808)`);
        chatWindow.style.setProperty('--dmm-button-hover-bg', `color-mix(in srgb, ${effectiveColor} 85%, #ffffff)`);
        chatWindow.style.setProperty('--dmm-bubble-timestamp', `color-mix(in srgb, ${effectiveColor} 40%, #ffffff)`);
        chatWindow.style.setProperty('--dmm-border-color', effectiveColor);
        chatWindow.style.setProperty('--dmm-menu-hover-bg', effectiveColor);
        chatWindow.style.setProperty('--dmm-resize-border', `color-mix(in srgb, ${effectiveColor} 50%, #667788)`);

        // Re-apply notification style if needed (theme change shouldn't clear it)
        const convData = ACTIVE_CONVERSATIONS[conversationId];
        const isMuted = isConversationMuted(conversationId);
        if (convData?.hasUnreadNotification && !isMuted) {
             chatWindow.classList.add('has-unread-notification');
        } else {
             chatWindow.classList.remove('has-unread-notification');
        }
    }
    function saveGlobalThemeColor(newColor) {
        localStorage.setItem(GLOBAL_THEME_STORAGE_KEY, newColor);
        for (const [convId, convData] of Object.entries(ACTIVE_CONVERSATIONS)) {
            if (convData.customWindow && document.body.contains(convData.customWindow)) {
                applyCurrentTheme(convData.customWindow, convId);
            }
        }
    }


    // --- Styles ---
     function addChatStyles() {
         GM_addStyle(`
             /* Styles pour la fenêtre de chat personnalisée */
             .custom-chat-window {
                 --dmm-primary-color: ${DEFAULT_THEME_COLOR}; /* Fallback */
                 --dmm-header-bg: color-mix(in srgb, var(--dmm-primary-color) 70%, #080808);
                 --dmm-button-hover-bg: color-mix(in srgb, var(--dmm-primary-color) 85%, #ffffff);
                 --dmm-bubble-timestamp: color-mix(in srgb, var(--dmm-primary-color) 40%, #ffffff);
                 --dmm-border-color: var(--dmm-primary-color);
                 --dmm-menu-hover-bg: var(--dmm-primary-color);
                 --dmm-resize-border: color-mix(in srgb, var(--dmm-primary-color) 50%, #667788);
                 position: fixed; z-index: 999999; width: 450px; height: 600px;
                 max-height: 95vh; max-width: 95vw; min-width: ${MIN_WINDOW_WIDTH}px; min-height: ${MIN_WINDOW_HEIGHT}px;
                 border: 1px solid var(--dmm-border-color); background-color: #101010; color: #e0e0e0;
                 border-radius: 5px; box-shadow: 0 0 15px color-mix(in srgb, var(--dmm-primary-color) 30%, transparent);
                 display: flex; flex-direction: column; top: 100px; left: calc(50% - 225px);
                 overflow: hidden; transition: max-height 0.3s ease-out, border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out; /* Added transitions */
             }
             .custom-chat-head {
                 background-color: var(--dmm-header-bg); color: #fff; padding: 6px 10px; font-weight: bold;
                 border-bottom: 1px solid var(--dmm-border-color); cursor: move; display: flex;
                 justify-content: space-between; align-items: center; border-radius: 5px 5px 0 0;
                 flex-shrink: 0; position: relative;
                 transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out, border-bottom-color 0.3s ease-in-out; /* Add transition for smooth color change */
             }
             /* Style for Unread Notification */
             .custom-chat-window.has-unread-notification {
                border-color: ${UNREAD_BORDER_COLOR}; /* Change main border */
                box-shadow: 0 0 15px color-mix(in srgb, ${UNREAD_NOTIFICATION_COLOR} 40%, transparent); /* Change shadow color */
             }
             .custom-chat-window.has-unread-notification .custom-chat-head {
                 background-color: ${UNREAD_NOTIFICATION_COLOR};
                 color: ${UNREAD_TEXT_COLOR};
                 border-bottom-color: ${UNREAD_BORDER_COLOR};
             }
             .custom-chat-window.has-unread-notification .custom-chat-head .title {
                 color: ${UNREAD_TEXT_COLOR}; /* Ensure title text is dark */
             }
             .custom-chat-window.has-unread-notification .custom-chat-head .controls span {
                 color: #333; /* Darker control icons on yellow */
             }
             .custom-chat-window.has-unread-notification .custom-chat-head .controls span:hover {
                 color: #000; /* Black on hover */
             }
             /* End Unread Notification */

             .custom-chat-head .title { flex-grow: 1; padding-right: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

             /* Styles for Header Mute Status */
             .custom-chat-head .mute-status-display {
                 font-size: 0.9em;
                 color: #ffcc66; /* Light orange/yellow */
                 margin-left: 10px; /* Space after title */
                 font-weight: normal;
                 display: none; /* Hidden by default */
                 vertical-align: middle; /* Align with title text */
                 white-space: nowrap; /* Prevent wrapping */
             }
             /* Unread notification state override for mute status */
              .custom-chat-window.has-unread-notification .custom-chat-head .mute-status-display {
                  color: var(--dmm-primary-color); /* Use theme color on yellow bg for contrast */
              }

             .custom-chat-head .controls { display: flex; align-items: center; flex-shrink: 0; white-space: nowrap; }
             .custom-chat-head .controls span { cursor: pointer; padding: 0 5px; font-size: 1.2em; font-family: Arial, sans-serif; font-weight: bold; user-select: none; line-height: 1; transition: color 0.2s ease; }
             .custom-chat-head .controls span:hover { color: #ffdddd; }
             .custom-chat-content { flex-grow: 1; overflow-y: auto; padding: 10px; background-color: #1a1a1a; display: flex; flex-direction: column; gap: 5px; transition: opacity 0.2s ease-out; }
             .custom-chat-content.loading::after { content: "Chargement des messages..."; display: block; text-align: center; padding: 20px; color: #ccc; font-style: italic; }
             .load-more-container { text-align: center; padding: 8px; border-bottom: 1px solid #2a2a2a; margin-bottom: 5px; flex-shrink: 0; }
             .load-more-link { color: #87ceeb; cursor: pointer; text-decoration: underline; font-size: 0.9em; }
             .load-more-link:hover { color: #aaeebb; }
             .load-more-link.loading { color: #aaa; cursor: default; text-decoration: none; }
             .load-more-link.loading::before { content: "Chargement... "; }
             .chat-bubble { max-width: 80%; padding: 8px 14px; border-radius: 18px; margin-bottom: 4px; line-height: 1.4; word-wrap: break-word; position: relative; }
             .bubble-content { white-space: pre-wrap; }
             .my-bubble { background-color: var(--dmm-primary-color); color: #ffffff; align-self: flex-end; border-bottom-right-radius: 5px; transition: background-color 0.3s ease-in-out; }
             .their-bubble { background-color: #3a3a3a; color: #e0e0e0; align-self: flex-start; border-bottom-left-radius: 5px; }
             .bubble-sender-name { font-size: 0.8em; font-weight: bold; color: #87ceeb; margin-bottom: 3px; }
             .bubble-timestamp { font-size: 0.7em; color: #a0a0a0; display: block; text-align: right; margin-top: 4px; clear: both; }
             .my-bubble .bubble-timestamp { color: var(--dmm-bubble-timestamp); transition: color 0.3s ease-in-out;}
             .custom-chat-reply { padding: 8px; border-top: 1px solid var(--dmm-border-color); background-color: #101010; display: flex; gap: 5px; flex-shrink: 0; transition: opacity 0.2s ease-out, border-color 0.3s ease-in-out; }
             .custom-chat-reply textarea { flex-grow: 1; height: 50px; min-height: 30px; max-height: 150px; padding: 5px; border: 1px solid #333; background-color: #222; color: #ddd; resize: vertical; font-family: inherit; font-size: 0.9em; }
             .custom-chat-reply button { padding: 10px 15px; background-color: var(--dmm-primary-color); color: white; border: none; border-radius: 3px; cursor: pointer; font-weight: bold; align-self: center; transition: background-color 0.2s ease; }
             .custom-chat-reply button:hover { background-color: var(--dmm-button-hover-bg); }
             .custom-chat-reply button:disabled { background-color: #555; cursor: not-allowed; }
             .hidden-original-databox { position: absolute !important; top: -9999px !important; left: -9999px !important; opacity: 0 !important; pointer-events: none !important; z-index: -1 !important; width: 1px !important; height: 1px !important; overflow: hidden !important; }
             .custom-chat-window.collapsed { max-height: 35px !important; min-height: 35px !important; height: 35px !important; }
             .custom-chat-window.collapsed .custom-chat-content,
             .custom-chat-window.collapsed .custom-chat-reply,
             .custom-chat-window.collapsed .participants-panel,
             .custom-chat-window.collapsed .color-picker-panel,
             .custom-chat-window.collapsed .more-opts-menu,
             .custom-chat-window.collapsed .resize-handle,
             .custom-chat-window.collapsed .mute-status-display { display: none; } /* Hide mute status when collapsed */
             .custom-chat-window.collapsed .custom-chat-head { border-radius: 5px; }
             .custom-chat-head .controls .theme-btn { padding: 0 8px; font-size: 1.1em; margin-right: 5px; font-family: 'Segoe UI Symbol', Arial, sans-serif; }
             .custom-chat-head .controls .participants-btn { padding: 0 8px; font-size: 1.2em; margin-right: 5px; font-family: 'Segoe UI Symbol', Arial, sans-serif; }
             .custom-chat-head .controls .more-opts-btn { padding: 0 8px; font-size: 1.4em; font-weight: bold; margin-right: 5px; font-family: 'Segoe UI Symbol', Arial, sans-serif; }
             .custom-chat-window .more-opts-menu { position: absolute; top: 35px; right: 5px; background-color: #2a2a2a; border: 1px solid var(--dmm-border-color); border-radius: 4px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); z-index: 1000001; min-width: 150px; padding: 5px 0; display: none; transition: border-color 0.3s ease-in-out; }
             .custom-chat-window .color-picker-panel { position: absolute; top: 35px; right: 5px; background-color: #2a2a2a; border: 1px solid var(--dmm-border-color); border-radius: 4px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); z-index: 1000001; padding: 10px; display: none; text-align: center; transition: border-color 0.3s ease-in-out; }
             .custom-chat-window .color-picker-panel label { display: block; margin-bottom: 5px; font-size: 0.9em; color: #ccc; }
             .custom-chat-window .color-picker-panel input[type="color"] { cursor: pointer; border: 1px solid #555; width: 50px; height: 30px; padding: 0; background-color: #333; }
             .custom-chat-window .color-picker-panel .reset-color-btn {
                 cursor: pointer;
                 margin-left: 8px;
                 padding-bottom: 15px;
                 font-size: 1em;
                 vertical-align: middle;
                 opacity: 0.8;
             }
             .custom-chat-window .color-picker-panel .reset-color-btn:hover {
                 opacity: 1;
             }
             .custom-chat-window .more-opts-menu .menu-item { padding: 8px 15px; color: #e0e0e0; cursor: pointer; font-size: 0.9em; white-space: nowrap; }
             .custom-chat-window .more-opts-menu .menu-item:hover { background-color: var(--dmm-menu-hover-bg); color: #ffffff; }
             .custom-chat-window .more-opts-menu hr { border: none; border-top: 1px solid #444; margin: 5px 0; }
             /* Style for settings items (color) */
             .custom-chat-window .more-opts-menu .convo-settings-item { display: flex; align-items: center; justify-content: space-between; padding: 6px 15px; font-size: 0.9em; color: #ccc; }
             .custom-chat-window .more-opts-menu .convo-settings-item label { margin-right: 8px; white-space: nowrap; cursor: pointer; }
             .custom-chat-window .more-opts-menu .convo-settings-item input[type="checkbox"] { margin-right: 5px; cursor: pointer; vertical-align: middle; }
             .custom-chat-window .more-opts-menu .convo-settings-item input[type="color"] { cursor: pointer; border: 1px solid #555; width: 35px; height: 20px; padding: 0; background-color: #333; vertical-align: middle; }
             .custom-chat-window .more-opts-menu .convo-settings-item input[type="color"]:disabled { cursor: not-allowed; opacity: 0.5; }

             /* Mute options styling (with checkmark) */
             .custom-chat-window .more-opts-menu .mute-option-item {
                 display: flex; /* Use flexbox for easy alignment */
                 align-items: center;
                 /* Reuse base menu-item padding etc. */
             }
             .custom-chat-window .more-opts-menu .mute-option-item .checkmark {
                 display: none; /* Hidden by default */
                 margin-right: 8px; /* Space between checkmark and text */
                 color: #66ff66; /* Green checkmark */
                 font-weight: bold;
                 font-size: 1.1em;
                 line-height: 1; /* Ensure alignment */
             }
             .custom-chat-window .more-opts-menu .mute-option-item .item-text {
                 flex-grow: 1; /* Allow text to take remaining space */
                 /* Style for timer text within the label */
                 color: #e0e0e0; /* Ensure consistent color */
             }
             /* Style for the timer part specifically if needed (e.g., slightly dimmer) */
             .custom-chat-window .more-opts-menu .mute-option-item .item-text span {
                 /* Example: Make timer slightly dimmer */
                 /* color: #bbb; */
                 /* font-style: italic; */
             }


             /* Participants Panel */
             .participants-panel { position: absolute; top: 0; right: -250px; width: 250px; height: 100%; background-color: rgba(31, 31, 31, 0.95); border-left: 1px solid var(--dmm-border-color); box-shadow: -2px 0 5px rgba(0, 0, 0, 0.4); z-index: 999998; display: flex; flex-direction: column; transition: right 0.3s ease-in-out, border-color 0.3s ease-in-out; overflow: hidden; }
             .participants-panel.active { right: 0; }
             .participants-panel-header { padding: 8px 12px; font-weight: bold; color: #fff; background-color: var(--dmm-header-bg); border-bottom: 1px solid var(--dmm-border-color); flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out; }
             .participants-panel-header .close-panel-btn { font-size: 1.2em; cursor: pointer; padding: 0 5px; }
             .participants-panel-list { padding: 10px; overflow-y: auto; flex-grow: 1; color: #ccc; font-size: 0.9em; line-height: 1.4; }
             .participants-panel-list p { margin: 0; padding: 0; word-break: break-word; }
             .resize-handle { position: absolute; bottom: 0; right: 0; width: 15px; height: 15px; background-color: transparent; border-bottom: 2px solid var(--dmm-resize-border); border-right: 2px solid var(--dmm-resize-border); cursor: nwse-resize; z-index: 1000000; transition: border-color 0.3s ease-in-out; }
             .custom-chat-window.has-unread-notification .resize-handle { /* Optional: Change resize handle color too */
                 border-color: color-mix(in srgb, ${UNREAD_BORDER_COLOR} 50%, #667788);
             }

            /* Sidebar Mute Highlight */
            #liste_messages li.message.dmm-muted-sidebar-item .message_titre {
                color: ${SIDEBAR_MUTED_COLOR} !important; /* Use important to override potential inline styles */
                font-weight: bold; /* Optional: make it bolder */
            }
            #liste_messages li.message.dmm-muted-sidebar-item:hover .message_titre {
                /* Optional: Style on hover if needed */
                 text-decoration: line-through;
            }
            /* Ensure normal color when class is removed */
            #liste_messages li.message:not(.dmm-muted-sidebar-item) .message_titre {
                color: inherit !important; /* Revert to default color */
                font-weight: normal;
                text-decoration: none;
            }

         `);
     }


    // --- Dragging/Resizing ---
    function makeDraggable(element) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        const h = element.querySelector(".custom-chat-head");
        if (h) { h.onmousedown = dragMouseDown; }
        else { element.onmousedown = dragMouseDown; }
        function dragMouseDown(e) {
            // Prevent drag if clicking on controls, menus, panels, or resize handle
            if (e.target.closest('.controls span') || e.target.closest('.more-opts-menu') || e.target.closest('.participants-panel') || e.target.closest('.color-picker-panel') || e.target.classList.contains('resize-handle')) return;
            e = e || window.event;
            pos3 = e.clientX; pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }
        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY;
            pos3 = e.clientX; pos4 = e.clientY;
            element.style.top = Math.max(0, (element.offsetTop - pos2)) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
        }
        function closeDragElement() {
            document.onmouseup = null; document.onmousemove = null;
        }
    }
    function makeResizable(element, handle) {
        let startX, startY, startWidth, startHeight;
        handle.addEventListener('mousedown', function(e) {
            e.preventDefault();
            startX = e.clientX; startY = e.clientY;
            const computedStyle = document.defaultView.getComputedStyle(element);
            startWidth = parseInt(computedStyle.width, 10);
            startHeight = parseInt(computedStyle.height, 10);
            document.addEventListener('mousemove', doDrag, false);
            document.addEventListener('mouseup', stopDrag, false);
        }, false);
        function doDrag(e) {
            let newWidth = startWidth + e.clientX - startX;
            let newHeight = startHeight + e.clientY - startY;
            newWidth = Math.max(MIN_WINDOW_WIDTH, newWidth);
            newHeight = Math.max(MIN_WINDOW_HEIGHT, newHeight);
            newWidth = Math.min(window.innerWidth - element.offsetLeft - 5, newWidth); // Prevent dragging off-screen right
            newHeight = Math.min(window.innerHeight - element.offsetTop - 5, newHeight); // Prevent dragging off-screen bottom
            element.style.width = newWidth + 'px';
            element.style.height = newHeight + 'px';
        }
        function stopDrag() {
            document.removeEventListener('mousemove', doDrag, false);
            document.removeEventListener('mouseup', stopDrag, false);
        }
    }


    // --- Message Parsing and Fetching ---
    function parseMessageElement(element) {
        const id = element.id.replace('convers_', ''); // Used for *content fetching*, not the LI ID
        const timestamp = element.querySelector('.ligne1')?.textContent.trim();
        const senderLine = element.querySelector('.ligne2')?.textContent.trim();
        const senderMatch = senderLine?.match(/Message de (.*)/);
        const sender = senderMatch ? senderMatch[1] : '?';
        if (!id || !timestamp || !senderLine) {
            // console.warn("DMM parseMessageElement: Failed to parse element", element);
            return null;
        }
        return { id, timestamp, sender }; // 'id' here is the internal message_id for content
    }
    function fetchMessageContent(messageId, conversationId, callback) {
        GM_xmlhttpRequest({
            method: "GET",
            url: `https://www.dreadcast.net/Menu/Messaging/action=ReadMessage&id_message=${messageId}&id_conversation=${conversationId}`,
            timeout: 15000,
            onload: function(r) {
                if (r.status === 200 && r.responseText) {
                    try {
                        let p = new DOMParser(), x = p.parseFromString(r.responseText, "text/xml"), m = x.querySelector("message");
                        const content = m ? (m.textContent || m.innerHTML).trim() : "Erreur: Contenu message vide";
                        callback(content);
                    } catch (e) { console.error(`%cDMM fetchMessageContent[${conversationId}]: Parse XML error for msg ${messageId}`, "color: red", e); callback("Erreur: Parse XML"); }
                } else { console.warn(`%cDMM fetchMessageContent[${conversationId}]: Load error for msg ${messageId}, Status: ${r.status}`, "color: red"); callback(`Erreur: Load ${r.status}`); }
            },
            onerror: function(e) { console.error(`%cDMM fetchMessageContent[${conversationId}]: Network error for msg ${messageId}`, "color: red", e); callback("Erreur: Réseau"); },
            ontimeout: function() { console.warn(`%cDMM fetchMessageContent[${conversationId}]: Timeout for msg ${messageId}`, "color: red"); callback("Erreur: Timeout"); }
        });
    }
    async function parseAndFetchInitialMessages(originalWindow, conversationId) {
        const logPrefix = `DMM PFIM (${conversationId}):`;
        const defaultResult = { messages: [], participants: [], totalMessages: 0, latestId: null, oldestId: null, allLoaded: true };
        const conversationZone = originalWindow.querySelector('.zone_conversation');
        if (!conversationZone) { console.warn(`${logPrefix} No .zone_conversation found.`); return defaultResult; }
        let participants = [];
        try {
            const participantsTitleDiv = Array.from(conversationZone.querySelectorAll('div')).find(div => div.textContent.includes('Participants'));
            if (participantsTitleDiv) {
                let currentElement = participantsTitleDiv.nextElementSibling;
                let participantsString = '';
                // Find the <p> tag containing participant names, skipping over potential <br> or other tags.
                while(currentElement && currentElement.tagName !== 'P') {
                    currentElement = currentElement.nextElementSibling;
                }
                 // Check if we found the P tag and make sure it doesn't contain a message link (edge case)
                 if (currentElement && currentElement.tagName === 'P' && !currentElement.querySelector('.link.conversation')) {
                    participantsString = currentElement.textContent.trim();
                }
                if (participantsString) { participants = participantsString.split(',').map(name => name.trim()).filter(name => name.length > 0); }
                // else { console.warn(`${logPrefix} Participants list <p> tag not found or empty near title.`); }
            } // else { console.warn(`${logPrefix} Participants title div not found.`); }
        } catch (e) { console.warn(`${logPrefix} Failed to parse participants list`, e); }


        // Find elements with 'convers_' ID inside the zone_conversation to parse message details
        const allElements = Array.from(conversationZone.querySelectorAll('.link.conversation[id^="convers_"]'));
        const total = allElements.length;
        if (total === 0) { defaultResult.participants = participants; return defaultResult; }

        const fetchElements = allElements.slice(0, INITIAL_LOAD_COUNT);
        let fetchedData = [];
        let fetchPromises = [];
        for (const el of fetchElements) {
            const parsed = parseMessageElement(el); // Parse elements with 'convers_' ID
            if (parsed) {
                let msgData = { ...parsed, content: null }; // msgData.id is the message_id (from convers_ID)
                fetchedData.push(msgData);
                fetchPromises.push(new Promise((resolve) => {
                    fetchMessageContent(msgData.id, conversationId, (content) => { // Use msgData.id here
                        const targetMsg = fetchedData.find(m => m.id === msgData.id);
                        if (targetMsg) targetMsg.content = content;
                        resolve();
                    });
                }));
            }
        }
        try {
            if (fetchPromises.length > 0) await Promise.all(fetchPromises);
            fetchedData.reverse(); // Display oldest first within the initial batch
            const latestId = fetchedData.length > 0 ? fetchedData[fetchedData.length - 1].id : null;
            const oldestId = fetchedData.length > 0 ? fetchedData[0].id : null;
            const allLoaded = total <= INITIAL_LOAD_COUNT;
            const result = { messages: fetchedData, participants: participants, totalMessages: total, latestId: latestId, oldestId: oldestId, allLoaded: allLoaded };
            return result;
        } catch (error) {
            console.error(`${logPrefix} Error during Promise.all or processing:`, error);
            defaultResult.participants = participants;
            return defaultResult;
        }
    }


    // --- UI Building and Manipulation ---

    /**
     * Adds a message bubble to the chat window.
     * @param {object} msgData - The message data { id, timestamp, sender, content }. id is message_id.
     * @param {HTMLElement} container - The DOM element to add the bubble to (.custom-chat-content).
     * @param {string} conversationId - The ID of the conversation.
     * @param {boolean} [prepend=false] - True if the bubble should be added to the top (loading older).
     * @param {boolean} [isInitialLoad=false] - True if this bubble is being added during the initial window build.
     * @returns {string|null} The ID of the added message, or null if not added.
     */
    function addBubble(msgData, container, conversationId, prepend = false, isInitialLoad = false) {
        if (!MY_NAME || !container) {
            console.warn(`DMM addBubble: MY_NAME (${MY_NAME}) or container invalid. Cannot add bubble.`, msgData);
            return null;
        }
        const isMine = msgData.sender === MY_NAME;


        if (msgData.id) {
            const existingById = container.querySelector(`.chat-bubble[data-message-id="${msgData.id}"]`);
            if (existingById) {
                // console.log(`%cDMM addBubble: Preventing duplicate add for ID ${msgData.id}`, "color: gray");
                return null; // Return null to indicate it wasn't added
            }
        }

        const bubble = document.createElement('div');
        bubble.classList.add('chat-bubble');
        if (msgData.id) bubble.dataset.messageId = msgData.id; // Store message_id
        bubble.classList.add(isMine ? 'my-bubble' : 'their-bubble');

        if (!isMine) {
            const n = document.createElement('div');
            n.classList.add('bubble-sender-name');
            n.textContent = msgData.sender;
            bubble.appendChild(n);
        }

        const c = document.createElement('div');
        c.classList.add('bubble-content');
        const tempDiv = document.createElement('div');
        tempDiv.textContent = msgData.content || "..."; // Set text content safely
        c.innerHTML = tempDiv.innerHTML.replace(/\n/g, '<br>'); // Convert newlines to <br>
        bubble.appendChild(c);

        if (msgData.timestamp) {
            const t = document.createElement('span');
            t.classList.add('bubble-timestamp');
            t.textContent = msgData.timestamp;
            bubble.appendChild(t);
        }

        const isScrolledToBottom = Math.abs(container.scrollHeight - container.clientHeight - container.scrollTop) < 50;
        const shouldScrollDown = !prepend && (isScrolledToBottom || isMine);

        if (prepend) {
            const loadMoreElem = container.querySelector('.load-more-container');
            if (loadMoreElem) {
                loadMoreElem.insertAdjacentElement('afterend', bubble);
            } else {
                container.insertBefore(bubble, container.firstChild);
            }
        } else {
            container.appendChild(bubble);
        }

        // --- Play Sound & Set Notification State --- //
        if (!isMine && !prepend && !isInitialLoad) {
            const isConvoMuted = isConversationMuted(conversationId); // Conversation specific mute

            // Check BOTH conversation mute AND global mute <<< MODIFIED >>>
            if (!isConvoMuted && !isGloballyMuted) {
                // Sound
                try {
                    const audio = new Audio(NOTIFICATION_SOUND_URL);
                    audio.play().catch(e => {
                        console.warn("DMM: Open window audio playback failed (interaction might be required for sound):", e.name, e.message);
                    });
                } catch (e) {
                    console.error("DMM: Error creating or playing open window notification sound:", e);
                }

                // Visual Notification State (only if conversation is not muted)
                const convData = ACTIVE_CONVERSATIONS[conversationId];
                const chatWindow = container.closest('.custom-chat-window');
                 if (convData && chatWindow && !chatWindow.classList.contains('has-unread-notification')) {
                     convData.hasUnreadNotification = true;
                     chatWindow.classList.add('has-unread-notification');
                 }

            } else if (isConvoMuted) {
                 // Still handle visual notification if window is open and conversation is muted (no sound)
                 const convData = ACTIVE_CONVERSATIONS[conversationId];
                 const chatWindow = container.closest('.custom-chat-window');
                 if (convData && chatWindow && !chatWindow.classList.contains('has-unread-notification')) {
                    // We might still want to visually indicate a new message arrived, even if muted.
                    // If you DON'T want the yellow highlight for muted convos, remove this part.
                    // For now, let's keep it, as the yellow indicates 'unread' visually.
                    convData.hasUnreadNotification = true;
                    chatWindow.classList.add('has-unread-notification');
                 }
                 // console.log(`%cDMM addBubble: Skipped sound for MUTED conversation ${conversationId}`, "color: gray;");
             } else if (isGloballyMuted) {
                 // Globally muted - No sound, but still handle visual notification if window is open
                 const convData = ACTIVE_CONVERSATIONS[conversationId];
                 const chatWindow = container.closest('.custom-chat-window');
                 if (convData && chatWindow && !chatWindow.classList.contains('has-unread-notification')) {
                    convData.hasUnreadNotification = true;
                    chatWindow.classList.add('has-unread-notification');
                 }
                 // console.log(`%cDMM addBubble: Skipped sound due to GLOBAL mute for conversation ${conversationId}`, "color: gray;");
             }
        }


        if (shouldScrollDown) {
            requestAnimationFrame(() => {
                if (container && container.isConnected) {
                    container.scrollTop = container.scrollHeight;
                }
            });
        }
        return msgData.id; // Return the message_id of the added bubble
    } // --- END of addBubble function ---


    function buildInitialChatUI(messages, container, conversationId, allMessagesLoaded) {
        container.innerHTML = '';
        container.classList.remove('loading');
        if (!MY_NAME) {
            container.innerHTML = "<p style='color:red;'>Erreur: Nom utilisateur non trouvé.</p>";
            return { latestId: null, oldestId: null };
        }
        if (!allMessagesLoaded) {
            addLoadMoreLink(container, conversationId);
        }
        let firstId = null; // Oldest message_id in this batch
        let lastId = null; // Latest message_id in this batch
        messages.forEach(msg => {
            // Pass 'true' for the new isInitialLoad parameter
            const addedId = addBubble(msg, container, conversationId, false, true); // isInitialLoad=true here
            if (addedId) { // addedId is the message_id
                const addedIdNum = parseInt(addedId);
                if (!firstId || (addedIdNum < parseInt(firstId))) { firstId = addedId; }
                if (!lastId || (addedIdNum > parseInt(lastId))) { lastId = addedId; }
            }
        });
        // Scroll to bottom after initial build
        setTimeout(() => { if (container && container.isConnected) container.scrollTop = container.scrollHeight; }, 100);
        return { latestId: lastId, oldestId: firstId }; // Return message_ids
    }


    function addLoadMoreLink(container, conversationId) {
        if (container.querySelector('.load-more-container')) return; // Avoid adding multiple links
        const loadMoreContainer = document.createElement('div');
        loadMoreContainer.classList.add('load-more-container');
        const loadMoreLink = document.createElement('a');
        loadMoreLink.classList.add('load-more-link');
        loadMoreLink.textContent = 'Afficher les messages précédents';
        loadMoreLink.href = '#';
        loadMoreLink.onclick = (e) => {
            e.preventDefault();
            const cData = ACTIVE_CONVERSATIONS[conversationId];
            if (cData && !cData.isLoadingOlder) {
                loadOlderMessages(conversationId, loadMoreLink);
            }
        };
        loadMoreContainer.appendChild(loadMoreLink);
        container.insertBefore(loadMoreContainer, container.firstChild);
    }


    async function loadOlderMessages(conversationId, linkElement) {
        const cData = ACTIVE_CONVERSATIONS[conversationId];
        const container = cData?.customWindow?.querySelector('.custom-chat-content');
        if (!cData || !container || cData.isLoadingOlder || cData.allMessagesLoaded) {
            if (cData?.allMessagesLoaded && linkElement?.parentElement) linkElement.parentElement.remove(); // Clean up link if already loaded
            return;
        }
        cData.isLoadingOlder = true;
        linkElement.classList.add('loading');
        linkElement.textContent = ''; // Clear text while loading


        try {
             // Fetch the full conversation page HTML to get all message elements
             const response = await new Promise((resolve, reject) => {
                 GM_xmlhttpRequest({
                     method: "GET", url: `https://www.dreadcast.net/Menu/Messaging/action=OpenMessage&id_conversation=${conversationId}`, timeout: 15000,
                     onload: (res) => { if (res.status === 200 && res.responseText) resolve(res.responseText); else reject(`HTTP Status ${res.status}`); },
                     onerror: (err) => reject("Network Error"), ontimeout: () => reject("Timeout")
                 });
             });

             const parser = new DOMParser();
             const doc = parser.parseFromString(response, 'text/html');
             const messageList = doc.querySelector('.zone_conversation');
             if (!messageList) throw new Error("Could not find .zone_conversation in older messages response.");

             // Get all message links (convers_...) from the fetched HTML
             const allElements = Array.from(messageList.querySelectorAll('.link.conversation[id^="convers_"]'));
             const currentOldestId = cData.oldestMessageId; // This is a message_id
             let olderElementsToLoad = [];

             // Find the index of the *element* corresponding to the current oldest message ID
             const currentOldestIndex = allElements.findIndex(el => el.id.replace('convers_', '') === currentOldestId);

             if (currentOldestIndex !== -1 && currentOldestId) {
                 // Find elements *before* the current oldest one in the full list (since list is newest first)
                 const startIndex = currentOldestIndex + 1;
                 const endIndex = Math.min(startIndex + LOAD_MORE_COUNT, allElements.length);
                 olderElementsToLoad = allElements.slice(startIndex, endIndex);
                 // console.log(`DMM LoadOlder: Found current oldest index ${currentOldestIndex}. Slicing from ${startIndex} to ${endIndex} (total ${allElements.length})`);
             } else if (currentOldestId) {
                  console.warn(`DMM LoadOlder: Could not find index for current oldest message ID ${currentOldestId}. List might have changed?`);
                  linkElement.textContent = 'Erreur index'; linkElement.style.color = '#aaa'; linkElement.onclick = (e) => e.preventDefault();
                  cData.isLoadingOlder = false; // Unlock
                  return;
             } else {
                 // This case should ideally not happen if initial load worked, but handle it.
                 console.warn(`DMM LoadOlder: Cannot load older - current oldest message ID is null.`);
                 cData.allMessagesLoaded = true; // Assume loaded if we have no starting point
                 if(linkElement.parentElement) linkElement.parentElement.remove();
                 cData.isLoadingOlder = false; // Unlock
                 return;
             }


             if (olderElementsToLoad.length > 0) {
                 let fetchedData = []; let fetchPromises = [];
                 for (const el of olderElementsToLoad) {
                     const parsed = parseMessageElement(el); // Parse the 'convers_' element
                     // Ensure we don't re-add a bubble that might already exist somehow (defensive check)
                     if (parsed && !container.querySelector(`.chat-bubble[data-message-id="${parsed.id}"]`)) {
                         let msgData = { ...parsed, content: null }; // msgData.id is message_id
                         fetchedData.push(msgData);
                         fetchPromises.push(new Promise((resolve) => {
                             fetchMessageContent(msgData.id, conversationId, (content) => { // Use message_id
                                 const targetMsg = fetchedData.find(m => m.id === msgData.id);
                                 if (targetMsg) targetMsg.content = content; resolve();
                             });
                         }));
                     }
                 }

                 if (fetchPromises.length > 0) await Promise.all(fetchPromises);

                 fetchedData.reverse(); // Reverse so oldest fetched appears first (top) in the UI

                 const oldScrollHeight = container.scrollHeight; const oldScrollTop = container.scrollTop;

                 let newOldestId = cData.oldestMessageId; // Start with current oldest message_id
                 fetchedData.forEach(msg => {
                     // Pass 'false' for the isInitialLoad parameter when loading older
                     const prependedId = addBubble(msg, container, conversationId, true, false); // prepend=true, isInitialLoad=false; prependedId is message_id
                     // Update the overall oldest message ID if the prepended one is older
                     if (prependedId && (!newOldestId || (parseInt(prependedId) < parseInt(newOldestId)))) {
                         newOldestId = prependedId;
                     }
                 });

                 // Restore scroll position relative to the old top content
                 if(fetchedData.length > 0) {
                      const newScrollHeight = container.scrollHeight;
                      container.scrollTop = oldScrollTop + (newScrollHeight - oldScrollHeight);
                 }

                 cData.oldestMessageId = newOldestId; // Update the tracked oldest message ID

                 // Check if we've reached the end of the conversation history
                 const endReached = (currentOldestIndex + 1 + olderElementsToLoad.length) >= allElements.length;
                 if (endReached || olderElementsToLoad.length < LOAD_MORE_COUNT) {
                     cData.allMessagesLoaded = true;
                     if (linkElement.parentElement) linkElement.parentElement.remove(); // Remove the link
                 }
             } else if (!cData.allMessagesLoaded && currentOldestId) {
                 // If no elements were found to load, but we thought there were more
                 // console.log(`DMM LoadOlder: No older elements found after index ${currentOldestIndex}. Marking as fully loaded.`);
                 cData.allMessagesLoaded = true;
                 if (linkElement.parentElement) linkElement.parentElement.remove();
             }

        } catch (error) {
             console.error(`DMM: Error loading older messages for ${conversationId}:`, error);
             linkElement.textContent = 'Erreur chargement'; linkElement.style.color = 'red';
             linkElement.style.cursor = 'default'; linkElement.onclick = (e) => e.preventDefault(); // Prevent retries on error
        } finally {
             cData.isLoadingOlder = false;
             // Restore link text if still loading and no error occurred and not fully loaded
             if (!cData.allMessagesLoaded && linkElement.classList.contains('loading') && !linkElement.textContent.includes('Erreur')) {
                 linkElement.classList.remove('loading');
                 linkElement.textContent = 'Afficher les messages précédents';
             } else if (cData.allMessagesLoaded && linkElement.parentElement) {
                 // Ensure link is removed if finally block confirms all loaded
                 linkElement.parentElement.remove();
             }
        }
    }


    let closeChatWindow = (conversationId, options = { removeOriginal: true }) => {
        // Ensure timer clearing happens *before* data deletion
        const cData = ACTIVE_CONVERSATIONS[conversationId]; // Get data first
        if (cData?.muteTimerIntervalId) {
            clearInterval(cData.muteTimerIntervalId);
            // console.log(`%cDMM Mute Timer: Cleared interval ${cData.muteTimerIntervalId} on window close for ${conversationId}`, "color: gray");
        }

        const customWindowId = `custom-chat-${conversationId}`;
        const chatWindow = document.getElementById(customWindowId);
        if (chatWindow) { chatWindow.remove(); }

        if (cData) { // Check if data existed (might have already been partially cleaned)
            const oRef = cData.originalWindow;
             // Only remove original if requested AND it wasn't specifically revealed for 'invite' action
             const shouldRemove = options.removeOriginal && oRef?.dataset.modernized !== 'revealed_for_invite';
            if (shouldRemove && oRef?.parentNode) {
                try { oRef.remove(); } catch (e) { console.warn(`DMM: Error removing original window ${conversationId} on close:`, e); }
            }
            delete ACTIVE_CONVERSATIONS[conversationId]; // Delete data *after* using it
             // console.log(`DMM: Closed window and deleted data for conversation ${conversationId}. Original removed: ${shouldRemove}`);
        }
    };


    function populateConversationSettingsUI(conversationId, checkbox, colorInput) {
        const setting = getConversationSetting(conversationId);
        checkbox.checked = setting.enabled;
        colorInput.value = setting.enabled ? setting.color : getSavedGlobalThemeColor(); // Show global if disabled
        colorInput.disabled = !setting.enabled;
    }


     function createCustomWindow(conversationId, otherParticipantName_UNUSED, initialResult, originalWindowRef) {
         const { messages, participants, totalMessages, latestId: initialLatestId, oldestId: initialOldestId, allLoaded } = initialResult;
         const windowId = `custom-chat-${conversationId}`;
         if (document.getElementById(windowId)) {
             // If DMM window already exists, just update the original ref and hide the new one.
             const existingData = ACTIVE_CONVERSATIONS[conversationId];
             if (existingData) {
                 existingData.originalWindow = originalWindowRef;
             }
             if (originalWindowRef?.parentNode) {
                 originalWindowRef.classList.add('hidden-original-databox'); // Hide the duplicate original
                 originalWindowRef.dataset.modernized = 'replaced'; // Mark as handled
             }
             console.warn(`DMM: createCustomWindow called for existing ID ${conversationId}. Updated original ref.`);
             const existingWindow = document.getElementById(windowId);
             if(existingWindow) {
                 bringWindowToFront(existingWindow); // Use new function instead of direct z-index
                 const txtArea = existingWindow.querySelector('.custom-chat-reply textarea');
                 if (txtArea) txtArea.focus();
             }
            return; // Don't create a new window
         }


         // --- Create Window Structure ---
         const chatWindow = document.createElement('div');
         chatWindow.id = windowId;
         chatWindow.classList.add('custom-chat-window');
         chatWindow.style.zIndex = '999999'; // Set initial z-index
         applyCurrentTheme(chatWindow, conversationId);

         // Add click handler to bring window to front
         chatWindow.addEventListener('mousedown', function(e) {
             // Don't change z-index if clicking close button or menu items
             if (!e.target.closest('.controls span') &&
                 !e.target.closest('.more-opts-menu') &&
                 !e.target.closest('.participants-panel') &&
                 !e.target.closest('.color-picker-panel')) {
                 bringWindowToFront(chatWindow);
             }
         });

         const head = document.createElement('div'); head.classList.add('custom-chat-head'); let actualTitleText = `Conversation ${conversationId}`; const originalTitleElement = originalWindowRef?.querySelector('.head .title'); if (originalTitleElement) { actualTitleText = originalTitleElement.textContent.trim(); } else { actualTitleText = `Messages ${conversationId}`; } const title = document.createElement('span'); title.classList.add('title'); title.textContent = actualTitleText; title.title = actualTitleText;

         // Add Mute Status Display
         const muteStatusDisplay = document.createElement('span');
         muteStatusDisplay.classList.add('mute-status-display');
         title.insertAdjacentElement('afterend', muteStatusDisplay); // Insert after title

         const controls = document.createElement('div'); controls.classList.add('controls'); const themeBtn = document.createElement('span'); themeBtn.classList.add('theme-btn'); themeBtn.innerHTML = '🎨'; themeBtn.title = 'Changer la couleur GLOBALE du thème'; controls.appendChild(themeBtn); const participantsBtn = document.createElement('span'); participantsBtn.classList.add('participants-btn'); participantsBtn.innerHTML = '👥'; participantsBtn.title = 'Afficher les participants'; controls.appendChild(participantsBtn); const moreOptsBtn = document.createElement('span'); moreOptsBtn.classList.add('more-opts-btn'); moreOptsBtn.innerHTML = '⋮'; moreOptsBtn.title = "Plus d'options"; controls.appendChild(moreOptsBtn); const closeBtn = document.createElement('span'); closeBtn.innerHTML = '×'; closeBtn.title = 'Fermer'; controls.appendChild(closeBtn); head.appendChild(title); head.appendChild(controls); chatWindow.appendChild(head);

         const content = document.createElement('div'); content.classList.add('custom-chat-content', 'loading'); chatWindow.appendChild(content);

         const replyDiv = document.createElement('div'); replyDiv.classList.add('custom-chat-reply'); const textarea = document.createElement('textarea'); textarea.placeholder = 'Écrire un message...'; textarea.setAttribute('aria-label', 'Message reply input'); const sendButton = document.createElement('button'); sendButton.textContent = 'Envoyer'; replyDiv.appendChild(textarea); replyDiv.appendChild(sendButton); chatWindow.appendChild(replyDiv);

         // --- More Options Menu Structure ---
         const moreOptionsMenu = document.createElement('div'); moreOptionsMenu.classList.add('more-opts-menu');
         const inviteItem = document.createElement('div'); inviteItem.classList.add('menu-item'); inviteItem.textContent = 'Inviter'; inviteItem.dataset.action = 'invite'; moreOptionsMenu.appendChild(inviteItem);
         const markUnreadItem = document.createElement('div'); markUnreadItem.classList.add('menu-item'); markUnreadItem.textContent = 'Marquer non lu'; markUnreadItem.dataset.action = 'mark_unread'; moreOptionsMenu.appendChild(markUnreadItem);
         const deleteItem = document.createElement('div'); deleteItem.classList.add('menu-item'); deleteItem.textContent = 'Supprimer'; deleteItem.dataset.action = 'delete'; moreOptionsMenu.appendChild(deleteItem);
         const settingsHr = document.createElement('hr'); moreOptionsMenu.appendChild(settingsHr);

         // Specific Color Setting
         const convoSettingsWrapper = document.createElement('div'); convoSettingsWrapper.classList.add('convo-settings-item');
         const specificColorLabel = document.createElement('label'); specificColorLabel.textContent = 'Couleur Spécifique:'; specificColorLabel.htmlFor = `dmm-specific-cb-${conversationId}`;
         const specificColorCheckbox = document.createElement('input'); specificColorCheckbox.type = 'checkbox'; specificColorCheckbox.id = `dmm-specific-cb-${conversationId}`; specificColorCheckbox.title = 'Activer une couleur unique pour cette conversation';
         const specificColorInput = document.createElement('input'); specificColorInput.type = 'color'; specificColorInput.id = `dmm-specific-color-${conversationId}`; specificColorInput.title = 'Choisir la couleur spécifique pour cette conversation';
         convoSettingsWrapper.appendChild(specificColorLabel);
         convoSettingsWrapper.appendChild(specificColorCheckbox);
         convoSettingsWrapper.appendChild(specificColorInput);
         moreOptionsMenu.appendChild(convoSettingsWrapper);

         // --- Mute Options ---
         const muteOptionsTitle = document.createElement('div');
         muteOptionsTitle.textContent = 'Mute Options:';
         muteOptionsTitle.style.padding = '6px 15px 3px'; // Style as a title
         muteOptionsTitle.style.fontSize = '0.8em';
         muteOptionsTitle.style.color = '#aaa';
         muteOptionsTitle.style.fontWeight = 'bold';
         moreOptionsMenu.appendChild(muteOptionsTitle);

         const muteOptionsContainer = document.createElement('div');
         muteOptionsContainer.classList.add('mute-options-container'); // Add a class for styling/selection
         moreOptionsMenu.appendChild(muteOptionsContainer);

         // Define options
         const muteChoices = [
             { label: 'Unmute', duration: MUTE_DURATIONS.UNMUTE },
             { label: 'Mute 2 min', duration: MUTE_DURATIONS.TWO_MINUTES },
             { label: 'Mute 15 min', duration: MUTE_DURATIONS.FIFTEEN_MINUTES },
             { label: 'Mute 1 hour', duration: MUTE_DURATIONS.ONE_HOUR },
             { label: 'Mute Forever', duration: MUTE_DURATIONS.FOREVER }
         ];

         // Create mute option items with checkmarks
         muteChoices.forEach(choice => {
             const item = document.createElement('div');
             item.classList.add('menu-item', 'mute-option-item'); // Add class for click listener

             const checkmarkSpan = document.createElement('span');
             checkmarkSpan.classList.add('checkmark');
             checkmarkSpan.innerHTML = '✓';
             item.appendChild(checkmarkSpan);

             const textSpan = document.createElement('span');
             textSpan.classList.add('item-text');
             textSpan.textContent = choice.label; // Set initial label
             item.appendChild(textSpan);

             item.dataset.duration = choice.duration === null ? 'null' : String(choice.duration); // Store duration
             item.dataset.originalLabel = choice.label; // Store original label for timer reset
             item.title = `${choice.label}`; // Initial title
             muteOptionsContainer.appendChild(item);
         });
         // --- Fin Mute Options ---

         chatWindow.appendChild(moreOptionsMenu); // Add the completed menu

         // --- Other Panels ---
         const participantsPanel = document.createElement('div'); participantsPanel.classList.add('participants-panel'); const panelHeader = document.createElement('div'); panelHeader.classList.add('participants-panel-header'); panelHeader.textContent = 'Participants'; const closePanelBtn = document.createElement('span'); closePanelBtn.classList.add('close-panel-btn'); closePanelBtn.innerHTML = '×'; closePanelBtn.title = 'Fermer'; panelHeader.appendChild(closePanelBtn); participantsPanel.appendChild(panelHeader); const participantsListDiv = document.createElement('div'); participantsListDiv.classList.add('participants-panel-list'); participantsPanel.appendChild(participantsListDiv); chatWindow.appendChild(participantsPanel);

         const colorPickerPanel = document.createElement('div');
         colorPickerPanel.classList.add('color-picker-panel');
         const colorLabel = document.createElement('label');
         colorLabel.textContent = 'Couleur Globale :';
         colorPickerPanel.appendChild(colorLabel);
         const colorInput = document.createElement('input');
         colorInput.type = 'color';
         colorInput.value = getSavedGlobalThemeColor();
         const resetColorBtn = document.createElement('span'); // Add reset button
         resetColorBtn.textContent = '❌';
         resetColorBtn.title = 'Rétablir la couleur par défaut';
         resetColorBtn.classList.add('reset-color-btn');
         colorPickerPanel.appendChild(colorInput);
         colorPickerPanel.appendChild(resetColorBtn); // Add reset button
         chatWindow.appendChild(colorPickerPanel);

         const resizeHandle = document.createElement('div');
         resizeHandle.classList.add('resize-handle');
         resizeHandle.title = 'Redimensionner';
         chatWindow.appendChild(resizeHandle);

         // --- Event Listeners & State ---
         let clickOutsideMenuHandler = null; let clickOutsidePanelHandler = null; let clickOutsideColorPickerHandler = null;

         // Function and Listeners to Clear Notification
         const clearNotification = () => {
             const convData = ACTIVE_CONVERSATIONS[conversationId];
             if (convData && convData.hasUnreadNotification) {
                  const isMuted = isConversationMuted(conversationId);
                  if (!isMuted) {
                      convData.hasUnreadNotification = false;
                      chatWindow.classList.remove('has-unread-notification');
                      // console.log(`%cDMM: Cleared unread notification for ${conversationId} via interaction.`, "color: green");
                  } else {
                      convData.hasUnreadNotification = false; // Still clear the flag even if muted
                      // console.log(`%cDMM: Cleared hasUnreadNotification flag for MUTED conversation ${conversationId} via interaction.`, "color: gray");
                  }
             }
         };
         chatWindow.addEventListener('mousedown', clearNotification, true);
         chatWindow.addEventListener('focusin', clearNotification);


         const closeThisChatWindow = (options = { removeOriginal: true }) => {
             chatWindow.removeEventListener('mousedown', clearNotification, true);
             chatWindow.removeEventListener('focusin', clearNotification);

             // Call original close function - this handles interval clearing and data deletion now
             closeChatWindow(conversationId, options);

             // Cleanup click handlers
             if (clickOutsideMenuHandler) { document.removeEventListener('click', clickOutsideMenuHandler, true); clickOutsideMenuHandler = null; }
             if (clickOutsidePanelHandler) { document.removeEventListener('click', clickOutsidePanelHandler, true); clickOutsidePanelHandler = null; }
             if (clickOutsideColorPickerHandler) { document.removeEventListener('click', clickOutsideColorPickerHandler, true); clickOutsideColorPickerHandler = null; }
         };

         const closeOtherPopups = (except) => { if (except !== 'menu' && moreOptionsMenu.style.display === 'block') { moreOptionsMenu.style.display = 'none'; if (clickOutsideMenuHandler) document.removeEventListener('click', clickOutsideMenuHandler, true); clickOutsideMenuHandler = null; } if (except !== 'panel' && participantsPanel.classList.contains('active')) { participantsPanel.classList.remove('active'); if (clickOutsidePanelHandler) document.removeEventListener('click', clickOutsidePanelHandler, true); clickOutsidePanelHandler = null; } if (except !== 'color' && colorPickerPanel.style.display === 'block') { colorPickerPanel.style.display = 'none'; if (clickOutsideColorPickerHandler) document.removeEventListener('click', clickOutsideColorPickerHandler, true); clickOutsideColorPickerHandler = null; } };

         closeBtn.onclick = () => closeThisChatWindow({ removeOriginal: true });

         sendButton.onclick = () => { // No changes needed here for mute highlight
             const messageText = textarea.value.trim();
             const cData = ACTIVE_CONVERSATIONS[conversationId];
             if (!messageText || sendButton.disabled) { if (!messageText) textarea.focus(); return; }
             if (!cData) { console.error(`DMM SendError[${conversationId}]: Conversation data not found!`); alert("Erreur critique DMM: Données de conversation manquantes."); return; }

             const currentOriginalWindow = cData.originalWindow;
             if (!currentOriginalWindow || !document.body.contains(currentOriginalWindow) || !currentOriginalWindow.querySelector('.zone_reponse textarea[name=nm_texte]') || !currentOriginalWindow.querySelector('.zone_reponse .btnTxt[onclick*="sendMessage"]')) {
                 console.error(`DMM SendError[${conversationId}]: Original window reference lost, detached, or incomplete. Refresh might be needed.`);
                 alert("Erreur DMM: Référence à la fenêtre originale perdue ou invalide. La page pourrait nécessiter un rafraîchissement ou la réouverture de la conversation.");
                 sendButton.disabled = true;
                 sendButton.textContent = 'Erreur Orig.';
                 return;
             }
             sendButton.disabled = true;
             const originalButtonText = sendButton.textContent;
             sendButton.textContent = 'Envoi...';
             let sendAttemptError = null;
             let originalClickSuccess = false;
             const originalTextarea = currentOriginalWindow.querySelector('.zone_reponse textarea[name=nm_texte]');
             const originalSendButton = currentOriginalWindow.querySelector('.zone_reponse .btnTxt[onclick*="sendMessage"]');

             if (originalTextarea && originalSendButton) {
                 try {
                     originalTextarea.value = messageText;
                     originalSendButton.click();
                     originalClickSuccess = true;
                     textarea.value = '';
                 } catch (e) { sendAttemptError = e; console.error(`%cDMM SendError[${conversationId}]: Error clicking original send button.`, "color: red", e); sendButton.textContent = 'Erreur Envoi'; }
             } else { sendAttemptError = new Error("Could not find original send elements just before use."); console.error(`%cDMM SendError[${conversationId}]: ${sendAttemptError.message}`, "color: red"); sendButton.textContent = 'Erreur Config'; }

             if (originalClickSuccess) {
                 GM_xmlhttpRequest({
                     method: "GET", url: `https://www.dreadcast.net/Menu/Messaging/action=OpenMessage&id_conversation=${conversationId}`, timeout: 10000,
                     onload: function(response) {
                         const currentSendButton = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`);
                         if (response.status === 200 && response.responseText) {
                             try {
                                 const latestCData = ACTIVE_CONVERSATIONS[conversationId];
                                 if (latestCData && latestCData.customWindow && document.body.contains(latestCData.customWindow)) {
                                     handleOpenMessageResponse(conversationId, response.responseText);
                                 }
                                 if (currentSendButton) { currentSendButton.textContent = originalButtonText; currentSendButton.disabled = false; }
                             } catch (handlerError) { console.error(`%cDMM SendError[${conversationId}]: Error in handleOpenMessageResponse after immediate fetch.`, "color: red", handlerError); if (currentSendButton) { currentSendButton.textContent = 'Erreur MàJ'; setTimeout(() => { const btn = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`); if (btn) { btn.disabled = false; btn.textContent = originalButtonText; } }, 2000); } }
                         } else { console.warn(`%cDMM Send[${conversationId}]: Immediate OpenMessage failed. Status: ${response.status}. UI will update later via /Check.`, "color: orange"); sendAttemptError = new Error(`Immediate OpenMessage fetch failed with status ${response.status}`); if (currentSendButton) { currentSendButton.textContent = 'MàJ différée'; setTimeout(() => { const btn = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`); if (btn) { btn.disabled = false; btn.textContent = originalButtonText; } }, 2500); } }
                     },
                     onerror: function(error) { console.error(`%cDMM SendError[${conversationId}]: Network error during immediate OpenMessage fetch.`, "color: red", error); sendAttemptError = new Error("Network error during immediate update fetch."); const currentSendButton = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`); if (currentSendButton) { currentSendButton.textContent = 'Erreur Réseau MàJ'; setTimeout(() => { const btn = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`); if (btn) { btn.disabled = false; btn.textContent = originalButtonText; } }, 2500); } },
                     ontimeout: function() { console.warn(`%cDMM Send[${conversationId}]: Timeout during immediate OpenMessage fetch. UI will update later.`, "color: orange"); sendAttemptError = new Error("Timeout during immediate update fetch."); const currentSendButton = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`); if (currentSendButton) { currentSendButton.textContent = 'Timeout MàJ'; setTimeout(() => { const btn = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`); if (btn) { btn.disabled = false; btn.textContent = originalButtonText; } }, 2500); } }
                 });
             } else { setTimeout(() => { const currentSendButton = document.querySelector(`#custom-chat-${conversationId} .custom-chat-reply button`); if (currentSendButton) { currentSendButton.disabled = false; currentSendButton.textContent = originalButtonText; } if (sendAttemptError && !originalClickSuccess) { alert(`Erreur DMM: Échec de l'envoi initial du message.\n${sendAttemptError.message}`); } }, 1500); }
         };

         textarea.addEventListener('keypress', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendButton.click(); } });

         // --- Other UI Handlers (Popups, Theme, Participants, Options, Resize, Drag) ---
         clickOutsideColorPickerHandler = (event) => { if (!colorPickerPanel.contains(event.target) && !themeBtn.contains(event.target)) { closeOtherPopups('none'); } };
         themeBtn.addEventListener('click', (event) => { event.stopPropagation(); const isDisplayed = colorPickerPanel.style.display === 'block'; closeOtherPopups(isDisplayed ? 'none' : 'color'); colorInput.value = getSavedGlobalThemeColor(); colorPickerPanel.style.display = isDisplayed ? 'none' : 'block'; if (!isDisplayed) { setTimeout(() => { document.addEventListener('click', clickOutsideColorPickerHandler, true); }, 0); } else { if (clickOutsideColorPickerHandler) { document.removeEventListener('click', clickOutsideColorPickerHandler, true); clickOutsideColorPickerHandler = null; } } });
         colorInput.addEventListener('input', (event) => { saveGlobalThemeColor(event.target.value); });
         resetColorBtn.addEventListener('click', () => {
             colorInput.value = DEFAULT_THEME_COLOR;
             saveGlobalThemeColor(DEFAULT_THEME_COLOR);
         });

         clickOutsideMenuHandler = (event) => { if (!moreOptionsMenu.contains(event.target) && !moreOptsBtn.contains(event.target)) { closeOtherPopups('none'); } };

         moreOptsBtn.addEventListener('click', (event) => {
            event.stopPropagation();
            const isDisplayed = moreOptionsMenu.style.display === 'block';
            closeOtherPopups(isDisplayed ? 'none' : 'menu');
            if (!isDisplayed) {
                // Update BOTH settings and mute UI when menu opens
                populateConversationSettingsUI(conversationId, specificColorCheckbox, specificColorInput);
                updateMuteOptionsUI(chatWindow, conversationId); // Pass chatWindow reference - THIS NOW UPDATES TIMER TOO
            }
            moreOptionsMenu.style.display = isDisplayed ? 'none' : 'block';
            if (!isDisplayed) {
                setTimeout(() => { document.addEventListener('click', clickOutsideMenuHandler, true); }, 0);
            } else {
                if (clickOutsideMenuHandler) {
                    document.removeEventListener('click', clickOutsideMenuHandler, true);
                    clickOutsideMenuHandler = null;
                }
            }
         });

         moreOptionsMenu.addEventListener('click', (event) => { // Handle menu item clicks
             const menuItem = event.target.closest('.menu-item:not(.convo-settings-item):not(.mute-option-item)'); // Exclude settings & mute items
             const settingsItem = event.target.closest('.convo-settings-item');
             const muteOptionItem = event.target.closest('.mute-option-item'); // Get mute item

             if (menuItem) { // Handle standard actions (invite, delete, etc.)
                 const action = menuItem.dataset.action;
                 closeOtherPopups('none');
                 try {
                     const cData = ACTIVE_CONVERSATIONS[conversationId];
                     const currentOriginalWindow = cData?.originalWindow;
                     const messagerie = unsafeWindow?.nav?.getMessagerie();
                     if (!messagerie && (action === 'mark_unread' || action === 'delete')) { console.error("DMM Error: unsafeWindow.nav.getMessagerie() is not accessible!"); alert("Erreur: Fonctionnalité de messagerie non trouvée."); return; }

                     if (action === 'invite') {
                         if (currentOriginalWindow && document.body.contains(currentOriginalWindow) && typeof unsafeWindow?.$ === 'function') {
                             try {
                                 const originalWindowId = `#${currentOriginalWindow.id}`;
                                 currentOriginalWindow.classList.remove('hidden-original-databox');
                                 currentOriginalWindow.style.opacity = ''; currentOriginalWindow.style.pointerEvents = '';
                                 currentOriginalWindow.style.zIndex = '9999'; currentOriginalWindow.style.top = ''; currentOriginalWindow.style.left = '';
                                 currentOriginalWindow.dataset.modernized = 'revealed_for_invite';
                                 unsafeWindow.$(`${originalWindowId} .contenu`).animate({height: '140px'}, 'fast');
                                 const $zoneReponse = unsafeWindow.$(`${originalWindowId} .zone_reponse`);
                                 $zoneReponse.slideDown('fast');
                                 unsafeWindow.$(`${originalWindowId} .autres_actions`).addClass('small');
                                 $zoneReponse.find('.cible').slideDown('fast');
                                 closeThisChatWindow({ removeOriginal: false });
                                 setTimeout(() => { const targetTextarea = currentOriginalWindow.querySelector('.zone_reponse textarea[name="nm_texte"]'); if(targetTextarea) targetTextarea.focus(); }, 400);
                             } catch (e) { console.error(`DMM: Error executing Invite action jQuery...`, e); alert("Erreur lors de la préparation de l'invitation."); closeThisChatWindow({ removeOriginal: true }); }
                         } else { console.error("DMM Error: Cannot perform Invite - original window ref missing or jQuery not available."); alert("Erreur: Impossible d'exécuter l'action 'Inviter'."); closeThisChatWindow({ removeOriginal: true }); }
                     } else if (action === 'mark_unread') {
                         messagerie.notReadMessage(conversationId);
                         setTimeout(() => closeThisChatWindow({ removeOriginal: true }), 100);
                     } else if (action === 'delete') {
                         messagerie.deleteMessage(conversationId);
                         setTimeout(() => closeThisChatWindow({ removeOriginal: true }), 100);
                     }
                 } catch (e) { console.error(`DMM Error executing action '${action}'...`, e); alert(`Une erreur est survenue lors de l'action ${action}`); }

             } else if (muteOptionItem) { // Handle Mute Option Click
                 const durationStr = muteOptionItem.dataset.duration;
                 let durationMs;
                 if (durationStr === 'null') durationMs = null;
                 else durationMs = parseInt(durationStr, 10);

                 if (typeof durationMs === 'number' || durationMs === null) {
                    // Don't allow clicking "Unmute" if already unmuted
                    const currentEndTime = getConversationMuteEndTime(conversationId);
                    if (durationMs === MUTE_DURATIONS.UNMUTE && currentEndTime === 0) {
                       // Already unmuted, do nothing
                    } else {
                       setConversationMuted(conversationId, durationMs); // This now handles all UI updates (window+sidebar)
                       // closeOtherPopups('none'); // Optionally close menu
                    }
                 } else {
                    console.warn("DMM: Invalid duration found on mute option item:", durationStr);
                 }

             } else if (settingsItem) { // Handle Color Settings Click
                 // Check if the click was directly on an input or its label
                 if (event.target.tagName === 'INPUT' && (event.target.type === 'checkbox' || event.target.type === 'color')) {
                     // Let the specific input's event listener handle it
                 } else if (event.target.tagName === 'LABEL') {
                     const inputId = event.target.htmlFor;
                     const inputElement = document.getElementById(inputId);
                     if (inputElement && inputElement.type === 'checkbox') {
                         inputElement.click();
                     }
                 }
             }
         });

         specificColorCheckbox.addEventListener('change', (event) => { const isEnabled = event.target.checked; const currentColor = specificColorInput.value; specificColorInput.disabled = !isEnabled; setConversationSetting(conversationId, { enabled: isEnabled, color: currentColor }); applyCurrentTheme(chatWindow, conversationId); });
         specificColorInput.addEventListener('input', (event) => { const newColor = event.target.value; if (!specificColorInput.disabled) { setConversationSetting(conversationId, { enabled: true, color: newColor }); applyCurrentTheme(chatWindow, conversationId); } });

         clickOutsidePanelHandler = (event) => { if (!participantsPanel.contains(event.target) && !participantsBtn.contains(event.target)) { closeOtherPopups('none'); } };
         participantsBtn.addEventListener('click', (event) => { event.stopPropagation(); const cData = ACTIVE_CONVERSATIONS[conversationId]; if (!cData) return; const isActive = participantsPanel.classList.contains('active'); closeOtherPopups(isActive ? 'none' : 'panel'); const listHtml = cData.participants.length > 0 ? cData.participants.join('<br>') : '<p style="color:#888;font-style:italic;">Aucun participant trouvé.</p>'; participantsListDiv.innerHTML = listHtml; participantsPanel.classList.toggle('active'); if (!isActive) { setTimeout(() => { document.addEventListener('click', clickOutsidePanelHandler, true); }, 0); } else { if (clickOutsidePanelHandler) { document.removeEventListener('click', clickOutsidePanelHandler, true); clickOutsidePanelHandler = null; } } });
         closePanelBtn.addEventListener('click', () => { closeOtherPopups('none'); });
         head.addEventListener('dblclick', (e) => { if (e.target.closest('.controls span') || e.target.closest('.participants-panel') || e.target.closest('.color-picker-panel') || e.target.closest('.more-opts-menu') || e.target.classList.contains('resize-handle') || e.target.classList.contains('mute-status-display')) return; const isCollapsed = chatWindow.classList.toggle('collapsed'); if (isCollapsed) { closeOtherPopups('none'); } });

         // --- Add to DOM and make interactive ---
         document.body.appendChild(chatWindow);
         makeDraggable(chatWindow);
         makeResizable(chatWindow, resizeHandle);

         // --- Build initial content ---
         const { latestId, oldestId } = buildInitialChatUI(messages, content, conversationId, allLoaded);

         // --- Setup Mute Timer Interval ---
         const muteTimerIntervalId = setInterval(() => {
             const currentConvData = ACTIVE_CONVERSATIONS[conversationId];
             // Check if window still exists before updating
             if (currentConvData?.customWindow && document.body.contains(currentConvData.customWindow)) {
                 // Update header inside the DMM window
                 updateHeaderMuteStatus(currentConvData.customWindow, conversationId);
                 // Check if mute expired using the main function (which updates sidebar if needed)
                 isConversationMuted(conversationId);
                 // Update menu timer display ONLY IF menu is currently open
                 const currentMenu = currentConvData.customWindow.querySelector('.more-opts-menu');
                 if (currentMenu && currentMenu.style.display === 'block') {
                     updateMuteOptionsUI(currentConvData.customWindow, conversationId);
                 }
             } else {
                 // Window is gone, clear the interval (check existence before clearing)
                 const intervalIdToClear = ACTIVE_CONVERSATIONS[conversationId]?.muteTimerIntervalId;
                 if (intervalIdToClear) {
                     clearInterval(intervalIdToClear);
                     if (ACTIVE_CONVERSATIONS[conversationId]) { // Check again before deleting prop
                         delete ACTIVE_CONVERSATIONS[conversationId].muteTimerIntervalId;
                     }
                     // console.log(`%cDMM Mute Timer: Cleared interval ${intervalIdToClear} for closed/missing window ${conversationId}`, "color: gray");
                 }
             }
         }, 20000); // Update every 20 seconds

         // --- Store conversation data ---
         ACTIVE_CONVERSATIONS[conversationId] = {
             customWindow: chatWindow,
             originalWindow: originalWindowRef,
             latestMessageId: latestId ?? initialLatestId, // message_id
             oldestMessageId: oldestId ?? initialOldestId, // message_id
             allMessagesLoaded: allLoaded,
             isLoadingOlder: false,
             participants: participants,
             hasUnreadNotification: false,
             muteTimerIntervalId: muteTimerIntervalId // Store the interval ID
         };

         // --- Initial Mute State UI Update ---
         updateHeaderMuteStatus(chatWindow, conversationId);
         updateMuteOptionsUI(chatWindow, conversationId); // Initial checkmark/timer update
         // updateSidebarMuteStatus(conversationId); // No need to call here, scanAndUpdateSidebarMutes runs on init/update

         // --- Final focus ---
         setTimeout(() => textarea.focus(), 200);

     } // --- END of createCustomWindow function ---


    // --- Click Simulation Functions ---
    function simulateClick(element) {
        return new Promise(resolve => {
            if (!element || !document.body.contains(element)) {
                console.warn("DMM simulateClick (.click()): Null or detached element"); resolve(false); return;
            }
            try { element.click(); resolve(true); }
            catch (e) { console.error(`DMM simulateClick (.click()): Error calling .click() on`, element, e); resolve(false); }
        });
    }
    async function initiateDoubleClick(selector, container = document) {
        const logPrefix = `DMM initiateDoubleClick (.click() x2) (${selector}):`;
        let element = null;
        try {
            element = container.querySelector(selector);
            if (!element || !document.body.contains(element)) { console.warn(`${logPrefix} Initial element not found or not in document.`); return false; }
            element.click(); // First click
            await new Promise(r => setTimeout(r, REFIND_DELAY));
            element = null; // Reset before re-find
            element = container.querySelector(selector); // RE-FIND ELEMENT
            if (!element || !document.body.contains(element)) { console.warn(`${logPrefix} Element could not be re-found after delay.`); return false; }
            element.click(); // Second click
            // console.log(`%c${logPrefix} Double .click() sequence completed.`, "color: green");
            return true;
        } catch (error) { console.error(`${logPrefix} Error during double .click() simulation:`, error); return false; }
    }


    // --- Core Logic Handlers ---

    async function handleNewMessageEvent(conversationId, folderId) {
        const MAX_ATTEMPTS = 5;
        const RETRY_DELAY = 100;
        const logPrefix = `DMM /Check Sim Handler [${conversationId}/${folderId}]:`;
        // const conversationData = ACTIVE_CONVERSATIONS[conversationId]; // Keep for potential future use, but check isn't needed here anymore
        let menuWasOpenedByScriptOnSuccessfulAttempt = false;
        let overallSuccess = false;

        for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
            // console.log(`%c${logPrefix} Attempt ${attempt}/${MAX_ATTEMPTS}...`, "color: #4682B4; font-weight: bold;");
            let currentAttemptMenuOpened = false;

            try {
                // --- Step 1: Ensure Main Message Menu is Visible ---
                const messageListContainer = document.getElementById('liste_messages');
                const mainMenuButton = document.getElementById('display_messagerie');
                const isListVisibleCheck = () => messageListContainer && document.body.contains(messageListContainer) && messageListContainer.offsetParent !== null && getComputedStyle(messageListContainer).visibility !== 'hidden' && getComputedStyle(messageListContainer).display !== 'none';

                if (!isListVisibleCheck()) {
                    if (!mainMenuButton) {
                        console.error(`${logPrefix} Attempt ${attempt}: Message list not visible and #display_messagerie not found! Aborting attempts.`);
                        return;
                    }
                    // console.log(`%c${logPrefix} Attempt ${attempt}: Main message list not visible. Clicking #display_messagerie...`, "color: #4682B4");
                    currentAttemptMenuOpened = true;
                    await new Promise(r => setTimeout(r, UI_CLICK_DELAY));
                    const click1Success = await simulateClick(mainMenuButton);
                    if (!click1Success) {
                        console.warn(`${logPrefix} Attempt ${attempt}: Failed to simulate click on #display_messagerie. Retrying...`);
                        await new Promise(r => setTimeout(r, RETRY_DELAY));
                        continue;
                    }
                    await new Promise(r => setTimeout(r, UI_WAIT_DELAY));
                    try {
                        await waitForElement('#liste_messages');
                        // console.log(`%c${logPrefix} Attempt ${attempt}: Main message list should now be visible.`, "color: #4682B4");
                    } catch (waitError) {
                        console.warn(`${logPrefix} Attempt ${attempt}: Main message list (#liste_messages) did not become visible after click. Retrying...`, waitError?.message || waitError);
                        await new Promise(r => setTimeout(r, RETRY_DELAY));
                        continue;
                    }
                }

                // --- Step 2: Ensure Correct Folder List is Visible ---
                const folderListULSelector = '#liste_messages ul#folder_list';
                let folderListUL = document.querySelector(folderListULSelector);
                const isFolderListVisibleCheck = () => folderListUL && folderListUL.offsetParent !== null && getComputedStyle(folderListUL).display !== 'none';
                if (!isFolderListVisibleCheck() && folderListUL) {
                     folderListUL.style.display = 'block'; // Force visible if needed
                     await new Promise(r => setTimeout(r, UI_WAIT_DELAY / 2));
                } else if (!folderListUL) {
                     console.error(`${logPrefix} Attempt ${attempt}: Folder list UL element ('${folderListULSelector}') not found! Retrying...`);
                     await new Promise(r => setTimeout(r, RETRY_DELAY));
                     continue;
                 }

                // --- Step 3: Click Target Folder LI ---
                folderListUL = document.querySelector(folderListULSelector); // Re-select
                const targetFolderLiSelector = `li#folder_${folderId}`;
                if (!folderListUL || !document.body.contains(folderListUL)) {
                    console.warn(`${logPrefix} Attempt ${attempt}: Cannot find folder list UL ('${folderListULSelector}') or it's detached before clicking folder LI. Retrying...`);
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue;
                }

                let targetFolderLi = null;
                try {
                    targetFolderLi = await waitForElement(targetFolderLiSelector, WAIT_FOR_ELEMENT_TIMEOUT, folderListUL);
                } catch (findError) {
                    console.warn(`${logPrefix} Attempt ${attempt}: Failed to find target folder LI '${targetFolderLiSelector}'. Retrying...`, findError?.message || findError);
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue;
                }

                // Click the folder ONLY if it's not already the active one
                const currentFolderSpan = document.querySelector('#current_folder');
                const currentFolderId = currentFolderSpan?.dataset?.id;
                let folderClicked = false;
                if(currentFolderId !== folderId) {
                    // console.log(`%c${logPrefix} Attempt ${attempt}: Found folder LI ${folderId}. Clicking...`, "color: #4682B4");
                    await new Promise(r => setTimeout(r, UI_CLICK_DELAY));
                    const click3Success = await simulateClick(targetFolderLi);
                    if(!click3Success) {
                         console.warn(`${logPrefix} Attempt ${attempt}: Failed to simulate click on target folder LI ${folderId}. Retrying...`);
                         await new Promise(r => setTimeout(r, RETRY_DELAY));
                         continue;
                    }
                    folderClicked = true;
                    // Wait longer if we clicked the folder, as it triggers an XHR load
                    await new Promise(r => setTimeout(r, UI_WAIT_DELAY * 1.5)); // Increased wait
                } // else { console.log(`%c${logPrefix} Attempt ${attempt}: Target folder ${folderId} already selected.`, "color: #4682B4"); }

                // --- Step 4: Find and Double-Click the Message LI ---
                // Target the specific message LI within the main list container
                const messageListContentUL = document.querySelector('#liste_messages .content ul');
                if (!messageListContentUL || !document.body.contains(messageListContentUL)) {
                    console.warn(`${logPrefix} Attempt ${attempt}: Cannot find #liste_messages .content ul container before message LI click. Retrying...`);
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue;
                }
                const targetMessageSelector = `li#message_${conversationId}`;
                // console.log(`%c${logPrefix} Attempt ${attempt}: Waiting for message LI '${targetMessageSelector}' within .content ul...`, "color: #4682B4");

                let targetMessageLi = null;
                try {
                    // Wait longer if the folder was just clicked
                    targetMessageLi = await waitForElement(targetMessageSelector, WAIT_FOR_ELEMENT_TIMEOUT * (folderClicked ? 3 : 2) , messageListContentUL);
                } catch (findMsgError) {
                    console.warn(`${logPrefix} Attempt ${attempt}: Failed to find target message LI '${targetMessageSelector}' after folder interaction. Retrying...`, findMsgError?.message || findMsgError);
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue;
                }

                // console.log(`%c${logPrefix} Attempt ${attempt}: Found message LI ${conversationId}. Initiating double-click...`, "color: #008080; font-weight: bold;");
                // Use the messageListContentUL as the container for the double-click simulation
                const success = await initiateDoubleClick(targetMessageSelector, messageListContentUL);

                if (success) {
                    console.log(`%c${logPrefix} Attempt ${attempt} SUCCEEDED. Double-click initiated for ${targetMessageSelector}.`, "color: green; font-weight: bold;");
                    overallSuccess = true;
                    menuWasOpenedByScriptOnSuccessfulAttempt = currentAttemptMenuOpened;
                    break; // Exit the retry loop
                } else {
                    console.warn(`${logPrefix} Attempt ${attempt}: Double-click simulation FAILED for ${targetMessageSelector}. Retrying...`);
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue;
                }

            } catch (error) {
                console.error(`${logPrefix} Attempt ${attempt}: Error during UI simulation steps. Retrying...`, error);
                await new Promise(r => setTimeout(r, RETRY_DELAY));
                continue;
            }
        } // --- END of for loop (attempts) ---

        if (!overallSuccess) {
             console.error(`${logPrefix} All ${MAX_ATTEMPTS} attempts failed to complete the UI simulation.`);
        }

        // --- Auto-close Menu ---
        if (overallSuccess && menuWasOpenedByScriptOnSuccessfulAttempt) {
            // console.log(`%c${logPrefix} FINALLY: Successful attempt opened the menu. Waiting briefly before attempting to close...`, "color: #4682B4");
            await new Promise(r => setTimeout(r, 150)); // Short delay

            const finalMenuButton = document.getElementById('display_messagerie');
            const finalList = document.getElementById('liste_messages');
            const finalIsListVisibleCheck = () => finalList && document.body.contains(finalList) && finalList.offsetParent !== null && getComputedStyle(finalList).visibility !== 'hidden' && getComputedStyle(finalList).display !== 'none';

            if (finalMenuButton && finalIsListVisibleCheck()) {
                 // console.log(`%c${logPrefix} FINALLY: List is still visible, attempting close click.`, "color: #4682B4");
                 await simulateClick(finalMenuButton);
                 // console.log(`%c${logPrefix} FINALLY: Clicked #display_messagerie to close menu.`, "color: #4682B4");
            } // else: Menu already closed or button missing
        }
        // console.log(`%c${logPrefix} Processing finished. Overall Success: ${overallSuccess}`, "color: #4682B4; font-weight: bold;");

    } // --- END of handleNewMessageEvent ---


    function handleOpenMessageResponse(conversationId, responseText) {
        const conversationData = ACTIVE_CONVERSATIONS[conversationId];
        const isConvoMuted = isConversationMuted(conversationId); // Check conversation-specific mute status

        // Check if window exists OR if it's a user override scenario (where window *should* exist soon)
        const dmmWindowExists = conversationData && conversationData.customWindow && document.body.contains(conversationData.customWindow);
        const isUserOverride = openingMutedOverride === conversationId; // Check if this ID is being overridden

        // If conversation muted AND the DMM window doesn't exist AND it's NOT a user override, suppress the update.
        if (isConvoMuted && !dmmWindowExists && !isUserOverride) {
            // console.log(`%cDMM OpenMessage Handler: Suppressing update for MUTED conversation ${conversationId} (no window, no override).`, "color: gray;");
            return;
        }

        // If window doesn't exist and it's *not* a user override case waiting for the window, abort.
        if (!dmmWindowExists && !isUserOverride) {
            // console.log(`%cDMM OpenMessage Handler: DMM window for ${conversationId} not active/attached (and not a pending override). Aborting UI update.`, "color: gray");
            return;
        }
        // At this point, either the window exists, or we expect it to exist shortly due to user override.

        const customContentArea = conversationData?.customWindow?.querySelector('.custom-chat-content');
        // If the window doesn't exist *yet* due to override, customContentArea will be null. This is handled below.
        if (!isUserOverride && !customContentArea) {
            console.warn(`%cDMM OpenMessage Handler[${conversationId}]: Content area not found for UI update (window exists but content area missing?).`, "color: red");
            return;
        }

        const currentLatestKnownId = conversationData?.latestMessageId; // This is message_id

        try {
            const parser = new DOMParser();
            const doc = parser.parseFromString(responseText, 'text/html');
            const latestConvList = doc.querySelector('.zone_conversation');
            if (!latestConvList) { console.warn(`%cDMM OpenMessage Handler[${conversationId}]: Could not find .zone_conversation in intercepted response. Cannot update messages.`, "color: orange"); return; }

            // Parse elements with 'convers_' ID from the response
            const serverElements = Array.from(latestConvList.querySelectorAll('.link.conversation[id^="convers_"]'));
            if (serverElements.length === 0) { /*console.log(`%cDMM OpenMessage Handler[${conversationId}]: No message elements found in response.`, "color: gray");*/ return; }

            let elementsToProcess = [];
            let highestServerId = null; // Highest message_id from server
            let highestServerIdNum = 0;

            serverElements.forEach(el => {
                const parsed = parseMessageElement(el); // Get { id, timestamp, sender } where id is message_id
                if (parsed) {
                    const elIdNum = parseInt(parsed.id);
                    if (!highestServerId || elIdNum > highestServerIdNum) { highestServerId = parsed.id; highestServerIdNum = elIdNum; }

                    // Check against existing content area *only if it exists*
                    const alreadyExists = customContentArea ? customContentArea.querySelector(`.chat-bubble[data-message-id="${parsed.id}"]`) : false;
                    // Compare message_id numbers
                    const isNewer = !currentLatestKnownId || elIdNum > parseInt(currentLatestKnownId);

                    if (!alreadyExists && isNewer) { elementsToProcess.push(el); } // Store the element to fetch content later
                }
            });

            // console.log(`%cDMM OpenMessage Handler[${conversationId}]: Server highest MsgID: ${highestServerId}. Client latest MsgID: ${currentLatestKnownId ?? 'None'}. Found ${elementsToProcess.length} potential new message(s) to add. Muted: ${isConvoMuted}, Override Pending: ${isUserOverride}`, "color: blue");

            if (elementsToProcess.length > 0) {
                elementsToProcess.reverse(); // Process oldest new message first

                let newlyProcessedRealIds = []; // Store message_ids added
                let fetchPromises = elementsToProcess.map(element => {
                    return new Promise(async (resolve) => {
                        const parsed = parseMessageElement(element); // Parse again to get message_id etc.
                        if (parsed) {
                            // Wait briefly if it's an override, allowing createCustomWindow to potentially finish first
                            if (isUserOverride) await new Promise(r => setTimeout(r, 50));

                            // Re-check DMM window/content area status right before fetching content
                            const finalConvDataCheck = ACTIVE_CONVERSATIONS[conversationId];
                            const finalContentAreaCheck = finalConvDataCheck?.customWindow?.querySelector('.custom-chat-content');

                            // If window/content area still doesn't exist (even after potential override delay), skip adding bubble
                            if (!finalConvDataCheck || !finalConvDataCheck.customWindow || !document.body.contains(finalConvDataCheck.customWindow) || !finalContentAreaCheck) {
                                // console.log(`%cDMM OpenMessage Handler [${conversationId}]: Window/Content disappeared before fetching msg ${parsed.id}. Aborting add.`, "color: orange");
                                resolve(); return;
                            }
                            // Final duplicate check within the confirmed content area using message_id
                            if (finalContentAreaCheck.querySelector(`.chat-bubble[data-message-id="${parsed.id}"]`)) {
                                // console.log(`%cDMM OpenMessage Handler [${conversationId}]: Duplicate msg ${parsed.id} detected before addBubble. Skipping.`, "color: gray");
                                resolve(); return;
                            }

                            fetchMessageContent(parsed.id, conversationId, (content) => { // Use message_id to fetch
                                // Check window status *again* after async fetch returns
                                const finalConvData = ACTIVE_CONVERSATIONS[conversationId];
                                if (!finalConvData || !finalConvData.customWindow || !document.body.contains(finalConvData.customWindow)) { resolve(); return; }
                                const finalContentArea = finalConvData.customWindow.querySelector('.custom-chat-content');
                                if (!finalContentArea) { resolve(); return; }
                                // Check duplicate again *after* fetch, *before* adding
                                if (finalContentArea.querySelector(`.chat-bubble[data-message-id="${parsed.id}"]`)) {
                                     // console.log(`%cDMM OpenMessage Handler [${conversationId}]: Duplicate msg ${parsed.id} detected AFTER fetch. Skipping add.`, "color: gray");
                                     resolve(); return;
                                }

                                const msgData = { ...parsed, content: content };
                                const processedId = addBubble(msgData, finalContentArea, conversationId, false, false); // isInitialLoad=false here; processedId is message_id
                                if (processedId) newlyProcessedRealIds.push(processedId);
                                resolve();
                            });
                        } else { resolve(); }
                    });
                }); // End map

                Promise.all(fetchPromises).then(() => {
                    // Final check after all bubbles *should* have been added
                    const postProcessConvData = ACTIVE_CONVERSATIONS[conversationId];
                    if (!postProcessConvData || !postProcessConvData.customWindow || !document.body.contains(postProcessConvData.customWindow)) { return; }

                    let overallLatestId = postProcessConvData.latestMessageId; // message_id
                    let didUpdateLatest = false;
                    try {
                        newlyProcessedRealIds.forEach(processedId => { // processedId is message_id
                            if (!processedId) return;
                            const processedIdNum = parseInt(processedId);
                            const currentLatestNum = overallLatestId ? parseInt(overallLatestId) : 0;
                            if (processedIdNum > currentLatestNum) { overallLatestId = processedId; didUpdateLatest = true; }
                        });
                        if (didUpdateLatest) {
                            postProcessConvData.latestMessageId = overallLatestId;
                            // console.log(`%cDMM OpenMessage Handler[${conversationId}]: Successfully processed ${newlyProcessedRealIds.length} message(s). Updated latest message ID to ${postProcessConvData.latestMessageId}`, "color: green");
                        }
                    } catch (idUpdateError) { console.error(`%cDMM OpenMessage Handler[${conversationId}]: Error updating latestMessageId after processing!`, "color: red", idUpdateError); }
                }).catch(error => { console.error(`%cDMM OpenMessage Handler[${conversationId}]: Error in Promise.all for new message fetches:`, "color: red", error); });
            } else if (highestServerId && currentLatestKnownId && parseInt(highestServerId) > parseInt(currentLatestKnownId)) {
                 // Update latest ID even if no new bubbles were added (they might have been added by another means or filtered)
                 const convDataForIdUpdate = ACTIVE_CONVERSATIONS[conversationId];
                 if (convDataForIdUpdate) { // Ensure data still exists
                    // console.log(`%cDMM OpenMessage Handler[${conversationId}]: Server latest message ID ${highestServerId} > Client ${currentLatestKnownId}, but no new elements added. Updating client latest message ID.`, "color: blue");
                    convDataForIdUpdate.latestMessageId = highestServerId;
                 }
             }
        } catch (e) {
            console.error(`%cDMM OpenMessage Handler[${conversationId}]: General error processing intercepted response:`, "color: red", e);
        }
    } // --- END of handleOpenMessageResponse ---


    // --- Click Handling on Sidebar ---
    function handleMessageListClick(event) {
        if (!event.isTrusted) return; // Ignore simulated clicks

        const messageLi = event.target.closest('li.message[id^="message_"]');
        if (!messageLi) return; // Click wasn't on a message LI element

        const conversationId = messageLi.id.replace('message_', '');

        // Mute Handling Logic
        const isMuted = isConversationMuted(conversationId);
        if (isMuted) {
            console.log(`%cDMM ClickIntercept: User click detected on sidebar for MUTED convo ${conversationId}. OVERRIDING mute for this open.`, "color: #8A2BE2;"); // Purple

            // Set the override flag
            openingMutedOverride = conversationId;
            // Clear previous timer if any
            if (openingMutedOverrideTimer) clearTimeout(openingMutedOverrideTimer);
            // Set a timer to clear the flag shortly after, in case observer is slow or DC action fails
            openingMutedOverrideTimer = setTimeout(() => {
                if (openingMutedOverride === conversationId) { // Ensure it wasn't changed by another click
                    // console.log(`%cDMM ClickIntercept: Clearing mute override flag for ${conversationId} after timeout.`, "color: gray;");
                    openingMutedOverride = null;
                }
                openingMutedOverrideTimer = null;
            }, 500); // 500ms should be enough for DC to add the window and observer to react

            // *** DO NOT preventDefault() or stopPropagation() here! ***
            // Let the original Dreadcast click handler proceed to open the db_message window.
            // The mainObserverCallback will handle the override.
            return; // Stop further processing *within this DMM handler*
        }
        // End Mute Handling


        // --- If NOT muted, proceed with existing logic ---
        const customWindowId = `custom-chat-${conversationId}`;
        const existingData = ACTIVE_CONVERSATIONS[conversationId];
        const customWindowElement = document.getElementById(customWindowId);

        if (existingData && customWindowElement && existingData.customWindow === customWindowElement && document.body.contains(customWindowElement)) {
            // console.log(`%cDMM ClickIntercept: User click detected on sidebar for OPEN (and not muted) DMM convo ${conversationId}. DMM PREVENTING default Dreadcast action.`, "color: blue");
            event.preventDefault(); // Stop Dreadcast from opening its own window
            event.stopPropagation(); // Stop event from bubbling further

            bringWindowToFront(customWindowElement); // Use new function to bring window to front
            if (customWindowElement.classList.contains('collapsed')) { customWindowElement.classList.remove('collapsed'); } // Uncollapse
            const textarea = customWindowElement.querySelector('.custom-chat-reply textarea');
            if (textarea) setTimeout(() => textarea.focus(), 50); // Focus after a tiny delay
            return; // Stop further processing by this handler
        }

        // If an original window was previously revealed for 'invite' and is still in the DOM...
        const revealedOriginal = document.querySelector(`#db_message_${conversationId}[data-modernized="revealed_for_invite"]`);
        if (revealedOriginal && document.body.contains(revealedOriginal)) {
            // console.log(`DMM ClickIntercept: User click on message_${conversationId} while original was revealed. Hiding original before allowing default action.`);
            revealedOriginal.classList.add('hidden-original-databox'); // Re-hide the original
            revealedOriginal.dataset.modernized = ''; // Clear the state
        } // else: No DMM window open, or original not revealed. Allow default Dreadcast action.
    } // --- END of handleMessageListClick ---

    function setupClickListener() {
        const stableParent = document.getElementById('liste_messages');
        if (stableParent) {
            stableParent.removeEventListener('click', handleMessageListClick, true); // Remove first
            stableParent.addEventListener('click', handleMessageListClick, true); // Add listener (capture phase)
            console.log("DMM: Sidebar click listener attached (capture phase).");
        } else {
            console.error("DMM: CRITICAL - #liste_messages not found for click listener. Retrying...");
            setTimeout(setupClickListener, 2000);
        }
    }


    // --- Main Observer Callback (Detects added original windows) ---
    const mainObserverCallback = async (mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList') {
                for (const node of mutation.addedNodes) {
                    // Check if the added node is an original Dreadcast message window
                    if (node.nodeType === Node.ELEMENT_NODE && node.id?.startsWith('db_message_')) {
                        const originalWindow = node;
                        const conversationId = originalWindow.id.replace('db_message_', '');

                        // Mute Check Logic
                        const isMuted = isConversationMuted(conversationId);
                        let skipMuteRemoval = false; // Flag to allow modernization even if muted

                        if (isMuted) {
                            // Check if this opening was triggered by user override click
                            if (openingMutedOverride === conversationId) {
                                console.log(`%cDMM MainObserver: Muted window ${conversationId} detected, but user override flag is set. Allowing modernization.`, "color: #8A2BE2;");
                                skipMuteRemoval = true; // Allow modernization
                                openingMutedOverride = null; // Consume the flag
                                if(openingMutedOverrideTimer) clearTimeout(openingMutedOverrideTimer); // Clear timer early
                                openingMutedOverrideTimer = null;
                            } else {
                                // Muted and NOT a user override click (e.g., from /Check simulation)
                                // Check if it hasn't ALREADY been marked/removed to avoid loops
                                if (originalWindow.dataset.modernized !== 'muted_removed') {
                                    console.log(`%cDMM MainObserver: Detected original window ${originalWindow.id} for MUTED conversation (no override). Removing it immediately.`, "color: #DAA520;");
                                    originalWindow.dataset.modernized = 'muted_removed'; // Mark state FIRST
                                    try {
                                        // Add an extra check to ensure it's still in the DOM
                                        if (originalWindow.parentNode) {
                                            originalWindow.remove();
                                        } else {
                                            // console.log(`%cDMM MainObserver: Muted window ${originalWindow.id} was already removed from DOM before explicit removal.`, "color: gray;");
                                        }
                                    } catch (removeError) {
                                        console.warn(`DMM MainObserver: Error removing muted original window ${originalWindow.id}:`, removeError);
                                    }
                                }
                                continue; // <<< IMPORTANT: Stop processing this node further if muted and not overridden
                            }
                        }
                        // End Mute Check Logic


                        // Skip if already handled/marked (unless overridden)
                        if (['processing', 'replaced', 'error', 'revealed_for_invite', 'muted_removed'].includes(originalWindow.dataset.modernized) && !skipMuteRemoval) {
                             // console.log(`DMM MainObserver: Skipping node ${originalWindow.id} with state: ${originalWindow.dataset.modernized}`);
                             continue;
                        }

                        // --- Proceed with modernization if not muted OR if mute was overridden ---
                        const existingDMMData = ACTIVE_CONVERSATIONS[conversationId];
                        const existingDMMWindow = document.getElementById(`custom-chat-${conversationId}`);

                        if (existingDMMData && existingDMMWindow && document.body.contains(existingDMMWindow)) {
                            // Update reference and hide original if DMM window already exists
                            // console.log(`DMM MainObserver: Detected original window ${originalWindow.id} for existing DMM window. Updating reference and hiding original.`);
                            existingDMMData.originalWindow = originalWindow;
                            originalWindow.classList.add('hidden-original-databox');
                            originalWindow.dataset.modernized = 'replaced';
                        } else {
                            // This is a NEW conversation window to modernize
                            originalWindow.dataset.modernized = 'processing';
                            // console.log(`%cDMM MainObserver: Detected new original window ${originalWindow.id} (Muted: ${isMuted}, Overridden: ${skipMuteRemoval}). Modernizing...`, "color: purple");
                            originalWindow.classList.add('hidden-original-databox'); // Hide it

                            if (!MY_NAME) MY_NAME = getMyCharacterName();
                            if (!MY_NAME) {
                                console.error("DMM CRITICAL: Failed to get character name when modernizing window!");
                                alert("Erreur critique DMM: Impossible d'obtenir le nom du personnage. Impossible d'ouvrir la fenêtre de message DMM.");
                                originalWindow.classList.remove('hidden-original-databox');
                                originalWindow.style.opacity = '0.7'; originalWindow.style.border = '2px dashed red'; originalWindow.style.pointerEvents = 'auto';
                                originalWindow.dataset.modernized = 'error';
                                continue; // Stop processing this node
                            }

                            // Fetch initial messages and create the DMM window
                            try {
                                const initialResult = await parseAndFetchInitialMessages(originalWindow, conversationId);
                                if (!initialResult || typeof initialResult !== 'object') { throw new Error(`parseAndFetchInitialMessages returned invalid result for ${conversationId}`); }
                                createCustomWindow(conversationId, null, initialResult, originalWindow);
                                originalWindow.dataset.modernized = 'replaced'; // Mark as replaced *after* successful creation
                                // console.log(`%cDMM MainObserver: Successfully modernized ${originalWindow.id}`, "color: purple");
                            } catch (error) {
                                console.error(`DMM MainObserver: Failed to process and modernize conversation ${conversationId}:`, error);
                                alert(`DMM Erreur: Impossible de charger la conversation ${conversationId}. La fenêtre originale reste visible (avec bordure rouge).`);
                                delete ACTIVE_CONVERSATIONS[conversationId]; // Clean up potentially partial data
                                originalWindow.classList.remove('hidden-original-databox');
                                originalWindow.style.opacity = '0.7'; originalWindow.style.border = '2px dashed red'; originalWindow.style.pointerEvents = 'auto';
                                originalWindow.dataset.modernized = 'error';
                            }
                        }
                    } // Fin if node.id startsWith db_message_
                } // Fin boucle addedNodes
            } // Fin if mutation.type childList
        } // Fin boucle mutationsList
    }; // --- END of mainObserverCallback ---

    // --- Sidebar Observer Callback (Detects changes in message list UL) ---
    const sidebarObserverCallback = (mutationsList) => {
        let listChanged = false;
        let contentChanged = false;

        for (const mutation of mutationsList) {
            // Check for both content UL changes and folder switches
            if (mutation.type === 'childList') {
                // Check added/removed nodes
                const changedNodes = [...mutation.addedNodes, ...mutation.removedNodes];
                for (const node of changedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        // Check for direct message items
                        if (node.matches?.('li.message[id^="message_"]')) {
                            listChanged = true;
                            break;
                        }
                        // Check for content container changes (folder switch)
                        if (node.matches?.('.content')) {
                            contentChanged = true;
                            break;
                        }
                        // Check for UL replacement
                        if (node.tagName === 'UL') {
                            contentChanged = true;
                            break;
                        }
                    }
                }
            }
            if (listChanged || contentChanged) break;
        }

        // Handle the changes
        if (listChanged || contentChanged) {
            // Clear any pending scan
            if (sidebarScanDebounceTimer) {
                clearTimeout(sidebarScanDebounceTimer);
            }

            // Set different delays based on change type
            const delay = contentChanged ? 500 : 300; // Longer delay for folder switches

            sidebarScanDebounceTimer = setTimeout(() => {
                scanAndUpdateSidebarMutes();
                sidebarScanDebounceTimer = null;
            }, delay);
        }
    };

    // --- Script Initialization ---
    console.log(`DMM: Dreadcast Dynamic Messages script v${SCRIPT_VERSION} starting...`);
    addChatStyles();

    async function initializeScript() { // Make async to await element
        const essentialElements = [
             document.body,
             document.getElementById('zone_messagerie'),
             document.getElementById('txt_pseudo'),
             document.getElementById('liste_messages') // Ensure message list exists for listener/observer
        ];

        if (essentialElements.every(el => el)) {
            console.log("DMM: Essential elements found.");
            if (!MY_NAME) MY_NAME = getMyCharacterName();
            if (!MY_NAME) { console.error("DMM: Failed to get character name on init! Aborting."); return; }

            // --- Load Global Mute State --- <<< NEW >>>
            loadGlobalMuteState();

            // --- Create Global Mute Button --- <<< NEW >>>
            createGlobalMuteButton(); // Async, will attach when ready

            // --- Setup XHR Wrapper --- // (Logic for /Check and OpenMessage remains the same)
            if (!unsafeWindow._original_XMLHttpRequest_open && !unsafeWindow._original_XMLHttpRequest_send) {
                console.log("DMM: Applying XMLHttpRequest wrappers for /Check and OpenMessage interception...");
                const origOpen = unsafeWindow.XMLHttpRequest.prototype.open;
                const origSend = unsafeWindow.XMLHttpRequest.prototype.send;
                let requestCounter = 0;

                unsafeWindow.XMLHttpRequest.prototype.open = function(method, url) {
                    this._requestMethod = method; this._requestURL = url; this._requestId = requestCounter++;
                    // console.log(`DMM XHR Open [${this._requestId}]: ${method} ${url}`);
                    return origOpen.apply(this, arguments);
                };

                unsafeWindow.XMLHttpRequest.prototype.send = function() {
                    const xhr = this;
                    const reqId = xhr._requestId; const targetUrl = xhr._requestURL;

                    const dmmReadyStateHandler = function() {
                        const rsUrl = xhr._requestURL; // Use the stored URL
                        if (xhr.readyState === 4) {
                            // console.log(`DMM XHR Done [${reqId}]: State 4 for ${rsUrl}, Status: ${xhr.status}`);
                            const currentStatus = xhr.status; let currentResponseText = null;
                            try { currentResponseText = xhr.responseText; } catch(e) { /* Ignore responseText access errors */ }

                            // --- Handle /Check ---
                            if (rsUrl && typeof rsUrl === 'string' && rsUrl.includes('/Check')) {
                                if (currentStatus === 200 && currentResponseText) {
                                    let match = null;
                                    try {
                                        // Regex to find nouveau_message event
                                        match = currentResponseText.match(/<evenement\s+type="nouveau_message">.*?<folder_(\d+)\s+quantite="\d+"\s+id_conversation="(\d+)"\s*\/?>.*?<\/evenement>/s);
                                    } catch (regexError) { console.error('DMM: Error during /Check regex matching!', regexError); }

                                    if (match && match[1] && match[2]) {
                                        const folderId = match[1];
                                        const conversationId = match[2];
                                        const isConvoMuted = isConversationMuted(conversationId); // Conversation specific mute

                                        // console.log(`%cDMM XHR /Check Match [${reqId}]: nouveau_message! Convo: ${conversationId}, Folder: ${folderId}, Muted: ${isConvoMuted}`, "color: orange");

                                        const conversationData = ACTIVE_CONVERSATIONS[conversationId];
                                        const isWindowOpen = conversationData && conversationData.customWindow && document.body.contains(conversationData.customWindow);

                                        if (isWindowOpen) {
                                            // DMM Window IS OPEN: Always trigger simulation to update content.
                                            // console.log(`%cDMM /Check: Window for ${conversationId} is OPEN. Triggering UI update simulation. (ConvoMuted: ${isConvoMuted}, GlobalMuted: ${isGloballyMuted})`, "color: blue");
                                            try {
                                                setTimeout(() => handleNewMessageEvent(conversationId, folderId), 0);
                                            } catch (e) { console.error(`DMM ERROR: Exception queueing handleNewMessageEvent for OPEN window ${conversationId}`, e); }
                                        } else {
                                            // DMM Window IS CLOSED:
                                            if (isConvoMuted) {
                                                // CONVO MUTED and CLOSED: Trigger simulation to mark as read silently. NO sound.
                                                // console.log(`%cDMM /Check: Window for ${conversationId} is CLOSED & CONVO MUTED. Triggering SILENT UI simulation.`, "color: #DAA520;");
                                                try {
                                                    setTimeout(() => handleNewMessageEvent(conversationId, folderId), 0);
                                                } catch (e) { console.error(`DMM ERROR: Exception queueing handleNewMessageEvent for CONVO MUTED/CLOSED window ${conversationId}`, e); }
                                            } else if (isGloballyMuted) {
                                                // GLOBALLY MUTED and CLOSED: Trigger simulation to mark as read silently. NO sound.
                                                // console.log(`%cDMM /Check: Window for ${conversationId} is CLOSED & GLOBALLY MUTED. Triggering SILENT UI simulation.`, "color: #DAA520;");
                                                try {
                                                    setTimeout(() => handleNewMessageEvent(conversationId, folderId), 0);
                                                } catch (e) { console.error(`DMM ERROR: Exception queueing handleNewMessageEvent for GLOBALLY MUTED/CLOSED window ${conversationId}`, e); }
                                            } else {
                                                // NOT muted (neither convo nor global) and CLOSED: Play "unopened" sound. DO NOT simulate click.
                                                // console.log(`%cDMM /Check: Window for ${conversationId} is CLOSED & NOT muted (convo or global). Playing UNOPENED notification sound.`, "color: #FF8C00");
                                                try {
                                                    const audio = new Audio(UNOPENED_NOTIFICATION_SOUND_URL);
                                                    audio.play().catch(e => { console.warn(`DMM: Unopened notification sound playback failed:`, e.name, e.message); });
                                                } catch (e) { console.error("DMM: Error creating/playing unopened notification sound:", e); }
                                                // No handleNewMessageEvent call here. Let user click.
                                            }
                                        }
                                        // <<< NEW >>> Update sidebar highlighting potentially needed after /Check handles marking read
                                        // Use a small delay to allow potential UI updates from handleNewMessageEvent to settle
                                        setTimeout(() => updateSidebarMuteStatus(conversationId), 150);
                                    } // end if match
                                } // end if status 200
                            } // end if /Check

                            // --- Handle OpenMessage ---
                            else if (rsUrl && typeof rsUrl === 'string' && rsUrl.includes('action=OpenMessage&id_conversation=')) {
                                if (currentStatus === 200 && currentResponseText) {
                                    const conversationIdMatch = rsUrl.match(/id_conversation=(\d+)/);
                                    if (conversationIdMatch && conversationIdMatch[1]) {
                                        const conversationId = conversationIdMatch[1];
                                        // console.log(`%cDMM XHR OpenMessage Resp [${reqId}]: Intercepted successful response for ${conversationId}. Processing...`, "color: #8A2BE2");
                                        try { setTimeout(() => handleOpenMessageResponse(conversationId, currentResponseText), 0); }
                                        catch (e) { console.error(`DMM ERROR: Exception queueing handleOpenMessageResponse for ${conversationId}`, e); }
                                    }
                                } else { /* console.warn(`DMM XHR OpenMessage Resp [${reqId}]: Intercepted non-200 status: ${currentStatus} for ${rsUrl}`); */ }
                            }

                             try { xhr.removeEventListener('readystatechange', dmmReadyStateHandler); } catch(removeError) { /* ignore */ }
                        } // end if readyState 4
                    }; // --- END of dmmReadyStateHandler ---

                    try { xhr.addEventListener('readystatechange', dmmReadyStateHandler); }
                    catch(addListenerError) { console.error(`DMM: FAILED to add readystatechange listener for ${targetUrl}!`, addListenerError); }

                    // Ensure originals are stored correctly if multiple XHRs happen quickly
                    if(!unsafeWindow._original_XMLHttpRequest_open) unsafeWindow._original_XMLHttpRequest_open = origOpen;
                    if(!unsafeWindow._original_XMLHttpRequest_send) unsafeWindow._original_XMLHttpRequest_send = origSend;

                    return origSend.apply(this, arguments);
                }; // --- END of send override ---
                console.log("DMM: XHR wrappers applied.");
            } else {
                console.log("DMM: XHR wrappers appear to be already applied. Skipping re-application.");
            }
            // --- End XHR Wrapper Setup ---

            // Initialize Main Observer
            if (!mainObserver) {
                mainObserver = new MutationObserver(mainObserverCallback);
                mainObserver.observe(document.body, { childList: true, subtree: true });
                console.log("DMM: Main observer initialized.");
            }

            // Initialize Sidebar Observer (modified to be more thorough)
            if (!sidebarObserver) {
                // Try to observe the entire message list container for better coverage
                const messageList = document.getElementById('liste_messages');
                if (messageList) {
                    sidebarObserver = new MutationObserver(sidebarObserverCallback);
                    sidebarObserver.observe(messageList, {
                        childList: true,
                        subtree: true,
                        attributes: false,
                        characterData: false
                    });
                    console.log("DMM: Enhanced sidebar observer initialized.");

                    // Initial scan
                    scanAndUpdateSidebarMutes();
                } else {
                    console.error("DMM: Could not find #liste_messages to observe sidebar changes!");
                }
            }

            // Setup Click Listener for the sidebar
            setupClickListener();

            // Initial scan of sidebar items for mute status
            scanAndUpdateSidebarMutes();

            console.log("DMM: Initialization complete.");
        } else {
            console.log("DMM: Waiting for essential elements... Retrying init.");
            const missing = essentialElements.map((el, i) => el ? '' : ['body', '#zone_messagerie', '#txt_pseudo', '#liste_messages'][i]).filter(Boolean);
            console.log("DMM: Missing elements:", missing.join(', '));
            setTimeout(initializeScript, 500); // Retry
        }
    } // --- END of initializeScript ---


    // --- Start Initialization ---
    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', initializeScript);
    } else {
        initializeScript(); // DOM is already ready
    }


    // --- Cleanup on page unload ---
     window.addEventListener('beforeunload', () => {
         console.log("DMM: Unloading script and cleaning up...");
         if (mainObserver) mainObserver.disconnect(); mainObserver = null;
         if (sidebarObserver) sidebarObserver.disconnect(); sidebarObserver = null;
         if (sidebarScanDebounceTimer) clearTimeout(sidebarScanDebounceTimer); sidebarScanDebounceTimer = null;
         if (openingMutedOverrideTimer) clearTimeout(openingMutedOverrideTimer); openingMutedOverrideTimer = null;

         // Clear all mute timers BEFORE deleting conversation data
         Object.keys(ACTIVE_CONVERSATIONS).forEach(convId => {
            const cData = ACTIVE_CONVERSATIONS[convId];
            if(cData?.muteTimerIntervalId) {
                clearInterval(cData.muteTimerIntervalId);
            }
         });

         // Now close windows and delete data
         Object.keys(ACTIVE_CONVERSATIONS).forEach(convId => {
             try { closeChatWindow(convId, { removeOriginal: true }); }
             catch(e) { console.warn("DMM: Error during unload window cleanup for convId:", convId, e); }
         });
         // Explicitly clear the object just in case closeChatWindow had issues
         for (let key in ACTIVE_CONVERSATIONS) { delete ACTIVE_CONVERSATIONS[key]; }


         const listenerTarget = document.getElementById('liste_messages');
         if (listenerTarget) { try { listenerTarget.removeEventListener('click', handleMessageListClick, true); } catch(e){} }

         // <<< NEW >>> Remove global mute button listener if needed (though often not strictly necessary on unload)
         const globalMuteButton = document.getElementById(GLOBAL_MUTE_BUTTON_ID);
         // Basic check: if it still exists, remove it (or its listener)
         if(globalMuteButton && globalMuteButton.parentNode) {
            try { globalMuteButton.remove(); } catch(e) {}
         }

         try {
              const win = unsafeWindow;
              if (win._original_XMLHttpRequest_open) { win.XMLHttpRequest.prototype.open = win._original_XMLHttpRequest_open; delete win._original_XMLHttpRequest_open; }
              if (win._original_XMLHttpRequest_send) { win.XMLHttpRequest.prototype.send = win._original_XMLHttpRequest_send; delete win._original_XMLHttpRequest_send; }
         } catch (e) { console.error("DMM: Error restoring original XMLHttpRequest methods:", e); }

         console.log("DMM: Cleanup complete.");
     });

})();