Dreadcast Dynamic Messages V1

Messagerie dynamique

// ==UserScript==
// @name           Dreadcast Dynamic Messages V1
// @namespace      http://tampermonkey.net/
// @version        1.9.2
// @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, unreadSeparatorVisible }
    let openingMutedOverride = null;
    let openingMutedOverrideTimer = null;
    const INITIAL_LOAD_COUNT = 10;
    const LOAD_MORE_COUNT = 10;
    // 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 = '1.7.7';

    // --- 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';
    // Header Constants
    const HEADERS_STORAGE_KEY = 'dmm_message_headers_v1';
    const SELECTED_HEADER_STORAGE_KEY = 'dmm_selected_header_v1';
    const MAX_HEADER_LENGTH = 30;
    const MAX_HEADER_HISTORY = 6;

    // --- Global Sound Mute State ---
    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';
    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
    };

    // --- Sound Settings ---
    const SOUND_SETTINGS_STORAGE_KEY = 'dmm_sound_settings_v1';
    const DEFAULT_SOUND_SETTINGS = {
        notificationVolume: 0.5,
        unopenedNotificationVolume: 0.5,
        customNotificationUrl: NOTIFICATION_SOUND_URL,
        customUnopenedUrl: UNOPENED_NOTIFICATION_SOUND_URL
    };

    // --- Unread Separator Constants & Cache ---
    const LAST_SEEN_MESSAGE_IDS_STORAGE_KEY = 'dmm_last_seen_message_ids_v1';
    let lastSeenMessageIds = {}; // Cache for loaded IDs
    const UNREAD_SEPARATOR_ID_PREFIX = 'dmm-unread-separator-'; // Prefix for separator ID
    // --- End Unread Separator ---

    // --- Message Cache Constants ---
    const MESSAGE_CACHE_EXPIRY = 48 * 60 * 60 * 1000; // 48 hours in milliseconds
    const MESSAGE_CACHE_CLEANUP_INTERVAL = 60 * 60 * 1000; // Run cleanup every hour

    // --- Message Cache ---
    const messageCache = new Map(); // Will store { content, timestamp } objects

    // --- Tooltip Variables ---
    let avatarTooltipElement = null;
    let avatarTooltipTimeout = null;

    // --- Edit Mode Constants & Variables ---
    const EDIT_MODE_TOGGLE_BUTTON_ID = 'dmm-edit-mode-toggle';
    const EDIT_POPUP_ID = 'dmm-edit-popup';
    const CUSTOM_CONVO_DATA_STORAGE_KEY = 'dmm_custom_conv_data_v1';
    let isEditModeActive = false;
    let customConversationData = {}; // Cache for custom titles/images { convId: { title: '...', imageUrl: '...' } }
    // --- End Edit Mode ---


    function getSoundSettings() {
        try {
            const stored = localStorage.getItem(SOUND_SETTINGS_STORAGE_KEY);
            return stored ? { ...DEFAULT_SOUND_SETTINGS, ...JSON.parse(stored) } : DEFAULT_SOUND_SETTINGS;
        } catch (e) {
            console.error("DMM Sound: Failed to load sound settings", e);
            return DEFAULT_SOUND_SETTINGS;
        }
    }

    function saveSoundSettings(settings) {
        try {
            localStorage.setItem(SOUND_SETTINGS_STORAGE_KEY, JSON.stringify(settings));
        } catch (e) {
            console.error("DMM Sound: Failed to save sound settings", e);
        }
    }

    function playNotificationSound(isUnopenedNotification = false) {
        const settings = getSoundSettings();
        try {
            const audio = new Audio(
                isUnopenedNotification ? settings.customUnopenedUrl : settings.customNotificationUrl
            );
            audio.volume = isUnopenedNotification ? settings.unopenedNotificationVolume : settings.notificationVolume;
            audio.play().catch(e => {
                console.warn("DMM: Audio playback failed:", e.name, e.message);
            });
        } catch (e) {
            console.error("DMM: Error creating/playing sound:", e);
        }
    }

    // --- Global Sound Mute Utilities ---
    function loadGlobalMuteState() {
        try {
            const storedValue = localStorage.getItem(GLOBAL_SOUND_MUTE_STORAGE_KEY);
            isGloballyMuted = storedValue === 'true'; // localStorage stores strings
        } 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));
        } 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 = '🔇';
                button.title = 'Activer les sons du script DMM';
                button.style.textDecoration = 'line-through';
                button.style.opacity = '0.7';
                button.style.fontSize = '1.5em';
            } else {
                button.textContent = '🔈';
                button.title = 'Couper les sons du script DMM';
                button.style.textDecoration = 'none';
                button.style.opacity = '1';
                button.style.fontSize = '1.5em';
            }
        }
    }

    async function createGlobalMuteButton() {
        try {
            const newsDiv = await waitForElement('.news', 5000);
            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
            }

            // Create container for both buttons
            const buttonContainer = document.createElement('span');
            buttonContainer.style.marginLeft = '10px';

            // Create settings button
            const settingsButton = document.createElement('span');
            settingsButton.id = 'dmm-sound-settings-button';
            settingsButton.textContent = '⚙';
            settingsButton.style.cursor = 'pointer';
            settingsButton.style.fontSize = '1.5em';
            settingsButton.style.verticalAlign = 'middle';
            settingsButton.style.marginRight = '5px';
            settingsButton.title = 'Paramètres des sons DMM';

            // Create settings panel
            const settingsPanel = document.createElement('div');
            settingsPanel.className = 'dmm-sound-settings-panel';
            settingsPanel.style.display = 'none';
            settingsPanel.innerHTML = `
                <div style="padding: 10px;">
                    <h4>Paramètres des Sons</h4>
                    <div class="setting-group">
                        <label>Volume notification (fenêtre ouverte):</label>
                        <input type="range" id="dmm-notification-volume" min="0" max="1" step="0.1">
                        <button class="test-sound-btn" data-type="notification">Test</button>
                    </div>
                    <div class="setting-group">
                        <label>Volume notification (fenêtre fermée):</label>
                        <input type="range" id="dmm-unopened-volume" min="0" max="1" step="0.1">
                        <button class="test-sound-btn" data-type="unopened">Test</button>
                    </div>
                    <div class="setting-group">
                        <label>URL son notification (ouverte):</label>
                        <input type="text" id="dmm-notification-url">
                        <button class="reset-url-btn" data-type="notification">Reset</button>
                    </div>
                    <div class="setting-group">
                        <label>URL son notification (fermée):</label>
                        <input type="text" id="dmm-unopened-url">
                        <button class="reset-url-btn" data-type="unopened">Reset</button>
                    </div>
                </div>
            `;

            // Add styles for settings panel
            GM_addStyle(`
                .dmm-sound-settings-panel {
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: #2a2a2a;
                    border: 1px solid #444;
                    border-radius: 4px;
                    box-shadow: 0 2px 10px rgba(0,0,0,0.5);
                    z-index: 9999999999;
                    min-width: 300px;
                }
                .dmm-sound-settings-panel h4 {
                    margin: 0 0 10px 0;
                    color: #fff;
                    text-align: center;
                }
                .setting-group {
                    margin-bottom: 10px;
                }
                .setting-group label {
                    display: block;
                    margin-bottom: 5px;
                    color: #ccc;
                }
                .setting-group input[type="range"] {
                    width: 80%;
                    vertical-align: middle;
                }
                .setting-group input[type="text"] {
                    width: 80%;
                    padding: 4px;
                    margin-bottom: 5px;
                    background: #333;
                    border: 1px solid #555;
                    color: #fff;
                }
                .test-sound-btn, .reset-url-btn {
                    padding: 2px 8px;
                    margin-left: 5px;
                    background: #444;
                    border: 1px solid #666;
                    color: #fff;
                    cursor: pointer;
                    border-radius: 3px;
                }
                .test-sound-btn:hover, .reset-url-btn:hover {
                    background: #555;
                }
            `);

            // Initialize settings panel with saved values
            const settings = getSoundSettings();
            const notificationVolume = settingsPanel.querySelector('#dmm-notification-volume');
            const unopenedVolume = settingsPanel.querySelector('#dmm-unopened-volume');
            const notificationUrl = settingsPanel.querySelector('#dmm-notification-url');
            const unopenedUrl = settingsPanel.querySelector('#dmm-unopened-url');

            notificationVolume.value = settings.notificationVolume;
            unopenedVolume.value = settings.unopenedNotificationVolume;
            notificationUrl.value = settings.customNotificationUrl;
            unopenedUrl.value = settings.customUnopenedUrl;

            // Event handlers for settings panel
            settingsButton.addEventListener('click', () => {
                settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
            });

            // Save settings changes
            [notificationVolume, unopenedVolume, notificationUrl, unopenedUrl].forEach(input => {
                input.addEventListener('change', () => {
                    const newSettings = {
                        notificationVolume: parseFloat(notificationVolume.value),
                        unopenedNotificationVolume: parseFloat(unopenedVolume.value),
                        customNotificationUrl: notificationUrl.value,
                        customUnopenedUrl: unopenedUrl.value
                    };
                    saveSoundSettings(newSettings);
                });
            });

            // Test sound buttons
            settingsPanel.querySelectorAll('.test-sound-btn').forEach(btn => {
                btn.addEventListener('click', () => {
                    const type = btn.dataset.type;
                    const settings = getSoundSettings();
                    const audio = new Audio(
                        type === 'notification' ? settings.customNotificationUrl : settings.customUnopenedUrl
                    );
                    audio.volume = type === 'notification' ? settings.notificationVolume : settings.unopenedNotificationVolume;
                    audio.play().catch(e => console.warn('DMM Sound Test: Playback failed:', e));
                });
            });

            // Reset URL buttons
            settingsPanel.querySelectorAll('.reset-url-btn').forEach(btn => {
                btn.addEventListener('click', () => {
                    const type = btn.dataset.type;
                    const input = settingsPanel.querySelector(`#dmm-${type}-url`);
                    input.value = type === 'notification' ? NOTIFICATION_SOUND_URL : UNOPENED_NOTIFICATION_SOUND_URL;
                    input.dispatchEvent(new Event('change'));
                });
            });

            // Close panel when clicking outside
            document.addEventListener('click', (e) => {
                if (!settingsPanel.contains(e.target) && e.target !== settingsButton) {
                    settingsPanel.style.display = 'none';
                }
            });

            // Create mute button (existing code)
            const muteButton = document.createElement('span');
            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
            muteButton.style.position = 'relative'; // Needed for z-index to reliably apply vs static elements

            muteButton.addEventListener('click', () => {
                isGloballyMuted = !isGloballyMuted;
                saveGlobalMuteState();
                updateGlobalMuteButtonAppearance();
            });

            // Insert the button after the news div
            buttonContainer.appendChild(settingsButton);
            buttonContainer.appendChild(muteButton);
            newsDiv.insertAdjacentElement('afterend', buttonContainer);
            document.body.appendChild(settingsPanel);

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

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

    // --- Header Management Functions ---
    function getHeaderHistory() {
        try {
            const stored = localStorage.getItem(HEADERS_STORAGE_KEY);
            return stored ? JSON.parse(stored) : [];
        } catch (e) {
            console.error("DMM: Failed to parse header history", e);
            return [];
        }
    }

    function addHeaderToHistory(header) {
        if (!header || header.length > MAX_HEADER_LENGTH) return;
        let headers = getHeaderHistory();
        // Remove if exists (to move to front)
        headers = headers.filter(h => h !== header);
        // Add to front
        headers.unshift(header);
        // Keep only MAX_HEADER_HISTORY entries
        headers = headers.slice(0, MAX_HEADER_HISTORY);
        try {
            localStorage.setItem(HEADERS_STORAGE_KEY, JSON.stringify(headers));
        } catch (e) {
            console.error("DMM: Failed to save header history", e);
        }
    }

    function getSelectedHeader() {
        return localStorage.getItem(SELECTED_HEADER_STORAGE_KEY) || '';
    }

    function setSelectedHeader(header) {
        if (header) {
            localStorage.setItem(SELECTED_HEADER_STORAGE_KEY, header);
        } else {
            localStorage.removeItem(SELECTED_HEADER_STORAGE_KEY);
        }
    }

    function updateHeaderHistory() {
        const headerPanel = document.querySelector('.header-panel');
        if (!headerPanel) return;

        const historyList = headerPanel.querySelector('.header-history-list');
        if (!historyList) return;

        const headers = getHeaderHistory();
        const selectedHeader = getSelectedHeader();

        // Clear existing list
        historyList.innerHTML = '';

        // Add each header to the list
        headers.forEach(header => {
            const item = document.createElement('div');
            item.classList.add('header-history-item');
            if (header === selectedHeader) {
                item.classList.add('selected');
            }
            item.textContent = header;
            item.dataset.header = header;
            historyList.appendChild(item);
        });

        // If empty, show placeholder
        if (headers.length === 0) {
            const placeholder = document.createElement('div');
            placeholder.classList.add('header-history-item');
            placeholder.style.fontStyle = 'italic';
            placeholder.style.color = '#666';
            placeholder.textContent = 'Aucun entête enregistré';
            historyList.appendChild(placeholder);
        }
    }

    // --- End Header Management Functions ---

    // --- Mute Utilities ---
    function getMutedData() {
        try {
            let stored = localStorage.getItem(MUTED_CONVERSATIONS_STORAGE_KEY_V3);
            if (!stored) {
                 stored = '{}';
            }

            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;

        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.
                    delete mutedData[idStr];
                    dataNeedsSaving = true;
                    isCurrentlyMuted = false;
                    needsSidebarUpdate = true;
                }
            } else {
                // Invalid entry (missing endTime or invalid type), clean up
                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
                 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)
        // 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) {
            if (mutedData[idStr]) {
                delete mutedData[idStr];
                needsSave = true;
            }
        } 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;
             }
        } else if (typeof durationMs === 'number' && durationMs > 0) {
            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;
            }
        } else {
        }

        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);
             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) {
            return; // Element not visible (e.g., different folder) or doesn't exist
        }

        const titleElement = listItem.querySelector('.message_titre');
        if (!titleElement) {
            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_"]');
        messageListItems.forEach(item => {
            const conversationId = item.id.replace('message_', '');
            if (conversationId) {
                // Existing mute status update
                updateSidebarMuteStatus(conversationId);

                // Apply customizations (NEW)
                applyCustomizationsToItem(item);

                // Apply edit mode visuals if active (NEW)
                if (isEditModeActive) {
                    item.classList.add('dmm-editable-item');
                    item.title = 'Cliquer pour éditer le titre/image';
                }
            }
        });
    }
    // --- 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);
                    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) {
        if (ms <= 0) return ""; // No time left or invalid input

        const totalSeconds = Math.round(ms / 1000);
        const minutes = Math.floor(totalSeconds / 60);

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

    // --- Tooltip Functions ---
    function createAvatarTooltip() {
        if (document.getElementById('dmm-avatar-tooltip')) {
            avatarTooltipElement = document.getElementById('dmm-avatar-tooltip');
            return;
        }
        avatarTooltipElement = document.createElement('div');
        avatarTooltipElement.id = 'dmm-avatar-tooltip';
        document.body.appendChild(avatarTooltipElement);
    }

    function showAvatarTooltip(avatarElement) {
        if (!avatarTooltipElement || !avatarElement) return;

        if (avatarTooltipTimeout) {
            clearTimeout(avatarTooltipTimeout);
            avatarTooltipTimeout = null;
        }

        const avatarSrc = avatarElement.style.backgroundImage.slice(4, -1).replace(/["']/g, "");
        if (!avatarSrc) return;

        const avatarRect = avatarElement.getBoundingClientRect();
        const tooltipWidth = 128 + 4;
        const tooltipHeight = 128 + 4;
        const spacing = 5;

        let idealTop = avatarRect.top - tooltipHeight - spacing;
        let idealLeft = avatarRect.left + (avatarRect.width / 2) - (tooltipWidth / 2);

        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;

        if (idealTop < 0) {
            idealTop = avatarRect.bottom + spacing;
        }

        if (idealTop + tooltipHeight > viewportHeight) {
            idealTop = Math.max(0, viewportHeight - tooltipHeight);
        }

        if (idealLeft < 0) {
            idealLeft = 0;
        }

        if (idealLeft + tooltipWidth > viewportWidth) {
            idealLeft = viewportWidth - tooltipWidth;
        }

        if (idealLeft < 0) idealLeft = 0;

        avatarTooltipElement.style.backgroundImage = `url('${avatarSrc}')`;
        avatarTooltipElement.style.top = `${Math.round(idealTop)}px`;
        avatarTooltipElement.style.left = `${Math.round(idealLeft)}px`;
        avatarTooltipElement.style.display = 'block';
    }

    function hideAvatarTooltip(delay = 150) {
        if (avatarTooltipTimeout) return;

        if (avatarTooltipElement) {
            avatarTooltipTimeout = setTimeout(() => {
                avatarTooltipElement.style.display = 'none';
                avatarTooltipElement.style.backgroundImage = '';
                avatarTooltipTimeout = null;
            }, delay);
        }
    }

    // --- Last Seen Message ID Management ---
    function loadLastSeenMessageIds() {
        try {
            const stored = localStorage.getItem(LAST_SEEN_MESSAGE_IDS_STORAGE_KEY);
            lastSeenMessageIds = stored ? JSON.parse(stored) : {};
        } catch (e) {
            console.error("DMM: Failed to load last seen message IDs", e);
            lastSeenMessageIds = {};
        }
    }

    function saveLastSeenMessageId(conversationId, messageId) {
        if (!conversationId || !messageId) return; // Don't save invalid data
        const currentLatestId = String(messageId); // Ensure it's a string
        if (lastSeenMessageIds[conversationId] !== currentLatestId) {
            lastSeenMessageIds[conversationId] = currentLatestId;
            try {
                localStorage.setItem(LAST_SEEN_MESSAGE_IDS_STORAGE_KEY, JSON.stringify(lastSeenMessageIds));
            } catch (e) {
                console.error("DMM: Failed to save last seen message IDs", e);
            }
        }
    }

    function getLastSeenMessageId(conversationId) {
        return lastSeenMessageIds[conversationId] || null;
    }
    // --- End Last Seen Message ID Management ---


    // --- 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; /* Changed from pre-wrap to normal for linkification */ }
             /* Style for links within bubbles */
             .bubble-content a { color: #add8e6 !important; /* Light blue, !important to override potential other styles */ text-decoration: underline !important; }
             .bubble-content a:hover { color: #ffeb3b !important; /* Yellow on hover */ }
             .my-bubble .bubble-content a { color: #ffffff !important; /* White link in my bubble */ text-decoration: underline !important; }
             .my-bubble .bubble-content a:hover { color: #ffff00 !important; /* Bright yellow on hover */ }

             /* Avatar styling - UPDATED */
             .their-bubble { display: flex; gap: 8px; }
             .their-bubble .bubble-avatar {
                 width: 28px;
                 height: 28px;
                 border-radius: 4px;
                 margin-top: 2px;
                 position: relative; /* Add for tooltip positioning */
                 background-size: cover;
                 background-position: center;
             }

             .their-bubble .bubble-content-wrapper {
                 flex: 1;
                 min-width: 0; /* Prevent flex item from overflowing */
                 display: flex;
                 flex-direction: column;
             }
             .their-bubble .bubble-header {
                 display: flex;
                 align-items: flex-start;
                 gap: 8px;
                 margin-bottom: 4px;
             }
             .their-bubble .bubble-sender-name {
                 flex: 1;
                 padding-top: 8px;
                 font-size: 0.8em;
                 font-weight: bold;
                 color: #87ceeb;
                 cursor: pointer; /* Show pointer on hover */
                 position: relative; /* For tooltip positioning */
             }

             .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; }

             /* Common header styles for both bubbles */
             .bubble-header {
                 display: flex;
                 align-items: flex-start;
                 gap: 8px;
                 margin-bottom: 4px;
             }
             .bubble-avatar {
                 width: 28px;
                 height: 28px;
                 border-radius: 4px;
                 margin-top: 2px;
                 background-size: cover;
                 background-position: center;
             }
             .bubble-sender-name {
                 flex: 1;
                 padding-top: 8px;
                 font-size: 0.8em;
                 font-weight: bold;
                 cursor: pointer;
                 position: relative;
             }

             /* Specific colors for their/my bubbles */
             .their-bubble .bubble-sender-name {
                 color: #87ceeb; /* Pale blue */
             }
             .my-bubble .bubble-sender-name {
                 color: #87ceeb; /* Blueish */
             }

             .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;
            }

            /* Header Panel */
            .header-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;
            }
            .header-input-container {
                display: flex;
                gap: 5px;
                margin-bottom: 10px;
            }
            .header-input-container input {
                flex-grow: 1;
                padding: 5px;
                background: #333;
                border: 1px solid #444;
                color: #fff;
                border-radius: 3px;
            }
            .header-input-container .checkmark-btn {
                cursor: pointer;
                color: #4CAF50;
                font-size: 1.2em;
                display: flex;
                align-items: center;
                padding: 0 5px;
                opacity: 0.8;
                transition: opacity 0.2s ease;
            }
            .header-input-container .checkmark-btn:hover {
                opacity: 1;
            }
            .header-history-list {
                max-height: 150px;
                overflow-y: auto;
            }
            .header-history-item {
                padding: 5px 10px;
                cursor: pointer;
                color: #ccc;
            }
            .header-history-item:hover {
                background-color: var(--dmm-menu-hover-bg);
                color: #fff;
            }
            .header-history-item.selected {
                background-color: var(--dmm-primary-color);
                color: #fff;
            }

            /* Unread Separator Line */ /* <<< NEW STYLE >>> */
             .unread-separator {
                 width: 100%;
                 display: flex;
                 align-items: center;
                 text-align: center;
                 margin: 10px 0;
                 padding: 0 5px; /* Padding inside */
                 box-sizing: border-box; /* Include padding in width */
                 opacity: 1;
                 transition: opacity 1.5s ease-out; /* Add transition */
             }
             .unread-separator.fade-out {
                 opacity: 0;
                 pointer-events: none;
             }
             .unread-separator::before,
             .unread-separator::after {
                 content: '';
                 flex: 1;
                 border-bottom: 1px solid #f06e6e; /* Red line */
             }
             .unread-separator:not(:empty)::before {
                 margin-right: .5em;
             }
             .unread-separator:not(:empty)::after {
                 margin-left: .5em;
             }
             .unread-separator span {
                 color: #f06e6e; /* Red text */
                 font-size: 0.8em;
                 font-weight: bold;
                 padding: 0 5px; /* Space around text */
                 white-space: nowrap;
             }

            /* Tooltip Element */
            #dmm-avatar-tooltip {
                position: fixed;
                width: 128px;
                height: 128px;
                border: 2px solid #555;
                border-radius: 4px;
                background-size: cover;
                background-position: center;
                background-repeat: no-repeat;
                z-index: 99999999;
                pointer-events: none;
                display: none;
                box-shadow: 0 2px 5px rgba(0,0,0,0.5);
            }

            /* Touch-specific styles */
            @media (hover: none) and (pointer: coarse) {
                .custom-chat-head .controls span,
                .menu-item,
                .load-more-link {
                    padding: 12px 15px; /* Larger touch targets */
                }

                .resize-handle {
                    width: 24px;
                    height: 24px; /* Larger resize handle for touch */
                }

                .custom-chat-reply button {
                    padding: 12px 20px;
                }
            }

            /* --- Edit Mode Styles --- */
            #${EDIT_MODE_TOGGLE_BUTTON_ID} {
                position: fixed;
                z-index: 100001;
                cursor: pointer;
                padding: 1px 2px;
                background-color: #355b75; /* Default dreadcast blue */
                color: white;
                border-radius: 50%;
                font-size: 15px;
                line-height: 1;
                box-shadow: 0 2px 5px rgba(0,0,0,0.3);
                transition: background-color 0.3s ease, transform 0.2s ease;
                user-select: none;
            }
            #${EDIT_MODE_TOGGLE_BUTTON_ID}:hover {
                transform: scale(1.1);
            }
            #${EDIT_MODE_TOGGLE_BUTTON_ID}.active {
                background-color: #e67e22; /* Active orange */
            }

            #${EDIT_POPUP_ID} {
                display: none; /* Hidden by default */
                position: fixed;
                z-index: 999999999998;
                background-color: #2f2f2f;
                border: 1px solid #555;
                border-radius: 5px;
                box-shadow: 0 4px 10px rgba(0,0,0,0.5);
                padding: 15px;
                min-width: 280px;
                color: #e0e0e0;
            }

            .dmm-edit-field {
                margin-bottom: 10px;
            }
            .dmm-edit-field label {
                display: block;
                margin-bottom: 4px;
                font-size: 0.9em;
                color: #ccc;
            }
            .dmm-edit-field input[type="text"] {
                width: calc(100% - 12px);
                padding: 6px;
                background-color: #1a1a1a;
                border: 1px solid #444;
                color: #ddd;
                border-radius: 3px;
            }
            .dmm-edit-buttons {
                margin-top: 15px;
                text-align: right;
                display: flex;
                justify-content: flex-end;
                gap: 8px;
            }
            .dmm-edit-buttons button {
                padding: 6px 12px;
                border: none;
                border-radius: 3px;
                cursor: pointer;
                font-weight: bold;
                transition: background-color 0.2s ease;
            }
            #dmm-edit-save {
                background-color: #2ecc71;
                color: white;
            }
            #dmm-edit-save:hover {
                background-color: #27ae60;
            }
            #dmm-edit-cancel, #dmm-edit-reset {
                background-color: #7f8c8d;
                color: white;
            }
            #dmm-edit-reset {
                background-color: #e74c3c;
            }
            #dmm-edit-cancel:hover, #dmm-edit-reset:hover {
                background-color: #95a5a6;
            }
            #dmm-edit-reset:hover {
                background-color: #c0392b;
            }

            /* Style for editable items in sidebar */
            .dmm-edit-mode-active #liste_messages li.message.dmm-editable-item {
                cursor: pointer;
                outline: 1px dashed #e67e22;
                outline-offset: -1px;
            }
            .dmm-edit-mode-active #liste_messages li.message.dmm-editable-item:hover {
                background-color: rgba(230, 126, 34, 0.15);
            }
            /* --- End Edit Mode Styles --- */

            /* Invite Input Styles - Revised for Slim Line */
            .dmm-invite-container {
                display: none;
                padding: 3px 8px;
                border-top: 1px solid #333;
                background-color: #151515;
                flex-shrink: 0;
                width: 100%;
                box-sizing: border-box;
            }

            .dmm-invite-container label {
                display: none;
            }

            .dmm-invite-container input[type="text"] {
                width: 100%;
                border: none;
                border-bottom: 1px solid #555;
                background-color: transparent;
                padding: 2px 0;
                font-size: 0.85em;
                color: #ccc;
                outline: none;
                box-shadow: none;
                border-radius: 0;
                transition: border-bottom-color 0.2s ease;
            }

            .dmm-invite-container input[type="text"]:focus {
                border-bottom-color: var(--dmm-primary-color);
            }

            /* Style for the invite menu item when active */
            .custom-chat-window .more-opts-menu .menu-item.invite-active {
                background-color: rgba(255, 255, 255, 0.1);
                font-weight: bold;
            }
            .custom-chat-window .more-opts-menu .menu-item.invite-active::before {
                content: '✓ ';
                color: #66ff66;
                margin-right: 4px;
            }
            /* End Invite Input Styles */

            .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;
            }
         `);
     }


    // --- 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;
            h.ontouchstart = dragTouchStart;
        }
        else {
            element.onmousedown = dragMouseDown;
            element.ontouchstart = dragTouchStart;
        }

        function dragTouchStart(e) {
            e.preventDefault();
            // 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;

            const touch = e.touches[0];
            pos3 = touch.clientX;
            pos4 = touch.clientY;
            document.ontouchend = closeDragElement;
            document.ontouchmove = elementTouchDrag;
        }

        function elementTouchDrag(e) {
            e.preventDefault();
            const touch = e.touches[0];
            pos1 = pos3 - touch.clientX;
            pos2 = pos4 - touch.clientY;
            pos3 = touch.clientX;
            pos4 = touch.clientY;
            element.style.top = Math.max(0, (element.offsetTop - pos2)) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
        }

        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;
            document.ontouchend = null;
            document.ontouchmove = null;
        }
    }

    function makeResizable(element, handle) {
        let startX, startY, startWidth, startHeight;

        // Mouse events
        handle.addEventListener('mousedown', initResize, false);

        // Touch events
        handle.addEventListener('touchstart', initTouchResize, false);

        function initTouchResize(e) {
            e.preventDefault();
            const touch = e.touches[0];
            startResize(touch.clientX, touch.clientY);
            document.addEventListener('touchmove', doTouchResize, false);
            document.addEventListener('touchend', stopResize, false);
        }

        function doTouchResize(e) {
            e.preventDefault();
            const touch = e.touches[0];
            resizeElement(touch.clientX, touch.clientY);
        }

        function initResize(e) {
            e.preventDefault();
            startResize(e.clientX, e.clientY);
            document.addEventListener('mousemove', doResize, false);
            document.addEventListener('mouseup', stopResize, false);
        }

        function startResize(x, y) {
            startX = x;
            startY = y;
            const computedStyle = document.defaultView.getComputedStyle(element);
            startWidth = parseInt(computedStyle.width, 10);
            startHeight = parseInt(computedStyle.height, 10);
        }

        function doResize(e) {
            resizeElement(e.clientX, e.clientY);
        }

        function resizeElement(x, y) {
            let newWidth = startWidth + x - startX;
            let newHeight = startHeight + y - startY;
            newWidth = Math.max(MIN_WINDOW_WIDTH, newWidth);
            newHeight = Math.max(MIN_WINDOW_HEIGHT, newHeight);
            newWidth = Math.min(window.innerWidth - element.offsetLeft - 5, newWidth);
            newHeight = Math.min(window.innerHeight - element.offsetTop - 5, newHeight);
            element.style.width = newWidth + 'px';
            element.style.height = newHeight + 'px';
        }

        function stopResize() {
            document.removeEventListener('mousemove', doResize, false);
            document.removeEventListener('mouseup', stopResize, false);
            document.removeEventListener('touchmove', doTouchResize, false);
            document.removeEventListener('touchend', stopResize, false);
        }
    }


    // --- Message Parsing and Fetching ---

    function cleanupMessageCache() {
        const now = Date.now();
        let cleanupCount = 0;

        for (const [key, data] of messageCache.entries()) {
            if (now - data.timestamp > MESSAGE_CACHE_EXPIRY) {
                messageCache.delete(key);
                cleanupCount++;
            }
        }
    }

    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) {
            return null;
        }
        return { id, timestamp, sender }; // 'id' here is the internal message_id for content
    }

    function fetchMessageContent(messageId, conversationId, callback) {
        const cacheKey = `${conversationId}_${messageId}`;

        // Check cache with expiry
        if (messageCache.has(cacheKey)) {
            const cachedData = messageCache.get(cacheKey);
            const now = Date.now();

            // If cached data is still valid
            if (now - cachedData.timestamp <= MESSAGE_CACHE_EXPIRY) {
                callback(cachedData.content);
                return;
            } else {
                // Remove expired entry
                messageCache.delete(cacheKey);
            }
        }

        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";

                        // Store content with timestamp
                        messageCache.set(cacheKey, {
                            content: content,
                            timestamp: Date.now()
                        });

                        callback(content);
                    } catch (e) {
                        console.error(`%cDMM fetchMessageContent[${conversationId}]: Parse XML error for msg ${messageId}`, "color: red", e);
                        callback("Erreur: Parse XML");
                    }
                } else {
                    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() {
                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) { return defaultResult; }

        // --- Participant Parsing (Keep existing logic) ---
        let participants = [];
        try {
            const participantsTitleDiv = Array.from(conversationZone.querySelectorAll('div')).find(div => div.textContent.includes('Participants'));
            if (participantsTitleDiv) {
                let currentElement = participantsTitleDiv.nextElementSibling;
                let participantsString = '';
                while(currentElement && currentElement.tagName !== 'P') {
                    currentElement = currentElement.nextElementSibling;
                }
                 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); }
            }
        } catch (e) { }

        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 = []; // This will hold the { id, timestamp, sender, content } objects
        const BATCH_SIZE = 15; // Keep batching

        for(let i = 0; i < Math.min(INITIAL_LOAD_COUNT, fetchElements.length); i += BATCH_SIZE) {
            const batch = fetchElements.slice(i, i + BATCH_SIZE);
            let batchPromises = []; // Promises for fetches within this batch

            for (const el of batch) {
                const parsed = parseMessageElement(el); // Get { id, timestamp, sender }
                if (parsed && parsed.id) { // Ensure we have a valid parsed object with an ID
                    let msgData = { ...parsed, content: null };
                    fetchedData.push(msgData); // Add structure immediately to fetchedData array

                    // --- Cache Check BEFORE deciding to Fetch ---
                    const cacheKey = `${conversationId}_${parsed.id}`;
                    const cached = messageCache.get(cacheKey);
                    const now = Date.now();

                    if (cached && (now - cached.timestamp <= MESSAGE_CACHE_EXPIRY)) {
                        // Use cached content directly & update the object in fetchedData
                        const targetMsg = fetchedData.find(m => m.id === parsed.id);
                        if (targetMsg) {
                             targetMsg.content = cached.content;
                        }
                    } else {
                        // Not cached or expired, push a fetch promise
                        batchPromises.push(new Promise((resolve) => {
                            // fetchMessageContent handles adding to cache on success
                            fetchMessageContent(parsed.id, conversationId, (content) => {
                                // Update the content in the already existing object in fetchedData
                                const targetMsg = fetchedData.find(m => m.id === parsed.id);
                                if (targetMsg) {
                                    targetMsg.content = content;
                                } else {
                                }
                                resolve(); // Resolve the promise for this specific fetch
                            });
                        }));
                    }
                    // --- End Cache Check ---
                } else {
                }
            } // End loop through batch elements

            // Process batch fetches (wait for all fetches in this batch to complete)
            if (batchPromises.length > 0) {
                await Promise.all(batchPromises);
            }
        } // End loop through batches

        // Reverse the array containing message data objects to show oldest first
        fetchedData.reverse();

        // Determine latest and oldest IDs from the fetchedData array
        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;
    }


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

    // --- Custom Conversation Data Storage ---
    function loadCustomConversationData() {
        try {
            const stored = localStorage.getItem(CUSTOM_CONVO_DATA_STORAGE_KEY);
            customConversationData = stored ? JSON.parse(stored) : {};
        } catch (e) {
            console.error("DMM EditMode: Failed to load custom conversation data", e);
            customConversationData = {};
        }
    }

    function saveCustomConversationData() {
        try {
            localStorage.setItem(CUSTOM_CONVO_DATA_STORAGE_KEY, JSON.stringify(customConversationData));
        } catch (e) {
            console.error("DMM EditMode: Failed to save custom conversation data", e);
        }
    }

    function getCustomData(conversationId) {
        const data = customConversationData[conversationId];
        if (data && (data.title !== undefined || data.imageUrl !== undefined)) {
            // Return a copy with defaults for potentially missing keys
            return {
                title: data.title !== undefined ? data.title : null,
                imageUrl: data.imageUrl !== undefined ? data.imageUrl : null
            };
        }
        return null; // No custom data set for this conversation
    }

    function setCustomData(conversationId, title, imageUrl) {
        const cleanTitle = title?.trim() || null;
        const cleanImageUrl = imageUrl?.trim() || null;

        if (cleanTitle === null && cleanImageUrl === null) {
            // If both are being removed, delete the entry
            if (customConversationData[conversationId]) {
                delete customConversationData[conversationId];
                saveCustomConversationData();
            } else {
            }
        } else {
            // Otherwise, create or update the entry
            if (!customConversationData[conversationId]) {
                customConversationData[conversationId] = {};
            }
            // Update only if values are provided (or explicitly nullified)
            if (title !== undefined) customConversationData[conversationId].title = cleanTitle;
            if (imageUrl !== undefined) customConversationData[conversationId].imageUrl = cleanImageUrl;

            // Clean up potentially nullified properties if they exist
            if (customConversationData[conversationId].title === null) delete customConversationData[conversationId].title;
            if (customConversationData[conversationId].imageUrl === null) delete customConversationData[conversationId].imageUrl;

            // If the object is now empty after removing nulls, remove the whole entry
            if (Object.keys(customConversationData[conversationId]).length === 0) {
                delete customConversationData[conversationId];
            } else {
            }
            saveCustomConversationData();
        }
    }
    // --- End Custom Conversation Data Storage ---

    // <<< NEW FUNCTION >>>
    function createUnreadSeparatorElement(conversationId) {
        const separator = document.createElement('div');
        separator.id = `${UNREAD_SEPARATOR_ID_PREFIX}${conversationId}`;
        separator.classList.add('unread-separator');
        const textSpan = document.createElement('span');
        textSpan.textContent = 'Nouveaux messages';
        separator.appendChild(textSpan);
        return separator;
    }

    // ================== START OF MODIFIED addBubble FUNCTION ===============
    /**
     * Adds a message bubble to the chat window OR a DocumentFragment. Detects URLs and makes them clickable.
 * @param {object} msgData - The message data { id, timestamp, sender, content }. id is message_id.
 * @param {HTMLElement|DocumentFragment} containerOrFragment - The DOM element or fragment to add the bubble to.
 * @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 (affects sound/notification).
 * @returns {HTMLElement|null} The added bubble element, or null if not added.
 */
function addBubble(msgData, containerOrFragment, conversationId, prepend = false, isInitialLoad = false) {
    // Determine if we are working with the live container or a fragment
    const isLiveContainer = containerOrFragment instanceof HTMLElement;
    // Try to get the live container even if appending to a fragment, for checks/scroll later
    const liveContainer = isLiveContainer ? containerOrFragment : ACTIVE_CONVERSATIONS[conversationId]?.customWindow?.querySelector('.custom-chat-content');

    if (!MY_NAME || (!liveContainer && !isInitialLoad)) { // Allow initial load even if liveContainer isn't found *yet*
        return null;
    }

    // --- Perform checks against the LIVE container if available ---
    if (msgData.id && liveContainer) {
        const existingById = liveContainer.querySelector(`.chat-bubble[data-message-id="${msgData.id}"]`);
        if (existingById) {
            return null; // Don't add duplicates
        }
    }

    // --- Bubble Creation Logic (largely unchanged) ---
    let processedContent = msgData.content || "...";
    let headerText = null;
    const headerMatch = processedContent.match(/^\s*\|([^|]+)\|\s*(\n|$)/);
    if (headerMatch) {
        headerText = headerMatch[1].trim();
        processedContent = processedContent.substring(headerMatch[0].length).trim();
    }

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

    const contentWrapper = document.createElement('div');
    contentWrapper.classList.add('bubble-content-wrapper');

    const headerContainer = document.createElement('div');
    headerContainer.classList.add('bubble-header');

    const avatarDiv = document.createElement('div');
    avatarDiv.classList.add('bubble-avatar');
    avatarDiv.style.cursor = 'pointer';
    avatarDiv.style.backgroundImage = `url('https://www.dreadcast.net/images/avatars/${encodeURIComponent(msgData.sender)}.png')`;
    avatarDiv.style.backgroundSize = 'cover';
    avatarDiv.style.backgroundPosition = 'center';
    avatarDiv.addEventListener('click', () => {
        window.open(`https://www.dreadcast.net/${msgData.sender}`, '_blank');
    });
    // Add hover listeners for tooltip
    avatarDiv.addEventListener('mouseenter', () => showAvatarTooltip(avatarDiv));
    avatarDiv.addEventListener('mouseleave', (e) => {
         if (!e.relatedTarget || !e.relatedTarget.closest('#dmm-avatar-tooltip')) {
             hideAvatarTooltip();
         }
    });
    headerContainer.appendChild(avatarDiv);

    const n = document.createElement('div');
    n.classList.add('bubble-sender-name');
    n.textContent = headerText ? `${msgData.sender} - ${headerText}` : msgData.sender;
    n.style.cursor = 'pointer';
    n.addEventListener('click', () => {
        window.open(`https://www.dreadcast.net/${msgData.sender}`, '_blank');
    });
    headerContainer.appendChild(n);
    contentWrapper.appendChild(headerContainer);

    const c = document.createElement('div');
    c.classList.add('bubble-content');

    // Linkification
    const tempDiv = document.createElement('div');
    tempDiv.textContent = processedContent;
    let escapedContent = tempDiv.innerHTML;
    escapedContent = escapedContent.replace(/\n/g, '<br>');
    const urlRegex = /(\b(?:https?:\/\/|www\.)[^\s<>"'()]+)/gi;
    const linkifiedContent = escapedContent.replace(urlRegex, (match) => {
        let href = match;
        if (href.toLowerCase().startsWith('www.')) {
            href = 'https://' + href;
        }
        // Ensure quotes in URL are handled correctly
        href = href.replace(/"/g, '%22').replace(/'/g, '%27');
        return `<a href="${href}" target="_blank" rel="noopener noreferrer">${match}</a>`;
    });
    c.innerHTML = linkifiedContent;
    contentWrapper.appendChild(c);

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


    // --- Scroll calculations should use the LIVE container if available ---
    let isScrolledToBottom = false;
    let shouldScrollDown = false;
    if (liveContainer) {
        // Consider a slightly larger threshold for scrolled to bottom
        isScrolledToBottom = Math.abs(liveContainer.scrollHeight - liveContainer.clientHeight - liveContainer.scrollTop) < 70;
        shouldScrollDown = !prepend && (isScrolledToBottom || isMine);
    }


    // --- Append to the correct target (live container or fragment) ---
    if (prepend) {
        // Prepending usually happens when loading older, directly into the live container
        if (isLiveContainer) {
             const loadMoreElem = containerOrFragment.querySelector('.load-more-container');
             if (loadMoreElem) {
                 // Use insertBefore on the *next* sibling of loadMoreElem
                 containerOrFragment.insertBefore(bubble, loadMoreElem.nextSibling);
             } else {
                 containerOrFragment.insertBefore(bubble, containerOrFragment.firstChild);
             }
        } else {
             // Prepending to a fragment (might happen during initial build if logic changes)
             // Insert before the first existing child of the fragment
             containerOrFragment.insertBefore(bubble, containerOrFragment.firstChild);
        }
    } else {
         // Append normally (to fragment during initial build, or live container otherwise)
         containerOrFragment.appendChild(bubble);
    }

    // --- Sound & Notification State (Only when adding to live container and not initial load) ---
    if (isLiveContainer && !isMine && !prepend && !isInitialLoad) {
         const isConvoMuted = isConversationMuted(conversationId);
         if (!isConvoMuted && !isGloballyMuted) {
             playNotificationSound(false);
             const convData = ACTIVE_CONVERSATIONS[conversationId];
             const chatWindow = liveContainer.closest('.custom-chat-window');
              if (convData && chatWindow && !chatWindow.classList.contains('has-unread-notification')) {
                  convData.hasUnreadNotification = true;
                  chatWindow.classList.add('has-unread-notification');
              }
          } else { // Muted or globally muted
              const convData = ACTIVE_CONVERSATIONS[conversationId];
              const chatWindow = liveContainer.closest('.custom-chat-window');
              if (convData && chatWindow && !chatWindow.classList.contains('has-unread-notification')) {
                 convData.hasUnreadNotification = true;
                 chatWindow.classList.add('has-unread-notification');
              }
          }
    }

    // --- Scrolling (Only if adding to live container and conditions met) ---
    if (isLiveContainer && shouldScrollDown) {
        // Use requestAnimationFrame for smoother scrolling after DOM update
        requestAnimationFrame(() => {
            // Double-check container exists before scrolling
            if (liveContainer && liveContainer.isConnected) {
                liveContainer.scrollTop = liveContainer.scrollHeight;
            }
        });
    }

    return bubble; // Return the created bubble element
} // --- END of modified addBubble function ---
    // =======================================================================
    // =================== END OF MODIFIED addBubble FUNCTION ================
    // =======================================================================


    // =======================================================================
    // ================== START OF MODIFIED buildInitialChatUI ===============
    // =======================================================================
    function buildInitialChatUI(messages, container, conversationId, allMessagesLoaded) {
        container.innerHTML = ''; // Clear existing content first
        container.classList.remove('loading');
        let separatorInserted = false;
        let separatorElement = null; // Keep track if separator element is created

        if (!MY_NAME) {
            container.innerHTML = "<p style='color:red;'>Erreur: Nom utilisateur non trouvé.</p>";
            return { latestId: null, oldestId: null, separatorInserted: false };
        }

        // --- Create a DocumentFragment ---
        const fragment = document.createDocumentFragment();

        // Add "Load More" link if applicable (to the fragment)
        if (!allMessagesLoaded) {
            // Create and append the load more link container to the fragment
            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);
            fragment.appendChild(loadMoreContainer); // Add to fragment
        }

        // --- Get Last Seen ID ---
        const lastSeenId = getLastSeenMessageId(conversationId);
        let lastSeenBubbleFound = false;
        let insertBeforeBubbleId = null; // Store the ID of the bubble to insert before

        // --- Add Bubbles to Fragment and Find Insertion Point ---
        let firstId = null;
        let lastId = null;

        messages.forEach(msg => {
            // Add bubble returns the ELEMENT, but we add it to the FRAGMENT
            // Pass fragment as container initially
            const addedBubbleElement = addBubble(msg, fragment, conversationId, false, true);

            if (addedBubbleElement) {
                // Check if this bubble corresponds to the last seen ID
                if (lastSeenId && msg.id === lastSeenId) {
                    lastSeenBubbleFound = true;
                }
                // If we just found the last seen bubble, the *current* bubble is the one to insert before
                else if (lastSeenBubbleFound && !insertBeforeBubbleId && msg.id) {
                    insertBeforeBubbleId = msg.id; // Store the message ID
                }

                // Track oldest/latest IDs
                if (msg.id) {
                    const msgIdNum = parseInt(msg.id);
                    if (!firstId || (msgIdNum < parseInt(firstId))) { firstId = msg.id; }
                    if (!lastId || (msgIdNum > parseInt(lastId))) { lastId = msg.id; }
                }
            }
        }); // End messages.forEach

        // --- Insert Separator Logic (within the fragment) ---
        if (lastSeenId) {
            separatorElement = createUnreadSeparatorElement(conversationId); // Create it regardless
            let insertedInFragment = false;

            // Find the actual bubble element in the fragment using the stored ID
            const bubbleToInsertBefore = insertBeforeBubbleId ? fragment.querySelector(`.chat-bubble[data-message-id="${insertBeforeBubbleId}"]`) : null;

            if (bubbleToInsertBefore) {
                fragment.insertBefore(separatorElement, bubbleToInsertBefore);
                insertedInFragment = true;
            } else if (lastSeenBubbleFound) {
                // If last seen was the last message, append separator to fragment
                fragment.appendChild(separatorElement);
                insertedInFragment = true;
            } else {
                 // Last seen ID exists, but not found in this batch (all are new)
                 // Insert at top (after potential load-more link)
                 const loadMoreNode = fragment.querySelector('.load-more-container');
                 if (loadMoreNode) {
                     // Insert after load-more link in fragment
                     fragment.insertBefore(separatorElement, loadMoreNode.nextSibling);
                 } else {
                     // Insert as first child in fragment
                     fragment.insertBefore(separatorElement, fragment.firstChild);
                 }
                insertedInFragment = true;
            }
            separatorInserted = insertedInFragment; // Update the flag based on actual insertion into fragment
        }

        // --- Append the entire fragment to the live DOM ---
        container.appendChild(fragment);

        // --- Scroll Logic (Runs after DOM is updated) ---
        setTimeout(() => {
            // Re-find the separator element in the *live* DOM now IF it was inserted
            const liveSeparatorElement = separatorInserted ? container.querySelector(`#${UNREAD_SEPARATOR_ID_PREFIX}${conversationId}`) : null;

            if (container && container.isConnected) {
                if (liveSeparatorElement && container.contains(liveSeparatorElement)) {
                    // Scroll to separator
                    const containerRect = container.getBoundingClientRect();
                    const separatorRect = liveSeparatorElement.getBoundingClientRect();
                    const desiredTopOffset = container.clientHeight * 0.4;
                    const scrollAmount = container.scrollTop + (separatorRect.top - containerRect.top) - desiredTopOffset;
                    container.scrollTo({ top: scrollAmount, behavior: 'auto' });
                    // Attach listener to the container (it will find the separator)
                    attachSeparatorRemovalListener(container, conversationId, lastId);
                } else {
                    // No separator visible/inserted, scroll to bottom
                    container.scrollTop = container.scrollHeight;
                }
            }
        }, 100);

        // Return the actual oldest/latest IDs found in the initial message data and separator status
        return { latestId: lastId, oldestId: firstId, separatorInserted: separatorInserted };
    }
    // =======================================================================
    // =================== END OF MODIFIED buildInitialChatUI ================
    // =======================================================================


    // =======================================================================
    // ================== START OF NEW Separator Removal Logic ===============
    // =======================================================================
    function removeUnreadSeparator(container, conversationId, latestMessageIdInView) {
        const separatorId = `${UNREAD_SEPARATOR_ID_PREFIX}${conversationId}`;
        const separator = container?.querySelector(`#${separatorId}`);
        if (separator) {
            // Add fade-out class and wait for animation
            separator.classList.add('fade-out');

            // Remove after animation completes
            setTimeout(() => {
                if (separator.isConnected) {
                    separator.remove();
                }
            }, 1500); // Match the CSS transition duration

            // Update the last seen ID immediately (don't wait for animation)
            if (latestMessageIdInView) {
                lastSeenMessageIds[conversationId] = String(latestMessageIdInView);
            }

            // Mark in conversation data that it's gone
            const cData = ACTIVE_CONVERSATIONS[conversationId];
            if (cData) {
                cData.unreadSeparatorVisible = false;
            }
            return true;
        }
        return false;
    }

    function attachSeparatorRemovalListener(container, conversationId, initialLatestId) {
        if (!container || !conversationId) return;

        let currentLatestId = initialLatestId; // Keep track of the latest ID

        // Listener function
        const interactionListener = (event) => {
            // Find the current *actual* latest message ID when interaction happens
            const bubbles = container.querySelectorAll('.chat-bubble[data-message-id]');
             if (bubbles.length > 0) {
                 currentLatestId = bubbles[bubbles.length - 1].dataset.messageId;
             }

            // Attempt to remove the separator
            const removed = removeUnreadSeparator(container, conversationId, currentLatestId);

            if (removed) {
                // Remove the listeners if the separator was successfully removed
                container.removeEventListener('scroll', interactionListener);
                container.removeEventListener('mousedown', interactionListener);
                container.removeEventListener('touchstart', interactionListener); // Add touch
                container.removeEventListener('focusin', interactionListener);
            } else {
                 // If separator somehow wasn't found, maybe still remove listeners?
                 container.removeEventListener('scroll', interactionListener);
                 container.removeEventListener('mousedown', interactionListener);
                 container.removeEventListener('touchstart', interactionListener);
                 container.removeEventListener('focusin', interactionListener);
            }
        };

        // Attach listeners
        container.addEventListener('scroll', interactionListener, { passive: true }); // Use passive for scroll
        container.addEventListener('mousedown', interactionListener);
        container.addEventListener('touchstart', interactionListener, { passive: true }); // Add touch
        container.addEventListener('focusin', interactionListener); // Catches focus shifts e.g., clicking textarea

         // Store in conversation data that separator is visible (optional)
         const cData = ACTIVE_CONVERSATIONS[conversationId];
         if (cData) {
             cData.unreadSeparatorVisible = true;
         }
    }
    // =================== END OF NEW Separator Removal Logic ================


    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);
             } else if (currentOldestId) {
                  linkElement.textContent = 'Erreur index'; linkElement.style.color = '#aaa'; linkElement.onclick = (e) => e.preventDefault();
                  cData.isLoadingOlder = false; // Unlock
                  return;
             } else {
                 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);

                 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
                     // addBubble now returns the element, we need the ID from msgData
                     addBubble(msg, container, conversationId, true, false); // prepend=true, isInitialLoad=false;
                     // Update the overall oldest message ID if the prepended one is older
                     if (msg.id && (!newOldestId || (parseInt(msg.id) < parseInt(newOldestId)))) {
                         newOldestId = msg.id;
                     }
                 });

                 // 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) {
                 cData.allMessagesLoaded = true;
                 if (linkElement.parentElement) linkElement.parentElement.remove();
             }

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


    // =======================================================================
    // ================== START OF MODIFIED closeChatWindow ==================
    // =======================================================================
    let closeChatWindow = (conversationId, options = { removeOriginal: true }) => {
        const cData = ACTIVE_CONVERSATIONS[conversationId]; // Get data first

        // --- SAVE LAST SEEN ID --- // <<< NEW SECTION >>>
        if (cData?.customWindow && document.body.contains(cData.customWindow)) {
            const contentArea = cData.customWindow.querySelector('.custom-chat-content');
            if (contentArea) {
                const bubbles = contentArea.querySelectorAll('.chat-bubble[data-message-id]');
                if (bubbles.length > 0) {
                    const lastBubble = bubbles[bubbles.length - 1];
                    const lastVisibleId = lastBubble.dataset.messageId;
                    saveLastSeenMessageId(conversationId, lastVisibleId);
                } else {
                }
            }
        }
        // --- END SAVE LAST SEEN ID ---

        // Ensure timer clearing happens *before* data deletion (existing code)
        if (cData?.muteTimerIntervalId) {
            clearInterval(cData.muteTimerIntervalId);
        }

        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) (existing code)
            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) { }
            }
            delete ACTIVE_CONVERSATIONS[conversationId]; // Delete data *after* using it (existing code)
        }
    };


    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;
    }
    async function createCustomWindow(conversationId, otherParticipantName_UNUSED, initialResult_IGNORED, originalWindowRef) {

        const windowId = `custom-chat-${conversationId}`;
        // --- Check if DMM window already exists ---
        if (document.getElementById(windowId)) {
            const existingData = ACTIVE_CONVERSATIONS[conversationId];
            if (existingData) {
                existingData.originalWindow = originalWindowRef; // Update ref
            }
            // Hide the newly added original window since DMM window exists
            if (originalWindowRef?.parentNode) {
                originalWindowRef.classList.add('hidden-original-databox');
                originalWindowRef.dataset.modernized = 'replaced';
            }
            // Bring existing DMM window to front and focus
            const existingWindow = document.getElementById(windowId);
            if (existingWindow) {
                bringWindowToFront(existingWindow);
                if (existingWindow.classList.contains('collapsed')) {
                    existingWindow.classList.remove('collapsed'); // Uncollapse if needed
                }
                const txtArea = existingWindow.querySelector('.custom-chat-reply textarea');
                if (txtArea) setTimeout(() => txtArea.focus(), 50); // Focus after slight delay
            }
            return; // Don't create a duplicate window
        }

        // --- Step 1: Create Window Structure IMMEDIATELY ---
        const chatWindow = document.createElement('div');
        chatWindow.id = windowId;
        chatWindow.classList.add('custom-chat-window');
        // Add temporary loading state class
        chatWindow.classList.add('dmm-loading-initial'); // New class for styling loading state
        chatWindow.style.zIndex = '999999'; // Ensure high z-index
        applyCurrentTheme(chatWindow, conversationId); // Apply theme early

        // Add click handler to bring window to front
        chatWindow.addEventListener('mousedown', function(e) {
            // Prevent drag if clicking interactive elements in header
            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.closest('.header-panel') && // Added check
                !e.target.classList.contains('resize-handle')) {
                bringWindowToFront(chatWindow);
            }
        });

        // --- Create Header ---
        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 {
            // Fallback title if original couldn't be read quickly
            actualTitleText = `Messages ${conversationId}`;
        }
        const title = document.createElement('span'); title.classList.add('title'); title.textContent = actualTitleText; title.title = actualTitleText;
        const muteStatusDisplay = document.createElement('span'); muteStatusDisplay.classList.add('mute-status-display');
        const controls = document.createElement('div'); controls.classList.add('controls');
        // Create control buttons (theme, participants, more, close)
        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);
        // Append header elements
        head.appendChild(title);
        title.insertAdjacentElement('afterend', muteStatusDisplay); // Insert mute display after title
        head.appendChild(controls);
        chatWindow.appendChild(head);

        // --- Create Content Area (with loader) ---
        const content = document.createElement('div');
        content.classList.add('custom-chat-content');
        content.innerHTML = '<div class="dmm-initial-loader">Chargement des messages...</div>'; // Loader
        chatWindow.appendChild(content);

        // Add Invite Container between content and reply area
        const inviteContainer = document.createElement('div');
        inviteContainer.id = `dmm-invite-container-${conversationId}`;
        inviteContainer.classList.add('dmm-invite-container');
        inviteContainer.innerHTML = `
            <input type="text" id="dmm-invite-input-${conversationId}" name="dmm_invite_input" placeholder="Inviter (nom1, nom2, ...)">
        `;
        chatWindow.appendChild(inviteContainer);

        // --- Create Reply Area ---
        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);

        // --- Create Hidden Menus/Panels ---
        // More Options Menu
        const moreOptionsMenu = document.createElement('div'); moreOptionsMenu.classList.add('more-opts-menu');
         const inviteItem = document.createElement('div'); inviteItem.classList.add('menu-item'); inviteItem.textContent = 'Inviter / Cacher Invite'; inviteItem.dataset.action = 'toggle_invite_input'; 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 headerBtnMenu = document.createElement('div'); headerBtnMenu.classList.add('menu-item'); headerBtnMenu.textContent = 'Entête'; headerBtnMenu.dataset.action = 'header'; moreOptionsMenu.appendChild(headerBtnMenu); // Button inside menu
         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'; 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');
         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 }
         ];
         muteChoices.forEach(choice => {
           const item = document.createElement('div'); item.classList.add('menu-item', 'mute-option-item');
           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; item.appendChild(textSpan);
           item.dataset.duration = choice.duration === null ? 'null' : String(choice.duration);
           item.dataset.originalLabel = choice.label; item.title = `${choice.label}`;
           muteOptionsContainer.appendChild(item);
         });
         moreOptionsMenu.appendChild(muteOptionsContainer);
        chatWindow.appendChild(moreOptionsMenu);

        // Modify menu click handler
        moreOptionsMenu.addEventListener('click', (event) => {
            // Handle menu item clicks: standard actions, mute options, settings clicks
            const menuItem = event.target.closest('.menu-item:not(.convo-settings-item):not(.mute-option-item)');
            const settingsItem = event.target.closest('.convo-settings-item');
            const muteOptionItem = event.target.closest('.mute-option-item');

            if (menuItem) { // Standard actions (invite, delete, mark unread, header)
                const action = menuItem.dataset.action;

                // --- Handle 'header' action ---
                if (action === 'header') {
                    event.stopPropagation(); // Prevent menu from closing itself immediately
                    const isDisplayed = headerPanel.style.display === 'block';
                    closeOtherPopups(isDisplayed ? 'none' : 'header');
                    headerPanel.style.display = isDisplayed ? 'none' : 'block';
                    if (!isDisplayed) {
                        updateHeaderHistory(); // Populate history when opened
                        setTimeout(() => {
                            if (!clickOutsideHeaderHandler) {
                                clickOutsideHeaderHandler = (e) => {
                                    if (!headerPanel.contains(e.target) && !menuItem.contains(e.target)) {
                                        closeOtherPopups('none');
                                    }
                                };
                                document.addEventListener('click', clickOutsideHeaderHandler, true);
                            }
                        }, 0);
                    }
                    return;
                }

                // Handle other actions
                if (action === 'toggle_invite_input') {
                    const invContainer = document.getElementById(`dmm-invite-container-${conversationId}`);
                    const invInput = document.getElementById(`dmm-invite-input-${conversationId}`);
                    if (invContainer && invInput) {
                        const isVisible = invContainer.style.display === 'block';
                        invContainer.style.display = isVisible ? 'none' : 'block';
                        menuItem.classList.toggle('invite-active', !isVisible);
                        if (!isVisible) {
                            setTimeout(() => invInput.focus(), 50);
                        }
                    }
                } else {
                    closeOtherPopups('none');
                }

                try {
                    const cData = ACTIVE_CONVERSATIONS[conversationId];
                    const currentOriginalWindow = cData?.originalWindow;
                    const messagerie = unsafeWindow?.nav?.getMessagerie();

                    if (action === 'mark_unread') {
                        if(messagerie) {
                            messagerie.notReadMessage(conversationId);
                            setTimeout(() => closeThisChatWindow({ removeOriginal: true }), 100);
                        } else {
                            alert("Erreur: Messagerie non trouvée.");
                        }
                    } else if (action === 'delete') {
                        if(messagerie) {
                            messagerie.deleteMessage(conversationId);
                            setTimeout(() => closeThisChatWindow({ removeOriginal: true }), 100);
                        } else {
                            alert("Erreur: Messagerie non trouvée.");
                        }
                    }
                } catch (e) {
                    alert(`Une erreur est survenue lors de l'action ${action}`);
                }

            } else if (muteOptionItem) {
                // ...existing mute option handling code...
            } else if (settingsItem) {
                // ...existing settings handling code...
            }
        });

        // Header Panel
        const headerPanel = document.createElement('div'); headerPanel.classList.add('header-panel');
        headerPanel.innerHTML = `
            <div class="header-input-container">
                <input type="text" maxlength="${MAX_HEADER_LENGTH}" placeholder="Entrez un entête...">
                <span class="checkmark-btn" title="Ajouter l'entête">✓</span>
            </div>
            <div class="header-history-container">
                <div class="header-history-list"></div>
            </div>`;
        chatWindow.appendChild(headerPanel);

        // Participants Panel
        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);

        // Color Picker Panel
        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(); colorPickerPanel.appendChild(colorInput);
         const resetColorBtn = document.createElement('span'); resetColorBtn.textContent = '❌'; resetColorBtn.title = 'Rétablir la couleur par défaut'; resetColorBtn.classList.add('reset-color-btn'); colorPickerPanel.appendChild(resetColorBtn);
        chatWindow.appendChild(colorPickerPanel);

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

        // --- Add Skeleton Window to DOM ---
        document.body.appendChild(chatWindow);
        makeDraggable(chatWindow);
        makeResizable(chatWindow, resizeHandle);
        bringWindowToFront(chatWindow); // Bring new window to front

        // --- Define close function and other handlers early ---
        let clickOutsideMenuHandler = null, clickOutsidePanelHandler = null, clickOutsideColorPickerHandler = null, clickOutsideHeaderHandler = 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 !== 'header' && headerPanel.style.display === 'block') {
               headerPanel.style.display = 'none';
               if (clickOutsideHeaderHandler) document.removeEventListener('click', clickOutsideHeaderHandler, true);
               clickOutsideHeaderHandler = 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;
           }
        };

        const closeThisChatWindow = (options = { removeOriginal: true }) => {
            chatWindow.removeEventListener('mousedown', clearNotification, true);
            chatWindow.removeEventListener('focusin', clearNotification);
            // Original close function handles interval clearing, data deletion, last seen ID saving
            closeChatWindow(conversationId, options);
            // Cleanup click handlers
            if (clickOutsideMenuHandler) document.removeEventListener('click', clickOutsideMenuHandler, true);
            if (clickOutsidePanelHandler) document.removeEventListener('click', clickOutsidePanelHandler, true);
            if (clickOutsideColorPickerHandler) document.removeEventListener('click', clickOutsideColorPickerHandler, true);
            if (clickOutsideHeaderHandler) document.removeEventListener('click', clickOutsideHeaderHandler, true);
            clickOutsideMenuHandler = clickOutsidePanelHandler = clickOutsideColorPickerHandler = clickOutsideHeaderHandler = null;
        };

        // --- Attach Event Listeners that work on the Skeleton ---
        const clearNotification = () => {
            const convData = ACTIVE_CONVERSATIONS[conversationId];
            if (convData && convData.hasUnreadNotification) {
                convData.hasUnreadNotification = false; // Clear flag regardless of mute
                if (!isConversationMuted(conversationId)) {
                    // Only remove visual style if not muted
                    const currentWindow = ACTIVE_CONVERSATIONS[conversationId]?.customWindow;
                    if (currentWindow) currentWindow.classList.remove('has-unread-notification');
                }
            }
        };
        chatWindow.addEventListener('mousedown', clearNotification, true);
        chatWindow.addEventListener('focusin', clearNotification);

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

        sendButton.onclick = () => {
            const selectedHeader = getSelectedHeader();
            const messageText = selectedHeader ?
                `| ${selectedHeader} |\n\n${textarea.value.trim()}` : // Wrap header
                textarea.value.trim();
            const cData = ACTIVE_CONVERSATIONS[conversationId];
            if (!messageText || sendButton.disabled) { if (!messageText) textarea.focus(); return; }
            if (!cData) { alert("Erreur critique DMM: Données de conversation manquantes."); return; }

            const currentOriginalWindow = cData.originalWindow;
            // Robust check for original window and elements
            if (!currentOriginalWindow || !document.body.contains(currentOriginalWindow)) {
                alert("Erreur DMM: Référence à la fenêtre originale perdue. La page pourrait nécessiter un rafraîchissement.");
                sendButton.disabled = true; sendButton.textContent = 'Erreur Orig.'; return;
            }
            const originalTextarea = currentOriginalWindow.querySelector('.zone_reponse textarea[name=nm_texte]');
            const originalSendButton = currentOriginalWindow.querySelector('.zone_reponse .btnTxt[onclick*="sendMessage"]');
            if (!originalTextarea || !originalSendButton) {
                alert("Erreur DMM: Éléments d'envoi originaux non trouvés. La page pourrait nécessiter un rafraîchissement.");
                sendButton.disabled = true; sendButton.textContent = 'Erreur Config'; return;
            }

            sendButton.disabled = true;
            const originalButtonText = sendButton.textContent; // Store original text
            sendButton.textContent = 'Envoi...';
            let sendAttemptError = null;
            let originalClickSuccess = false;

            try {
                originalTextarea.value = messageText;
                originalSendButton.click();
                originalClickSuccess = true;
                textarea.value = ''; // Clear custom textarea on success
            } catch (e) {
                sendAttemptError = e;
                sendButton.textContent = 'Erreur Envoi';
            }

            // Re-enable button after a delay, regardless of fetch outcome (fetch is for update, not send confirmation)
            setTimeout(() => {
                // Check if button still exists
                const currentSendButton = document.querySelector(`#${windowId} .custom-chat-reply button`);
                if (currentSendButton) {
                    currentSendButton.disabled = false;
                    currentSendButton.textContent = originalButtonText; // Restore original text
                }
                 if (sendAttemptError && !originalClickSuccess) { // Show alert only if click itself failed
                     alert(`Erreur DMM: Échec de l'envoi initial du message.\n${sendAttemptError.message}`);
                 }
            }, 1500); // Re-enable after 1.5 seconds

            // Fetch update immediately IF original click succeeded
            if (originalClickSuccess) {
                GM_xmlhttpRequest({
                    method: "GET", url: `https://www.dreadcast.net/Menu/Messaging/action=OpenMessage&id_conversation=${conversationId}`, timeout: 10000,
                    onload: function(response) {
                        // Check if our window still exists
                        const latestCData = ACTIVE_CONVERSATIONS[conversationId];
                        if (latestCData && latestCData.customWindow && document.body.contains(latestCData.customWindow)) {
                            if (response.status === 200 && response.responseText) {
                                try {
                                    handleOpenMessageResponse(conversationId, response.responseText);
                                } catch (handlerError) { }
                            } else { }
                        }
                    },
                    onerror: function(error) { },
                    ontimeout: function() { }
                });
            }
        }; // End sendButton.onclick

        textarea.addEventListener('keypress', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendButton.click(); } });
        head.addEventListener('dblclick', (e) => {
            // Prevent collapse if clicking interactive elements in header
            if (e.target.closest('.controls span') || e.target.closest('.mute-status-display')) return;
            const isCollapsed = chatWindow.classList.toggle('collapsed');
            if (isCollapsed) { closeOtherPopups('none'); }
        });

        // Theme Picker Listeners
        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(); // Ensure current global color shown
            colorPickerPanel.style.display = isDisplayed ? 'none' : 'block';
            if (!isDisplayed) { setTimeout(() => { document.addEventListener('click', clickOutsideColorPickerHandler, true); }, 0); }
        });
        colorInput.addEventListener('input', (event) => { saveGlobalThemeColor(event.target.value); });
        resetColorBtn.addEventListener('click', () => { colorInput.value = DEFAULT_THEME_COLOR; saveGlobalThemeColor(DEFAULT_THEME_COLOR); });

        // More Options Menu Listeners
        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
            }
            moreOptionsMenu.style.display = isDisplayed ? 'none' : 'block';
            if (!isDisplayed) { setTimeout(() => { document.addEventListener('click', clickOutsideMenuHandler, true); }, 0); }
        });
        moreOptionsMenu.addEventListener('click', (event) => {
           // Handle menu item clicks: standard actions, mute options, settings clicks
            const menuItem = event.target.closest('.menu-item:not(.convo-settings-item):not(.mute-option-item)');
            const settingsItem = event.target.closest('.convo-settings-item');
            const muteOptionItem = event.target.closest('.mute-option-item');

            if (menuItem) { // Standard actions (invite, delete, mark unread, header)
                const action = menuItem.dataset.action;
                if (action !== 'header') { // Don't close menu for header action
                     closeOtherPopups('none');
                }
                try {
                    const cData = ACTIVE_CONVERSATIONS[conversationId];
                    const currentOriginalWindow = cData?.originalWindow;
                    const messagerie = unsafeWindow?.nav?.getMessagerie();

                    if (action === 'invite') { }
                    else if (action === 'mark_unread') { if(messagerie) { messagerie.notReadMessage(conversationId); setTimeout(() => closeThisChatWindow({ removeOriginal: true }), 100); } else { alert("Erreur: Messagerie non trouvée."); }}
                    else if (action === 'delete') { if(messagerie) { messagerie.deleteMessage(conversationId); setTimeout(() => closeThisChatWindow({ removeOriginal: true }), 100); } else { alert("Erreur: Messagerie non trouvée."); } }
                    // Header action is handled by its own listener below
                } catch (e) { alert(`Une erreur est survenue lors de l'action ${action}`); }

            } else if (muteOptionItem) { // 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) {
                    const currentEndTime = getConversationMuteEndTime(conversationId);
                    // Prevent clicking "Unmute" if already unmuted
                    if (!(durationMs === MUTE_DURATIONS.UNMUTE && currentEndTime === 0)) {
                       setConversationMuted(conversationId, durationMs);
                       // No need to close menu here, updateMuteOptionsUI will refresh it
                    }
                } else { }

            } else if (settingsItem) { // Color Settings Click (on wrapper)
                // Trigger click on checkbox if label is clicked
                if (event.target.tagName === 'LABEL') {
                    const inputId = event.target.htmlFor;
                    const inputElement = document.getElementById(inputId);
                    if (inputElement && inputElement.type === 'checkbox') inputElement.click();
                }
                // Allow event to propagate for direct clicks on checkbox/color input
            }
        });
        // Specific listeners for color settings inputs
        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); } });

        // Participants Panel Listeners
        clickOutsidePanelHandler = (event) => { if (!participantsPanel.contains(event.target) && !participantsBtn.contains(event.target)) { closeOtherPopups('none'); } };
        participantsBtn.addEventListener('click', (event) => {
            event.stopPropagation();
            const cData = ACTIVE_CONVERSATIONS[conversationId]; // Get current data
            if (!cData) return;
            const isActive = participantsPanel.classList.contains('active');
            closeOtherPopups(isActive ? 'none' : 'panel');
            // Populate ONLY when opening
            if (!isActive) {
               const listHtml = cData.participants && 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); }
        });
        closePanelBtn.addEventListener('click', () => { closeOtherPopups('none'); });

        // Header Panel Listeners (using headerBtnMenu from the options menu)
         clickOutsideHeaderHandler = (event) => { if (!headerPanel.contains(event.target) && !headerBtnMenu.contains(event.target)) { closeOtherPopups('none'); } };
         headerBtnMenu.addEventListener('click', (event) => { // Attach to the menu item
             event.stopPropagation(); // Stop menu from closing itself
             const isDisplayed = headerPanel.style.display === 'block';
             closeOtherPopups(isDisplayed ? 'none' : 'header'); // Close others except header panel
             headerPanel.style.display = isDisplayed ? 'none' : 'block';
             if (!isDisplayed) {
                 updateHeaderHistory(); // Populate history when opened
                 setTimeout(() => { document.addEventListener('click', clickOutsideHeaderHandler, true); }, 0); // Add close listener
             }
         });
        // Listener for header input submit button
        headerPanel.querySelector('.checkmark-btn').addEventListener('click', () => {
            const headerInput = headerPanel.querySelector('input');
            const header = headerInput.value.trim();
            if (header) {
                addHeaderToHistory(header);
                setSelectedHeader(header); // Auto-select the newly added header
                headerInput.value = '';
                updateHeaderHistory(); // Refresh display
            }
        });
        // Listener for clicking history items
        headerPanel.querySelector('.header-history-list').addEventListener('click', (e) => {
            const item = e.target.closest('.header-history-item');
            if (item) {
                const header = item.dataset.header;
                const currentSelected = getSelectedHeader();
                setSelectedHeader(currentSelected === header ? '' : header); // Toggle selection
                updateHeaderHistory(); // Update visuals
            }
        });
        // --- End Header Panel listeners ---

        // --- Step 2: Prepare Conversation Data Object (minimal initial state) ---
        ACTIVE_CONVERSATIONS[conversationId] = {
            customWindow: chatWindow,
            originalWindow: originalWindowRef,
            latestMessageId: null, // Set after fetch
            oldestMessageId: null, // Set after fetch
            allMessagesLoaded: false, // Assume not loaded
            isLoadingOlder: false,
            participants: [], // Set after fetch
            hasUnreadNotification: false,
            unreadSeparatorVisible: false, // Set after build
            muteTimerIntervalId: null // Set after fetch
        };

        // --- Step 3: Asynchronously Fetch and Build Content ---
        (async () => {
            let fetchedMessages = [];
            let fetchedParticipants = [];
            let initialLatestId = null;
            let initialOldestId = null;
            let allMessagesLoaded = true;
            let fetchError = null;

            try {
                // Ensure My Name is available
                if (!MY_NAME) MY_NAME = getMyCharacterName();
                if (!MY_NAME) throw new Error("Character name unavailable for fetching messages.");

                // Fetch initial data using the existing function
                const initialResult = await parseAndFetchInitialMessages(originalWindowRef, conversationId);
                if (!initialResult || typeof initialResult !== 'object') {
                    throw new Error(`parseAndFetchInitialMessages returned invalid result for ${conversationId}`);
                }
                fetchedMessages = initialResult.messages;
                fetchedParticipants = initialResult.participants;
                initialLatestId = initialResult.latestId;
                initialOldestId = initialResult.oldestId;
                allMessagesLoaded = initialResult.allLoaded;

            } catch (error) {
                fetchError = error;
            }

            // --- Re-check if window and data still exist before updating DOM ---
            const currentConvData = ACTIVE_CONVERSATIONS[conversationId];
            const currentChatWindow = document.getElementById(windowId); // Re-fetch window by ID

            if (!currentConvData || !currentChatWindow || !document.body.contains(currentChatWindow)) {
                // Ensure original window is handled if left in a hidden state
                if (originalWindowRef && originalWindowRef.dataset.modernized !== 'replaced' && originalWindowRef.dataset.modernized !== 'revealed_for_invite' && originalWindowRef.parentNode) {
                    try {
                        originalWindowRef.remove();
                    } catch(e){ }
                }
                return; // Stop processing if window was closed
            }

            // --- Update Conversation Data with Fetched Info ---
            currentConvData.participants = fetchedParticipants;
            currentConvData.latestMessageId = initialLatestId;
            currentConvData.oldestMessageId = initialOldestId;
            currentConvData.allMessagesLoaded = allMessagesLoaded;

            // --- Populate Content Area or Show Error ---
            let separatorInserted = false;
            if (content) { // Ensure content area still exists
                if (!fetchError) {
                    content.innerHTML = ''; // Clear loader FIRST
                    try {
                       const buildResult = buildInitialChatUI(fetchedMessages, content, conversationId, allMessagesLoaded);
                       // Update data with potentially more accurate IDs from buildResult
                       currentConvData.latestMessageId = buildResult.latestId ?? initialLatestId;
                       currentConvData.oldestMessageId = buildResult.oldestId ?? initialOldestId;
                       currentConvData.unreadSeparatorVisible = buildResult.separatorInserted;
                       separatorInserted = buildResult.separatorInserted; // Use for logging
                    } catch (buildError) {
                        content.innerHTML = `<div class="dmm-error">Erreur lors de l'affichage des messages.</div>`;
                        fetchError = buildError; // Mark as error state
                    }
                } else {
                    // Show fetch error in content area
                    content.innerHTML = `<div class="dmm-error">Erreur: ${fetchError.message || 'Impossible de charger les messages.'}</div>`;
                }
            } else {
                 fetchError = fetchError || new Error("Content area missing"); // Ensure error state if content missing
            }


            // --- Remove loading class from window ---
            currentChatWindow.classList.remove('dmm-loading-initial');

            // --- Setup Mute Timer Interval (now that IDs are potentially known) ---
            const muteTimerIntervalId = setInterval(() => {
               const convDataCheck = ACTIVE_CONVERSATIONS[conversationId];
               const windowCheck = document.getElementById(windowId); // Check by ID each time
               if (convDataCheck && windowCheck && document.body.contains(windowCheck)) {
                    updateHeaderMuteStatus(windowCheck, conversationId);
                    isConversationMuted(conversationId); // Checks expiry, updates sidebar if needed
                    const currentMenu = windowCheck.querySelector('.more-opts-menu');
                    if (currentMenu && currentMenu.style.display === 'block') {
                        updateMuteOptionsUI(windowCheck, conversationId);
                    }
               } else {
                   // Window gone, clear interval
                    const intervalIdToClear = ACTIVE_CONVERSATIONS[conversationId]?.muteTimerIntervalId;
                    if (intervalIdToClear) {
                        clearInterval(intervalIdToClear);
                        // Check existence before deleting property
                        if (ACTIVE_CONVERSATIONS[conversationId]) {
                            delete ACTIVE_CONVERSATIONS[conversationId].muteTimerIntervalId;
                        }
                    }
               }
            }, 20000); // 20 seconds
            currentConvData.muteTimerIntervalId = muteTimerIntervalId; // Store interval ID

            // --- Final UI Updates ---
            updateHeaderMuteStatus(currentChatWindow, conversationId);
            // Initial Mute options UI update (checkmarks etc.) only if menu exists
            const menuForUpdate = currentChatWindow.querySelector('.more-opts-menu');
            if(menuForUpdate) {
                updateMuteOptionsUI(currentChatWindow, conversationId);
            }

            // Focus textarea after content is loaded (or error shown)
            if(textarea && document.body.contains(textarea)) {
               setTimeout(() => textarea.focus(), 50);
            }

            // Mark original window as replaced *only* if everything succeeded
            if (!fetchError && originalWindowRef && originalWindowRef.parentNode) {
                originalWindowRef.dataset.modernized = 'replaced';
            } else if (fetchError && originalWindowRef) {
                 // Mark original window with error state if fetch/build failed
                 originalWindowRef.dataset.modernized = 'error_post_fetch';
            }

        })(); // --- End of async function execution ---

    } // --- END of createCustomWindow function ---
     // =======================================================================
     // =================== END OF MODIFIED createCustomWindow ================
     // =======================================================================


    // --- Click Simulation Functions ---
    function simulateClick(element) {
        return new Promise(resolve => {
            if (!element || !document.body.contains(element)) {
                resolve(false); return;
            }
            try { element.click(); resolve(true); }
            catch (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)) { resolve(false); return; }
            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)) { resolve(false); return; }
            element.click(); // Second click
            return true;
        } catch (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}]:`;
        let menuWasOpenedByScriptOnSuccessfulAttempt = false;
        let overallSuccess = false;

        for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
            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) {
                        return;
                    }
                    currentAttemptMenuOpened = true;
                    await new Promise(r => setTimeout(r, UI_CLICK_DELAY));
                    const click1Success = await simulateClick(mainMenuButton);
                    if (!click1Success) {
                        await new Promise(r => setTimeout(r, RETRY_DELAY));
                        continue;
                    }
                    await new Promise(r => setTimeout(r, UI_WAIT_DELAY));
                    try {
                        await waitForElement('#liste_messages');
                    } catch (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) {
                     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)) {
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue;
                }

                let targetFolderLi = null;
                try {
                    targetFolderLi = await waitForElement(targetFolderLiSelector, WAIT_FOR_ELEMENT_TIMEOUT, folderListUL);
                } catch (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) {
                    await new Promise(r => setTimeout(r, UI_CLICK_DELAY));
                    const click3Success = await simulateClick(targetFolderLi);
                    if(!click3Success) {
                         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
                }

                // --- 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)) {
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue;
                }
                const targetMessageSelector = `li#message_${conversationId}`;

                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) {
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue;
                }

                // Use the messageListContentUL as the container for the double-click simulation
                const success = await initiateDoubleClick(targetMessageSelector, messageListContentUL);

                if (success) {
                    overallSuccess = true;
                    menuWasOpenedByScriptOnSuccessfulAttempt = currentAttemptMenuOpened;
                    break; // Exit the retry loop
                } else {
                    await new Promise(r => setTimeout(r, RETRY_DELAY));
                    continue;
                }

            } catch (error) {
                await new Promise(r => setTimeout(r, RETRY_DELAY));
                continue;
            }
        } // --- END of for loop (attempts) ---

        if (!overallSuccess) {
        }

        // --- Auto-close Menu ---
        if (overallSuccess && menuWasOpenedByScriptOnSuccessfulAttempt) {
            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()) {
                 await simulateClick(finalMenuButton);
            }
        }

    } // --- 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) {
            return;
        }

        // If window doesn't exist and it's *not* a user override case waiting for the window, abort.
        if (!dmmWindowExists && !isUserOverride) {
            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) {
            return;
        }

        const currentLatestKnownId = conversationData?.latestMessageId; // This is message_id
        const currentLatestKnownIdNum = currentLatestKnownId ? parseInt(currentLatestKnownId) : 0;

        try {
            const parser = new DOMParser();
            const doc = parser.parseFromString(responseText, 'text/html');
            const latestConvList = doc.querySelector('.zone_conversation');
            if (!latestConvList) { return; }

            const serverElements = Array.from(latestConvList.querySelectorAll('.link.conversation[id^="convers_"]'));
            if (serverElements.length === 0) { return; }

            // --- Optimization: Find highest server ID first ---
            let highestServerId = null;
            let highestServerIdNum = 0;
            serverElements.forEach(el => {
                const msgId = el.id.replace('convers_', '');
                if (msgId) {
                    try {
                        const idNum = parseInt(msgId);
                        if (!isNaN(idNum) && idNum > highestServerIdNum) {
                            highestServerId = msgId;
                            highestServerIdNum = idNum;
                        }
                    } catch (e) { /* ignore parse errors */ }
                }
            });

            // --- Optimization: Early exit if client is already up-to-date ---
            if (highestServerIdNum > 0 && highestServerIdNum <= currentLatestKnownIdNum) {
                // Ensure the conversation data's latest ID reflects the server truth if needed
                if (conversationData && highestServerId && highestServerId !== conversationData.latestMessageId) {
                     if (!conversationData.latestMessageId || parseInt(highestServerId) > parseInt(conversationData.latestMessageId)) {
                         conversationData.latestMessageId = highestServerId;
                     }
                }
                return; // Nothing new to add
            }
            // --- End Optimizations ---

            let elementsToProcess = [];

            serverElements.forEach(el => {
                const parsed = parseMessageElement(el);
                if (parsed && parsed.id) {
                     try {
                         const elIdNum = parseInt(parsed.id);
                         if (isNaN(elIdNum)) return; // Skip if ID is not a number

                         // Check if newer than client's known latest ID
                         if (elIdNum > currentLatestKnownIdNum) {
                             // *** Optimization: Check for duplicates in live DOM BEFORE deciding to process/fetch ***
                             // This check requires customContentArea to exist, handle the override case where it might be null temporarily
                             const alreadyExists = customContentArea ? customContentArea.querySelector(`.chat-bubble[data-message-id="${parsed.id}"]`) : false;
                             if (!alreadyExists) {
                                 elementsToProcess.push(el); // Store the element
                             }
                         }
                     } catch (e) { /* ignore parse errors */ }
                 }
            });

            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 && parsed.id) { // Ensure parsed and id exist
                            // Wait briefly if it's an override, allowing createCustomWindow to potentially finish first
                            if (isUserOverride) await new Promise(r => setTimeout(r, 75)); // Slightly longer delay?

                            // Re-check DMM window/content area status right before fetching content
                            const finalConvDataCheck = ACTIVE_CONVERSATIONS[conversationId];
                            // Content area check is crucial here
                            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) {
                                resolve(); return;
                            }
                            // Final duplicate check within the confirmed content area using message_id
                            if (finalContentAreaCheck.querySelector(`.chat-bubble[data-message-id="${parsed.id}"]`)) {
                                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}"]`)) {
                                        resolve(); return;
                                }

                                const msgData = { ...parsed, content: content };
                                const addedBubble = addBubble(msgData, finalContentArea, conversationId, false, false); // isInitialLoad=false here;
                                if (addedBubble && addedBubble.dataset.messageId) {
                                    newlyProcessedRealIds.push(addedBubble.dataset.messageId);
                                }
                                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 overallLatestNum = overallLatestId ? parseInt(overallLatestId) : 0;
                    let didUpdateLatest = false;

                    try {
                        newlyProcessedRealIds.forEach(processedId => { // processedId is message_id
                            if (!processedId) return;
                            const processedIdNum = parseInt(processedId);
                            if (!isNaN(processedIdNum) && processedIdNum > overallLatestNum) {
                                overallLatestId = processedId;
                                overallLatestNum = processedIdNum; // Update number for comparison
                                didUpdateLatest = true;
                            }
                        });

                        // Also consider the highest ID seen from the server response itself
                        if (highestServerIdNum > overallLatestNum) {
                             overallLatestId = highestServerId;
                             overallLatestNum = highestServerIdNum;
                             didUpdateLatest = true;
                        }

                        if (didUpdateLatest) {
                            postProcessConvData.latestMessageId = overallLatestId;
                        }
                    } catch (idUpdateError) { }
                }).catch(error => { });
            } else if (highestServerIdNum > 0 && highestServerIdNum > currentLatestKnownIdNum) {
                 // 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
                    convDataForIdUpdate.latestMessageId = highestServerId;
                 }
             }
        } catch (e) {
        }
    } // --- END of handleOpenMessageResponse ---


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

        const messageLi = event.target.closest('li.message[id^="message_"]');
        if (!messageLi) return;

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

        // --- EDIT MODE CHECK (NEW) ---
        if (isEditModeActive) {
            event.preventDefault();
            event.stopPropagation();
            closeEditPopup();
            openEditPopup(conversationId, messageLi);
            return;
        }

        // Mute Handling Logic
        const isMuted = isConversationMuted(conversationId);
        if (isMuted) {
            // 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
                    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 its own window.
            // The mainObserverCallback will handle the override.
            return;
        }
        // End Mute Handling


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

        if (existingDMMData && customWindowElement && existingDMMData.customWindow === customWindowElement && document.body.contains(customWindowElement)) {
            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)) {
            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)
        } else {
            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) {
                                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') {
                                    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();
                                        }
                                    } catch (e) {
                                    }
                                }
                                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) {
                             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
                            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';
                            originalWindow.classList.add('hidden-original-databox'); // Hide it

                            if (!MY_NAME) MY_NAME = getMyCharacterName();
                            if (!MY_NAME) {
                                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
                            } catch (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 ---
    addChatStyles(); // Make sure styles are added

    // =======================================================================
    // ================== START OF MODIFIED initializeScript =================
    // =======================================================================
    async function initializeScript() {
        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)) {
            if (!MY_NAME) MY_NAME = getMyCharacterName();
            if (!MY_NAME) { return; }

            // --- Load Global Mute State ---
            loadGlobalMuteState();

            // --- Load Custom Conversation Data
            loadCustomConversationData();

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

            // --- Load Last Seen IDs --- // <<< NEW LINE >>>
            loadLastSeenMessageIds();

            // --- Start periodic cache cleanup ---
            const cacheCleanupIntervalId = setInterval(cleanupMessageCache, MESSAGE_CACHE_CLEANUP_INTERVAL);
            cleanupMessageCache(); // Initial cleanup
            if (!window.DMM_CACHE_CLEANUP_INTERVAL) {
                window.DMM_CACHE_CLEANUP_INTERVAL = cacheCleanupIntervalId;
            }


// --- Setup XHR Wrapper ---
            if (!unsafeWindow._original_XMLHttpRequest_open && !unsafeWindow._original_XMLHttpRequest_send) {
                const origOpen = unsafeWindow.XMLHttpRequest.prototype.open;
                const origSend = unsafeWindow.XMLHttpRequest.prototype.send;
                let requestCounter = 0;

                unsafeWindow.XMLHttpRequest.prototype.open = function(method, url) {
                    // Store properties directly on the XHR object
                    this._dmm_requestMethod = method; // Use distinct property names
                    this._dmm_requestURL = url;
                    this._dmm_requestId = requestCounter++;
                    return origOpen.apply(this, arguments);
                };

                unsafeWindow.XMLHttpRequest.prototype.send = function(body) {
                    const xhr = this;
                    // Retrieve properties stored during open
                    const reqId = xhr._dmm_requestId;
                    const targetUrl = xhr._dmm_requestURL;
                    const method = xhr._dmm_requestMethod;
                    let payloadToSend = body; // Start with the original body

                    // --- Intercept POST to Menu/Messaging/NewMessage ---
                    // Check if properties were actually set during open
                    if (!method || !targetUrl) {
                    }
                    // *** Corrected URL Check: No leading slash ***
                    else if (method === 'POST' && typeof targetUrl === 'string' && targetUrl.includes('Menu/Messaging/NewMessage')) {

                        if (body && (typeof body === 'string' || body instanceof URLSearchParams)) {
                            try {
                                // Use a defensive copy if it's already URLSearchParams
                                const params = (body instanceof URLSearchParams) ? new URLSearchParams(body.toString()) : new URLSearchParams(body);
                                const conversationId = params.get('nm_idConvers');

                                if (conversationId) {
                                    const dmmInviteInputId = `dmm-invite-input-${conversationId}`;
                                    const dmmInviteInput = document.getElementById(dmmInviteInputId);

                                    // Robust check
                                    let inputFound = false;
                                    if (dmmInviteInput) {
                                        if (document.body.contains(dmmInviteInput)) {
                                            inputFound = true;
                                        } else {
                                            const parentWindow = dmmInviteInput.closest('.custom-chat-window');
                                        }
                                    } else {
                                    }

                                    if (inputFound) {
                                        const inviteValue = dmmInviteInput.value.trim();
                                        const currentCible = params.get('nm_cible') || '';

                                        params.set('nm_cible', inviteValue);
                                        payloadToSend = params.toString();

                                    } else {
                                    }
                                } else {
                                }
                            } catch (e) {
                                payloadToSend = body;
                            }
                        } else {
                        }
                    } else if (method === 'POST') { // Log why it didn't match if it was a POST
                    }
                    // --- End Intercept NewMessage block ---


                    // --- Setup readyState listener ---
                    const dmmReadyStateHandler = function() {
                        const rsUrl = xhr._dmm_requestURL; // Use stored URL
                        if (xhr.readyState === 4) {
                            const currentStatus = xhr.status; let currentResponseText = null;
                            try { currentResponseText = xhr.responseText; } catch(e) { /* Ignore */ }

                            // --- Handle /Check ---
                            if (rsUrl && typeof rsUrl === 'string' && rsUrl.includes('/Check')) {
                                if (currentStatus === 200 && currentResponseText) {
                                    let match = null;
                                    try {
                                        match = currentResponseText.match(/<evenement\s+type="nouveau_message">.*?<folder_(\d+)\s+quantite="\d+"\s+id_conversation="(\d+)"\s*\/?>.*?<\/evenement>/s);
                                    } catch (regexError) { }

                                    if (match && match[1] && match[2]) {
                                        const folderId = match[1]; const conversationId = match[2];
                                        const conversationData = ACTIVE_CONVERSATIONS[conversationId];
                                        const isWindowOpen = conversationData && conversationData.customWindow && document.body.contains(conversationData.customWindow);
                                        if (isWindowOpen) {
                                            try { setTimeout(() => handleNewMessageEvent(conversationId, folderId), 0); } catch (e) { }
                                        } else {
                                            const isSpecificConvoMuted = isConversationMuted(conversationId);
                                            if (isSpecificConvoMuted) {
                                                try { setTimeout(() => handleNewMessageEvent(conversationId, folderId), 0); } catch (e) { }
                                            } else {
                                                if (!isGloballyMuted) { playNotificationSound(true); }
                                            }
                                        }
                                        setTimeout(() => updateSidebarMuteStatus(conversationId), 150);
                                    }
                                 }
                            }
                            // --- 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];
                                        try { setTimeout(() => handleOpenMessageResponse(conversationId, currentResponseText), 0); }
                                        catch (e) { }
                                    }
                                 }
                            }
                            // Remove listener once done
                            try { xhr.removeEventListener('readystatechange', dmmReadyStateHandler); } catch(removeError) { /* ignore */ }
                        } // end if readyState 4
                    };
                    // Attach listener
                    try { xhr.addEventListener('readystatechange', dmmReadyStateHandler); }
                    catch(addListenerError) { }

                    // Store originals safely
                    if(!unsafeWindow._original_XMLHttpRequest_open) unsafeWindow._original_XMLHttpRequest_open = origOpen;
                    if(!unsafeWindow._original_XMLHttpRequest_send) unsafeWindow._original_XMLHttpRequest_send = origSend;

                    // --- Call original send ---
                    return origSend.apply(this, [payloadToSend]); // Use the payloadToSend variable
                }; // --- END of send override ---
            } else {
            }
            // --- End XHR Wrapper Setup ---



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

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

                    // Initial scan
                    scanAndUpdateSidebarMutes();
                } else {
                }
            }

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

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

            // Create tooltip element
            createAvatarTooltip();

            // Create Edit Mode UI
            createEditModeToggleButton();
            createEditPopup();

        } else {
            const missing = essentialElements.map((el, i) => el ? '' : ['body', '#zone_messagerie', '#txt_pseudo', '#liste_messages'][i]).filter(Boolean);
            setTimeout(initializeScript, 500); // Retry
        }
    } // --- END of initializeScript ---
    // =======================================================================
    // =================== END OF MODIFIED initializeScript ==================
    // =======================================================================


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


    // --- Cleanup on page unload ---
     window.addEventListener('beforeunload', () => {
         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 }); } // closeChatWindow now saves last seen ID
             catch(e) { }
         });
         // Explicitly clear the object just in case closeChatWindow had issues
         for (let key in ACTIVE_CONVERSATIONS) { delete ACTIVE_CONVERSATIONS[key]; }

         // Clear cache cleanup interval
         if (window.DMM_CACHE_CLEANUP_INTERVAL) {
             clearInterval(window.DMM_CACHE_CLEANUP_INTERVAL);
             delete window.DMM_CACHE_CLEANUP_INTERVAL;
         }

         // Clear message cache
         messageCache.clear();

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

         // Remove global mute button listener if needed
         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) { }

     });

    // Add new functions near other UI/Click handling functions
    function createEditModeToggleButton() {
        if (document.getElementById(EDIT_MODE_TOGGLE_BUTTON_ID)) return;

        // Create the button
        const button = document.createElement('div');
        button.id = EDIT_MODE_TOGGLE_BUTTON_ID;
        button.textContent = '✏️';
        button.title = 'Activer/Désactiver le mode édition des conversations';
        button.addEventListener('click', toggleEditMode);

        // Position the button relative to grid-title
        const gridTitle = document.querySelector('.grid.grid-title');
        if (gridTitle) {
            // Wait for layout to be complete
            setTimeout(() => {
                const rect = gridTitle.getBoundingClientRect();
                button.style.top = `${rect.top + window.scrollY}px`;
                button.style.left = `${rect.right + window.scrollX - 12}px`;

                // Add scroll listener to maintain position
                window.addEventListener('scroll', () => {
                    const updatedRect = gridTitle.getBoundingClientRect();
                    button.style.top = `${updatedRect.top + window.scrollY}px`;
                    button.style.left = `${updatedRect.right + window.scrollX + 10}px`;
                });
            }, 0);
        } else {
        }

        document.body.appendChild(button);
        updateEditModeButtonVisuals();
    }

    function createEditPopup() {
        if (document.getElementById(EDIT_POPUP_ID)) return;

        const popup = document.createElement('div');
        popup.id = EDIT_POPUP_ID;
        popup.innerHTML = `
            <h4>Éditer Conversation <span id="dmm-edit-popup-conv-id"></span></h4>
            <div class="dmm-edit-field">
                <label for="dmm-edit-title">Titre:</label>
                <input type="text" id="dmm-edit-title" placeholder="Laisser vide pour restaurer">
            </div>
            <div class="dmm-edit-field">
                <label for="dmm-edit-image">URL Image:</label>
                <input type="text" id="dmm-edit-image" placeholder="Laisser vide pour restaurer">
            </div>
            <div class="dmm-edit-buttons">
                <button id="dmm-edit-save">Sauver</button>
                <button id="dmm-edit-cancel">Annuler</button>
                <button id="dmm-edit-reset">Reset</button>
            </div>
        `;
        popup.dataset.conversationId = '';

        document.body.appendChild(popup);
    }

    function updateEditModeButtonVisuals() {
        const button = document.getElementById(EDIT_MODE_TOGGLE_BUTTON_ID);
        if (!button) return;

        if (isEditModeActive) {
            button.classList.add('active');
            button.style.backgroundColor = '#e67e22';
        } else {
            button.classList.remove('active');
            button.style.backgroundColor = '#3498db';
        }
    }

    function toggleEditMode() {
        isEditModeActive = !isEditModeActive;
        closeEditPopup();
        updateEditModeButtonVisuals();
        updateSidebarItemsEditableState();
    }

    function updateSidebarItemsEditableState() {
        const messageListItems = document.querySelectorAll('#liste_messages li.message[id^="message_"]');
        messageListItems.forEach(item => {
            if (isEditModeActive) {
                item.classList.add('dmm-editable-item');
                item.title = 'Cliquer pour éditer le titre/image';
            } else {
                item.classList.remove('dmm-editable-item');
                item.title = '';
            }
        });
        document.body.classList.toggle('dmm-edit-mode-active', isEditModeActive);
    }

    let closePopupHandler = null;

    function openEditPopup(conversationId, listItemElement) {
        const popup = document.getElementById(EDIT_POPUP_ID);
        if (!popup || !listItemElement) return;

        const customData = getCustomData(conversationId) || { title: null, imageUrl: null };

        popup.querySelector('#dmm-edit-popup-conv-id').textContent = `(#${conversationId})`;
        popup.querySelector('#dmm-edit-title').value = customData.title || '';
        popup.querySelector('#dmm-edit-image').value = customData.imageUrl || '';
        popup.dataset.conversationId = conversationId;

        const saveBtn = popup.querySelector('#dmm-edit-save');
        const cancelBtn = popup.querySelector('#dmm-edit-cancel');
        const resetBtn = popup.querySelector('#dmm-edit-reset');

        saveBtn.replaceWith(saveBtn.cloneNode(true));
        cancelBtn.replaceWith(cancelBtn.cloneNode(true));
        resetBtn.replaceWith(resetBtn.cloneNode(true));

        popup.querySelector('#dmm-edit-save').addEventListener('click', handleEditPopupSave);
        popup.querySelector('#dmm-edit-cancel').addEventListener('click', closeEditPopup);
        popup.querySelector('#dmm-edit-reset').addEventListener('click', handleEditPopupReset);

        // Position popup relative to list item
        const rect = listItemElement.getBoundingClientRect();
        const popupHeight = popup.offsetHeight || 150;
        const popupWidth = popup.offsetWidth || 300;

        let top = rect.top + window.scrollY - (popupHeight / 2) + (rect.height / 2);
        let left = rect.right + window.scrollX + 10;

        // Adjust if off-screen
        if (left + popupWidth > window.innerWidth) {
            left = rect.left + window.scrollX - popupWidth - 10;
        }
        if (top < window.scrollY) {
            top = window.scrollY + 5;
        }
        if (top + popupHeight > window.innerHeight + window.scrollY) {
            top = window.innerHeight + window.scrollY - popupHeight - 5;
        }
        if (left < window.scrollX) left = window.scrollX + 5;

        popup.style.top = `${Math.max(0, top)}px`;
        popup.style.left = `${Math.max(0, left)}px`;
        popup.style.display = 'block';
        popup.querySelector('#dmm-edit-title').focus();

        if (closePopupHandler) {
            document.removeEventListener('mousedown', closePopupHandler, true);
            document.removeEventListener('keydown', closePopupHandler, true);
            closePopupHandler = null;
        }

        closePopupHandler = (event) => {
            if (event.type === 'keydown' && event.key === 'Escape') {
                closeEditPopup();
            } else if (event.type === 'mousedown' && !popup.contains(event.target)) {
                const toggleButton = document.getElementById(EDIT_MODE_TOGGLE_BUTTON_ID);
                if (!toggleButton?.contains(event.target) && !listItemElement.contains(event.target)) {
                    closeEditPopup();
                }
            }
        };

        setTimeout(() => {
            document.addEventListener('mousedown', closePopupHandler, true);
            document.addEventListener('keydown', closePopupHandler, true);
        }, 50);
    }

    function closeEditPopup() {
        const popup = document.getElementById(EDIT_POPUP_ID);
        if (popup) {
            popup.style.display = 'none';
            popup.dataset.conversationId = '';
        }
        // Remove listeners
        if (closePopupHandler) {
            document.removeEventListener('mousedown', closePopupHandler, true);
            document.removeEventListener('keydown', closePopupHandler, true);
            closePopupHandler = null;
        }
    }

    function handleEditPopupSave() {
        const popup = document.getElementById(EDIT_POPUP_ID);
        const conversationId = popup.dataset.conversationId;
        if (!conversationId) return;

        const newTitle = popup.querySelector('#dmm-edit-title').value;
        const newImageUrl = popup.querySelector('#dmm-edit-image').value;

        setCustomData(conversationId, newTitle, newImageUrl);
        closeEditPopup();

        // Update the list item
        const listItem = document.getElementById(`message_${conversationId}`);
        if (listItem) {
            applyCustomizationsToItem(listItem);
        }
    }

    function handleEditPopupReset() {
        const popup = document.getElementById(EDIT_POPUP_ID);
        const conversationId = popup.dataset.conversationId;
        if (!conversationId) return;

        setCustomData(conversationId, null, null);
        closeEditPopup();

        // Update the list item to restore originals
        const listItem = document.getElementById(`message_${conversationId}`);
        if (listItem) {
            applyCustomizationsToItem(listItem);
        }
    }

    /**
     * Applies custom title and image to a sidebar list item, or restores originals.
     * Stores original values in data attributes if not already present.
     */
    function applyCustomizationsToItem(listItem) {
        if (!listItem || !listItem.id || !listItem.id.startsWith('message_')) return;

        const conversationId = listItem.id.replace('message_', '');
        const titleElement = listItem.querySelector('.message_titre');
        const imgElement = listItem.querySelector('img');

        if (!titleElement || !imgElement) {
            return;
        }

        // Store originals if not already stored
        if (!listItem.hasAttribute('data-original-title')) {
            listItem.setAttribute('data-original-title', titleElement.textContent);
        }
        if (!listItem.hasAttribute('data-original-src')) {
            listItem.setAttribute('data-original-src', imgElement.src);
        }

        // Get custom data
        const customData = getCustomData(conversationId);

        // Apply or restore
        let appliedCustomTitle = false;
        let appliedCustomImage = false;

        if (customData) {
            // Apply custom title if set
            if (customData.title) {
                titleElement.textContent = customData.title;
                appliedCustomTitle = true;
            }
            // Apply custom image if set
            if (customData.imageUrl) {
                if (customData.imageUrl.startsWith('http://') || customData.imageUrl.startsWith('https://')) {
                    imgElement.src = customData.imageUrl;
                    appliedCustomImage = true;
                } else {
                }
            }
        }

        // Restore title if not customized
        if (!appliedCustomTitle && listItem.hasAttribute('data-original-title')) {
            if (titleElement.textContent !== listItem.getAttribute('data-original-title')) {
                titleElement.textContent = listItem.getAttribute('data-original-title');
            }
        }

        // Restore image if not customized
        if (!appliedCustomImage && listItem.hasAttribute('data-original-src')) {
            if (imgElement.src !== listItem.getAttribute('data-original-src')) {
                imgElement.src = listItem.getAttribute('data-original-src');
            }
        }
    }
})();