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

/// <summary>
/// Onglet QUÊTES dans le panneau des paramètres
/// Affiche la liste des quêtes à gauche et les détails à droite (personnages, badges, formations)
/// </summary>
public class QuetesTab : SettingsTab
{
    private QuetesTabConfig config;
    private GeneralConfig generalConfig;
    
    // Références aux blocs
    private GameObject questListBlock;
    private GameObject detailsTabsMenu;
    private GameObject detailsBlock;
    
    // Contenu des détails
    private GameObject detailsHeader;
    private GameObject detailsContent;
    
    // Onglets des détails
    private List<Button> detailTabButtons = new List<Button>();
    private int activeDetailTab = 0;
    
    // Données
    private List<Quest> questsData = new List<Quest>();
    private Quest selectedQuest;
    private Dictionary<int, List<QuestCharacter>> questCharacters = new Dictionary<int, List<QuestCharacter>>();

    // Bouton "JOUER" (sous le cadre de gauche)
    private GameObject playQuestButtonObj;
    private Button playQuestButton;
    private TextMeshProUGUI playQuestButtonText;
    private bool isLaunchingFromQuetesTab = false;

    private const string PendingQuestPreviewMapIdKey = "PendingQuestPreviewMapId";
    private const string PendingQuestPreviewQuestIdKey = "PendingQuestPreviewQuestId";
    
    // Références aux items de quête pour la sélection
    private Dictionary<int, GameObject> questItemsMap = new Dictionary<int, GameObject>();
    
    // 🔧 NOUVEAU : Variable pour stocker la difficulté du badge à effacer (utilisée dans le callback du lien)
    [HideInInspector]
    public string currentResetBadgeDifficulty = "";
    
    // Mosaïque de personnages
    private List<QuestCharacter> currentCharacters = new List<QuestCharacter>();
    private int selectedCharacterIndex = 0;
    private int visibleStartIndex = 0; // Index du premier personnage visible
    private List<GameObject> avatarButtons = new List<GameObject>();
    private GameObject avatarsContainer;
    private GameObject characterDetailPanel;
    // private TextMeshProUGUI characterNameText; // Unused - gardé pour référence future
    private TextMeshProUGUI characterDescriptionText;
    private TextMeshProUGUI characterLongDescriptionText;
    
    // Flèches de navigation
    private GameObject leftArrow;
    private GameObject rightArrow;
    
    // Sprites et polices
    private TMP_FontAsset titleFont;
    private TMP_FontAsset textFont;
    // Textures/sprites persistants (UI de la liste) - nettoyés uniquement à la fermeture du panel
    private readonly List<Texture2D> dynamicTextures = new List<Texture2D>();
    private readonly List<Sprite> dynamicSprites = new List<Sprite>();

    // Textures/sprites liés aux détails de quête (personnages/badges/formations) - nettoyés à CHAQUE changement de quête / onglet
    private readonly List<Texture2D> detailTextures = new List<Texture2D>();
    private readonly List<Sprite> detailSprites = new List<Sprite>();

    // Permet d'invalider les coroutines de chargement quand on change de quête/onglet
    private int detailsGeneration = 0;

    private enum ResourceScope
    {
        Persistent,
        Details
    }

    private ResourceScope currentScope = ResourceScope.Persistent;

    // Cache pour fleche_formation.png (évite de re-télécharger et de recréer des textures en boucle)
    private Sprite cachedFormationArrowSprite;
    private Texture2D cachedFormationArrowTexture;
    private bool isLoadingFormationArrowSprite = false;
    private readonly List<Image> pendingFormationArrowTargets = new List<Image>();
    
    // ✅ Cache UI pour éviter les allocations (Texture2D/Sprite) en boucle lors de la navigation PERSONNAGES (WebGL OOM)
    private readonly Dictionary<string, Sprite> roundedSpriteCache = new Dictionary<string, Sprite>();
    private readonly Dictionary<string, Sprite> roundedBorderOnlySpriteCache = new Dictionary<string, Sprite>();

    // ✅ Cache global avatars téléchargés (LRU) : RemoteSpriteCache
    
    // Flag pour forcer la reconstruction de l'interface
    private bool needsRebuild = false;
    
    // Protection contre les clics multiples rapides
    private bool isLoadingData = false;
    private bool isRefreshingData = false;
    private bool isSwitchingTab = false;
    private bool isSelectingQuest = false;
    private float lastClickTime = 0f;
    private const float CLICK_DEBOUNCE_TIME = 0.3f; // 300ms entre les clics
    
    // Flag pour activer les 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;
    
    // Helper pour les logs conditionnels
    [System.Diagnostics.Conditional("UNITY_EDITOR")]
    private void LogVerbose(string message)
    {
        if (VERBOSE_DEBUG) Debug.Log(message);
    }
    
    protected override void Awake()
    {
        base.Awake();
        LogVerbose("[QuetesTab] AWAKE");

        // ✅ Point de nettoyage global: si on change de scène alors que le panel est ouvert,
        // on libère les ressources "Details" (avatars/badges/etc.) pour éviter les fuites WebGL.
        CleanupPoints.Instance?.RegisterOnSceneChange("QuetesTab", () =>
        {
            CleanupDetailResources();
        });
        
        // Charger la configuration
        if (GeneralConfigManager.Instance != null)
        {
            generalConfig = GeneralConfigManager.Instance.GetConfig();
            config = generalConfig?.quetesTabConfig;
            
            if (config != null)
            {
                LoadFonts();
                StartCoroutine(BuildInterface());
            }
            else
            {
                Debug.LogError("[QuetesTab] ❌ Configuration quetesTabConfig non trouvée!");
            }
        }
        else
        {
            Debug.LogError("[QuetesTab] ❌ GeneralConfigManager.Instance est null!");
        }
    }
    
    private bool isInitialized = false;
    
    protected override void OnEnable()
    {
        base.OnEnable();
        
        // Si l'interface a été détruite (needsRebuild=true), la reconstruire
        if (needsRebuild)
        {
            needsRebuild = false;
            StartCoroutine(RebuildInterface());
            return;
        }
        
        // Si l'interface n'est pas encore construite, attendre
        if (questListBlock == null)
        {
            StartCoroutine(WaitForInterfaceAndPopulate());
            return;
        }
        
        LoadQuestsData();
        PopulateQuestList();
        
        // GARANTIR que le bouton JOUER existe
        EnsurePlayButtonExists();
        if (playQuestButtonObj == null) CreatePlayButtonFallback();
        
        isInitialized = true;
    }

    protected override void OnDisable()
    {
        base.OnDisable();
        try { CleanupPoints.GetExisting()?.UnregisterOnSceneChange("QuetesTab"); } catch { }
    }
    
    IEnumerator RebuildInterface()
    {
        LoadFonts();
        yield return StartCoroutine(BuildInterface());
        LoadQuestsData();
        PopulateQuestList();
        
        EnsurePlayButtonExists();
        if (playQuestButtonObj == null) CreatePlayButtonFallback();
        
        isInitialized = true;
    }
    
    IEnumerator WaitForInterfaceAndPopulate()
    {
        int maxWait = 50;
        int waited = 0;
        
        while (questListBlock == null && waited < maxWait)
        {
            yield return new WaitForSeconds(0.1f);
            waited++;
        }
        
        if (questListBlock != null && !isInitialized)
        {
            LoadQuestsData();
            PopulateQuestList();
            EnsurePlayButtonExists();
            if (playQuestButtonObj == null) CreatePlayButtonFallback();
            isInitialized = true;
        }
    }
    
    void Start()
    {
        StartCoroutine(DelayedButtonCreationCheck());
    }
    
    IEnumerator DelayedButtonCreationCheck()
    {
        yield return new WaitForSeconds(1.5f);
        
        if (playQuestButtonObj == null && gameObject.activeInHierarchy)
        {
            LogVerbose("[QuetesTab] Création retardée du bouton");
            EnsurePlayButtonExists();
            if (playQuestButtonObj == null) CreatePlayButtonFallback();
        }
    }
    
    void OnDestroy()
    {
        CleanupResources();
    }
    
    /// <summary>
    /// Appelé par SettingsManager quand le panel se ferme (pas juste changement d'onglet)
    /// </summary>
    public void CleanupOnPanelClose()
    {
        CleanupResources();
    }
    
    // NOTE: OnDisable est déjà géré plus haut (désinscription CleanupPoints). Ne pas dupliquer.
    
    void CleanupResources()
    {
        // Nettoyage complet (panel qui se ferme)
        CleanupDetailResources();

        for (int i = 0; i < dynamicSprites.Count; i++)
        {
            Sprite sp = dynamicSprites[i];
            if (sp != null) Destroy(sp);
        }
        dynamicSprites.Clear();

        for (int i = 0; i < dynamicTextures.Count; i++)
        {
            Texture2D tex = dynamicTextures[i];
            if (tex != null) Destroy(tex);
        }
        dynamicTextures.Clear();
        
        // NOTE(WebGL): On évite GC.Collect / Resources.UnloadUnusedAssets() ici.
        // On détruit déjà explicitement textures/sprites (persistants + détails).
    }

    private void CleanupDetailResources()
    {
        // Invalider toutes les coroutines de chargement en cours (avatars/badges/images)
        detailsGeneration++;

        for (int i = 0; i < detailSprites.Count; i++)
        {
            Sprite sp = detailSprites[i];
            if (sp != null) Destroy(sp);
        }
        detailSprites.Clear();

        for (int i = 0; i < detailTextures.Count; i++)
        {
            Texture2D tex = detailTextures[i];
            if (tex != null) Destroy(tex);
        }
        detailTextures.Clear();

        // Vider les caches liés aux détails (ils peuvent référencer des sprites/tex détruits)
        roundedSpriteCache.Clear();
        roundedBorderOnlySpriteCache.Clear();
    }

    private Sprite GetCachedRoundedSprite(int width, int height, int radius, Color fillColor)
    {
        string key = $"rounded:{width}:{height}:{radius}";
        if (roundedSpriteCache.TryGetValue(key, out var cached) && cached != null) return cached;
        var created = CreateRoundedSprite(width, height, radius, fillColor);
        roundedSpriteCache[key] = created;
        return created;
    }

    private Sprite GetCachedRoundedBorderOnlySprite(int width, int height, int radius, int borderWidth)
    {
        string key = $"borderOnly:{width}:{height}:{radius}:{borderWidth}";
        if (roundedBorderOnlySpriteCache.TryGetValue(key, out var cached) && cached != null) return cached;
        var created = CreateRoundedBorderOnlySprite(width, height, radius, borderWidth);
        roundedBorderOnlySpriteCache[key] = created;
        return created;
    }

    private void TrackPersistent(Texture2D tex, Sprite sp)
    {
        if (tex != null) dynamicTextures.Add(tex);
        if (sp != null) dynamicSprites.Add(sp);
    }

    private void TrackDetails(Texture2D tex, Sprite sp)
    {
        if (tex != null) detailTextures.Add(tex);
        if (sp != null) detailSprites.Add(sp);
    }

    private void SetScope(ResourceScope scope)
    {
        currentScope = scope;
    }
    
    void LoadFonts()
    {
        string titleFontName = config?.fonts?.titleFontName ?? "Anton-Regular SDF";
        string textFontName = config?.fonts?.textFontName ?? "Lato-Regular SDF";
        
        titleFont = Resources.Load<TMP_FontAsset>($"Fonts/{titleFontName}");
        if (titleFont == null)
            titleFont = Resources.Load<TMP_FontAsset>(titleFontName);
            
        textFont = Resources.Load<TMP_FontAsset>($"Fonts/{textFontName}");
        if (textFont == null)
            textFont = Resources.Load<TMP_FontAsset>(textFontName);
            
    }
    
    void LoadQuestsData()
    {
        questsData.Clear();
        
        MainSceneManager mainSceneManager = FindFirstObjectByType<MainSceneManager>();
        
        if (mainSceneManager != null)
        {
            var sceneConfigField = mainSceneManager.GetType().GetField("sceneConfig", 
                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            
            if (sceneConfigField != null)
            {
                MainSceneConfig sceneConfig = sceneConfigField.GetValue(mainSceneManager) as MainSceneConfig;
                
                if (sceneConfig != null && sceneConfig.quests != null && sceneConfig.quests.Count > 0)
                {
                    questsData = new List<Quest>();
                    foreach (Quest q in sceneConfig.quests)
                    {
                        if (q.has_access) questsData.Add(q);
                    }
                }
            }
        }
        else
        {
            StartCoroutine(LoadQuestsFromApi());
            return;
        }
        
        if (questsData.Count == 0) CreateFallbackQuests();
        
        StartCoroutine(LoadQuestCharacters());
        StartCoroutine(RefreshQuestsFromApi());
    }
    
    IEnumerator LoadQuestsFromApi()
    {
        // Utiliser l'instance singleton plutôt que FindFirstObjectByType
        GeneralConfigManager configManager = GeneralConfigManager.Instance;
        if (configManager == null)
        {
            Debug.LogWarning("[QuetesTab] GeneralConfigManager.Instance est NULL");
            CreateFallbackQuests();
            PopulateQuestList();
            yield break;
        }
        
        string apiUrl = configManager.GetMainSceneConfigApiUrl();
        Debug.Log($"[QuetesTab] Chargement des quêtes depuis l'API: {apiUrl}");
        
        if (string.IsNullOrEmpty(apiUrl))
        {
            CreateFallbackQuests();
            PopulateQuestList();
            yield break;
        }
        
        string token = UserDataManager.Instance != null ? UserDataManager.Instance.token : "";
        bool hasToken = !string.IsNullOrEmpty(token);
        
        using (UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.Get(apiUrl))
        {
            if (hasToken) request.SetRequestHeader("Authorization", $"Bearer {token}");
            
            yield return request.SendWebRequest();
            
            if (request.result != UnityEngine.Networking.UnityWebRequest.Result.Success)
            {
                CreateFallbackQuests();
                PopulateQuestList();
                yield break;
            }
            
            string jsonResponse = request.downloadHandler.text;
            
            if (string.IsNullOrEmpty(jsonResponse))
            {
                CreateFallbackQuests();
                PopulateQuestList();
                yield break;
            }
            
            try
            {
                ApiMainSceneResponse response = JsonUtility.FromJson<ApiMainSceneResponse>(jsonResponse);
                
                if (response != null && response.status == "success" && response.data != null && response.data.quests != null)
                {
                    foreach (var apiQuest in response.data.quests)
                    {
                        Quest quest = new Quest
                        {
                            id = apiQuest.id,
                            title = apiQuest.title,
                            description = apiQuest.description,
                            trailer_url = apiQuest.trailer_url,
                            duration = apiQuest.duration,
                            visibility = apiQuest.visibility,
                            status = apiQuest.status,
                            trainings = new List<Training>(),
                            has_access = apiQuest.has_access,
                            sprite_url = apiQuest.sprite_url,
                            sprite_position = apiQuest.sprite_position,
                            sprite_size = apiQuest.sprite_size,
                            tooltip_text = apiQuest.tooltip_text,
                            user = apiQuest.user
                        };
                        
                        if (apiQuest.trainings != null)
                        {
                            foreach (var apiTraining in apiQuest.trainings)
                            {
                                quest.trainings.Add(new Training
                                {
                                    title = apiTraining.title,
                                    url = apiTraining.url
                                });
                            }
                        }
                        
                        if (quest.has_access) questsData.Add(quest);
                    }
                    
                    PopulateQuestList();
                }
                else
                {
                    CreateFallbackQuests();
                    PopulateQuestList();
                }
            }
            catch (System.Exception e)
            {
                Debug.LogError($"[QuetesTab] ❌ Exception JSON: {e.Message}");
                CreateFallbackQuests();
                PopulateQuestList();
            }
            
            yield return StartCoroutine(LoadQuestCharacters());
            PopulateQuestList();
        }
    }
    
    IEnumerator RefreshQuestsFromApi()
    {
        // Utiliser l'instance singleton
        GeneralConfigManager configManager = GeneralConfigManager.Instance;
        if (configManager == null) yield break;
        
        string apiUrl = configManager.GetMainSceneConfigApiUrl();
        string token = UserDataManager.Instance != null ? UserDataManager.Instance.token : "";
        bool hasToken = !string.IsNullOrEmpty(token);
        
        using (UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.Get(apiUrl))
        {
            if (hasToken) request.SetRequestHeader("Authorization", $"Bearer {token}");
            
            yield return request.SendWebRequest();
            
            if (request.result != UnityEngine.Networking.UnityWebRequest.Result.Success) yield break;
            
            string jsonResponse = request.downloadHandler.text;
            
            try
            {
                ApiMainSceneResponse response = JsonUtility.FromJson<ApiMainSceneResponse>(jsonResponse);
                
                if (response != null && response.status == "success" && response.data != null && response.data.quests != null)
                {
                    foreach (var apiQuest in response.data.quests)
                    {
                        Quest existingQuest = questsData.Find(q => q.id == apiQuest.id);
                        if (existingQuest != null && apiQuest.user != null)
                            existingQuest.user = apiQuest.user;
                    }
                }
            }
            catch { }
        }
    }
    
    void CreateFallbackQuests()
    {
        LogVerbose("[QuetesTab] Création de quêtes de fallback");
        
        // Créer des quêtes d'exemple pour le fallback avec des titres réalistes
        string[] titles = new string[]
        {
            "QUÊTE 1 : LES DENTS DE L'AMER",
            "QUÊTE 2 : LA MARCHE DES ZOMBIES",
            "QUÊTE 3 : JURASSIC VILLE",
            "QUÊTE 4 : LE CLAN DES CIGARES",
            "QUÊTE 5 : BULLES SUR LA VILLE"
        };
        
        for (int i = 0; i < titles.Length; i++)
        {
            var quest = new Quest
            {
                id = i + 1,
                title = titles[i],
                description = $"Les rideaux de fer tombent sur la ville... Découvrez pourquoi les habitants du quartier sont sur les dents ! C'est une aventure passionnante qui vous attend dans cette quête {i + 1}.",
                duration = "2 H 00",
                trainings = new List<Training>
                {
                    new Training { title = "Comprendre le régime d'assurance maladie", url = "" },
                    new Training { title = "Comprendre l'assurance complémentaire santé", url = "" }
                }
            };
            
            questsData.Add(quest);
            LogVerbose($"[QuetesTab] ✅ Quête fallback créée: {quest.title}");
        }
        
        LogVerbose($"[QuetesTab] ✅ {questsData.Count} quêtes de fallback créées au total");
    }
    
    IEnumerator LoadQuestCharacters()
    {
        // Charger les personnages depuis persos.json
        string persosJsonPath = Application.streamingAssetsPath + "/persos.json";
        
        if (System.IO.File.Exists(persosJsonPath))
        {
            string json = System.IO.File.ReadAllText(persosJsonPath);
            CharactersData charactersData = JsonUtility.FromJson<CharactersData>(json);
            
            if (charactersData != null && charactersData.personnages != null)
            {
                foreach (Quest quest in questsData)
                {
                    questCharacters[quest.id] = charactersData.personnages;
                }
            }
        }
        
        yield return null;
    }
    
    IEnumerator BuildInterface()
    {
        LogVerbose("[QuetesTab] 🎮 JOUER: === BuildInterface DÉMARRÉ ===");
        
        if (config == null)
        {
            LogVerbose("[QuetesTab] 🎮 JOUER: config est null, tentative de rechargement...");
            if (GeneralConfigManager.Instance != null)
            {
                generalConfig = GeneralConfigManager.Instance.GetConfig();
                config = generalConfig?.quetesTabConfig;
            }
            
            if (config == null)
            {
                Debug.LogError("[QuetesTab] ❌ Configuration null - ABANDON");
                yield break;
            }
        }
        
        LogVerbose("[QuetesTab] 🎮 JOUER: Création des blocs UI...");
        CreateQuestListBlock();
        CreateDetailsTabsMenu();
        
        LogVerbose("[QuetesTab] 🎮 JOUER: Appel de CreateDetailsBlock()");
        CreateDetailsBlock();
        LogVerbose($"[QuetesTab] 🎮 JOUER: Après CreateDetailsBlock - playQuestButtonObj={playQuestButtonObj != null}");
        
        yield return null;
        
        LoadQuestsData();
        yield return new WaitForSeconds(0.2f);
        PopulateQuestList();
        
        LogVerbose("[QuetesTab] 🎮 JOUER: Appel de EnsurePlayButtonExists()");
        EnsurePlayButtonExists();
        LogVerbose($"[QuetesTab] 🎮 JOUER: Après EnsurePlayButtonExists - playQuestButtonObj={playQuestButtonObj != null}");
        
        if (playQuestButtonObj == null)
        {
            LogVerbose("[QuetesTab] 🎮 JOUER: Appel de CreatePlayButtonFallback()");
            CreatePlayButtonFallback();
        }
        
        isInitialized = true;
        LogVerbose($"[QuetesTab] 🎮 JOUER: BuildInterface terminé - bouton={playQuestButtonObj != null}");
    }
    
    void CreateQuestListBlock()
    {
        var listConfig = config.questListBlock;
        
        questListBlock = new GameObject("QuestListBlock");
        questListBlock.transform.SetParent(transform, false);
        questListBlock.transform.SetAsLastSibling();
        
        RectTransform rect = questListBlock.AddComponent<RectTransform>();
        rect.anchorMin = new Vector2(0, 1);
        rect.anchorMax = new Vector2(0, 1);
        rect.pivot = new Vector2(0, 1);
        rect.anchoredPosition = new Vector2(listConfig.x, -listConfig.y);
        rect.sizeDelta = new Vector2(listConfig.width, listConfig.height);
        
        // Image de fond avec coins arrondis (sprite blanc + couleur via Image.color)
        Image bgImage = questListBlock.AddComponent<Image>();
        bgImage.sprite = CreateRoundedSprite((int)listConfig.width, (int)listConfig.height,
            listConfig.cornerRadius, Color.white);
        bgImage.color = HexToColor(listConfig.backgroundColor);
        bgImage.type = Image.Type.Simple;
        bgImage.raycastTarget = false;
        
        if (listConfig.shadowEnabled)
        {
            Shadow shadow = questListBlock.AddComponent<Shadow>();
            shadow.effectColor = HexToColor(listConfig.shadowColor);
            shadow.effectDistance = new Vector2(0, -listConfig.shadowOffsetY);
        }
        
        // ScrollRect pour la liste - APPROCHE SIMPLIFIÉE SANS LAYOUTGROUP
        GameObject viewport = new GameObject("Viewport");
        viewport.transform.SetParent(questListBlock.transform, false);
        
        RectTransform viewportRect = viewport.AddComponent<RectTransform>();
        viewportRect.anchorMin = Vector2.zero;
        viewportRect.anchorMax = Vector2.one;
        // Réserver de l'espace pour la scrollbar à droite
        float scrollbarWidth = 8f; // Largeur de la scrollbar
        float scrollbarSpacing = 8f; // Espace entre le contenu et la scrollbar
        viewportRect.offsetMin = new Vector2(listConfig.padding, listConfig.padding);
        viewportRect.offsetMax = new Vector2(-listConfig.padding - scrollbarWidth - scrollbarSpacing, -listConfig.padding);
        
        // IMPORTANT: RectMask2D au lieu de Mask pour un meilleur clipping
        RectMask2D rectMask = viewport.AddComponent<RectMask2D>();
        
        // Content container - SANS LayoutGroup, positionnement manuel
        GameObject content = new GameObject("Content");
        content.transform.SetParent(viewport.transform, false);
        
        RectTransform contentRect = content.AddComponent<RectTransform>();
        // Ancré en haut à gauche, taille calculée manuellement
        contentRect.anchorMin = new Vector2(0, 1);
        contentRect.anchorMax = new Vector2(0, 1);
        contentRect.pivot = new Vector2(0, 1);
        contentRect.anchoredPosition = Vector2.zero;
        // La taille sera mise à jour dans PopulateQuestList
        float contentWidth = listConfig.width - listConfig.padding * 2 - scrollbarWidth - scrollbarSpacing;
        contentRect.sizeDelta = new Vector2(contentWidth, 100); // Hauteur temporaire
        
        // Créer la Scrollbar verticale - APPROCHE SIMPLIFIÉE SANS SPRITES CUSTOM
        GameObject scrollbarObj = new GameObject("Scrollbar Vertical");
        scrollbarObj.transform.SetParent(questListBlock.transform, false);
        
        RectTransform scrollbarRect = scrollbarObj.AddComponent<RectTransform>();
        scrollbarRect.anchorMin = new Vector2(1, 0);
        scrollbarRect.anchorMax = new Vector2(1, 1);
        scrollbarRect.pivot = new Vector2(1, 0.5f);
        scrollbarRect.anchoredPosition = new Vector2(-listConfig.padding - 4f, 0);
        scrollbarRect.sizeDelta = new Vector2(scrollbarWidth, 0);
        
        Scrollbar scrollbar = scrollbarObj.AddComponent<Scrollbar>();
        scrollbar.direction = Scrollbar.Direction.BottomToTop;
        
        // Sliding Area (zone fonctionnelle)
        GameObject slidingArea = new GameObject("Sliding Area");
        slidingArea.transform.SetParent(scrollbarObj.transform, false);
        
        RectTransform slidingAreaRect = slidingArea.AddComponent<RectTransform>();
        slidingAreaRect.anchorMin = Vector2.zero;
        slidingAreaRect.anchorMax = Vector2.one;
        slidingAreaRect.sizeDelta = Vector2.zero;
        slidingAreaRect.offsetMin = Vector2.zero;
        slidingAreaRect.offsetMax = Vector2.zero;
        
        // Handle (simple, sans marges pour WebGL)
        GameObject handle = new GameObject("Handle");
        handle.transform.SetParent(slidingArea.transform, false);
        
        RectTransform handleRect = handle.AddComponent<RectTransform>();
        handleRect.anchorMin = Vector2.zero;
        handleRect.anchorMax = Vector2.one;
        handleRect.sizeDelta = Vector2.zero;
        handleRect.offsetMin = Vector2.zero;
        handleRect.offsetMax = Vector2.zero;
        
        Image handleImage = handle.AddComponent<Image>();
        handleImage.color = HexToColor("#ab907f");
        handleImage.raycastTarget = true;
        
        // Assigner le handle à la scrollbar
        scrollbar.targetGraphic = handleImage;
        scrollbar.handleRect = handleRect;
        
        // Configurer le ScrollRect avec la scrollbar
        ScrollRect scrollRect = questListBlock.AddComponent<ScrollRect>();
        scrollRect.content = contentRect;
        scrollRect.viewport = viewportRect;
        scrollRect.horizontal = false;
        scrollRect.vertical = true;
        scrollRect.verticalScrollbar = scrollbar;
        scrollRect.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.Permanent; // Toujours visible
        scrollRect.verticalScrollbarSpacing = scrollbarSpacing;
        scrollRect.movementType = ScrollRect.MovementType.Clamped;
        scrollRect.scrollSensitivity = 20f;
        
        // Désactiver l'inertie en WebGL pour éviter les comportements bizarres
        #if UNITY_WEBGL && !UNITY_EDITOR
        scrollRect.inertia = false;
        #endif
        
        // 🖱️ WEBGL SCROLL FIX: Ajouter le handler JavaScript pour capturer le scroll en WebGL (molette + trackpad Mac)
#if UNITY_WEBGL && !UNITY_EDITOR
        if (scrollRect.GetComponent<WebGLScrollHandler>() == null)
        {
            var handler = scrollRect.gameObject.AddComponent<WebGLScrollHandler>();
            handler.scrollSpeed = 800f; // Sensibilité adaptée pour la liste des quêtes
            handler.debugLogs = false;
            LogVerbose("[QuetesTab] 🖱️ WebGLScrollHandler ajouté pour le scroll (molette + trackpad)");
        }
#endif
        
        LogVerbose($"[QuetesTab] ✅ Bloc liste créé avec scrollbar verticale (style maquette) - Position: ({listConfig.x}, {listConfig.y}), Taille: {listConfig.width}x{listConfig.height}");
    }

    private void OnPlaySelectedQuestClicked()
    {
        LogVerbose("[QuetesTab] 🎮 JOUER: === BOUTON CLIQUÉ ===");
        
        if (isLaunchingFromQuetesTab)
        {
            Debug.LogWarning("[QuetesTab] 🎮 JOUER: ⚠️ Lancement déjà en cours, clic ignoré");
            return;
        }

        if (selectedQuest == null)
        {
            Debug.LogWarning("[QuetesTab] 🎮 JOUER: ⚠️ Aucune quête sélectionnée");
            return;
        }

        LogVerbose($"[QuetesTab] 🎮 JOUER: Quête sélectionnée = {selectedQuest.title} (ID: {selectedQuest.id})");
        isLaunchingFromQuetesTab = true;
        if (playQuestButton != null) playQuestButton.interactable = false;
        StartCoroutine(PlaySelectedQuestFlow());
    }

    private IEnumerator PlaySelectedQuestFlow()
    {
        LogVerbose("[QuetesTab] 🎮 JOUER: === PlaySelectedQuestFlow DÉMARRÉ ===");
        
        // Déterminer mapId depuis l'index dans la liste (plus robuste que questId-1)
        int questId = selectedQuest.id;
        int idx = questsData != null ? questsData.FindIndex(q => q != null && q.id == questId) : -1;
        if (idx < 0) idx = Mathf.Max(0, questId - 1);
        string mapId = $"map-Q{idx}";

        LogVerbose($"[QuetesTab] 🎮 JOUER: questId={questId}, idx={idx}, mapId={mapId}");
        
        PlayerPrefs.SetString(PendingQuestPreviewMapIdKey, mapId);
        PlayerPrefs.SetInt(PendingQuestPreviewQuestIdKey, questId);
        PlayerPrefs.Save();
        
        LogVerbose($"[QuetesTab] 🎮 JOUER: ✅ PlayerPrefs sauvegardés - mapId='{mapId}', questId={questId}");

        // 🧹 CORRECTION OOM WebGL: Nettoyer les ressources AVANT de charger la scène
        LogVerbose("[QuetesTab] 🎮 JOUER: 🧹 Nettoyage des ressources...");
        
        // 1. Nettoyer les ressources de QuetesTab (textures, sprites, etc.)
        CleanupDetailResources(); // Nettoyer les détails (avatars, badges, images)
        
        // 2. Nettoyer les ressources persistantes de tous les onglets
        var allQueteTabs = FindObjectsByType<QuetesTab>(FindObjectsSortMode.None);
        foreach (var qt in allQueteTabs)
        {
            if (qt != null && qt != this)
            {
                try { qt.CleanupOnPanelClose(); } catch { }
            }
        }
        
        // 3. Nettoyer les ressources des autres onglets s'ils ont des sprites/textures
        var allScoresTabs = FindObjectsByType<ScoresTab>(FindObjectsSortMode.None);
        foreach (var st in allScoresTabs)
        {
            if (st != null)
            {
                // ScoresTab sera nettoyé automatiquement lors de la destruction de la scène
                try { st.gameObject.SetActive(false); } catch { }
            }
        }
        
        var allClassementTabs = FindObjectsByType<ClassementTab>(FindObjectsSortMode.None);
        foreach (var ct in allClassementTabs)
        {
            if (ct != null)
            {
                // ClassementTab sera nettoyé automatiquement lors de la destruction de la scène
                try { ct.gameObject.SetActive(false); } catch { }
            }
        }
        
        // 3. WebGL: Libérer la mémoire de manière agressive pour éviter abort("OOM")
#if UNITY_WEBGL && !UNITY_EDITOR
        LogVerbose("[QuetesTab] 🎮 JOUER: 🧹 WebGL - Nettoyage mémoire agressif...");
        
        // Purger les caches persistants (comme dans CleanupPoints)
        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 (coûteux mais nécessaire en WebGL)
        LogVerbose("[QuetesTab] 🎮 JOUER: 🧹 Resources.UnloadUnusedAssets()...");
        yield return Resources.UnloadUnusedAssets();
        
        // Garbage collection
        try { System.GC.Collect(); } catch { }
        
        // Encore un frame pour finaliser le nettoyage
        yield return null;
        
        LogVerbose("[QuetesTab] 🎮 JOUER: ✅ Nettoyage mémoire WebGL terminé");
#else
        // Même en dehors de WebGL, attendre un frame pour que les Destroy() prennent effet
        yield return null;
#endif

        // 4. Maintenant on peut charger la scène en toute sécurité
        LogVerbose("[QuetesTab] 🎮 JOUER: 🚀 Chargement de la scène 'main'...");
        
        if (UnifiedLoadingManager.Instance != null)
        {
            UnifiedLoadingManager.LoadMenuScene("main");
        }
        else
        {
            SceneManager.LoadScene("main");
        }
        
        yield break; // La scène va être rechargée, pas besoin de continuer
    }

    private void EnsurePlayButtonExists()
    {
        if (playQuestButtonObj != null) return;
        
        if (detailsBlock == null)
        {
            Transform existingDetailsBlock = transform.Find("DetailsBlock");
            if (existingDetailsBlock != null)
                detailsBlock = existingDetailsBlock.gameObject;
            else
            {
                CreatePlayButtonFallback();
                return;
            }
        }
        
        if (config != null && config.detailsBlock != null)
            CreatePlaySelectedQuestButtonInDetailsBlock(config.detailsBlock);
        else
            CreatePlayButtonFallback();
    }
    
    private void CreatePlayButtonFallback()
    {
        if (playQuestButtonObj != null) return;
        
        Transform parent = detailsBlock?.transform ?? transform;
        
        // Valeurs par défaut
        float buttonWidth = 300f;
        float buttonHeight = 80f;
        float borderRadius = 35f;
        float borderWidth = 4f;
        Color startColor = HexToColor("#CE9BFD");
        Color endColor = HexToColor("#9A2DFF");
        Color borderColor = HexToColor("#f5ece5");
        Color textColor = Color.white;
        float textFontSize = 28f;
        float padding = 20f;
        
        // Essayer d'obtenir le style depuis la config
        ButtonStyleConfig buttonStyle = GeneralConfigManager.Instance?.GetButtonStyle("validationDefault");
        if (buttonStyle != null)
        {
            buttonWidth = buttonStyle.width;
            buttonHeight = buttonStyle.height;
            borderRadius = buttonStyle.borderRadius;
            borderWidth = buttonStyle.borderWidth;
            if (!string.IsNullOrEmpty(buttonStyle.gradient?.startColor)) startColor = HexToColor(buttonStyle.gradient.startColor);
            if (!string.IsNullOrEmpty(buttonStyle.gradient?.endColor)) endColor = HexToColor(buttonStyle.gradient.endColor);
            if (!string.IsNullOrEmpty(buttonStyle.borderColor)) borderColor = HexToColor(buttonStyle.borderColor);
            if (!string.IsNullOrEmpty(buttonStyle.text?.color)) textColor = HexToColor(buttonStyle.text.color);
            if (buttonStyle.text?.fontSize > 0) textFontSize = buttonStyle.text.fontSize;
        }
        
        playQuestButtonObj = new GameObject("PlaySelectedQuestButton");
        playQuestButtonObj.transform.SetParent(parent, false);
        playQuestButtonObj.transform.SetAsLastSibling();
        
        RectTransform buttonRect = playQuestButtonObj.AddComponent<RectTransform>();
        buttonRect.anchorMin = new Vector2(0.5f, 0f);
        buttonRect.anchorMax = new Vector2(0.5f, 0f);
        buttonRect.pivot = new Vector2(0.5f, 0f);
        buttonRect.sizeDelta = new Vector2(buttonWidth, buttonHeight);
        buttonRect.anchoredPosition = new Vector2(0, padding);
        
        Image buttonBg = playQuestButtonObj.AddComponent<Image>();
        buttonBg.sprite = CreateGradientSpriteWithBorder((int)buttonWidth, (int)buttonHeight, borderRadius,
            startColor, endColor, borderColor, borderWidth);
        buttonBg.type = Image.Type.Simple;
        buttonBg.color = Color.white;
        buttonBg.raycastTarget = true;

        // ✅ Ombre depuis le style validationDefault (pour correspondre au bouton Intro)
        if (buttonStyle?.shadow != null && buttonStyle.shadow.enabled)
        {
            Shadow shadow = playQuestButtonObj.GetComponent<Shadow>();
            if (shadow == null) shadow = playQuestButtonObj.AddComponent<Shadow>();
            shadow.effectColor = HexToColor(buttonStyle.shadow.color ?? "#00000040");
            shadow.effectDistance = new Vector2(buttonStyle.shadow.offsetX, -buttonStyle.shadow.offsetY);
        }
        
        playQuestButton = playQuestButtonObj.AddComponent<Button>();
        playQuestButton.targetGraphic = buttonBg;
        playQuestButton.transition = Selectable.Transition.ColorTint;
        
        ColorBlock colors = playQuestButton.colors;
        colors.normalColor = Color.white;
        colors.highlightedColor = new Color(1.05f, 1.05f, 1.05f, 1f);
        colors.pressedColor = new Color(0.9f, 0.9f, 0.9f, 1f);
        playQuestButton.colors = colors;
        
        GameObject textObj = new GameObject("Text");
        textObj.transform.SetParent(playQuestButtonObj.transform, false);
        RectTransform textRect = textObj.AddComponent<RectTransform>();
        textRect.anchorMin = Vector2.zero;
        textRect.anchorMax = Vector2.one;
        textRect.sizeDelta = Vector2.zero;
        
        playQuestButtonText = textObj.AddComponent<TextMeshProUGUI>();
        playQuestButtonText.text = "JOUER";
        playQuestButtonText.fontSize = textFontSize;
        playQuestButtonText.color = textColor;
        playQuestButtonText.alignment = TextAlignmentOptions.Center;
        playQuestButtonText.verticalAlignment = VerticalAlignmentOptions.Middle;
        playQuestButtonText.raycastTarget = false;
        
        TMP_FontAsset antonFont = Resources.Load<TMP_FontAsset>("Fonts/Anton-Regular SDF");
        if (antonFont != null) playQuestButtonText.font = antonFont;
        
        playQuestButton.onClick.AddListener(OnPlaySelectedQuestClicked);
        playQuestButtonObj.SetActive(false);
        
        LogVerbose("[QuetesTab] 🎮 JOUER: Bouton créé (fallback)");
    }

    private void UpdatePlayQuestButtonState()
    {
        if (playQuestButtonObj == null) EnsurePlayButtonExists();
        if (playQuestButtonObj == null) return;

        bool shouldShow = selectedQuest != null;
        playQuestButtonObj.SetActive(shouldShow);
        
        if (playQuestButton != null)
            playQuestButton.interactable = shouldShow && !isLaunchingFromQuetesTab;

        // Mettre à jour le libellé en fonction de l'état d'avancement de la quête sélectionnée
        UpdatePlayQuestButtonLabel();
    }

    private void UpdatePlayQuestButtonLabel()
    {
        // Tenter de récupérer la référence au texte si elle a été perdue
        if (playQuestButtonText == null && playQuestButtonObj != null)
        {
            Transform t = playQuestButtonObj.transform.Find("Text");
            if (t != null) playQuestButtonText = t.GetComponent<TextMeshProUGUI>();
        }

        if (playQuestButtonText == null) return;

        // Règle demandée:
        // - si completion > 0 et < 100 => "REPRENDRE"
        // - sinon => "JOUER"
        playQuestButtonText.text = IsSelectedQuestInProgress() ? "REPRENDRE" : "JOUER";
    }

    private bool IsSelectedQuestInProgress()
    {
        if (selectedQuest == null) return false;
        if (selectedQuest.user == null || selectedQuest.user.Count == 0) return false;

        // Si UNE des difficultés a une progression entre 1 et 99%, on considère la quête "en cours".
        for (int i = 0; i < selectedQuest.user.Count; i++)
        {
            QuestUserProgress p = selectedQuest.user[i];
            if (p == null) continue;
            if (p.completion > 0 && p.completion < 100) return true;
        }

        return false;
    }
    
    void CreateDetailsTabsMenu()
    {
        var menuConfig = config.detailsTabsMenu;
        
        detailsTabsMenu = new GameObject("DetailsTabsMenu");
        detailsTabsMenu.transform.SetParent(transform, false);
        
        RectTransform rect = detailsTabsMenu.AddComponent<RectTransform>();
        rect.anchorMin = new Vector2(0, 1);
        rect.anchorMax = new Vector2(0, 1);
        rect.pivot = new Vector2(0, 1);
        rect.anchoredPosition = new Vector2(menuConfig.x, -menuConfig.y);
        rect.sizeDelta = new Vector2(menuConfig.width, menuConfig.height);
        
        // Container pour les onglets
        GameObject tabsContainer = new GameObject("TabsContainer");
        tabsContainer.transform.SetParent(detailsTabsMenu.transform, false);
        
        RectTransform tabsRect = tabsContainer.AddComponent<RectTransform>();
        tabsRect.anchorMin = new Vector2(0, 0);
        tabsRect.anchorMax = new Vector2(1, 1);
        tabsRect.offsetMin = new Vector2(menuConfig.paddingHorizontal, menuConfig.paddingVertical);
        tabsRect.offsetMax = new Vector2(-menuConfig.paddingHorizontal, -menuConfig.paddingVertical);
        
        HorizontalLayoutGroup layout = tabsContainer.AddComponent<HorizontalLayoutGroup>();
        layout.spacing = menuConfig.tabSpacing;
        layout.childAlignment = TextAnchor.MiddleCenter;
        layout.childControlWidth = false;
        layout.childControlHeight = false;
        layout.childForceExpandWidth = false;
        layout.childForceExpandHeight = false;
        
        CreateDetailTabButton(tabsContainer.transform, "PERSONNAGES", 0);
        CreateDetailTabButton(tabsContainer.transform, "BADGES", 1);
        CreateDetailTabButton(tabsContainer.transform, "FORMATIONS", 2);
    }
    
    void CreateDetailsBlock()
    {
        var detailsConfig = config.detailsBlock;
        if (detailsConfig == null) return;
        
        detailsBlock = new GameObject("DetailsBlock");
        detailsBlock.transform.SetParent(transform, false);
        
        RectTransform rect = detailsBlock.AddComponent<RectTransform>();
        rect.anchorMin = new Vector2(0, 1);
        rect.anchorMax = new Vector2(0, 1);
        rect.pivot = new Vector2(0, 1);
        rect.anchoredPosition = new Vector2(detailsConfig.x, -detailsConfig.y);
        rect.sizeDelta = new Vector2(detailsConfig.width, detailsConfig.height);
        
        // Image de fond avec coins arrondis (sprite blanc + couleur via Image.color)
        Image bgImage = detailsBlock.AddComponent<Image>();
        bgImage.sprite = CreateRoundedSprite((int)detailsConfig.width, (int)detailsConfig.height,
            detailsConfig.cornerRadius, Color.white);
        bgImage.color = HexToColor(detailsConfig.backgroundColor);
        bgImage.type = Image.Type.Simple;
        bgImage.raycastTarget = false;
        
        // Ombre
        if (detailsConfig.shadowEnabled)
        {
            Shadow shadow = detailsBlock.AddComponent<Shadow>();
            shadow.effectColor = HexToColor(detailsConfig.shadowColor);
            shadow.effectDistance = new Vector2(0, -detailsConfig.shadowOffsetY);
        }
        
        CreateDetailsContent();
        
        LogVerbose("[QuetesTab] 🎮 JOUER: CreateDetailsBlock -> appel CreatePlaySelectedQuestButtonInDetailsBlock");
        CreatePlaySelectedQuestButtonInDetailsBlock(detailsConfig);
    }

    private void CreatePlaySelectedQuestButtonInDetailsBlock(QuetesTabDetailsBlockConfig detailsConfig)
    {
        LogVerbose($"[QuetesTab] 🎮 JOUER: CreateButton APPELÉ - playQuestButtonObj={playQuestButtonObj != null}, detailsBlock={detailsBlock != null}, detailsConfig={detailsConfig != null}");
        
        if (playQuestButtonObj != null)
        {
            LogVerbose("[QuetesTab] 🎮 JOUER: SKIP - bouton existe déjà");
            return;
        }
        if (detailsBlock == null || detailsConfig == null)
        {
            Debug.LogError($"[QuetesTab] 🎮 JOUER: ÉCHEC - detailsBlock={detailsBlock != null}, detailsConfig={detailsConfig != null}");
            return;
        }
        
        // Style validationDefault
        ButtonStyleConfig buttonStyle = GeneralConfigManager.Instance?.GetButtonStyle("validationDefault");
        float buttonWidth = buttonStyle?.width ?? 300f;
        float buttonHeight = buttonStyle?.height ?? 80f;
        float borderRadius = buttonStyle?.borderRadius ?? 35f;
        float borderWidth = buttonStyle?.borderWidth ?? 4f;
        Color startColor = HexToColor(buttonStyle?.gradient?.startColor ?? "#CE9BFD");
        Color endColor = HexToColor(buttonStyle?.gradient?.endColor ?? "#9A2DFF");
        Color borderColor = HexToColor(buttonStyle?.borderColor ?? "#f5ece5");
        Color textColor = HexToColor(buttonStyle?.text?.color ?? "#FFFFFF");
        float textFontSize = buttonStyle?.text?.fontSize ?? 28f;

        // 🔧 NOUVEAU : Récupérer la position du bouton depuis general-config.json
        // positionX = 0 signifie centré, positionY est l'espace depuis le bord inférieur du detailsBlock
        float playButtonX = detailsConfig.playButton?.positionX ?? 0f;
        float playButtonY = detailsConfig.playButton?.positionY ?? 20f;
        
        // Padding du bloc de détails
        float padding = config.detailsContent?.padding ?? 20f;
        float bottomSpacing = 20f;

        // Ajuster la largeur si le cadre est trop étroit
        float maxWidth = detailsConfig.width - (padding * 2);
        buttonWidth = Mathf.Min(buttonWidth, maxWidth);

        playQuestButtonObj = new GameObject("PlaySelectedQuestButton");
        // 🔧 CORRECTION : Parent = parent du detailsBlock (le panelRoot) pour être EN DESSOUS et non à l'intérieur
        playQuestButtonObj.transform.SetParent(detailsBlock.transform.parent, false);
        
        // S'assurer que le bouton est au-dessus des autres éléments
        playQuestButtonObj.transform.SetAsLastSibling();

        RectTransform buttonRect = playQuestButtonObj.AddComponent<RectTransform>();
        
        // 🔧 CORRECTION : Ancrer au bas du detailsBlock (en utilisant la position Y du detailsBlock)
        // Le bouton sera positionné SOUS le detailsBlock
        buttonRect.anchorMin = new Vector2(0.5f, 1f); // Ancré en haut-centre du parent
        buttonRect.anchorMax = new Vector2(0.5f, 1f);
        buttonRect.pivot = new Vector2(0.5f, 1f); // Pivot en haut du bouton
        buttonRect.sizeDelta = new Vector2(buttonWidth, buttonHeight);
        
        // Position Y du detailsBlock + sa hauteur + espace = position du bouton
        float detailsBlockBottom = detailsConfig.y + detailsConfig.height;
        buttonRect.anchoredPosition = new Vector2(playButtonX, -detailsBlockBottom - playButtonY);
        
        LogVerbose($"[QuetesTab] 🎮 JOUER: Bouton positionné SOUS le detailsBlock - X={playButtonX}, Y offset={playButtonY}, Position finale Y={-detailsBlockBottom - playButtonY}");
        
        if (buttonWidth <= 0 || buttonHeight <= 0)
            buttonRect.sizeDelta = new Vector2(300f, 80f);

        Image buttonBg = playQuestButtonObj.AddComponent<Image>();
        buttonBg.sprite = CreateGradientSpriteWithBorder((int)buttonWidth, (int)buttonHeight, borderRadius,
            startColor, endColor, borderColor, borderWidth);
        buttonBg.type = Image.Type.Simple;
        buttonBg.color = Color.white;
        buttonBg.raycastTarget = true;

        // ✅ Ombre depuis le style validationDefault (même rendu que le bouton Intro)
        if (buttonStyle?.shadow != null && buttonStyle.shadow.enabled)
        {
            Shadow shadow = playQuestButtonObj.GetComponent<Shadow>();
            if (shadow == null) shadow = playQuestButtonObj.AddComponent<Shadow>();
            shadow.effectColor = HexToColor(buttonStyle.shadow.color ?? "#00000040");
            shadow.effectDistance = new Vector2(buttonStyle.shadow.offsetX, -buttonStyle.shadow.offsetY);
        }
        else
        {
            // Fallback cohérent (historique Intro)
            Shadow shadow = playQuestButtonObj.GetComponent<Shadow>();
            if (shadow == null) shadow = playQuestButtonObj.AddComponent<Shadow>();
            shadow.effectColor = new Color(0, 0, 0, 0.25f);
            shadow.effectDistance = new Vector2(0, -6);
        }

        playQuestButton = playQuestButtonObj.AddComponent<Button>();
        playQuestButton.targetGraphic = buttonBg;
        playQuestButton.transition = Selectable.Transition.ColorTint;

        ColorBlock colors = playQuestButton.colors;
        colors.normalColor = Color.white;
        colors.highlightedColor = new Color(1.05f, 1.05f, 1.05f, 1f);
        colors.pressedColor = new Color(0.9f, 0.9f, 0.9f, 1f);
        playQuestButton.colors = colors;

        GameObject textObj = new GameObject("Text");
        textObj.transform.SetParent(playQuestButtonObj.transform, false);
        RectTransform textRect = textObj.AddComponent<RectTransform>();
        textRect.anchorMin = Vector2.zero;
        textRect.anchorMax = Vector2.one;
        textRect.sizeDelta = Vector2.zero;

        playQuestButtonText = textObj.AddComponent<TextMeshProUGUI>();
        playQuestButtonText.text = "JOUER";
        playQuestButtonText.fontSize = textFontSize;
        playQuestButtonText.color = textColor;
        playQuestButtonText.alignment = TextAlignmentOptions.Center;
        playQuestButtonText.verticalAlignment = VerticalAlignmentOptions.Middle;
        playQuestButtonText.fontStyle = buttonStyle?.text?.GetFontStyle() ?? FontStyles.Normal;
        playQuestButtonText.raycastTarget = false;

        TMP_FontAsset antonFont = Resources.Load<TMP_FontAsset>("Fonts/Anton-Regular SDF");
        if (antonFont != null) playQuestButtonText.font = antonFont;

        playQuestButton.onClick.AddListener(OnPlaySelectedQuestClicked);

        // 🔧 SUPPRIMÉ : Plus besoin de réserver l'espace dans detailsContent car le bouton est maintenant EN DESSOUS

        playQuestButtonObj.SetActive(false);
        
        LogVerbose("[QuetesTab] 🎮 JOUER: Bouton créé");
    }
    
    void CreateDetailsHeader()
    {
        var headerConfig = config.detailsHeader;
        
        detailsHeader = new GameObject("DetailsHeader");
        detailsHeader.transform.SetParent(detailsBlock.transform, false);
        
        RectTransform rect = detailsHeader.AddComponent<RectTransform>();
        rect.anchorMin = new Vector2(0, 1);
        rect.anchorMax = new Vector2(1, 1);
        rect.pivot = new Vector2(0.5f, 1);
        rect.sizeDelta = new Vector2(0, headerConfig.height);
        rect.anchoredPosition = Vector2.zero;
        
        // Container pour les onglets
        GameObject tabsContainer = new GameObject("TabsContainer");
        tabsContainer.transform.SetParent(detailsHeader.transform, false);
        
        RectTransform tabsRect = tabsContainer.AddComponent<RectTransform>();
        tabsRect.anchorMin = new Vector2(0, 0);
        tabsRect.anchorMax = new Vector2(1, 1);
        tabsRect.offsetMin = new Vector2(headerConfig.paddingHorizontal, headerConfig.paddingVertical);
        tabsRect.offsetMax = new Vector2(-headerConfig.paddingHorizontal, -headerConfig.paddingVertical);
        
        HorizontalLayoutGroup layout = tabsContainer.AddComponent<HorizontalLayoutGroup>();
        layout.spacing = headerConfig.tabSpacing;
        layout.childAlignment = TextAnchor.MiddleCenter;
        layout.childControlWidth = false;
        layout.childControlHeight = false;
        layout.childForceExpandWidth = false;
        layout.childForceExpandHeight = false;
        
        // Créer les 3 boutons d'onglets
        CreateDetailTabButton(tabsContainer.transform, "PERSONNAGES", 0);
        CreateDetailTabButton(tabsContainer.transform, "BADGES", 1);
        CreateDetailTabButton(tabsContainer.transform, "FORMATIONS", 2);
        
        LogVerbose("[QuetesTab] ✅ Header des détails créé avec 3 onglets");
    }
    
    void CreateDetailTabButton(Transform parent, string label, int index)
    {
        var tabConfig = config.detailsTabsMenu.tabStyle;
        
        GameObject tabButton = new GameObject($"TabButton_{label}");
        tabButton.transform.SetParent(parent, false);
        
        RectTransform rect = tabButton.AddComponent<RectTransform>();
        rect.sizeDelta = new Vector2(tabConfig.width, tabConfig.height);
        
        LayoutElement layoutElement = tabButton.AddComponent<LayoutElement>();
        layoutElement.preferredWidth = tabConfig.width;
        layoutElement.preferredHeight = tabConfig.height;
        
        Image image = tabButton.AddComponent<Image>();
        image.sprite = CreateRoundedSprite((int)tabConfig.width, (int)tabConfig.height, 
            tabConfig.cornerRadius, Color.white);
        image.type = Image.Type.Simple;
        image.color = Color.clear; // Transparent par défaut
        image.raycastTarget = true;
        
        Button button = tabButton.AddComponent<Button>();
        button.targetGraphic = image;
        button.transition = Selectable.Transition.None;
        
        button.onClick.AddListener(() => {
            LogVerbose($"[QuetesTab] Clic sur l'onglet {label} (index {index})");
            ShowDetailTab(index);
        });
        
        // Texte
        GameObject textObj = new GameObject("Text");
        textObj.transform.SetParent(tabButton.transform, false);
        
        RectTransform textRect = textObj.AddComponent<RectTransform>();
        textRect.anchorMin = Vector2.zero;
        textRect.anchorMax = Vector2.one;
        textRect.offsetMin = Vector2.zero;
        textRect.offsetMax = Vector2.zero;
        
        TextMeshProUGUI text = textObj.AddComponent<TextMeshProUGUI>();
        text.text = label;
        text.fontSize = 32;
        // Couleur initiale : premier onglet sélectionné, les autres non
        text.color = (index == 0) ? HexToColor("#64477D") : HexToColor("#ac99b1");
        text.alignment = TextAlignmentOptions.Center;
        text.fontStyle = FontStyles.Normal;
        if (titleFont != null) text.font = titleFont;
        
        detailTabButtons.Add(button);
        
        LogVerbose($"[QuetesTab] Bouton d'onglet créé: {label}");
    }
    
    void CreateDetailsContent()
    {
        var contentConfig = config.detailsContent;
        
        // Le detailsContent occupe maintenant tout le bloc car le menu d'onglets est séparé au-dessus
        detailsContent = new GameObject("DetailsContent");
        detailsContent.transform.SetParent(detailsBlock.transform, false);
        
        RectTransform rect = detailsContent.AddComponent<RectTransform>();
        rect.anchorMin = new Vector2(0, 0);
        rect.anchorMax = new Vector2(1, 1);
        // Occupe tout le bloc avec du padding
        rect.offsetMin = new Vector2(contentConfig.padding, contentConfig.padding);
        rect.offsetMax = new Vector2(-contentConfig.padding, -contentConfig.padding);
        
        // Pas de cadre par défaut - il sera créé dynamiquement quand une quête est sélectionnée
        // Afficher le message par défaut
        ShowNoQuestSelectedMessage();
        
        LogVerbose("[QuetesTab] ✅ Zone de contenu des détails créée (occupe tout le bloc)");
    }
    
    void ShowNoQuestSelectedMessage()
    {
        // Nettoyer le contenu existant
        foreach (Transform child in detailsContent.transform)
        {
            Destroy(child.gameObject);
        }
        
        // Texte centré "Sélectionnez une quête"
        GameObject messageObj = new GameObject("NoQuestMessage");
        messageObj.transform.SetParent(detailsContent.transform, false);
        
        RectTransform rect = messageObj.AddComponent<RectTransform>();
        rect.anchorMin = Vector2.zero;
        rect.anchorMax = Vector2.one;
        rect.offsetMin = Vector2.zero;
        rect.offsetMax = Vector2.zero;
        
        TextMeshProUGUI text = messageObj.AddComponent<TextMeshProUGUI>();
        text.text = "Sélectionnez une quête";
        text.fontSize = 28;
        text.color = HexToColor("#8a7a9a");
        text.alignment = TextAlignmentOptions.Center;
        text.fontStyle = FontStyles.Italic;
        if (textFont != null) text.font = textFont;
    }
    
    void PopulateQuestList()
    {
        LogVerbose($"[QUESTLIST_DEBUG] 🔄 PopulateQuestList() appelé - {questsData.Count} quêtes à afficher");
        
        // Vider le dictionnaire des références
        questItemsMap.Clear();
        
        if (questListBlock == null)
        {
            Debug.LogError("[QuetesTab] ❌ questListBlock est null!");
            return;
        }
        
        Transform viewport = questListBlock.transform.Find("Viewport");
        if (viewport == null)
        {
            Debug.LogError("[QuetesTab] ❌ Viewport non trouvé!");
            return;
        }
        
        Transform content = viewport.Find("Content");
        if (content == null)
        {
            Debug.LogError("[QuetesTab] ❌ Content non trouvé!");
            return;
        }
        
        // Nettoyer le contenu existant
        while (content.childCount > 0)
        {
            DestroyImmediate(content.GetChild(0).gameObject);
        }
        
        if (questsData.Count == 0)
        {
            Debug.LogWarning("[QuetesTab] ⚠️ Aucune quête à afficher");
            return;
        }
        
        var listConfig = config.questListBlock;
        var itemConfig = listConfig.itemStyle;
        // Largeur ajustée pour tenir compte de la scrollbar (8px) + spacing (8px)
        float scrollbarWidth = 8f;
        float scrollbarSpacing = 8f;
        float itemWidth = listConfig.width - listConfig.padding * 2 - scrollbarWidth - scrollbarSpacing;
        float itemHeight = itemConfig.height;
        float spacing = listConfig.itemSpacing;
        
        // Calculer la hauteur totale du content
        float totalHeight = questsData.Count * itemHeight + (questsData.Count - 1) * spacing;
        
        RectTransform contentRect = content.GetComponent<RectTransform>();
        contentRect.sizeDelta = new Vector2(itemWidth, totalHeight);
        
        LogVerbose($"[QUESTLIST_DEBUG] 📐 Content size définie à: {itemWidth}x{totalHeight}");
        
        // Créer chaque item avec positionnement manuel
        float currentY = 0;
        for (int i = 0; i < questsData.Count; i++)
        {
            Quest quest = questsData[i];
            if (quest != null)
            {
                CreateQuestListItemManual(content, quest, itemWidth, itemHeight, currentY);
                currentY -= itemHeight + spacing;
            }
        }
        
        LogVerbose($"[QUESTLIST_DEBUG] ✅ {questsData.Count} quêtes créées manuellement");
        
        // Vérifier le premier item
        if (content.childCount > 0)
        {
            RectTransform firstItem = content.GetChild(0).GetComponent<RectTransform>();
            LogVerbose($"[QUESTLIST_DEBUG] 📐 Premier item - pos: {firstItem.anchoredPosition}, size: {firstItem.sizeDelta}");
        }
        
        // Sélectionner la première quête avec un petit délai pour s'assurer que tout est prêt
        if (questsData.Count > 0)
        {
            StartCoroutine(SelectFirstQuestWithDelay());
        }
    }
    
    IEnumerator SelectFirstQuestWithDelay()
    {
        // Attendre que l'interface soit complètement prête
        yield return new WaitForEndOfFrame();
        yield return new WaitForSeconds(0.1f);
        
        if (questsData == null || questsData.Count == 0)
        {
            yield break;
        }
        
        // Déterminer quelle quête sélectionner
        Quest questToSelect = null;
        
        // Vérifier si on vient d'une map (CurrentMapId est défini)
        string currentMapId = PlayerPrefs.GetString("CurrentMapId", "");
        if (!string.IsNullOrEmpty(currentMapId) && currentMapId.StartsWith("map-Q"))
        {
            // Extraire l'index de la map (map-Q0 -> 0, map-Q1 -> 1, etc.)
            string indexStr = currentMapId.Replace("map-Q", "");
            if (int.TryParse(indexStr, out int mapIndex))
            {
                // mapIndex correspond à la position dans la liste des quêtes
                if (mapIndex >= 0 && mapIndex < questsData.Count)
                {
                    questToSelect = questsData[mapIndex];
                    LogVerbose($"[QuetesTab] 🎯 Sélection automatique de la quête de la map actuelle (index {mapIndex}): {questToSelect.title}");
                }
                else
                {
                    Debug.LogWarning($"[QuetesTab] ⚠️ Index de map {mapIndex} hors limites ({questsData.Count} quêtes), sélection de la première");
                    questToSelect = questsData[0];
                }
            }
            else
            {
                Debug.LogWarning($"[QuetesTab] ⚠️ Impossible d'extraire l'index de '{currentMapId}', sélection de la première");
                questToSelect = questsData[0];
            }
        }
        else
        {
            // Pas de CurrentMapId ou format invalide -> on est dans la scène Main
            questToSelect = questsData[0];
            LogVerbose($"[QuetesTab] 🎯 Sélection automatique de la première quête (scène Main): {questToSelect.title}");
        }
        
        if (questToSelect != null)
        {
            SelectQuest(questToSelect);
        }
    }
    
    void CreateQuestListItemManual(Transform parent, Quest quest, float width, float height, float yPos)
    {
        var itemConfig = config.questListBlock.itemStyle;
        var lockedConfig = itemConfig.lockedStyle;
        
        // Déterminer si la quête est verrouillée
        bool isLocked = quest.status == "locked";
        
        GameObject item = new GameObject($"QuestItem_{quest.id}");
        item.transform.SetParent(parent, false);
        
        RectTransform itemRect = item.AddComponent<RectTransform>();
        // Positionnement manuel - ancré en haut à gauche du parent
        itemRect.anchorMin = new Vector2(0, 1);
        itemRect.anchorMax = new Vector2(0, 1);
        itemRect.pivot = new Vector2(0, 1);
        itemRect.anchoredPosition = new Vector2(0, yPos);
        itemRect.sizeDelta = new Vector2(width, height);
        
        LogVerbose($"[QUESTLIST_DEBUG] 🔨 Item '{quest.title}' créé - pos: (0, {yPos}), size: ({width}, {height}), locked: {isLocked}");
        
        // Fond avec coins arrondis - gris si locked
        Image bgImage = item.AddComponent<Image>();
        bgImage.sprite = CreateRoundedSprite((int)width, (int)height, itemConfig.cornerRadius, Color.white);
        
        string bgColor = isLocked && lockedConfig != null 
            ? lockedConfig.backgroundColor 
            : itemConfig.normalBackgroundColor;
        bgImage.color = HexToColor(bgColor);
        bgImage.type = Image.Type.Simple;
        bgImage.raycastTarget = true;
        
        // Bouton - désactivé si locked
        if (!isLocked)
        {
            Button button = item.AddComponent<Button>();
            button.targetGraphic = bgImage;
            button.transition = Selectable.Transition.None; // On gère manuellement les couleurs
            
            button.onClick.AddListener(() => {
                LogVerbose($"[QuetesTab] 🖱️ CLIC sur quête: {quest.title} (ID: {quest.id})");
                SelectQuest(quest);
            });
        }
        else
        {
            LogVerbose($"[QuetesTab] 🔒 Quête {quest.id} verrouillée - pas de bouton");
        }
        
        // Zone de texte qui remplit l'item avec padding
        float padding = itemConfig.paddingHorizontal;
        float paddingV = itemConfig.paddingVertical;
        float textWidth = width - padding * 2;
        
        // Couleurs du titre et description selon l'état
        string titleColorHex = isLocked && lockedConfig != null 
            ? lockedConfig.titleColor 
            : itemConfig.titleColor;
        string descColorHex = isLocked && lockedConfig != null 
            ? lockedConfig.descriptionColor 
            : itemConfig.descriptionColor;
        
        // Calculer la largeur du titre pour laisser de la place au cadenas si locked
        float lockIconSpace = 0;
        if (isLocked && lockedConfig != null)
        {
            lockIconSpace = lockedConfig.lockIconSize + lockedConfig.lockIconMarginLeft + 10;
        }
        
        // Titre en haut - centré horizontalement
        GameObject titleObj = new GameObject("Title");
        titleObj.transform.SetParent(item.transform, false);
        
        RectTransform titleRect = titleObj.AddComponent<RectTransform>();
        titleRect.anchorMin = new Vector2(0, 1);
        titleRect.anchorMax = new Vector2(0, 1);
        titleRect.pivot = new Vector2(0, 1);
        titleRect.anchoredPosition = new Vector2(padding, -paddingV);
        titleRect.sizeDelta = new Vector2(textWidth - lockIconSpace, 40); // Hauteur 40px pour le titre
        
        TextMeshProUGUI titleText = titleObj.AddComponent<TextMeshProUGUI>();
        titleText.text = (quest.title ?? "Titre manquant").ToUpper(); // En majuscules
        titleText.fontSize = itemConfig.titleFontSize;
        Color titleColorValue = HexToColor(titleColorHex);
        titleText.color = titleColorValue;
        titleText.fontStyle = FontStyles.Bold;
        titleText.alignment = TextAlignmentOptions.Center; // Texte centré dans la zone
        titleText.raycastTarget = false;
        titleText.overflowMode = TextOverflowModes.Truncate;
        // Police Anton pour le titre
        if (titleFont != null) titleText.font = titleFont;
        
        LogVerbose($"[QUESTLIST_DEBUG] 📝 Titre: '{titleText.text}' - couleur: {titleColorValue}, fontSize: {itemConfig.titleFontSize}, font: {(titleText.font != null ? titleText.font.name : "DEFAULT")}");
        
        // Ajouter l'icône cadenas si locked
        if (isLocked && lockedConfig != null)
        {
            GameObject lockObj = new GameObject("LockIcon");
            lockObj.transform.SetParent(item.transform, false);
            
            RectTransform lockRect = lockObj.AddComponent<RectTransform>();
            lockRect.anchorMin = new Vector2(1, 1);
            lockRect.anchorMax = new Vector2(1, 1);
            lockRect.pivot = new Vector2(1, 0.5f);
            // Positionné à droite du titre, aligné verticalement avec lui
            lockRect.anchoredPosition = new Vector2(-padding, -(paddingV + 20)); // 20 = moitié de 40px hauteur titre
            lockRect.sizeDelta = new Vector2(lockedConfig.lockIconSize, lockedConfig.lockIconSize);
            
            // D'abord essayer de charger l'image
            string lockIconUrl = GeneralConfigManager.Instance?.GetUIUrl(lockedConfig.lockIconUrl) ?? "";
            LogVerbose($"[QuetesTab] 🔒 URL cadenas construite: '{lockIconUrl}' (depuis '{lockedConfig.lockIconUrl}')");
            
            if (!string.IsNullOrEmpty(lockIconUrl))
            {
                Image lockImage = lockObj.AddComponent<Image>();
                lockImage.preserveAspect = true;
                lockImage.raycastTarget = false;
                lockImage.color = Color.clear; // Transparent jusqu'au chargement du sprite
                lockImage.type = Image.Type.Simple;
                
                // Lancer la coroutine sur GeneralConfigManager qui reste actif
                // (évite l'interruption quand l'onglet est désactivé)
                if (GeneralConfigManager.Instance != null)
                {
                    GeneralConfigManager.Instance.StartCoroutine(LoadLockIconSprite(lockIconUrl, lockImage, lockObj));
                }
                else
                {
                    StartCoroutine(LoadLockIconSprite(lockIconUrl, lockImage, lockObj));
                }
            }
            else
            {
                // Fallback: utiliser un texte cadenas
                Debug.LogWarning($"[QuetesTab] ⚠️ URL cadenas vide, utilisation du texte emoji");
                CreateLockTextFallback(lockObj, lockedConfig.lockIconSize);
            }
            
            LogVerbose($"[QuetesTab] 🔒 Icône cadenas ajoutée pour quête {quest.id}");
        }
        
        // Description sous le titre
        GameObject descObj = new GameObject("Description");
        descObj.transform.SetParent(item.transform, false);
        
        RectTransform descRect = descObj.AddComponent<RectTransform>();
        descRect.anchorMin = new Vector2(0, 1);
        descRect.anchorMax = new Vector2(0, 1);
        descRect.pivot = new Vector2(0, 1);
        descRect.anchoredPosition = new Vector2(padding, -(paddingV + 45)); // Sous le titre (40px + 5px spacing)
        descRect.sizeDelta = new Vector2(textWidth, height - paddingV * 2 - 50);
        
        TextMeshProUGUI descText = descObj.AddComponent<TextMeshProUGUI>();
        descText.text = quest.description ?? "Aucune description disponible";
        descText.fontSize = itemConfig.descriptionFontSize;
        descText.color = HexToColor(descColorHex);
        descText.alignment = TextAlignmentOptions.TopLeft;
        descText.overflowMode = TextOverflowModes.Truncate;
        descText.textWrappingMode = TMPro.TextWrappingModes.Normal;
        descText.maxVisibleLines = 4;
        descText.raycastTarget = false;
        if (textFont != null) descText.font = textFont;
        
        // Stocker la référence pour la sélection (même si locked, pour la mise à jour d'apparence)
        questItemsMap[quest.id] = item;
        
        // Marquer comme locked pour UpdateQuestItemsAppearance
        if (isLocked)
        {
            item.AddComponent<LockedQuestMarker>();
        }
        
        LogVerbose($"[QUESTLIST_DEBUG] ✅ Item complet créé pour: {quest.title}");
    }
    
    IEnumerator LoadLockIconSprite(string url, Image targetImage, GameObject lockObj)
    {
        LogVerbose($"[QuetesTab] 🔒 Tentative de chargement du cadenas: {url}");
        
        using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(url))
        {
            yield return request.SendWebRequest();
            
            if (request.result == UnityWebRequest.Result.Success)
            {
                Texture2D texture = DownloadHandlerTexture.GetContent(request);
                LogVerbose($"[QuetesTab] 🔒 Texture chargée: {texture.width}x{texture.height}, format: {texture.format}");
                Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), 
                    new Vector2(0.5f, 0.5f), 100f);

                // Ressource persistante (liste des quêtes)
                TrackPersistent(texture, sprite);
                    
                if (targetImage != null)
                {
                    targetImage.sprite = sprite;
                    targetImage.color = Color.white; // Important : couleur blanche pour afficher le sprite correctement
                    targetImage.type = Image.Type.Simple;
                    LogVerbose($"[QuetesTab] ✅ Icône cadenas chargée et assignée avec succès");
                }
            }
            else
            {
                Debug.LogError($"[QuetesTab] ❌ Impossible de charger l'icône cadenas: {url}");
                Debug.LogError($"[QuetesTab] ❌ Erreur: {request.error}, Code: {request.responseCode}");
                
                // En cas d'échec, supprimer l'image et utiliser un texte emoji cadenas
                if (lockObj != null && targetImage != null)
                {
                    Destroy(targetImage);
                    float iconSize = config?.questListBlock?.itemStyle?.lockedStyle?.lockIconSize ?? 24;
                    CreateLockTextFallback(lockObj, iconSize);
                }
            }
        }
    }
    
    void CreateLockTextFallback(GameObject parent, float size)
    {
        // Créer un texte avec le symbole cadenas Unicode
        TextMeshProUGUI lockText = parent.AddComponent<TextMeshProUGUI>();
        lockText.text = "🔒"; // Emoji cadenas
        lockText.fontSize = size * 0.8f;
        lockText.color = HexToColor("#808080");
        lockText.alignment = TextAlignmentOptions.Center;
        lockText.raycastTarget = false;
        
        LogVerbose("[QuetesTab] ✅ Fallback texte cadenas créé");
    }
    
    void SelectQuest(Quest quest)
    {
        // **PROTECTION : Empêcher les clics multiples rapides**
        float currentTime = Time.realtimeSinceStartup;
        if (currentTime - lastClickTime < CLICK_DEBOUNCE_TIME)
        {
            LogVerbose($"[QuetesTab] ⏱️ Clic ignoré (debounce): {currentTime - lastClickTime:F3}s depuis le dernier clic");
            return;
        }
        lastClickTime = currentTime;
        
        if (isSelectingQuest)
        {
            Debug.LogWarning("[QuetesTab] ⚠️ Sélection déjà en cours, ignoré");
            return;
        }
        
        LogVerbose($"[QuetesTab] 🎯 SelectQuest() appelé pour: {quest?.title ?? "NULL"} (ID: {quest?.id ?? -1})");
        
        if (quest == null)
        {
            Debug.LogError("[QuetesTab] ❌ Quest est NULL!");
            return;
        }
        
        isSelectingQuest = true;
        StartCoroutine(SelectQuestCoroutine(quest));
    }
    
    IEnumerator SelectQuestCoroutine(Quest quest)
    {
        // Changement de quête => libérer immédiatement toutes les ressources liées aux détails précédents
        // (sinon OOM WebGL après quelques clics)
        CleanupDetailResources();

        selectedQuest = quest;
        LogVerbose($"[QuetesTab] ✅ selectedQuest défini: {selectedQuest.title}");

        // ✅ IMPORTANT: au changement de quête, revenir par défaut sur PERSONNAGES AVANT le refresh API.
        // Sinon LoadQuestDetailsFromApi() ré-affiche l'ancien onglet actif (ex: FORMATIONS) et on se retrouve
        // avec le menu PERSONNAGES sélectionné mais le contenu FORMATIONS encore visible.
        activeDetailTab = 0;
        UpdateDetailTabButtons(0);

        // GARANTIR que le bouton JOUER existe avant de le mettre à jour
        EnsurePlayButtonExists();

        // Afficher/mettre à jour le bouton JOUER
        UpdatePlayQuestButtonState();
        
        // Mettre à jour l'apparence de tous les items de quête
        UpdateQuestItemsAppearance(quest.id);

        // 🔧 NOUVEAU : s'assurer que la quête sélectionnée est visible dans la liste (auto-scroll)
        StartCoroutine(ScrollSelectedQuestIntoView(quest.id));
        
        if (detailsContent == null)
        {
            Debug.LogError("[QuetesTab] ❌ detailsContent est NULL! Impossible d'afficher les détails.");
            isSelectingQuest = false; // Libérer le flag
            yield break;
        }
        
        LogVerbose($"[QuetesTab] ✅ detailsContent existe: {detailsContent.name}");
        
        // Appeler l'API pour récupérer les détails de la quête (personnages, badges, etc.)
        LogVerbose("[QuetesTab] 🚀 Démarrage de LoadQuestDetailsFromApi...");
        yield return StartCoroutine(LoadQuestDetailsFromApi(quest.id));
        
        isSelectingQuest = false; // Libérer le flag
    }

    /// <summary>
    /// Fait défiler automatiquement la liste des quêtes pour que l'item sélectionné soit visible.
    /// Utile quand on ouvre Options->Quêtes depuis Map et que la quête est loin en bas de la liste.
    /// </summary>
    IEnumerator ScrollSelectedQuestIntoView(int questId)
    {
        // Attendre 1 frame pour laisser le ScrollRect/layout se stabiliser
        yield return null;
        Canvas.ForceUpdateCanvases();

        if (questListBlock == null) yield break;
        if (!questItemsMap.TryGetValue(questId, out var item) || item == null) yield break;

        ScrollRect scrollRect = questListBlock.GetComponent<ScrollRect>();
        if (scrollRect == null || scrollRect.content == null || scrollRect.viewport == null) yield break;

        RectTransform content = scrollRect.content;
        RectTransform viewport = scrollRect.viewport;
        RectTransform itemRect = item.GetComponent<RectTransform>();
        if (content == null || viewport == null || itemRect == null) yield break;

        float contentH = content.rect.height;
        float viewportH = viewport.rect.height;
        if (contentH <= viewportH + 0.01f)
        {
            scrollRect.verticalNormalizedPosition = 1f; // tout visible
            yield break;
        }

        float currentScrollY = content.anchoredPosition.y;

        // Avec pivot haut (0,1), itemRect.anchoredPosition.y est négatif quand l'item est plus bas.
        float itemTop = -itemRect.anchoredPosition.y;
        float itemBottom = itemTop + itemRect.rect.height;

        float margin = 20f;
        float targetScrollY = currentScrollY;

        // Si le haut de l'item est au-dessus de la zone visible, remonter
        if (itemTop - margin < currentScrollY)
        {
            targetScrollY = Mathf.Max(0f, itemTop - margin);
        }
        // Si le bas de l'item est sous la zone visible, descendre
        else if (itemBottom + margin > currentScrollY + viewportH)
        {
            targetScrollY = Mathf.Min(contentH - viewportH, itemBottom + margin - viewportH);
        }

        // Appliquer via normalizedPosition (plus robuste que l'anchoredPosition direct)
        float normalized = 1f - (targetScrollY / (contentH - viewportH));
        scrollRect.StopMovement();
        scrollRect.verticalNormalizedPosition = Mathf.Clamp01(normalized);
    }
    
    void UpdateQuestItemsAppearance(int selectedQuestId)
    {
        var itemConfig = config.questListBlock.itemStyle;
        var lockedConfig = itemConfig.lockedStyle;
        
        foreach (var kvp in questItemsMap)
        {
            int questId = kvp.Key;
            GameObject item = kvp.Value;
            
            if (item == null) continue;
            
            // Vérifier si c'est une quête verrouillée
            bool isLocked = item.GetComponent<LockedQuestMarker>() != null;
            
            // Ne pas changer l'apparence des quêtes verrouillées
            if (isLocked)
            {
                continue;
            }
            
            bool isSelected = (questId == selectedQuestId);
            
            // Mettre à jour la couleur de fond
            Image bgImage = item.GetComponent<Image>();
            if (bgImage != null)
            {
                bgImage.color = isSelected 
                    ? HexToColor(itemConfig.selectedBackgroundColor) 
                    : HexToColor(itemConfig.normalBackgroundColor);
            }
            
            // Mettre à jour la couleur du titre
            Transform titleTransform = item.transform.Find("Title");
            if (titleTransform != null)
            {
                TextMeshProUGUI titleText = titleTransform.GetComponent<TextMeshProUGUI>();
                if (titleText != null)
                {
                    titleText.color = isSelected ? Color.white : HexToColor(itemConfig.titleColor);
                }
            }
            
            // Mettre à jour la couleur de la description
            Transform descTransform = item.transform.Find("Description");
            if (descTransform != null)
            {
                TextMeshProUGUI descText = descTransform.GetComponent<TextMeshProUGUI>();
                if (descText != null)
                {
                    descText.color = isSelected ? Color.white : HexToColor(itemConfig.descriptionColor);
                }
            }
        }
    }
    
    IEnumerator LoadQuestDetailsFromApi(int questId)
    {
        LogVerbose($"[QuetesTab] 📡 Chargement des détails de la quête {questId}...");
        
        // Afficher un message de chargement
        ShowLoadingMessage("Chargement des détails...");
        
        // Construire l'URL de l'API
        string apiUrl = GeneralConfigManager.Instance?.GetQuestConfigApiUrl(questId);
        
        if (string.IsNullOrEmpty(apiUrl))
        {
            Debug.LogError("[QuetesTab] ❌ Impossible de construire l'URL de l'API");
            ShowDetailTab(activeDetailTab);
            yield break;
        }
        
        // Récupérer le token d'authentification
        string token = UserDataManager.Instance?.token;
        
        if (string.IsNullOrEmpty(token))
        {
            Debug.LogWarning("[QuetesTab] ⚠️ Pas de token d'authentification");
            ShowDetailTab(activeDetailTab);
            yield break;
        }
        
        LogVerbose($"[QuetesTab] 🌐 Appel API: {apiUrl}");
        
        using (UnityWebRequest www = UnityWebRequest.Get(apiUrl))
        {
            www.SetRequestHeader("Authorization", $"Bearer {token}");
            yield return www.SendWebRequest();
            
            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"[QuetesTab] ❌ Erreur API: {www.error}");
                Debug.LogError($"[QuetesTab] Code: {www.responseCode}");
                ShowDetailTab(activeDetailTab);
                yield break;
            }
            
            string jsonResponse = www.downloadHandler.text;
            LogVerbose($"[QuetesTab] ✅ Réponse reçue ({jsonResponse.Length} caractères)");
            LogVerbose($"[QuetesTab] 🔍 Début réponse: {jsonResponse.Substring(0, Mathf.Min(500, jsonResponse.Length))}");
            
            // Parser la réponse
            try
            {
                // Log la réponse brute pour debug
                LogVerbose($"[QuetesTab] 📄 Réponse JSON brute (premiers 1000 caractères):\n{jsonResponse.Substring(0, Mathf.Min(1000, jsonResponse.Length))}");
                
                QuestDetailApiResponse response = JsonUtility.FromJson<QuestDetailApiResponse>(jsonResponse);
                
                if (response != null && response.status == "success" && response.data != null)
                {
                    LogVerbose($"[QuetesTab] ✅ Détails de la quête chargés");
                    
                    // Stocker les personnages
                    if (response.data.characters != null && response.data.characters.Count > 0)
                    {
                        questCharacters[questId] = response.data.characters;
                        LogVerbose($"[QuetesTab] 👥 {response.data.characters.Count} personnages chargés");
                        
                        foreach (var character in response.data.characters)
                        {
                            // Log toutes les propriétés pour debug
                            Debug.Log($"[QuetesTab] 👤 Personnage: id={character.id}, name='{character.name}', revealed={character.revealed}");
                            Debug.Log($"[QuetesTab]   - image='{character.image}', short_desc='{character.short_description}'");
                            Debug.Log($"[QuetesTab]   - GetName()='{character.GetName()}', GetAvatarUrl()='{character.GetAvatarUrl()}'");
                        }
                    }
                    else
                    {
                        LogVerbose("[QuetesTab] ⚠️ Aucun personnage dans la réponse API");
                    }
                    
                    // Stocker les badges si présents
                    if (response.data.badges != null && response.data.badges.Count > 0)
                    {
                        LogVerbose($"[QuetesTab] 🏅 {response.data.badges.Count} badges chargés");
                    }
                    
                    // Stocker les formations dans selectedQuest
                    if (response.data.trainings != null && response.data.trainings.Count > 0)
                    {
                        selectedQuest.trainings = response.data.trainings;
                        LogVerbose($"[QuetesTab] 📚 {response.data.trainings.Count} formations chargées");
                        foreach (var training in response.data.trainings)
                        {
                            LogVerbose($"[QuetesTab]   - Formation: '{training.title}' → {training.url}");
                        }
                    }
                    else
                    {
                        LogVerbose("[QuetesTab] ⚠️ Aucune formation dans la réponse API");
                    }
                }
                else
                {
                    Debug.LogWarning($"[QuetesTab] ⚠️ Réponse API invalide ou sans données");
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"[QuetesTab] ❌ Erreur parsing JSON: {e.Message}");
            }
        }
        
        // Afficher l'onglet actif avec les nouvelles données
        ShowDetailTab(activeDetailTab);
    }
    
    void ShowLoadingMessage(string message)
    {
        LogVerbose($"[QuetesTab] 📢 ShowLoadingMessage() appelé avec: '{message}'");
        
        if (detailsContent == null)
        {
            Debug.LogError("[QuetesTab] ❌ detailsContent est NULL dans ShowLoadingMessage!");
            return;
        }

        // On reconstruit le contenu des détails: nettoyer les ressources de détails avant de détruire l'UI
        CleanupDetailResources();
        SetScope(ResourceScope.Details);
        
        // Nettoyer le contenu existant
        foreach (Transform child in detailsContent.transform)
        {
            Destroy(child.gameObject);
        }
        
        GameObject loadingObj = new GameObject("LoadingMessage");
        loadingObj.transform.SetParent(detailsContent.transform, false);
        LogVerbose("[QuetesTab] ✅ LoadingMessage créé");
        
        RectTransform rect = loadingObj.AddComponent<RectTransform>();
        rect.anchorMin = Vector2.zero;
        rect.anchorMax = Vector2.one;
        rect.offsetMin = Vector2.zero;
        rect.offsetMax = Vector2.zero;
        
        TextMeshProUGUI text = loadingObj.AddComponent<TextMeshProUGUI>();
        text.text = message;
        text.fontSize = 24;
        text.color = HexToColor("#a95bfb");
        text.alignment = TextAlignmentOptions.Center;
        text.fontStyle = FontStyles.Italic;
        if (textFont != null) text.font = textFont;

        // Revenir au scope par défaut (liste persistante) après construction
        SetScope(ResourceScope.Persistent);
    }
    
    void ShowDetailTab(int index)
    {
        // **PROTECTION : Empêcher les clics multiples rapides sur les onglets**
        if (isSwitchingTab)
        {
            Debug.LogWarning("[QuetesTab] ⚠️ Changement d'onglet déjà en cours, ignoré");
            return;
        }

        isSwitchingTab = true;
        activeDetailTab = index;

        // Changement d'onglet => on reconstruit l'UI des détails => nettoyer ressources de détails
        CleanupDetailResources();
        SetScope(ResourceScope.Details);

        try
        {
            // Mettre à jour les couleurs des boutons d'onglets
            UpdateDetailTabButtons(index);

            // Afficher le contenu correspondant
            switch (index)
            {
                case 0: // PERSONNAGES
                    ShowPersonnagesContent();
                    break;
                case 1: // BADGES
                    ShowBadgesContent();
                    break;
                case 2: // FORMATIONS
                    ShowFormationsContent();
                    break;
            }
        }
        finally
        {
            SetScope(ResourceScope.Persistent);
            isSwitchingTab = false;
        }
    }
    
    void UpdateDetailTabButtons(int selectedIndex)
    {
        for (int i = 0; i < detailTabButtons.Count; i++)
        {
            Button button = detailTabButtons[i];
            Image image = button.GetComponent<Image>();
            TextMeshProUGUI text = button.GetComponentInChildren<TextMeshProUGUI>();
            
            // Pas de fond (gélule) - toujours transparent
            image.color = Color.clear;
            
            if (i == selectedIndex)
            {
                // Onglet sélectionné : texte en violet foncé
                if (text != null)
                    text.color = HexToColor("#64477D");
            }
            else
            {
                // Onglet non sélectionné : texte en gris violet clair
                if (text != null)
                    text.color = HexToColor("#ac99b1");
            }
        }
    }
    
    void ShowPersonnagesContent()
    {
        LogVerbose("[QuetesTab] ========== ShowPersonnagesContent() MOSAIQUE ==========");
        
        // Nettoyer le contenu existant
        foreach (Transform child in detailsContent.transform)
        {
            Destroy(child.gameObject);
        }
        avatarButtons.Clear();
        
        if (selectedQuest == null)
        {
            LogVerbose("[QuetesTab] ⚠️ selectedQuest est NULL");
            ShowNoQuestSelectedMessage();
            return;
        }
        
        
        // Récupérer les personnages de la quête
        currentCharacters = GetQuestCharacters(selectedQuest.id);
        
        if (currentCharacters == null || currentCharacters.Count == 0)
        {
            LogVerbose("[QuetesTab] ⚠️ Aucun personnage dans la liste");
            CreateEmptyMessage("Aucun personnage pour cette quête");
            return;
        }
        
        LogVerbose($"[QuetesTab] 👥 {currentCharacters.Count} personnages à afficher en mosaïque");
        
        var mosaicConfig = config.characterMosaic;
        var detailConfig = config.characterDetail;
        float padding = mosaicConfig.padding;
        float avatarSize = mosaicConfig.avatarSize;
        float spacing = mosaicConfig.avatarSpacing;
        float arrowSize = mosaicConfig.arrowSize;
        
        // Container principal
        GameObject container = new GameObject("PersonnagesContainer");
        container.transform.SetParent(detailsContent.transform, false);
        
        RectTransform containerRect = container.AddComponent<RectTransform>();
        containerRect.anchorMin = Vector2.zero;
        containerRect.anchorMax = Vector2.one;
        containerRect.offsetMin = Vector2.zero;
        containerRect.offsetMax = Vector2.zero;
        
        // Zone de mosaïque avec flèches
        GameObject mosaicZone = new GameObject("MosaicZone");
        mosaicZone.transform.SetParent(container.transform, false);
        
        RectTransform mosaicRect = mosaicZone.AddComponent<RectTransform>();
        mosaicRect.anchorMin = new Vector2(0, 1);
        mosaicRect.anchorMax = new Vector2(1, 1);
        mosaicRect.pivot = new Vector2(0.5f, 1);
        mosaicRect.anchoredPosition = new Vector2(0, -padding);
        float mosaicHeight = avatarSize + padding;
        mosaicRect.sizeDelta = new Vector2(0, mosaicHeight);
        
        // Flèche gauche
        leftArrow = CreateNavigationArrow(mosaicZone.transform, true, arrowSize, mosaicConfig.arrowColor);
        
        // Conteneur des avatars (centré) - seulement 3 visibles
        avatarsContainer = new GameObject("AvatarsContainer");
        avatarsContainer.transform.SetParent(mosaicZone.transform, false);
        
        RectTransform avatarsRect = avatarsContainer.AddComponent<RectTransform>();
        avatarsRect.anchorMin = new Vector2(0.5f, 0.5f);
        avatarsRect.anchorMax = new Vector2(0.5f, 0.5f);
        avatarsRect.pivot = new Vector2(0.5f, 0.5f);
        
        // Calculer la largeur totale pour 3 avatars max
        int maxVisible = mosaicConfig.columns; // 3 par défaut
        float totalAvatarsWidth = maxVisible * avatarSize + (maxVisible - 1) * spacing;
        avatarsRect.sizeDelta = new Vector2(totalAvatarsWidth, avatarSize);
        
        // Initialiser l'index de départ
        visibleStartIndex = 0;
        
        // Créer les 3 avatars visibles
        RefreshVisibleAvatars();
        
        // Flèche droite
        rightArrow = CreateNavigationArrow(mosaicZone.transform, false, arrowSize, mosaicConfig.arrowColor);
        
        // Mettre à jour la visibilité des flèches
        UpdateArrowsVisibility();
        
        // Zone de détail du personnage
        CreateCharacterDetailPanel(container.transform, mosaicHeight + padding, detailConfig);
        
        // Sélectionner le premier personnage par défaut
        selectedCharacterIndex = 0;
        UpdateSelectedCharacter();
        
        LogVerbose("[QuetesTab] ✅ Mosaïque créée");
    }
    
    GameObject CreateNavigationArrow(Transform parent, bool isLeft, float size, string colorHex)
    {
        GameObject arrow = new GameObject(isLeft ? "LeftArrow" : "RightArrow");
        arrow.transform.SetParent(parent, false);
        
        RectTransform arrowRect = arrow.AddComponent<RectTransform>();
        arrowRect.anchorMin = new Vector2(isLeft ? 0 : 1, 0.5f);
        arrowRect.anchorMax = new Vector2(isLeft ? 0 : 1, 0.5f);
        arrowRect.pivot = new Vector2(0.5f, 0.5f);
        arrowRect.anchoredPosition = new Vector2(isLeft ? size / 2 + 10 : -size / 2 - 10, 0);
        arrowRect.sizeDelta = new Vector2(size, size);
        
        // Image de la flèche
        Image arrowImage = arrow.AddComponent<Image>();
        arrowImage.color = Color.white;
        arrowImage.raycastTarget = true;
        
        // Charger l'image depuis l'URL
        string imageName = isLeft ? "nav_gauche.png" : "nav_droite.png";
        string imageUrl = GeneralConfigManager.Instance?.GetUIUrl(imageName) ?? "";
        if (!string.IsNullOrEmpty(imageUrl))
        {
            StartCoroutine(LoadArrowSprite(imageUrl, arrowImage));
        }
        
        // Bouton
        Button button = arrow.AddComponent<Button>();
        button.transition = Selectable.Transition.ColorTint;
        button.targetGraphic = arrowImage;
        
        int direction = isLeft ? -1 : 1;
        button.onClick.AddListener(() => NavigateCharacter(direction));
        
        return arrow;
    }
    
    IEnumerator LoadArrowSprite(string url, Image targetImage)
    {
        using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(url))
        {
            yield return request.SendWebRequest();
            
            if (request.result == UnityWebRequest.Result.Success)
            {
                Texture2D texture = DownloadHandlerTexture.GetContent(request);
                Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), 
                    new Vector2(0.5f, 0.5f), 100f);
                TrackPersistent(texture, sprite);
                if (targetImage != null)
                {
                    targetImage.sprite = sprite;
                }
            }
            else
            {
                Debug.LogWarning($"[QuetesTab] ⚠️ Impossible de charger l'image flèche: {url}");
            }
        }
    }
    
    IEnumerator LoadFormationArrowSprite(Image targetImage)
    {
        // Cache (un seul téléchargement pour tout l'onglet QUÊTES)
        if (targetImage == null) yield break;

        if (cachedFormationArrowSprite != null)
        {
            targetImage.sprite = cachedFormationArrowSprite;
            yield break;
        }

        pendingFormationArrowTargets.Add(targetImage);
        if (isLoadingFormationArrowSprite) yield break;

        isLoadingFormationArrowSprite = true;

        string uiPath = generalConfig?.assetsPaths?.uiPath ?? "https://ujsa.studioplc.fr/datas/UI/";
        string url = uiPath + "fleche_formation.png";

        using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(url))
        {
            yield return request.SendWebRequest();

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

                cachedFormationArrowTexture = texture;
                cachedFormationArrowSprite = sprite;
                TrackPersistent(texture, sprite);

                for (int i = 0; i < pendingFormationArrowTargets.Count; i++)
                {
                    Image img = pendingFormationArrowTargets[i];
                    if (img != null)
                    {
                        img.sprite = cachedFormationArrowSprite;
                    }
                }
                pendingFormationArrowTargets.Clear();
            }
            else
            {
                Debug.LogWarning($"[QuetesTab] ⚠️ Impossible de charger fleche_formation.png: {url}");
            }
        }

        isLoadingFormationArrowSprite = false;
    }
    
    void CreateAvatarButton(Transform parent, QuestCharacter character, int index, float xPos, float size, QuetesTabCharacterMosaicConfig mosaicConfig)
    {
        // Cadre carré avec fond et coins arrondis
        GameObject frameObj = new GameObject($"AvatarFrame_{index}");
        frameObj.transform.SetParent(parent, false);
        
        RectTransform frameRect = frameObj.AddComponent<RectTransform>();
        frameRect.anchorMin = new Vector2(0.5f, 0.5f);
        frameRect.anchorMax = new Vector2(0.5f, 0.5f);
        frameRect.pivot = new Vector2(0.5f, 0.5f);
        frameRect.anchoredPosition = new Vector2(xPos, 0);
        frameRect.sizeDelta = new Vector2(size, size);
        
        // Fond du cadre avec coins arrondis (couleur fixe, pas de changement à la sélection)
        Image frameImage = frameObj.AddComponent<Image>();
        frameImage.sprite = GetCachedRoundedSprite((int)size, (int)size, (int)mosaicConfig.avatarCornerRadius, Color.white);
        frameImage.color = HexToColor(mosaicConfig.avatarBorderColor);
        frameImage.type = Image.Type.Simple;
        frameImage.raycastTarget = true;
        
        // Bouton
        Button button = frameObj.AddComponent<Button>();
        button.targetGraphic = frameImage;
        button.transition = Selectable.Transition.None;
        
        int capturedIndex = index;
        button.onClick.AddListener(() => SelectCharacter(capturedIndex));
        
        // Image de l'avatar qui remplit tout le cadre (sans bordure intérieure)
        GameObject avatarObj = new GameObject("AvatarImage");
        avatarObj.transform.SetParent(frameObj.transform, false);
        
        RectTransform avatarRect = avatarObj.AddComponent<RectTransform>();
        // L'avatar remplit tout le cadre
        avatarRect.anchorMin = Vector2.zero;
        avatarRect.anchorMax = Vector2.one;
        avatarRect.pivot = new Vector2(0.5f, 0.5f);
        avatarRect.offsetMin = Vector2.zero;
        avatarRect.offsetMax = Vector2.zero;
        
        Image avatarImage = avatarObj.AddComponent<Image>();
        avatarImage.sprite = GetCachedRoundedSprite((int)size, (int)size, (int)mosaicConfig.avatarCornerRadius, Color.white);
        avatarImage.color = HexToColor("#e8ddd5"); // Couleur par défaut avant chargement
        avatarImage.type = Image.Type.Simple;
        avatarImage.preserveAspect = false; // Remplir tout l'espace
        avatarImage.raycastTarget = false;
        
        // Charger l'avatar
        string avatarUrl = character.GetAvatarUrl();
        Debug.Log($"[QuetesTab] 👤 Personnage '{character.GetName()}': avatarUrl='{avatarUrl}'");
        if (!string.IsNullOrEmpty(avatarUrl))
        {
            string fullUrl = GetAvatarUrl(avatarUrl);
            Debug.Log($"[QuetesTab] 🔗 URL complète: {fullUrl}");
            StartCoroutine(LoadAvatarSprite(fullUrl, avatarImage));
        }
        else
        {
            Debug.LogWarning($"[QuetesTab] ⚠️ Pas d'URL pour le personnage '{character.GetName()}'");
        }
        
        // Bordure pointillée (cachée par défaut)
        CreateDashedBorder(frameObj.transform, size, mosaicConfig.avatarCornerRadius);
        
        avatarButtons.Add(frameObj);
    }
    
    void CreateDashedBorder(Transform parent, float size, float cornerRadius)
    {
        // Créer un cadre de sélection propre (comme le cadre des avatars mais juste le contour)
        GameObject borderContainer = new GameObject("DashedBorder");
        borderContainer.transform.SetParent(parent, false);
        
        RectTransform containerRect = borderContainer.AddComponent<RectTransform>();
        containerRect.anchorMin = Vector2.zero;
        containerRect.anchorMax = Vector2.one;
        containerRect.offsetMin = new Vector2(-4, -4);
        containerRect.offsetMax = new Vector2(4, 4);
        
        // Utiliser la même méthode que pour les cadres existants
        Image borderImage = borderContainer.AddComponent<Image>();
        int borderSize = (int)(size + 8);
        borderImage.sprite = GetCachedRoundedBorderOnlySprite(borderSize, borderSize, (int)cornerRadius + 2, 3);
        borderImage.color = HexToColor("#a23cff");
        borderImage.type = Image.Type.Simple;
        borderImage.raycastTarget = false;
        
        borderContainer.SetActive(false);
    }
    
    Sprite CreateRoundedBorderOnlySprite(int width, int height, int radius, int borderWidth)
    {
        Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
        Color[] pixels = new Color[width * height];
        
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                // Vérifier si le pixel est dans le rectangle arrondi extérieur
                bool inOuter = IsInsideRoundedRect(x, y, width, height, radius);
                // Vérifier si le pixel est dans le rectangle arrondi intérieur
                bool inInner = IsInsideRoundedRect(x - borderWidth, y - borderWidth, 
                    width - borderWidth * 2, height - borderWidth * 2, 
                    Mathf.Max(0, radius - borderWidth));
                
                // Le pixel fait partie de la bordure s'il est dans l'extérieur mais pas dans l'intérieur
                if (inOuter && !inInner)
                {
                    pixels[y * width + x] = Color.white;
                }
                else
                {
                    pixels[y * width + x] = Color.clear;
                }
            }
        }
        
        texture.SetPixels(pixels);
        texture.Apply();
        
        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), 100f);

        // IMPORTANT: tracker pour nettoyage (sinon fuite massive en WebGL pendant la navigation)
        if (currentScope == ResourceScope.Details)
            TrackDetails(texture, sprite);
        else
            TrackPersistent(texture, sprite);

        return sprite;
    }
    
    bool IsInsideRoundedRect(int x, int y, int width, int height, int radius)
    {
        if (x < 0 || y < 0 || x >= width || y >= height) return false;
        
        // Vérifier les coins
        // Coin bas-gauche
        if (x < radius && y < radius)
        {
            float dx = radius - x;
            float dy = radius - y;
            return (dx * dx + dy * dy) <= radius * radius;
        }
        // Coin bas-droite
        if (x >= width - radius && y < radius)
        {
            float dx = x - (width - radius - 1);
            float dy = radius - y;
            return (dx * dx + dy * dy) <= radius * radius;
        }
        // Coin haut-gauche
        if (x < radius && y >= height - radius)
        {
            float dx = radius - x;
            float dy = y - (height - radius - 1);
            return (dx * dx + dy * dy) <= radius * radius;
        }
        // Coin haut-droite
        if (x >= width - radius && y >= height - radius)
        {
            float dx = x - (width - radius - 1);
            float dy = y - (height - radius - 1);
            return (dx * dx + dy * dy) <= radius * radius;
        }
        
        // Dans la zone centrale ou les côtés
        return true;
    }
    
    void CreateCharacterDetailPanel(Transform parent, float topOffset, QuetesTabCharacterDetailConfig detailConfig)
    {
        characterDetailPanel = new GameObject("CharacterDetailPanel");
        characterDetailPanel.transform.SetParent(parent, false);
        
        RectTransform panelRect = characterDetailPanel.AddComponent<RectTransform>();
        panelRect.anchorMin = new Vector2(0, 1);
        panelRect.anchorMax = new Vector2(1, 1);
        panelRect.pivot = new Vector2(0.5f, 1);
        panelRect.anchoredPosition = new Vector2(0, -(topOffset + detailConfig.marginTop));
        // +20px vers le bas pour éviter de couper les descriptions longues
        panelRect.sizeDelta = new Vector2(-detailConfig.padding * 2, detailConfig.height + 20f);
        
        // Pas de fond pour le panel principal (transparent)
        
        // Charger les polices
        TMP_FontAsset latoBoldFont = Resources.Load<TMP_FontAsset>("Fonts/Lato-Bold SDF");
        TMP_FontAsset latoRegularFont = Resources.Load<TMP_FontAsset>("Fonts/Lato-Regular SDF");
        
        // Description courte (en haut, sans cadre)
        GameObject descObj = new GameObject("Description");
        descObj.transform.SetParent(characterDetailPanel.transform, false);
        
        RectTransform descRect = descObj.AddComponent<RectTransform>();
        descRect.anchorMin = new Vector2(0, 1);
        descRect.anchorMax = new Vector2(1, 1);
        descRect.pivot = new Vector2(0.5f, 1);
        descRect.anchoredPosition = new Vector2(0, 0);
        descRect.sizeDelta = new Vector2(-detailConfig.padding * 2, 35);
        
        characterDescriptionText = descObj.AddComponent<TextMeshProUGUI>();
        characterDescriptionText.text = "";
        characterDescriptionText.fontSize = 24;
        characterDescriptionText.color = HexToColor("#64477f");
        characterDescriptionText.alignment = TextAlignmentOptions.Center;
        characterDescriptionText.textWrappingMode = TextWrappingModes.Normal;
        characterDescriptionText.overflowMode = TextOverflowModes.Truncate;
        characterDescriptionText.fontStyle = FontStyles.Normal; // Pas d'italique
        characterDescriptionText.raycastTarget = false;
        if (latoBoldFont != null) characterDescriptionText.font = latoBoldFont;
        
        // Description longue (dans un cadre aux coins arrondis)
        float longDescTop = 45; // Après la description courte
        
        GameObject longDescContainer = new GameObject("LongDescriptionContainer");
        longDescContainer.transform.SetParent(characterDetailPanel.transform, false);
        
        RectTransform longDescContainerRect = longDescContainer.AddComponent<RectTransform>();
        longDescContainerRect.anchorMin = new Vector2(0, 0);
        longDescContainerRect.anchorMax = new Vector2(1, 1);
        longDescContainerRect.pivot = new Vector2(0.5f, 0.5f);
        longDescContainerRect.offsetMin = new Vector2(0, 0);
        longDescContainerRect.offsetMax = new Vector2(0, -longDescTop);
        
        // Fond avec coins arrondis pour la description longue
        Image longDescBg = longDescContainer.AddComponent<Image>();
        longDescBg.sprite = CreateRoundedSprite(600, 100, 12, Color.white);
        longDescBg.color = HexToColor(detailConfig.backgroundColor);
        longDescBg.type = Image.Type.Sliced;
        
        // Texte de la description longue
        GameObject longDescObj = new GameObject("LongDescriptionText");
        longDescObj.transform.SetParent(longDescContainer.transform, false);
        
        RectTransform longDescRect = longDescObj.AddComponent<RectTransform>();
        longDescRect.anchorMin = Vector2.zero;
        longDescRect.anchorMax = Vector2.one;
        longDescRect.offsetMin = new Vector2(15, 10);
        longDescRect.offsetMax = new Vector2(-15, -10);
        
        characterLongDescriptionText = longDescObj.AddComponent<TextMeshProUGUI>();
        characterLongDescriptionText.text = "";
        characterLongDescriptionText.fontSize = 18;
        characterLongDescriptionText.color = HexToColor("#64477f");
        characterLongDescriptionText.alignment = TextAlignmentOptions.Center;
        characterLongDescriptionText.textWrappingMode = TextWrappingModes.Normal;
        characterLongDescriptionText.overflowMode = TextOverflowModes.Ellipsis;
        characterLongDescriptionText.fontStyle = FontStyles.Normal;
        characterLongDescriptionText.raycastTarget = false;
        if (latoRegularFont != null) characterLongDescriptionText.font = latoRegularFont;
        
        // On n'utilise plus characterNameText (variable commentée)
        // characterNameText = null;
    }
    
    void RefreshVisibleAvatars()
    {
        if (avatarsContainer == null || currentCharacters == null) return;
        
        var mosaicConfig = config.characterMosaic;
        float avatarSize = mosaicConfig.avatarSize;
        float spacing = mosaicConfig.avatarSpacing;
        int maxVisible = mosaicConfig.columns;
        
        // Supprimer les anciens avatars
        foreach (var avatar in avatarButtons)
        {
            if (avatar != null) Destroy(avatar);
        }
        avatarButtons.Clear();
        
        // Calculer les positions
        float totalWidth = maxVisible * avatarSize + (maxVisible - 1) * spacing;
        float startX = -totalWidth / 2 + avatarSize / 2;
        
        // Créer les avatars visibles (max 3)
        for (int i = 0; i < maxVisible; i++)
        {
            int characterIndex = visibleStartIndex + i;
            if (characterIndex >= currentCharacters.Count) break;
            
            float xPos = startX + i * (avatarSize + spacing);
            CreateAvatarButton(avatarsContainer.transform, currentCharacters[characterIndex], characterIndex, xPos, avatarSize, mosaicConfig);
        }
        
        LogVerbose($"[QuetesTab] 🔄 Avatars rafraîchis: visibleStartIndex={visibleStartIndex}, total={currentCharacters.Count}");
    }
    
    void NavigateCharacter(int direction)
    {
        if (currentCharacters == null || currentCharacters.Count == 0) return;
        
        var mosaicConfig = config.characterMosaic;
        int maxVisible = mosaicConfig.columns;
        
        // Déplacer la fenêtre visible
        visibleStartIndex += direction;
        
        // Bornes
        if (visibleStartIndex < 0)
            visibleStartIndex = 0;
        else if (visibleStartIndex > currentCharacters.Count - maxVisible)
            visibleStartIndex = Mathf.Max(0, currentCharacters.Count - maxVisible);
        
        // Si l'avatar sélectionné n'est plus visible, sélectionner le premier visible
        int visibleEndIndex = visibleStartIndex + maxVisible - 1;
        if (selectedCharacterIndex < visibleStartIndex || selectedCharacterIndex > visibleEndIndex)
        {
            selectedCharacterIndex = visibleStartIndex;
        }
        
        // Rafraîchir l'affichage
        RefreshVisibleAvatars();
        
        // Mettre à jour la visibilité des flèches
        UpdateArrowsVisibility();
        
        // Mettre à jour la sélection
        UpdateSelectedCharacter();
    }
    
    void UpdateArrowsVisibility()
    {
        if (currentCharacters == null || currentCharacters.Count == 0) return;
        
        var mosaicConfig = config.characterMosaic;
        int maxVisible = mosaicConfig.columns;
        
        // Cacher la flèche gauche si on est au début
        if (leftArrow != null)
        {
            leftArrow.SetActive(visibleStartIndex > 0);
        }
        
        // Cacher la flèche droite si on est à la fin
        if (rightArrow != null)
        {
            int maxStartIndex = Mathf.Max(0, currentCharacters.Count - maxVisible);
            rightArrow.SetActive(visibleStartIndex < maxStartIndex);
        }
        
        LogVerbose($"[QuetesTab] 🔄 Flèches mises à jour: gauche={leftArrow?.activeSelf}, droite={rightArrow?.activeSelf}, visibleStartIndex={visibleStartIndex}, total={currentCharacters.Count}");
    }
    
    void SelectCharacter(int index)
    {
        if (currentCharacters == null || index < 0 || index >= currentCharacters.Count) return;
        
        selectedCharacterIndex = index;
        UpdateSelectedCharacter();
    }
    
    void UpdateSelectedCharacter()
    {
        if (currentCharacters == null || currentCharacters.Count == 0) return;
        
        var mosaicConfig = config.characterMosaic;
        QuestCharacter character = currentCharacters[selectedCharacterIndex];
        
        // Mettre à jour la bordure pointillée des avatars visibles
        for (int i = 0; i < avatarButtons.Count; i++)
        {
            int characterIndex = visibleStartIndex + i;
            bool isSelected = (characterIndex == selectedCharacterIndex);
            
            // Afficher/cacher la bordure pointillée
            Transform dashedBorder = avatarButtons[i].transform.Find("DashedBorder");
            if (dashedBorder != null)
            {
                dashedBorder.gameObject.SetActive(isSelected);
            }
        }
        
        // Mettre à jour le panneau de détail (sans le nom)
        // Règle demandée: si le personnage vient de l'API et revealed=false => afficher "???" dans le titre/description
        // (On protège le fallback local persos.json qui n'a pas forcément de champ 'revealed' et serait false par défaut)
        bool maskText = character != null && character.IsFromApi() && !character.IsRevealed();

        if (characterDescriptionText != null)
        {
            characterDescriptionText.text = maskText ? "???" : (character.GetDescription() ?? "");
        }
        
        if (characterLongDescriptionText != null)
        {
            characterLongDescriptionText.text = maskText ? "???" : (character.GetLongDescription() ?? "");
        }
        
        LogVerbose($"[QuetesTab] 👤 Personnage sélectionné: {character.GetName()}");
    }
    
    IEnumerator LoadAvatarSprite(string url, Image targetImage)
    {
        Debug.Log($"[QuetesTab] 🖼️ Début chargement avatar: {url}");
        
        int gen = detailsGeneration;

        // Cache global (LRU): si déjà téléchargé, réutiliser sans re-télécharger
        if (RemoteSpriteCache.Instance != null && RemoteSpriteCache.Instance.TryGet(url, out var cachedSprite) && cachedSprite != null)
                {
            if (gen == detailsGeneration && targetImage != null)
                {
                targetImage.sprite = cachedSprite;
                targetImage.color = Color.white;
            }
                    yield break;
                }

        // Charger via le cache global (il gère l'éviction et la destruction)
        bool done = false;
        Sprite loaded = null;
        string err = null;
        yield return RemoteSpriteCache.Instance.GetOrLoad(
            url,
            sp => { loaded = sp; done = true; },
            e => { err = e; done = true; }
        );
        if (!done) yield break;
        if (gen != detailsGeneration) yield break;
        if (targetImage == null) yield break;
        if (loaded != null)
        {
            targetImage.sprite = loaded;
                targetImage.color = Color.white;
            }
            else
            {
            Debug.LogError($"[QuetesTab] ❌ Échec du chargement de l'avatar (cache): {url} - {err}");
        }
    }
    
    string GetAvatarUrl(string avatarFileName)
    {
        // Si c'est déjà une URL complète, la retourner directement
        if (!string.IsNullOrEmpty(avatarFileName) && (avatarFileName.StartsWith("http://") || avatarFileName.StartsWith("https://")))
        {
            return avatarFileName;
        }
        
        // Sinon, ajouter le préfixe
        string uiPath = generalConfig?.assetsPaths?.uiPath ?? "https://ujsa.studioplc.fr/datas/UI/";
        return uiPath + avatarFileName;
    }
    
    private int selectedBadgeIndex = -1; // Index du badge sélectionné (-1 = aucun)
    
    void ShowBadgesContent()
    {
        LogVerbose("[QuetesTab] 🏆 ShowBadgesContent() appelé");
        
        // Nettoyer le contenu existant
        foreach (Transform child in detailsContent.transform)
        {
            Destroy(child.gameObject);
        }
        
        if (selectedQuest == null)
        {
            Debug.LogWarning("[QuetesTab] ⚠️ selectedQuest est NULL");
            ShowNoQuestSelectedMessage();
            return;
        }
        
        if (selectedQuest.user == null || selectedQuest.user.Count == 0)
        {
            Debug.LogWarning("[QuetesTab] ⚠️ Aucun badge dans la liste");
            CreateEmptyMessage("Aucun badge disponible pour cette quête");
            return;
        }
        
        LogVerbose($"[QuetesTab] 🏆 Affichage de {selectedQuest.user.Count} badges");
        
        // Lire les paramètres depuis la config
        float topMargin = 10f;
        float badgeSpacing = 30f;
        float badgeSize = 150f;
        float titleHeight = 35f;
        float badgeRatio = 461f / 400f;
        float badgeTitleSpacing = 10f;
        
        if (config != null && config.detailsContent != null && config.detailsContent.badgeDisplay != null)
        {
            var badgeConfig = config.detailsContent.badgeDisplay;
            topMargin = badgeConfig.topMargin;
            badgeSpacing = badgeConfig.badgeSpacing;
            badgeSize = badgeConfig.badgeSize;
            titleHeight = badgeConfig.titleHeight;
            badgeRatio = badgeConfig.badgeRatio;
            badgeTitleSpacing = badgeConfig.badgeTitleSpacing;
        }
        
        // Container principal
        GameObject container = new GameObject("BadgesContainer");
        container.transform.SetParent(detailsContent.transform, false);
        
        RectTransform containerRect = container.AddComponent<RectTransform>();
        containerRect.anchorMin = Vector2.zero;
        containerRect.anchorMax = Vector2.one;
        containerRect.offsetMin = Vector2.zero;
        containerRect.offsetMax = Vector2.zero;
        
        // Zone des badges (en haut, centré horizontalement)
        GameObject badgesRow = new GameObject("BadgesRow");
        badgesRow.transform.SetParent(container.transform, false);
        
        RectTransform badgesRowRect = badgesRow.AddComponent<RectTransform>();
        badgesRowRect.anchorMin = new Vector2(0.5f, 1);
        badgesRowRect.anchorMax = new Vector2(0.5f, 1);
        badgesRowRect.pivot = new Vector2(0.5f, 1);
        badgesRowRect.anchoredPosition = new Vector2(0, -topMargin); // Lire depuis config
        
        // Layout horizontal pour centrer les badges
        HorizontalLayoutGroup layout = badgesRow.AddComponent<HorizontalLayoutGroup>();
        layout.spacing = badgeSpacing; // Lire depuis config
        layout.childAlignment = TextAnchor.UpperCenter;
        layout.childControlWidth = false;
        layout.childControlHeight = false;
        layout.childForceExpandWidth = false;
        layout.childForceExpandHeight = false;
        
        ContentSizeFitter sizeFitter = badgesRow.AddComponent<ContentSizeFitter>();
        sizeFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
        sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
        
        for (int i = 0; i < selectedQuest.user.Count; i++)
        {
            QuestUserProgress badgeData = selectedQuest.user[i];
            CreateBadgeItem(badgesRow.transform, badgeData, i, badgeSize);
        }
        
        // Zone de détail du badge sélectionné (en dessous) - masquée par défaut
        float badgeHeight = badgeSize * badgeRatio;
        // topMargin + badgeHeight + titleHeight + badgeTitleSpacing
        float detailPanelTopOffset = topMargin + badgeHeight + titleHeight + badgeTitleSpacing;
        CreateBadgeDetailPanel(container.transform, detailPanelTopOffset);
        
        // Aucun badge sélectionné par défaut - le panneau de détails reste vide/masqué
        selectedBadgeIndex = -1;
        
        // Masquer le panneau de détails au départ
        Transform detailPanel = container.transform.Find("BadgeDetailPanel");
        if (detailPanel != null)
        {
            detailPanel.gameObject.SetActive(false);
        }
        
        LogVerbose("[QuetesTab] ✅ Badges affichés (aucun badge sélectionné par défaut)");
    }
    
    void CreateBadgeItem(Transform parent, QuestUserProgress badgeData, int index, float size)
    {
        // Container pour le badge et son titre
        GameObject badgeContainer = new GameObject($"BadgeContainer_{badgeData.difficulty}");
        badgeContainer.transform.SetParent(parent, false);
        
        // Lire les valeurs depuis la config
        float badgeRatio = 461f / 400f; // Valeur par défaut
        float titleHeight = 35f; // Valeur par défaut
        float spacing = 10f; // Valeur par défaut
        float titleWidthMultiplier = 1.5f; // Valeur par défaut
        float titleFontSize = 20f; // Valeur par défaut
        string titleColor = "#64477f"; // Valeur par défaut
        string titleFontPath = "Fonts/Lato-Bold SDF"; // Valeur par défaut
        
        if (config != null && config.detailsContent != null && config.detailsContent.badgeDisplay != null)
        {
            var badgeConfig = config.detailsContent.badgeDisplay;
            badgeRatio = badgeConfig.badgeRatio;
            titleHeight = badgeConfig.titleHeight;
            spacing = badgeConfig.badgeTitleSpacing;
            titleWidthMultiplier = badgeConfig.titleWidthMultiplier;
            titleFontSize = badgeConfig.titleFontSize;
            titleColor = badgeConfig.titleColor;
            titleFontPath = badgeConfig.titleFontName;
        }
        
        // Calculer les dimensions
        float badgeWidth = size;
        float badgeHeight = size * badgeRatio;
        float titleWidth = badgeWidth * titleWidthMultiplier; // Largeur du titre calculée depuis config
        
        RectTransform containerRect = badgeContainer.AddComponent<RectTransform>();
        // Container garde la largeur du badge pour le layout (les badges ne se chevauchent pas)
        containerRect.sizeDelta = new Vector2(badgeWidth, badgeHeight + titleHeight + spacing);
        
        LayoutElement layoutElement = badgeContainer.AddComponent<LayoutElement>();
        layoutElement.preferredWidth = badgeWidth; // Container à la largeur du badge
        layoutElement.preferredHeight = badgeHeight + titleHeight + spacing;
        
        // Image du badge
        GameObject badgeObj = new GameObject($"Badge_{badgeData.difficulty}");
        badgeObj.transform.SetParent(badgeContainer.transform, false);
        
        RectTransform badgeRect = badgeObj.AddComponent<RectTransform>();
        badgeRect.anchorMin = new Vector2(0.5f, 1);
        badgeRect.anchorMax = new Vector2(0.5f, 1);
        badgeRect.pivot = new Vector2(0.5f, 1);
        badgeRect.anchoredPosition = new Vector2(0, 0);
        badgeRect.sizeDelta = new Vector2(badgeWidth, badgeHeight);
        
        Image badgeImage = badgeObj.AddComponent<Image>();
        badgeImage.color = Color.white;
        badgeImage.preserveAspect = true; // Préserver le ratio de l'image
        
        // Charger l'image du badge depuis l'URL
        if (!string.IsNullOrEmpty(badgeData.badge))
        {
            StartCoroutine(LoadBadgeImage(badgeData.badge, badgeImage));
        }
        
        // Badge de pourcentage SEULEMENT si completion = 100
        if (badgeData.completion == 100)
        {
            CreateScoreBadge(badgeObj.transform, badgeData.score, badgeWidth);
        }
        
        // Titre sous le badge (centré, peut dépasser de la largeur du badge)
        GameObject titleObj = new GameObject("BadgeTitle");
        titleObj.transform.SetParent(badgeContainer.transform, false);
        
        RectTransform titleRect = titleObj.AddComponent<RectTransform>();
        // Ancré au centre du container (qui est à la largeur du badge)
        titleRect.anchorMin = new Vector2(0.5f, 0);
        titleRect.anchorMax = new Vector2(0.5f, 0);
        titleRect.pivot = new Vector2(0.5f, 0);
        titleRect.anchoredPosition = new Vector2(0, 0);
        // Le titre peut être plus large que le container (déborde)
        titleRect.sizeDelta = new Vector2(titleWidth, titleHeight);
        
        TextMeshProUGUI titleText = titleObj.AddComponent<TextMeshProUGUI>();
        titleText.text = ""; // Sera rempli par UpdateSelectedBadge
        titleText.fontSize = titleFontSize; // Lire depuis config
        titleText.color = HexToColor(titleColor); // Lire depuis config
        titleText.alignment = TextAlignmentOptions.Center;
        titleText.textWrappingMode = TextWrappingModes.Normal;
        titleText.overflowMode = TextOverflowModes.Overflow; // Permettre le débordement
        titleText.fontStyle = FontStyles.Normal;
        titleText.raycastTarget = false;
        
        // S'assurer que le titre peut dépasser du container (pas de masque)
        RectMask2D mask = badgeContainer.GetComponent<RectMask2D>();
        if (mask != null)
        {
            Destroy(mask);
        }
        
        // Charger la police depuis la config
        TMP_FontAsset customFont = Resources.Load<TMP_FontAsset>(titleFontPath);
        if (customFont != null) titleText.font = customFont;
        
        // Masquer le titre par défaut (sera affiché seulement pour le badge sélectionné)
        titleObj.SetActive(false);
        
        // Bouton pour sélectionner le badge (sur le container pour couvrir badge + titre)
        Button button = badgeContainer.AddComponent<Button>();
        button.targetGraphic = badgeImage;
        int capturedIndex = index;
        button.onClick.AddListener(() => OnBadgeClicked(capturedIndex));
        
        LogVerbose($"[QuetesTab] Badge créé: {badgeData.difficulty} (taille: {badgeWidth}x{badgeHeight}, score: {badgeData.score}%)");
    }
    
    void CreateScoreBadge(Transform parent, int score, float badgeWidth)
    {
        // Conteneur pour le badge de score
        GameObject scoreBadgeObj = new GameObject("ScoreBadge");
        scoreBadgeObj.transform.SetParent(parent, false);
        
        // Taille du badge de score (environ 25-30% de la largeur du badge)
        float badgeSize = badgeWidth * 0.35f;
        
        RectTransform scoreRect = scoreBadgeObj.AddComponent<RectTransform>();
        scoreRect.sizeDelta = new Vector2(badgeSize, badgeSize);
        
        // Position dans le coin supérieur droit
        scoreRect.anchorMin = new Vector2(1, 1);
        scoreRect.anchorMax = new Vector2(1, 1);
        scoreRect.pivot = new Vector2(1, 1);
        scoreRect.anchoredPosition = new Vector2(0, 0);
        
        // Fond violet circulaire
        Image circleBg = scoreBadgeObj.AddComponent<Image>();
        circleBg.sprite = CreateCircleSprite((int)badgeSize);
        circleBg.color = HexToColor("#8b5fb3"); // Violet comme sur la maquette
        
        // Texte du score
        GameObject textObj = new GameObject("ScoreText");
        textObj.transform.SetParent(scoreBadgeObj.transform, false);
        
        RectTransform textRect = textObj.AddComponent<RectTransform>();
        textRect.anchorMin = Vector2.zero;
        textRect.anchorMax = Vector2.one;
        textRect.sizeDelta = Vector2.zero;
        textRect.anchoredPosition = Vector2.zero;
        
        TextMeshProUGUI scoreText = textObj.AddComponent<TextMeshProUGUI>();
        scoreText.text = $"{score}%";
        scoreText.fontSize = badgeSize * 0.35f; // Taille proportionnelle
        scoreText.color = Color.white;
        scoreText.alignment = TextAlignmentOptions.Center;
        scoreText.fontStyle = FontStyles.Bold;
        if (titleFont != null) scoreText.font = titleFont; // Police Anton
        
        LogVerbose($"[QuetesTab] Badge de score créé: {score}% (taille: {badgeSize})");
    }
    
    Sprite CreateCircleSprite(int size)
    {
        Texture2D texture = new Texture2D(size, size, TextureFormat.RGBA32, false);
        texture.filterMode = FilterMode.Bilinear;
        
        Color[] pixels = new Color[size * size];
        float center = size / 2f;
        float radius = size / 2f;
        
        for (int y = 0; y < size; y++)
        {
            for (int x = 0; x < size; x++)
            {
                float distance = Vector2.Distance(new Vector2(x, y), new Vector2(center, center));
                
                if (distance <= radius)
                {
                    pixels[y * size + x] = Color.white;
                }
                else
                {
                    pixels[y * size + x] = Color.clear;
                }
            }
        }
        
        texture.SetPixels(pixels);
        texture.Apply();

        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f);

        if (currentScope == ResourceScope.Details)
            TrackDetails(texture, sprite);
        else
            TrackPersistent(texture, sprite);

        return sprite;
    }
    
    System.Collections.IEnumerator LoadBadgeImage(string url, Image targetImage)
    {
        int gen = detailsGeneration;
        using (UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequestTexture.GetTexture(url))
        {
            yield return request.SendWebRequest();
            
            if (request.result == UnityEngine.Networking.UnityWebRequest.Result.Success)
            {
                Texture2D texture = UnityEngine.Networking.DownloadHandlerTexture.GetContent(request);
                Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), 
                    new Vector2(0.5f, 0.5f), 100f);

                if (gen != detailsGeneration || targetImage == null)
                {
                    Destroy(sprite);
                    Destroy(texture);
                    yield break;
                }

                TrackDetails(texture, sprite);
                targetImage.sprite = sprite;
            }
            else
            {
                Debug.LogWarning($"[QuetesTab] Échec du chargement du badge: {url}");
            }
        }
    }
    
    void CreateBadgeDetailPanel(Transform parent, float topOffset)
    {
        GameObject detailPanel = new GameObject("BadgeDetailPanel");
        detailPanel.transform.SetParent(parent, false);
        
        RectTransform panelRect = detailPanel.AddComponent<RectTransform>();
        panelRect.anchorMin = new Vector2(0, 1);
        panelRect.anchorMax = new Vector2(1, 1);
        panelRect.pivot = new Vector2(0.5f, 1);
        panelRect.anchoredPosition = new Vector2(0, -topOffset);
        panelRect.sizeDelta = new Vector2(-30, 260); // 🔧 CORRECTION : Hauteur augmentée de 10px (250 → 260)
        
        // Charger les polices (même style que les personnages)
        TMP_FontAsset latoRegularFont = Resources.Load<TMP_FontAsset>("Fonts/Lato-Regular SDF");
        
        // Description du badge dans un cadre (même style que description longue des personnages)
        // Plus besoin de marge pour le titre puisqu'il est sous le badge maintenant
        float descTop = 10; // Marge supérieure réduite
        
        GameObject descContainer = new GameObject("BadgeDescriptionContainer");
        descContainer.transform.SetParent(detailPanel.transform, false);
        
        RectTransform descContainerRect = descContainer.AddComponent<RectTransform>();
        descContainerRect.anchorMin = new Vector2(0, 0);
        descContainerRect.anchorMax = new Vector2(1, 1);
        descContainerRect.pivot = new Vector2(0.5f, 0.5f);
        descContainerRect.offsetMin = new Vector2(0, 100); // Laisser 100px en bas pour le bouton (80 + 20 marge)
        descContainerRect.offsetMax = new Vector2(0, -descTop);
        
        // Fond avec coins arrondis pour la description (même style que personnages)
        Image descBg = descContainer.AddComponent<Image>();
        descBg.sprite = CreateRoundedSprite(600, 100, 12, Color.white);
        descBg.color = HexToColor("#ecdfd6"); // Même couleur que characterDetail.backgroundColor
        descBg.type = Image.Type.Sliced;
        descBg.raycastTarget = false; // 🔧 IMPORTANT : ne pas bloquer les clics sur les liens du texte
        
        // Texte de la description
        GameObject descTextObj = new GameObject("BadgeDescriptionText");
        descTextObj.transform.SetParent(descContainer.transform, false);
        
        RectTransform descTextRect = descTextObj.AddComponent<RectTransform>();
        descTextRect.anchorMin = Vector2.zero;
        descTextRect.anchorMax = Vector2.one;
        descTextRect.offsetMin = new Vector2(15, 10);
        descTextRect.offsetMax = new Vector2(-15, -10);
        
        TextMeshProUGUI descText = descTextObj.AddComponent<TextMeshProUGUI>();
        descText.text = "";
        descText.fontSize = 18;
        descText.color = HexToColor("#64477f");
        descText.alignment = TextAlignmentOptions.Center;
        descText.textWrappingMode = TextWrappingModes.Normal;
        descText.overflowMode = TextOverflowModes.Ellipsis;
        descText.fontStyle = FontStyles.Normal;
        descText.raycastTarget = true; // 🔧 CORRECTION : Activer le raycast pour permettre les clics sur les liens
        if (latoRegularFont != null) descText.font = latoRegularFont;
        
        // 🔧 NOUVEAU : Ajouter un component pour détecter les clics sur les liens TextMeshPro
        BadgeLinkClickHandler linkHandler = descTextObj.AddComponent<BadgeLinkClickHandler>();
        linkHandler.questesTab = this;
    }
    
    void OnBadgeClicked(int index)
    {
        LogVerbose($"[QuetesTab] Badge {index} cliqué");
        selectedBadgeIndex = index;
        
        // Afficher le panneau de détails s'il était masqué
        Transform detailPanel = detailsContent.transform.Find("BadgesContainer/BadgeDetailPanel");
        if (detailPanel != null && !detailPanel.gameObject.activeSelf)
        {
            detailPanel.gameObject.SetActive(true);
        }
        
        UpdateSelectedBadge();
    }
    
    void UpdateSelectedBadge()
    {
        if (selectedQuest == null || selectedQuest.user == null || selectedBadgeIndex < 0 || selectedBadgeIndex >= selectedQuest.user.Count)
            return;
        
        QuestUserProgress selectedBadge = selectedQuest.user[selectedBadgeIndex];
        string difficulty = selectedBadge.difficulty;
        bool isUnlocked = selectedBadge.completion >= 100;
        
        // Récupérer la config des badges
        var badgesConfig = GeneralConfigManager.Instance?.GetConfig()?.badges;
        if (badgesConfig == null)
        {
            Debug.LogWarning("[QuetesTab] Configuration des badges non trouvée");
            return;
        }
        
        // Masquer tous les titres d'abord
        Transform badgesRow = detailsContent.transform.Find("BadgesContainer/BadgesRow");
        if (badgesRow != null)
        {
            for (int i = 0; i < badgesRow.childCount; i++)
            {
                Transform badgeContainer = badgesRow.GetChild(i);
                Transform titleObj = badgeContainer.Find("BadgeTitle");
                if (titleObj != null)
                {
                    titleObj.gameObject.SetActive(false);
                }
            }
        }
        
        // Afficher et mettre à jour le titre sous le badge sélectionné
        if (badgesRow != null && selectedBadgeIndex < badgesRow.childCount)
        {
            Transform badgeContainer = badgesRow.GetChild(selectedBadgeIndex);
            Transform titleObj = badgeContainer.Find("BadgeTitle");
            if (titleObj != null)
            {
                titleObj.gameObject.SetActive(true);
                TextMeshProUGUI titleText = titleObj.GetComponent<TextMeshProUGUI>();
                
                if (titleText != null)
                {
                    if (isUnlocked)
                    {
                        string titre = GetBadgeTitre(badgesConfig, difficulty);
                        titleText.text = titre;
                    }
                    else
                    {
                        titleText.text = "???";
                    }
                }
            }
        }
        
        // Mettre à jour la description dans le panneau de détails
        Transform detailPanel = detailsContent.transform.Find("BadgesContainer/BadgeDetailPanel");
        if (detailPanel != null)
        {
            LogVerbose($"[QuetesTab] DetailPanel trouvé, actif: {detailPanel.gameObject.activeSelf}");
            
            Transform descContainer = detailPanel.Find("BadgeDescriptionContainer");
            if (descContainer == null)
            {
                Debug.LogWarning("[QuetesTab] ⚠️ BadgeDescriptionContainer non trouvé ou détruit");
                return;
            }
            
            TextMeshProUGUI descText = descContainer.Find("BadgeDescriptionText")?.GetComponent<TextMeshProUGUI>();
            
            if (descText != null)
            {
                if (isUnlocked)
                {
                    string texte = GetBadgeTexte(badgesConfig, difficulty);
                        descText.text = texte;
                }
                else
                {
                    descText.text = "Vous n'avez pas encore débloqué ce badge.";
                }
            }
            
            // Ajouter/afficher le bouton "EFFACER LE BADGE" si completion = 100
            UpdateResetBadgeButton(detailPanel, isUnlocked, difficulty);
        }
        else
        {
            Debug.LogWarning("[QuetesTab] ⚠️ DetailPanel non trouvé ou détruit");
        }
        
        LogVerbose($"[QuetesTab] Badge {difficulty} mis à jour - Débloqué: {isUnlocked}, Completion: {selectedBadge.completion}%");
    }
    
    void UpdateResetBadgeButton(Transform detailPanel, bool isCompleted, string difficulty)
    {
        // 🔧 NOUVEAU : Ajouter le lien dans le texte de description au lieu d'un bouton séparé
        // Le bouton "EFFACER LE BADGE" est maintenant remplacé par un lien dans le texte de description
        
        // Vérifier que le panel existe toujours
        if (detailPanel == null)
        {
            Debug.LogWarning("[QuetesTab] ⚠️ DetailPanel est null ou détruit");
            return;
        }
        
        LogVerbose($"[QuetesTab] 🔘 UpdateResetBadgeButton (lien) - isCompleted: {isCompleted}, difficulty: {difficulty}");
        
        // Stocker la difficulté courante pour pouvoir l'utiliser dans le callback du lien
        currentResetBadgeDifficulty = difficulty;
    }
    
    public void OnResetBadgeClicked(string difficulty)
    {
        if (selectedQuest == null) return;
        
        LogVerbose($"[QuetesTab] Demande d'effacement du badge {difficulty} pour la quête {selectedQuest.id}");
        
        // Appeler l'endpoint pour effacer le badge
        StartCoroutine(ResetBadgeCoroutine(selectedQuest.id, difficulty));
    }
    
    IEnumerator ResetBadgeCoroutine(int questId, string difficulty)
    {
        string url = GeneralConfigManager.Instance.GetQuestAnswersResetByDifficultyUrl(questId);
        
        LogVerbose($"[QuetesTab] 🌐 POST {url}");
        
        // Créer le body JSON
        string jsonBody = $"{{\"difficulty\":\"{difficulty}\"}}";
        LogVerbose($"[QuetesTab] 📦 Body: {jsonBody}");
        
        using (UnityWebRequest www = new UnityWebRequest(url, "POST"))
        {
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody);
            www.uploadHandler = new UploadHandlerRaw(bodyRaw);
            www.downloadHandler = new DownloadHandlerBuffer();
            www.SetRequestHeader("Content-Type", "application/json");
            
            // Ajouter le token d'authentification
            string token = UserDataManager.Instance != null ? (UserDataManager.Instance.token ?? "") : "";
            if (string.IsNullOrEmpty(token))
                token = PlayerPrefs.GetString("auth_token", "");
            if (!string.IsNullOrEmpty(token))
            {
                www.SetRequestHeader("Authorization", $"Bearer {token}");
                LogVerbose("[QuetesTab] 🔑 Token ajouté à la requête reset badge");
            }
            else
            {
                Debug.LogWarning("[QuetesTab] ⚠️ Aucun token trouvé (UserDataManager/PlayerPrefs) pour reset badge");
            }
            
            yield return www.SendWebRequest();
            
            if (www.result == UnityWebRequest.Result.Success)
            {
                LogVerbose($"[QuetesTab] ✅ Badge {difficulty} effacé avec succès pour la quête {questId}");
                
                // Rafraîchir les données depuis l'API pour mettre à jour user/completion
                yield return StartCoroutine(RefreshQuestsFromApi());
                
                // Rafraîchir l'affichage de la quête sélectionnée
                if (selectedQuest != null)
                {
                    // Retrouver la quête mise à jour dans la liste
                    Quest updatedQuest = questsData?.Find(q => q.id == selectedQuest.id);
                    if (updatedQuest != null)
                    {
                        SelectQuest(updatedQuest);
                        LogVerbose($"[QuetesTab] 🔄 Quête {questId} rafraîchie après reset du badge");
                    }
                }
            }
            else
            {
                Debug.LogError($"[QuetesTab] ❌ Erreur lors de l'effacement du badge: {www.error} (HTTP {www.responseCode})");
                if (!string.IsNullOrEmpty(www.downloadHandler?.text))
                    Debug.LogError($"[QuetesTab] ❌ Body: {www.downloadHandler.text}");
            }
        }
    }
    
    string GetBadgeTitre(BadgesConfig config, string difficulty)
    {
        if (config == null || config.titres == null) return difficulty;
        return config.titres.Get(difficulty);
    }
    
    string GetBadgeTexte(BadgesConfig config, string difficulty)
    {
        if (config == null || config.textes == null) return "";
        return config.textes.Get(difficulty);
    }
    
    void ShowFormationsContent()
    {
        LogVerbose("[QuetesTab] 🎓 ShowFormationsContent() appelé");
        
        // Nettoyer le contenu existant
        foreach (Transform child in detailsContent.transform)
        {
            Destroy(child.gameObject);
        }
        
        if (selectedQuest == null)
        {
            Debug.LogWarning("[QuetesTab] ⚠️ selectedQuest est NULL");
            ShowNoQuestSelectedMessage();
            return;
        }
        
        
        if (selectedQuest.trainings == null || selectedQuest.trainings.Count == 0)
        {
            Debug.LogWarning($"[QuetesTab] ⚠️ Aucune formation pour la quête {selectedQuest.id}");
            CreateEmptyMessage("Aucune formation pour cette quête");
            return;
        }
        
        LogVerbose($"[QuetesTab] 📚 Affichage de {selectedQuest.trainings.Count} formations (mode images)");
        
        // Lire les paramètres depuis la config
        float imageWidth = 270f;
        float imageHeight = 360f;
        float spacing = 30f;
        float startX = 15f;
        float startY = -25f;
        float cornerRadius = 15f;
        float shadowOffsetX = 0f;
        float shadowOffsetY = 8f;
        Color shadowColor = HexToColor("#dbc3b7");
        
        if (config != null && config.detailsContent != null && config.detailsContent.formationsDisplay != null)
        {
            var formConfig = config.detailsContent.formationsDisplay;
            imageWidth = formConfig.imageWidth;
            imageHeight = formConfig.imageHeight;
            spacing = formConfig.spacing;
            startX = formConfig.startX;
            startY = formConfig.startY;
            cornerRadius = formConfig.cornerRadius;
            shadowOffsetX = formConfig.shadowOffsetX;
            shadowOffsetY = formConfig.shadowOffsetY;
            shadowColor = HexToColor(formConfig.shadowColor);
        }
        
        int index = 0;
        foreach (Training training in selectedQuest.trainings)
        {
            int row = index / 2; // Ligne (0, 1, 2...)
            int col = index % 2; // Colonne (0 ou 1)
            
            float xPos = startX + col * (imageWidth + spacing);
            float yPos = startY - row * (imageHeight + spacing);
            
            // Créer le conteneur global
            GameObject container = new GameObject($"Formation_{index}_Container");
            container.transform.SetParent(detailsContent.transform, false);
            
            RectTransform containerRect = container.AddComponent<RectTransform>();
            containerRect.anchorMin = new Vector2(0, 1);
            containerRect.anchorMax = new Vector2(0, 1);
            containerRect.pivot = new Vector2(0, 1);
            containerRect.anchoredPosition = new Vector2(xPos, yPos);
            containerRect.sizeDelta = new Vector2(imageWidth, imageHeight);
            
            // Créer l'ombre DERRIÈRE (image avec même sprite arrondi)
            GameObject shadowObj = new GameObject("Shadow");
            shadowObj.transform.SetParent(container.transform, false);
            
            RectTransform shadowRect = shadowObj.AddComponent<RectTransform>();
            shadowRect.anchorMin = Vector2.zero;
            shadowRect.anchorMax = Vector2.one;
            shadowRect.sizeDelta = Vector2.zero;
            shadowRect.anchoredPosition = new Vector2(shadowOffsetX, -shadowOffsetY); // Ombre verticale uniquement
            
            Image shadowImage = shadowObj.AddComponent<Image>();
            shadowImage.sprite = CreateRoundedRectSprite((int)imageWidth, (int)imageHeight, (int)cornerRadius);
            shadowImage.color = shadowColor; // Couleur beige de la config
            shadowImage.raycastTarget = false;
            
            // Mettre l'ombre en premier (derrière)
            shadowObj.transform.SetAsFirstSibling();
            
            // Créer le conteneur de l'image (AU-DESSUS de l'ombre)
            GameObject item = new GameObject("FormationImage");
            item.transform.SetParent(container.transform, false);
            
            RectTransform itemRect = item.AddComponent<RectTransform>();
            itemRect.anchorMin = Vector2.zero;
            itemRect.anchorMax = Vector2.one;
            itemRect.sizeDelta = Vector2.zero;
            itemRect.anchoredPosition = Vector2.zero;
            
            // Image de masque (pour le Mask component)
            Image maskImage = item.AddComponent<Image>();
            maskImage.sprite = CreateRoundedRectSprite((int)imageWidth, (int)imageHeight, (int)cornerRadius);
            maskImage.color = Color.white;
            maskImage.raycastTarget = true;
            
            // Ajouter un Mask pour les coins arrondis
            Mask mask = item.AddComponent<Mask>();
            mask.showMaskGraphic = true; // Afficher le masque blanc comme fond
            
            // Image de la formation (enfant, sera masquée)
            GameObject imageObj = new GameObject("Portrait");
            imageObj.transform.SetParent(item.transform, false);
            
            RectTransform imageRect = imageObj.AddComponent<RectTransform>();
            imageRect.anchorMin = new Vector2(0.5f, 0.5f);
            imageRect.anchorMax = new Vector2(0.5f, 0.5f);
            imageRect.pivot = new Vector2(0.5f, 0.5f);
            imageRect.anchoredPosition = Vector2.zero;
            // La taille sera ajustée après le chargement de l'image
            
            Image formationImage = imageObj.AddComponent<Image>();
            formationImage.preserveAspect = true;
            formationImage.raycastTarget = false;
            
            // Charger l'image portrait depuis l'URL avec ajustement "cover"
            if (!string.IsNullOrEmpty(training.portrait))
            {
                StartCoroutine(LoadFormationImageCover(formationImage, imageRect, training.portrait, imageWidth, imageHeight));
            }
            else
            {
                // Image placeholder si pas d'URL
                formationImage.color = HexToColor("#ecdfd6");
                imageRect.sizeDelta = new Vector2(imageWidth, imageHeight);
            }
            
            // Rendre cliquable pour ouvrir l'URL de la formation
            if (!string.IsNullOrEmpty(training.url))
            {
                Button button = item.AddComponent<Button>();
                button.targetGraphic = maskImage;
                button.transition = Selectable.Transition.ColorTint;
                
                ColorBlock colors = button.colors;
                colors.normalColor = Color.white;
                colors.highlightedColor = new Color(0.95f, 0.95f, 0.95f, 1f);
                colors.pressedColor = new Color(0.85f, 0.85f, 0.85f, 1f);
                button.colors = colors;
                
                string url = training.url;
                button.onClick.AddListener(() => {
                    LogVerbose($"[QuetesTab] 🔗 Ouverture formation: {training.title} → {url}");
                    Application.OpenURL(url);
                });
            }
            
            LogVerbose($"[QuetesTab] ✓ Formation image créée avec ombre: {training.title} à ({xPos}, {yPos})");
            index++;
        }
        
        LogVerbose($"[QuetesTab] ✅ {selectedQuest.trainings.Count} formations affichées en grille");
    }
    
    /// <summary>
    /// Charge l'image d'une formation depuis une URL
    /// </summary>
    IEnumerator LoadFormationImage(Image targetImage, string imageUrl)
    {
        int gen = detailsGeneration;
        using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(imageUrl))
        {
            yield return request.SendWebRequest();
            
            if (request.result == UnityWebRequest.Result.Success)
            {
                Texture2D texture = DownloadHandlerTexture.GetContent(request);
                Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), 
                    new Vector2(0.5f, 0.5f), 100f);
                
                if (gen != detailsGeneration || targetImage == null)
                {
                    Destroy(sprite);
                    Destroy(texture);
                    yield break;
                }

                TrackDetails(texture, sprite);
                targetImage.sprite = sprite;
            }
            else
            {
                Debug.LogWarning($"[QuetesTab] ⚠️ Erreur chargement image formation: {imageUrl}");
            }
        }
    }
    
    /// <summary>
    /// Charge l'image d'une formation et l'ajuste en mode "cover" (remplir le cadre sans blanc)
    /// </summary>
    IEnumerator LoadFormationImageCover(Image targetImage, RectTransform imageRect, string imageUrl, float containerWidth, float containerHeight)
    {
        int gen = detailsGeneration;
        using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(imageUrl))
        {
            yield return request.SendWebRequest();
            
            if (request.result == UnityWebRequest.Result.Success)
            {
                Texture2D texture = DownloadHandlerTexture.GetContent(request);
                Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), 
                    new Vector2(0.5f, 0.5f), 100f);
                
                if (gen != detailsGeneration || targetImage == null || imageRect == null)
                {
                    Destroy(sprite);
                    Destroy(texture);
                    yield break;
                }

                TrackDetails(texture, sprite);
                targetImage.sprite = sprite;

                // Calculer la taille pour remplir le cadre (mode "cover")
                float imageAspect = (float)texture.width / texture.height;
                float containerAspect = containerWidth / containerHeight;

                float width, height;

                if (imageAspect > containerAspect)
                {
                    // Image plus large que le conteneur : ajuster sur la hauteur
                    height = containerHeight;
                    width = height * imageAspect;
                }
                else
                {
                    // Image plus haute que le conteneur : ajuster sur la largeur
                    width = containerWidth;
                    height = width / imageAspect;
                }

                imageRect.sizeDelta = new Vector2(width, height);

                LogVerbose($"[QuetesTab] Image ajustée en mode cover: {width}x{height} (original: {texture.width}x{texture.height})");
            }
            else
            {
                Debug.LogWarning($"[QuetesTab] ⚠️ Erreur chargement image formation: {imageUrl}");
            }
        }
    }
    
    void CreateEmptyMessage(string message)
    {
        GameObject messageObj = new GameObject("EmptyMessage");
        messageObj.transform.SetParent(detailsContent.transform, false);
        
        RectTransform rect = messageObj.AddComponent<RectTransform>();
        rect.anchorMin = Vector2.zero;
        rect.anchorMax = Vector2.one;
        rect.offsetMin = Vector2.zero;
        rect.offsetMax = Vector2.zero;
        
        TextMeshProUGUI text = messageObj.AddComponent<TextMeshProUGUI>();
        text.text = message;
        text.fontSize = 24;
        text.color = HexToColor("#8a8a8a");
        text.alignment = TextAlignmentOptions.Center;
        if (textFont != null) text.font = textFont;
    }
    
    List<QuestCharacter> GetQuestCharacters(int questId)
    {
        if (questCharacters.ContainsKey(questId))
        {
            return questCharacters[questId];
        }
        return new List<QuestCharacter>();
    }
    
    // ========== Utilitaires ==========
    
    Sprite CreateRoundedSprite(int width, int height, float radius, Color fillColor)
    {
        Texture2D texture = new Texture2D(width, height);
        Color[] pixels = new Color[width * height];

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                float alpha = 1f;

                // Coins arrondis
                if (x < radius && y > height - radius)
                {
                    float dx = radius - x;
                    float dy = (height - radius) - y;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius) alpha = 0f;
                }
                else if (x > width - radius && y > height - radius)
                {
                    float dx = x - (width - radius);
                    float dy = (height - radius) - y;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius) alpha = 0f;
                }
                else if (x < radius && y < radius)
                {
                    float dx = radius - x;
                    float dy = radius - y;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius) alpha = 0f;
                }
                else if (x > width - radius && y < radius)
                {
                    float dx = x - (width - radius);
                    float dy = radius - y;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius) alpha = 0f;
                }

                pixels[y * width + x] = new Color(fillColor.r, fillColor.g, fillColor.b, fillColor.a * alpha);
            }
        }

        texture.SetPixels(pixels);
        texture.Apply();

        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), 100f);

        if (currentScope == ResourceScope.Details)
            TrackDetails(texture, sprite);
        else
            TrackPersistent(texture, sprite);

        return sprite;
    }
    
    Sprite CreateGradientSpriteWithBorder(int width, int height, float radius, Color startColor, Color endColor, Color borderColor, float borderWidth)
    {
        Texture2D texture = new Texture2D(width, height);
        Color[] pixels = new Color[width * height];

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                float alpha = 1f;
                bool isBorder = false;
                
                // Vérifier si on est dans la bordure
                if (x < borderWidth || x >= width - borderWidth || y < borderWidth || y >= height - borderWidth)
                {
                    isBorder = true;
                }

                // Coins arrondis
                float distanceFromCorner = float.MaxValue;
                
                // Coin supérieur gauche
                if (x < radius && y > height - radius)
                {
                    float dx = radius - x;
                    float dy = (height - radius) - y;
                    distanceFromCorner = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distanceFromCorner > radius) alpha = 0f;
                    else if (distanceFromCorner > radius - borderWidth) isBorder = true;
                }
                // Coin supérieur droit
                else if (x > width - radius && y > height - radius)
                {
                    float dx = x - (width - radius);
                    float dy = (height - radius) - y;
                    distanceFromCorner = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distanceFromCorner > radius) alpha = 0f;
                    else if (distanceFromCorner > radius - borderWidth) isBorder = true;
                }
                // Coin inférieur gauche
                else if (x < radius && y < radius)
                {
                    float dx = radius - x;
                    float dy = radius - y;
                    distanceFromCorner = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distanceFromCorner > radius) alpha = 0f;
                    else if (distanceFromCorner > radius - borderWidth) isBorder = true;
                }
                // Coin inférieur droit
                else if (x > width - radius && y < radius)
                {
                    float dx = x - (width - radius);
                    float dy = radius - y;
                    distanceFromCorner = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distanceFromCorner > radius) alpha = 0f;
                    else if (distanceFromCorner > radius - borderWidth) isBorder = true;
                }

                Color pixelColor;
                if (isBorder && alpha > 0)
                {
                    pixelColor = borderColor;
                }
                else
                {
                    // Dégradé vertical (du bas vers le haut)
                    float t = (float)y / height;
                    pixelColor = Color.Lerp(endColor, startColor, t);
                }
                
                pixelColor.a *= alpha;
                pixels[y * width + x] = pixelColor;
            }
        }

        texture.SetPixels(pixels);
        texture.Apply();

        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), 100f);

        if (currentScope == ResourceScope.Details)
            TrackDetails(texture, sprite);
        else
            TrackPersistent(texture, sprite);

        return sprite;
    }
    
    Color HexToColor(string hex)
    {
        if (string.IsNullOrEmpty(hex)) return Color.white;
        
        hex = hex.TrimStart('#');
        
        if (hex.Length == 6)
            hex = hex + "FF";
        
        if (hex.Length == 8)
        {
            byte r = byte.Parse(hex.Substring(0, 2), System.Globalization.NumberStyles.HexNumber);
            byte g = byte.Parse(hex.Substring(2, 2), System.Globalization.NumberStyles.HexNumber);
            byte b = byte.Parse(hex.Substring(4, 2), System.Globalization.NumberStyles.HexNumber);
            byte a = byte.Parse(hex.Substring(6, 2), System.Globalization.NumberStyles.HexNumber);
            
            return new Color(r / 255f, g / 255f, b / 255f, a / 255f);
        }
        
        return Color.white;
    }
    
    /// <summary>
    /// Crée un sprite avec coins arrondis (masque blanc)
    /// </summary>
    Sprite CreateRoundedRectSprite(int width, int height, int radius)
    {
        Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
        texture.filterMode = FilterMode.Bilinear;
        Color[] pixels = new Color[width * height];
        
        // Remplir tout en blanc transparent d'abord
        for (int i = 0; i < pixels.Length; i++)
        {
            pixels[i] = new Color(1f, 1f, 1f, 0f);
        }
        
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                float alpha = 1f;
                
                // Coin supérieur gauche
                if (x < radius && y > height - radius)
                {
                    float dx = radius - x - 0.5f;
                    float dy = y - (height - radius) - 0.5f;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius)
                    {
                        alpha = 0f;
                    }
                    else if (distance > radius - 1f)
                    {
                        // Anti-aliasing
                        alpha = 1f - (distance - (radius - 1f));
                    }
                }
                // Coin supérieur droit
                else if (x > width - radius - 1 && y > height - radius)
                {
                    float dx = x - (width - radius - 1) - 0.5f;
                    float dy = y - (height - radius) - 0.5f;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius)
                    {
                        alpha = 0f;
                    }
                    else if (distance > radius - 1f)
                    {
                        alpha = 1f - (distance - (radius - 1f));
                    }
                }
                // Coin inférieur gauche
                else if (x < radius && y < radius)
                {
                    float dx = radius - x - 0.5f;
                    float dy = radius - y - 0.5f;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius)
                    {
                        alpha = 0f;
                    }
                    else if (distance > radius - 1f)
                    {
                        alpha = 1f - (distance - (radius - 1f));
                    }
                }
                // Coin inférieur droit
                else if (x > width - radius - 1 && y < radius)
                {
                    float dx = x - (width - radius - 1) - 0.5f;
                    float dy = radius - y - 0.5f;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius)
                    {
                        alpha = 0f;
                    }
                    else if (distance > radius - 1f)
                    {
                        alpha = 1f - (distance - (radius - 1f));
                    }
                }
                
                pixels[y * width + x] = new Color(1f, 1f, 1f, alpha);
            }
        }
        
        texture.SetPixels(pixels);
        texture.Apply();
        
        return Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f));
    }
}

// ========== Classes de données ==========

[Serializable]
public class CharactersData
{
    public List<QuestCharacter> personnages;
}

[Serializable]
public class QuestCharacter
{
    // Champs français (depuis persos.json local)
    public string nom;
    public string description_courte;
    public string description;
    public string avatar;
    public string avatar_vide;
    public string png;
    
    // Champs anglais (depuis API)
    public int id;
    public string name;
    public string image;           // URL de l'image depuis l'API
    public bool revealed;          // Si le personnage est révélé ou non
    public string short_description;
    public string long_description;
    public string avatar_url;
    public string image_url;
    
    // Propriétés pour obtenir les valeurs (priorité à l'anglais car c'est ce que renvoie l'API)
    public string GetName() => !string.IsNullOrEmpty(name) ? name : nom;
    public string GetDescription() => !string.IsNullOrEmpty(short_description) ? short_description : description_courte;
    public string GetLongDescription() => !string.IsNullOrEmpty(long_description) ? long_description : description;
    public string GetAvatarUrl() 
    {
        // Priorité: image (API) > avatar_url > image_url > avatar (local)
        if (!string.IsNullOrEmpty(image)) return image;
        if (!string.IsNullOrEmpty(avatar_url)) return avatar_url;
        if (!string.IsNullOrEmpty(image_url)) return image_url;
        return avatar;
    }
    public bool IsRevealed() => revealed;

    // Heuristique: permet de distinguer un personnage provenant de l'API (avec champs anglais/id)
    // du fallback local (persos.json) qui ne remplit généralement pas ces champs.
    public bool IsFromApi()
    {
        return id != 0
               || !string.IsNullOrEmpty(name)
               || !string.IsNullOrEmpty(short_description)
               || !string.IsNullOrEmpty(long_description)
               || !string.IsNullOrEmpty(image)
               || !string.IsNullOrEmpty(avatar_url)
               || !string.IsNullOrEmpty(image_url);
    }
}

// Réponse de l'API pour une quête individuelle
[Serializable]
public class QuestDetailApiResponse
{
    public string status;
    public string message;
    public QuestDetailData data;
}

[Serializable]
public class QuestDetailData
{
    public int id;
    public string title;
    public string description;
    public string duration;
    public string trailer_url;
    public List<Training> trainings;
    public List<QuestCharacter> characters; // Personnages de la quête
    public List<QuestBadge> badges;
}

[Serializable]
public class QuestBadge
{
    public int id;
    public string name;
    public string description;
    public string image_url;
    public bool unlocked;
}

/// <summary>
/// Marqueur pour identifier les quêtes verrouillées dans l'onglet Quêtes
/// </summary>
public class LockedQuestMarker : MonoBehaviour { }

/// <summary>
/// Component pour détecter les clics sur les liens dans TextMeshPro
/// </summary>
public class BadgeLinkClickHandler : MonoBehaviour, UnityEngine.EventSystems.IPointerClickHandler
{
    public QuetesTab questesTab;
    
    public void OnPointerClick(UnityEngine.EventSystems.PointerEventData eventData)
    {
        if (questesTab == null) return;
        
        TextMeshProUGUI textComponent = GetComponent<TextMeshProUGUI>();
        if (textComponent == null) return;
        
        // Convertir la position du clic en position dans le texte.
        // IMPORTANT : passer une caméra correcte (sinon FindIntersectingLink peut renvoyer -1 selon le mode Canvas).
        Camera cam = eventData != null ? eventData.pressEventCamera : null;
        if (cam == null)
        {
            Canvas canvas = questesTab.GetComponentInParent<Canvas>();
            if (canvas != null) cam = canvas.worldCamera;
        }
        int linkIndex = TMP_TextUtilities.FindIntersectingLink(textComponent, eventData.position, cam);
        
        if (linkIndex != -1)
        {
            // Récupérer les infos du lien
            TMP_LinkInfo linkInfo = textComponent.textInfo.linkInfo[linkIndex];
            string linkId = linkInfo.GetLinkID();
            
            Debug.Log($"[BadgeLinkClickHandler] Lien cliqué: {linkId}");
            
            // Si c'est le lien "reset_badge", appeler la fonction d'effacement
            if (linkId == "reset_badge")
            {
                questesTab.OnResetBadgeClicked(questesTab.currentResetBadgeDifficulty);
            }
        }
        else
        {
            // Utile pour debug si les clics arrivent mais que TMP ne détecte pas le lien
            Debug.Log("[BadgeLinkClickHandler] Clic reçu mais aucun lien détecté (linkIndex = -1)");
        }
    }
}

