using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Video;
using UnityEngine.UI;
using TMPro;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;

public class MapManager : MonoBehaviour
{
    [Header("Configuration")]
    public string mapsRegistryUrl = ""; // Si vide, utilise la valeur de general-config.json

    [Header("Debug")]
    public bool showDebugZones = true;
    public bool decorativeVideosVisible = true;
    
    // Référence statique au shader pour éviter les problèmes de Shader.Find en WebGL
    private static Shader videoAlphaMaskShader = null;
    private static bool shaderChecked = false;

    [Header("Transparence Vidéos Décoratives")]
    [Tooltip("Multiplier pour l'étendue du dégradé (1.2 = défaut). Plus petit = transparence plus proche des bords")]
    [Range(0.5f, 2.0f)]
    public float vignetteExtent = 1.2f;
    
    [Tooltip("Point où la transparence commence (0.5 = 50% du rayon). Plus petit = centre transparent plus grand")]
    [Range(0.0f, 1.0f)]
    public float vignetteStartRadius = 0.5f;
    
    [Tooltip("Courbe du dégradé (0.8 = défaut). Plus petit = dégradé plus doux, plus grand = dégradé plus dur")]
    [Range(0.1f, 3.0f)]
    public float vignetteCurve = 0.8f;
    
    [Tooltip("Désactiver complètement le masque de transparence")]
    public bool disableVignetteMask = true;

    [Header("UI References")]
    public Camera mainCamera;

    private string mapConfigUrl;
    private string currentMapId;
    private MapConfigData mapConfig;
    private string currentStepId;
    private MapStep currentStep;
    private List<MapZoneClickable> zones = new List<MapZoneClickable>();
    // NOTE: headerPanel supprimé - maintenant géré par HeaderManager
    private GameObject popupContainer;
    private MapZone currentZoneData; // Zone actuellement cliquée (pour récupérer gameType)
    private GameObject backgroundObject;
    public GameObject scrollViewObject; // Public pour que SettingsManager puisse désactiver le ScrollRect
    private GameObject viewportObject;
    private GameObject contentObject;
    private bool isDragging = false;
    private Vector2 dragStartPos;
    private Vector2 contentStartPos;
    private List<GameObject> decorators = new List<GameObject>();
    private List<GameObject> decorativeVideos = new List<GameObject>();
    private List<GameObject> stepDecorativeImages = new List<GameObject>(); // Images décoratives du step actuel
    private GameObject activePopup; // Popup actuellement ouverte
    
    // Listes pour tracker les ressources créées dynamiquement (nettoyage WebGL)
    private readonly List<Texture2D> dynamicTextures = new List<Texture2D>();
    private readonly List<Sprite> dynamicSprites = new List<Sprite>();
    
    // Flag pour logs verbeux (désactivé en production)
    // (static readonly pour éviter CS0162 "unreachable code detected" quand le compilateur voit une constante)
    private static readonly bool VERBOSE_DEBUG = false;
    
    [System.Diagnostics.Conditional("UNITY_EDITOR")]
    private void LogVerbose(string message)
    {
        if (VERBOSE_DEBUG) Debug.Log(message);
    }
    
    private Vector2 popupRelativePosition; // Position relative de la popup par rapport au scroll
    private RectTransform activePopupZone; // Zone associée au popup actif (pour suivre le scroll)
    private bool isFullscreenVideoPlaying = false; // Indique si une vidéo fullscreen est en cours

    // Modale d'information (clic sur la jauge/fleur)
    private GameObject zenitudeInfoModalRoot;
    private bool zenitudeInfoModalCloseArmed = false;
    
    // Panneau de texte en bas pour les zones sans action
    private GameObject bottomTextPanel;

    /// <summary>
    /// Vérifie si une vidéo plein écran est actuellement en cours de lecture
    /// </summary>
    public bool IsFullscreenVideoPlaying()
    {
        return isFullscreenVideoPlaying;
    }

    // Fonction helper pour trouver le Canvas principal (pas PopupContainer)
    private Canvas GetMainCanvas()
    {
        Canvas canvas = null;
        Canvas[] allCanvases = FindObjectsByType<Canvas>(FindObjectsSortMode.None);
        
        // Chercher le Canvas nommé "Canvas"
        foreach (Canvas c in allCanvases)
        {
            if (c.gameObject.name == "Canvas")
            {
                canvas = c;
                break;
            }
        }
        
        // Fallback : prendre le premier qui n'est pas PopupContainer
        if (canvas == null)
        {
            foreach (Canvas c in allCanvases)
            {
                if (c.gameObject.name != "PopupContainer")
                {
                    canvas = c;
                    break;
                }
            }
        }
        
        return canvas;
    }
    
    // Overlay noir immédiat pour éviter l'écran blanc pendant le chargement
    private GameObject immediateBlackOverlay;
    // Indique si on utilise l'overlay du SceneTransitionManager
    private bool usingSceneTransitionOverlay = false;
    
    void Awake()
    {
        LogVerbose("[MapManager] 🔵 Awake() appelé - MapManager initialisé");
        
        // 🖤 PRIORITÉ ABSOLUE #1 : Forcer la caméra à avoir un fond noir IMMÉDIATEMENT
        ForceCameraBlackBackground();
        
        // 🖤 PRIORITÉ ABSOLUE #2 : Créer un écran noir immédiatement pour éviter le flash blanc
        CreateImmediateBlackOverlay();
    }
    
    /// <summary>
    /// Force toutes les caméras à avoir un fond noir pour éviter le flash blanc
    /// </summary>
    private void ForceCameraBlackBackground()
    {
        // Forcer Camera.main si elle existe
        if (Camera.main != null)
        {
            Camera.main.backgroundColor = Color.black;
            Camera.main.clearFlags = CameraClearFlags.SolidColor;
            LogVerbose("[MapManager] 🖤 Camera.main forcée en fond noir");
        }
        
        // Forcer toutes les caméras de la scène
        Camera[] allCameras = FindObjectsByType<Camera>(FindObjectsSortMode.None);
        foreach (Camera cam in allCameras)
        {
            if (cam != null)
            {
                cam.backgroundColor = Color.black;
                cam.clearFlags = CameraClearFlags.SolidColor;
            }
        }
        
        if (allCameras.Length > 0)
        {
            LogVerbose($"[MapManager] 🖤 {allCameras.Length} caméra(s) forcée(s) en fond noir");
        }
    }
    
    /// <summary>
    /// Crée un overlay noir IMMÉDIATEMENT pour éviter l'écran blanc pendant le chargement.
    /// Cet overlay sera masqué une fois que le background de la scène est chargé.
    /// </summary>
    private void CreateImmediateBlackOverlay()
    {
        // Vérifier si le SceneTransitionManager a un overlay actif
        if (SceneTransitionManager.IsOverlayVisible)
        {
            LogVerbose("[MapManager] ✅ SceneTransitionManager overlay déjà présent - on l'utilise");
            usingSceneTransitionOverlay = true;
            return;
        }
        
        // Vérifier si UnifiedLoadingManager a un overlay de secours actif
        if (UnifiedLoadingManager.IsFallbackOverlayVisible)
        {
            LogVerbose("[MapManager] ✅ UnifiedLoadingManager fallback overlay déjà présent - on l'utilise");
            usingSceneTransitionOverlay = true; // On réutilise le même flag
            return;
        }
        
        // Vérifier s'il existe déjà un overlay (IntroTransitionOverlay par exemple)
        if (IntroTransitionOverlay.Instance != null && IntroTransitionOverlay.IsOverlayVisible())
        {
            LogVerbose("[MapManager] ✅ IntroTransitionOverlay déjà présent - pas besoin d'overlay supplémentaire");
            return;
        }
        
        // Vérifier si le LoadingScreenManager est déjà visible
        if (LoadingScreenManager.Instance != null && LoadingScreenManager.Instance.IsLoading)
        {
            LogVerbose("[MapManager] ✅ LoadingScreenManager déjà visible - pas besoin d'overlay supplémentaire");
            return;
        }
        
        LogVerbose("[MapManager] 🖤 Création d'un overlay noir immédiat pour éviter le flash blanc");
        
        // Créer un canvas overlay noir
        immediateBlackOverlay = new GameObject("ImmediateBlackOverlay_Map");
        
        Canvas overlayCanvas = immediateBlackOverlay.AddComponent<Canvas>();
        overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
        overlayCanvas.sortingOrder = 10000; // Au-dessus de tout
        
        // Ajouter un fond noir
        GameObject blackPanel = new GameObject("BlackPanel");
        blackPanel.transform.SetParent(immediateBlackOverlay.transform, false);
        
        RectTransform rectTransform = blackPanel.AddComponent<RectTransform>();
        rectTransform.anchorMin = Vector2.zero;
        rectTransform.anchorMax = Vector2.one;
        rectTransform.sizeDelta = Vector2.zero;
        rectTransform.anchoredPosition = Vector2.zero;
        
        Image blackImage = blackPanel.AddComponent<Image>();
        blackImage.color = Color.black;
        
        LogVerbose("[MapManager] ✅ Overlay noir créé");
    }
    
    /// <summary>
    /// Masque l'overlay noir immédiat avec un fade out
    /// </summary>
    private void HideImmediateBlackOverlay()
    {
        // Si on utilise l'overlay du SceneTransitionManager ou UnifiedLoadingManager, le masquer
        if (usingSceneTransitionOverlay)
        {
            // Masquer les deux types d'overlays
            if (SceneTransitionManager.IsOverlayVisible)
            {
                LogVerbose("[MapManager] 🎬 Masquage de l'overlay SceneTransitionManager");
                SceneTransitionManager.HideTransitionOverlay();
            }
            if (UnifiedLoadingManager.IsFallbackOverlayVisible)
            {
                LogVerbose("[MapManager] 🎬 Masquage de l'overlay UnifiedLoadingManager");
                UnifiedLoadingManager.HideFallbackOverlay();
            }
            usingSceneTransitionOverlay = false;
            return;
        }
        
        // Masquer aussi l'IntroTransitionOverlay s'il est encore visible
        if (IntroTransitionOverlay.Instance != null && IntroTransitionOverlay.IsOverlayVisible())
        {
            LogVerbose("[MapManager] 🎬 Masquage de l'IntroTransitionOverlay");
            IntroTransitionOverlay.HideOverlay();
        }
        
        if (immediateBlackOverlay != null)
        {
            LogVerbose("[MapManager] 🎬 Masquage de l'overlay noir immédiat");
            StartCoroutine(FadeOutAndDestroyOverlay());
        }
    }
    
    private IEnumerator FadeOutAndDestroyOverlay()
    {
        if (immediateBlackOverlay == null) yield break;
        
        Image blackImage = immediateBlackOverlay.GetComponentInChildren<Image>();
        if (blackImage != null)
        {
            float fadeDuration = 0.3f;
            float elapsed = 0f;
            Color startColor = blackImage.color;
            
            while (elapsed < fadeDuration)
            {
                elapsed += Time.deltaTime;
                float alpha = Mathf.Lerp(1f, 0f, elapsed / fadeDuration);
                blackImage.color = new Color(startColor.r, startColor.g, startColor.b, alpha);
                yield return null;
            }
        }
        
        if (immediateBlackOverlay != null)
        {
            Destroy(immediateBlackOverlay);
            immediateBlackOverlay = null;
            LogVerbose("[MapManager] ✅ Overlay noir détruit");
        }
    }
    
    void Start()
    {
        LogVerbose("[MapManager] 🚀 Start() appelé - Début de l'initialisation");
        StartCoroutine(InitializeMapManager());
    }
    
    IEnumerator InitializeMapManager()
    {
        LogVerbose("[MapManager] 🔄 InitializeMapManager() démarré");
        
        // 🧹 NOUVEAU : Nettoyer DIRECTEMENT les layouts de jeu (y compris dans DontDestroyOnLoad)
        LogVerbose("[MapManager] 🧹 Nettoyage des layouts de jeu résiduels...");
        var shootingLayouts = FindObjectsByType<ShootingGameLayout>(FindObjectsInactive.Include, FindObjectsSortMode.None);
        foreach (var layout in shootingLayouts)
        {
            LogVerbose($"[MapManager] 🗑️ ShootingGameLayout détruit (scene: {layout.gameObject.scene.name})");
            DestroyImmediate(layout.gameObject);
        }
        var calculatorLayouts = FindObjectsByType<CalculatorGameLayout>(FindObjectsInactive.Include, FindObjectsSortMode.None);
        foreach (var layout in calculatorLayouts)
        {
            LogVerbose($"[MapManager] 🗑️ CalculatorGameLayout détruit (scene: {layout.gameObject.scene.name})");
            DestroyImmediate(layout.gameObject);
        }
        var trousLayouts = FindObjectsByType<TrousGameLayout>(FindObjectsInactive.Include, FindObjectsSortMode.None);
        foreach (var layout in trousLayouts)
        {
            LogVerbose($"[MapManager] 🗑️ TrousGameLayout détruit (scene: {layout.gameObject.scene.name})");
            DestroyImmediate(layout.gameObject);
        }
        
        // **NETTOYER LES CANVAS DES JEUX PRÉCÉDENTS** (Fix WebGL)
        LogVerbose("[MapManager] 🧹 Nettoyage des canvas de jeux précédents...");
        Canvas[] allCanvases = FindObjectsByType<Canvas>(FindObjectsSortMode.None);
        int cleanedCount = 0;
        foreach (Canvas existingCanvas in allCanvases)
        {
            if (existingCanvas != null && existingCanvas.gameObject != null)
            {
                // Ne pas détruire :
                // - Les canvas persistants (DontDestroyOnLoad) SAUF ceux de jeux
                // - Les canvas de loading
                // - Le canvas de la Map (on va le réutiliser)
                bool isPersistent = existingCanvas.gameObject.scene.name == "DontDestroyOnLoad";
                bool isLoading = existingCanvas.name.Contains("Loading") || 
                                existingCanvas.GetComponentInParent<LoadingScreenManager>() != null || 
                                existingCanvas.GetComponentInParent<UnifiedLoadingManager>() != null;
                bool isMapCanvas = existingCanvas.name.Contains("MapCanvas") || existingCanvas.GetComponent<MapManager>() != null;
                
                // Détecter les canvas de jeux (ShootingGameLayout, CalculatorGameLayout, TrousGameLayout, etc.)
                bool isGameCanvas = existingCanvas.GetComponentInChildren<ShootingGameLayout>() != null ||
                                   existingCanvas.GetComponentInChildren<CalculatorGameLayout>() != null ||
                                   existingCanvas.GetComponentInChildren<TrousGameLayout>() != null ||
                                   existingCanvas.name.Contains("GameCanvas") ||
                                   existingCanvas.name.Contains("ShootingGame") ||
                                   existingCanvas.name.Contains("Calculator") ||
                                   existingCanvas.name.Contains("Trous") ||
                                   existingCanvas.sortingOrder >= 5; // Les jeux ont souvent un sortingOrder élevé
                
                // MODIFIÉ : Détruire AUSSI les canvas de jeu dans DontDestroyOnLoad
                if (isGameCanvas || (!isPersistent && !isLoading && !isMapCanvas && existingCanvas.sortingOrder > 0))
                {
                    LogVerbose($"[MapManager] 🗑️ Destruction canvas de jeu: {existingCanvas.name} (sortingOrder: {existingCanvas.sortingOrder}, persistent: {isPersistent})");
                    DestroyImmediate(existingCanvas.gameObject);
                    cleanedCount++;
                }
            }
        }
        LogVerbose($"[MapManager] ✅ Nettoyage terminé - {cleanedCount} canvas détruits");
        
        // 🧹 NOUVEAU : Nettoyer le DialogueBottomBanner qui peut persister après un jeu
        GameObject bottomBanner = GameObject.Find("DialogueBottomBanner");
        if (bottomBanner != null)
        {
            LogVerbose("[MapManager] 🧹 DialogueBottomBanner trouvé et détruit (résidu de jeu précédent)");
            DialogueDebugLog.Critical("[MapManager] DestroyImmediate(DialogueBottomBanner) appelé");
            DestroyImmediate(bottomBanner);
        }
        
        // Nettoyer aussi d'autres éléments de dialogue qui pourraient persister
        GameObject[] dialogueObjects = new GameObject[] {
            GameObject.Find("DialoguePanel"),
            GameObject.Find("SubtitlePanel"),
            GameObject.Find("DialogueFrame"),
            GameObject.Find("DialogueCanvas")
        };
        foreach (var obj in dialogueObjects)
        {
            if (obj != null)
            {
                LogVerbose($"[MapManager] 🧹 Élément dialogue '{obj.name}' détruit");
                DestroyImmediate(obj);
            }
        }
        
        // Masquer l'overlay de transition de l'intro dès le début
        // (évite l'écran noir prolongé si on vient directement de l'intro)
        if (IntroTransitionOverlay.Instance != null)
        {
            LogVerbose("[MapManager] ✅ Masquage de l'overlay de transition de l'intro");
            IntroTransitionOverlay.HideOverlay();
        }
        
        // Créer GeneralConfigManager s'il n'existe pas
        if (GeneralConfigManager.Instance == null)
        {
            GameObject configManagerObj = new GameObject("GeneralConfigManager");
            configManagerObj.AddComponent<GeneralConfigManager>();
            LogVerbose("[MapManager] GeneralConfigManager créé automatiquement");
        }
        
        // Attendre que GeneralConfigManager soit initialisé
        while (GeneralConfigManager.Instance == null)
        {
            yield return null;
        }
        
        // Attendre que la configuration soit chargée
        while (!GeneralConfigManager.Instance.IsConfigLoaded())
        {
            yield return null;
        }
        
        LogVerbose("[MapManager] GeneralConfigManager chargé et prêt");
        
        // Initialiser le volume audio depuis les paramètres
        bool soundEnabled = PlayerPrefs.GetInt("SoundEnabled", 1) == 1;
        AudioListener.volume = soundEnabled ? 1f : 0f;
        LogVerbose($"[MapManager] Volume audio initialisé: {AudioListener.volume} (SoundEnabled: {soundEnabled})");
        
        // Utiliser la configuration générale pour la map par défaut
        string defaultMapId = GeneralConfigManager.Instance.GetDefaultMapId();
        LogVerbose($"[MapManager] Map par défaut depuis GeneralConfigManager: {defaultMapId}");
        
        currentMapId = PlayerPrefs.GetString("CurrentMapId", defaultMapId);
        
        // Validation préliminaire du mapId - format attendu: "map-Q0" à "map-Q9"
        const int MAX_VALID_MAP_INDEX = 9;
        bool isValidMapId = false;
        if (!string.IsNullOrEmpty(currentMapId) && currentMapId.StartsWith("map-Q"))
        {
            string indexStr = currentMapId.Replace("map-Q", "");
            if (int.TryParse(indexStr, out int mapIndex) && mapIndex >= 0 && mapIndex <= MAX_VALID_MAP_INDEX)
            {
                isValidMapId = true;
            }
        }
        
        if (!isValidMapId)
        {
            Debug.LogWarning($"[MapManager] ⚠️ MapId invalide détecté: '{currentMapId}', nettoyage et fallback vers '{defaultMapId}'");
            PlayerPrefs.SetString("CurrentMapId", defaultMapId);
            PlayerPrefs.DeleteKey("CurrentStepId_" + currentMapId);
            
            // Extraire et mettre à jour le QuestId depuis le mapId par défaut
            // Format: map-Q0 → Q0 → index 0 → questId = 0 + 1 = 1
            // Format: map-Q1 → Q1 → index 1 → questId = 1 + 1 = 2
            if (defaultMapId.StartsWith("map-Q"))
            {
                string indexStr = defaultMapId.Replace("map-Q", "");
                if (int.TryParse(indexStr, out int index))
                {
                    int defaultQuestId = index + 1; // Q0 = questId 1, Q1 = questId 2, etc.
                    PlayerPrefs.SetInt("CurrentQuestId", defaultQuestId);
                    LogVerbose($"[MapManager] ✅ CurrentQuestId mis à jour: {defaultQuestId} (index: {index})");
                }
            }
            
            PlayerPrefs.Save();
            currentMapId = defaultMapId;
        }
        
        currentStepId = PlayerPrefs.GetString("CurrentStepId_" + currentMapId, "");

        LogVerbose($"[MapManager] Configuration chargée - Map: {currentMapId}, Step: {currentStepId}");
        LogVerbose($"[MapManager] Map par défaut depuis config: {defaultMapId}");
        LogVerbose($"[MapManager] DEBUG - PlayerPrefs CurrentStepId: '{PlayerPrefs.GetString("CurrentStepId_" + currentMapId, "")}'");
        LogVerbose($"[MapManager] DEBUG - PlayerPrefs NextStepId: '{PlayerPrefs.GetString("NextStepId_" + currentMapId, "")}'");

        // Afficher l'écran de chargement
        if (UnifiedLoadingManager.Instance != null)
        {
            UnifiedLoadingManager.ShowLoading("Chargement de la carte...", LoadingContext.Map);
        }

        // TOUJOURS récupérer le questId depuis l'API projects pour être sûr d'avoir le bon ID
        // (le questId dans PlayerPrefs peut être incorrect car calculé depuis le mapId)
        int questId = 0;
        bool hasToken = UserDataManager.Instance != null && !string.IsNullOrEmpty(UserDataManager.Instance.token);
        LogVerbose($"[MapManager] 🔍 Token disponible: {hasToken}");
        LogVerbose($"[MapManager] 🔍 GeneralConfigManager.Instance != null: {GeneralConfigManager.Instance != null}");
        LogVerbose($"[MapManager] 🔍 UserDataManager.Instance != null: {UserDataManager.Instance != null}");
        
        // Récupérer le questId depuis l'API projects pour avoir le vrai ID
        if (hasToken && GeneralConfigManager.Instance != null && !string.IsNullOrEmpty(currentMapId) && currentMapId.StartsWith("map-Q"))
        {
            LogVerbose($"[MapManager] 🔍 Récupération du questId depuis l'API projects pour mapId: {currentMapId}");
            yield return StartCoroutine(LoadQuestIdFromProjectsApi(currentMapId));
            questId = PlayerPrefs.GetInt("CurrentQuestId", 0);
            LogVerbose($"[MapManager] 🔍 QuestId récupéré depuis l'API: {questId}");
        }
        else
        {
            // Fallback : utiliser le questId depuis PlayerPrefs si l'API n'est pas disponible
            questId = PlayerPrefs.GetInt("CurrentQuestId", 0);
            LogVerbose($"[MapManager] 🔍 QuestId depuis PlayerPrefs (fallback): {questId}");
        }
        
        // TOUJOURS charger depuis l'API uniquement - plus de fichiers locaux
        if (questId > 0 && GeneralConfigManager.Instance != null && UserDataManager.Instance != null && hasToken)
        {
            LogVerbose($"[MapManager] 🌐 QuestId trouvé ({questId}), chargement depuis l'API");
            yield return StartCoroutine(LoadMapConfiguration());
        }
        else
        {
            if (questId <= 0)
            {
                Debug.LogError($"[MapManager] ❌ ERREUR : QuestId invalide ou manquant ({questId}). Impossible de charger la map depuis l'API.");
            }
            else if (GeneralConfigManager.Instance == null)
            {
                Debug.LogError("[MapManager] ❌ ERREUR : GeneralConfigManager non disponible. Impossible de charger la map depuis l'API.");
            }
            else if (UserDataManager.Instance == null || !hasToken)
            {
                Debug.LogError("[MapManager] ❌ ERREUR : Utilisateur non connecté ou token manquant. Impossible de charger la map depuis l'API.");
            }
            // Plus de fallback vers LoadMapFromRegistry - les fichiers locaux n'existent plus
        }
    }


    IEnumerator LoadMapFromRegistry()
    {
        // Utiliser l'URL du registre depuis la configuration générale
        string registryUrl = "";
        if (GeneralConfigManager.Instance != null)
        {
            registryUrl = GeneralConfigManager.Instance.GetMapsRegistryUrl();
        }
        else if (!string.IsNullOrEmpty(mapsRegistryUrl))
        {
            registryUrl = mapsRegistryUrl;
        }
        
        if (string.IsNullOrEmpty(registryUrl))
        {
            Debug.LogError("[MapManager] mapsRegistryUrl introuvable et GeneralConfigManager non disponible");
            yield break;
        }
            
        LogVerbose($"[MapManager] Chargement registre: {registryUrl}");

        using (UnityWebRequest www = UnityWebRequest.Get(registryUrl))
        {
            www.SetRequestHeader("Cache-Control", "no-cache");
            yield return www.SendWebRequest();

            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"[MapManager] Erreur registre: {www.error}");
                yield break;
            }

            string json = www.downloadHandler.text;
            MapRegistryWrapper registry = JsonUtility.FromJson<MapRegistryWrapper>(json);
            
            if (registry == null || registry.maps == null)
            {
                Debug.LogError("[MapManager] ❌ ERREUR : Le registre des maps est vide ou invalide !");
                Debug.LogError($"[MapManager] JSON reçu: {json}");
                yield break;
            }
            
            LogVerbose($"[MapManager] Registre chargé - {registry.maps.Count} maps disponibles");
            foreach (var map in registry.maps)
            {
                LogVerbose($"[MapManager]   - {map.id}: {map.name} (configUrl: {map.configUrl})");
            }
            
            MapInfo mapInfo = registry.maps.Find(m => m.id == currentMapId);

            if (mapInfo == null)
            {
                Debug.LogWarning($"[MapManager] ⚠️ Map introuvable dans le registre: '{currentMapId}'");
                Debug.LogWarning($"[MapManager] Maps disponibles dans le registre:");
                foreach (var map in registry.maps)
                {
                    Debug.LogWarning($"[MapManager]   - {map.id}");
                }
                
                // Fallback vers la map par défaut
                string defaultMapId = GeneralConfigManager.Instance?.GetDefaultMapId() ?? "map-Q0";
                LogVerbose($"[MapManager] 🔄 Fallback vers la map par défaut: {defaultMapId}");
                
                // Nettoyer l'ancien mapId invalide des PlayerPrefs
                PlayerPrefs.SetString("CurrentMapId", defaultMapId);
                PlayerPrefs.Save();
                currentMapId = defaultMapId;
                
                mapInfo = registry.maps.Find(m => m.id == currentMapId);
                if (mapInfo == null)
                {
                    // Si même la map par défaut n'existe pas, prendre la première map disponible
                    if (registry.maps.Count > 0)
                    {
                        mapInfo = registry.maps[0];
                        currentMapId = mapInfo.id;
                        PlayerPrefs.SetString("CurrentMapId", currentMapId);
                        PlayerPrefs.Save();
                        LogVerbose($"[MapManager] 🔄 Utilisation de la première map disponible: {currentMapId}");
                    }
                    else
                    {
                        Debug.LogError("[MapManager] ❌ ERREUR : Aucune map disponible dans le registre !");
                        yield break;
                    }
                }
            }
            
            if (string.IsNullOrEmpty(mapInfo.configUrl))
            {
                Debug.LogError($"[MapManager] ❌ ERREUR : configUrl est vide pour la map '{currentMapId}' !");
                Debug.LogError($"[MapManager] Vérifiez le fichier maps-registry.json");
                yield break;
            }

            // Utiliser GetMapConfigUrl pour construire l'URL complète avec le bon préfixe
            mapConfigUrl = GeneralConfigManager.Instance.GetMapConfigUrl(mapInfo.configUrl);
            LogVerbose($"[MapManager] Map trouvée: {mapInfo.name}, URL: {mapConfigUrl}");

            yield return StartCoroutine(LoadMapConfiguration());
        }
    }

    /// <summary>
    /// Charge les quêtes depuis l'API projects pour trouver le questId réel correspondant au mapId
    /// </summary>
    IEnumerator LoadQuestIdFromProjectsApi(string mapId)
    {
        if (GeneralConfigManager.Instance == null || UserDataManager.Instance == null)
        {
            Debug.LogWarning("[MapManager] ⚠️ Impossible de charger les quêtes : GeneralConfigManager ou UserDataManager non disponible");
            yield break;
        }
        
        string apiUrl = GeneralConfigManager.Instance.GetMainSceneConfigApiUrl();
        if (string.IsNullOrEmpty(apiUrl))
        {
            Debug.LogWarning("[MapManager] ⚠️ Impossible de construire l'URL de l'API projects");
            yield break;
        }
        
        LogVerbose($"[MapManager] 🔍 Chargement des quêtes depuis: {apiUrl}");
        
        string token = UserDataManager.Instance.token;
        
        using (UnityWebRequest www = UnityWebRequest.Get(apiUrl))
        {
            www.SetRequestHeader("Authorization", $"Bearer {token}");
            yield return www.SendWebRequest();
            
            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.LogWarning($"[MapManager] ⚠️ Erreur lors du chargement des quêtes: {www.error}");
                yield break;
            }
            
            string jsonData = www.downloadHandler.text;
            ApiMainSceneResponse apiResponse = JsonUtility.FromJson<ApiMainSceneResponse>(jsonData);
            
            if (apiResponse != null && apiResponse.status == "success" && apiResponse.data != null && apiResponse.data.quests != null)
            {
                LogVerbose($"[MapManager] 🔍 Recherche du questId pour mapId: {mapId}");
                LogVerbose($"[MapManager] 🔍 Nombre de quêtes dans l'API: {apiResponse.data.quests.Count}");
                
                // Trouver la quête correspondant au mapId
                // Le mapId est généré dans MainSceneManager comme: map-Q{index}
                // où index est la position dans la liste (0, 1, 2, ...), PAS quest.id - 1
                if (mapId.StartsWith("map-Q"))
                {
                    string indexStr = mapId.Replace("map-Q", "");
                    if (int.TryParse(indexStr, out int mapIndex))
                    {
                        LogVerbose($"[MapManager] 🔍 mapId index extrait: {mapIndex}");
                        
                        // Le mapIndex correspond à l'index dans la liste des quêtes
                        if (mapIndex >= 0 && mapIndex < apiResponse.data.quests.Count)
                        {
                            var quest = apiResponse.data.quests[mapIndex];
                            int realQuestId = quest.id;
                            PlayerPrefs.SetInt("CurrentQuestId", realQuestId);
                            PlayerPrefs.Save();
                            LogVerbose($"[MapManager] ✅ QuestId trouvé depuis l'API: {realQuestId} pour mapId: {mapId} (index: {mapIndex})");
                            yield break;
                        }
                        else
                        {
                            Debug.LogWarning($"[MapManager] ⚠️ Index {mapIndex} hors limites (0-{apiResponse.data.quests.Count - 1})");
                        }
                    }
                }
                
                Debug.LogWarning($"[MapManager] ⚠️ Aucune quête trouvée pour mapId: {mapId}");
                Debug.LogWarning($"[MapManager] ⚠️ MapIds disponibles dans l'API:");
                for (int i = 0; i < apiResponse.data.quests.Count; i++)
                {
                    var quest = apiResponse.data.quests[i];
                    string generatedMapId = $"map-Q{i}";
                    Debug.LogWarning($"[MapManager]   - Index {i}: Quest id={quest.id} → {generatedMapId}");
                }
            }
            else
            {
                Debug.LogWarning("[MapManager] ⚠️ Réponse API invalide ou sans quêtes");
            }
        }
    }

    /// <summary>
    /// Charge la configuration de la map depuis l'API
    /// </summary>
    IEnumerator LoadMapConfigurationFromApi(int questId)
    {
        string apiUrl = GeneralConfigManager.Instance.GetQuestConfigApiUrl(questId);
        
        if (string.IsNullOrEmpty(apiUrl))
        {
            Debug.LogError("[MapManager] ❌ Impossible de construire l'URL de l'API quest");
            yield break;
        }

        LogVerbose($"[MapManager] 🌐 Appel API quest: {apiUrl}");

        string token = UserDataManager.Instance.token;

        using (UnityWebRequest www = UnityWebRequest.Get(apiUrl))
        {
            // Ajouter l'authentification Bearer Token
            www.SetRequestHeader("Authorization", $"Bearer {token}");

            yield return www.SendWebRequest();

            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"[MapManager] ❌ Erreur de chargement de la configuration depuis l'API : {www.error}");
                Debug.LogError($"URL utilisée : {apiUrl}");
                Debug.LogError($"Code de réponse : {www.responseCode}");
                if (!string.IsNullOrEmpty(www.downloadHandler?.text))
                {
                    Debug.LogError($"Réponse serveur : {www.downloadHandler.text}");
                }
                yield break;
            }

            string jsonData = www.downloadHandler.text;
            LogVerbose($"[MapManager] ✅ Configuration reçue depuis l'API ({jsonData.Length} caractères)");
            LogVerbose($"[MapManager] 🔍 Début de la réponse: {jsonData.Substring(0, Mathf.Min(500, jsonData.Length))}...");

            // Parser la réponse de l'API
            // Format API possible 1: {"status": "success", "message": "...", "data": {MapConfigData}}
            // Format API possible 2: {"status": "success", "message": "...", "data": {"mapConfig": {MapConfigData}}}
            // Format fichier local: {"mapConfig": {...}}
            ApiQuestResponse apiResponse = null;
            ApiQuestResponseWithWrapper apiResponseWrapper = null;
            MapConfigWrapper fileWrapper = null;
            
            try
            {
                // Essayer d'abord le format API direct (data = MapConfigData)
                apiResponse = JsonUtility.FromJson<ApiQuestResponse>(jsonData);
                LogVerbose($"[MapManager] 🔍 Parsing API direct - apiResponse != null: {apiResponse != null}");
                if (apiResponse != null)
                {
                    LogVerbose($"[MapManager] 🔍 status: '{apiResponse.status}', data != null: {apiResponse.data != null}");
                    if (apiResponse.data != null)
                    {
                        LogVerbose($"[MapManager] 🔍 data.steps != null: {apiResponse.data.steps != null}, steps count: {apiResponse.data.steps?.Count ?? 0}");
                    }
                }
                
                if (apiResponse != null && apiResponse.data != null && apiResponse.status == "success")
                {
                    mapConfig = apiResponse.data;
                    LogVerbose("[MapManager] ✅ Configuration parsée depuis l'API (format API direct)");
                    LogVerbose($"[MapManager] 🔍 Steps count: {mapConfig.steps?.Count ?? 0}");
                    if (mapConfig.steps != null && mapConfig.steps.Count > 0)
                    {
                        LogVerbose($"[MapManager] 🔍 Premier step: id={mapConfig.steps[0].id}, type={mapConfig.steps[0].type}");
                    }
                }
                else
                {
                    // Essayer le format API avec wrapper (data = MapConfigWrapper)
                    Debug.LogWarning("[MapManager] ⚠️ Format API direct non valide, essai format API avec wrapper...");
                    apiResponseWrapper = JsonUtility.FromJson<ApiQuestResponseWithWrapper>(jsonData);
                    LogVerbose($"[MapManager] 🔍 Parsing API wrapper - apiResponseWrapper != null: {apiResponseWrapper != null}");
                    if (apiResponseWrapper != null)
                    {
                        LogVerbose($"[MapManager] 🔍 status: '{apiResponseWrapper.status}', data != null: {apiResponseWrapper.data != null}");
                        if (apiResponseWrapper.data != null)
                        {
                            LogVerbose($"[MapManager] 🔍 data.mapConfig != null: {apiResponseWrapper.data.mapConfig != null}");
                        }
                    }
                    
                    if (apiResponseWrapper != null && apiResponseWrapper.data != null && apiResponseWrapper.data.mapConfig != null && apiResponseWrapper.status == "success")
                    {
                        mapConfig = apiResponseWrapper.data.mapConfig;
                        LogVerbose("[MapManager] ✅ Configuration parsée depuis l'API (format API avec wrapper)");
                        LogVerbose($"[MapManager] 🔍 Steps count: {mapConfig.steps?.Count ?? 0}");
                    }
                    else
                    {
                        // Essayer le format fichier local (fallback)
                        Debug.LogWarning("[MapManager] ⚠️ Format API wrapper non valide, essai format fichier local...");
                        fileWrapper = JsonUtility.FromJson<MapConfigWrapper>(jsonData);
                        LogVerbose($"[MapManager] 🔍 Parsing fichier - fileWrapper != null: {fileWrapper != null}");
                        if (fileWrapper != null)
                        {
                            LogVerbose($"[MapManager] 🔍 mapConfig != null: {fileWrapper.mapConfig != null}");
                        }
                        
                        if (fileWrapper != null && fileWrapper.mapConfig != null)
                        {
                            mapConfig = fileWrapper.mapConfig;
                            LogVerbose("[MapManager] ✅ Configuration parsée avec wrapper (format fichier local)");
                            LogVerbose($"[MapManager] 🔍 Steps count: {mapConfig.steps?.Count ?? 0}");
                        }
                        else
                        {
                            Debug.LogError("[MapManager] ❌ Structure de réponse invalide depuis l'API");
                            Debug.LogError($"[MapManager] Réponse complète (premiers 1000 caractères): {jsonData.Substring(0, Mathf.Min(1000, jsonData.Length))}...");
                        }
                    }
                }
            }
            catch (System.Exception e)
            {
                Debug.LogError($"[MapManager] ❌ Erreur lors du parsing de la réponse API : {e.Message}");
                Debug.LogError($"[MapManager] Stack trace: {e.StackTrace}");
                Debug.LogError($"[MapManager] JSON reçu (premiers 500 caractères): {jsonData.Substring(0, Mathf.Min(500, jsonData.Length))}...");
            }
            
            // Vérification finale
            if (mapConfig == null)
            {
                Debug.LogError("[MapManager] ❌ mapConfig est null après parsing - Le chargement a échoué");
                Debug.LogError("[MapManager] Vérifiez le format de la réponse API dans les logs ci-dessus");
            }
            else
            {
                LogVerbose($"[MapManager] ✅ mapConfig chargé avec succès - {mapConfig.steps?.Count ?? 0} steps");
            }
        }
    }

    IEnumerator LoadMapConfiguration()
    {
        LogVerbose("[MapManager] 🔍 Début de LoadMapConfiguration()");
        
        // TOUJOURS charger depuis l'API uniquement - plus de fichiers locaux
        int questId = PlayerPrefs.GetInt("CurrentQuestId", 0);
        LogVerbose($"[MapManager] 🔍 CurrentQuestId depuis PlayerPrefs: {questId}");
        LogVerbose($"[MapManager] 🔍 GeneralConfigManager.Instance != null: {GeneralConfigManager.Instance != null}");
        LogVerbose($"[MapManager] 🔍 UserDataManager.Instance != null: {UserDataManager.Instance != null}");
        
        bool hasToken = UserDataManager.Instance != null && !string.IsNullOrEmpty(UserDataManager.Instance.token);
        LogVerbose($"[MapManager] 🔍 Token disponible: {hasToken}");
        
        // Si le questId n'est pas valide, essayer de le récupérer depuis l'API projects
        if (questId <= 0 && hasToken && GeneralConfigManager.Instance != null && !string.IsNullOrEmpty(currentMapId) && currentMapId.StartsWith("map-Q"))
        {
            LogVerbose($"[MapManager] 🔍 QuestId invalide ou manquant, tentative de récupération depuis l'API projects pour mapId: {currentMapId}");
            yield return StartCoroutine(LoadQuestIdFromProjectsApi(currentMapId));
            questId = PlayerPrefs.GetInt("CurrentQuestId", 0);
            LogVerbose($"[MapManager] 🔍 QuestId récupéré depuis l'API: {questId}");
        }
        
        // Vérifier que tous les prérequis sont disponibles pour charger depuis l'API
        if (questId <= 0)
        {
            Debug.LogError($"[MapManager] ❌ ERREUR : QuestId non disponible ({questId}). Impossible de charger la configuration depuis l'API.");
            yield break;
        }
        
        if (GeneralConfigManager.Instance == null)
        {
            Debug.LogError("[MapManager] ❌ ERREUR : GeneralConfigManager non disponible. Impossible de charger la configuration depuis l'API.");
            yield break;
        }
        
        if (UserDataManager.Instance == null || !hasToken)
        {
            Debug.LogError("[MapManager] ❌ ERREUR : Utilisateur non connecté ou token manquant. Impossible de charger la configuration depuis l'API.");
            yield break;
        }
        
        // Charger depuis l'API
        LogVerbose($"[MapManager] 🌐 Tentative de chargement depuis l'API avec questId: {questId}");
        yield return StartCoroutine(LoadMapConfigurationFromApi(questId));
        
        // Vérifier que la configuration a été chargée
        if (mapConfig == null)
        {
            Debug.LogError("[MapManager] ❌ ERREUR : Impossible de charger la configuration depuis l'API");
            yield break;
        }
        
        LogVerbose($"[MapManager] ✅ Configuration chargée depuis l'API avec succès - {mapConfig.steps?.Count ?? 0} steps");
        
        // DIAGNOSTIC : Afficher tous les steps
        for (int i = 0; i < mapConfig.steps.Count; i++)
        {
            MapStep step = mapConfig.steps[i];
            LogVerbose($"[MapManager] Step {i}: {step.id} - Type: {step.type} - NextStep: {step.nextStepId}");
        }

        // LOGIQUE SIMPLIFIÉE ET CORRIGÉE
        LogVerbose($"[MapManager] DEBUG - currentStepId initial='{currentStepId}', steps.Count={mapConfig.steps.Count}");

        if (mapConfig.steps.Count == 0)
        {
            Debug.LogError("[MapManager] Aucun step dans la config !");
            yield break;
        }

        MapStep firstStep = mapConfig.steps[0];
        string nextStepId = PlayerPrefs.GetString("NextStepId_" + currentMapId, "");
        string savedCurrentStepId = PlayerPrefs.GetString("CurrentStepId_" + currentMapId, "");
        bool introWatched = PlayerPrefs.GetInt("IntroWatched_" + currentMapId, 0) == 1;
        int lastCompletedGame = PlayerPrefs.GetInt("LastCompletedGame_" + currentMapId, 0);
        bool launchedFromPreview = PlayerPrefs.GetInt("LaunchedFromPreview_" + currentMapId, 0) == 1;
        
        LogVerbose($"[MapManager] Premier step: id='{firstStep.id}', type='{firstStep.type}'");
        LogVerbose($"[MapManager] NextStepId sauvegardé: '{nextStepId}'");
        LogVerbose($"[MapManager] CurrentStepId sauvegardé: '{savedCurrentStepId}'");
        LogVerbose($"[MapManager] IntroWatched: {introWatched}");
        LogVerbose($"[MapManager] LastCompletedGame: {lastCompletedGame}");
        LogVerbose($"[MapManager] LaunchedFromPreview: {launchedFromPreview}");

        // CAS 1: Lancement depuis le panel de prévisualisation → Utiliser EXCLUSIVEMENT last_completed_game
        // C'est la source de vérité de l'API pour savoir où reprendre
        if (launchedFromPreview)
        {
            // Nettoyer le flag immédiatement
            PlayerPrefs.DeleteKey("LaunchedFromPreview_" + currentMapId);
            PlayerPrefs.DeleteKey("LastCompletedGame_" + currentMapId);
            
            if (lastCompletedGame > 0)
            {
                // On a un dernier jeu terminé → Se positionner sur le step SUIVANT
                string resumeStepId = FindStepIdAfterGame(lastCompletedGame);
                if (!string.IsNullOrEmpty(resumeStepId))
                {
                    currentStepId = resumeStepId;
                    LogVerbose($"[MapManager] ✅ CAS 1a: Reprise depuis panel - Après jeu {lastCompletedGame} → Step: {currentStepId}");
                }
                else
                {
                    // Le gameId n'a pas été trouvé, commencer au début (après intro si vue)
                    currentStepId = GetFirstPlayableStepId(firstStep, introWatched);
                    Debug.LogWarning($"[MapManager] ⚠️ CAS 1a fallback: gameId {lastCompletedGame} non trouvé → Step: {currentStepId}");
                }
            }
            else
            {
                // Pas de progression (last_completed_game == 0) → Commencer au début
                currentStepId = GetFirstPlayableStepId(firstStep, introWatched);
                LogVerbose($"[MapManager] ✅ CAS 1b: Lancement depuis panel sans progression → Step: {currentStepId}");
            }
            
            PlayerPrefs.SetString("CurrentStepId_" + currentMapId, currentStepId);
            PlayerPrefs.Save();
        }
        // CAS 2: On a un NextStepId → On revient d'un niveau via MapManager.LaunchLevel
        else if (!string.IsNullOrEmpty(nextStepId))
        {
            currentStepId = nextStepId;
            PlayerPrefs.SetString("CurrentStepId_" + currentMapId, currentStepId);
            PlayerPrefs.DeleteKey("NextStepId_" + currentMapId);
            PlayerPrefs.Save();
            LogVerbose($"[MapManager] ✅ CAS 2: Retour de niveau (via NextStepId) - Passage au step: {currentStepId}");
        }
        // CAS 3: On a un CurrentStepId sauvegardé (navigation interne sur la map)
        else if (!string.IsNullOrEmpty(savedCurrentStepId))
        {
            currentStepId = savedCurrentStepId;
            LogVerbose($"[MapManager] ✅ CAS 3: Reprise depuis CurrentStepId sauvegardé: {currentStepId}");
        }
        // CAS 4: Premier chargement de la map (intro jamais vue)
        else if (firstStep.type == "fullscreen_video" && !introWatched)
        {
            currentStepId = firstStep.id;
            PlayerPrefs.SetString("CurrentStepId_" + currentMapId, currentStepId);
            PlayerPrefs.Save();
            LogVerbose($"[MapManager] ✅ CAS 4: PREMIÈRE VISITE - Démarrage avec l'intro: {currentStepId}");
        }
        // CAS 5: Pas de step sauvegardé → Démarrer au début
        else
        {
            currentStepId = GetFirstPlayableStepId(firstStep, introWatched);
            PlayerPrefs.SetString("CurrentStepId_" + currentMapId, currentStepId);
            PlayerPrefs.Save();
            LogVerbose($"[MapManager] ✅ CAS 5: Démarrage au premier step jouable: {currentStepId}");
        }

        currentStep = mapConfig.steps.Find(s => s.id == currentStepId);
        LogVerbose($"[MapManager] currentStep trouvé: {currentStep?.id} - Type: {currentStep?.type}");

        // OPTIMISATION : Si le step actuel est une vidéo fullscreen, la lancer IMMÉDIATEMENT
        // pendant que les autres éléments se chargent en arrière-plan
        bool isVideoStep = currentStep != null && currentStep.type == "fullscreen_video";
        
        if (isVideoStep)
        {
            LogVerbose("[MapManager] 🎬 OPTIMISATION: Lancement de la vidéo EN PREMIER");
            
            // Masquer l'overlay de transition immédiatement
            if (IntroTransitionOverlay.Instance != null)
            {
                IntroTransitionOverlay.HideOverlay();
            }
            if (UnifiedLoadingManager.Instance != null)
            {
                UnifiedLoadingManager.HideLoading();
            }
            
            // Lancer la vidéo IMMÉDIATEMENT
            ShowFullscreenVideo();
            
            // Charger les autres éléments EN PARALLÈLE pendant que la vidéo joue
            StartCoroutine(LoadMapElementsInBackground());
        }
        else
        {
            // FONCTIONNEMENT CLASSIQUE : Créer la carte directement
            LogVerbose("[MapManager] Création de la carte directement");
            yield return StartCoroutine(LoadMapElementsSequential());
            LoadCurrentStep();
        }
        
        // Restaurer la position du scroll si elle existe
        ScrollRect scrollRect = scrollViewObject != null ? scrollViewObject.GetComponent<ScrollRect>() : null;
        if (scrollRect != null && scrollRect.content != null)
        {
            float savedScrollX = PlayerPrefs.GetFloat("ScrollPosX_" + currentMapId, 0);
            float savedScrollY = PlayerPrefs.GetFloat("ScrollPosY_" + currentMapId, 0);
            
            if (savedScrollX != 0 || savedScrollY != 0)
            {
                // Attendre une frame pour que le layout soit calculé
                yield return null;
                
                scrollRect.content.anchoredPosition = new Vector2(savedScrollX, savedScrollY);
                LogVerbose($"[MapManager] ✅ Position scroll restaurée: ({savedScrollX}, {savedScrollY})");
            }
            else
            {
                LogVerbose($"[MapManager] Aucune position scroll sauvegardée, démarrage à (0, 0)");
            }
        }

        // NE PAS masquer le loading ici - laisser le SceneReadyManager le gérer
        // L'écran de chargement sera masqué automatiquement quand tout sera prêt
        LogVerbose("[MapManager] ✅ Initialisation terminée - en attente de SceneReadyManager");
        
        // ANCIEN CODE (désactivé) :
        // if (UnifiedLoadingManager.Instance != null)
        // {
        //     UnifiedLoadingManager.HideLoadingAfterDelay(0.5f);
        // }
    }

    /// <summary>
    /// Charge les éléments de la map EN ARRIÈRE-PLAN pendant que la vidéo joue
    /// Quand la vidéo se termine, tout est déjà prêt !
    /// </summary>
    IEnumerator LoadMapElementsInBackground()
    {
        LogVerbose("[MapManager] 🔄 Chargement des éléments en arrière-plan (pendant la vidéo)...");
        
        // Charger le background
        yield return StartCoroutine(SetupBackground());
        LogVerbose("[MapManager] ✅ Background chargé (en arrière-plan)");

        // Utiliser HeaderManager avec la configuration depuis general-config.json
        DefaultHeaderConfig headerConfig = GeneralConfigManager.Instance.GetDefaultHeaderConfig();
        if (headerConfig != null && headerConfig.show)
        {
            Canvas canvas = GetMainCanvas();
            if (canvas != null)
            {
                yield return StartCoroutine(HeaderManager.Instance.CreateHeader(canvas, OnHeaderElementClick));
                LogVerbose("[MapManager] ✅ Header chargé (en arrière-plan)");
            }
        }

        // Créer les vidéos décoratives et le popup container
        CreateDecorativeVideos();
        CreatePopupContainer();
        
        LogVerbose("[MapManager] ✅ Tous les éléments chargés en arrière-plan - Prêt pour après la vidéo !");
        
        // Marquer comme prêt pour le SceneReadyManager
        backgroundElementsLoaded = true;
    }
    
    /// <summary>
    /// Chargement séquentiel classique (pour les steps non-vidéo)
    /// </summary>
    IEnumerator LoadMapElementsSequential()
    {
        yield return StartCoroutine(SetupBackground());

        // Utiliser HeaderManager avec la configuration depuis general-config.json
        DefaultHeaderConfig headerConfig = GeneralConfigManager.Instance.GetDefaultHeaderConfig();
        if (headerConfig != null && headerConfig.show)
        {
            Canvas canvas = GetMainCanvas();
            if (canvas != null)
            {
                yield return StartCoroutine(HeaderManager.Instance.CreateHeader(canvas, OnHeaderElementClick));
            }
        }

        CreateDecorativeVideos();
        CreatePopupContainer();
        
        // Masquer l'overlay de transition
        if (IntroTransitionOverlay.Instance != null)
        {
            IntroTransitionOverlay.HideOverlay();
        }
    }
    
    // Flag pour savoir si les éléments de fond sont chargés
    private bool backgroundElementsLoaded = false;
    
    void LoadCurrentStep()
    {
        LogVerbose($"[MapManager] ═══ LoadCurrentStep ═══");
        LogVerbose($"[MapManager] currentStepId au début: '{currentStepId}'");
        LogVerbose($"[MapManager] currentMapId: '{currentMapId}'");
        
        if (mapConfig == null || mapConfig.steps == null || mapConfig.steps.Count == 0)
        {
            Debug.LogError("[MapManager] Aucun step défini!");
            return;
        }

        if (string.IsNullOrEmpty(currentStepId))
        {
            Debug.LogWarning("[MapManager] currentStepId VIDE - utilisation du premier step");
            currentStepId = mapConfig.steps[0].id;
            SaveCurrentStep();
        }

        currentStep = mapConfig.steps.Find(s => s.id == currentStepId);

        if (currentStep == null)
        {
            Debug.LogError($"[MapManager] ❌ Step introuvable: '{currentStepId}'");
            Debug.LogError($"[MapManager] Steps disponibles:");
            foreach (var step in mapConfig.steps)
            {
                Debug.LogError($"  - {step.id} ({step.type})");
            }
            currentStep = mapConfig.steps[0];
            currentStepId = currentStep.id;
            SaveCurrentStep();
        }

        LogVerbose($"[MapManager] ✅ Step actuel: {currentStep.id} ({currentStep.type})");
        LogVerbose($"[MapManager] Zones dans ce step: {currentStep.zones?.Count ?? 0}");
        LogVerbose($"[MapManager] Images décoratives: {currentStep.decorativeImages?.Count ?? 0}");

        // Mettre à jour la jauge de zénitude du header selon l'avancement
        if (HeaderManager.Instance != null)
        {
            HeaderManager.Instance.RefreshMapJauge();
        }

        if (currentStep.type == "fullscreen_video")
        {
            ShowFullscreenVideo();
        }
        else if (currentStep.type == "zones")
        {
            ShowZones();
        }
        
        LogVerbose($"[MapManager] ═══ Fin LoadCurrentStep ═══");
    }

    /// <summary>
    /// Retourne le numéro du step actuel (sans compter l'intro), pour afficher la fleur appropriée
    /// </summary>
    public int GetCurrentStepNumber()
    {
        if (mapConfig == null || mapConfig.steps == null || mapConfig.steps.Count == 0)
        {
            Debug.LogWarning("[MapManager] Impossible de calculer le numéro de step : mapConfig ou steps invalides");
            return 1;
        }

        if (string.IsNullOrEmpty(currentStepId))
        {
            Debug.LogWarning("[MapManager] Impossible de calculer le numéro de step : currentStepId vide");
            return 1;
        }

        // Compter uniquement les steps de type "zones" (ignore intro et fullscreen_video)
        List<MapStep> zoneSteps = mapConfig.steps.FindAll(s => s != null && s.type == "zones");
        if (zoneSteps == null || zoneSteps.Count == 0)
        {
            Debug.LogWarning("[MapManager] Impossible de calculer le numéro de step : aucun step de type 'zones'");
            return 1;
        }

        // Trouver l'index du step courant dans la liste des steps "zones"
        int currentZoneIndex = zoneSteps.FindIndex(s => s.id == currentStepId);
        if (currentZoneIndex < 0)
        {
            // Si on est sur un step non-zones, prendre le dernier step zones atteint
            int globalIndex = mapConfig.steps.FindIndex(s => s != null && s.id == currentStepId);
            if (globalIndex < 0)
            {
                Debug.LogWarning($"[MapManager] Impossible de trouver le step actuel: {currentStepId}");
                return 1;
            }

            currentZoneIndex = 0;
            for (int i = 0; i < zoneSteps.Count; i++)
            {
                int zi = mapConfig.steps.FindIndex(s => s != null && s.id == zoneSteps[i].id);
                if (zi <= globalIndex) currentZoneIndex = i;
                else break;
            }
        }

        // Retourner l'index + 1 (car on commence à 1, pas 0)
        return currentZoneIndex + 1;
    }
    
    /// <summary>
    /// Calcule la progression dans la quête (0.0 à 1.0) et retourne la phase de jauge appropriée (1, 2 ou 3)
    /// Retourne 0 si la progression ne peut pas être calculée
    /// 🔧 NOTE : Cette méthode est maintenant obsolète, remplacée par GetCurrentStepNumber() pour les fleurs
    /// </summary>
    public int GetJaugePhase()
    {
        if (mapConfig == null || mapConfig.steps == null || mapConfig.steps.Count == 0)
        {
            Debug.LogWarning("[MapManager] Impossible de calculer la phase : mapConfig ou steps invalides");
            return 0;
        }

        if (string.IsNullOrEmpty(currentStepId))
        {
            Debug.LogWarning("[MapManager] Impossible de calculer la phase : currentStepId vide");
            return 0;
        }

        // IMPORTANT: la jauge doit ignorer les steps non jouables (ex: fullscreen_video)
        // et ne compter que les steps de type "zones".
        List<MapStep> zoneSteps = mapConfig.steps.FindAll(s => s != null && s.type == "zones");
        if (zoneSteps == null || zoneSteps.Count == 0)
        {
            Debug.LogWarning("[MapManager] Impossible de calculer la phase : aucun step de type 'zones'");
            return 0;
        }

        // Trouver l'index du step courant dans la liste des steps "zones"
        int currentZoneIndex = zoneSteps.FindIndex(s => s.id == currentStepId);
        if (currentZoneIndex < 0)
        {
            // Si on est sur un step non-zones, prendre le dernier step zones atteint (selon l'ordre global)
            int globalIndex = mapConfig.steps.FindIndex(s => s != null && s.id == currentStepId);
            if (globalIndex < 0)
            {
                Debug.LogWarning($"[MapManager] Impossible de trouver le step actuel: {currentStepId}");
                return 0;
            }

            currentZoneIndex = 0;
            for (int i = 0; i < zoneSteps.Count; i++)
            {
                int zi = mapConfig.steps.FindIndex(s => s != null && s.id == zoneSteps[i].id);
                if (zi <= globalIndex) currentZoneIndex = i;
                else break;
            }
        }

        // Calculer la progression (0.0 à 1.0) sur les steps "zones" uniquement
        float progression = (float)(currentZoneIndex + 1) / zoneSteps.Count;

        // Déterminer la phase selon la progression
        // 30% premiers steps → phase 1
        // 30% steps suivants → phase 2
        // 40% derniers steps → phase 3
        if (progression <= 0.30f)
        {
            return 1;
        }
        else if (progression <= 0.60f) // 30% + 30% = 60%
        {
            return 2;
        }
        else
        {
            return 3;
        }
    }

    void ShowFullscreenVideo()
    {
        LogVerbose($"[MapManager] ========================================");
        LogVerbose($"[MapManager] DEBUT ShowFullscreenVideo()");
        LogVerbose($"[MapManager] ========================================");
        LogVerbose($"[MapManager] Step: {currentStep.id}");
        LogVerbose($"[MapManager] Video URL originale: {currentStep.videoUrl}");
        
        Canvas canvas = GetMainCanvas();
        if (canvas == null)
        {
            Debug.LogError("[MapManager] Aucun Canvas trouvé pour afficher la vidéo !");
            return;
        }
        LogVerbose($"[MapManager] Canvas trouvé: {canvas.name}");

        // Masquer le header pendant la vidéo plein écran
        if (HeaderManager.Instance != null)
        {
            HeaderManager.Instance.HideHeader();
            LogVerbose("[MapManager] Header masqué pour la vidéo plein écran");
        }

        GameObject videoObj = new GameObject("FullscreenVideo");
        videoObj.transform.SetParent(canvas.transform, false);
        videoObj.transform.SetAsLastSibling();

        RectTransform videoRect = videoObj.AddComponent<RectTransform>();
        videoRect.anchorMin = Vector2.zero;
        videoRect.anchorMax = Vector2.one;
        videoRect.offsetMin = Vector2.zero;
        videoRect.offsetMax = Vector2.zero;

        // 🔧 NORMALISATION : Passer par GetVideoUrl pour normaliser l'URL (ajoute underscore si manquant)
        // Ex: https://.../.../Q07intro.mp4 → https://.../.../Q07_intro.mp4
        string fullVideoUrl = GeneralConfigManager.Instance.GetVideoUrl(currentStep.videoUrl);
        LogVerbose($"[MapManager] Video URL complète (normalisée): {fullVideoUrl}");
        
        MapStep stepWithFullUrl = new MapStep
        {
            id = currentStep.id,
            type = currentStep.type,
            nextStepId = currentStep.nextStepId,
            videoUrl = fullVideoUrl,
            skipable = currentStep.skipable,
            autoAdvance = currentStep.autoAdvance,
            zones = currentStep.zones,
            decorativeImages = currentStep.decorativeImages
        };

        LogVerbose($"[MapManager] Ajout du composant MapFullscreenVideo...");
        MapFullscreenVideo fullscreenVideo = videoObj.AddComponent<MapFullscreenVideo>();
        LogVerbose($"[MapManager] Appel de Initialize()...");
        
        isFullscreenVideoPlaying = true; // Bloquer les clics globaux pendant la vidéo
        
        // Masquer le header pendant la vidéo plein écran
        if (HeaderManager.Instance != null)
        {
            HeaderManager.Instance.HideHeader();
        }
        
        fullscreenVideo.Initialize(stepWithFullUrl, OnFullscreenVideoComplete);
        LogVerbose($"[MapManager] ShowFullscreenVideo() terminé");
        LogVerbose($"[MapManager] ========================================");
    }

    void OnFullscreenVideoComplete()
    {
        LogVerbose($"[MapManager] OnFullscreenVideoComplete - Step actuel: {currentStep.id}, NextStepId: {currentStep.nextStepId}");

        isFullscreenVideoPlaying = false; // Réactiver les clics globaux
        
        // Réafficher le header après la vidéo plein écran
        if (HeaderManager.Instance != null)
        {
            HeaderManager.Instance.ShowHeader();
        }
        
        // Si c'est l'intro, la marquer comme vue
        if (currentStep.id == "intro" || currentStep.id.Contains("intro"))
        {
            PlayerPrefs.SetInt("IntroWatched_" + currentMapId, 1);
            PlayerPrefs.Save();
            LogVerbose($"[MapManager] ✅ Intro marquée comme vue pour la map {currentMapId}");
        }

        // Attendre que les éléments de fond soient chargés avant de continuer
        StartCoroutine(WaitForBackgroundAndContinue());
    }
    
    /// <summary>
    /// Attend que les éléments de fond soient chargés puis passe au step suivant
    /// </summary>
    IEnumerator WaitForBackgroundAndContinue()
    {
        // Si les éléments ne sont pas encore chargés, attendre
        if (!backgroundElementsLoaded)
        {
            LogVerbose("[MapManager] ⏳ Vidéo terminée - Attente des éléments en arrière-plan...");
            
            float timeout = 10f;
            float elapsed = 0f;
            
            while (!backgroundElementsLoaded && elapsed < timeout)
            {
                elapsed += Time.deltaTime;
                yield return null;
            }
            
            if (!backgroundElementsLoaded)
            {
                Debug.LogWarning("[MapManager] ⚠️ Timeout - Les éléments n'ont pas fini de charger, on continue quand même");
            }
        }
        
        LogVerbose("[MapManager] ✅ Éléments chargés - Passage au step suivant INSTANTANÉ !");

        if (!string.IsNullOrEmpty(currentStep.nextStepId))
        {
            LogVerbose("[MapManager] Passage au step suivant");
            GoToNextStep(currentStep.nextStepId);
        }
        else
        {
            LogVerbose("[MapManager] Dernier step atteint (vidéo outro terminée) - retour scène Main");
            
            // Utiliser UnifiedLoadingManager si disponible pour une meilleure transition
            if (UnifiedLoadingManager.Instance != null)
            {
                UnifiedLoadingManager.LoadMenuScene("main");
            }
            else
            {
                SceneManager.LoadScene("main");
            }
        }
    }


    void ShowZones()
    {
        LogVerbose($"[MapManager] ═══ ShowZones ═══");
        LogVerbose($"[MapManager] currentStep: {currentStep?.id}");
        LogVerbose($"[MapManager] Zones à afficher: {currentStep?.zones?.Count ?? 0}");
        LogVerbose($"[MapManager] backgroundObject null? {backgroundObject == null}, actif? {backgroundObject?.activeInHierarchy}");
        
        ClearZones();
        CreateClickableZones();
        CreateStepDecorativeImages(); // Charger les images décoratives du step
        
        LogVerbose($"[MapManager] ✅ ShowZones terminé - {zones.Count} zones créées, {decorators.Count} decorators dans la liste");
        LogVerbose($"[MapManager] ═══ Fin ShowZones ═══");
    }

    void ClearZones()
    {
        foreach (MapZoneClickable zone in zones)
        {
            if (zone != null)
                Destroy(zone.gameObject);
        }
        zones.Clear();

        foreach (GameObject decorator in decorators)
        {
            if (decorator != null)
                Destroy(decorator);
        }
        decorators.Clear();

        // Nettoyer les images décoratives du step
        foreach (GameObject decorImage in stepDecorativeImages)
        {
            if (decorImage != null)
                Destroy(decorImage);
        }
        stepDecorativeImages.Clear();
    }

    public void GoToNextStep(string nextStepId)
    {
        if (string.IsNullOrEmpty(nextStepId))
        {
            Debug.LogError("[MapManager] nextStepId vide!");
            return;
        }

        currentStepId = nextStepId;
        SaveCurrentStep();
        LoadCurrentStep();
    }

    void SaveCurrentStep()
    {
        PlayerPrefs.SetString("CurrentStepId_" + currentMapId, currentStepId);
        PlayerPrefs.Save();
        LogVerbose($"[MapManager] Step sauvegardé: {currentStepId}");
    }
    
    /// <summary>
    /// Retourne l'ID du premier step jouable (après l'intro si elle a déjà été vue)
    /// </summary>
    string GetFirstPlayableStepId(MapStep firstStep, bool introWatched)
    {
        // Si l'intro a déjà été vue et que le premier step est une vidéo, passer au suivant
        if (introWatched && firstStep.type == "fullscreen_video" && !string.IsNullOrEmpty(firstStep.nextStepId))
        {
            return firstStep.nextStepId;
        }
        return firstStep.id;
    }
    
    /// <summary>
    /// Trouve le step suivant celui qui contient le jeu avec le gameId spécifié
    /// Utilisé pour reprendre une quête après le dernier jeu terminé
    /// </summary>
    /// <param name="gameId">ID du dernier jeu terminé</param>
    /// <returns>ID du step suivant, ou null si non trouvé</returns>
    string FindStepIdAfterGame(int gameId)
    {
        if (mapConfig == null || mapConfig.steps == null || gameId <= 0)
        {
            return null;
        }
        
        foreach (var step in mapConfig.steps)
        {
            // Vérifier seulement les steps de type "zones"
            if (step.type != "zones" || step.zones == null)
            {
                continue;
            }
            
            // Chercher si une des zones contient ce gameId
            foreach (var zone in step.zones)
            {
                if (zone.gameId == gameId)
                {
                    LogVerbose($"[MapManager] 🎯 Jeu {gameId} trouvé dans step '{step.id}' (zone: {zone.id})");
                    
                    // Si ce step a un nextStepId, le retourner
                    if (!string.IsNullOrEmpty(step.nextStepId))
                    {
                        LogVerbose($"[MapManager] ➡️ Step suivant après gameId {gameId}: '{step.nextStepId}'");
                        return step.nextStepId;
                    }
                    else
                    {
                        LogVerbose($"[MapManager] ⚠️ Step '{step.id}' n'a pas de nextStepId - c'est le dernier step");
                        // C'est le dernier step, on reste dessus
                        return step.id;
                    }
                }
            }
        }
        
        Debug.LogWarning($"[MapManager] ⚠️ gameId {gameId} non trouvé dans aucun step");
        return null;
    }

    IEnumerator SetupBackground()
    {
        Canvas canvas = GetMainCanvas();
        if (canvas == null)
        {
            Debug.LogError("[MapManager] Canvas principal introuvable!");
            // 🖤 Masquer l'overlay même en cas d'erreur
            HideImmediateBlackOverlay();
            yield break;
        }

        LogVerbose($"[MapManager] SetupBackground - Canvas trouvé: {canvas.gameObject.name}");

        scrollViewObject = new GameObject("ScrollView");
        scrollViewObject.transform.SetParent(canvas.transform, false);
        scrollViewObject.transform.SetAsFirstSibling();

        // Calculer headerHeight pour le log (depuis general-config.json maintenant)
        DefaultHeaderConfig headerConfig = GeneralConfigManager.Instance.GetDefaultHeaderConfig();
        float headerHeight = (headerConfig != null && headerConfig.show) ? headerConfig.height : 0f;

        RectTransform scrollRect = scrollViewObject.AddComponent<RectTransform>();
        scrollRect.anchorMin = Vector2.zero;
        scrollRect.anchorMax = Vector2.one;
        scrollRect.offsetMin = Vector2.zero;

        // PAS d'offset pour le header - le ScrollView fait toute la hauteur de l'écran
        // Le header sera par-dessus en overlay
        scrollRect.offsetMax = Vector2.zero;  // Pas de -headerHeight
        scrollRect.anchoredPosition = Vector2.zero;
        scrollRect.sizeDelta = Vector2.zero;

        LogVerbose($"[MapManager] SetupBackground - ScrollView créé (plein écran), headerHeight (overlay): {headerHeight}");

        viewportObject = new GameObject("Viewport");
        viewportObject.transform.SetParent(scrollViewObject.transform, false);

        RectTransform viewportRect = viewportObject.AddComponent<RectTransform>();
        viewportRect.anchorMin = Vector2.zero;
        viewportRect.anchorMax = Vector2.one;
        viewportRect.pivot = new Vector2(0.5f, 0.5f);
        viewportRect.anchoredPosition = Vector2.zero;
        viewportRect.sizeDelta = Vector2.zero;
        viewportRect.offsetMin = Vector2.zero;
        viewportRect.offsetMax = Vector2.zero;

        Image viewportImage = viewportObject.AddComponent<Image>();
        viewportImage.color = Color.clear; // 🖤 TRANSPARENT au lieu de blanc pour éviter le flash blanc
        // En WebGL, activer raycastTarget pour permettre le scroll avec la molette
        #if UNITY_WEBGL && !UNITY_EDITOR
        viewportImage.raycastTarget = true; // Permet le scroll en WebGL
        #else
        viewportImage.raycastTarget = false;
        #endif

        // NOTE: PAS de Mask pour permettre aux decorators (sprites des personnages) 
        // d'être toujours visibles. Si on a besoin de masquer le contenu hors viewport,
        // il faudrait utiliser une autre approche qui ne masque pas les decorators.

        contentObject = new GameObject("Content");
        contentObject.transform.SetParent(viewportObject.transform, false);

        RectTransform contentRect = contentObject.AddComponent<RectTransform>();
        // Content doit être ancré en haut à gauche avec une taille fixe
        contentRect.anchorMin = new Vector2(0, 1);  // Haut gauche
        contentRect.anchorMax = new Vector2(0, 1);  // Haut gauche
        contentRect.pivot = new Vector2(0, 1);      // Pivot en haut gauche
        contentRect.anchoredPosition = Vector2.zero;
        // La taille sera définie dans LoadBackgroundImageScrollable

        LogVerbose("[MapManager] SetupBackground - Structure créée, chargement image...");

        if (mapConfig.background.type == "image")
        {
            yield return StartCoroutine(LoadBackgroundImageScrollable(contentRect));
        }

        LogVerbose("[MapManager] SetupBackground - Image chargée, ajout ScrollRect...");

        ScrollRect scrollRectComponent = scrollViewObject.AddComponent<ScrollRect>();
        scrollRectComponent.content = contentRect;
        scrollRectComponent.viewport = viewportRect;
        scrollRectComponent.horizontal = true;  // Activer le scroll horizontal
        scrollRectComponent.vertical = false;
        scrollRectComponent.movementType = ScrollRect.MovementType.Clamped;
        scrollRectComponent.scrollSensitivity = 20f; // Sensibilité du scroll (molette de souris)
        scrollRectComponent.enabled = true;
        
        // En WebGL, s'assurer que le ScrollRect peut recevoir les événements de scroll et drag
        #if UNITY_WEBGL && !UNITY_EDITOR
        // Le ScrollRect natif de Unity devrait gérer automatiquement la molette de souris et le drag en WebGL
        // Mais on s'assure que tout est activé et que le viewport peut recevoir les raycasts
        scrollRectComponent.enabled = true;
        
        // **FIX WEBGL : Désactiver l'inertia pour éviter le scroll automatique après avoir lâché**
        scrollRectComponent.inertia = false; // Désactive l'effet de momentum
        LogVerbose("[MapManager] ⚠️ Inertia désactivée pour WebGL - scroll s'arrête immédiatement");
        
        // S'assurer que le viewport peut recevoir les raycasts pour le drag
        if (viewportImage != null)
        {
            viewportImage.raycastTarget = true; // Permet le drag en WebGL
        }
        LogVerbose("[MapManager] ScrollRect configuré pour WebGL - horizontal: true, scrollSensitivity: 20, drag activé, inertia désactivée");
        #endif

        LogVerbose("[MapManager] SetupBackground - TERMINÉ");
        
        // 🖤 Masquer l'overlay noir immédiat maintenant que le background est chargé
        HideImmediateBlackOverlay();
    }

    IEnumerator LoadBackgroundImageScrollable(RectTransform contentRect)
    {
        // Construire l'URL complète avec GeneralConfigManager
        string fullUrl = mapConfig.background.type == "image" 
            ? GeneralConfigManager.Instance.GetBackgroundImageUrl(mapConfig.background.url)
            : GeneralConfigManager.Instance.GetBackgroundVideoUrl(mapConfig.background.url);
            
        LogVerbose($"[MapManager] LoadBackgroundImageScrollable - URL: {fullUrl}");

        using (UnityWebRequest www = UnityWebRequestTexture.GetTexture(fullUrl))
        {
            yield return www.SendWebRequest();

            if (www.result == UnityWebRequest.Result.Success)
            {
                // Déclarer les variables avant le try-catch pour pouvoir les utiliser après
                DefaultHeaderConfig headerConfig = GeneralConfigManager.Instance.GetDefaultHeaderConfig();
                float headerHeight = (headerConfig != null && headerConfig.show) ? headerConfig.height : 0f;
                
                // CORRECTION WebGL : Utiliser la résolution de référence du CanvasScaler au lieu de Screen.height
                // En WebGL, Screen.height peut être différent de la résolution de référence à cause du scaling
                float fullScreenHeight = 1080f;  // Résolution de référence du CanvasScaler
                
                // Si on peut récupérer le CanvasScaler, utiliser sa résolution de référence
                Canvas canvas = GetMainCanvas();
                if (canvas != null)
                {
                    CanvasScaler scaler = canvas.GetComponent<CanvasScaler>();
                    if (scaler != null && scaler.uiScaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize)
                    {
                        fullScreenHeight = scaler.referenceResolution.y;
                        LogVerbose($"[MapManager] Utilisation de la résolution de référence du CanvasScaler: {fullScreenHeight}px");
                    }
                }
                
                float imageAspect = 1f;
                float calculatedWidth = 0f;
                Texture2D loadedTexture = null;
                
                try
                {
                    loadedTexture = ((DownloadHandlerTexture)www.downloadHandler).texture;
                    LogVerbose($"[MapManager] Image chargée: {loadedTexture.width}x{loadedTexture.height}");

                    if (contentObject == null)
                    {
                        Debug.LogError("[MapManager] contentObject est NULL !");
                        yield break;
                    }

                    backgroundObject = new GameObject("MapBackground");
                    backgroundObject.transform.SetParent(contentObject.transform, false);
                    LogVerbose("[MapManager] MapBackground créé");

                    RectTransform bgRect = backgroundObject.AddComponent<RectTransform>();
                    // Ancrer en haut à gauche avec une taille FIXE
                    bgRect.anchorMin = new Vector2(0, 1);  // Haut gauche
                    bgRect.anchorMax = new Vector2(0, 1);  // Haut gauche
                    bgRect.pivot = new Vector2(0, 1);      // Pivot en haut gauche
                    bgRect.anchoredPosition = Vector2.zero;
                    LogVerbose("[MapManager] RectTransform configuré avec ancrage fixe en haut à gauche");

                    // Calculer les dimensions
                    imageAspect = (float)loadedTexture.width / loadedTexture.height;
                    calculatedWidth = fullScreenHeight * imageAspect;  // 1080 × 2.666667 = 2880

                    LogVerbose($"[MapManager] Calculs - fullScreenHeight: {fullScreenHeight}, imageAspect: {imageAspect}, calculatedWidth: {calculatedWidth}");
                    LogVerbose($"[MapManager] Dimensions image originale: {loadedTexture.width}x{loadedTexture.height}");
                    LogVerbose($"[MapManager] Dimensions calculées pour l'affichage: {calculatedWidth}x{fullScreenHeight}");

                    // Définir une taille FIXE pour l'image
                    bgRect.sizeDelta = new Vector2(calculatedWidth, fullScreenHeight);
                    LogVerbose($"[MapManager] bgRect.sizeDelta défini: {calculatedWidth}x{fullScreenHeight}");

                    RawImage rawImage = backgroundObject.AddComponent<RawImage>();
                    rawImage.texture = loadedTexture;
                    rawImage.raycastTarget = false;
                    rawImage.color = Color.white;
                    rawImage.uvRect = new Rect(0, 0, 1, 1);
                    
                    LogVerbose($"[MapManager] RawImage configurée sans AspectRatioFitter (taille fixe)");
                    
                    // Content doit avoir la largeur calculée pour permettre le scroll horizontal
                    // et la hauteur de l'écran complet
                    contentRect.sizeDelta = new Vector2(calculatedWidth, fullScreenHeight);
                    LogVerbose($"[MapManager] Tailles appliquées - bgRect: {calculatedWidth}x{fullScreenHeight}, contentRect: {calculatedWidth}x{fullScreenHeight}");
                }
                catch (System.Exception e)
                {
                    Debug.LogError($"[MapManager] ERREUR dans LoadBackgroundImageScrollable: {e.Message}");
                    Debug.LogError($"[MapManager] Stack trace: {e.StackTrace}");
                    yield break;
                }
                
                // Attendre une frame pour que le layout soit calculé
                yield return null;
                
                // Vérifier les dimensions réelles après le layout
                if (backgroundObject != null)
                {
                    RectTransform bgRect = backgroundObject.GetComponent<RectTransform>();
                    if (bgRect != null)
                    {
                        float actualWidth = bgRect.rect.width;
                        float actualHeight = bgRect.rect.height;
                        float actualAspect = actualWidth / actualHeight;
                        
                        LogVerbose($"[MapManager] ⚠️ DIMENSIONS RÉELLES après layout - width: {actualWidth}, height: {actualHeight}, actualAspect: {actualAspect:F6} (attendu: {imageAspect:F6})");
                        LogVerbose($"[MapManager] Attendu: {calculatedWidth}x{fullScreenHeight} = {imageAspect:F6}");
                        
                        if (Mathf.Abs(actualAspect - imageAspect) > 0.01f)
                        {
                            Debug.LogWarning($"[MapManager] ⚠️ PROBLÈME DE RATIO - l'image sera déformée ! Ratio actuel: {actualAspect:F6}, ratio attendu: {imageAspect:F6}");
                        }
                    }
                }
                
                // Utiliser les variables déjà calculées pour CreateScrollDragArea
                // Si l'image n'a pas été chargée, utiliser des valeurs par défaut
                if (calculatedWidth == 0f && backgroundObject != null)
                {
                    RawImage rawImg = backgroundObject.GetComponent<RawImage>();
                    if (rawImg != null && rawImg.texture != null)
                    {
                        Texture2D tex = rawImg.texture as Texture2D;
                        if (tex != null)
                        {
                            imageAspect = (float)tex.width / tex.height;
                            calculatedWidth = fullScreenHeight * imageAspect;
                        }
                    }
                }
                
                CreateScrollDragArea(calculatedWidth, fullScreenHeight);

                LogVerbose("[MapManager] LoadBackgroundImageScrollable - TERMINÉ");
            }
            else
            {
                Debug.LogError($"[MapManager] Erreur chargement image: {www.error}");
            }
        }
    }

    void CreateScrollDragArea(float width, float height)
    {
        if (contentObject == null) return;

        GameObject dragArea = new GameObject("ScrollDragArea");
        dragArea.transform.SetParent(contentObject.transform, false);

        RectTransform dragRect = dragArea.AddComponent<RectTransform>();
        dragRect.anchorMin = Vector2.zero;
        dragRect.anchorMax = Vector2.zero;
        dragRect.pivot = new Vector2(0, 0.5f);
        dragRect.anchoredPosition = Vector2.zero;
        dragRect.sizeDelta = new Vector2(width, height);

        Image dragImage = dragArea.AddComponent<Image>();
        dragImage.color = new Color(0, 0, 0, 0);
        dragImage.raycastTarget = true;
    }

    // NOTE: CreateHeader() et CreateHeaderElement() supprimés - maintenant géré par HeaderManager

    void CreateClickableZones()
    {
        if (backgroundObject == null || currentStep == null || currentStep.zones == null)
        {
            Debug.LogError("[MapManager] Impossible de créer les zones");
            return;
        }

        LogVerbose($"[MapManager] CreateClickableZones - {currentStep.zones.Count} zones à créer");

        GameObject zonesContainerObj = new GameObject("ZonesContainer");
        zonesContainerObj.transform.SetParent(backgroundObject.transform, false);

        RectTransform zonesRect = zonesContainerObj.AddComponent<RectTransform>();
        zonesRect.anchorMin = Vector2.zero;
        zonesRect.anchorMax = Vector2.one;
        zonesRect.pivot = new Vector2(0, 0);
        zonesRect.anchoredPosition = Vector2.zero;
        zonesRect.offsetMin = Vector2.zero;
        zonesRect.offsetMax = Vector2.zero;

        RawImage rawImage = backgroundObject.GetComponent<RawImage>();
        Texture2D texture = rawImage.texture as Texture2D;
        RectTransform bgRect = backgroundObject.GetComponent<RectTransform>();

        float scaleX = bgRect.sizeDelta.x / texture.width;
        float scaleY = bgRect.sizeDelta.y / texture.height;  // Utiliser la hauteur réelle de l'image

        LogVerbose($"[MapManager] CreateClickableZones - bgRect: {bgRect.sizeDelta.x}x{bgRect.sizeDelta.y}, texture: {texture.width}x{texture.height}, scaleX: {scaleX}, scaleY: {scaleY}");

        Texture2D whiteTex = new Texture2D(1, 1);
        whiteTex.SetPixel(0, 0, Color.white);
        whiteTex.Apply();
        dynamicTextures.Add(whiteTex);
        Sprite whiteSprite = Sprite.Create(whiteTex, new Rect(0, 0, 1, 1), new Vector2(0.5f, 0.5f));
        dynamicSprites.Add(whiteSprite);

        foreach (MapZone zoneData in currentStep.zones)
        {
            GameObject zoneObj = new GameObject($"Zone_{zoneData.id}");
            zoneObj.transform.SetParent(zonesContainerObj.transform, false);

            RectTransform rt = zoneObj.AddComponent<RectTransform>();
            rt.anchorMin = Vector2.zero;
            rt.anchorMax = Vector2.zero;
            rt.pivot = new Vector2(0.5f, 0.5f);

            Vector2 scaledPos = new Vector2(zoneData.position.x * scaleX, zoneData.position.y * scaleY);
            Vector2 scaledSize = new Vector2(zoneData.size.x * scaleX, zoneData.size.y * scaleY);

            rt.anchoredPosition = scaledPos;
            rt.sizeDelta = scaledSize;

            Image img = zoneObj.AddComponent<Image>();
            img.sprite = whiteSprite;
            img.raycastTarget = true;
            img.enabled = showDebugZones;

            if (ColorUtility.TryParseHtmlString(zoneData.debugColor, out Color debugColor))
            {
                img.color = debugColor;
            }

            MapZoneClickable clickable = zoneObj.AddComponent<MapZoneClickable>();
            clickable.Initialize(zoneData, this);

            zones.Add(clickable);

            // Créer le decorator uniquement si decoratorImageUrl est défini ET decoratorInvisible est false
            if (!string.IsNullOrEmpty(zoneData.decoratorImageUrl) && !zoneData.decoratorInvisible)
            {
                StartCoroutine(CreateZoneDecoratorWithImage(zoneData, zonesContainerObj.transform, scaleX, scaleY));
            }
        }

        LogVerbose($"[MapManager] {zones.Count} zones créées pour step {currentStep.id}");
        LogVerbose($"[MapManager] ZonesContainer actif: {zonesContainerObj.activeInHierarchy}, parent: {zonesContainerObj.transform.parent?.name}");
    }

    string GetFullPath(Transform transform)
    {
        string path = transform.name;
        while (transform.parent != null)
        {
            transform = transform.parent;
            path = transform.name + "/" + path;
        }
        return path;
    }

    IEnumerator CreateZoneDecoratorWithImage(MapZone zone, Transform parent, float scaleX, float scaleY)
    {
        // Construire l'URL complète avec GeneralConfigManager
        string fullImageUrl = GeneralConfigManager.Instance.GetDecoratorImageUrl(zone.decoratorImageUrl);
        LogVerbose($"[MapManager] 🖼️ Chargement decorator pour zone '{zone.id}': {fullImageUrl}");
        
        using (UnityWebRequest www = UnityWebRequestTexture.GetTexture(fullImageUrl))
        {
            yield return www.SendWebRequest();

            if (www.result == UnityWebRequest.Result.Success)
            {
                Texture2D texture = ((DownloadHandlerTexture)www.downloadHandler).texture;
                Sprite sprite = Sprite.Create(texture,
                    new Rect(0, 0, texture.width, texture.height),
                    new Vector2(0.5f, 0.5f));

                GameObject decorObj = new GameObject($"Decorator_{zone.id}");
                decorObj.transform.SetParent(parent, false);

                RectTransform rect = decorObj.AddComponent<RectTransform>();
                rect.anchorMin = Vector2.zero;
                rect.anchorMax = Vector2.zero;
                rect.pivot = new Vector2(0.5f, 0.5f);

                Vector2 scaledPos = new Vector2(zone.position.x * scaleX, zone.position.y * scaleY);

                // Résoudre la taille du décorateur avec fallback vers defaultZoneDecorator
                var defaultZoneDecorator = GeneralConfigManager.Instance?.GetDefaultZoneDecorator();
                float decoratorWidth = zone.decoratorSize?.x 
                    ?? mapConfig.zoneDecorator?.size?.x 
                    ?? (defaultZoneDecorator != null ? defaultZoneDecorator.size.x : 50f);
                float decoratorHeight = zone.decoratorSize?.y 
                    ?? mapConfig.zoneDecorator?.size?.y 
                    ?? (defaultZoneDecorator != null ? defaultZoneDecorator.size.y : 50f);
                Vector2 scaledSize = new Vector2(decoratorWidth * scaleX, decoratorHeight * scaleY);

                rect.anchoredPosition = scaledPos;
                rect.sizeDelta = scaledSize;

                Image image = decorObj.AddComponent<Image>();
                image.sprite = sprite;
                image.raycastTarget = true;  // Activer les clics sur le decorator
                image.color = Color.white; // Couleur blanche = opaque
                image.enabled = true;
                
                // Ajouter MapZoneClickable sur le decorator
                // Il est maintenant sûr car MapZoneClickable ne touche plus la couleur si sprite != null
                MapZoneClickable clickable = decorObj.AddComponent<MapZoneClickable>();
                clickable.Initialize(zone, this);

                // IMPORTANT : S'assurer que le decorator est bien actif et visible
                decorObj.SetActive(true);
                image.enabled = true;
                
                decorators.Add(decorObj);
                
                // CRUCIAL : Ajouter le clickable du decorator à la liste zones
                // Sinon HandleClick() ne le détectera jamais !
                zones.Add(clickable);
                
                LogVerbose($"[MapManager] ✅ Decorator créé pour '{zone.id}' à la position {scaledPos}, taille {scaledSize}, actif: {decorObj.activeInHierarchy}");
                LogVerbose($"[MapManager] Decorator parent: {decorObj.transform.parent?.name}, hiérarchie complète: {GetFullPath(decorObj.transform)}");
                LogVerbose($"[MapManager] Image enabled: {image.enabled}, color: {image.color}, sprite null?: {image.sprite == null}, raycastTarget: {image.raycastTarget}");
                LogVerbose($"[MapManager] ✅ Decorator ajouté à la liste zones (total zones: {zones.Count})");
            }
            else
            {
                Debug.LogError($"[MapManager] ❌ Erreur chargement decorator pour '{zone.id}': {www.error}");
            }
        }
    }

    void CreatePopupContainer()
    {
        popupContainer = new GameObject("PopupContainer");

        Canvas popupCanvas = popupContainer.AddComponent<Canvas>();
        popupCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
        popupCanvas.sortingOrder = 1000;

        CanvasScaler scaler = popupContainer.AddComponent<CanvasScaler>();
        scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        scaler.referenceResolution = new Vector2(1920, 1080);

        popupContainer.AddComponent<GraphicRaycaster>();

        RectTransform rect = popupContainer.GetComponent<RectTransform>();
        rect.anchorMin = Vector2.zero;
        rect.anchorMax = Vector2.one;
        rect.sizeDelta = Vector2.zero;

        DontDestroyOnLoad(popupContainer);
        popupContainer.SetActive(true);
    }

    void CreateDecorativeVideos()
    {
        if (mapConfig.decorativeVideos == null || mapConfig.decorativeVideos.Count == 0 || backgroundObject == null)
        {
            LogVerbose("[MapManager] CreateDecorativeVideos - Aucune vidéo ou background null");
            return;
        }

        LogVerbose($"[MapManager] CreateDecorativeVideos - {mapConfig.decorativeVideos.Count} vidéos à créer");

        RawImage rawImage = backgroundObject.GetComponent<RawImage>();
        Texture2D texture = rawImage.texture as Texture2D;
        RectTransform bgRect = backgroundObject.GetComponent<RectTransform>();

        float scaleX = bgRect.sizeDelta.x / texture.width;
        float scaleY = bgRect.sizeDelta.y / texture.height;  // Utiliser la hauteur réelle de l'image

        LogVerbose($"[MapManager] CreateDecorativeVideos - bgRect: {bgRect.sizeDelta.x}x{bgRect.sizeDelta.y}, texture: {texture.width}x{texture.height}, scaleX: {scaleX}, scaleY: {scaleY}");

        foreach (MapDecorativeVideo videoData in mapConfig.decorativeVideos)
        {
            StartCoroutine(CreateDecorativeVideo(videoData, scaleX, scaleY));
        }
    }

    // Helper pour résoudre les paramètres de vignette (vidéo > global > défaut)
    (float extent, float startRadius, float curve, bool disableMask) ResolveVignetteParams(MapDecorativeVideo videoData)
    {
        // Si la vidéo a sa propre config, l'utiliser directement
        if (videoData.vignetteConfig != null)
        {
            float extent = videoData.vignetteConfig.extent > 0f ? videoData.vignetteConfig.extent : vignetteExtent;
            float startRadius = (videoData.vignetteConfig.startRadius >= 0f && videoData.vignetteConfig.startRadius <= 1f) 
                ? videoData.vignetteConfig.startRadius : vignetteStartRadius;
            float curve = videoData.vignetteConfig.curve > 0f ? videoData.vignetteConfig.curve : vignetteCurve;
            bool disableMask = videoData.vignetteConfig.disableMask;
            
            LogVerbose($"[MapManager] Vignette résolue pour {videoData.id} (config vidéo) - extent: {extent}, startRadius: {startRadius}, curve: {curve}, disableMask: {disableMask}");
            return (extent, startRadius, curve, disableMask);
        }

        // Sinon, utiliser la config globale si elle existe
        if (mapConfig.vignetteConfig != null)
        {
            float extent = mapConfig.vignetteConfig.extent > 0f ? mapConfig.vignetteConfig.extent : vignetteExtent;
            float startRadius = (mapConfig.vignetteConfig.startRadius >= 0f && mapConfig.vignetteConfig.startRadius <= 1f) 
                ? mapConfig.vignetteConfig.startRadius : vignetteStartRadius;
            float curve = mapConfig.vignetteConfig.curve > 0f ? mapConfig.vignetteConfig.curve : vignetteCurve;
            bool disableMask = mapConfig.vignetteConfig.disableMask;
            
            LogVerbose($"[MapManager] Vignette résolue pour {videoData.id} (config globale) - extent: {extent}, startRadius: {startRadius}, curve: {curve}, disableMask: {disableMask}");
            return (extent, startRadius, curve, disableMask);
        }

        // Sinon, utiliser les valeurs par défaut
        LogVerbose($"[MapManager] Vignette résolue pour {videoData.id} (valeurs par défaut) - extent: {vignetteExtent}, startRadius: {vignetteStartRadius}, curve: {vignetteCurve}, disableMask: {disableVignetteMask}");
        return (vignetteExtent, vignetteStartRadius, vignetteCurve, disableVignetteMask);
    }

    IEnumerator CreateDecorativeVideo(MapDecorativeVideo videoData, float scaleX, float scaleY)
    {
        GameObject videoContainer = new GameObject($"DecorativeVideo_{videoData.id}");
        videoContainer.transform.SetParent(backgroundObject.transform, false);

        RectTransform videoRect = videoContainer.AddComponent<RectTransform>();
        videoRect.anchorMin = Vector2.zero;
        videoRect.anchorMax = Vector2.zero;
        videoRect.pivot = new Vector2(0.5f, 0.5f);

        Vector2 scaledPos = new Vector2(videoData.position.x * scaleX, videoData.position.y * scaleY);
        Vector2 scaledSize = new Vector2(videoData.size.x * scaleX, videoData.size.y * scaleY);

        videoRect.anchoredPosition = scaledPos;
        videoRect.sizeDelta = scaledSize;

        // ⚠️ IMPORTANT WebGL: limiter la taille des RenderTexture pour éviter abort(OOM)
        int rtSize = 1024;
        #if UNITY_WEBGL && !UNITY_EDITOR
        rtSize = 512;
        #endif
        RenderTexture renderTexture = new RenderTexture(rtSize, rtSize, 0);
        renderTexture.Create();

        GameObject videoPlayerObj = new GameObject("VideoPlayer");
        videoPlayerObj.transform.SetParent(videoContainer.transform, false);

        VideoPlayer videoPlayer = videoPlayerObj.AddComponent<VideoPlayer>();
        videoPlayer.playOnAwake = false;
        videoPlayer.renderMode = VideoRenderMode.RenderTexture;
        videoPlayer.targetTexture = renderTexture;
        videoPlayer.isLooping = videoData.loop;
        videoPlayer.aspectRatio = VideoAspectRatio.FitInside;
        videoPlayer.audioOutputMode = VideoAudioOutputMode.None;
        
        // Construire l'URL complète avec GeneralConfigManager
        string fullVideoUrl = GeneralConfigManager.Instance.GetVideoUrl(videoData.videoUrl);
        videoPlayer.url = fullVideoUrl;

        GameObject rawImageObj = new GameObject("VideoDisplay");
        rawImageObj.transform.SetParent(videoContainer.transform, false);

        RectTransform rawImageRect = rawImageObj.AddComponent<RectTransform>();
        rawImageRect.anchorMin = Vector2.zero;
        rawImageRect.anchorMax = Vector2.one;
        rawImageRect.offsetMin = Vector2.zero;
        rawImageRect.offsetMax = Vector2.zero;

        RawImage rawImage = rawImageObj.AddComponent<RawImage>();
        rawImage.texture = renderTexture;
        rawImage.raycastTarget = false;

        // Résoudre les paramètres de vignette (vidéo > global > défaut)
        LogVerbose($"[MapManager] CreateDecorativeVideo - {videoData.id} - vignetteConfig null? {videoData.vignetteConfig == null}");
        if (videoData.vignetteConfig != null)
        {
            LogVerbose($"[MapManager] CreateDecorativeVideo - {videoData.id} - vignetteConfig: extent={videoData.vignetteConfig.extent}, startRadius={videoData.vignetteConfig.startRadius}, curve={videoData.vignetteConfig.curve}, disableMask={videoData.vignetteConfig.disableMask}");
        }
        var vignetteParams = ResolveVignetteParams(videoData);
        
        // Créer la texture de masque de vignette (toujours créée, même si le shader n'est pas disponible)
        int maskSize = 512;
        #if UNITY_WEBGL && !UNITY_EDITOR
        maskSize = 256;
        #endif
        Texture2D maskTex = CreateVignetteTexture(maskSize, vignetteParams.extent, vignetteParams.startRadius, vignetteParams.curve, vignetteParams.disableMask);
        
        // CORRECTION WebGL : Charger le shader une seule fois et le réutiliser
        if (!shaderChecked)
        {
            // Essayer Shader.Find d'abord
            videoAlphaMaskShader = Shader.Find("UI/VideoAlphaMask");
            
            // Si ça échoue (WebGL), charger depuis Resources
            if (videoAlphaMaskShader == null)
            {
                videoAlphaMaskShader = Resources.Load<Shader>("Shaders/UIVideoAlphaMask");
                if (videoAlphaMaskShader != null)
                {
                    LogVerbose("[MapManager] ✅ Shader 'UI/VideoAlphaMask' chargé depuis Resources");
                }
            }
            else
            {
                LogVerbose($"[MapManager] ✅ Shader 'UI/VideoAlphaMask' trouvé via Shader.Find");
            }
            
            if (videoAlphaMaskShader == null)
            {
                Debug.LogWarning("[MapManager] ⚠️ Shader 'UI/VideoAlphaMask' introuvable!");
            }
            
            shaderChecked = true;
        }
        
        Shader videoShader = videoAlphaMaskShader;
        
        // Vérifier si le shader est supporté sur cette plateforme
        if (videoShader != null && !videoShader.isSupported)
        {
            Debug.LogWarning($"[MapManager] ⚠️ Shader 'UI/VideoAlphaMask' non supporté sur cette plateforme");
            videoShader = null;
        }
        
        // Si le shader n'est toujours pas trouvé, utiliser UI/Default
        if (videoShader == null)
        {
            Debug.LogWarning($"[MapManager] ⚠️ Shader 'UI/VideoAlphaMask' introuvable ou non supporté, utilisation de 'UI/Default' pour {videoData.id}");
            videoShader = Shader.Find("UI/Default");
        }
        
        if (videoShader != null)
        {
            Material videoMaskMat = new Material(videoShader);
            
            // Seulement définir la texture de masque si le shader le supporte
            if (videoShader.name.Contains("VideoAlphaMask"))
            {
                videoMaskMat.SetTexture("_MaskTex", maskTex);
                LogVerbose($"[MapManager] ✅ Texture de masque appliquée au shader VideoAlphaMask pour {videoData.id}");
            }
            else
            {
                // Si on utilise UI/Default, le shader ne supporte pas le masque
                // En WebGL, on peut utiliser un CanvasGroup avec alpha pour simuler la transparence
                // mais cela ne donnera pas l'effet de vignette exact
                Debug.LogWarning($"[MapManager] ⚠️ Shader VideoAlphaMask non disponible, effet de vignette désactivé pour {videoData.id}");
            }
            
            rawImage.material = videoMaskMat;
            LogVerbose($"[MapManager] ✅ Matériau vidéo créé avec shader: {videoShader.name}");

            // 🔧 Attacher un holder pour nettoyer explicitement RT/Material/Mask quand on détruit la vidéo décorative
            DecorativeVideoResourceHolder holder = videoContainer.AddComponent<DecorativeVideoResourceHolder>();
            holder.videoPlayer = videoPlayer;
            holder.renderTexture = renderTexture;
            holder.maskTexture = maskTex;
            holder.material = videoMaskMat;
        }
        else
        {
            Debug.LogError($"[MapManager] ❌ Aucun shader disponible pour les vidéos décoratives!");
        }
        
        rawImage.color = Color.white;

        videoPlayer.Prepare();
        videoPlayer.prepareCompleted += (source) =>
        {
            if (videoData.autoPlay)
            {
                videoPlayer.Play();
            }
        };

        decorativeVideos.Add(videoContainer);
        yield return null;
    }

    /// <summary>
    /// Holder de ressources pour une vidéo décorative.
    /// Permet d'éviter les fuites en WebGL: RenderTexture + Material + Texture2D masque.
    /// </summary>
    private class DecorativeVideoResourceHolder : MonoBehaviour
    {
        public VideoPlayer videoPlayer;
        public RenderTexture renderTexture;
        public Texture2D maskTexture;
        public Material material;

        private void OnDestroy()
        {
            try
            {
                if (videoPlayer != null)
                {
                    if (videoPlayer.isPlaying) videoPlayer.Stop();
                    videoPlayer.targetTexture = null;
                }
            }
            catch { /* noop */ }

            if (renderTexture != null)
            {
                try { renderTexture.Release(); } catch { }
                Destroy(renderTexture);
                renderTexture = null;
            }

            if (material != null)
            {
                Destroy(material);
                material = null;
            }

            if (maskTexture != null)
            {
                Destroy(maskTexture);
                maskTexture = null;
            }
        }
    }

    Texture2D CreateVignetteTexture(int size, float extent, float startRadius, float curve, bool disableMask)
    {
        Texture2D texture = new Texture2D(size, size, TextureFormat.RGBA32, false);
        Color[] pixels = new Color[size * size];

        float center = size / 2f;
        float maxDist = center * extent;

        for (int y = 0; y < size; y++)
        {
            for (int x = 0; x < size; x++)
            {
                float dx = x - center;
                float dy = y - center;
                float dist = Mathf.Sqrt(dx * dx + dy * dy);
                
                // Si le masque est désactivé, alpha = 1 partout
                float alpha;
                if (disableMask)
                {
                    alpha = 1f;
                }
                else
                {
                    alpha = 1f - Mathf.Clamp01((dist - center * startRadius) / (maxDist - center * startRadius));
                    alpha = Mathf.Pow(alpha, curve);
                }
                
                pixels[y * size + x] = new Color(1f, 1f, 1f, alpha);
            }
        }

        texture.SetPixels(pixels);
        texture.Apply();
        texture.filterMode = FilterMode.Bilinear;
        dynamicTextures.Add(texture);
        return texture;
    }

    void CreateStepDecorativeImages()
    {
        if (currentStep == null || currentStep.decorativeImages == null || currentStep.decorativeImages.Count == 0 || backgroundObject == null)
        {
            LogVerbose("[MapManager] CreateStepDecorativeImages - Aucune image décorative ou background null");
            return;
        }

        LogVerbose($"[MapManager] CreateStepDecorativeImages - {currentStep.decorativeImages.Count} images à créer");

        RawImage rawImage = backgroundObject.GetComponent<RawImage>();
        Texture2D texture = rawImage.texture as Texture2D;
        RectTransform bgRect = backgroundObject.GetComponent<RectTransform>();

        float scaleX = bgRect.sizeDelta.x / texture.width;
        float scaleY = bgRect.sizeDelta.y / texture.height;  // Utiliser la hauteur réelle de l'image

        LogVerbose($"[MapManager] CreateStepDecorativeImages - bgRect: {bgRect.sizeDelta.x}x{bgRect.sizeDelta.y}, texture: {texture.width}x{texture.height}, scaleX: {scaleX}, scaleY: {scaleY}");

        foreach (MapDecorativeImage imageData in currentStep.decorativeImages)
        {
            StartCoroutine(CreateStepDecorativeImage(imageData, scaleX, scaleY));
        }
    }

    IEnumerator CreateStepDecorativeImage(MapDecorativeImage imageData, float scaleX, float scaleY)
    {
        // Construire l'URL complète avec GeneralConfigManager
        string fullImageUrl = GeneralConfigManager.Instance.GetDecoratorImageUrl(imageData.imageUrl);
        
        using (UnityWebRequest www = UnityWebRequestTexture.GetTexture(fullImageUrl))
        {
            yield return www.SendWebRequest();

            if (www.result == UnityWebRequest.Result.Success)
            {
                Texture2D texture = ((DownloadHandlerTexture)www.downloadHandler).texture;
                Sprite sprite = Sprite.Create(texture,
                    new Rect(0, 0, texture.width, texture.height),
                    new Vector2(0.5f, 0.5f));

                GameObject imageObj = new GameObject($"StepDecorativeImage_{imageData.id}");
                imageObj.transform.SetParent(backgroundObject.transform, false);

                RectTransform rect = imageObj.AddComponent<RectTransform>();
                rect.anchorMin = Vector2.zero;
                rect.anchorMax = Vector2.zero;
                rect.pivot = new Vector2(0.5f, 0.5f);

                Vector2 scaledPos = new Vector2(imageData.position.x * scaleX, imageData.position.y * scaleY);
                Vector2 scaledSize = new Vector2(imageData.size.x * scaleX, imageData.size.y * scaleY);

                rect.anchoredPosition = scaledPos;
                rect.sizeDelta = scaledSize;

                Image image = imageObj.AddComponent<Image>();
                image.sprite = sprite;
                image.raycastTarget = false;

                Canvas canvas = imageObj.AddComponent<Canvas>();
                canvas.overrideSorting = true;
                canvas.sortingOrder = imageData.sortingOrder;

                stepDecorativeImages.Add(imageObj);
                
                // Ajouter un effet de clignotement pour attirer l'attention
                StartCoroutine(BlinkDecorativeImage(image));

                LogVerbose($"[MapManager] Image décorative créée avec effet de clignotement: {imageData.id} à la position {scaledPos}");
            }
            else
            {
                Debug.LogError($"[MapManager] Erreur chargement image décorative {imageData.id}: {www.error}");
            }
        }
    }

    /// <summary>
    /// Fait clignoter une image décorative pour attirer l'attention du joueur
    /// </summary>
    IEnumerator BlinkDecorativeImage(Image image)
    {
        if (image == null) yield break;
        
        float blinkDuration = 0.8f; // Durée d'un cycle complet (fade out + fade in)
        float minAlpha = 0.3f; // Alpha minimum (30% visible)
        float maxAlpha = 1f; // Alpha maximum (100% visible)
        
        while (image != null && image.gameObject != null && image.gameObject.activeInHierarchy)
        {
            // Fade out
            float elapsed = 0f;
            while (elapsed < blinkDuration / 2)
            {
                if (image == null || image.gameObject == null) yield break;
                
                elapsed += Time.deltaTime;
                float t = elapsed / (blinkDuration / 2);
                Color color = image.color;
                color.a = Mathf.Lerp(maxAlpha, minAlpha, t);
                image.color = color;
                yield return null;
            }
            
            // Fade in
            elapsed = 0f;
            while (elapsed < blinkDuration / 2)
            {
                if (image == null || image.gameObject == null) yield break;
                
                elapsed += Time.deltaTime;
                float t = elapsed / (blinkDuration / 2);
                Color color = image.color;
                color.a = Mathf.Lerp(minAlpha, maxAlpha, t);
                image.color = color;
                yield return null;
            }
        }
    }

    void Update()
    {
        // Gérer la touche ESC pour retourner à la scène main
        if (Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame)
        {
            LogVerbose("[MapManager] 🔙 Touche ESC détectée - Retour à la scène main");
            
            // IMPORTANT : Fermer toutes les popups avant de quitter
            HideAllPopups();
            
            // IMPORTANT : Sauvegarder l'état actuel avant de quitter
            if (!string.IsNullOrEmpty(currentStepId))
            {
                PlayerPrefs.SetString("CurrentStepId_" + currentMapId, currentStepId);
                // CRUCIAL : Supprimer NextStepId car on ne revient PAS d'un niveau
                // Sinon NextStepId aura la priorité au prochain chargement !
                PlayerPrefs.DeleteKey("NextStepId_" + currentMapId);
                
                // IMPORTANT : Sauvegarder la position du scroll
                ScrollRect scrollRect = scrollViewObject != null ? scrollViewObject.GetComponent<ScrollRect>() : null;
                if (scrollRect != null && scrollRect.content != null)
                {
                    Vector2 scrollPos = scrollRect.content.anchoredPosition;
                    PlayerPrefs.SetFloat("ScrollPosX_" + currentMapId, scrollPos.x);
                    PlayerPrefs.SetFloat("ScrollPosY_" + currentMapId, scrollPos.y);
                    LogVerbose($"[MapManager] ✅ Position scroll sauvegardée: {scrollPos}");
                }
                
                PlayerPrefs.Save();
                LogVerbose($"[MapManager] ✅ État sauvegardé - CurrentStepId: {currentStepId}");
                LogVerbose($"[MapManager] ✅ NextStepId nettoyé pour éviter les conflits");
            }
            
            if (UnifiedLoadingManager.Instance != null)
            {
                UnifiedLoadingManager.LoadMenuScene("main");
            }
            else
            {
                SceneManager.LoadScene("main");
            }
            return;
        }

        // Ne pas gérer les clics si le panneau des paramètres ou la popup de login est ouvert
        if (SettingsManager.Instance != null && SettingsManager.Instance.settingsPanel != null && SettingsManager.Instance.settingsPanel.activeSelf)
        {
            // Panneau des paramètres ouvert, ne pas gérer les clics mais continuer le scroll
            HandleDragScroll();
            HandleScroll();
            HandleDebugToggle();
            UpdatePopupPosition();
            CheckDecorativeVideosPlaying();
            return;
        }
        
        // Vérifier si la popup de login est ouverte
        LoginPopup loginPopup = FindFirstObjectByType<LoginPopup>();
        if (loginPopup != null)
        {
            // Popup de login ouverte, ne pas gérer les clics mais continuer le scroll
            HandleDragScroll();
            HandleScroll();
            HandleDebugToggle();
            UpdatePopupPosition();
            CheckDecorativeVideosPlaying();
            return;
        }
        
        HandleDragScroll();
        HandleScroll();
        HandleClick();
        HandleDebugToggle();
        UpdatePopupPosition(); // NOUVEAU : Mettre à jour la position de la popup
        CheckDecorativeVideosPlaying(); // Vérifier que les vidéos décoratives jouent
    }

    void HandleDebugToggle()
    {
        if (Keyboard.current != null && Keyboard.current.dKey.wasPressedThisFrame)
        {
            showDebugZones = !showDebugZones;
            ToggleZonesVisibility();
            
            // Toggle des vidéos décoratives
            decorativeVideosVisible = !decorativeVideosVisible;
            ToggleDecorativeVideos();
            
            LogVerbose($"[MapManager] Debug toggle - Zones: {showDebugZones}, Vidéos: {decorativeVideosVisible}");
        }
    }

    void UpdatePopupPosition()
    {
        // Mettre à jour la position du popup pour qu'il suive la zone pendant le scroll
        if (activePopup != null && activePopupZone != null)
        {
            // Convertir la position de la zone vers le canvas popup
            Vector3[] zoneCorners = new Vector3[4];
            activePopupZone.GetWorldCorners(zoneCorners);
            Vector3 zoneCenterWorld = (zoneCorners[0] + zoneCorners[2]) * 0.5f;
            
            RectTransform popupCanvasRect = popupContainer.GetComponent<RectTransform>();
            CanvasScaler popupScaler = popupContainer.GetComponent<CanvasScaler>();
            Vector2 referenceResolution = popupScaler.referenceResolution;
            
            // Calculer le scaleFactor
            float scaleX = referenceResolution.x / Screen.width;
            float scaleY = referenceResolution.y / Screen.height;
            float scaleFactor = Mathf.Lerp(scaleX, scaleY, popupScaler.matchWidthOrHeight);
            
            // Obtenir le coin inférieur gauche du canvas popup
            Vector3[] popupCanvasCorners = new Vector3[4];
            popupCanvasRect.GetWorldCorners(popupCanvasCorners);
            Vector3 popupCanvasBottomLeft = popupCanvasCorners[0];
            
            // Calculer l'offset
            Vector2 screenOffset = new Vector2(
                zoneCenterWorld.x - popupCanvasBottomLeft.x,
                zoneCenterWorld.y - popupCanvasBottomLeft.y
            );
            
            Vector2 canvasOffset = screenOffset * scaleFactor;
            
            RectTransform popupRect = activePopup.GetComponent<RectTransform>();
            popupRect.anchoredPosition = canvasOffset;
        }
    }

    void CheckDecorativeVideosPlaying()
    {
        // Vérifier toutes les 60 frames (environ 1 seconde à 60fps) pour ne pas surcharger
        if (Time.frameCount % 60 != 0) return;
        
        if (mapConfig == null || mapConfig.decorativeVideos == null) return;
        
        foreach (GameObject videoContainer in decorativeVideos)
        {
            if (videoContainer == null) continue;
            
            VideoPlayer videoPlayer = videoContainer.GetComponentInChildren<VideoPlayer>();
            if (videoPlayer == null) continue;
            
            // Trouver les données de cette vidéo pour vérifier autoPlay
            string videoId = videoContainer.name.Replace("DecorativeVideo_", "");
            MapDecorativeVideo videoData = mapConfig.decorativeVideos.Find(v => v.id == videoId);
            
            if (videoData == null || !videoData.autoPlay) continue;
            
            // Si la vidéo est préparée mais ne joue pas, la relancer
            if (videoPlayer.isPrepared && !videoPlayer.isPlaying)
            {
                videoPlayer.Play();
            }
        }
    }

    Vector2 ConvertScreenToMapPosition(Vector2 screenPos)
    {
        // Convertir la position de clic de l'écran vers l'espace de la carte
        // Résolution de votre map : 5760x2160
        if (scrollViewObject != null && backgroundObject != null)
        {
            ScrollRect scrollRect = scrollViewObject.GetComponent<ScrollRect>();
            RectTransform bgRect = backgroundObject.GetComponent<RectTransform>();
            
            if (scrollRect != null && bgRect != null)
            {
                // Obtenir la position actuelle du scroll
                Vector2 scrollOffset = scrollRect.content.anchoredPosition;
                
                // Calculer les facteurs de scale pour votre résolution spécifique
                float mapWidth = 5760f;  // Votre résolution de map
                float mapHeight = 2160f;
                
                // Calculer le scale basé sur la taille réelle de l'image affichée
                RawImage rawImage = backgroundObject.GetComponent<RawImage>();
                if (rawImage != null && rawImage.texture != null)
                {
                    Texture2D texture = rawImage.texture as Texture2D;
                    
                    // CORRECTION : Vérifier que les dimensions de texture sont valides
                    if (texture == null || texture.width <= 0 || texture.height <= 0)
                    {
                        Debug.LogWarning($"[MapManager] Texture invalide pour conversion de position. Utilisation de la taille du RectTransform.");
                        // Utiliser directement screenPos comme position relative
                        return screenPos;
                    }
                    
                    float scaleX = bgRect.sizeDelta.x / texture.width;
                    float scaleY = bgRect.sizeDelta.y / texture.height;
                    
                    // CORRECTION : Vérifier que les scales ne sont pas zéro ou invalides
                    if (scaleX <= 0 || scaleY <= 0 || float.IsInfinity(scaleX) || float.IsInfinity(scaleY))
                    {
                        Debug.LogWarning($"[MapManager] Scale invalide (scaleX={scaleX}, scaleY={scaleY}). Utilisation de screenPos.");
                        return screenPos;
                    }
                    
                    // Convertir la position écran en position map
                    Vector2 mapPos = screenPos;
                    
                    // Compenser le scroll
                    mapPos.x -= scrollOffset.x;
                    mapPos.y -= scrollOffset.y;
                    
                    // Convertir en coordonnées de la texture originale
                    mapPos.x = mapPos.x / scaleX;
                    mapPos.y = mapPos.y / scaleY;
                    
                    // Ajuster pour votre résolution spécifique
                    mapPos.x = (mapPos.x / texture.width) * mapWidth;
                    mapPos.y = (mapPos.y / texture.height) * mapHeight;
                    
                    // CORRECTION : Vérifier que le résultat final est valide
                    if (float.IsInfinity(mapPos.x) || float.IsInfinity(mapPos.y) || float.IsNaN(mapPos.x) || float.IsNaN(mapPos.y))
                    {
                        Debug.LogWarning($"[MapManager] Position calculée invalide ({mapPos}). Utilisation de screenPos.");
                        return screenPos;
                    }
                    
                    return mapPos;
                }
            }
        }

        return screenPos; // Fallback
    }

    void ToggleZonesVisibility()
    {
        foreach (MapZoneClickable zone in zones)
        {
            if (zone != null)
            {
                // NE PAS désactiver l'Image si c'est un decorator (sprite de personnage)
                // Les decorators ont un nom qui commence par "Decorator_"
                bool isDecorator = zone.gameObject.name.StartsWith("Decorator_");
                
                if (!isDecorator)
                {
                    // C'est une zone de debug (rectangle coloré), toggle son Image
                    Image img = zone.GetComponent<Image>();
                    if (img != null)
                    {
                        img.enabled = showDebugZones;
                    }
                }
                // Si c'est un decorator, on ne touche PAS à son Image (il reste toujours visible)
            }
        }
        
        // IMPORTANT : S'assurer que tous les decorators (sprites des personnages) restent visibles
        // Même quand showDebugZones = false
        foreach (GameObject decorator in decorators)
        {
            if (decorator != null)
            {
                // Les decorators sont toujours visibles (on ne les masque jamais)
                decorator.SetActive(true);
                
                // S'assurer que l'Image du decorator est aussi enabled
                Image decoratorImg = decorator.GetComponent<Image>();
                if (decoratorImg != null)
                {
                    decoratorImg.enabled = true;
                }
            }
        }
    }
    
    void ToggleDecorativeVideos()
    {
        if (decorativeVideosVisible)
        {
            // Recréer les vidéos décoratives
            CreateDecorativeVideos();
        }
        else
        {
            // Effacer les vidéos décoratives
            ClearDecorativeVideos();
        }
    }
    
    void ClearDecorativeVideos()
    {
        foreach (GameObject videoContainer in decorativeVideos)
        {
            if (videoContainer != null)
            {
                DestroyImmediate(videoContainer);
            }
        }
        decorativeVideos.Clear();
        LogVerbose("[MapManager] Toutes les vidéos décoratives ont été effacées");
    }

    // Fonction pour recréer les vidéos avec les nouveaux paramètres de transparence
    [ContextMenu("Recréer les vidéos décoratives")]
    public void RecreateDecorativeVideos()
    {
        LogVerbose("[MapManager] Recréation des vidéos décoratives avec les nouveaux paramètres de transparence...");
        ClearDecorativeVideos();
        CreateDecorativeVideos();
        LogVerbose($"[MapManager] Vidéos recréées - Extent: {vignetteExtent}, Start: {vignetteStartRadius}, Curve: {vignetteCurve}, Masque désactivé: {disableVignetteMask}");
    }

    void HandleDragScroll()
    {
        if (scrollViewObject == null || contentObject == null) return;

        ScrollRect scrollRect = scrollViewObject.GetComponent<ScrollRect>();
        if (scrollRect == null) return;

        bool isPressed = false;
        Vector2 currentPos = Vector2.zero;

        #if UNITY_WEBGL && !UNITY_EDITOR
        // En WebGL, utiliser WebGLClickReceiver pour détecter le drag
        if (WebGLClickReceiver.IsDragging())
        {
            Vector2 dragStart = WebGLClickReceiver.GetDragStartPosition();
            Vector2 dragCurrent = WebGLClickReceiver.GetDragCurrentPosition();
            
            if (!isDragging)
            {
                // Démarrer le drag
                isDragging = true;
                dragStartPos = dragStart;
                contentStartPos = scrollRect.content.anchoredPosition;
            }
            
            // Continuer le drag
            float dragDistance = dragCurrent.x - dragStart.x;
            Vector2 newPos = contentStartPos;
            newPos.x += dragDistance;

            float maxScroll = Mathf.Max(0, scrollRect.content.sizeDelta.x - scrollRect.viewport.rect.width);
            newPos.x = Mathf.Clamp(newPos.x, -maxScroll, 0);

            scrollRect.content.anchoredPosition = newPos;
        }
        else if (isDragging)
        {
            // Arrêter le drag
            isDragging = false;
        }
        #else
        // Détecter le drag avec le nouveau Input System pour les autres plateformes
        if (Mouse.current != null)
        {
            isPressed = Mouse.current.leftButton.isPressed;
            currentPos = Mouse.current.position.ReadValue();
        }

        // Code de drag manuel pour les autres plateformes
        if (isPressed && !isDragging)
        {
            isDragging = true;
            dragStartPos = currentPos;
            contentStartPos = scrollRect.content.anchoredPosition;
        }

        if (isDragging && isPressed)
        {
            float dragDistance = currentPos.x - dragStartPos.x;
            Vector2 newPos = contentStartPos;
            newPos.x += dragDistance;

            float maxScroll = Mathf.Max(0, scrollRect.content.sizeDelta.x - scrollRect.viewport.rect.width);
            newPos.x = Mathf.Clamp(newPos.x, -maxScroll, 0);

            scrollRect.content.anchoredPosition = newPos;
            
            // Fermer le panneau de texte en bas lors du drag
            HideBottomTextPanel();
        }

        if (!isPressed && isDragging)
        {
            isDragging = false;
        }
        #endif
    }

    void HandleClick()
    {
        // Ne pas gérer les clics si une vidéo fullscreen est en cours
        if (isFullscreenVideoPlaying)
        {
            return;
        }
        
        bool released = false;
        Vector2 clickPos = Vector2.zero;

        if (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame)
        {
            dragStartPos = Mouse.current.position.ReadValue();
        }

        if (Mouse.current != null && Mouse.current.leftButton.wasReleasedThisFrame)
        {
            released = true;
            clickPos = Mouse.current.position.ReadValue();
        }
        
        // WebGL fallback: vérifier si un clic a été reçu depuis JavaScript
        #if UNITY_WEBGL && !UNITY_EDITOR
        if (WebGLClickReceiver.WasClickedThisFrame())
        {
            released = true;
            clickPos = WebGLClickReceiver.GetLastClickPosition();
            dragStartPos = clickPos; // Pas de drag en WebGL via JavaScript
            LogVerbose($"[MapManager] Clic WebGL détecté: ({clickPos.x:F0}, {clickPos.y:F0})");
        }
        #endif

        if (released)
        {
            float distance = Vector2.Distance(clickPos, dragStartPos);
            LogVerbose($"[MapManager] HandleClick - Clic détecté à ({clickPos.x:F0}, {clickPos.y:F0}), distance: {distance:F1}px, zones disponibles: {zones.Count}");

            if (distance < 15f)
            {
                bool clickedOnZone = false;
                foreach (MapZoneClickable zone in zones)
                {
                    RectTransform zoneRect = zone.GetComponent<RectTransform>();

                    if (RectTransformUtility.RectangleContainsScreenPoint(zoneRect, clickPos, null))
                    {
                        clickedOnZone = true;
                        MapZone zoneData = zone.GetZoneData();
                        
                        LogVerbose($"[MapManager] ✅ Clic sur zone: {zoneData.id}, directClick: {zoneData.directClick}, hasPopup: {zoneData.hasPopup}");

                        // Si directClick est true, aller directement au jeu sans afficher la popup
                        if (zoneData.directClick)
                        {
                            // IMPORTANT : Définir currentZoneData avant LaunchLevel pour récupérer gameType
                            currentZoneData = zoneData;
                            // Clic direct vers le jeu sans afficher la popup
                            LaunchLevel(zoneData.targetLevelId);
                        }
                        else if (zoneData.hasPopup)
                        {
                            // Afficher la popup (avec ou sans bouton selon popupClickable)
                            // CORRECTION : Passer le RectTransform de la zone au popup
                            // pour qu'il puisse suivre la zone pendant le scroll
                            ShowPopup(zoneData, zoneRect);
                        }
                        else if (!string.IsNullOrEmpty(zoneData.targetLevelId))
                        {
                            // IMPORTANT : Définir currentZoneData avant OnClick pour récupérer gameType
                            currentZoneData = zoneData;
                            zone.OnClick();
                        }
                        else
                        {
                            // Zone non cliquable: targetLevelId null/vide, hasPopup false, directClick false
                            // Afficher le panneau de texte en bas avec "..."
                            LogVerbose($"[MapManager] Zone {zoneData.id} cliquée - affichage du panneau de texte en bas");
                            ShowBottomTextPanel();
                        }
                        break;
                    }
                }

                if (!clickedOnZone)
                {
                    LogVerbose($"[MapManager] ❌ Aucune zone cliquée à cette position");
                    HideAllPopups();
                }
            }
            else
            {
                LogVerbose($"[MapManager] ⚠️ Clic ignoré car distance trop grande (drag détecté): {distance:F1}px");
            }
        }
    }

    void HandleScroll()
    {
        if (scrollViewObject == null) return;

        ScrollRect scrollRect = scrollViewObject.GetComponent<ScrollRect>();
        if (scrollRect == null) return;

        float scrollSpeed = 500f;
        float movement = 0f;

        // Support clavier (flèches gauche/droite) - sens inversé
        if (Keyboard.current != null)
        {
            if (Keyboard.current.leftArrowKey.isPressed)
            {
                movement = scrollSpeed * Time.deltaTime; // Flèche gauche → scroll vers la droite
            }
            else if (Keyboard.current.rightArrowKey.isPressed)
            {
                movement = -scrollSpeed * Time.deltaTime; // Flèche droite → scroll vers la gauche
            }
        }
        
        // Support molette de souris en WebGL
        #if UNITY_WEBGL && !UNITY_EDITOR
        if (Mouse.current != null)
        {
            Vector2 scrollDelta = Mouse.current.scroll.ReadValue();
            if (scrollDelta.y != 0f)
            {
                // La molette de la souris en WebGL
                movement = scrollDelta.y * scrollSpeed * Time.deltaTime * 2f; // Multiplier pour plus de sensibilité
            }
        }
        #else
        // Support molette de souris pour les autres plateformes
        if (Mouse.current != null)
        {
            Vector2 scrollDelta = Mouse.current.scroll.ReadValue();
            if (scrollDelta.y != 0f)
            {
                movement = scrollDelta.y * scrollSpeed * Time.deltaTime;
            }
        }
        #endif

        if (movement != 0f)
        {
            Vector2 pos = scrollRect.content.anchoredPosition;
            pos.x += movement;

            float maxScroll = Mathf.Max(0, scrollRect.content.sizeDelta.x - scrollRect.viewport.rect.width);
            pos.x = Mathf.Clamp(pos.x, -maxScroll, 0);

            scrollRect.content.anchoredPosition = pos;
            
            // Fermer le panneau de texte en bas lors du scroll
            HideBottomTextPanel();
        }
    }

    /// <summary>
    /// Résout la configuration popup en utilisant defaultPopupConfig de general-config.json comme source principale
    /// </summary>
    private MapPopupConfig ResolvePopupConfig()
    {
        // Utiliser defaultPopupConfig de general-config.json comme source principale
        var defaultConfig = GeneralConfigManager.Instance?.GetDefaultPopupConfig();
        if (defaultConfig != null)
        {
            LogVerbose($"[MapManager] Utilisation de defaultPopupConfig depuis general-config.json (size: {defaultConfig.width}x{defaultConfig.height})");
            // Convertir DefaultPopupConfig en MapPopupConfig avec TOUS les paramètres
            return new MapPopupConfig
            {
                width = defaultConfig.width,
                height = defaultConfig.height,
                backgroundColor = defaultConfig.backgroundColor,
                borderColor = defaultConfig.borderColor,
                borderWidth = defaultConfig.borderWidth,
                textPosition = defaultConfig.textPosition,
                textHeight = (int)defaultConfig.textHeight,
                textPaddingTop = defaultConfig.textPaddingTop,
                textPaddingBottom = defaultConfig.textPaddingBottom,
                textPaddingLeft = defaultConfig.textPaddingLeft,
                textPaddingRight = defaultConfig.textPaddingRight,
                textFontSize = (int)defaultConfig.textFontSize,
                textColor = defaultConfig.textColor,
                textBackgroundColor = defaultConfig.textBackgroundColor,
                textPaddingHorizontal = (int)defaultConfig.textPaddingHorizontal,
                showDelay = defaultConfig.showDelay,
                fadeSpeed = defaultConfig.fadeSpeed,
                buttonText = defaultConfig.buttonText,
                buttonBottomMargin = (int)defaultConfig.buttonBottomMargin,
                buttonStyle = defaultConfig.buttonStyle
            };
        }
        
        // Fallback: valeurs par défaut hardcodées si general-config non disponible
        LogVerbose("[MapManager] Utilisation des valeurs par défaut hardcodées pour popup (general-config non disponible)");
        return new MapPopupConfig();
    }

    void ShowPopup(MapZone zoneData, RectTransform zoneRect)
    {
        Debug.Log($"[MapManager] 🎯 ShowPopup appelé pour zone {zoneData.id}, hasPopup: {zoneData.hasPopup}, popupClickable: {zoneData.popupClickable}");
        
        HideAllPopups();

        if (popupContainer != null)
        {
            popupContainer.SetActive(true);
            Debug.Log($"[MapManager] PopupContainer activé");
        }
        else
        {
            Debug.LogError($"[MapManager] ❌ popupContainer est NULL !");
            return;
        }

        // Résoudre la config popup avec fallback vers defaultPopupConfig
        MapPopupConfig resolvedPopupConfig = ResolvePopupConfig();
        Debug.Log($"[MapManager] Config popup résolue: width={resolvedPopupConfig.width}, height={resolvedPopupConfig.height}");

        GameObject popupObj = new GameObject($"Popup_{zoneData.id}");
        popupObj.transform.SetParent(popupContainer.transform, false);

        RectTransform popupRect = popupObj.AddComponent<RectTransform>();
        popupRect.anchorMin = Vector2.zero;
        popupRect.anchorMax = Vector2.zero;
        popupRect.pivot = new Vector2(0.5f, 0.5f);
        popupRect.sizeDelta = new Vector2(resolvedPopupConfig.width, resolvedPopupConfig.height);
        
        // Convertir la position de la zone vers le canvas popup
        // 1. Obtenir la position écran du centre de la zone
        Vector3[] zoneCorners = new Vector3[4];
        zoneRect.GetWorldCorners(zoneCorners);
        Vector3 zoneCenterWorld = (zoneCorners[0] + zoneCorners[2]) * 0.5f;
        
        // 2. Convertir en position locale dans le canvas popup
        RectTransform popupCanvasRect = popupContainer.GetComponent<RectTransform>();
        
        // Pour les deux canvas en ScreenSpaceOverlay, on doit convertir en tenant compte du CanvasScaler
        CanvasScaler popupScaler = popupContainer.GetComponent<CanvasScaler>();
        Vector2 referenceResolution = popupScaler.referenceResolution; // 1920x1080
        
        // Calculer le ratio entre l'écran réel et la résolution de référence
        float scaleX = referenceResolution.x / Screen.width;
        float scaleY = referenceResolution.y / Screen.height;
        
        // Appliquer le match du scaler (par défaut 0.5)
        float scaleFactor = Mathf.Lerp(scaleX, scaleY, popupScaler.matchWidthOrHeight);
        
        // Obtenir le coin inférieur gauche du canvas popup en pixels écran
        Vector3[] popupCanvasCorners = new Vector3[4];
        popupCanvasRect.GetWorldCorners(popupCanvasCorners);
        Vector3 popupCanvasBottomLeft = popupCanvasCorners[0];
        
        // Calculer l'offset en pixels écran
        Vector2 screenOffset = new Vector2(
            zoneCenterWorld.x - popupCanvasBottomLeft.x,
            zoneCenterWorld.y - popupCanvasBottomLeft.y
        );
        
        // Convertir en coordonnées du canvas (résolution de référence)
        Vector2 canvasOffset = screenOffset * scaleFactor;
        
        popupRect.anchoredPosition = canvasOffset;
        
        Debug.Log($"[MapManager] 📍 Popup positionné sur zone - ZoneCenter écran: {zoneCenterWorld}, Offset écran: {screenOffset}, ScaleFactor: {scaleFactor:F3}, Position finale: {canvasOffset}");
        
        Canvas popupCanvas = popupContainer.GetComponent<Canvas>();
        Debug.Log($"[MapManager] 📊 Canvas popup - sortingOrder: {popupCanvas.sortingOrder}, Screen: {Screen.width}x{Screen.height}, Ref: {referenceResolution}");

        // Stocker la référence de la popup active et de la zone pour suivre le scroll
        activePopup = popupObj;
        activePopupZone = zoneRect;
        
        Debug.Log($"[MapManager] Popup GameObject - Nom: {popupObj.name}, Actif: {popupObj.activeInHierarchy}, Parent: {popupObj.transform.parent?.name}");

        CanvasGroup cg = popupObj.AddComponent<CanvasGroup>();
        cg.alpha = 0f;
        Debug.Log($"[MapManager] CanvasGroup créé avec alpha=0");

        MapPopup popup = popupObj.AddComponent<MapPopup>();
        popup.Initialize(resolvedPopupConfig, this);
        Debug.Log($"[MapManager] MapPopup initialisé");
        
        // Stocker la zone actuelle pour récupérer gameType
        currentZoneData = zoneData;
        
        // Construire l'URL complète avec GeneralConfigManager
        string fullPopupVideoUrl = GeneralConfigManager.Instance.GetPopupVideoUrl(zoneData.popupVideo);
        Debug.Log($"[MapManager] URL vidéo: {fullPopupVideoUrl}");
        Debug.Log($"[MapManager] Texte popup: {zoneData.popupText}");
        Debug.Log($"[MapManager] popupClickable: {zoneData.popupClickable}");
        
        popup.SetupPopupContent(fullPopupVideoUrl, zoneData.popupText, zoneData.targetLevelId, zoneData.popupClickable, zoneData.buttonText);
        Debug.Log($"[MapManager] SetupPopupContent terminé, appel de Show()...");
        popup.Show();
        Debug.Log($"[MapManager] Show() appelé");
    }

    void HideAllPopups()
    {
        if (popupContainer != null)
        {
            foreach (Transform child in popupContainer.transform)
            {
                Destroy(child.gameObject);
            }
        }

        // NOUVEAU : Nettoyer les références
        activePopup = null;
        activePopupZone = null;
        
        // Masquer aussi le panneau de texte en bas
        HideBottomTextPanel();
    }

    /// <summary>
    /// Appelé par WebGLClickReceiver quand un clic est reçu depuis JavaScript
    /// </summary>
    public void OnWebGLClick(Vector2 clickPosition)
    {
        LogVerbose($"[MapManager] OnWebGLClick: ({clickPosition.x:F0}, {clickPosition.y:F0})");
        
        // Vérifier si on a cliqué sur une zone
        foreach (MapZoneClickable zone in zones)
        {
            RectTransform zoneRect = zone.GetComponent<RectTransform>();
            if (zoneRect != null && RectTransformUtility.RectangleContainsScreenPoint(zoneRect, clickPosition, null))
            {
                MapZone zoneData = zone.GetZoneData();
                LogVerbose($"[MapManager] ✅ OnWebGLClick sur zone: {zoneData.id}, directClick: {zoneData.directClick}, hasPopup: {zoneData.hasPopup}");
                
                
                if (zoneData.directClick && !string.IsNullOrEmpty(zoneData.targetLevelId))
                {
                    currentZoneData = zoneData;
                    LaunchLevel(zoneData.targetLevelId);
                }
                else if (zoneData.hasPopup)
                {
                    ShowPopup(zoneData, zoneRect);
                }
                else if (!string.IsNullOrEmpty(zoneData.targetLevelId))
                {
                    // Zone cliquable avec targetLevelId mais pas de directClick ni popup
                    currentZoneData = zoneData;
                    zone.GetComponent<MapZoneClickable>()?.OnClick();
                }
                else
                {
                    // Zone non cliquable: targetLevelId null/vide, hasPopup false, directClick false
                    // Afficher le panneau de texte en bas avec "..."
                    LogVerbose($"[MapManager] Zone {zoneData.id} cliquée - affichage du panneau de texte en bas");
                    ShowBottomTextPanel();
                }
                return;
            }
        }
        
        LogVerbose("[MapManager] ❌ OnWebGLClick: Aucune zone cliquée");
    }
    
    public void LaunchLevel(string levelId)
    {
        LogVerbose($"[MapManager] Lancement niveau: {levelId}");

        if (popupContainer != null)
        {
            Destroy(popupContainer);
        }
        
        // Masquer le panneau de texte en bas s'il est visible
        HideBottomTextPanel();

        if (currentStep != null && !string.IsNullOrEmpty(currentStep.nextStepId))
        {
            PlayerPrefs.SetString("NextStepId_" + currentMapId, currentStep.nextStepId);
            PlayerPrefs.Save();
            LogVerbose($"[MapManager] NextStepId sauvegardé: {currentStep.nextStepId} (depuis step: {currentStep.id})");
        }
        else
        {
            Debug.LogWarning($"[MapManager] Pas de NextStepId pour le step: {currentStep?.id}");
        }

        // IMPORTANT : Définir que nous venons de la Map pour retourner ici après le jeu
        PlayerPrefs.SetString("ReturnToScene", "Map");
        PlayerPrefs.SetString("CurrentMapId", currentMapId);
        LogVerbose($"[MapManager] ✅ ReturnToScene défini à 'Map'");
        LogVerbose($"[MapManager] ✅ CurrentMapId sauvegardé: {currentMapId}");

        // NOUVEAU : Déterminer le type de jeu (depuis la zone OU auto-détection)
        string gameType = "shooting"; // Par défaut
        
        if (currentZoneData != null && !string.IsNullOrEmpty(currentZoneData.gameType))
        {
            gameType = currentZoneData.gameType;
            LogVerbose($"[MapManager] Type de jeu depuis la zone (avant normalisation): {gameType}");
            // IMPORTANT : Normaliser le type (text_hole -> trous)
            gameType = NormalizeGameType(gameType);
            LogVerbose($"[MapManager] Type de jeu normalisé: {gameType}");
        }
        else
        {
            Debug.LogWarning($"[MapManager] Pas de gameType spécifié, utilisation auto-détection pour: {levelId}");
            gameType = DetectGameType(levelId);
            LogVerbose($"[MapManager] Type détecté automatiquement: {gameType}");
        }
        
        // IMPORTANT : Définir le type de jeu dans PlayerPrefs AVANT de sauvegarder
        LogVerbose($"[MapManager] ═══════════════════════════════════════");
        LogVerbose($"[MapManager] DÉFINITION DU TYPE DE JEU DANS PLAYERPREFS");
        LogVerbose($"[MapManager] LevelId: {levelId}");
        LogVerbose($"[MapManager] GameType: {gameType}");
        LogVerbose($"[MapManager] ═══════════════════════════════════════");
        PlayerPrefs.SetString("CurrentLevelType", gameType);
        PlayerPrefs.SetString("CurrentLevelId", levelId);
        
        // Construire l'URL de la config (les niveaux sont dans json/, même dossier que les maps)
        string configUrl = GeneralConfigManager.Instance != null 
            ? GeneralConfigManager.Instance.GetMapConfigUrl(levelId + ".json")
            : $"STREAMING_ASSETS/json/{levelId}.json";
        
        PlayerPrefs.SetString("GameConfigUrl", configUrl);
        PlayerPrefs.SetString("CurrentLevelId", levelId);
        PlayerPrefs.SetString("GamePhase", "Before");
        PlayerPrefs.Save();
        
        // NOUVEAU : Si un gameId est disponible, charger les données du jeu depuis l'API
        int gameId = currentZoneData?.gameId ?? 0;
        
        LogVerbose($"[MapManager] ═══════════════════════════════════════");
        LogVerbose($"[MapManager] 🔍 DIAGNOSTIC GAME ID");
        LogVerbose($"[MapManager] currentZoneData null: {currentZoneData == null}");
        LogVerbose($"[MapManager] gameId récupéré: {gameId}");
        if (currentZoneData != null)
        {
            LogVerbose($"[MapManager] Zone ID: {currentZoneData.id}");
            LogVerbose($"[MapManager] Zone targetLevelId: {currentZoneData.targetLevelId}");
            LogVerbose($"[MapManager] Zone gameId: {currentZoneData.gameId}");
            LogVerbose($"[MapManager] Zone gameType: {currentZoneData.gameType}");
        }
        LogVerbose($"[MapManager] ═══════════════════════════════════════");
        
        if (gameId > 0)
        {
            LogVerbose($"[MapManager] 🎮 GameId disponible: {gameId} - Chargement des données depuis l'API...");
            StartCoroutine(LoadGameDataAndLaunch(gameId, gameType, configUrl));
        }
        else
        {
            LogVerbose($"[MapManager] ⚠️ Pas de gameId (gameId={gameId}) - Lancement direct avec la config locale");
            LogVerbose($"[MapManager] Transition vers scène Player pour dialogue...");
            LogVerbose($"[MapManager] Type: {gameType}, Config: {configUrl}");
            UnityEngine.SceneManagement.SceneManager.LoadScene("Player");
        }
    }
    
    /// <summary>
    /// Charge les données du jeu depuis l'API puis lance la scène
    /// </summary>
    private IEnumerator LoadGameDataAndLaunch(int gameId, string gameType, string configUrl)
    {
        // Afficher un écran de chargement
        if (UnifiedLoadingManager.Instance != null)
        {
            UnifiedLoadingManager.ShowLoading("Chargement du jeu...", LoadingContext.Game);
        }
        
        bool loadingComplete = false;
        bool loadingSuccess = false;
        
        // Récupérer la difficulté depuis PlayerPrefs (sauvegardée par MainSceneManager)
        string difficulty = PlayerPrefs.GetString("CurrentQuestDifficulty", "Débutant");
        LogVerbose($"[MapManager] 📊 Difficulté récupérée: {difficulty}");
        
        // Charger les données du jeu via GameDataManager
        GameDataManager.Instance.LoadGameData(
            gameId,
            difficulty,
            onSuccess: (gameData) => {
                LogVerbose($"[MapManager] ✅ Données du jeu {gameId} chargées avec succès (difficulté: {difficulty})");
                loadingComplete = true;
                loadingSuccess = true;
            },
            onError: (error) => {
                Debug.LogError($"[MapManager] ❌ Erreur lors du chargement du jeu {gameId}: {error}");
                loadingComplete = true;
                loadingSuccess = false;
            }
        );
        
        // Attendre la fin du chargement
        while (!loadingComplete)
        {
            yield return null;
        }
        
        // Lancer la scène même si le chargement API a échoué (fallback sur config locale)
        if (!loadingSuccess)
        {
            Debug.LogWarning($"[MapManager] ⚠️ Fallback sur la configuration locale: {configUrl}");
        }
        
        LogVerbose($"[MapManager] Transition vers scène Player pour dialogue...");
        LogVerbose($"[MapManager] Type: {gameType}, Config: {configUrl}");
        LogVerbose($"[MapManager] Données API disponibles: {GameDataManager.Instance.HasData}");
        
        UnityEngine.SceneManagement.SceneManager.LoadScene("Player");
    }
    
    /// <summary>
    /// Normalise le type de jeu pour correspondre aux noms de scène Unity
    /// Même logique que GameEntry.GetNormalizedType()
    /// </summary>
    private string NormalizeGameType(string rawType)
    {
        if (string.IsNullOrEmpty(rawType))
        {
            return "shooting";
        }
        
        switch (rawType.ToLower())
        {
            case "shooting":
                return "shooting";
            case "text_hole":
            case "trous":
                return "trous";
            case "calculator":
                return "calculator";
            default:
                Debug.LogWarning($"[MapManager] Type de jeu inconnu '{rawType}', utilisation de 'shooting' par défaut");
                return "shooting";
        }
    }
    
    private string DetectGameType(string levelId)
    {
        LogVerbose($"[MapManager] DetectGameType appelé pour: {levelId}");
        
        // 1. Essayer de trouver le type depuis LevelManager (levels-config.json)
        if (LevelManager.Instance != null)
        {
            LogVerbose($"[MapManager] LevelManager.Instance trouvé");
            if (LevelManager.Instance.IsConfigurationLoaded())
            {
                LogVerbose($"[MapManager] Configuration LevelManager chargée");
                LevelData level = LevelManager.Instance.GetLevel(levelId);
                if (level != null)
                {
                    LogVerbose($"[MapManager] LevelData trouvé: id={level.id}, type={level.type}");
                    if (!string.IsNullOrEmpty(level.type))
                    {
                        LogVerbose($"[MapManager] ✅ Type '{level.type}' trouvé dans levels-config.json pour {levelId}");
                        return level.type;
                    }
                    else
                    {
                        Debug.LogWarning($"[MapManager] ⚠️ LevelData.type est vide pour {levelId}");
                    }
                }
                else
                {
                    Debug.LogWarning($"[MapManager] ⚠️ LevelData introuvable pour {levelId} dans levels-config.json");
                }
            }
            else
            {
                Debug.LogWarning($"[MapManager] ⚠️ Configuration LevelManager non chargée");
            }
        }
        else
        {
            Debug.LogWarning($"[MapManager] ⚠️ LevelManager.Instance est null");
        }
        
        // 2. Détection depuis le nom du fichier (fallback)
        string levelIdLower = levelId.ToLowerInvariant();
        if (levelIdLower.Contains("trous") || levelIdLower.Contains("trou"))
        {
            LogVerbose($"[MapManager] Type 'trous' détecté depuis le nom: {levelId}");
            return "trous";
        }
        if (levelIdLower.Contains("calculator") || levelIdLower.Contains("calc"))
        {
            LogVerbose($"[MapManager] Type 'calculator' détecté depuis le nom: {levelId}");
            return "calculator";
        }
        
        // 3. Par défaut, retourner "shooting"
        Debug.LogWarning($"[MapManager] ⚠️ Auto-détection: utilisation du type par défaut 'shooting' pour {levelId}");
        return "shooting";
    }

    public void OnHeaderElementClick(string action)
    {
        LogVerbose($"[MapManager] OnHeaderElementClick appelé avec action: {action}");

        switch (action)
        {
            case "settings":
                LogVerbose("[MapManager] Tentative d'ouverture des paramètres...");
                
                // Obtenir ou créer l'instance du SettingsManager
                SettingsManager settingsManager = SettingsManager.GetOrCreateInstance();
                
                if (settingsManager != null)
                {
                    // Si l'instance vient d'être créée, attendre une frame pour l'initialisation
                    if (settingsManager.settingsPanel == null)
                    {
                        LogVerbose("[MapManager] ⚙️ SettingsManager vient d'être créé, attente de l'initialisation...");
                        StartCoroutine(WaitForSettingsManagerAndOpen());
                    }
                    else
                    {
                        LogVerbose("[MapManager] ✅ SettingsManager.Instance trouvé, appel de OpenSettings()");
                        settingsManager.OpenSettings();
                    }
                }
                else
                {
                    Debug.LogError("[MapManager] ❌ Impossible de créer le SettingsManager !");
                }
                break;

            case "back":
                StartCoroutine(ReturnToMenuWithCleanup());
                break;
            
            case "zenitudeInfo":
                ShowZenitudeInfoPanel();
                break;

            default:
                Debug.LogWarning($"[MapManager] Action inconnue: {action}");
                break;
        }
    }

    private void ShowZenitudeInfoPanel()
    {
        // Fermer si déjà ouvert (toggle simple)
        if (zenitudeInfoModalRoot != null)
        {
            Destroy(zenitudeInfoModalRoot);
            zenitudeInfoModalRoot = null;
            return;
        }

        var generalConfig = GeneralConfigManager.Instance?.GetConfig();
        // ✅ Utiliser le style "informationPanel" défini dans general-config.json (panelStyles.informationPanel)
        // Fallback: alertPanel
        var panelStyle = generalConfig?.panelStyles?.GetStyle("informationPanel")
                         ?? generalConfig?.panelStyles?.GetStyle("alertPanel");
        if (panelStyle == null)
        {
            Debug.LogWarning("[MapManager] informationPanel introuvable dans la config, annulation.");
            return;
        }

        // Canvas overlay dédié, MAIS attaché au canvas principal de la scène Map
        // (sinon, selon les stacks de Canvas/overlays, il peut se retrouver derrière).
        Canvas parentCanvas = GetMainCanvas();
        if (parentCanvas == null)
        {
            Debug.LogWarning("[MapManager] Canvas principal introuvable, annulation modale zénitude.");
            return;
        }

        zenitudeInfoModalRoot = new GameObject("ZenitudeInfoModalRoot");
        zenitudeInfoModalRoot.transform.SetParent(parentCanvas.transform, false);
        zenitudeInfoModalRoot.transform.SetAsLastSibling();

        RectTransform rootRect = zenitudeInfoModalRoot.AddComponent<RectTransform>();
        rootRect.anchorMin = Vector2.zero;
        rootRect.anchorMax = Vector2.one;
        rootRect.offsetMin = Vector2.zero;
        rootRect.offsetMax = Vector2.zero;

        Canvas overlayCanvas = zenitudeInfoModalRoot.AddComponent<Canvas>();
        overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
        overlayCanvas.overrideSorting = true;
        // Très haut pour être au-dessus des autres overlays (header, scroll, loading, etc.)
        overlayCanvas.sortingOrder = 70000;

        // Scaler pour une cohérence visuelle (comme les autres overlays)
        CanvasScaler scaler = zenitudeInfoModalRoot.AddComponent<CanvasScaler>();
        scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        if (generalConfig?.defaultResolution != null && generalConfig.defaultResolution.width > 0 && generalConfig.defaultResolution.height > 0)
            scaler.referenceResolution = new Vector2(generalConfig.defaultResolution.width, generalConfig.defaultResolution.height);
        else
            scaler.referenceResolution = new Vector2(1920, 1080);
        scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
        scaler.matchWidthOrHeight = 0.5f;

        zenitudeInfoModalRoot.AddComponent<GraphicRaycaster>();

        // Overlay sombre
        if (panelStyle.overlay == null || panelStyle.overlay.enabled)
        {
            GameObject overlayObj = new GameObject("ModalOverlay");
            overlayObj.transform.SetParent(zenitudeInfoModalRoot.transform, false);
            RectTransform overlayRect = overlayObj.AddComponent<RectTransform>();
            overlayRect.anchorMin = Vector2.zero;
            overlayRect.anchorMax = Vector2.one;
            overlayRect.offsetMin = Vector2.zero;
            overlayRect.offsetMax = Vector2.zero;

            Image overlayImage = overlayObj.AddComponent<Image>();
            Color overlayColor = HexToColorSafe(panelStyle.overlay?.color ?? "#000000");
            float overlayOpacity = panelStyle.overlay != null ? panelStyle.overlay.opacity : 0.5f;
            overlayImage.color = new Color(overlayColor.r, overlayColor.g, overlayColor.b, overlayOpacity);
            overlayImage.raycastTarget = true;

            // ⚠️ Important : éviter que le même clic qui ouvre la modale la ferme immédiatement.
            // On "arme" la fermeture à la frame suivante seulement.
            zenitudeInfoModalCloseArmed = false;
            StartCoroutine(ArmZenitudeInfoModalCloseNextFrame());

            UnityEngine.EventSystems.EventTrigger trigger = overlayObj.AddComponent<UnityEngine.EventSystems.EventTrigger>();
            UnityEngine.EventSystems.EventTrigger.Entry clickEntry = new UnityEngine.EventSystems.EventTrigger.Entry();
            clickEntry.eventID = UnityEngine.EventSystems.EventTriggerType.PointerClick;
            clickEntry.callback.AddListener((evt) =>
            {
                if (!zenitudeInfoModalCloseArmed) return;
                CloseZenitudeInfoPanel();
            });
            trigger.triggers.Add(clickEntry);
        }

        float modalWidth = panelStyle.width > 0 ? panelStyle.width : 800f;
        float modalHeight = panelStyle.height > 0 ? panelStyle.height : 400f;

        // Panel stylé
        var panel = StyledPanelBuilder.CreatePanelFromStyle(zenitudeInfoModalRoot.transform, panelStyle, modalWidth, modalHeight, "InformationPanel");
        if (panel == null || panel.panelRect == null)
        {
            Debug.LogWarning("[MapManager] Impossible de créer la modale informationPanel.");
            CloseZenitudeInfoPanel();
            return;
        }

        panel.panelRect.anchorMin = new Vector2(0.5f, 0.5f);
        panel.panelRect.anchorMax = new Vector2(0.5f, 0.5f);
        panel.panelRect.pivot = new Vector2(0.5f, 0.5f);
        panel.panelRect.anchoredPosition = Vector2.zero;

        // Header: titre + bouton fermer
        if (panel.headerObject != null && panelStyle.header != null && panelStyle.header.enabled)
        {
            float closeSize = panelStyle.header.closeButton?.size ?? 70f;
            float closeMarginRight = panelStyle.header.closeButton?.marginRight ?? 30f;

            // Titre
            GameObject titleObj = new GameObject("Title");
            titleObj.transform.SetParent(panel.headerObject.transform, false);
            RectTransform titleRect = titleObj.AddComponent<RectTransform>();
            titleRect.anchorMin = new Vector2(0, 0);
            titleRect.anchorMax = new Vector2(1, 1);
            // Centrer le titre par rapport à toute la largeur du panel (et non l'espace restant).
            // On "réserve" symétriquement l'espace du bouton de fermeture (droite) aussi à gauche,
            // afin que le centre géométrique du titre reste le centre du panel.
            float reservedSide = closeSize + closeMarginRight;
            titleRect.offsetMin = new Vector2(20 + reservedSide, 0);
            titleRect.offsetMax = new Vector2(-(20 + reservedSide), 0);

            TextMeshProUGUI titleText = titleObj.AddComponent<TextMeshProUGUI>();
            titleText.text = "JAUGE DE ZÉNITUDE";
            titleText.alignment = TextAlignmentOptions.Center;
            titleText.fontSize = panelStyle.header.titleFontSize > 0 ? panelStyle.header.titleFontSize : 50f;
            titleText.color = HexToColorSafe(panelStyle.header.titleColor ?? "#64477f");
            titleText.fontStyle = FontStyles.Bold;
            titleText.raycastTarget = false;

            if (!string.IsNullOrEmpty(panelStyle.header.titleFontName))
            {
                TMP_FontAsset font = Resources.Load<TMP_FontAsset>($"Fonts/{panelStyle.header.titleFontName}");
                if (font == null) font = Resources.Load<TMP_FontAsset>($"Fonts & Materials/{panelStyle.header.titleFontName}");
                if (font != null) titleText.font = font;
            }

            // Bouton fermer
            GameObject closeObj = new GameObject("CloseButton");
            closeObj.transform.SetParent(panel.headerObject.transform, false);
            RectTransform closeRect = closeObj.AddComponent<RectTransform>();
            closeRect.anchorMin = new Vector2(1, 0.5f);
            closeRect.anchorMax = new Vector2(1, 0.5f);
            closeRect.pivot = new Vector2(1, 0.5f);
            closeRect.sizeDelta = new Vector2(closeSize, closeSize);
            closeRect.anchoredPosition = new Vector2(-closeMarginRight, 0);

            Image closeImg = closeObj.AddComponent<Image>();
            closeImg.color = Color.white;
            closeImg.raycastTarget = true;
            closeImg.type = Image.Type.Simple;

            Button closeBtn = closeObj.AddComponent<Button>();
            closeBtn.transition = Selectable.Transition.ColorTint;
            closeBtn.onClick.AddListener(CloseZenitudeInfoPanel);

            // Charger le sprite du bouton fermer depuis l'UI path
            string closeImageUrl = panelStyle.header.closeButton?.imageUrl ?? "UI_fermer.png";
            string closeUrl = GeneralConfigManager.Instance != null ? GeneralConfigManager.Instance.GetUIUrl(closeImageUrl) : closeImageUrl;
            StartCoroutine(RemoteSpriteCache.Instance.GetOrLoad(
                closeUrl,
                onSuccess: (sp) => { if (closeImg != null) closeImg.sprite = sp; },
                onError: (err) => { Debug.LogWarning($"[MapManager] CloseButton sprite load failed: {err}"); }
            ));
        }

        // Content: texte
        if (panel.contentContainer != null)
        {
            GameObject textObj = new GameObject("ContentText");
            textObj.transform.SetParent(panel.contentContainer.transform, false);
            RectTransform textRect = textObj.AddComponent<RectTransform>();
            textRect.anchorMin = Vector2.zero;
            textRect.anchorMax = Vector2.one;
            textRect.offsetMin = new Vector2(50, 40);
            textRect.offsetMax = new Vector2(-50, -30);

            TextMeshProUGUI contentText = textObj.AddComponent<TextMeshProUGUI>();
            contentText.text = "La jauge de zénitude reflète le niveau de sérénité des habitants de la ville, qui souhaitent être protégés par leurs assureurs contre certains risques. En effet, depuis la grande disparition, les padassiens sont paniqués. En leur apportant tes connaissances en assurance lors des mini-jeux, tu vas les rendre plus zen face aux événements imprévus qui touchent leur ville et leur quotidien.";
            contentText.alignment = TextAlignmentOptions.Center;
            contentText.textWrappingMode = TextWrappingModes.Normal;
            contentText.raycastTarget = false;

            if (panelStyle.content != null)
            {
                contentText.fontSize = panelStyle.content.fontSize > 0 ? panelStyle.content.fontSize : 22f;
                contentText.color = HexToColorSafe(panelStyle.content.textColor ?? "#64477f");

                if (!string.IsNullOrEmpty(panelStyle.content.fontName))
                {
                    TMP_FontAsset font = Resources.Load<TMP_FontAsset>($"Fonts/{panelStyle.content.fontName}");
                    if (font == null) font = Resources.Load<TMP_FontAsset>($"Fonts & Materials/{panelStyle.content.fontName}");
                    if (font != null) contentText.font = font;
                }
            }
            else
            {
                contentText.fontSize = 22f;
                contentText.color = new Color(0.39f, 0.28f, 0.5f, 1f);
            }
        }
    }

    private void CloseZenitudeInfoPanel()
    {
        zenitudeInfoModalCloseArmed = false;
        if (zenitudeInfoModalRoot != null)
        {
            Destroy(zenitudeInfoModalRoot);
            zenitudeInfoModalRoot = null;
        }
    }

    /// <summary>
    /// Affiche un panneau de texte en bas de l'écran avec un texte aléatoire
    /// </summary>
    private void ShowBottomTextPanel()
    {
        // Masquer d'abord le panneau existant s'il y en a un
        HideBottomTextPanel();
        
        Canvas parentCanvas = GetMainCanvas();
        if (parentCanvas == null)
        {
            Debug.LogWarning("[MapManager] Canvas principal introuvable, impossible d'afficher le panneau de texte");
            return;
        }
        
        // Récupérer la configuration depuis general-config.json
        var generalConfig = GeneralConfigManager.Instance?.GetConfig();
        var config = generalConfig?.bottomTextPanel;
        
        // Valeurs par défaut si la config n'est pas disponible
        if (config == null)
        {
            Debug.LogWarning("[MapManager] Configuration bottomTextPanel introuvable, utilisation des valeurs par défaut");
            config = new BottomTextPanelConfig
            {
                texts = new string[] { "..." },
                width = 0.6f,
                alpha = 0.95f,
                backgroundColor = "#f5ece5",
                textColor = "#7c6b63",
                fontSize = 32f,
                anchorMinY = 0.05f,
                anchorMaxY = 0.15f
            };
        }
        
        // Choisir un texte au hasard
        string randomText = "...";
        if (config.texts != null && config.texts.Length > 0)
        {
            int randomIndex = UnityEngine.Random.Range(0, config.texts.Length);
            randomText = config.texts[randomIndex];
        }
        
        // Créer le panneau
        bottomTextPanel = new GameObject("BottomTextPanel");
        bottomTextPanel.transform.SetParent(parentCanvas.transform, false);
        bottomTextPanel.transform.SetAsLastSibling();
        
        RectTransform panelRect = bottomTextPanel.AddComponent<RectTransform>();
        // Positionner en bas de l'écran selon la configuration
        float horizontalMargin = (1f - config.width) / 2f; // Centrer horizontalement
        panelRect.anchorMin = new Vector2(horizontalMargin, config.anchorMinY);
        panelRect.anchorMax = new Vector2(1f - horizontalMargin, config.anchorMaxY);
        panelRect.pivot = new Vector2(0.5f, 0.5f);
        panelRect.offsetMin = Vector2.zero;
        panelRect.offsetMax = Vector2.zero;
        
        // Ajouter l'image de fond
        Image panelImage = bottomTextPanel.AddComponent<Image>();
        Color bgColor = HexToColorSafe(config.backgroundColor);
        bgColor.a = config.alpha; // Appliquer l'alpha
        panelImage.color = bgColor;
        panelImage.raycastTarget = true;
        
        // Ajouter des coins arrondis
        panelImage.type = Image.Type.Sliced;
        
        // Ajouter une ombre
        Shadow shadow = bottomTextPanel.AddComponent<Shadow>();
        shadow.effectColor = new Color(0, 0, 0, 0.2f);
        shadow.effectDistance = new Vector2(0, 4);
        
        // Créer le texte
        GameObject textObj = new GameObject("Text");
        textObj.transform.SetParent(bottomTextPanel.transform, false);
        
        RectTransform textRect = textObj.AddComponent<RectTransform>();
        textRect.anchorMin = Vector2.zero;
        textRect.anchorMax = Vector2.one;
        textRect.offsetMin = new Vector2(20, 10);
        textRect.offsetMax = new Vector2(-20, -10);
        
        TextMeshProUGUI textComponent = textObj.AddComponent<TextMeshProUGUI>();
        textComponent.text = randomText;
        textComponent.fontSize = config.fontSize;
        textComponent.color = HexToColorSafe(config.textColor);
        textComponent.alignment = TextAlignmentOptions.Center;
        textComponent.raycastTarget = false;
        
        LogVerbose($"[MapManager] Panneau de texte en bas affiché avec le texte: {randomText}");
    }
    
    /// <summary>
    /// Masque le panneau de texte en bas de l'écran
    /// </summary>
    private void HideBottomTextPanel()
    {
        if (bottomTextPanel != null)
        {
            Destroy(bottomTextPanel);
            bottomTextPanel = null;
            LogVerbose("[MapManager] Panneau de texte en bas masqué");
        }
    }

    private IEnumerator ArmZenitudeInfoModalCloseNextFrame()
    {
        yield return null; // attendre 1 frame
        zenitudeInfoModalCloseArmed = true;
    }

    private static Color HexToColorSafe(string hex)
    {
        if (string.IsNullOrEmpty(hex)) return Color.white;
        if (ColorUtility.TryParseHtmlString(hex, out var c)) return c;
        return Color.white;
    }
    
    /// <summary>
    /// Attend que SettingsManager soit initialisé puis ouvre le panneau
    /// </summary>
    private System.Collections.IEnumerator WaitForSettingsManagerAndOpen()
    {
        yield return null; // Attendre une frame pour que Awake() soit appelé
        
        if (SettingsManager.Instance != null)
        {
            LogVerbose("[MapManager] ✅ SettingsManager.Instance créé, ouverture du panneau...");
            SettingsManager.Instance.OpenSettings();
        }
        else
        {
            Debug.LogError("[MapManager] ❌ SettingsManager.Instance est toujours NULL après création !");
        }
    }
    
    /// <summary>
    /// Retourne au menu avec nettoyage mémoire WebGL
    /// </summary>
    private IEnumerator ReturnToMenuWithCleanup()
    {
        LogVerbose("[MapManager] Retour au menu - Nettoyage mémoire...");
        
        // Fermer toutes les popups
        HideAllPopups();
        
        // Nettoyer les ressources locales
        CleanupDynamicResources();
        
#if UNITY_WEBGL && !UNITY_EDITOR
        Debug.Log("[MapManager] 🧹 WebGL - Nettoyage mémoire avant retour menu...");
        
        // Purger les caches persistants
        try { if (RemoteSpriteCache.GetExisting() != null) RemoteSpriteCache.GetExisting().ClearAll(); } catch { }
        try { if (GameDataManager.Instance != null) GameDataManager.Instance.ClearAssetCache(); } catch { }
        
        // Attendre un frame pour que les Destroy() prennent effet
        yield return null;
        yield return new WaitForEndOfFrame();
        
        // Libérer les assets non référencés
        yield return Resources.UnloadUnusedAssets();
        try { System.GC.Collect(); } catch { }
        
        yield return null;
        Debug.Log("[MapManager] ✅ Nettoyage mémoire WebGL terminé");
#else
        yield return null;
#endif
        
        SceneManager.LoadScene("menu");
    }
    
    void OnDestroy()
    {
        // Fermer toutes les popups avant de quitter la scène
        HideAllPopups();
        CleanupDynamicResources();
        HideBottomTextPanel();
    }
    
    /// <summary>
    /// Nettoie toutes les ressources créées dynamiquement (important pour WebGL)
    /// </summary>
    void CleanupDynamicResources()
    {
        foreach (var sprite in dynamicSprites)
        {
            if (sprite != null) Destroy(sprite);
        }
        dynamicSprites.Clear();
        
        foreach (var texture in dynamicTextures)
        {
            if (texture != null) Destroy(texture);
        }
        dynamicTextures.Clear();
        
        LogVerbose("[MapManager] 🧹 Ressources dynamiques nettoyées");
    }
}