			(function(){
				var root = document.querySelector('section.avi-radar-heatmap-module');
				if (!root || root.dataset.heatmapReady === '1') return;
				root.dataset.heatmapReady = '1';
				var config = {};
				try { config = JSON.parse(root.getAttribute('data-heatmap') || '{}') || {}; } catch (e) { config = {}; }
				// #Sprint44-Plugin — Token embed depuis attribut dédié
				var _embedToken = root.getAttribute('data-embed-token') || '';
				if (_embedToken) config.embed_token = _embedToken;
				// #Sprint44-Plugin — token accessible globalement
				if (_embedToken) window.aviEmbedToken = _embedToken;
				function getHmLang(){
					var r = document.getElementById('avision-dashboard-root');
					var l = (r && (r.dataset.language || r.dataset.lang)) || '';
					return (['fr','en','es','de','pt-br','tr'].indexOf(l) !== -1) ? l : (config.lang || 'en');
				}
				var HM_I18N = {
					en: {
						absent_top20: 'Not in top 20 on this area',
						scan_frequency: 'Scan frequency',
						disclaimer_short: 'Google Algorithm Warning',
						disclaimer: 'Warning: Google algorithms. Do not massively modify your GBP listing based on these signals or risk re-verification or suspension.',
						modal_title: 'Interactive local map',
						modal_kw_section_title: 'Keyword selection',
						grid_desc_3: 'A few streets around your address (9 points · ~350m between points)',
						grid_desc_5: 'Your immediate neighborhood (25 points · ~710m between points)',
						grid_desc_7: 'Your wider neighborhood (49 points · ~710m between points)',
						grid_desc_9: 'Your city district (81 points · ~570m between points)',
						grid_desc_11: 'Between neighborhood and borough (121 points · ~850m between points)',
						grid_desc_13: 'Your borough or municipality (169 points · ~710m between points)',
						grid_desc_15: 'Wide city coverage — ideal for dense urban areas (225 points · ~710m between points)',
						grid_desc_21: 'Full territory — ideal for service-area businesses covering multiple zones (441 points · ~710m between points)',
						present_on: 'Present on ',
						present_on_sfx: ' point(s)',
						competitor: 'Competitor',
						legend_competitor: 'Competitor',
						concurrents: 'Competitors:',
						show_competitors: 'Show competitors',
						hide_competitors: 'Hide competitors',
						top_rival: '⚠️ Top rival',
						best_rank_label: 'best position',
						coverage_label: 'of scanned area',
						view_on_google: 'View on Google',
						zone_help: 'Competitor territory zones',
						threat_high: 'High threat',
						threat_medium: 'Medium threat',
						threat_low: 'Low threat',
						outranks_label: 'Outranks you at',
						locations_label: 'scanned locations',
						reviews_label: 'reviews',
						intel_title: 'Competitor Intelligence',
						intel_subtitle: 'Top rivals by threat level',
						intel_no_scan: 'Launch a scan to see competitor analysis.',
						intel_see_all: 'See all in Competitors tab',
						intel_col_pos: 'Pos.',
						intel_col_name: 'Competitor',
						intel_col_rating: '⭐ Rat.',
						intel_col_coverage: '📍 Cov.',
						intel_expand_tooltip: 'Expand panel',
						tab_competitors: '🥊 Competitors',
						comp_tab_title: 'Competitor Analysis',
						comp_tab_sub: 'All competitors ranked by threat level',
						comp_export_excel: 'Export Excel',
						comp_export_pdf: 'Export PDF',
						comp_tab_empty: 'Launch a scan and activate competitors to see the analysis.',
						comp_sort_label: 'Sort by:',
						sort_threat: '🔴 Threat',
						sort_coverage: '📍 Coverage',
						sort_rating: '⭐ Rating',
						sort_reviews: '💬 Reviews',
						sort_name: 'A-Z Name',
						col_threat: 'Threat',
						col_category: 'Category',
						col_reviews: '💬 Reviews',
						tip_threat: 'Threat level based on best rank position',
						tip_rank: 'Best rank position across all scanned points',
						tip_rating: 'Average Google rating',
						tip_reviews: 'Total number of Google reviews',
						tip_coverage: 'Percentage of scanned points where this competitor appears',
						tip_name: 'Sort alphabetically by competitor name',
						tip_competitor: 'Business competing with you in local search results',
						tip_category: 'Categories reflect each business\'s own Google Business Profile listing, not how Avision classifies them. Google may include results loosely related to your keyword based on relevance, location, and reviews — this is normal Google Maps behavior, not an Avision error.',
						tip_gbp: 'Click to open the Google Business Profile',
						kw_power_label: 'Keyword Power',
						kw_power_dominant: 'Dominant',
						kw_power_strong: 'Strong',
						kw_power_growing: 'Growing',
						kw_power_weak: 'Weak',
						kw_power_critical: 'Critical',
						kw_power_point_sub: 'This specific location',
						solde: 'Balance',
								select_kw_hint: 'Select up to 5 keywords to display local halos.',
						multi_credits: ' keyword(s) = ×',
						multi_credits_sfx: ' credits',
						grille_drawer: 'drawer grid',
						lancer_scan: 'Launch scan',
						scan_btn_single: 'Relaunch scan — ',
						scan_btn_multi: 'Relaunch ',
						scan_btn_scans: ' scans — ',
						scan_btn_credits: ' credits',
						scan_sending: 'Scan request sent...',
						sort_rank: '# Rank',
						sort_scanned: 'Scanned',
						kw_panel_title: 'Radar Keywords',
						kw_radar_label: 'Radar keywords',
						suivis: 'tracked',
						selected: 'selected',
						no_kw_tracked: 'No Radar keywords tracked. Add keywords in Growth Radar first.',
						pending_kw: 'Pending',
						step1_label: 'Radar Keywords',
						step1_hint: 'Select 1 to 5 keywords to map',
						step2_label: 'Heatmap View',
						step2_hint: 'Choose your map style and preview',
						open_map_btn: 'Open Map',
						open_map_no_scan: 'Launch a scan first',
						no_kw_selected: 'Select at least 1 keyword first',
						kw_label_plural: 'keyword(s)',
						credits_label: 'credits',
						confirm_scan: 'Launch scan?',
						confirm_ok: 'Confirm & Launch',
						confirm_cancel: 'Cancel',
						balance_after_label: 'Balance after scan',
						step3_label: 'New Scan',
						step3_hint: 'Select grid size and launch scan',
						vue_heatmap: 'Heatmap View',
						aucun_scan: 'No scan yet',
						design_grid: 'Grid',
						design_points: 'Points',
						design_territory: 'Territory',
						territoire_top3: 'Top 3 Territory',
						rang_moyen: 'Average Rank',
						dernier_statut: 'Last status',
						nouveau_scan: 'New scan',
						credits_dispo: 'credits available',
						heatmap_note: 'This view uses your existing Growth Radar keywords. Grid size determines the number of scanned points and credit cost.',
						taille_grille: 'Grid size',
						cout: 'Cost',
						apres_scan: 'After scan',
						scan_feedback_no_keyword: 'Select a Radar keyword first.',
						scan_feedback_no_center: 'GBP center unavailable for this profile. Run a first scan from a geolocated source or reconnect Google.',
						scan_feedback_ready: 'Ready to launch a standard scan.',
						insufficient_credits: 'Insufficient credits for this grid size.',
						lancer_scan_drawer: 'Launch scan',
						legend_top3: 'Top 3',
						legend_found: 'Rank 4-10',
						legend_weak: 'Rank 11-20',
						legend_missing: '>20',
						hm_map_view_title: 'Growth Radar Heatmap',
						hm_map_view_body: 'The Heatmap goes further than your average rank. It shows your real Google Maps visibility street by street, across a grid of scan points around your location. Each point reveals where customers can — or can\'t — find you. Use it to spot blind zones, compare keywords side by side, and track your local dominance over time.',
						active_keyword: 'Active keyword',
						avi_recommendations_placeholder: 'Keyword-specific advice will appear here after RAG connection.',
						avi_recommendations: 'AVI Recommendations',
						tab_map: '🗺️ Map',
						tab_evolution: '📊 Evolution',
						evolution_intro: 'Compare your scans over time. Select up to 5 scans to display side by side.',
						evolution_loading: 'Loading scan history...',
						evolution_no_scans: 'No completed scans found for this keyword.',
						evolution_select_hint: 'Select 2 to 5 scans to compare.',
						evolution_compare_btn: 'Compare selected scans',
						evolution_delete_btn: 'Delete',
						evolution_delete_confirm: 'Delete this scan? This action cannot be undone.',
						evolution_rank_label: 'Avg rank',
						evolution_territory_label: 'Top 3 territory',
						evolution_grid_label: 'Grid',
						evolution_sort_recent: '↓ Most recent first',
						evolution_sort_oldest: '↑ Oldest first',
						evo_focus_btn: '⛶ Focus mode',
						evo_ref_label: 'Reference',
						evo_ref_tooltip: 'Reference map — all comparisons (↑ ↓) are calculated relative to this scan.',
						evo_mixed_grids_warning: 'Mixed grid sizes detected. Comparisons are approximate — only center points are shared between different grid sizes.',
						evo_diff_grid_tooltip: 'This scan uses a different grid size than the reference. Only the center points are comparable.',
						evo_common_points: 'shared center points with reference',
						evolution_date_label: 'Date',
						no_keyword: 'No keyword',
						center_not_found: 'Geographic center not found for this profile.',
						leaflet_not_loaded: 'Map unavailable: Leaflet is not loaded.',
						scan_missing: 'Missing scan:',
						launch_first_scan: 'Launch a first scan to display local positions.',
						scan_refused: 'The Heatmap scan was refused.',
						scan_launched: 'Heatmap scan launched',
						scan_result_pending: 'Result will appear after processing.',
						scan_error: 'Error during scan launch.',
						coverage_score_label: 'Top 3 Coverage',
						coverage_score_none: 'No scan yet',
						trend_up_title: 'Improved vs previous scan',
						trend_down_title: 'Declined vs previous scan',
						trend_same_title: 'Stable vs previous scan',
						trend_new_title: 'New scan point — no previous data',
						trend_legend_title: 'Trend vs previous scan',
						schedule_active:       'Scheduled scan active',
						schedule_saved:        'Schedule saved',
						schedule_cancel:       'Cancel schedule',
						schedule_note_once:    'One-time scan',
						schedule_note_freq:    'scan(s)/month — ~{cost} credits/month',
						schedule_confirm_freq: 'Scheduled scan — {freq_label} · {scans}/month',
						schedule_credits_line: '{cost} credits/scan × {scans} scans = {total} credits/month',
						schedule_balance_ok:   'Current balance: {balance} credits ✓',
						schedule_balance_warn: 'Current balance: {balance} credits ⚠ — reload recommended',
						schedule_multi_warn:   'An active schedule already exists — it will be updated.',
					},
					fr: {
						absent_top20: 'Absent du top 20 sur cette zone',
						scan_frequency: 'Fréquence de scan',
						disclaimer_short: 'Avertissement algorithme Google',
						disclaimer: 'Attention aux algorithmes Google. Ne modifiez pas massivement votre fiche GBP sur la base de ces signaux sous peine de re-vérification ou suspension de votre fiche.',
						modal_title: 'Carte locale interactive',
						modal_kw_section_title: 'Sélection mots-clés',
						grid_desc_3: 'Quelques rues autour de votre adresse (9 points · ~350m entre points)',
						grid_desc_5: 'Votre quartier immédiat (25 points · ~710m entre points)',
						grid_desc_7: 'Votre quartier élargi (49 points · ~710m entre points)',
						grid_desc_9: 'Votre secteur de ville (81 points · ~570m entre points)',
						grid_desc_11: 'Entre quartier et arrondissement (121 points · ~850m entre points)',
						grid_desc_13: 'Votre arrondissement ou commune (169 points · ~710m entre points)',
						grid_desc_15: 'Large couverture urbaine — idéal pour les zones denses (225 points · ~710m entre points)',
						grid_desc_21: 'Territoire complet — idéal pour les SAB couvrant plusieurs zones (441 points · ~710m entre points)',
						present_on: 'Présent sur ',
						present_on_sfx: ' point(s)',
						competitor: 'Concurrent',
						legend_competitor: 'Concurrent',
						concurrents: 'Concurrents :',
						show_competitors: 'Afficher les concurrents',
						hide_competitors: 'Masquer les concurrents',
						top_rival: '⚠️ Concurrent dominant',
						best_rank_label: 'meilleure position',
						coverage_label: 'de la zone scannée',
						view_on_google: 'Voir sur Google',
						zone_help: 'Zones de territoire concurrent',
						threat_high: 'Menace élevée',
						threat_medium: 'Menace modérée',
						threat_low: 'Faible menace',
						outranks_label: 'Vous devance sur',
						locations_label: 'emplacements scannés',
						reviews_label: 'avis',
						intel_title: 'Analyse Concurrentielle',
						intel_subtitle: 'Top rivaux par niveau de menace',
						intel_no_scan: 'Lancez un scan pour voir l\'analyse concurrentielle.',
						intel_see_all: 'Voir tout dans l\'onglet Concurrents',
						intel_col_pos: 'Pos.',
						intel_col_name: 'Concurrent',
						intel_col_rating: '⭐ Note',
						intel_col_coverage: '📍 Couv.',
						intel_expand_tooltip: 'Agrandir le panel',
						tab_competitors: '🥊 Concurrents',
						comp_tab_title: 'Analyse Concurrentielle',
						comp_tab_sub: 'Tous les concurrents classés par niveau de menace',
						comp_export_excel: 'Exporter Excel',
						comp_export_pdf: 'Exporter PDF',
						comp_tab_empty: 'Lancez un scan et activez les concurrents pour voir l\'analyse.',
						comp_sort_label: 'Trier par :',
						sort_threat: '🔴 Menace',
						sort_coverage: '📍 Couverture',
						sort_rating: '⭐ Note',
						sort_reviews: '💬 Avis',
						sort_name: 'A-Z Nom',
						col_threat: 'Menace',
						col_category: 'Catégorie',
						col_reviews: '💬 Avis',
						tip_threat: 'Niveau de menace basé sur la meilleure position',
						tip_rank: 'Meilleure position sur tous les points scannés',
						tip_rating: 'Note Google moyenne',
						tip_reviews: 'Nombre total d\'avis Google',
						tip_coverage: 'Pourcentage de points scannés où ce concurrent apparaît',
						tip_name: 'Trier par ordre alphabétique',
						tip_competitor: 'Business en concurrence avec vous dans les résultats locaux',
						tip_category: 'Les catégories proviennent de la fiche Google Business Profile de chaque établissement, pas d\'un classement Avision. Google peut inclure des résultats peu liés à votre mot-clé selon la pertinence, la localisation et les avis — c\'est un comportement normal de Google Maps, pas une erreur Avision.',
						tip_gbp: 'Cliquer pour ouvrir la fiche Google Business',
						kw_power_label: 'Puissance du mot-clé',
						kw_power_dominant: 'Dominant',
						kw_power_strong: 'Fort',
						kw_power_growing: 'En progression',
						kw_power_weak: 'Faible',
						kw_power_critical: 'Critique',
						kw_power_point_sub: 'Cet emplacement précis',
						solde: 'Solde',
						multi_credits: ' mot(s)-clé(s) = ×',
						multi_credits_sfx: ' crédits',
						grille_drawer: 'grille drawer',
						lancer_scan: 'Lancer le scan',
						scan_btn_single: 'Relancer un scan — ',
						scan_btn_multi: 'Relancer ',
						scan_btn_scans: ' scans — ',
						scan_btn_credits: ' crédits',
						scan_sending: 'Demande de scan envoyée...',
						sort_rank: '# Position',
						sort_scanned: 'Scannés',
						kw_panel_title: 'Mots-clés Radar',
						kw_radar_label: 'mots-clés Radar',
						suivis: 'suivis',
						selected: 'sélectionnés',
						no_kw_tracked: 'Aucun mot-clé Radar suivi pour ce profil. Ajoutez des mots-clés dans Growth Radar.',
						pending_kw: 'En attente',
						step1_label: 'Mots-clés Radar',
						step1_hint: 'Sélectionnez 1 à 5 mots-clés à cartographier',
						step2_label: 'Aperçu Heatmap',
						step2_hint: 'Choisissez le style de carte et prévisualisez',
						open_map_btn: 'Ouvrir la carte',
						open_map_no_scan: 'Lancez un scan d\'abord',
						no_kw_selected: 'Sélectionnez au moins 1 mot-clé d\'abord',
						select_kw_hint: 'Sélectionnez jusqu\'à 5 mots-clés pour afficher les halos locaux.',
						kw_label_plural: 'mot(s)-clé(s)',
						credits_label: 'crédits',
						confirm_scan: 'Lancer le scan ?',
						confirm_ok: 'Confirmer et lancer',
						confirm_cancel: 'Annuler',
						balance_after_label: 'Solde après scan',
						step3_label: 'Nouveau scan',
						step3_hint: 'Choisissez la taille de grille et lancez le scan',
						vue_heatmap: 'Vue Heatmap',
						aucun_scan: 'Aucun scan',
						design_grid: 'Grille',
						design_points: 'Points',
						design_territory: 'Territoire',
						territoire_top3: 'Territoire Top 3',
						rang_moyen: 'Rang moyen',
						dernier_statut: 'Dernier statut',
						nouveau_scan: 'Nouveau scan',
						credits_dispo: 'crédits disponibles',
						heatmap_note: 'Cette vue utilise les mots-clés Growth Radar existants. La taille choisie détermine le nombre de points scannés et donc le coût en crédits.',
						taille_grille: 'Taille de grille',
						cout: 'Coût',
						apres_scan: 'Après scan',
						scan_feedback_no_keyword: 'Sélectionnez d\'abord un mot-clé Radar.',
						scan_feedback_no_center: 'Centre GBP indisponible pour ce profil. Lancez un premier scan depuis une source géolocalisée ou reconnectez Google.',
						scan_feedback_ready: 'Prêt à lancer un scan standard.',
						insufficient_credits: 'Crédits insuffisants pour cette taille de grille.',
						lancer_scan_drawer: 'Lancer le scan',
						legend_top3: 'Top 3',
						legend_found: 'Rang 4-10',
						legend_weak: 'Rang 11-20',
						legend_missing: '>20',
						hm_map_view_title: 'Growth Radar Heatmap',
						hm_map_view_body: 'La Heatmap va plus loin que votre rang moyen. Elle révèle votre visibilité réelle sur Google Maps rue par rue, sur une grille de points autour de votre établissement. Chaque point indique où vos clients peuvent — ou ne peuvent pas — vous trouver. Identifiez vos zones aveugles, comparez vos mots-clés et suivez votre progression locale dans le temps.',
						active_keyword: 'Mot-clé actif',
						avi_recommendations_placeholder: 'Les conseils par mot-clé apparaîtront ici après branchement RAG.',
						avi_recommendations: 'Recommandations AVI',
						tab_map: '🗺️ Carte',
						tab_evolution: '📊 Évolution',
						evolution_intro: 'Comparez vos scans dans le temps. Sélectionnez jusqu\'à 5 scans à afficher côte à côte.',
						evolution_loading: 'Chargement de l\'historique...',
						evolution_no_scans: 'Aucun scan terminé trouvé pour ce mot-clé.',
						evolution_select_hint: 'Sélectionnez 2 à 5 scans à comparer.',
						evolution_compare_btn: 'Comparer les scans sélectionnés',
						evolution_delete_btn: 'Supprimer',
						evolution_delete_confirm: 'Supprimer ce scan ? Cette action est irréversible.',
						evolution_rank_label: 'Rang moy.',
						evolution_territory_label: 'Territoire Top 3',
						evolution_grid_label: 'Grille',
						evolution_sort_recent: '↓ Plus récent d\'abord',
						evolution_sort_oldest: '↑ Plus ancien d\'abord',
						evo_focus_btn: '⛶ Mode Focus',
						evo_ref_label: 'Référence',
						evo_ref_tooltip: 'Carte de référence — toutes les comparaisons (↑ ↓) sont calculées par rapport à ce scan.',
						evo_mixed_grids_warning: 'Tailles de grille différentes détectées. Les comparaisons sont approximatives — seuls les points centraux sont communs entre grilles de tailles différentes.',
						evo_diff_grid_tooltip: 'Ce scan utilise une grille différente de la référence. Seuls les points centraux sont comparables.',
						evo_common_points: 'points centraux communs avec la référence',
						evolution_date_label: 'Date',
						no_keyword: 'Aucun mot-clé',
						center_not_found: 'Centre géographique introuvable pour ce profil.',
						leaflet_not_loaded: 'Carte indisponible : Leaflet n\'est pas chargé.',
						scan_missing: 'Scan manquant :',
						launch_first_scan: 'Lancez un premier scan pour afficher les positions locales.',
						scan_refused: 'Le scan Heatmap a été refusé.',
						scan_launched: 'Scan Heatmap lancé',
						scan_result_pending: 'Le résultat apparaîtra après traitement.',
						scan_error: 'Erreur pendant le lancement du scan.',
						coverage_score_label: 'Couverture Top 3',
						coverage_score_none: 'Aucun scan',
						trend_up_title: 'Progression depuis le scan précédent',
						trend_down_title: 'Régression depuis le scan précédent',
						trend_same_title: 'Stable depuis le scan précédent',
						trend_new_title: 'Nouveau point — aucune donnée précédente',
						trend_legend_title: 'Tendance vs scan précédent',
						schedule_active:       'Scan programmé actif',
						schedule_saved:        'Programmation enregistrée',
						schedule_cancel:       'Annuler la programmation',
						schedule_note_once:    'Scan unique',
						schedule_note_freq:    'scan(s)/mois — ~{cost} crédits/mois',
						schedule_confirm_freq: 'Scan programmé — {freq_label} · {scans}/mois',
						schedule_credits_line: '{cost} crédits/scan × {scans} scans = {total} crédits/mois',
						schedule_balance_ok:   'Solde actuel : {balance} crédits ✓',
						schedule_balance_warn: 'Solde actuel : {balance} crédits ⚠ — rechargement conseillé',
						schedule_multi_warn:   'Un schedule actif existe déjà — il sera mis à jour.',
					},
					es: {
						absent_top20: 'Ausente del top 20 en esta zona',
						scan_frequency: 'Frecuencia de escaneo',
						disclaimer_short: 'Advertencia algoritmo Google',
						disclaimer: 'Atención a los algoritmos de Google. No modifique masivamente su ficha GBP basándose en estas señales so pena de re-verificación o suspensión.',
						modal_title: 'Mapa local interactivo',
						modal_kw_section_title: 'Selección de palabras clave',
						grid_desc_3: 'Algunas calles alrededor de su dirección (9 puntos · ~350m entre puntos)',
						grid_desc_5: 'Su barrio inmediato (25 puntos · ~710m entre puntos)',
						grid_desc_7: 'Su barrio ampliado (49 puntos · ~710m entre puntos)',
						grid_desc_9: 'Su sector de ciudad (81 puntos · ~570m entre puntos)',
						grid_desc_11: 'Entre barrio y distrito (121 puntos · ~850m entre puntos)',
						grid_desc_13: 'Su distrito o municipio (169 puntos · ~710m entre puntos)',
						grid_desc_15: 'Amplia cobertura urbana — ideal para zonas densas (225 puntos · ~710m entre puntos)',
						grid_desc_21: 'Territorio completo — ideal para SAB que cubren varias zonas (441 puntos · ~710m entre puntos)',
						present_on: 'Presente en ',
						present_on_sfx: ' punto(s)',
						competitor: 'Competidor',
						legend_competitor: 'Competidor',
						concurrents: 'Competidores:',
						show_competitors: 'Mostrar competidores',
						hide_competitors: 'Ocultar competidores',
						top_rival: '⚠️ Rival dominante',
						best_rank_label: 'mejor posición',
						coverage_label: 'del área escaneada',
						view_on_google: 'Ver en Google',
						zone_help: 'Zonas de territorio competidor',
						threat_high: 'Amenaza alta',
						threat_medium: 'Amenaza media',
						threat_low: 'Amenaza baja',
						outranks_label: 'Te supera en',
						locations_label: 'ubicaciones escaneadas',
						reviews_label: 'reseñas',
						intel_title: 'Inteligencia Competitiva',
						intel_subtitle: 'Principales rivales por nivel de amenaza',
						intel_no_scan: 'Lanza un escaneo para ver el análisis competitivo.',
						intel_see_all: 'Ver todo en la pestaña Competidores',
						intel_col_pos: 'Pos.',
						intel_col_name: 'Competidor',
						intel_col_rating: '⭐ Cal.',
						intel_col_coverage: '📍 Cob.',
						intel_expand_tooltip: 'Ampliar panel',
						tab_competitors: '🥊 Competidores',
						comp_tab_title: 'Análisis Competitivo',
						comp_tab_sub: 'Todos los competidores clasificados por nivel de amenaza',
						comp_export_excel: 'Exportar Excel',
						comp_export_pdf: 'Exportar PDF',
						comp_tab_empty: 'Lanza un escaneo y activa los competidores para ver el análisis.',
						comp_sort_label: 'Ordenar por:',
						sort_threat: '🔴 Amenaza',
						sort_coverage: '📍 Cobertura',
						sort_rating: '⭐ Calificación',
						sort_reviews: '💬 Reseñas',
						sort_name: 'A-Z Nombre',
						col_threat: 'Amenaza',
						col_category: 'Categoría',
						col_reviews: '💬 Reseñas',
						tip_threat: 'Nivel de amenaza basado en la mejor posición',
						tip_rank: 'Mejor posición en todos los puntos escaneados',
						tip_rating: 'Calificación promedio de Google',
						tip_reviews: 'Número total de reseñas de Google',
						tip_coverage: 'Porcentaje de puntos escaneados donde aparece este competidor',
						tip_name: 'Ordenar alfabéticamente por nombre',
						tip_competitor: 'Negocio que compite contigo en los resultados locales',
						tip_category: 'Las categorías provienen de la ficha de Google Business Profile de cada negocio, no de una clasificación de Avision. Google puede incluir resultados poco relacionados con tu palabra clave según relevancia, ubicación y reseñas — esto es un comportamiento normal de Google Maps, no un error de Avision.',
						tip_gbp: 'Clic para abrir el perfil de Google Business',
						kw_power_label: 'Potencia de palabra clave',
						kw_power_dominant: 'Dominante',
						kw_power_strong: 'Fuerte',
						kw_power_growing: 'En crecimiento',
						kw_power_weak: 'Débil',
						kw_power_critical: 'Crítico',
						kw_power_point_sub: 'Esta ubicación específica',
						solde: 'Saldo',
						multi_credits: ' palabra(s) clave = ×',
						multi_credits_sfx: ' créditos',
						grille_drawer: 'cuadrícula drawer',
						lancer_scan: 'Lanzar escaneo',
						scan_btn_single: 'Relanzar escaneo — ',
						scan_btn_multi: 'Relanzar ',
						scan_btn_scans: ' escaneos — ',
						scan_btn_credits: ' créditos',
						scan_sending: 'Solicitud de escaneo enviada...',
						sort_rank: '# Posición',
						sort_scanned: 'Escaneados',
						kw_panel_title: 'Palabras clave Radar',
						kw_radar_label: 'palabras clave Radar',
						suivis: 'seguidos',
						selected: 'seleccionadas',
						no_kw_tracked: 'No hay palabras clave Radar seguidas para este perfil. Añada palabras clave en Growth Radar.',
						pending_kw: 'Pendiente',
						step1_label: 'Palabras clave Radar',
						step1_hint: 'Selecciona entre 1 y 5 palabras clave',
						step2_label: 'Vista del mapa',
						step2_hint: 'Elige el estilo de mapa y previsualiza',
						open_map_btn: 'Abrir el mapa',
						open_map_no_scan: 'Lanza un escaneo primero',
						no_kw_selected: 'Selecciona al menos 1 palabra clave primero',
						select_kw_hint: 'Selecciona hasta 5 palabras clave para mostrar los halos locales.',
						kw_label_plural: 'palabra(s) clave',
						credits_label: 'créditos',
						confirm_scan: '¿Lanzar escaneo?',
						confirm_ok: 'Confirmar y lanzar',
						confirm_cancel: 'Cancelar',
						balance_after_label: 'Saldo después del escaneo',
						step3_label: 'Nuevo escaneo',
						step3_hint: 'Elige el tamaño de cuadrícula y lanza el escaneo',
						vue_heatmap: 'Vista Heatmap',
						aucun_scan: 'Sin escaneo',
						design_grid: 'Cuadrícula',
						design_points: 'Puntos',
						design_territory: 'Territorio',
						territoire_top3: 'Territorio Top 3',
						rang_moyen: 'Rango promedio',
						dernier_statut: 'Último estado',
						nouveau_scan: 'Nuevo escaneo',
						credits_dispo: 'créditos disponibles',
						heatmap_note: 'Esta vista utiliza sus palabras clave Growth Radar existentes. El tamaño de la cuadrícula determina el número de puntos escaneados y el coste en créditos.',
						taille_grille: 'Tamaño de cuadrícula',
						cout: 'Coste',
						apres_scan: 'Tras escaneo',
						scan_feedback_no_keyword: 'Seleccione primero una palabra clave Radar.',
						scan_feedback_no_center: 'Centro GBP no disponible para este perfil. Lance un primer escaneo desde una fuente geolocalizada o reconecte Google.',
						scan_feedback_ready: 'Listo para lanzar un escaneo estándar.',
						insufficient_credits: 'Créditos insuficientes para este tamaño de cuadrícula.',
						lancer_scan_drawer: 'Lanzar escaneo',
						legend_top3: 'Top 3',
						legend_found: 'Rango 4-10',
						legend_weak: 'Rango 11-20',
						legend_missing: '>20',
						hm_map_view_title: 'Growth Radar Heatmap',
						hm_map_view_body: 'El Heatmap va más allá de tu rango promedio. Muestra tu visibilidad real en Google Maps calle a calle, en una cuadrícula de puntos alrededor de tu negocio. Cada punto revela dónde tus clientes pueden — o no pueden — encontrarte. Detecta zonas ciegas, compara palabras clave y sigue tu dominio local en el tiempo.',
						active_keyword: 'Palabra clave activa',
						avi_recommendations_placeholder: 'Los consejos por palabra clave aparecerán aquí tras la conexión RAG.',
						avi_recommendations: 'Recomendaciones AVI',
						tab_map: '🗺️ Mapa',
						tab_evolution: '📊 Evolución',
						evolution_intro: 'Compare sus escaneos en el tiempo. Seleccione hasta 5 escaneos para mostrar uno al lado del otro.',
						evolution_loading: 'Cargando historial de escaneos...',
						evolution_no_scans: 'No se encontraron escaneos completados para esta palabra clave.',
						evolution_select_hint: 'Seleccione entre 2 y 5 escaneos para comparar.',
						evolution_compare_btn: 'Comparar escaneos seleccionados',
						evolution_delete_btn: 'Eliminar',
						evolution_delete_confirm: '¿Eliminar este escaneo? Esta acción es irreversible.',
						evolution_rank_label: 'Rango prom.',
						evolution_territory_label: 'Territorio Top 3',
						evolution_grid_label: 'Cuadrícula',
						evolution_sort_recent: '↓ Más reciente primero',
						evolution_sort_oldest: '↑ Más antiguo primero',
						evo_focus_btn: '⛶ Modo Enfoque',
						evo_ref_label: 'Referencia',
						evo_ref_tooltip: 'Mapa de referencia — todas las comparaciones (↑ ↓) se calculan respecto a este escaneo.',
						evo_mixed_grids_warning: 'Tamaños de cuadrícula mixtos detectados. Las comparaciones son aproximadas — solo los puntos centrales son comunes entre cuadrículas de diferentes tamaños.',
						evo_diff_grid_tooltip: 'Este escaneo usa una cuadrícula diferente a la referencia. Solo los puntos centrales son comparables.',
						evo_common_points: 'puntos centrales comunes con la referencia',
						evolution_date_label: 'Fecha',
						no_keyword: 'Sin palabra clave',
						center_not_found: 'Centro geográfico no encontrado para este perfil.',
						leaflet_not_loaded: 'Mapa no disponible: Leaflet no está cargado.',
						scan_missing: 'Escaneo faltante:',
						launch_first_scan: 'Lance un primer escaneo para mostrar las posiciones locales.',
						scan_refused: 'El escaneo Heatmap fue rechazado.',
						scan_launched: 'Escaneo Heatmap lanzado',
						scan_result_pending: 'El resultado aparecerá tras el procesamiento.',
						scan_error: 'Error durante el lanzamiento del escaneo.',
						coverage_score_label: 'Cobertura Top 3',
						coverage_score_none: 'Sin escaneo',
						trend_up_title: 'Mejora respecto al escaneo anterior',
						trend_down_title: 'Descenso respecto al escaneo anterior',
						trend_same_title: 'Estable respecto al escaneo anterior',
						trend_new_title: 'Nuevo punto — sin datos previos',
						trend_legend_title: 'Tendencia vs escaneo anterior',
						schedule_active:       'Escaneo programado activo',
						schedule_saved:        'Programación guardada',
						schedule_cancel:       'Cancelar programación',
						schedule_note_once:    'Escaneo único',
						schedule_note_freq:    'escaneo(s)/mes — ~{cost} créditos/mes',
						schedule_confirm_freq: 'Escaneo programado — {freq_label} · {scans}/mes',
						schedule_credits_line: '{cost} créditos/escaneo × {scans} escaneos = {total} créditos/mes',
						schedule_balance_ok:   'Saldo actual: {balance} créditos ✓',
						schedule_balance_warn: 'Saldo actual: {balance} créditos ⚠ — recarga recomendada',
						schedule_multi_warn:   'Ya existe un programa activo — se actualizará.',
					},
					de: {
						absent_top20: 'Nicht in den Top 20 in diesem Bereich',
						scan_frequency: 'Scan-Häufigkeit',
						disclaimer_short: 'Google-Algorithmus-Warnung',
						disclaimer: 'Achtung: Google-Algorithmen. Nehmen Sie keine massiven Änderungen an Ihrem GBP-Eintrag vor — sonst riskieren Sie eine erneute Verifizierung oder Sperrung.',
						modal_title: 'Interaktive lokale Karte',
						modal_kw_section_title: 'Keyword-Auswahl',
						grid_desc_3: 'Einige Straßen rund um Ihre Adresse (9 Punkte · ~350m zwischen Punkten)',
						grid_desc_5: 'Ihre unmittelbare Nachbarschaft (25 Punkte · ~710m zwischen Punkten)',
						grid_desc_7: 'Ihre erweiterte Nachbarschaft (49 Punkte · ~710m zwischen Punkten)',
						grid_desc_9: 'Ihr Stadtgebiet (81 Punkte · ~570m zwischen Punkten)',
						grid_desc_11: 'Zwischen Viertel und Bezirk (121 Punkte · ~850m zwischen Punkten)',
						grid_desc_13: 'Ihr Bezirk oder Ihre Gemeinde (169 Punkte · ~710m zwischen Punkten)',
						grid_desc_15: 'Breite städtische Abdeckung — ideal für dichte Gebiete (225 Punkte · ~710m zwischen Punkten)',
						grid_desc_21: 'Vollständiges Gebiet — ideal für SAB mit mehreren Zonen (441 Punkte · ~710m zwischen Punkten)',
						present_on: 'Vorhanden auf ',
						present_on_sfx: ' Punkt(e)',
						competitor: 'Mitbewerber',
						legend_competitor: 'Mitbewerber',
						concurrents: 'Mitbewerber:',
						show_competitors: 'Mitbewerber anzeigen',
						hide_competitors: 'Mitbewerber ausblenden',
						top_rival: '⚠️ Dominanter Mitbewerber',
						best_rank_label: 'beste Position',
						coverage_label: 'des gescannten Bereichs',
						view_on_google: 'Auf Google ansehen',
						zone_help: 'Konkurrenz-Territoriumszonen',
						threat_high: 'Hohe Bedrohung',
						threat_medium: 'Mittlere Bedrohung',
						threat_low: 'Geringe Bedrohung',
						outranks_label: 'Überholt Sie auf',
						locations_label: 'gescannte Standorte',
						reviews_label: 'Bewertungen',
						intel_title: 'Wettbewerbsanalyse',
						intel_subtitle: 'Top-Rivalen nach Bedrohungsniveau',
						intel_no_scan: 'Starten Sie einen Scan, um die Wettbewerbsanalyse zu sehen.',
						intel_see_all: 'Alle im Tab Mitbewerber ansehen',
						intel_col_pos: 'Pos.',
						intel_col_name: 'Mitbewerber',
						intel_col_rating: '⭐ Note',
						intel_col_coverage: '📍 Abdeckung',
						intel_expand_tooltip: 'Panel vergrößern',
						tab_competitors: '🥊 Mitbewerber',
						comp_tab_title: 'Wettbewerbsanalyse',
						comp_tab_sub: 'Alle Mitbewerber nach Bedrohungsniveau sortiert',
						comp_export_excel: 'Excel exportieren',
						comp_export_pdf: 'PDF exportieren',
						comp_tab_empty: 'Starten Sie einen Scan und aktivieren Sie Mitbewerber.',
						comp_sort_label: 'Sortieren nach:',
						sort_threat: '🔴 Bedrohung',
						sort_coverage: '📍 Abdeckung',
						sort_rating: '⭐ Note',
						sort_reviews: '💬 Bewertungen',
						sort_name: 'A-Z Name',
						col_threat: 'Bedrohung',
						col_category: 'Kategorie',
						col_reviews: '💬 Bewertungen',
						tip_threat: 'Bedrohungsniveau basierend auf der besten Position',
						tip_rank: 'Beste Position über alle gescannten Punkte',
						tip_rating: 'Durchschnittliche Google-Bewertung',
						tip_reviews: 'Gesamtzahl der Google-Bewertungen',
						tip_coverage: 'Prozentsatz der gescannten Punkte, an denen dieser Mitbewerber erscheint',
						tip_name: 'Alphabetisch sortieren',
						tip_competitor: 'Unternehmen, das mit Ihnen in den lokalen Ergebnissen konkurriert',
						tip_category: 'Kategorien stammen aus dem Google Business Profile jedes Unternehmens.',
						tip_gbp: 'Klicken, um das Google Business Profil zu öffnen',
						kw_power_label: 'Keyword-Stärke',
						kw_power_dominant: 'Dominant',
						kw_power_strong: 'Stark',
						kw_power_growing: 'Wachsend',
						kw_power_weak: 'Schwach',
						kw_power_critical: 'Kritisch',
						kw_power_point_sub: 'Dieser genaue Standort',
						solde: 'Guthaben',
						multi_credits: ' Keyword(s) = ×',
						multi_credits_sfx: ' Credits',
						grille_drawer: 'Raster-Drawer',
						lancer_scan: 'Scan starten',
						scan_btn_single: 'Scan neu starten — ',
						scan_btn_multi: 'Neu starten ',
						scan_btn_scans: ' Scans — ',
						scan_btn_credits: ' Credits',
						scan_sending: 'Scan-Anfrage gesendet...',
						sort_rank: '# Position',
						sort_scanned: 'Gescannt',
						kw_panel_title: 'Radar-Keywords',
						kw_radar_label: 'Radar-Keywords',
						suivis: 'verfolgt',
						selected: 'ausgewählt',
						no_kw_tracked: 'Keine Radar-Keywords für dieses Profil. Fügen Sie Keywords in Growth Radar hinzu.',
						pending_kw: 'Ausstehend',
						step1_label: 'Radar-Keywords',
						step1_hint: 'Wählen Sie 1 bis 5 Keywords aus',
						step2_label: 'Heatmap-Vorschau',
						step2_hint: 'Wählen Sie den Kartenstil und zeigen Sie eine Vorschau an',
						open_map_btn: 'Karte öffnen',
						open_map_no_scan: 'Starten Sie zuerst einen Scan',
						no_kw_selected: 'Wählen Sie zuerst mindestens 1 Keyword aus',
						select_kw_hint: 'Wählen Sie bis zu 5 Keywords aus, um lokale Halos anzuzeigen.',
						kw_label_plural: 'Keyword(s)',
						credits_label: 'Credits',
						confirm_scan: 'Scan starten?',
						confirm_ok: 'Bestätigen und starten',
						confirm_cancel: 'Abbrechen',
						balance_after_label: 'Guthaben nach Scan',
						step3_label: 'Neuer Scan',
						step3_hint: 'Rastergröße wählen und Scan starten',
						vue_heatmap: 'Heatmap-Ansicht',
						aucun_scan: 'Kein Scan',
						design_grid: 'Raster',
						design_points: 'Punkte',
						design_territory: 'Gebiet',
						territoire_top3: 'Top-3-Gebiet',
						rang_moyen: 'Durchschnittlicher Rang',
						dernier_statut: 'Letzter Status',
						nouveau_scan: 'Neuer Scan',
						credits_dispo: 'Credits verfügbar',
						heatmap_note: 'Diese Ansicht verwendet bestehende Growth Radar Keywords. Die gewählte Größe bestimmt die Anzahl der gescannten Punkte und damit die Credit-Kosten.',
						taille_grille: 'Rastergröße',
						cout: 'Kosten',
						apres_scan: 'Nach Scan',
						scan_feedback_no_keyword: 'Wählen Sie zuerst ein Radar-Keyword.',
						scan_feedback_no_center: 'GBP-Zentrum für dieses Profil nicht verfügbar.',
						scan_feedback_ready: 'Bereit für einen Standard-Scan.',
						insufficient_credits: 'Nicht genug Credits für diese Rastergröße.',
						lancer_scan_drawer: 'Scan starten',
						legend_top3: 'Top 3',
						legend_found: 'Rang 4-10',
						legend_weak: 'Rang 11-20',
						legend_missing: '>20',
						hm_map_view_title: 'Growth Radar Heatmap',
						hm_map_view_body: 'Die Heatmap geht über Ihren Durchschnittsrang hinaus. Sie zeigt Ihre tatsächliche Sichtbarkeit auf Google Maps Straße für Straße.',
						active_keyword: 'Aktives Keyword',
						avi_recommendations_placeholder: 'Keyword-Empfehlungen erscheinen hier nach RAG-Verbindung.',
						avi_recommendations: 'AVI-Empfehlungen',
						tab_map: '🗺️ Karte',
						tab_evolution: '📊 Entwicklung',
						evolution_intro: 'Vergleichen Sie Ihre Scans im Zeitverlauf. Wählen Sie bis zu 5 Scans aus.',
						evolution_loading: 'Verlauf wird geladen...',
						evolution_no_scans: 'Kein abgeschlossener Scan für dieses Keyword gefunden.',
						evolution_select_hint: 'Wählen Sie 2 bis 5 Scans zum Vergleichen.',
						evolution_compare_btn: 'Ausgewählte Scans vergleichen',
						evolution_delete_btn: 'Löschen',
						evolution_delete_confirm: 'Diesen Scan löschen? Diese Aktion ist unwiderruflich.',
						evolution_rank_label: 'Durchschn. Rang',
						evolution_territory_label: 'Top-3-Gebiet',
						evolution_grid_label: 'Raster',
						evolution_sort_recent: '↓ Neueste zuerst',
						evolution_sort_oldest: '↑ Älteste zuerst',
						evo_focus_btn: '⛶ Fokus-Modus',
						evo_ref_label: 'Referenz',
						evo_ref_tooltip: 'Referenzkarte — alle Vergleiche (↑ ↓) werden relativ zu diesem Scan berechnet.',
						evo_mixed_grids_warning: 'Unterschiedliche Rastergrößen erkannt. Vergleiche sind näherungsweise.',
						evo_diff_grid_tooltip: 'Dieser Scan verwendet ein anderes Raster als die Referenz.',
						evo_common_points: 'gemeinsame Mittelpunkte mit der Referenz',
						evolution_date_label: 'Datum',
						no_keyword: 'Kein Keyword',
						center_not_found: 'Geografisches Zentrum für dieses Profil nicht gefunden.',
						leaflet_not_loaded: 'Karte nicht verfügbar: Leaflet ist nicht geladen.',
						scan_missing: 'Fehlender Scan:',
						launch_first_scan: 'Starten Sie einen ersten Scan, um lokale Positionen anzuzeigen.',
						scan_refused: 'Der Heatmap-Scan wurde abgelehnt.',
						scan_launched: 'Heatmap-Scan gestartet',
						scan_result_pending: 'Das Ergebnis erscheint nach der Verarbeitung.',
						scan_error: 'Fehler beim Starten des Scans.',
						coverage_score_label: 'Top-3-Abdeckung',
						coverage_score_none: 'Kein Scan',
						trend_up_title: 'Fortschritt seit dem letzten Scan',
						trend_down_title: 'Rückgang seit dem letzten Scan',
						trend_same_title: 'Stabil seit dem letzten Scan',
						trend_new_title: 'Neuer Punkt — keine vorherigen Daten',
						trend_legend_title: 'Trend vs. letzter Scan',
						schedule_active:       'Geplanter Scan aktiv',
						schedule_saved:        'Planung gespeichert',
						schedule_cancel:       'Planung abbrechen',
						schedule_note_once:    'Einmaliger Scan',
						schedule_note_freq:    'Scan(s)/Monat — ~{cost} Credits/Monat',
						schedule_confirm_freq: 'Geplanter Scan — {freq_label} · {scans}/Monat',
						schedule_credits_line: '{cost} Credits/Scan × {scans} Scans = {total} Credits/Monat',
						schedule_balance_ok:   'Aktuelles Guthaben: {balance} Credits ✓',
						schedule_balance_warn: 'Aktuelles Guthaben: {balance} Credits ⚠ — Aufladung empfohlen',
						schedule_multi_warn:   'Ein aktiver Plan existiert bereits — er wird aktualisiert.',
					},
					'pt-br': {
						absent_top20: 'Não está no top 20 nesta área',
						scan_frequency: 'Frequência de scan',
						disclaimer_short: 'Aviso do Algoritmo Google',
						disclaimer: 'Atenção: algoritmos do Google. Não modifique massivamente seu perfil GBP com base nesses sinais, pois pode arriscar reverificação ou suspensão.',
						modal_title: 'Mapa local interativo',
						modal_kw_section_title: 'Seleção de palavras-chave',
						grid_desc_3: 'Algumas ruas ao redor do seu endereço (9 pontos · ~350m entre pontos)',
						grid_desc_5: 'Sua vizinhança imediata (25 pontos · ~710m entre pontos)',
						grid_desc_7: 'Sua vizinhança ampliada (49 pontos · ~710m entre pontos)',
						grid_desc_9: 'Sua área urbana (81 pontos · ~570m entre pontos)',
						grid_desc_11: 'Entre bairro e distrito (121 pontos · ~850m entre pontos)',
						grid_desc_13: 'Seu distrito ou município (169 pontos · ~710m entre pontos)',
						grid_desc_15: 'Ampla cobertura urbana — ideal para áreas densas (225 pontos · ~710m entre pontos)',
						grid_desc_21: 'Território completo — ideal para SAB cobrindo várias zonas (441 pontos · ~710m entre pontos)',
						present_on: 'Presente em ',
						present_on_sfx: ' ponto(s)',
						competitor: 'Concorrente',
						legend_competitor: 'Concorrente',
						concurrents: 'Concorrentes:',
						show_competitors: 'Mostrar concorrentes',
						hide_competitors: 'Ocultar concorrentes',
						top_rival: '⚠️ Concorrente dominante',
						best_rank_label: 'melhor posição',
						coverage_label: 'da área escaneada',
						view_on_google: 'Ver no Google',
						zone_help: 'Zonas de território concorrente',
						threat_high: 'Ameaça alta',
						threat_medium: 'Ameaça moderada',
						threat_low: 'Ameaça baixa',
						outranks_label: 'Supera você em',
						locations_label: 'locais escaneados',
						reviews_label: 'avaliações',
						intel_title: 'Análise Competitiva',
						intel_subtitle: 'Principais rivais por nível de ameaça',
						intel_no_scan: 'Inicie um scan para ver a análise competitiva.',
						intel_see_all: 'Ver tudo na aba Concorrentes',
						intel_col_pos: 'Pos.',
						intel_col_name: 'Concorrente',
						intel_col_rating: '⭐ Nota',
						intel_col_coverage: '📍 Cobert.',
						intel_expand_tooltip: 'Expandir painel',
						tab_competitors: '🥊 Concorrentes',
						comp_tab_title: 'Análise Competitiva',
						comp_tab_sub: 'Todos os concorrentes classificados por nível de ameaça',
						comp_export_excel: 'Exportar Excel',
						comp_export_pdf: 'Exportar PDF',
						comp_tab_empty: 'Inicie um scan e ative os concorrentes para ver a análise.',
						comp_sort_label: 'Ordenar por:',
						sort_threat: '🔴 Ameaça',
						sort_coverage: '📍 Cobertura',
						sort_rating: '⭐ Nota',
						sort_reviews: '💬 Avaliações',
						sort_name: 'A-Z Nome',
						col_threat: 'Ameaça',
						col_category: 'Categoria',
						col_reviews: '💬 Avaliações',
						tip_threat: 'Nível de ameaça baseado na melhor posição',
						tip_rank: 'Melhor posição em todos os pontos escaneados',
						tip_rating: 'Avaliação média do Google',
						tip_reviews: 'Número total de avaliações do Google',
						tip_coverage: 'Percentual de pontos escaneados onde este concorrente aparece',
						tip_name: 'Ordenar alfabeticamente',
						tip_competitor: 'Empresa concorrendo com você nos resultados locais',
						tip_category: 'As categorias vêm do perfil Google Business de cada estabelecimento.',
						tip_gbp: 'Clique para abrir o perfil Google Business',
						kw_power_label: 'Força da palavra-chave',
						kw_power_dominant: 'Dominante',
						kw_power_strong: 'Forte',
						kw_power_growing: 'Em crescimento',
						kw_power_weak: 'Fraco',
						kw_power_critical: 'Crítico',
						kw_power_point_sub: 'Este local específico',
						solde: 'Saldo',
						multi_credits: ' palavra(s)-chave = ×',
						multi_credits_sfx: ' créditos',
						grille_drawer: 'drawer de grade',
						lancer_scan: 'Iniciar scan',
						scan_btn_single: 'Reiniciar scan — ',
						scan_btn_multi: 'Reiniciar ',
						scan_btn_scans: ' scans — ',
						scan_btn_credits: ' créditos',
						scan_sending: 'Solicitação de scan enviada...',
						sort_rank: '# Posição',
						sort_scanned: 'Escaneados',
						kw_panel_title: 'Keywords Radar',
						kw_radar_label: 'palavras-chave Radar',
						suivis: 'monitorados',
						selected: 'selecionados',
						no_kw_tracked: 'Nenhuma keyword Radar para este perfil. Adicione keywords no Growth Radar.',
						pending_kw: 'Pendente',
						step1_label: 'Keywords Radar',
						step1_hint: 'Selecione 1 a 5 keywords para mapear',
						step2_label: 'Prévia do Heatmap',
						step2_hint: 'Escolha o estilo de mapa e visualize',
						open_map_btn: 'Abrir mapa',
						open_map_no_scan: 'Inicie um scan primeiro',
						no_kw_selected: 'Selecione pelo menos 1 keyword primeiro',
						select_kw_hint: 'Selecione até 5 keywords para exibir halos locais.',
						kw_label_plural: 'palavra(s)-chave',
						credits_label: 'créditos',
						confirm_scan: 'Iniciar scan?',
						confirm_ok: 'Confirmar e iniciar',
						confirm_cancel: 'Cancelar',
						balance_after_label: 'Saldo após scan',
						step3_label: 'Novo scan',
						step3_hint: 'Escolha o tamanho da grade e inicie o scan',
						vue_heatmap: 'Vista Heatmap',
						aucun_scan: 'Nenhum scan',
						design_grid: 'Grade',
						design_points: 'Pontos',
						design_territory: 'Território',
						territoire_top3: 'Território Top 3',
						rang_moyen: 'Rank médio',
						dernier_statut: 'Último status',
						nouveau_scan: 'Novo scan',
						credits_dispo: 'créditos disponíveis',
						heatmap_note: 'Esta vista usa as keywords Growth Radar existentes. O tamanho escolhido determina o número de pontos escaneados e o custo em créditos.',
						taille_grille: 'Tamanho da grade',
						cout: 'Custo',
						apres_scan: 'Após scan',
						scan_feedback_no_keyword: 'Selecione primeiro uma keyword Radar.',
						scan_feedback_no_center: 'Centro GBP indisponível para este perfil.',
						scan_feedback_ready: 'Pronto para iniciar um scan padrão.',
						insufficient_credits: 'Créditos insuficientes para este tamanho de grade.',
						lancer_scan_drawer: 'Iniciar scan',
						legend_top3: 'Top 3',
						legend_found: 'Rank 4-10',
						legend_weak: 'Rank 11-20',
						legend_missing: '>20',
						hm_map_view_title: 'Growth Radar Heatmap',
						hm_map_view_body: 'O Heatmap vai além do seu rank médio. Ele revela sua visibilidade real no Google Maps rua por rua.',
						active_keyword: 'Keyword ativa',
						avi_recommendations_placeholder: 'Recomendações por keyword aparecerão aqui após conexão RAG.',
						avi_recommendations: 'Recomendações AVI',
						tab_map: '🗺️ Mapa',
						tab_evolution: '📊 Evolução',
						evolution_intro: 'Compare seus scans ao longo do tempo. Selecione até 5 scans.',
						evolution_loading: 'Carregando histórico...',
						evolution_no_scans: 'Nenhum scan concluído encontrado para esta keyword.',
						evolution_select_hint: 'Selecione 2 a 5 scans para comparar.',
						evolution_compare_btn: 'Comparar scans selecionados',
						evolution_delete_btn: 'Excluir',
						evolution_delete_confirm: 'Excluir este scan? Esta ação é irreversível.',
						evolution_rank_label: 'Rank médio',
						evolution_territory_label: 'Território Top 3',
						evolution_grid_label: 'Grade',
						evolution_sort_recent: '↓ Mais recente primeiro',
						evolution_sort_oldest: '↑ Mais antigo primeiro',
						evo_focus_btn: '⛶ Modo Foco',
						evo_ref_label: 'Referência',
						evo_ref_tooltip: 'Mapa de referência — todas as comparações (↑ ↓) são calculadas em relação a este scan.',
						evo_mixed_grids_warning: 'Tamanhos de grade diferentes detectados. As comparações são aproximadas.',
						evo_diff_grid_tooltip: 'Este scan usa uma grade diferente da referência.',
						evo_common_points: 'pontos centrais comuns com a referência',
						evolution_date_label: 'Data',
						no_keyword: 'Nenhuma keyword',
						center_not_found: 'Centro geográfico não encontrado para este perfil.',
						leaflet_not_loaded: 'Mapa indisponível: Leaflet não está carregado.',
						scan_missing: 'Scan ausente:',
						launch_first_scan: 'Inicie um primeiro scan para exibir as posições locais.',
						scan_refused: 'O scan Heatmap foi recusado.',
						scan_launched: 'Scan Heatmap iniciado',
						scan_result_pending: 'O resultado aparecerá após o processamento.',
						scan_error: 'Erro ao iniciar o scan.',
						coverage_score_label: 'Cobertura Top 3',
						coverage_score_none: 'Nenhum scan',
						trend_up_title: 'Progresso desde o último scan',
						trend_down_title: 'Regressão desde o último scan',
						trend_same_title: 'Estável desde o último scan',
						trend_new_title: 'Novo ponto — sem dados anteriores',
						trend_legend_title: 'Tendência vs último scan',
						schedule_active:       'Scan programado ativo',
						schedule_saved:        'Programação salva',
						schedule_cancel:       'Cancelar programação',
						schedule_note_once:    'Scan único',
						schedule_note_freq:    'scan(s)/mês — ~{cost} créditos/mês',
						schedule_confirm_freq: 'Scan programado — {freq_label} · {scans}/mês',
						schedule_credits_line: '{cost} créditos/scan × {scans} scans = {total} créditos/mês',
						schedule_balance_ok:   'Saldo atual: {balance} créditos ✓',
						schedule_balance_warn: 'Saldo atual: {balance} créditos ⚠ — recarga recomendada',
						schedule_multi_warn:   'Já existe uma programação ativa — ela será atualizada.',
					},
					tr: {
						absent_top20: 'Bu bölgede ilk 20\'de değil',
						scan_frequency: 'Tarama sıklığı',
						disclaimer_short: 'Google Algoritma Uyarısı',
						disclaimer: 'Uyarı: Google algoritmaları. Bu sinyallere dayanarak GBP listenizde toplu değişiklik yapmayın; aksi takdirde yeniden doğrulama veya askıya alma riskiyle karşılaşabilirsiniz.',
						modal_title: 'Etkileşimli yerel harita',
						modal_kw_section_title: 'Anahtar kelime seçimi',
						grid_desc_3: 'Adresiniz etrafındaki birkaç sokak (9 nokta · ~350m noktalar arası)',
						grid_desc_5: 'Yakın çevreniz (25 nokta · ~710m noktalar arası)',
						grid_desc_7: 'Genişletilmiş çevreniz (49 nokta · ~710m noktalar arası)',
						grid_desc_9: 'Şehir bölgeniz (81 nokta · ~570m noktalar arası)',
						grid_desc_11: 'Mahalle ve ilçe arası (121 nokta · ~850m noktalar arası)',
						grid_desc_13: 'İlçeniz veya belediyeniz (169 nokta · ~710m noktalar arası)',
						grid_desc_15: 'Geniş kentsel kapsama — yoğun bölgeler için ideal (225 nokta · ~710m noktalar arası)',
						grid_desc_21: 'Tam bölge — birden fazla alanı kapsayan SAB için ideal (441 nokta · ~710m noktalar arası)',
						present_on: 'Mevcut: ',
						present_on_sfx: ' nokta',
						competitor: 'Rakip',
						legend_competitor: 'Rakip',
						concurrents: 'Rakipler:',
						show_competitors: 'Rakipleri göster',
						hide_competitors: 'Rakipleri gizle',
						top_rival: '⚠️ Baskın rakip',
						best_rank_label: 'en iyi konum',
						coverage_label: 'taranan alanın',
						view_on_google: 'Google\'da görüntüle',
						zone_help: 'Rakip bölge alanları',
						threat_high: 'Yüksek tehdit',
						threat_medium: 'Orta tehdit',
						threat_low: 'Düşük tehdit',
						outranks_label: 'Sizi geçiyor:',
						locations_label: 'taranan konum',
						reviews_label: 'yorum',
						intel_title: 'Rekabet Analizi',
						intel_subtitle: 'Tehdit düzeyine göre en iyi rakipler',
						intel_no_scan: 'Rekabet analizini görmek için bir tarama başlatın.',
						intel_see_all: 'Rakipler sekmesinde tümünü görün',
						intel_col_pos: 'Sıra',
						intel_col_name: 'Rakip',
						intel_col_rating: '⭐ Puan',
						intel_col_coverage: '📍 Kapsama',
						intel_expand_tooltip: 'Paneli genişlet',
						tab_competitors: '🥊 Rakipler',
						comp_tab_title: 'Rekabet Analizi',
						comp_tab_sub: 'Tehdit düzeyine göre sıralanmış tüm rakipler',
						comp_export_excel: 'Excel dışa aktar',
						comp_export_pdf: 'PDF dışa aktar',
						comp_tab_empty: 'Analizi görmek için tarama başlatın ve rakipleri etkinleştirin.',
						comp_sort_label: 'Sırala:',
						sort_threat: '🔴 Tehdit',
						sort_coverage: '📍 Kapsama',
						sort_rating: '⭐ Puan',
						sort_reviews: '💬 Yorumlar',
						sort_name: 'A-Z Ad',
						col_threat: 'Tehdit',
						col_category: 'Kategori',
						col_reviews: '💬 Yorumlar',
						tip_threat: 'En iyi konuma dayalı tehdit düzeyi',
						tip_rank: 'Tüm taranan noktalardaki en iyi konum',
						tip_rating: 'Ortalama Google puanı',
						tip_reviews: 'Toplam Google yorum sayısı',
						tip_coverage: 'Bu rakibin göründüğü taranan nokta yüzdesi',
						tip_name: 'Alfabetik sıralama',
						tip_competitor: 'Yerel sonuçlarda sizinle rekabet eden işletme',
						tip_category: 'Kategoriler her işletmenin Google Business profilinden gelmektedir.',
						tip_gbp: 'Google Business profilini açmak için tıklayın',
						kw_power_label: 'Anahtar kelime gücü',
						kw_power_dominant: 'Baskın',
						kw_power_strong: 'Güçlü',
						kw_power_growing: 'Büyüyor',
						kw_power_weak: 'Zayıf',
						kw_power_critical: 'Kritik',
						kw_power_point_sub: 'Bu tam konum',
						solde: 'Bakiye',
						multi_credits: ' anahtar kelime = ×',
						multi_credits_sfx: ' kredi',
						grille_drawer: 'ızgara çekmecesi',
						lancer_scan: 'Taramayı başlat',
						scan_btn_single: 'Taramayı yeniden başlat — ',
						scan_btn_multi: 'Yeniden başlat ',
						scan_btn_scans: ' tarama — ',
						scan_btn_credits: ' kredi',
						scan_sending: 'Tarama isteği gönderildi...',
						sort_rank: '# Konum',
						sort_scanned: 'Taranan',
						kw_panel_title: 'Radar Anahtar Kelimeleri',
						kw_radar_label: 'Radar anahtar kelimeleri',
						suivis: 'takip edilen',
						selected: 'seçilen',
						no_kw_tracked: 'Bu profil için Radar anahtar kelimesi yok. Growth Radar\'a anahtar kelime ekleyin.',
						pending_kw: 'Beklemede',
						step1_label: 'Radar Anahtar Kelimeleri',
						step1_hint: '1 ila 5 anahtar kelime seçin',
						step2_label: 'Heatmap Önizleme',
						step2_hint: 'Harita stilini seçin ve önizleyin',
						open_map_btn: 'Haritayı aç',
						open_map_no_scan: 'Önce bir tarama başlatın',
						no_kw_selected: 'Önce en az 1 anahtar kelime seçin',
						select_kw_hint: 'Yerel haleleri göstermek için 5\'e kadar anahtar kelime seçin.',
						kw_label_plural: 'anahtar kelime',
						credits_label: 'kredi',
						confirm_scan: 'Tarama başlatılsın mı?',
						confirm_ok: 'Onayla ve başlat',
						confirm_cancel: 'İptal',
						balance_after_label: 'Tarama sonrası bakiye',
						step3_label: 'Yeni tarama',
						step3_hint: 'Izgara boyutunu seçin ve taramayı başlatın',
						vue_heatmap: 'Heatmap Görünümü',
						aucun_scan: 'Tarama yok',
						design_grid: 'Izgara',
						design_points: 'Noktalar',
						design_territory: 'Bölge',
						territoire_top3: 'Top 3 Bölgesi',
						rang_moyen: 'Ortalama sıralama',
						dernier_statut: 'Son durum',
						nouveau_scan: 'Yeni tarama',
						credits_dispo: 'kredi mevcut',
						heatmap_note: 'Bu görünüm mevcut Growth Radar anahtar kelimelerini kullanır. Seçilen boyut taranan nokta sayısını ve kredi maliyetini belirler.',
						taille_grille: 'Izgara boyutu',
						cout: 'Maliyet',
						apres_scan: 'Tarama sonrası',
						scan_feedback_no_keyword: 'Önce bir Radar anahtar kelimesi seçin.',
						scan_feedback_no_center: 'Bu profil için GBP merkezi mevcut değil.',
						scan_feedback_ready: 'Standart tarama için hazır.',
						insufficient_credits: 'Bu ızgara boyutu için yeterli kredi yok.',
						lancer_scan_drawer: 'Taramayı başlat',
						legend_top3: 'Top 3',
						legend_found: 'Sıra 4-10',
						legend_weak: 'Sıra 11-20',
						legend_missing: '>20',
						hm_map_view_title: 'Growth Radar Heatmap',
						hm_map_view_body: 'Heatmap ortalama sıralamanızın ötesine geçer. Google Maps\'teki gerçek görünürlüğünüzü sokak sokak ortaya koyar.',
						active_keyword: 'Aktif anahtar kelime',
						avi_recommendations_placeholder: 'RAG bağlantısından sonra anahtar kelime önerileri burada görünecek.',
						avi_recommendations: 'AVI Önerileri',
						tab_map: '🗺️ Harita',
						tab_evolution: '📊 Gelişim',
						evolution_intro: 'Taramalarınızı zaman içinde karşılaştırın. 5\'e kadar tarama seçin.',
						evolution_loading: 'Geçmiş yükleniyor...',
						evolution_no_scans: 'Bu anahtar kelime için tamamlanmış tarama bulunamadı.',
						evolution_select_hint: 'Karşılaştırmak için 2 ila 5 tarama seçin.',
						evolution_compare_btn: 'Seçili taramaları karşılaştır',
						evolution_delete_btn: 'Sil',
						evolution_delete_confirm: 'Bu tarama silinsin mi? Bu işlem geri alınamaz.',
						evolution_rank_label: 'Ort. sıralama',
						evolution_territory_label: 'Top 3 Bölgesi',
						evolution_grid_label: 'Izgara',
						evolution_sort_recent: '↓ En yeni önce',
						evolution_sort_oldest: '↑ En eski önce',
						evo_focus_btn: '⛶ Odak Modu',
						evo_ref_label: 'Referans',
						evo_ref_tooltip: 'Referans harita — tüm karşılaştırmalar (↑ ↓) bu taramaya göre hesaplanır.',
						evo_mixed_grids_warning: 'Farklı ızgara boyutları tespit edildi. Karşılaştırmalar yaklaşıktır.',
						evo_diff_grid_tooltip: 'Bu tarama referanstan farklı bir ızgara kullanıyor.',
						evo_common_points: 'referansla ortak merkez noktalar',
						evolution_date_label: 'Tarih',
						no_keyword: 'Anahtar kelime yok',
						center_not_found: 'Bu profil için coğrafi merkez bulunamadı.',
						leaflet_not_loaded: 'Harita mevcut değil: Leaflet yüklenmedi.',
						scan_missing: 'Eksik tarama:',
						launch_first_scan: 'Yerel konumları göstermek için ilk taramayı başlatın.',
						scan_refused: 'Heatmap taraması reddedildi.',
						scan_launched: 'Heatmap taraması başlatıldı',
						scan_result_pending: 'Sonuç işlemden sonra görünecek.',
						scan_error: 'Tarama başlatılırken hata.',
						coverage_score_label: 'Top 3 Kapsaması',
						coverage_score_none: 'Tarama yok',
						trend_up_title: 'Son taramadan bu yana ilerleme',
						trend_down_title: 'Son taramadan bu yana gerileme',
						trend_same_title: 'Son taramadan bu yana kararlı',
						trend_new_title: 'Yeni nokta — önceki veri yok',
						trend_legend_title: 'Son taramaya karşı eğilim',
						schedule_active:       'Programlı tarama aktif',
						schedule_saved:        'Program kaydedildi',
						schedule_cancel:       'Programı iptal et',
						schedule_note_once:    'Tek tarama',
						schedule_note_freq:    'tarama/ay — ~{cost} kredi/ay',
						schedule_confirm_freq: 'Programlı tarama — {freq_label} · {scans}/ay',
						schedule_credits_line: '{cost} kredi/tarama × {scans} tarama = {total} kredi/ay',
						schedule_balance_ok:   'Mevcut bakiye: {balance} kredi ✓',
						schedule_balance_warn: 'Mevcut bakiye: {balance} kredi ⚠ — yükleme önerilir',
						schedule_multi_warn:   'Aktif bir program zaten mevcut — güncellenecek.',
					}
				};
				function hm_t(key){
					var d = HM_I18N[getHmLang()] || HM_I18N.fr;
					return d[key] !== undefined ? d[key] : ((HM_I18N.en && HM_I18N.en[key]) || key);
				}
				var radarCard = root.closest ? root.closest('.keyword-radar-card') : null;
				var toggle = root.querySelector('[data-avi-heatmap-toggle]');
				var drawer = root ? root.querySelector('.avi-heatmap-drawer') : null;
				var modal = root ? root.querySelector('.avi-heatmap-modal') : null;
				var designButtons = root.querySelectorAll('[data-avi-heatmap-design]');
				var keywordButtons = root.querySelectorAll('[data-heatmap-keyword]');
				var gridSelect = root.querySelector('[data-avi-heatmap-grid-size]');
				var gridDescEl = root.querySelector('[data-avi-heatmap-grid-desc]');
				var costEl = root.querySelector('[data-avi-heatmap-cost]');
				var balanceEl = root.querySelector('[data-avi-heatmap-balance]');
				var balanceAfterEl = root.querySelector('[data-avi-heatmap-balance-after]');
				var feedbackEl = root.querySelector('[data-avi-heatmap-scan-feedback]');
				var scanButtons = root.querySelectorAll('[data-avi-heatmap-scan-submit]');
				// #Sprint43-scheduler — DOM refs fréquence
				var frequencySelect = root.querySelector('[data-avi-hm-frequency]');
				var scheduleNoteEl  = root.querySelector('[data-avi-hm-schedule-note]');
				var keywordLabels = root.querySelectorAll('[data-avi-heatmap-current-keyword]');
				var modalKeywordButtons = root.querySelectorAll('[data-avi-heatmap-modal-keyword]');
				var modalKeywordSearch = root.querySelector('[data-avi-heatmap-kw-search="modal"]');
				var modalRankedKeywordButtons = root.querySelectorAll('[data-keyword-rank]');
				var modalAvgRankEl = root.querySelector('[data-avi-heatmap-modal-avg-rank]');
				var modalTerritoryEl = root.querySelector('[data-avi-heatmap-modal-territory]');
				var modalMissingEl = root.querySelector('[data-avi-heatmap-modal-missing]');
				var modalSelectionNoteEl = root.querySelector('[data-avi-heatmap-modal-selection-note]');
				var mapContainer = root.querySelector('[data-avi-heatmap-leaflet]');
				var mapOverlay = root.querySelector('[data-avi-heatmap-map-overlay]');
				var fallbackDesignMode = 'grid';
				var selectedKeyword = String(config.keyword || '').trim();
				var profileId = String(config.profile_id || root.getAttribute('data-profile-id') || 0);
				var creditBalance = Number(config.credit_balance || 0);
				var isSubmitting = false;
				var isScanInProgress = false;
				var preserveFeedback = false;
				var leafletMap = null;
				var leafletRenderer = null;
				var leafletLayer = null;
				var scanCache = {};
				var trendCache = {};
				var _renderMapDebounceTimer = null;
				function renderSelectedMapDebounced() {
					clearTimeout(_renderMapDebounceTimer);
					_renderMapDebounceTimer = setTimeout(function() {
						renderSelectedMap();
					}, 800);
				}
				// trendCache[keyword] = { prevScanId, points: { point_id: prevRank } }
				// Stocke les rangs du scan N-1 par keyword pour calcul de tendance
				var trendLayers = {};
				var competitorMarkers = [];
				var _activeCompPids = {}; // Anti-régression #Sprint43-Frontend5-pids — place_id actifs persistants
				var _lastBestByPid = {}; // Anti-régression #Sprint43-C12-bestbypid — exposé pour sidebar niveau bataille
				var _allPointsByPid = {}; // Anti-régression #Sprint43-C12-zone — tous points grille par concurrent pour polygone
				var _activeCompZones = {}; // Anti-régression #Sprint43-C12-zone — état polygones actifs
				var _compZoneLayers = {}; // Anti-régression #Sprint43-C12-zone — L.polygon par pid
				// Anti-régression #Sprint43-C12-battlestyle-absolute
				// Logique bataille basée sur rank concurrent absolu + comparaison profil
				// DANGER/Menace = rank concurrent absolu (indépendant du profil)
				// En lutte/Repoussé = comparaison relative profileRank vs compRank
				function _battleStyle(profileRank, compRank) {
					var compAbsent = (compRank == null || compRank > 20);
					var profAbsent = (profileRank == null || profileRank > 20);
					var _bl = getHmLang();
					var _bLabels = {
						repousse: { en: '✅ Repelled', fr: '✅ Repoussé', es: '✅ Repelido', de: '✅ Zurückgedrängt', 'pt-br': '✅ Repelido', tr: '✅ Geri püskürtüldü' },
						danger:   { en: '⚔️ DANGER',   fr: '⚔️ DANGER',   es: '⚔️ PELIGRO',  de: '⚔️ GEFAHR',       'pt-br': '⚔️ PERIGO',   tr: '⚔️ TEHLİKE' },
						menace:   { en: '⚠️ Threat',   fr: '⚠️ Menace',   es: '⚠️ Amenaza',  de: '⚠️ Bedrohung',    'pt-br': '⚠️ Ameaça',   tr: '⚠️ Tehdit' },
						lutte:    { en: '🔴 Fighting',  fr: '🔴 En lutte', es: '🔴 En pugna', de: '🔴 Im Kampf',      'pt-br': '🔴 Em disputa', tr: '🔴 Mücadelede' }
					};
					var _bl_get = function(k) { return (_bLabels[k][_bl] || _bLabels[k].en); };
					if (compAbsent && profAbsent) return null;
					if (compAbsent) return { color: '#94a3b8', pulse: false, label: _bl_get('repousse'), cls: 'threat--repousse' };
					if (compRank <= 3)  return { color: '#dc2626', pulse: 'fast',  label: _bl_get('danger'),   cls: 'threat--danger' };
					if (compRank <= 10) return { color: '#f97316', pulse: 'slow',  label: _bl_get('menace'),   cls: 'threat--menace' };
					if (!profAbsent && compRank > profileRank) return { color: '#94a3b8', pulse: false, label: _bl_get('repousse'), cls: 'threat--repousse' };
					return { color: '#f59e0b', pulse: false, label: _bl_get('lutte'), cls: 'threat--lutte' };
				}
				var heatmapHitTargets = [];
				var heatmapHoverKey = '';
				var heatmapHoverPopup = null;
				var heatmapCompetitorPopupOpen = false;
				var heatmapIsZoomRefresh = false;
				var selectedMapKeywords = [];
				var haloColors = ['#10b981', '#f59e0b', '#ef4444', '#3b82f6', '#a855f7'];
				if (selectedKeyword) {
					scanCache[selectedKeyword.toLowerCase()] = {
						keyword: selectedKeyword,
						avg_rank: config.avg_rank || null,
						territory_pct: config.territory_pct || null,
						scan_date: config.scan_date || null,
						radius_km: Number(config.default_radius_km || 5),
						grid_size: Number(config.default_grid_size || 7),
						map_points: Array.isArray(config.map_points) ? config.map_points : [],
						competitors: Array.isArray(config.competitors) ? config.competitors : [],
						status: config.status || null
					};
				}
				function safeGetDesign(){
					try {
						return window.localStorage ? String(window.localStorage.getItem('avi_heatmap_design_mode') || '') : fallbackDesignMode;
					} catch (e) {
						return fallbackDesignMode;
					}
				}
				function safeSetDesign(mode){
					fallbackDesignMode = mode;
					try {
						if (window.localStorage) window.localStorage.setItem('avi_heatmap_design_mode', mode);
					} catch (e) {}
				}
				function hasCenter(){
					return !!(config.center && isFinite(Number(config.center.lat)) && isFinite(Number(config.center.lng)));
				}
				function setFeedback(message, state){
					if (!feedbackEl) return;
					feedbackEl.textContent = message || '';
					feedbackEl.setAttribute('data-state', state || 'info');
					var modalFeedbackEl = root.querySelector(
						'[data-avi-heatmap-scan-feedback][style]');
					if (modalFeedbackEl) {
						if (message) {
							modalFeedbackEl.style.display = '';
							modalFeedbackEl.textContent = message;
							modalFeedbackEl.setAttribute('data-state', state || 'info');
						} else {
							modalFeedbackEl.style.display = 'none';
							modalFeedbackEl.textContent = '';
						}
					}
				}
				function selectedGridSize(){
					var n = gridSelect ? parseInt(gridSelect.value, 10) : Number(config.default_grid_size || 7);
					return (isFinite(n) && n > 0) ? n : 7;
				}
				function updateModalScanCostText(){
					return;
				}
				function setScanButtonsDisabled(disabled){
					scanButtons.forEach(function(btn){
						btn.disabled = !!disabled;
					});
				}
				function refreshCreditState(){
					var size = selectedGridSize();
					var kwCount = selectedMapKeywords.length || 1;
					var cost = size * size * kwCount;
					var after = Math.max(0, creditBalance - cost);
					if (costEl) costEl.textContent = String(cost);
					var multiEl = root.querySelector('[data-avi-heatmap-modal-scan-multi]');
					if (multiEl) {
						multiEl.textContent = kwCount > 1
							? kwCount + hm_t('multi_credits') + kwCount + 'x' + hm_t('multi_credits_sfx')
							: hm_t('multi_credits').replace('1 ', '1 ');
					}
					if (balanceEl) balanceEl.textContent = String(Math.max(0, creditBalance));
					if (balanceAfterEl) balanceAfterEl.textContent = String(after);
					var disabled = isSubmitting || selectedMapKeywords.length === 0 || !hasCenter() || cost > creditBalance;
					setScanButtonsDisabled(disabled);
					var feedbackEl = root.querySelector(
						'[data-avi-heatmap-scan-feedback]'
					);
					if (feedbackEl && !preserveFeedback) {
						if (selectedMapKeywords.length === 0) {
							feedbackEl.textContent = hm_t('no_kw_selected')
								|| 'Select at least 1 keyword first';
							feedbackEl.style.color = '#ef4444';
							updateModalScanCostText();
							return;
						} else if (cost > creditBalance) {
							feedbackEl.textContent = hm_t('insufficient_credits')
								|| 'Insufficient credits for this grid size.';
							feedbackEl.style.color = '#ef4444';
							updateModalScanCostText();
							return;
						} else {
							feedbackEl.style.color = '';
						}
					}
					updateModalScanCostText();
					if (preserveFeedback && !isSubmitting) return;
					if (!selectedKeyword) {
						setFeedback(hm_t('scan_feedback_no_keyword'), 'warning');
					} else if (!hasCenter()) {
						setFeedback(hm_t('scan_feedback_no_center'), 'warning');
					} else if (cost > creditBalance) {
						setFeedback(hm_t('insufficient_credits') || 'Insufficient credits for this grid size.', 'warning');
					} else if (!isSubmitting) {
						setFeedback(hm_t('scan_feedback_ready'), 'ok');
					}
					updateModalScanCostText();
				}
				function applyDesign(mode){
					mode = ['grid', 'points', 'territory'].indexOf(mode) >= 0 ? mode : 'grid';
					root.classList.remove('is-design-grid', 'is-design-points', 'is-design-territory');
					root.classList.add('is-design-' + mode);
					designButtons.forEach(function(btn){
						btn.classList.toggle('is-active', btn.getAttribute('data-avi-heatmap-design') === mode);
					});
					safeSetDesign(mode);
				}
				function setSelectedKeyword(keyword){
					if (!preserveFeedback) preserveFeedback = false;
					selectedKeyword = String(keyword || '').trim();
					keywordButtons.forEach(function(btn){
						btn.classList.toggle('is-selected', String(btn.getAttribute('data-heatmap-keyword') || '').trim().toLowerCase() === selectedKeyword.toLowerCase());
					});
					keywordLabels.forEach(function(el){
						el.textContent = selectedKeyword || hm_t('no_keyword');
					});
					updateDrawerKeywordButtons();
					updateModalKeywordButtons();
					refreshCreditState();
				}
				function applyHeatmapI18n(){
					root.querySelectorAll('[data-hm-i18n]').forEach(function(el){
						var key = el.getAttribute('data-hm-i18n') || '';
						if (!key) return;
						if (el.children && el.children.length) {
							setHmTextPreserveChildren(el, hm_t(key));
						} else {
							el.textContent = hm_t(key);
						}
					});
				}
				function updateGridDescription(){
					if (!gridDescEl) return;
					gridDescEl.textContent = hm_t('grid_desc_' + selectedGridSize());
				}
				function restHeaders(){
					var headers;
					if (typeof window.aviWpJsonFetchHeaders === 'function') {
						// #Sprint44-Plugin — utiliser comme base mais ne pas court-circuiter
						headers = window.aviWpJsonFetchHeaders();
					} else {
						headers = { 'Content-Type': 'application/json' };
						var nonce = config.nonce || '';
						if (nonce) headers['X-WP-Nonce'] = nonce;
					}
					// #Sprint44-Plugin — toujours injecter le token embed par-dessus
					var embedToken = config.embed_token
						|| window.aviEmbedToken
						|| (root && root.getAttribute('data-embed-token'))
						|| '';
					if (embedToken) {
						headers['X-Embed-Token'] = embedToken;
						delete headers['X-WP-Nonce'];
					}
					return headers;
				}
				function scanUrl(){
					var url = String(config.rest_url || '/wp-json/avision/v1/heatmap-scan');
					if (typeof window.aviAppendTestDriveParamsToWpAvisionUrl === 'function') {
						url = window.aviAppendTestDriveParamsToWpAvisionUrl(url);
					}
					return url;
				}
				function latestUrl(keyword){
					var base = String(config.latest_rest_url || '/wp-json/avision/v1/heatmap-latest');
					var sep = base.indexOf('?') >= 0 ? '&' : '?';
					var url = base + sep + 'profile_id=' + encodeURIComponent(String(config.profile_id || root.getAttribute('data-profile-id') || 0)) + '&keyword=' + encodeURIComponent(keyword || '');
					if (typeof window.aviAppendTestDriveParamsToWpAvisionUrl === 'function') {
						url = window.aviAppendTestDriveParamsToWpAvisionUrl(url);
					}
					return url;
				}
				// #Sprint43-scheduler — REST endpoint schedule (corrigé)
				function scheduleUrl() {
					var base = (config.rest_url || '').replace(/heatmap-scan.*$/, '');
					return base + 'heatmap-schedule';
				}
				// #Sprint43-scheduler — multiplicateurs mensuels par fréquence
				var SCHEDULE_MONTHLY_MULTIPLIERS = {
					daily:      30,
					every3days: 10,
					weekly:     4,
					every2weeks: 2,
					monthly:    1
				};
				var SCHEDULE_FREQ_LABELS = {
					daily:      { en: 'Daily', fr: 'Quotidien', es: 'Diario', de: 'Täglich', 'pt-br': 'Diário', tr: 'Günlük' },
					every3days: { en: 'Every 3 days', fr: 'Tous les 3 jours', es: 'Cada 3 días', de: 'Alle 3 Tage', 'pt-br': 'A cada 3 dias', tr: 'Her 3 günde' },
					weekly:     { en: 'Weekly', fr: 'Hebdomadaire', es: 'Semanal', de: 'Wöchentlich', 'pt-br': 'Semanal', tr: 'Haftalık' },
					every2weeks:{ en: 'Every 2 weeks', fr: 'Toutes les 2 semaines', es: 'Cada 2 semanas', de: 'Alle 2 Wochen', 'pt-br': 'A cada 2 semanas', tr: 'Her 2 haftada' },
					monthly:    { en: 'Monthly', fr: 'Mensuel', es: 'Mensual', de: 'Monatlich', 'pt-br': 'Mensal', tr: 'Aylık' }
				};
				// #Sprint43-scheduler — coût via selectedGridSize() + selectedMapKeywords (même logique refreshCreditState l.695–697)
				function updateScheduleNote() {
					// #Sprint43-scheduler fix — ne pas écraser le bloc schedule actif
					if (
						scheduleNoteEl &&
						(scheduleNoteEl.querySelector('.avi-hm-schedule-active-block') ||
						 scheduleNoteEl.querySelector('.avi-hm-schedule-badge-row'))
					) {
						return; // bloc actif affiché → ne pas écraser
					}
					if (!scheduleNoteEl) return;
					var freq = frequencySelect ? frequencySelect.value : 'once';
					if (freq === 'once') {
						scheduleNoteEl.textContent = hm_t('schedule_note_once');
						return;
					}
					var mult  = SCHEDULE_MONTHLY_MULTIPLIERS[freq] || 1;
					var lang  = getHmLang();
					var label = (SCHEDULE_FREQ_LABELS[freq] || {})[lang] || freq;
					var size  = selectedGridSize();
					var kwCount = selectedMapKeywords.length || 1;
					var costPerScan = size * size * kwCount;
					var note  = hm_t('schedule_note_freq')
						.replace('{cost}', costPerScan)
						.replace('{scans}', mult);
					scheduleNoteEl.textContent = label + ' — ' + note;
				}
				// #Sprint43-scheduler — helper dates cycle programmé
				function formatScheduleDates(schedule) {
					var lang        = getHmLang();
					var frequency   = schedule.frequency;
					var createdAt   = new Date(schedule.created_at.replace(' ', 'T'));
					var nextRunAt   = new Date(schedule.next_run_at.replace(' ', 'T'));
					var mult        = SCHEDULE_MONTHLY_MULTIPLIERS[frequency] || 1;
					var freqLabel   = (SCHEDULE_FREQ_LABELS[frequency] || {})[lang] || frequency;

					var intervalMs = {
						daily:       1 * 24 * 3600 * 1000,
						every3days:  3 * 24 * 3600 * 1000,
						weekly:      7 * 24 * 3600 * 1000,
						every2weeks: 14 * 24 * 3600 * 1000,
						monthly:     30 * 24 * 3600 * 1000
					};
					var interval   = intervalMs[frequency] || intervalMs.weekly;
					var upcoming   = [];
					var d          = new Date(nextRunAt);
					var maxDates   = Math.min(mult, 4);
					for (var i = 0; i < maxDates; i++) {
						upcoming.push(d.toLocaleDateString(
							lang === 'fr' ? 'fr-FR' : lang === 'es' ? 'es-ES' : lang === 'de' ? 'de-DE' : lang === 'pt-br' ? 'pt-BR' : lang === 'tr' ? 'tr-TR' : 'en-GB',
							{ day: '2-digit', month: 'short', year: 'numeric' }
						));
						d = new Date(d.getTime() + interval);
					}

					var dateStart = createdAt.toLocaleDateString(
						lang === 'fr' ? 'fr-FR' : lang === 'es' ? 'es-ES' : lang === 'de' ? 'de-DE' : lang === 'pt-br' ? 'pt-BR' : lang === 'tr' ? 'tr-TR' : 'en-GB',
						{ day: '2-digit', month: 'short', year: 'numeric' }
					);

					return {
						freqLabel:  freqLabel,
						dateStart:  dateStart,
						nextRun:    upcoming[0] || '—',
						upcoming:   upcoming,
						mult:       mult
					};
				}
				// #Sprint43-scheduler — builder HTML panneau overlay schedules
				function buildSchedulePanelHTML(schedules) {
					var lang = getHmLang();
					var labels = {
						title:    { en: 'Scheduled Scans', fr: 'Scans programmés', es: 'Escaneos programados', de: 'Geplante Scans', 'pt-br': 'Scans programados', tr: 'Programlı Taramalar' },
						grid:     { en: 'Grid', fr: 'Grille', es: 'Cuadrícula', de: 'Raster', 'pt-br': 'Grade', tr: 'Izgara' },
						keywords: { en: 'Keywords', fr: 'Mots-clés', es: 'Palabras clave', de: 'Keywords', 'pt-br': 'Palavras-chave', tr: 'Anahtar kelimeler' },
						freq:     { en: 'Frequency', fr: 'Fréquence', es: 'Frecuencia', de: 'Häufigkeit', 'pt-br': 'Frequência', tr: 'Sıklık' },
						next:     { en: 'Next scan', fr: 'Prochain scan', es: 'Próximo escaneo', de: 'Nächster Scan', 'pt-br': 'Próximo scan', tr: 'Sonraki tarama' },
						cancel:   { en: 'Cancel', fr: 'Annuler', es: 'Cancelar', de: 'Abbrechen', 'pt-br': 'Cancelar', tr: 'İptal' },
						empty:    { en: 'No scheduled scans', fr: 'Aucun scan programmé', es: 'Sin escaneos programados', de: 'Keine geplanten Scans', 'pt-br': 'Nenhum scan programado', tr: 'Programlı tarama yok' },
						strategy: {
							en: 'Each grid = one tracking strategy',
							fr: 'Chaque grille = une stratégie de suivi',
							es: 'Cada cuadrícula = una estrategia de seguimiento',
							de: 'Jedes Raster = eine Tracking-Strategie',
							'pt-br': 'Cada grade = uma estratégia de rastreamento',
							tr: 'Her ızgara = bir takip stratejisi'
						}
					};
					var t = function(k) { return (labels[k] || {})[lang] || (labels[k] || {}).en || k; };

					var rows = '';
					if (!schedules || schedules.length === 0) {
						rows = '<tr><td colspan="5" class="avi-hm-sp-empty">' + t('empty') + '</td></tr>';
					} else {
						schedules.forEach(function(s) {
							var dates   = formatScheduleDates(s);
							var kwList  = Array.isArray(s.keywords)
							              ? s.keywords.join(', ')
							              : (s.keywords || '—');
							var gridLbl = s.grid_size + '×' + s.grid_size;
							rows += '<tr class="avi-hm-sp-row" data-schedule-id="' + s.id + '">'
							      + '<td><span class="avi-hm-sp-grid-badge">' + gridLbl + '</span></td>'
							      + '<td class="avi-hm-sp-kw">' + kwList + '</td>'
							      + '<td><span class="avi-hm-sp-freq-badge">' + dates.freqLabel + '</span></td>'
							      + '<td class="avi-hm-sp-next">' + dates.nextRun + '</td>'
							      + '<td><button type="button" class="avi-hm-sp-cancel-btn" data-schedule-id="' + s.id + '" aria-label="' + t('cancel') + '" title="' + t('cancel') + '">\u2715</button></td>'
							      + '</tr>';
						});
					}

					return '<div class="avi-hm-schedule-panel" id="aviHmSchedulePanel" hidden>'
					     + '<div class="avi-hm-sp-backdrop" data-avi-sp-close></div>'
					     + '<div class="avi-hm-sp-drawer">'
					     +   '<div class="avi-hm-sp-header">'
					     +     '<span class="avi-hm-sp-title">📅 ' + t('title') + '</span>'
					     +     '<button type="button" class="avi-hm-sp-close" data-avi-sp-close>✕</button>'
					     +   '</div>'
					     +   '<p class="avi-hm-sp-strategy">'
					     +     '<span class="avi-hm-sp-strategy-icon">💡</span> ' + t('strategy')
					     +   '</p>'
					     +   '<div class="avi-hm-sp-table-wrap">'
					     +     '<table class="avi-hm-sp-table">'
					     +       '<thead><tr>'
					     +         '<th>' + t('grid') + '</th>'
					     +         '<th>' + t('keywords') + '</th>'
					     +         '<th>' + t('freq') + '</th>'
					     +         '<th>' + t('next') + '</th>'
					     +         '<th></th>'
					     +       '</tr></thead>'
					     +       '<tbody>' + rows + '</tbody>'
					     +     '</table>'
					     +   '</div>'
					     + '</div>'
					     + '</div>';
				}
				// #Sprint43-scheduler — ouvrir/fermer panneau overlay schedules
				function openSchedulePanel() {
					// Supprimer ancien panneau si existant
					var old = document.getElementById('aviHmSchedulePanel');
					if (old) old.remove();

					// Injecter dans le root heatmap
					var panelWrap = document.createElement('div');
					panelWrap.innerHTML = buildSchedulePanelHTML(window._aviActiveSchedules || []);
					var panel = panelWrap.firstElementChild;
					document.body.appendChild(panel);
					// Forcer reflow puis afficher
					panel.removeAttribute('hidden');
					setTimeout(function() { panel.classList.add('is-open'); }, 10);

					// Binder fermeture (backdrop + bouton ✕)
					panel.querySelectorAll('[data-avi-sp-close]').forEach(function(el) {
						el.addEventListener('click', closeSchedulePanel);
					});

					// Binder boutons Cancel par schedule
					panel.querySelectorAll('.avi-hm-sp-cancel-btn').forEach(function(btn) {
						btn.addEventListener('click', function() {
							var sid = Number(this.getAttribute('data-schedule-id'));
							cancelHeatmapSchedule(sid, function() {
								// Rafraîchir le panneau après annulation
								loadHeatmapSchedules(function() { openSchedulePanel(); });
							});
						});
					});
				}
				function closeSchedulePanel() {
					var panel = document.getElementById('aviHmSchedulePanel');
					if (!panel) return;
					panel.classList.remove('is-open');
					setTimeout(function() { panel.remove(); }, 250);
				}
				// #Sprint43-scheduler — rendu schedule actif dans le drawer
				function renderActiveSchedule(schedules) {
					// Accepte un tableau (tous les schedules actifs)
					var list = Array.isArray(schedules) ? schedules : [schedules];
					if (!scheduleNoteEl) return;
					var lang  = getHmLang();
					var count = list.length;
					var labels = {
						one:    { en: 'active schedule', fr: 'scan programmé actif', es: 'escaneo programado activo', de: 'aktiver Plan', 'pt-br': 'scan programado ativo', tr: 'aktif program' },
						many:   { en: 'active schedules', fr: 'scans programmés actifs', es: 'escaneos programados activos', de: 'aktive Pläne', 'pt-br': 'scans programados ativos', tr: 'aktif programlar' },
						manage: { en: 'Manage', fr: 'Gérer', es: 'Gestionar', de: 'Verwalten', 'pt-br': 'Gerenciar', tr: 'Yönet' }
					};
					var t = function(k) { return (labels[k] || {})[lang] || (labels[k] || {}).en || k; };
					var label = count > 1 ? t('many') : t('one');

					scheduleNoteEl.innerHTML =
						'<div class="avi-hm-schedule-badge-row">'
					  + '<span class="avi-hm-schedule-active-dot"></span>'
					  + '<span class="avi-hm-schedule-badge-count">'
					  +   count + ' ' + label
					  + '</span>'
					  + '<button type="button" class="avi-hm-schedule-manage-btn" '
					  +         'data-avi-schedule-manage>'
					  +   t('manage') + ' →'
					  + '</button>'
					  + '</div>';

					// Binder bouton Manage
					var manageBtn = scheduleNoteEl.querySelector('[data-avi-schedule-manage]');
					if (manageBtn) {
						manageBtn.addEventListener('click', openSchedulePanel);
					}
				}
				// #Sprint43-scheduler — annulation schedule
				function cancelHeatmapSchedule(scheduleId, onSuccess) {
					if (!scheduleId) return;
					fetch(scheduleUrl() + '/' + scheduleId, {
						method: 'DELETE',
						credentials: 'same-origin',
						headers: restHeaders()
					})
					.then(function(r) { return r.ok ? r.json() : Promise.reject(r.status); })
					.then(function() {
						// Retirer du cache local
						window._aviActiveSchedules = (window._aviActiveSchedules || [])
							.filter(function(s) { return String(s.id) !== String(scheduleId); });
						// Si plus aucun schedule → reset UI
						if ((window._aviActiveSchedules || []).length === 0) {
							if (scheduleNoteEl) scheduleNoteEl.innerHTML = '';
							if (frequencySelect) frequencySelect.value = 'once';
							updateScheduleNote();
							closeSchedulePanel();
						}
						if (typeof onSuccess === 'function') onSuccess();
					})
					.catch(function(err) {
						console.warn('[Avision] cancelHeatmapSchedule error', err);
					});
				}
				// #Sprint43-scheduler
				function loadHeatmapSchedules(onLoaded) {
					var profileId = Number(config.profile_id || root.getAttribute('data-profile-id') || 0);
					if (!profileId) return;
					fetch(scheduleUrl() + '?profile_id=' + profileId, {
						method: 'GET',
						credentials: 'same-origin',
						headers: restHeaders()
					})
					.then(function(r){ return r.ok ? r.json() : null; })
					.then(function(data) {
						var list   = Array.isArray(data) ? data : (data && data.schedules ? data.schedules : []);
						var active = list.filter(function(s) {
							return s.status === 'active';
						});
						window._aviActiveSchedules = active;
						if (active.length > 0) {
							renderActiveSchedule(active);
						} else {
							updateScheduleNote();
						}
						if (typeof onLoaded === 'function') onLoaded(active);
					})
					.catch(function() {
						window._aviActiveSchedules = [];
						updateScheduleNote();
					});
				}
				// #Sprint43-scheduler
				function saveHeatmapSchedule(freq, onSuccess) {
					var profileId = Number(config.profile_id || root.getAttribute('data-profile-id') || 0);
					var keywords = selectedMapKeywords.length
						? selectedMapKeywords.slice()
						: (selectedKeyword ? [selectedKeyword] : []);
					var gridSize = selectedGridSize();
					var radiusKm = Number(config.default_radius_km || 5);
					var notify   = '';
					var existing = (window._aviActiveSchedules || []).filter(function(s){
						return s.status === 'active';
					});
					var method   = existing.length > 0 ? 'PUT' : 'POST';
					var url      = existing.length > 0
						? scheduleUrl() + '/' + existing[0].id
						: scheduleUrl();
					fetch(url, {
						method: method,
						credentials: 'same-origin',
						headers: restHeaders(),
						body: JSON.stringify({
							profile_id:   profileId,
							keywords:     keywords,
							grid_size:    gridSize,
							radius_km:    radiusKm,
							frequency:    freq,
							notify_email: notify
						})
					})
					.then(function(r){ return r.ok ? r.json() : Promise.reject(r.status); })
					.then(function(data) {
						if (scheduleNoteEl) {
							scheduleNoteEl.textContent = hm_t('schedule_saved');
						}
						// #Sprint43-scheduler fix PUT id
						var savedId = data.id || (existing.length > 0 ? existing[0].id : null);
						window._aviActiveSchedules = [{ id: savedId, status: 'active', frequency: freq }];
						if (typeof onSuccess === 'function') onSuccess(data);
					})
					.catch(function(err) {
						console.warn('[Avision] saveHeatmapSchedule error', err);
					});
				}
				function normalizeZone(point){
					var zone = point && point.zone ? String(point.zone) : '';
					if (zone === 'top3' || zone === 'found' || zone === 'weak' || zone === 'missing') return zone;
					if (zone === 'green') return 'top3';
					if (zone === 'orange') return 'found';
					if (zone === 'red') return 'weak';
					if (zone === 'gray' || zone === 'grey') return 'missing';
					var rank = point && point.rank !== null && point.rank !== undefined ? Number(point.rank) : 0;
					if (rank > 0 && rank <= 3) return 'top3';
					if (rank > 0 && rank <= 10) return 'found';
					if (rank > 10 && rank <= 20) return 'weak';
					return 'missing';
				}
				function setMapOverlay(message, blocking){
					if (!mapOverlay) return;
					mapOverlay.textContent = message || '';
					mapOverlay.hidden = !message;
					mapOverlay.setAttribute('data-blocking', blocking ? '1' : '0');
				}
				function validPoint(point){
					return point && isFinite(Number(point.lat)) && isFinite(Number(point.lng));
				}
				function escapeHtml(value){
					return String(value === null || value === undefined ? '' : value).replace(/[&<>"']/g, function(ch){
						return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' })[ch];
					});
				}
				function calcPointPower(rank) {
					var r = (typeof rank === 'number')
						? rank : null;
					var level, color, emoji;
					if (r !== null && r <= 3) {
						level = hm_t('kw_power_dominant') || 'Dominant';
						color = '#16a34a';
						emoji = '💚';
					} else if (r !== null && r <= 10) {
						level = hm_t('kw_power_strong') || 'Strong';
						color = '#22c55e';
						emoji = '🟢';
					} else if (r !== null && r <= 20) {
						level = hm_t('kw_power_weak') || 'Weak';
						color = '#f97316';
						emoji = '🟠';
					} else {
						level = hm_t('kw_power_critical') || 'Critical';
						color = '#64748b';
						emoji = '⚫';
					}
					return {
						level: level,
						color: color,
						emoji: emoji,
						rank: r
					};
				}
				function heatmapPopupHtml(target){
					var zone = target.type === 'center' || target.type === 'ring'
						? (target.bgColor === '#16a34a' ? 'top3'
							: target.bgColor === '#84cc16' ? 'found'
							: target.bgColor === '#f97316' ? 'weak'
							: 'missing')
						: 'missing';
					var icon = zone === 'top3' ? '🟢'
						: zone === 'found' ? '🟡'
						: zone === 'weak' ? '🟠' : '⚫';
					var isMissing = zone === 'missing';
					var isTop3 = zone === 'top3';
					var kw = String(target.keyword || '').replace(/[<>&]/g, '');
					var rankDisplay = target.rankText || '>20';
					var pointRank = null;
					if (typeof target.rank === 'number') {
						pointRank = target.rank;
					} else if (target.rankText) {
						var rankTextValue = String(target.rankText);
						if (rankTextValue.indexOf('>') >= 0) {
							pointRank = null;
						} else {
							var parsedRank = parseInt(
								rankTextValue.replace(
									/[^0-9]/g, ''
								), 10
							);
							pointRank = isFinite(parsedRank)
								? parsedRank : null;
						}
					}
					var power = calcPointPower(pointRank);
					var powerHtml = '';
					if (power) {
						var powerBarWidth = power.emoji === '💚'
							? 100 : power.emoji === '🟢'
							? 75 : power.emoji === '🟠'
							? 40 : 15;
						powerHtml =
							'<div class="avi-hm-popup__kw-power">' +
							'<span class="avi-hm-popup__kw-power-bar">' +
								'<span class="avi-hm-popup__kw-power-fill" ' +
									'style="width:' + powerBarWidth + '%;' +
									'background:' + power.color + ';">' +
								'</span>' +
							'</span>' +
							'<span class="avi-hm-popup__kw-power-label" ' +
								'style="color:' + power.color + ';">' +
								power.emoji + ' ' +
								(hm_t('kw_power_label') || 'Keyword Power') + ': ' +
								'<strong>' + power.level + '</strong>' +
							'</span>' +
							'<span class="avi-hm-popup__kw-power-sub">' +
								(hm_t('kw_power_point_sub') ||
									'This specific location') +
							'</span>' +
							'</div>';
					}
					return '<div class="avi-hm-popup">' +
						'<div class="avi-hm-popup__rank" style="color:' + target.bgColor + '">' +
						icon + ' ' + rankDisplay + '</div>' +
						'<div class="avi-hm-popup__kw">' + kw + '</div>' +
						(isTop3
							? '<div class="avi-hm-popup__badge avi-hm-popup__badge--top3">✓ Top 3</div>'
							: '') +
						(isMissing
							? '<div class="avi-hm-popup__absent">' +
								hm_t('absent_top20') + '</div>'
							: '') +
						powerHtml +
						'</div>';
				}
				function formatMetric(value, suffix){
					if (value === null || value === undefined || value === '') return '—';
					var number = Number(value);
					if (!isFinite(number)) return String(value);
					var text = String(Math.round(number * 100) / 100);
					return suffix ? text + suffix : text;
				}
				function setHmTextPreserveChildren(el, text){
					if (!el) return;
					var first = el.firstChild;
					if (first && first.nodeType === 3) {
						first.nodeValue = text + ' ';
						return;
					}
					el.insertBefore(document.createTextNode(text + ' '), first || null);
				}
				function hmPedagoStep(index, color, title, text){
					return '<div style="display:flex;gap:10px;align-items:flex-start;padding:10px;background:#f8fafc;border-radius:8px;border-left:3px solid ' + color + '">' +
						'<span style="background:' + color + ';color:#fff;border-radius:50%;width:22px;height:22px;min-width:22px;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:.75rem">' + index + '</span>' +
						'<div style="color:#1e293b"><strong>' + title + '</strong><br><span style="font-size:.82rem;color:#475569">' + text + '</span></div></div>';
				}
				function hmPedagoSteps(a, b, c){
					return '<div style="display:flex;flex-direction:column;gap:8px;margin:12px 0">' +
						hmPedagoStep('1', '#3b82f6', a[0], a[1]) +
						hmPedagoStep('2', '#10b981', b[0], b[1]) +
						hmPedagoStep('3', '#8b5cf6', c[0], c[1]) +
					'</div>';
				}
				var HM_PEDAGO = {
					hm_keywords: {
						icon: '🔍',
						en: { title: 'Radar Keywords', body: '<p style="color:#1e293b">Select up to <strong>5 keywords</strong> to compare local visibility on the same map.</p>' + hmPedagoSteps(['Choose keywords', 'Each keyword receives a colored halo.'], ['Compare zones', 'Green, orange and grey reveal where visibility changes.'], ['Prioritize work', 'Focus first on weak nearby zones.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Coach\'s Tip</strong><br><span style="color:#92400e">A good average rank can hide invisible streets.</span></div>', cta: 'Got it!' },
						fr: { title: 'Mots-clés Radar', body: '<p style="color:#1e293b">Sélectionne jusqu\'à <strong>5 mots-clés</strong> pour comparer la visibilité locale sur la même carte.</p>' + hmPedagoSteps(['Choisis les mots-clés', 'Chaque mot-clé reçoit un halo coloré.'], ['Compare les zones', 'Vert, orange et gris révèlent les variations de visibilité.'], ['Priorise le travail', 'Commence par les zones faibles proches.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Conseil AVI</strong><br><span style="color:#92400e">Un bon rang moyen peut cacher des rues invisibles.</span></div>', cta: 'J\'ai compris !' },
						es: { title: 'Palabras clave Radar', body: '<p style="color:#1e293b">Selecciona hasta <strong>5 palabras clave</strong> para comparar la visibilidad local en el mismo mapa.</p>' + hmPedagoSteps(['Elige palabras clave', 'Cada palabra clave recibe un halo de color.'], ['Compara zonas', 'Verde, naranja y gris revelan cambios de visibilidad.'], ['Prioriza', 'Empieza por zonas débiles cercanas.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Consejo AVI</strong><br><span style="color:#92400e">Un buen promedio puede ocultar calles invisibles.</span></div>', cta: '¡Entendido!' },
						de: { title: 'Radar-Keywords', body: '<p style="color:#1e293b">Wähle bis zu <strong>5 Keywords</strong> aus, um die lokale Sichtbarkeit auf derselben Karte zu vergleichen.</p>' + hmPedagoSteps(['Keywords wählen', 'Jedes Keyword erhält einen farbigen Halo.'], ['Zonen vergleichen', 'Grün, orange und grau zeigen Sichtbarkeitsunterschiede.'], ['Arbeit priorisieren', 'Beginne mit den schwachen nahen Zonen.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 AVI-Tipp</strong><br><span style="color:#92400e">Ein guter Durchschnittsrang kann unsichtbare Straßen verbergen.</span></div>', cta: 'Verstanden!' },
						'pt-br': { title: 'Keywords Radar', body: '<p style="color:#1e293b">Selecione até <strong>5 palavras-chave</strong> para comparar a visibilidade local no mesmo mapa.</p>' + hmPedagoSteps(['Escolha as keywords', 'Cada keyword recebe um halo colorido.'], ['Compare as zonas', 'Verde, laranja e cinza revelam variações de visibilidade.'], ['Priorize o trabalho', 'Comece pelas zonas fracas mais próximas.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Dica AVI</strong><br><span style="color:#92400e">Um bom rank médio pode esconder ruas invisíveis.</span></div>', cta: 'Entendido!' },
						tr: { title: 'Radar Anahtar Kelimeleri', body: '<p style="color:#1e293b">Aynı haritada yerel görünürlüğü karşılaştırmak için <strong>5 anahtar kelimeye</strong> kadar seçin.</p>' + hmPedagoSteps(['Anahtar kelimeleri seç', 'Her anahtar kelime renkli bir hale alır.'], ['Bölgeleri karşılaştır', 'Yeşil, turuncu ve gri görünürlük farklılıklarını gösterir.'], ['İşi önceliklendir', 'Yakın zayıf bölgelerden başla.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 AVI İpucu</strong><br><span style="color:#92400e">İyi bir ortalama sıralama görünmez sokakları gizleyebilir.</span></div>', cta: 'Anlaşıldı!' }
					},
					hm_map_view: {
						icon: '🗺️',
						en: { title: 'Reading the Map', body: '<p style="color:#1e293b">The heatmap shows your rank at each geographic point.</p>' + hmPedagoSteps(['Top 3', 'Strong dominance around that point.'], ['Rank 4-10', 'Visible, but competitors can still overtake you.'], ['Rank 11-20', 'Weak visibility that should become a priority.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Coach\'s Tip</strong><br><span style="color:#92400e">Turn nearby orange and grey zones green first.</span></div>', cta: 'Got it!' },
						fr: { title: 'Lire la carte', body: '<p style="color:#1e293b">La heatmap montre ton rang à chaque point géographique.</p>' + hmPedagoSteps(['Top 3', 'Dominance forte autour de ce point.'], ['Rang 4-10', 'Visible, mais des concurrents peuvent encore te dépasser.'], ['Rang 11-20', 'Visibilité faible à traiter en priorité.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Conseil AVI</strong><br><span style="color:#92400e">Transforme d\'abord les zones proches orange et grises en vert.</span></div>', cta: 'J\'ai compris !' },
						es: { title: 'Leer el mapa', body: '<p style="color:#1e293b">El heatmap muestra tu posición en cada punto geográfico.</p>' + hmPedagoSteps(['Top 3', 'Dominio fuerte alrededor de este punto.'], ['Posición 4-10', 'Visible, pero los competidores aún pueden superarte.'], ['Posición 11-20', 'Visibilidad débil a tratar con prioridad.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Consejo AVI</strong><br><span style="color:#92400e">Transforma primero las zonas naranja y grises cercanas en verde.</span></div>', cta: '¡Entendido!' },
						de: { title: 'Karte lesen', body: '<p style="color:#1e293b">Die Heatmap zeigt dein Ranking an jedem geografischen Punkt.</p>' + hmPedagoSteps(['Top 3', 'Starke Dominanz rund um diesen Punkt.'], ['Rang 4-10', 'Sichtbar, aber Mitbewerber können dich noch überholen.'], ['Rang 11-20', 'Schwache Sichtbarkeit — vorrangig behandeln.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 AVI-Tipp</strong><br><span style="color:#92400e">Verwandle zuerst die nahen orangen und grauen Zonen in Grün.</span></div>', cta: 'Verstanden!' },
						'pt-br': { title: 'Ler o mapa', body: '<p style="color:#1e293b">O heatmap mostra seu ranking em cada ponto geográfico.</p>' + hmPedagoSteps(['Top 3', 'Forte dominância ao redor deste ponto.'], ['Posição 4-10', 'Visível, mas concorrentes ainda podem superá-lo.'], ['Posição 11-20', 'Visibilidade fraca — tratar com prioridade.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Dica AVI</strong><br><span style="color:#92400e">Transforme primeiro as zonas laranja e cinzas próximas em verde.</span></div>', cta: 'Entendido!' },
						tr: { title: 'Haritayı oku', body: '<p style="color:#1e293b">Heatmap, her coğrafi noktadaki sıralamani gösterir.</p>' + hmPedagoSteps(['Top 3', 'Bu nokta etrafında güçlü hakimiyet.'], ['Sıra 4-10', 'Görünür, ancak rakipler hâlâ sizi geçebilir.'], ['Sıra 11-20', 'Zayıf görünürlük — öncelikli olarak ele alın.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 AVI İpucu</strong><br><span style="color:#92400e">Önce yakın turuncu ve gri bölgeleri yeşile dönüştür.</span></div>', cta: 'Anlaşıldı!' }
					},
					hm_scan: {
						icon: '📡',
						en: { title: 'Launching a Scan', body: '<p style="color:#1e293b">Each scan queries real Google results for each grid point.</p>' + hmPedagoSteps(['Pick a grid', 'Choose the size based on the area you want to study.'], ['Spend credits', 'Each grid point consumes credits for the selected keyword.'], ['Read results', 'Use the map after processing to prioritize action.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Coach\'s Tip</strong><br><span style="color:#92400e">Start with 7×7 for a balanced view.</span></div>', cta: 'Got it!' },
						fr: { title: 'Lancer un scan', body: '<p style="color:#1e293b">Chaque scan interroge les vrais résultats Google pour chaque point.</p>' + hmPedagoSteps(['Choisis une grille', 'Adapte la taille à la zone que tu veux analyser.'], ['Consomme des crédits', 'Chaque point de grille consomme des crédits pour le mot-clé choisi.'], ['Lis les résultats', 'Utilise la carte après traitement pour prioriser les actions.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Conseil AVI</strong><br><span style="color:#92400e">Commence par 7×7 pour une vue équilibrée.</span></div>', cta: 'J\'ai compris !' },
						es: { title: 'Lanzar un escaneo', body: '<p style="color:#1e293b">Cada escaneo consulta los resultados reales de Google para cada punto.</p>' + hmPedagoSteps(['Elige una cuadrícula', 'Adapta el tamaño a la zona que quieres analizar.'], ['Consume créditos', 'Cada punto de cuadrícula consume créditos para la palabra clave elegida.'], ['Lee los resultados', 'Usa el mapa tras el procesamiento para priorizar acciones.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Consejo AVI</strong><br><span style="color:#92400e">Empieza con 7×7 para una vista equilibrada.</span></div>', cta: '¡Entendido!' },
						de: { title: 'Scan starten', body: '<p style="color:#1e293b">Jeder Scan fragt die echten Google-Ergebnisse für jeden Punkt ab.</p>' + hmPedagoSteps(['Raster wählen', 'Passe die Größe an die Zone an, die du analysieren möchtest.'], ['Credits verbrauchen', 'Jeder Rasterpunkt verbraucht Credits für das gewählte Keyword.'], ['Ergebnisse lesen', 'Nutze die Karte nach der Verarbeitung, um Prioritäten zu setzen.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 AVI-Tipp</strong><br><span style="color:#92400e">Beginne mit 7×7 für eine ausgewogene Ansicht.</span></div>', cta: 'Verstanden!' },
						'pt-br': { title: 'Iniciar um scan', body: '<p style="color:#1e293b">Cada scan consulta os resultados reais do Google para cada ponto.</p>' + hmPedagoSteps(['Escolha uma grade', 'Adapte o tamanho à zona que deseja analisar.'], ['Consuma créditos', 'Cada ponto de grade consome créditos para a keyword escolhida.'], ['Leia os resultados', 'Use o mapa após o processamento para priorizar ações.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Dica AVI</strong><br><span style="color:#92400e">Comece com 7×7 para uma visão equilibrada.</span></div>', cta: 'Entendido!' },
						tr: { title: 'Tarama başlat', body: '<p style="color:#1e293b">Her tarama, her nokta için gerçek Google sonuçlarını sorgular.</p>' + hmPedagoSteps(['Izgara seç', 'Boyutu analiz etmek istediğin bölgeye göre ayarla.'], ['Kredi harca', 'Her ızgara noktası, seçilen keyword için kredi tüketir.'], ['Sonuçları oku', 'Öncelikleri belirlemek için işlem sonrası haritayı kullan.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 AVI İpucu</strong><br><span style="color:#92400e">Dengeli bir görünüm için 7×7 ile başla.</span></div>', cta: 'Anlaşıldı!' }
					},
					hm_territory: {
						icon: '🏆',
						en: { title: 'Top 3 Territory', body: '<p style="color:#1e293b">This percentage shows the share of scanned zones where you are in the top 3.</p>' + hmPedagoSteps(['Above 60%', 'Strong dominance on most scanned points.'], ['20% to 60%', 'Partial visibility with clear room for growth.'], ['Below 20%', 'Critical visibility that needs immediate attention.']) + '<div style="margin:10px 0 4px;padding:10px 12px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px"><div style="font-size:.72rem;font-weight:800;color:#0369a1;text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">📐 Health indicator by grid size</div><div style="display:flex;flex-direction:column;gap:4px;font-size:.8rem;color:#1e293b"><div><strong>3×3 (your street)</strong> → <span style="color:#16a34a;font-weight:700">expect 80–100%</span> if your listing is optimised</div><div><strong>7×7 (neighbourhood)</strong> → <span style="color:#f59e0b;font-weight:700">expect 40–70%</span></div><div><strong>13×13+ (city)</strong> → <span style="color:#ef4444;font-weight:700">expect 20–50%</span></div><div style="font-size:.7rem;color:#64748b;margin-top:6px;font-style:italic">* Score is calculated in real time from actual Google results for your selected keywords.</div></div></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Coach\'s Tip</strong><br><span style="color:#92400e">Read territory together with average rank.</span></div>', cta: 'Got it!' },
						fr: { title: 'Territoire Top 3', body: '<p style="color:#1e293b">Ce pourcentage montre la part des zones scannées où tu es dans le top 3.</p>' + hmPedagoSteps(['Au-dessus de 60%', 'Dominance forte sur la majorité des points scannés.'], ['20% à 60%', 'Visibilité partielle avec une marge de progression nette.'], ['En dessous de 20%', 'Visibilité critique à traiter rapidement.']) + '<div style="margin:10px 0 4px;padding:10px 12px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px"><div style="font-size:.72rem;font-weight:800;color:#0369a1;text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">📐 Health indicator — Indicateur de santé par taille de grille</div><div style="display:flex;flex-direction:column;gap:4px;font-size:.8rem;color:#1e293b"><div><strong>3×3 (tes rues)</strong> → <span style="color:#16a34a;font-weight:700">attendu 80–100%</span> si ta fiche est optimisée</div><div><strong>7×7 (quartier)</strong> → <span style="color:#f59e0b;font-weight:700">attendu 40–70%</span></div><div><strong>13×13+ (ville)</strong> → <span style="color:#ef4444;font-weight:700">attendu 20–50%</span></div><div style="font-size:.7rem;color:#64748b;margin-top:6px;font-style:italic">* Le score est calculé en temps réel depuis les vrais résultats Google pour les mots-clés sélectionnés.</div></div></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Conseil AVI</strong><br><span style="color:#92400e">Lis le territoire avec le rang moyen.</span></div>', cta: 'J\'ai compris !' },
						es: { title: 'Territorio Top 3', body: '<p style="color:#1e293b">Este porcentaje muestra la parte de las zonas escaneadas donde estás en el top 3.</p>' + hmPedagoSteps(['Por encima del 60%', 'Dominio fuerte en la mayoría de los puntos escaneados.'], ['20% a 60%', 'Visibilidad parcial con margen de progresión claro.'], ['Por debajo del 20%', 'Visibilidad crítica a tratar rápidamente.']) + '<div style="margin:10px 0 4px;padding:10px 12px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px"><div style="font-size:.72rem;font-weight:800;color:#0369a1;text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">📐 Health indicator — Indicador de salud por tamaño de cuadrícula</div><div style="display:flex;flex-direction:column;gap:4px;font-size:.8rem;color:#1e293b"><div><strong>3×3 (tus calles)</strong> → <span style="color:#16a34a;font-weight:700">esperado 80–100%</span> si tu ficha está optimizada</div><div><strong>7×7 (barrio)</strong> → <span style="color:#f59e0b;font-weight:700">esperado 40–70%</span></div><div><strong>13×13+ (ciudad)</strong> → <span style="color:#ef4444;font-weight:700">esperado 20–50%</span></div><div style="font-size:.7rem;color:#64748b;margin-top:6px;font-style:italic">* La puntuación se calcula en tiempo real desde los resultados reales de Google para las palabras clave seleccionadas.</div></div></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Consejo AVI</strong><br><span style="color:#92400e">Lee el territorio junto con el rango promedio.</span></div>', cta: '¡Entendido!' },
						de: { title: 'Top-3-Gebiet', body: '<p style="color:#1e293b">Dieser Prozentsatz zeigt den Anteil der gescannten Zonen, in denen du in den Top 3 bist.</p>' + hmPedagoSteps(['Über 60%', 'Starke Dominanz auf den meisten gescannten Punkten.'], ['20% bis 60%', 'Teilweise Sichtbarkeit mit klarem Verbesserungspotenzial.'], ['Unter 20%', 'Kritische Sichtbarkeit — sofort handeln.']) + '<div style="margin:10px 0 4px;padding:10px 12px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px"><div style="font-size:.72rem;font-weight:800;color:#0369a1;text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">📐 Health indicator — Gesundheitsindikator nach Rastergröße</div><div style="display:flex;flex-direction:column;gap:4px;font-size:.8rem;color:#1e293b"><div><strong>3×3 (deine Straßen)</strong> → <span style="color:#16a34a;font-weight:700">erwartet 80–100%</span> bei optimiertem Profil</div><div><strong>7×7 (Viertel)</strong> → <span style="color:#f59e0b;font-weight:700">erwartet 40–70%</span></div><div><strong>13×13+ (Stadt)</strong> → <span style="color:#ef4444;font-weight:700">erwartet 20–50%</span></div><div style="font-size:.7rem;color:#64748b;margin-top:6px;font-style:italic">* Der Score wird in Echtzeit aus echten Google-Ergebnissen für die gewählten Keywords berechnet.</div></div></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 AVI-Tipp</strong><br><span style="color:#92400e">Lies das Gebiet immer zusammen mit dem Durchschnittsrang.</span></div>', cta: 'Verstanden!' },
						'pt-br': { title: 'Território Top 3', body: '<p style="color:#1e293b">Este percentual mostra a parte das zonas escaneadas onde você está no top 3.</p>' + hmPedagoSteps(['Acima de 60%', 'Forte dominância na maioria dos pontos escaneados.'], ['20% a 60%', 'Visibilidade parcial com margem de progressão clara.'], ['Abaixo de 20%', 'Visibilidade crítica — agir rapidamente.']) + '<div style="margin:10px 0 4px;padding:10px 12px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px"><div style="font-size:.72rem;font-weight:800;color:#0369a1;text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">📐 Health indicator — Indicador de saúde por tamanho de grade</div><div style="display:flex;flex-direction:column;gap:4px;font-size:.8rem;color:#1e293b"><div><strong>3×3 (suas ruas)</strong> → <span style="color:#16a34a;font-weight:700">esperado 80–100%</span> se o perfil estiver otimizado</div><div><strong>7×7 (bairro)</strong> → <span style="color:#f59e0b;font-weight:700">esperado 40–70%</span></div><div><strong>13×13+ (cidade)</strong> → <span style="color:#ef4444;font-weight:700">esperado 20–50%</span></div><div style="font-size:.7rem;color:#64748b;margin-top:6px;font-style:italic">* O score é calculado em tempo real a partir dos resultados reais do Google para as keywords selecionadas.</div></div></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Dica AVI</strong><br><span style="color:#92400e">Leia o território junto com o rank médio.</span></div>', cta: 'Entendido!' },
						tr: { title: 'Top 3 Bölgesi', body: '<p style="color:#1e293b">Bu yüzde, taranan bölgelerin ilk 3\'te olduğunuz kısmını gösterir.</p>' + hmPedagoSteps(['%60 üzerinde', 'Taranan noktaların çoğunda güçlü hakimiyet.'], ['%20 ile %60 arası', 'Kısmi görünürlük — net ilerleme potansiyeli var.'], ['%20 altında', 'Kritik görünürlük — hemen harekete geçin.']) + '<div style="margin:10px 0 4px;padding:10px 12px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px"><div style="font-size:.72rem;font-weight:800;color:#0369a1;text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">📐 Health indicator — Izgara boyutuna göre sağlık göstergesi</div><div style="display:flex;flex-direction:column;gap:4px;font-size:.8rem;color:#1e293b"><div><strong>3×3 (sokakların)</strong> → <span style="color:#16a34a;font-weight:700">beklenen 80–100%</span> profil optimize edilmişse</div><div><strong>7×7 (mahalle)</strong> → <span style="color:#f59e0b;font-weight:700">beklenen 40–70%</span></div><div><strong>13×13+ (şehir)</strong> → <span style="color:#ef4444;font-weight:700">beklenen 20–50%</span></div><div style="font-size:.7rem;color:#64748b;margin-top:6px;font-style:italic">* Skor, seçilen keywords için gerçek Google sonuçlarından gerçek zamanlı hesaplanır.</div></div></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 AVI İpucu</strong><br><span style="color:#92400e">Bölgeyi her zaman ortalama sıralamayla birlikte oku.</span></div>', cta: 'Anlaşıldı!' }
					},
					hm_avg_rank: {
						icon: '📊',
						en: { title: 'Average Rank', body: '<p style="color:#1e293b">Average rank summarizes where you appear across scanned zones.</p>' + hmPedagoSteps(['Rank 1-3', 'Excellent visibility on the scanned area.'], ['Rank 4-10', 'Visible but still improvable.'], ['Rank 11-20', 'Weak visibility that should be strengthened.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Coach\'s Tip</strong><br><span style="color:#92400e">A great rank with poor territory is still a risk.</span></div>', cta: 'Got it!' },
						fr: { title: 'Rang moyen', body: '<p style="color:#1e293b">Le rang moyen résume où tu apparais sur les zones scannées.</p>' + hmPedagoSteps(['Rang 1-3', 'Excellente visibilité sur la zone scannée.'], ['Rang 4-10', 'Visible mais encore améliorable.'], ['Rang 11-20', 'Visibilité faible à renforcer.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Conseil AVI</strong><br><span style="color:#92400e">Un excellent rang avec peu de territoire reste risqué.</span></div>', cta: 'J\'ai compris !' },
						es: { title: 'Rango promedio', body: '<p style="color:#1e293b">El rango promedio resume dónde apareces en las zonas escaneadas.</p>' + hmPedagoSteps(['Rango 1-3', 'Excelente visibilidad en la zona escaneada.'], ['Rango 4-10', 'Visible pero aún mejorable.'], ['Rango 11-20', 'Visibilidad débil a reforzar.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Consejo AVI</strong><br><span style="color:#92400e">Un excelente rango con poco territorio sigue siendo arriesgado.</span></div>', cta: '¡Entendido!' },
						de: { title: 'Durchschnittlicher Rang', body: '<p style="color:#1e293b">Der Durchschnittsrang fasst zusammen, wo du in den gescannten Zonen erscheinst.</p>' + hmPedagoSteps(['Rang 1-3', 'Ausgezeichnete Sichtbarkeit in der gescannten Zone.'], ['Rang 4-10', 'Sichtbar, aber noch verbesserungsfähig.'], ['Rang 11-20', 'Schwache Sichtbarkeit — stärken.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 AVI-Tipp</strong><br><span style="color:#92400e">Ein ausgezeichneter Rang mit wenig Gebiet bleibt riskant.</span></div>', cta: 'Verstanden!' },
						'pt-br': { title: 'Rank médio', body: '<p style="color:#1e293b">O rank médio resume onde você aparece nas zonas escaneadas.</p>' + hmPedagoSteps(['Rank 1-3', 'Excelente visibilidade na zona escaneada.'], ['Rank 4-10', 'Visível mas ainda melhorável.'], ['Rank 11-20', 'Visibilidade fraca — reforçar.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Dica AVI</strong><br><span style="color:#92400e">Um rank excelente com pouco território continua sendo arriscado.</span></div>', cta: 'Entendido!' },
						tr: { title: 'Ortalama sıralama', body: '<p style="color:#1e293b">Ortalama sıralama, taranan bölgelerde nerede göründüğünüzü özetler.</p>' + hmPedagoSteps(['Sıra 1-3', 'Taranan bölgede mükemmel görünürlük.'], ['Sıra 4-10', 'Görünür ama hâlâ geliştirilebilir.'], ['Sıra 11-20', 'Zayıf görünürlük — güçlendirin.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 AVI İpucu</strong><br><span style="color:#92400e">Az bölgeyle mükemmel bir sıralama hâlâ risklidir.</span></div>', cta: 'Anlaşıldı!' }
					},
					hm_keywords_modal: {
						icon: '🔍',
						en: { title: 'Keyword Selection', body: '<p style="color:#1e293b">Select <strong>1 to 5 keywords</strong> to display simultaneously on the map. Each keyword gets its own color halo on each grid point.</p>' + hmPedagoSteps(['Click a keyword', 'It activates and appears on the map with its color (green, orange, red).'], ['Add up to 5', 'Each additional keyword adds its own halo layer on the same map.'], ['Compare visually', 'Zones where all keywords score well = your strongest territory.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Tip</strong><br><span style="color:#92400e">Start with your main keyword, then add variations to spot hidden gaps.</span></div>', cta: 'Got it!' },
						fr: { title: 'Sélection des mots-clés', body: '<p style="color:#1e293b">Sélectionnez <strong>1 à 5 mots-clés</strong> à afficher simultanément sur la carte. Chaque mot-clé obtient son propre halo coloré sur chaque point.</p>' + hmPedagoSteps(['Cliquer un mot-clé', 'Il s\'active et s\'affiche avec sa couleur (vert, orange, rouge).'], ['Ajouter jusqu\'à 5', 'Chaque mot-clé supplémentaire ajoute sa propre couche de halos.'], ['Comparer visuellement', 'Les zones où tous les mots-clés sont verts = votre territoire fort.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Astuce</strong><br><span style="color:#92400e">Commencez par votre mot-clé principal, puis ajoutez des variantes pour repérer les angles morts.</span></div>', cta: 'Compris !' },
						es: { title: 'Selección de palabras clave', body: '<p style="color:#1e293b">Seleccione <strong>1 a 5 palabras clave</strong> para mostrar simultáneamente en el mapa. Cada palabra clave obtiene su propio halo de color.</p>' + hmPedagoSteps(['Hacer clic en una palabra', 'Se activa y aparece en el mapa con su color.'], ['Añadir hasta 5', 'Cada palabra clave añade su propia capa de halos.'], ['Comparar visualmente', 'Las zonas donde todas las palabras son verdes = su territorio fuerte.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Consejo</strong><br><span style="color:#92400e">Empiece con su palabra clave principal y añada variantes para detectar puntos ciegos.</span></div>', cta: '¡Entendido!' },
						de: { title: 'Keyword-Auswahl', body: '<p style="color:#1e293b">Wähle <strong>1 bis 5 Keywords</strong> aus, die gleichzeitig auf der Karte angezeigt werden. Jedes Keyword erhält seinen eigenen farbigen Halo an jedem Punkt.</p>' + hmPedagoSteps(['Ein Keyword anklicken', 'Es wird aktiviert und mit seiner Farbe angezeigt (grün, orange, rot).'], ['Bis zu 5 hinzufügen', 'Jedes zusätzliche Keyword fügt seine eigene Halo-Schicht hinzu.'], ['Visuell vergleichen', 'Zonen, in denen alle Keywords grün sind = dein starkes Gebiet.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Tipp</strong><br><span style="color:#92400e">Beginne mit deinem Haupt-Keyword, füge dann Varianten hinzu, um blinde Flecken zu entdecken.</span></div>', cta: 'Verstanden!' },
						'pt-br': { title: 'Seleção de palavras-chave', body: '<p style="color:#1e293b">Selecione <strong>1 a 5 palavras-chave</strong> para exibir simultaneamente no mapa. Cada keyword recebe seu próprio halo colorido em cada ponto.</p>' + hmPedagoSteps(['Clicar numa keyword', 'Ela é ativada e exibida com sua cor (verde, laranja, vermelho).'], ['Adicionar até 5', 'Cada keyword adicional adiciona sua própria camada de halos.'], ['Comparar visualmente', 'Zonas onde todas as keywords são verdes = seu território forte.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 Dica</strong><br><span style="color:#92400e">Comece pela sua keyword principal, depois adicione variantes para identificar pontos cegos.</span></div>', cta: 'Entendido!' },
						tr: { title: 'Anahtar kelime seçimi', body: '<p style="color:#1e293b">Haritada aynı anda görüntülemek için <strong>1 ila 5 anahtar kelime</strong> seçin. Her anahtar kelime her noktada kendi renkli halosunu alır.</p>' + hmPedagoSteps(['Bir anahtar kelimeye tıkla', 'Etkinleşir ve rengiyle görüntülenir (yeşil, turuncu, kırmızı).'], ['5\'e kadar ekle', 'Her ek anahtar kelime kendi halo katmanını ekler.'], ['Görsel olarak karşılaştır', 'Tüm anahtar kelimelerin yeşil olduğu bölgeler = güçlü bölgen.']) + '<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px"><strong>💡 İpucu</strong><br><span style="color:#92400e">Ana keywordınla başla, ardından kör noktaları tespit etmek için varyantlar ekle.</span></div>', cta: 'Anlaşıldı!' }
					},
					hm_credits: {
						icon: '💳',
						en: { title: 'Credits & Grid Size', body: '<p style="color:#1e293b">Each scan consumes credits based on grid size × number of keywords selected.</p>' + hmPedagoSteps(['3×3 = 9 pts', '9 credits per keyword. Ideal for your immediate street.'], ['7×7 = 49 pts', '49 credits per keyword. Best balance for a neighbourhood.'], ['13×13 = 169 pts', '169 credits per keyword. Full city-level analysis.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Multi-keyword cost</strong><br><span style="color:#0369a1;font-size:.82rem">5 keywords on a 7×7 grid = 5 × 49 = <strong>245 credits</strong>.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Tip</strong><br><span style="color:#92400e">Buy credits in the Credits section to always have scans ready.</span></div>', cta: 'Got it!' },
						fr: { title: 'Crédits & Taille de grille', body: '<p style="color:#1e293b">Chaque scan consomme des crédits selon la taille de grille × le nombre de mots-clés sélectionnés.</p>' + hmPedagoSteps(['3×3 = 9 pts', '9 crédits par mot-clé. Idéal pour votre rue immédiate.'], ['7×7 = 49 pts', '49 crédits par mot-clé. Meilleur équilibre pour un quartier.'], ['13×13 = 169 pts', '169 crédits par mot-clé. Analyse complète à l\'échelle de la ville.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Coût multi-mots-clés</strong><br><span style="color:#0369a1;font-size:.82rem">5 mots-clés sur une grille 7×7 = 5 × 49 = <strong>245 crédits</strong>.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Astuce</strong><br><span style="color:#92400e">Achetez des crédits dans la section Crédits pour toujours avoir des scans disponibles.</span></div>', cta: 'Compris !' },
						es: { title: 'Créditos & Tamaño de cuadrícula', body: '<p style="color:#1e293b">Cada escaneo consume créditos según el tamaño de cuadrícula × el número de palabras clave seleccionadas.</p>' + hmPedagoSteps(['3×3 = 9 pts', '9 créditos por palabra clave. Ideal para tu calle inmediata.'], ['7×7 = 49 pts', '49 créditos por palabra clave. Mejor equilibrio para un barrio.'], ['13×13 = 169 pts', '169 créditos por palabra clave. Análisis completo a escala de ciudad.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Coste multi-palabras clave</strong><br><span style="color:#0369a1;font-size:.82rem">5 palabras clave en una cuadrícula 7×7 = 5 × 49 = <strong>245 créditos</strong>.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Consejo</strong><br><span style="color:#92400e">Compra créditos en la sección Créditos para tener siempre escaneos disponibles.</span></div>', cta: '¡Entendido!' },
						de: { title: 'Credits & Rastergröße', body: '<p style="color:#1e293b">Jeder Scan verbraucht Credits entsprechend der Rastergröße × Anzahl der gewählten Keywords.</p>' + hmPedagoSteps(['3×3 = 9 Pkt', '9 Credits pro Keyword. Ideal für deine unmittelbare Straße.'], ['7×7 = 49 Pkt', '49 Credits pro Keyword. Bestes Gleichgewicht für ein Viertel.'], ['13×13 = 169 Pkt', '169 Credits pro Keyword. Vollständige Analyse auf Stadtebene.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Multi-Keyword-Kosten</strong><br><span style="color:#0369a1;font-size:.82rem">5 Keywords auf einem 7×7-Raster = 5 × 49 = <strong>245 Credits</strong>.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Tipp</strong><br><span style="color:#92400e">Kaufe Credits im Bereich Credits, um immer Scans verfügbar zu haben.</span></div>', cta: 'Verstanden!' },
						'pt-br': { title: 'Créditos & Tamanho de grade', body: '<p style="color:#1e293b">Cada scan consome créditos conforme o tamanho da grade × o número de keywords selecionadas.</p>' + hmPedagoSteps(['3×3 = 9 pts', '9 créditos por keyword. Ideal para sua rua imediata.'], ['7×7 = 49 pts', '49 créditos por keyword. Melhor equilíbrio para um bairro.'], ['13×13 = 169 pts', '169 créditos por keyword. Análise completa na escala da cidade.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Custo multi-keywords</strong><br><span style="color:#0369a1;font-size:.82rem">5 keywords em uma grade 7×7 = 5 × 49 = <strong>245 créditos</strong>.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Dica</strong><br><span style="color:#92400e">Compre créditos na seção Créditos para sempre ter scans disponíveis.</span></div>', cta: 'Entendido!' },
						tr: { title: 'Krediler & Izgara boyutu', body: '<p style="color:#1e293b">Her tarama, ızgara boyutu × seçilen keyword sayısına göre kredi tüketir.</p>' + hmPedagoSteps(['3×3 = 9 nok', 'Keyword başına 9 kredi. Yakın sokağın için ideal.'], ['7×7 = 49 nok', 'Keyword başına 49 kredi. Mahalle için en iyi denge.'], ['13×13 = 169 nok', 'Keyword başına 169 kredi. Şehir ölçeğinde tam analiz.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Çoklu keyword maliyeti</strong><br><span style="color:#0369a1;font-size:.82rem">7×7 ızgarada 5 keyword = 5 × 49 = <strong>245 kredi</strong>.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 İpucu</strong><br><span style="color:#92400e">Her zaman tarama yapabilmek için Krediler bölümünden kredi satın al.</span></div>', cta: 'Anlaşıldı!' }
					},
					hm_grid: {
						icon: '📐',
						en: { title: 'Choosing the Grid', body: '<p style="color:#1e293b">The grid defines the geographic area studied. Larger = more data, more credits.</p>' + hmPedagoSteps(['3×3 — Your street', 'Perfect for checking your immediate zone. Fast and cheap.'], ['7×7 — Your neighbourhood', 'The best starting point. Reveals nearby competition.'], ['13×13 — Your city', 'Full competitive landscape. Use monthly for strategic review.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Recommended progression</strong><br><span style="color:#0369a1;font-size:.82rem">Start 3×3 → validate → then 7×7 → then 13×13 for full picture.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Tip</strong><br><span style="color:#92400e">Change grid in the left panel before launching your scan.</span></div>', cta: 'Got it!' },
						fr: { title: 'Choisir la grille', body: '<p style="color:#1e293b">La grille définit la zone géographique étudiée. Plus grande = plus de données, plus de crédits.</p>' + hmPedagoSteps(['3×3 — Votre rue', 'Parfait pour vérifier votre zone immédiate. Rapide et économique.'], ['7×7 — Votre quartier', 'Le meilleur point de départ. Révèle la concurrence proche.'], ['13×13 — Votre ville', 'Vue concurrentielle complète. À utiliser mensuellement.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Progression recommandée</strong><br><span style="color:#0369a1;font-size:.82rem">Commencez 3×3 → validez → puis 7×7 → puis 13×13 pour la vue complète.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Astuce</strong><br><span style="color:#92400e">Changez la grille dans le panneau gauche avant de lancer votre scan.</span></div>', cta: 'Compris !' },
						es: { title: 'Elegir la cuadrícula', body: '<p style="color:#1e293b">La cuadrícula define la zona geográfica estudiada. Más grande = más datos, más créditos.</p>' + hmPedagoSteps(['3×3 — Tu calle', 'Perfecto para verificar tu zona inmediata. Rápido y económico.'], ['7×7 — Tu barrio', 'El mejor punto de partida. Revela la competencia cercana.'], ['13×13 — Tu ciudad', 'Vista competitiva completa. Úsalo mensualmente.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Progresión recomendada</strong><br><span style="color:#0369a1;font-size:.82rem">Empieza 3×3 → valida → luego 7×7 → luego 13×13 para la vista completa.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Consejo</strong><br><span style="color:#92400e">Cambia la cuadrícula en el panel izquierdo antes de lanzar tu escaneo.</span></div>', cta: '¡Entendido!' },
						de: { title: 'Raster wählen', body: '<p style="color:#1e293b">Das Raster definiert die untersuchte geografische Zone. Größer = mehr Daten, mehr Credits.</p>' + hmPedagoSteps(['3×3 — Deine Straße', 'Perfekt zur Überprüfung deiner unmittelbaren Zone. Schnell und günstig.'], ['7×7 — Dein Viertel', 'Der beste Ausgangspunkt. Enthüllt die nahe Konkurrenz.'], ['13×13 — Deine Stadt', 'Vollständige Wettbewerbsansicht. Monatlich nutzen.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Empfohlene Vorgehensweise</strong><br><span style="color:#0369a1;font-size:.82rem">Beginne mit 3×3 → bestätige → dann 7×7 → dann 13×13 für die vollständige Ansicht.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Tipp</strong><br><span style="color:#92400e">Ändere das Raster im linken Panel vor dem Scan-Start.</span></div>', cta: 'Verstanden!' },
						'pt-br': { title: 'Escolher a grade', body: '<p style="color:#1e293b">A grade define a zona geográfica estudada. Maior = mais dados, mais créditos.</p>' + hmPedagoSteps(['3×3 — Sua rua', 'Perfeito para verificar sua zona imediata. Rápido e econômico.'], ['7×7 — Seu bairro', 'O melhor ponto de partida. Revela a concorrência próxima.'], ['13×13 — Sua cidade', 'Visão competitiva completa. Usar mensalmente.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Progressão recomendada</strong><br><span style="color:#0369a1;font-size:.82rem">Comece 3×3 → valide → depois 7×7 → depois 13×13 para a visão completa.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Dica</strong><br><span style="color:#92400e">Altere a grade no painel esquerdo antes de iniciar o scan.</span></div>', cta: 'Entendido!' },
						tr: { title: 'Izgara seç', body: '<p style="color:#1e293b">Izgara, incelenen coğrafi bölgeyi tanımlar. Büyük = daha fazla veri, daha fazla kredi.</p>' + hmPedagoSteps(['3×3 — Sokağın', 'Yakın bölgeni kontrol etmek için mükemmel. Hızlı ve ekonomik.'], ['7×7 — Mahallen', 'En iyi başlangıç noktası. Yakın rekabeti ortaya koyar.'], ['13×13 — Şehrin', 'Tam rekabet görünümü. Aylık kullan.']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📐 Önerilen ilerleme</strong><br><span style="color:#0369a1;font-size:.82rem">3×3 ile başla → doğrula → sonra 7×7 → sonra tam görünüm için 13×13.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 İpucu</strong><br><span style="color:#92400e">Scan başlatmadan önce sol panelde ızgarayı değiştir.</span></div>', cta: 'Anlaşıldı!' }
					},
					hm_competitors_info: {
						icon: '🏴',
						en: { title: 'Understanding Competitors', body: '<p style="color:#1e293b">Competitors shown are <strong>exactly what Google returns</strong> for your keyword — including businesses from other industries. This is real data, not filtered. <strong>Transparency is a strength</strong>: even an off-topic business can steal your local visibility by occupying a top position on the map.</p>' + hmPedagoSteps(['High count', 'Appears on many grid points = dominates the area for this keyword.'], ['Best rank', 'Their best position found across all scanned points.'], ['Toggle buttons', 'Click a competitor button to show/hide their marker on the map.']) + '<div style="background:#faf5ff;border:1px solid #e9d5ff;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>⚠️ Important</strong><br><span style="color:#6d28d9;font-size:.82rem">A competitor with a different activity can still steal your local visibility.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Tip</strong><br><span style="color:#92400e">Study their GBP profile to understand why Google ranks them for your keyword.</span></div><div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>🔍 How many competitors per grid?</strong><br><span style="color:#166534;font-size:.82rem">3×3 → up to 10 · 5×5 → 20 · 7×7 → 35 · 9×9 → 50 · 13×13 → 75 · 15×15 → 150 · 21×21 → 300. Increase your grid size and relaunch a scan to discover more.</span></div>', cta: 'Got it!' },
						fr: { title: 'Comprendre les concurrents', body: '<p style="color:#1e293b">Les concurrents affichés sont <strong>exactement ce que Google retourne</strong> pour votre mot-clé — y compris des entreprises d\'autres secteurs. Ce sont des données réelles, non filtrées. <strong>La transparence est une force</strong> : même un business hors-sujet peut voler votre visibilité locale en occupant une position haute sur la carte.</p>' + hmPedagoSteps(['Compteur élevé', 'Apparaît sur beaucoup de points = domine la zone pour ce mot-clé.'], ['Meilleur rang', 'Leur meilleure position trouvée sur tous les points scannés.'], ['Boutons toggle', 'Cliquez un bouton concurrent pour afficher/masquer son marqueur.']) + '<div style="background:#faf5ff;border:1px solid #e9d5ff;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>⚠️ Important</strong><br><span style="color:#6d28d9;font-size:.82rem">Un concurrent avec une activité différente peut quand même voler votre visibilité locale.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Astuce</strong><br><span style="color:#92400e">Étudiez leur fiche GBP pour comprendre pourquoi Google les classe pour votre mot-clé.</span></div><div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>🔍 Combien de concurrents par grille ?</strong><br><span style="color:#166534;font-size:.82rem">3×3 → jusqu\'à 10 · 5×5 → 20 · 7×7 → 35 · 9×9 → 50 · 13×13 → 75 · 15×15 → 150 · 21×21 → 300. Augmentez la taille de la grille et relancez un scan pour en découvrir davantage.</span></div>', cta: 'Compris !' },
						es: { title: 'Entender a los competidores', body: '<p style="color:#1e293b">Los competidores mostrados son <strong>exactamente lo que Google devuelve</strong> para su palabra clave — incluidas empresas de otros sectores. Son datos reales, sin filtrar. <strong>La transparencia es una fortaleza</strong>: incluso un negocio fuera de tema puede robar su visibilidad local ocupando una posición alta en el mapa.</p>' + hmPedagoSteps(['Contador alto', 'Aparece en muchos puntos = domina el área para esta palabra clave.'], ['Mejor rango', 'Su mejor posición encontrada en todos los puntos escaneados.'], ['Botones toggle', 'Haga clic en un botón para mostrar/ocultar su marcador en el mapa.']) + '<div style="background:#faf5ff;border:1px solid #e9d5ff;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>⚠️ Importante</strong><br><span style="color:#6d28d9;font-size:.82rem">Un competidor con actividad diferente puede robar su visibilidad local.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Consejo</strong><br><span style="color:#92400e">Estudie su perfil GBP para entender por qué Google los clasifica para su palabra clave.</span></div><div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>🔍 ¿Cuántos competidores por cuadrícula?</strong><br><span style="color:#166534;font-size:.82rem">3×3 → hasta 10 · 5×5 → 20 · 7×7 → 35 · 9×9 → 50 · 13×13 → 75 · 15×15 → 150 · 21×21 → 300. Aumente el tamaño de la cuadrícula y vuelva a escanear para descubrir más.</span></div>', cta: '¡Entendido!' },
						de: { title: 'Mitbewerber verstehen', body: '<p style="color:#1e293b">Die angezeigten Mitbewerber sind <strong>genau das, was Google zurückgibt</strong> für dein Keyword — einschließlich Unternehmen aus anderen Branchen. Das sind echte, ungefilterte Daten. <strong>Transparenz ist eine Stärke</strong>: Selbst ein branchenfremdes Unternehmen kann deine lokale Sichtbarkeit stehlen, indem es eine hohe Position auf der Karte einnimmt.</p>' + hmPedagoSteps(['Hoher Zähler', 'Erscheint an vielen Punkten = dominiert die Zone für dieses Keyword.'], ['Bestes Ranking', 'Ihre beste gefundene Position über alle gescannten Punkte.'], ['Toggle-Buttons', 'Klicke einen Mitbewerber-Button, um seinen Marker ein-/auszublenden.']) + '<div style="background:#faf5ff;border:1px solid #e9d5ff;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>⚠️ Wichtig</strong><br><span style="color:#6d28d9;font-size:.82rem">Ein Mitbewerber mit anderer Tätigkeit kann trotzdem deine lokale Sichtbarkeit stehlen.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Tipp</strong><br><span style="color:#92400e">Studiere ihr GBP-Profil, um zu verstehen, warum Google sie für dein Keyword rankt.</span></div><div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>🔍 Wie viele Mitbewerber pro Raster?</strong><br><span style="color:#166534;font-size:.82rem">3×3 → bis zu 10 · 5×5 → 20 · 7×7 → 35 · 9×9 → 50 · 13×13 → 75 · 15×15 → 150 · 21×21 → 300. Vergröße das Raster und starte einen neuen Scan, um mehr zu entdecken.</span></div>', cta: 'Verstanden!' },
						'pt-br': { title: 'Entender os concorrentes', body: '<p style="color:#1e293b">Os concorrentes exibidos são <strong>exatamente o que o Google retorna</strong> para sua keyword — incluindo empresas de outros setores. São dados reais, não filtrados. <strong>A transparência é uma força</strong>: mesmo um negócio fora do tema pode roubar sua visibilidade local ocupando uma posição alta no mapa.</p>' + hmPedagoSteps(['Contador alto', 'Aparece em muitos pontos = domina a zona para esta keyword.'], ['Melhor rank', 'Sua melhor posição encontrada em todos os pontos escaneados.'], ['Botões toggle', 'Clique num botão de concorrente para mostrar/ocultar seu marcador.']) + '<div style="background:#faf5ff;border:1px solid #e9d5ff;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>⚠️ Importante</strong><br><span style="color:#6d28d9;font-size:.82rem">Um concorrente com atividade diferente pode mesmo assim roubar sua visibilidade local.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Dica</strong><br><span style="color:#92400e">Estude o perfil GBP deles para entender por que o Google os classifica para sua keyword.</span></div><div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>🔍 Quantos concorrentes por grade?</strong><br><span style="color:#166534;font-size:.82rem">3×3 → até 10 · 5×5 → 20 · 7×7 → 35 · 9×9 → 50 · 13×13 → 75 · 15×15 → 150 · 21×21 → 300. Aumente o tamanho da grade e inicie um novo scan para descobrir mais.</span></div>', cta: 'Entendido!' },
						tr: { title: 'Rakipleri anla', body: '<p style="color:#1e293b">Görüntülenen rakipler, keywordin için <strong>Google\'ın tam olarak döndürdüğü</strong> sonuçlardır — diğer sektörlerden işletmeler dahil. Bunlar gerçek, filtrelenmemiş verilerdir. <strong>Şeffaflık bir güçtür</strong>: konuyla ilgisi olmayan bir işletme bile haritada yüksek bir konumu işgal ederek yerel görünürlüğünü çalabilir.</p>' + hmPedagoSteps(['Yüksek sayaç', 'Çok noktada görünüyor = bu keyword için bölgeye hâkim.'], ['En iyi sıralama', 'Taranan tüm noktalarda bulunan en iyi konumları.'], ['Toggle butonları', 'Rakip butonuna tıklayarak işaretçisini göster/gizle.']) + '<div style="background:#faf5ff;border:1px solid #e9d5ff;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>⚠️ Önemli</strong><br><span style="color:#6d28d9;font-size:.82rem">Farklı faaliyet alanına sahip bir rakip yine de yerel görünürlüğünü çalabilir.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 İpucu</strong><br><span style="color:#92400e">Google\'ın onları keywordin için neden sıraladığını anlamak için GBP profillerini incele.</span></div><div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>🔍 Izgara başına kaç rakip?</strong><br><span style="color:#166534;font-size:.82rem">3×3 → 10\'a kadar · 5×5 → 20 · 7×7 → 35 · 9×9 → 50 · 13×13 → 75 · 15×15 → 150 · 21×21 → 300. Daha fazlasını keşfetmek için ızgara boyutunu büyüt ve yeni tarama başlat.</span></div>', cta: 'Anlaşıldı!' }
					},
					hm_scan_modal: {
						icon: '🚀',
						en: { title: 'Relaunch a Scan', body: '<p style="color:#1e293b">Relaunching a scan refreshes your visibility data for the selected keywords on the current grid.</p>' + hmPedagoSteps(['Select keywords', 'Only selected keywords (highlighted) will be scanned.'], ['Check credits', 'Balance shown = available. After = remaining after scan.'], ['Click Launch', 'Results appear on the map once processing is complete (1-3 min).']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📅 When to rescan?</strong><br><span style="color:#0369a1;font-size:.82rem">Weekly for active keywords. Monthly for strategic ones.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Tip</strong><br><span style="color:#92400e">Change the grid size in the left drawer before relaunching for a different zoom level.</span></div>', cta: 'Got it!' },
						fr: { title: 'Relancer un scan', body: '<p style="color:#1e293b">Relancer un scan actualise vos données de visibilité pour les mots-clés sélectionnés sur la grille en cours.</p>' + hmPedagoSteps(['Sélectionner les mots-clés', 'Seuls les mots-clés actifs (surlignés) seront scannés.'], ['Vérifier les crédits', 'Balance affichée = disponible. Après = restant après le scan.'], ['Cliquer Lancer', 'Les résultats apparaissent une fois le traitement terminé (1-3 min).']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📅 Quand rescanner ?</strong><br><span style="color:#0369a1;font-size:.82rem">Chaque semaine pour les mots-clés actifs. Mensuellement pour les stratégiques.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Astuce</strong><br><span style="color:#92400e">Changez la taille de grille dans le panneau gauche avant de relancer pour un autre niveau de zoom.</span></div>', cta: 'Compris !' },
						es: { title: 'Relanzar un escaneo', body: '<p style="color:#1e293b">Relanzar un escaneo actualiza sus datos de visibilidad para las palabras clave seleccionadas en la cuadrícula actual.</p>' + hmPedagoSteps(['Seleccionar palabras clave', 'Solo las palabras activas (resaltadas) serán escaneadas.'], ['Verificar créditos', 'Balance = disponible. Después = restante tras el escaneo.'], ['Hacer clic en Lanzar', 'Los resultados aparecen una vez procesado (1-3 min).']) + '<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>📅 ¿Cuándo reescanear?</strong><br><span style="color:#0369a1;font-size:.82rem">Semanalmente para palabras activas. Mensualmente para las estratégicas.</span></div><div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-top:8px"><strong>💡 Consejo</strong><br><span style="color:#92400e">Cambie el tamaño de cuadrícula en el panel izquierdo antes de relanzar.</span></div>', cta: '¡Entendido!' }
					},
					// Anti-régression #Sprint43-C12-zone-pedago
					hm_comp_zone: {
						icon: '⬡',
						en: { title: 'Competitor territory zones', body: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 160" style="width:100%;max-width:320px;display:block;margin:0 auto 12px;border-radius:8px;background:#f0ede8"><rect x="0" y="0" width="320" height="160" rx="8" fill="#e8ede4"/><rect x="40" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">1</text><rect x="120" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".15" stroke="#10b981" stroke-width="1"/><text x="150" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">4</text><rect x="200" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="55" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><rect x="40" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">2</text><rect x="120" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".2" stroke="#10b981" stroke-width="1"/><text x="150" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">3</text><rect x="200" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="125" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><polygon points="115,15 265,15 265,155 115,155" fill="#dc2626" fill-opacity=".12" stroke="#dc2626" stroke-width="2" stroke-dasharray="6,3"/><text x="190" y="12" text-anchor="middle" font-size="9" font-weight="800" fill="#dc2626" letter-spacing="1">NORTH-EAST</text><circle cx="185" cy="52" r="10" fill="#dc2626" stroke="white" stroke-width="1.5"/><text x="185" y="56" text-anchor="middle" font-size="8" font-weight="900" fill="white">2</text></svg><p style="color:#1e293b">The <strong>⬡</strong> button displays the <strong>geographic territory</strong> of this competitor for this keyword on your grid.</p><ul style="margin:8px 0;padding-left:16px;color:#475569;font-size:.82rem;line-height:1.7"><li>Each <strong>dashed polygon</strong> = a zone where this competitor outranks you</li><li><strong>Geographic labels</strong> (North, South-East…) show where in your area they are threatening</li><li>Zones <strong>without a polygon</strong> = territory already reconquered ✅</li><li>With each scan, polygons <strong>shrink</strong> as you gain ground</li></ul><div style="background:#fef2f2;border-left:3px solid #dc2626;padding:8px 10px;border-radius:0 4px 4px 0;font-size:.78rem;color:#991b1b">🎯 <strong>Goal:</strong> make all red polygons disappear scan after scan.</div>', cta: 'Got it!' },
						fr: { title: 'Zones de territoire concurrent', body: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 160" style="width:100%;max-width:320px;display:block;margin:0 auto 12px;border-radius:8px;background:#f0ede8"><rect x="0" y="0" width="320" height="160" rx="8" fill="#e8ede4"/><rect x="40" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">1</text><rect x="120" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".15" stroke="#10b981" stroke-width="1"/><text x="150" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">4</text><rect x="200" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="55" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><rect x="40" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">2</text><rect x="120" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".2" stroke="#10b981" stroke-width="1"/><text x="150" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">3</text><rect x="200" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="125" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><polygon points="115,15 265,15 265,155 115,155" fill="#dc2626" fill-opacity=".12" stroke="#dc2626" stroke-width="2" stroke-dasharray="6,3"/><text x="190" y="12" text-anchor="middle" font-size="9" font-weight="800" fill="#dc2626" letter-spacing="1">NORD-EST</text><circle cx="185" cy="52" r="10" fill="#dc2626" stroke="white" stroke-width="1.5"/><text x="185" y="56" text-anchor="middle" font-size="8" font-weight="900" fill="white">2</text></svg><p style="color:#1e293b">Le bouton <strong>⬡</strong> affiche le <strong>territoire géographique</strong> du concurrent pour ce mot-clé sur votre grille.</p><ul style="margin:8px 0;padding-left:16px;color:#475569;font-size:.82rem;line-height:1.7"><li>Chaque <strong>polygone en pointillés</strong> = une zone où ce concurrent vous devance</li><li>Les <strong>labels géographiques</strong> (Nord, Sud-Est…) indiquent où dans votre zone il est menaçant</li><li>Les zones <strong>sans polygone</strong> = territoire déjà reconquis ✅</li><li>Au fil des scans, les polygones <strong>rétrécissent</strong> à mesure que vous gagnez du terrain</li></ul><div style="background:#fef2f2;border-left:3px solid #dc2626;padding:8px 10px;border-radius:0 4px 4px 0;font-size:.78rem;color:#991b1b">🎯 <strong>Objectif :</strong> faire disparaître tous les polygones rouges scan après scan.</div>', cta: 'Compris !' },
						es: { title: 'Zonas de territorio competidor', body: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 160" style="width:100%;max-width:320px;display:block;margin:0 auto 12px;border-radius:8px;background:#f0ede8"><rect x="0" y="0" width="320" height="160" rx="8" fill="#e8ede4"/><rect x="40" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">1</text><rect x="120" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".15" stroke="#10b981" stroke-width="1"/><text x="150" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">4</text><rect x="200" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="55" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><rect x="40" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">2</text><rect x="120" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".2" stroke="#10b981" stroke-width="1"/><text x="150" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">3</text><rect x="200" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="125" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><polygon points="115,15 265,15 265,155 115,155" fill="#dc2626" fill-opacity=".12" stroke="#dc2626" stroke-width="2" stroke-dasharray="6,3"/><text x="190" y="12" text-anchor="middle" font-size="9" font-weight="800" fill="#dc2626" letter-spacing="1">NORESTE</text><circle cx="185" cy="52" r="10" fill="#dc2626" stroke="white" stroke-width="1.5"/><text x="185" y="56" text-anchor="middle" font-size="8" font-weight="900" fill="white">2</text></svg><p style="color:#1e293b">El botón <strong>⬡</strong> muestra el <strong>territorio geográfico</strong> del competidor para esta palabra clave en su cuadrícula.</p><ul style="margin:8px 0;padding-left:16px;color:#475569;font-size:.82rem;line-height:1.7"><li>Cada <strong>polígono punteado</strong> = una zona donde este competidor le supera</li><li>Las <strong>etiquetas geográficas</strong> (Norte, Sur-Este…) indican dónde en su área es amenazante</li><li>Las zonas <strong>sin polígono</strong> = territorio ya reconquistado ✅</li><li>Con cada escaneo, los polígonos <strong>se reducen</strong> a medida que gana terreno</li></ul><div style="background:#fef2f2;border-left:3px solid #dc2626;padding:8px 10px;border-radius:0 4px 4px 0;font-size:.78rem;color:#991b1b">🎯 <strong>Objetivo:</strong> hacer desaparecer todos los polígonos rojos escaneo tras escaneo.</div>', cta: '¡Entendido!' },
						de: { title: 'Konkurrenzbereiche', body: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 160" style="width:100%;max-width:320px;display:block;margin:0 auto 12px;border-radius:8px;background:#f0ede8"><rect x="0" y="0" width="320" height="160" rx="8" fill="#e8ede4"/><rect x="40" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">1</text><rect x="120" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".15" stroke="#10b981" stroke-width="1"/><text x="150" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">4</text><rect x="200" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="55" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><rect x="40" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">2</text><rect x="120" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".2" stroke="#10b981" stroke-width="1"/><text x="150" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">3</text><rect x="200" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="125" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><polygon points="115,15 265,15 265,155 115,155" fill="#dc2626" fill-opacity=".12" stroke="#dc2626" stroke-width="2" stroke-dasharray="6,3"/><text x="190" y="12" text-anchor="middle" font-size="9" font-weight="800" fill="#dc2626" letter-spacing="1">NORD-OST</text><circle cx="185" cy="52" r="10" fill="#dc2626" stroke="white" stroke-width="1.5"/><text x="185" y="56" text-anchor="middle" font-size="8" font-weight="900" fill="white">2</text></svg><p style="color:#1e293b">Die Schaltfläche <strong>⬡</strong> zeigt das <strong>geografische Gebiet</strong> des Mitbewerbers für dieses Keyword in deinem Raster.</p><ul style="margin:8px 0;padding-left:16px;color:#475569;font-size:.82rem;line-height:1.7"><li>Jedes <strong>gestrichelte Polygon</strong> = eine Zone, in der dieser Mitbewerber dich überholt</li><li>Die <strong>geografischen Labels</strong> (Nord, Südost…) zeigen, wo in deiner Zone er bedrohlich ist</li><li>Zonen <strong>ohne Polygon</strong> = bereits zurückgewonnenes Gebiet ✅</li><li>Mit jedem Scan <strong>schrumpfen</strong> die Polygone, wenn du Terrain gewinnst</li></ul><div style="background:#fef2f2;border-left:3px solid #dc2626;padding:8px 10px;border-radius:0 4px 4px 0;font-size:.78rem;color:#991b1b">🎯 <strong>Ziel:</strong> Alle roten Polygone Scan für Scan verschwinden lassen.</div>', cta: 'Verstanden!' },
						'pt-br': { title: 'Zonas de território concorrente', body: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 160" style="width:100%;max-width:320px;display:block;margin:0 auto 12px;border-radius:8px;background:#f0ede8"><rect x="0" y="0" width="320" height="160" rx="8" fill="#e8ede4"/><rect x="40" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">1</text><rect x="120" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".15" stroke="#10b981" stroke-width="1"/><text x="150" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">4</text><rect x="200" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="55" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><rect x="40" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">2</text><rect x="120" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".2" stroke="#10b981" stroke-width="1"/><text x="150" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">3</text><rect x="200" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="125" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><polygon points="115,15 265,15 265,155 115,155" fill="#dc2626" fill-opacity=".12" stroke="#dc2626" stroke-width="2" stroke-dasharray="6,3"/><text x="190" y="12" text-anchor="middle" font-size="9" font-weight="800" fill="#dc2626" letter-spacing="1">NORDESTE</text><circle cx="185" cy="52" r="10" fill="#dc2626" stroke="white" stroke-width="1.5"/><text x="185" y="56" text-anchor="middle" font-size="8" font-weight="900" fill="white">2</text></svg><p style="color:#1e293b">O botão <strong>⬡</strong> exibe o <strong>território geográfico</strong> do concorrente para esta keyword na sua grade.</p><ul style="margin:8px 0;padding-left:16px;color:#475569;font-size:.82rem;line-height:1.7"><li>Cada <strong>polígono pontilhado</strong> = uma zona onde este concorrente supera você</li><li>Os <strong>rótulos geográficos</strong> (Norte, Sudeste…) indicam onde na sua zona ele é ameaçador</li><li>Zonas <strong>sem polígono</strong> = território já reconquistado ✅</li><li>A cada scan, os polígonos <strong>encolhem</strong> à medida que você ganha terreno</li></ul><div style="background:#fef2f2;border-left:3px solid #dc2626;padding:8px 10px;border-radius:0 4px 4px 0;font-size:.78rem;color:#991b1b">🎯 <strong>Objetivo:</strong> fazer desaparecer todos os polígonos vermelhos scan após scan.</div>', cta: 'Entendido!' },
						tr: { title: 'Rakip bölge alanları', body: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 160" style="width:100%;max-width:320px;display:block;margin:0 auto 12px;border-radius:8px;background:#f0ede8"><rect x="0" y="0" width="320" height="160" rx="8" fill="#e8ede4"/><rect x="40" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">1</text><rect x="120" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".15" stroke="#10b981" stroke-width="1"/><text x="150" y="55" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">4</text><rect x="200" y="20" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="55" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><rect x="40" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".25" stroke="#10b981" stroke-width="1"/><text x="70" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">2</text><rect x="120" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".2" stroke="#10b981" stroke-width="1"/><text x="150" y="125" text-anchor="middle" font-size="14" font-weight="700" fill="#065f46">3</text><rect x="200" y="90" width="60" height="60" rx="4" fill="#10b981" fill-opacity=".1" stroke="#10b981" stroke-width="1"/><text x="230" y="125" text-anchor="middle" font-size="11" font-weight="700" fill="#94a3b8">&gt;20</text><polygon points="115,15 265,15 265,155 115,155" fill="#dc2626" fill-opacity=".12" stroke="#dc2626" stroke-width="2" stroke-dasharray="6,3"/><text x="190" y="12" text-anchor="middle" font-size="9" font-weight="800" fill="#dc2626" letter-spacing="1">KUZEYDOĞU</text><circle cx="185" cy="52" r="10" fill="#dc2626" stroke="white" stroke-width="1.5"/><text x="185" y="56" text-anchor="middle" font-size="8" font-weight="900" fill="white">2</text></svg><p style="color:#1e293b"><strong>⬡</strong> düğmesi, bu keyword için rakibin ızgarandaki <strong>coğrafi bölgesini</strong> gösterir.</p><ul style="margin:8px 0;padding-left:16px;color:#475569;font-size:.82rem;line-height:1.7"><li>Her <strong>kesik çizgili poligon</strong> = bu rakibin seni geçtiği bir bölge</li><li><strong>Coğrafi etiketler</strong> (Kuzey, Güneydoğu…) bölgende nerede tehdit oluşturduğunu gösterir</li><li><strong>Poligonsuz</strong> bölgeler = zaten geri kazanılmış alan ✅</li><li>Her taramayla, alan kazandıkça poligonlar <strong>küçülür</strong></li></ul><div style="background:#fef2f2;border-left:3px solid #dc2626;padding:8px 10px;border-radius:0 4px 4px 0;font-size:.78rem;color:#991b1b">🎯 <strong>Hedef:</strong> Tarama tarama tüm kırmızı poligonları yok et.</div>', cta: 'Anlaşıldı!' }
					},
					// #Sprint43-scheduler — modal pédago scans programmés
					hm_schedule: {
						icon: '📅',
						en: {
							title: 'Scheduled Scans',
							body:  '<p style="margin:0 0 12px;font-size:.9rem;color:#374151">'
								+ 'Automate your heatmap scans to monitor your local visibility '
								+ 'over time — without any manual action.</p>'
								+ '<div class="avi-pedago-steps">'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">1</span>'
								+   '<div><strong>Choose your frequency</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Daily, every 3 days, weekly, every 2 weeks, or monthly. '
								+   'Cancel anytime.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">2</span>'
								+   '<div><strong>Credits checked before each scan</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Your balance is verified before every scheduled scan. '
								+   'If too low, the scan is skipped and you get an email alert — '
								+   'top up anytime.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">3</span>'
								+   '<div><strong>Results always up to date</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Latest scan on your live heatmap. All previous scans in the '
								+   '<em>Evolution</em> tab.</span></div>'
								+ '</div>'
								+ '</div>'
								+ '<div class="avi-hm-pedago-strategy">'
								+   '<div class="avi-hm-pedago-strategy-title">'
								+     '💡 Multi-grid strategy'
								+   '</div>'
								+   '<p style="font-size:.82rem;color:#374151;margin:6px 0 8px">'
								+     'You can run <strong>one schedule per grid size</strong> — '
								+     'each grid = one tracking strategy:'
								+   '</p>'
								+   '<div class="avi-hm-pedago-grid-list">'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge sm">3×3</span>'
								+       '<span>Niche keywords — monthly tracking</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge md">7×7</span>'
								+       '<span>Broad keywords — weekly tracking</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge lg">21×21</span>'
								+       '<span>Priority keywords — daily tracking</span>'
								+     '</div>'
								+   '</div>'
								+   '<p style="font-size:.78rem;color:#6b7280;margin:8px 0 4px;'
								+           'border-top:1px solid #ddd6fe;padding-top:8px">'
								+ '💬 <strong>Tip :</strong> Scanning 2+ keywords per grid gives richer '
								+ 'comparative data. You can also track the same grid size with '
								+ 'different keywords as separate schedules.'
								+ '</p>'
								+   '<p style="font-size:.78rem;color:#6d28d9;margin:8px 0 0;font-weight:500">'
								+     '★ Unique to Avision — not available in Local Falcon or competitors'
								+   '</p>'
								+ '</div>',
							cta: 'Got it!'
						},
						fr: {
							title: 'Scans programmés',
							body:  '<p style="margin:0 0 12px;font-size:.9rem;color:#374151">'
								+ 'Automatisez vos scans pour suivre votre visibilité locale '
								+ 'dans le temps — sans aucune action manuelle.</p>'
								+ '<div class="avi-pedago-steps">'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">1</span>'
								+   '<div><strong>Choisissez votre fréquence</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Quotidien, tous les 3 jours, hebdomadaire, toutes les 2 semaines ou mensuel. '
								+   'Annulable à tout moment.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">2</span>'
								+   '<div><strong>Crédits vérifiés avant chaque scan</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Votre solde est contrôlé avant chaque scan programmé. '
								+   'Si insuffisant, le scan est reporté et vous recevez une alerte email — '
								+   'rechargez à tout moment.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">3</span>'
								+   '<div><strong>Résultats toujours à jour</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Dernier scan sur votre heatmap en direct. Tous les scans précédents '
								+   'dans l\'onglet <em>Évolution</em>.</span></div>'
								+ '</div>'
								+ '</div>'
								+ '<div class="avi-hm-pedago-strategy">'
								+   '<div class="avi-hm-pedago-strategy-title">'
								+     '💡 Stratégie multi-grilles'
								+   '</div>'
								+   '<p style="font-size:.82rem;color:#374151;margin:6px 0 8px">'
								+     'Vous pouvez avoir <strong>un schedule par taille de grille</strong> — '
								+     'chaque grille = une stratégie de suivi :'
								+   '</p>'
								+   '<div class="avi-hm-pedago-grid-list">'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge sm">3×3</span>'
								+       '<span>Mots-clés niche — suivi mensuel</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge md">7×7</span>'
								+       '<span>Mots-clés larges — suivi hebdomadaire</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge lg">21×21</span>'
								+       '<span>Mots-clés prioritaires — suivi quotidien</span>'
								+     '</div>'
								+   '</div>'
								+   '<p style="font-size:.78rem;color:#6b7280;margin:8px 0 4px;'
								+           'border-top:1px solid #ddd6fe;padding-top:8px">'
								+ '💬 <strong>Conseil :</strong> Scanner 2+ mots-clés par grille '
								+ 'offre des données comparatives plus riches. Vous pouvez aussi '
								+ 'suivre la même grille avec des mots-clés différents '
								+ 'en schedules séparés.'
								+ '</p>'
								+   '<p style="font-size:.78rem;color:#6d28d9;margin:8px 0 0;font-weight:500">'
								+     '★ Exclusif Avision — non disponible chez Local Falcon ou la concurrence'
								+   '</p>'
								+ '</div>',
							cta: 'Compris !'
						},
						es: {
							title: 'Escaneos programados',
							body:  '<p style="margin:0 0 12px;font-size:.9rem;color:#374151">'
								+ 'Automatiza tus escaneos para monitorear tu visibilidad local '
								+ 'en el tiempo — sin ninguna acción manual.</p>'
								+ '<div class="avi-pedago-steps">'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">1</span>'
								+   '<div><strong>Elige tu frecuencia</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Diario, cada 3 días, semanal, cada 2 semanas o mensual. '
								+   'Cancela cuando quieras.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">2</span>'
								+   '<div><strong>Créditos verificados antes de cada escaneo</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Tu saldo se verifica antes de cada escaneo programado. '
								+   'Si es insuficiente, el escaneo se omite y recibes una alerta — '
								+   'recarga cuando quieras.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">3</span>'
								+   '<div><strong>Resultados siempre actualizados</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Último escaneo en tu heatmap en vivo. Todos los anteriores '
								+   'en la pestaña <em>Evolución</em>.</span></div>'
								+ '</div>'
								+ '</div>'
								+ '<div class="avi-hm-pedago-strategy">'
								+   '<div class="avi-hm-pedago-strategy-title">'
								+     '💡 Estrategia multi-cuadrícula'
								+   '</div>'
								+   '<p style="font-size:.82rem;color:#374151;margin:6px 0 8px">'
								+     'Puedes tener <strong>un programa por tamaño de cuadrícula</strong> — '
								+     'cada cuadrícula = una estrategia de seguimiento:'
								+   '</p>'
								+   '<div class="avi-hm-pedago-grid-list">'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge sm">3×3</span>'
								+       '<span>Palabras clave nicho — seguimiento mensual</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge md">7×7</span>'
								+       '<span>Palabras clave amplias — seguimiento semanal</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge lg">21×21</span>'
								+       '<span>Palabras clave prioritarias — seguimiento diario</span>'
								+     '</div>'
								+   '</div>'
								+   '<p style="font-size:.78rem;color:#6b7280;margin:8px 0 4px;'
								+           'border-top:1px solid #ddd6fe;padding-top:8px">'
								+ '💬 <strong>Consejo :</strong> Escanear 2+ palabras clave por '
								+ 'cuadrícula ofrece datos comparativos más ricos. También puedes '
								+ 'seguir la misma cuadrícula con palabras clave diferentes '
								+ 'en programas separados.'
								+ '</p>'
								+   '<p style="font-size:.78rem;color:#6d28d9;margin:8px 0 0;font-weight:500">'
								+     '★ Exclusivo de Avision — no disponible en Local Falcon ni competidores'
								+   '</p>'
								+ '</div>',
							cta: '¡Entendido!'
						},
						de: {
							title: 'Geplante Scans',
							body:  '<p style="margin:0 0 12px;font-size:.9rem;color:#374151">'
								+ 'Automatisiere deine Scans, um deine lokale Sichtbarkeit '
								+ 'im Laufe der Zeit zu verfolgen — ohne manuelle Aktionen.</p>'
								+ '<div class="avi-pedago-steps">'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">1</span>'
								+   '<div><strong>Wähle deine Häufigkeit</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Täglich, alle 3 Tage, wöchentlich, alle 2 Wochen oder monatlich. '
								+   'Jederzeit kündbar.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">2</span>'
								+   '<div><strong>Credits vor jedem Scan geprüft</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Dein Guthaben wird vor jedem geplanten Scan überprüft. '
								+   'Bei unzureichendem Guthaben wird der Scan verschoben und du erhältst eine E-Mail — '
								+   'jederzeit aufladen.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">3</span>'
								+   '<div><strong>Ergebnisse immer aktuell</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Letzter Scan live auf deiner Heatmap. Alle vorherigen Scans '
								+   'im Tab <em>Entwicklung</em>.</span></div>'
								+ '</div>'
								+ '</div>'
								+ '<div class="avi-hm-pedago-strategy">'
								+   '<div class="avi-hm-pedago-strategy-title">'
								+     '💡 Multi-Raster-Strategie'
								+   '</div>'
								+   '<p style="font-size:.82rem;color:#374151;margin:6px 0 8px">'
								+     'Du kannst <strong>einen Schedule pro Rastergröße</strong> haben — '
								+     'jedes Raster = eine Tracking-Strategie :'
								+   '</p>'
								+   '<div class="avi-hm-pedago-grid-list">'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge sm">3×3</span>'
								+       '<span>Nischen-Keywords — monatliches Tracking</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge md">7×7</span>'
								+       '<span>Breite Keywords — wöchentliches Tracking</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge lg">21×21</span>'
								+       '<span>Prioritäts-Keywords — tägliches Tracking</span>'
								+     '</div>'
								+   '</div>'
								+   '<p style="font-size:.78rem;color:#6b7280;margin:8px 0 4px;'
								+           'border-top:1px solid #ddd6fe;padding-top:8px">'
								+ '💬 <strong>Tipp:</strong> 2+ Keywords pro Raster zu scannen '
								+ 'liefert reichhaltigere Vergleichsdaten. Du kannst auch '
								+ 'dasselbe Raster mit verschiedenen Keywords '
								+ 'in separaten Schedules verfolgen.'
								+ '</p>'
								+   '<p style="font-size:.78rem;color:#6d28d9;margin:8px 0 0;font-weight:500">'
								+     '★ Exklusiv bei Avision — nicht verfügbar bei Local Falcon oder Mitbewerbern'
								+   '</p>'
								+ '</div>',
							cta: 'Verstanden!'
						},
						'pt-br': {
							title: 'Scans programados',
							body:  '<p style="margin:0 0 12px;font-size:.9rem;color:#374151">'
								+ 'Automatize seus scans para acompanhar sua visibilidade local '
								+ 'ao longo do tempo — sem nenhuma ação manual.</p>'
								+ '<div class="avi-pedago-steps">'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">1</span>'
								+   '<div><strong>Escolha sua frequência</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Diário, a cada 3 dias, semanal, a cada 2 semanas ou mensal. '
								+   'Cancelável a qualquer momento.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">2</span>'
								+   '<div><strong>Créditos verificados antes de cada scan</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Seu saldo é verificado antes de cada scan programado. '
								+   'Se insuficiente, o scan é adiado e você recebe um alerta por e-mail — '
								+   'recarregue a qualquer momento.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">3</span>'
								+   '<div><strong>Resultados sempre atualizados</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Último scan ao vivo no seu heatmap. Todos os scans anteriores '
								+   'na aba <em>Evolução</em>.</span></div>'
								+ '</div>'
								+ '</div>'
								+ '<div class="avi-hm-pedago-strategy">'
								+   '<div class="avi-hm-pedago-strategy-title">'
								+     '💡 Estratégia multi-grades'
								+   '</div>'
								+   '<p style="font-size:.82rem;color:#374151;margin:6px 0 8px">'
								+     'Você pode ter <strong>um schedule por tamanho de grade</strong> — '
								+     'cada grade = uma estratégia de rastreamento :'
								+   '</p>'
								+   '<div class="avi-hm-pedago-grid-list">'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge sm">3×3</span>'
								+       '<span>Keywords de nicho — rastreamento mensal</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge md">7×7</span>'
								+       '<span>Keywords amplas — rastreamento semanal</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge lg">21×21</span>'
								+       '<span>Keywords prioritárias — rastreamento diário</span>'
								+     '</div>'
								+   '</div>'
								+   '<p style="font-size:.78rem;color:#6b7280;margin:8px 0 4px;'
								+           'border-top:1px solid #ddd6fe;padding-top:8px">'
								+ '💬 <strong>Dica:</strong> Escanear 2+ keywords por grade '
								+ 'oferece dados comparativos mais ricos. Você também pode '
								+ 'acompanhar a mesma grade com keywords diferentes '
								+ 'em schedules separados.'
								+ '</p>'
								+   '<p style="font-size:.78rem;color:#6d28d9;margin:8px 0 0;font-weight:500">'
								+     '★ Exclusivo Avision — não disponível no Local Falcon ou na concorrência'
								+   '</p>'
								+ '</div>',
							cta: 'Entendido!'
						},
						tr: {
							title: 'Programlı taramalar',
							body:  '<p style="margin:0 0 12px;font-size:.9rem;color:#374151">'
								+ 'Taramalarını otomatikleştir ve yerel görünürlüğünü '
								+ 'zaman içinde takip et — manuel işlem olmadan.</p>'
								+ '<div class="avi-pedago-steps">'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">1</span>'
								+   '<div><strong>Sıklığını seç</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Günlük, her 3 günde, haftalık, her 2 haftada veya aylık. '
								+   'İstediğin zaman iptal edilebilir.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">2</span>'
								+   '<div><strong>Her taramadan önce krediler kontrol edilir</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Bakiyen her programlı taramadan önce kontrol edilir. '
								+   'Yetersizse tarama ertelenir ve e-posta uyarısı alırsın — '
								+   'istediğin zaman yükle.</span></div>'
								+ '</div>'
								+ '<div class="avi-pedago-step">'
								+   '<span class="avi-pedago-num">3</span>'
								+   '<div><strong>Sonuçlar her zaman güncel</strong><br>'
								+   '<span style="font-size:.82rem;color:#6b7280">'
								+   'Son tarama heatmap\'inde canlı. Tüm önceki taramalar '
								+   '<em>Gelişim</em> sekmesinde.</span></div>'
								+ '</div>'
								+ '</div>'
								+ '<div class="avi-hm-pedago-strategy">'
								+   '<div class="avi-hm-pedago-strategy-title">'
								+     '💡 Çoklu ızgara stratejisi'
								+   '</div>'
								+   '<p style="font-size:.82rem;color:#374151;margin:6px 0 8px">'
								+     '<strong>Izgara boyutu başına bir schedule</strong> oluşturabilirsin — '
								+     'her ızgara = bir takip stratejisi :'
								+   '</p>'
								+   '<div class="avi-hm-pedago-grid-list">'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge sm">3×3</span>'
								+       '<span>Niş keywordler — aylık takip</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge md">7×7</span>'
								+       '<span>Geniş keywordler — haftalık takip</span>'
								+     '</div>'
								+     '<div class="avi-hm-pedago-grid-item">'
								+       '<span class="avi-hm-pedago-grid-badge lg">21×21</span>'
								+       '<span>Öncelikli keywordler — günlük takip</span>'
								+     '</div>'
								+   '</div>'
								+   '<p style="font-size:.78rem;color:#6b7280;margin:8px 0 4px;'
								+           'border-top:1px solid #ddd6fe;padding-top:8px">'
								+ '💬 <strong>İpucu:</strong> Izgara başına 2+ keyword taramak '
								+ 'daha zengin karşılaştırmalı veriler sağlar. Aynı ızgarayı '
								+ 'farklı keywordlerle ayrı schedulelarda da takip edebilirsin.'
								+ '</p>'
								+   '<p style="font-size:.78rem;color:#6d28d9;margin:8px 0 0;font-weight:500">'
								+     '★ Avision\'a özel — Local Falcon veya rakiplerde mevcut değil'
								+   '</p>'
								+ '</div>',
							cta: 'Anlaşıldı!'
						}
					},
				};
				function openHmPedagoModal(key){
					var data = HM_PEDAGO[key];
					if (key === 'hm_map_view') {
						var lang = getHmLang();
						data = {
							icon: '🗺️',
							en: {
								title: hm_t('hm_map_view_title') || 'Growth Radar Heatmap',
								body: '<p style="color:#1e293b;line-height:1.8;margin:0">' +
									(hm_t('hm_map_view_body') || '').replace(/\. /g, '.<br><br>') + '</p>',
								cta: lang === 'fr' ? 'J\'ai compris !'
									: lang === 'es' ? '¡Entendido!'
									: lang === 'de' ? 'Verstanden!'
									: lang === 'pt-br' ? 'Entendido!'
									: lang === 'tr' ? 'Anlaşıldı!'
									: 'Got it!'
							}
						};
					}
					if (!data) return;
					var content = data[getHmLang()] || data.fr || data.en;
					var overlay = root.querySelector('#aviHmPedagoOverlay');
					var iconEl = root.querySelector('#aviHmPedagoIcon');
					var titleEl = root.querySelector('#aviHmPedagoTitle');
					var bodyEl = root.querySelector('#aviHmPedagoBody');
					var ctaEl = root.querySelector('#aviHmPedagoCta');
					if (!overlay || !titleEl || !bodyEl || !ctaEl) return;
					if (iconEl) iconEl.textContent = data.icon || '📌';
					titleEl.textContent = content.title || '';
					bodyEl.innerHTML = content.body || '';
					ctaEl.textContent = content.cta || 'OK';
					overlay.style.display = 'flex';
					overlay.setAttribute('aria-hidden', 'false');
				}
				function closeHmPedagoModal(){
					var overlay = root.querySelector('#aviHmPedagoOverlay');
					if (!overlay) return;
					overlay.style.display = 'none';
					overlay.setAttribute('aria-hidden', 'true');
				}
				function updateCoverageBadge(keyword) {
					if (!root) return;
					var badge = root.querySelector('[data-avi-hm-coverage]');
					if (!badge) return;

					// Détermine le keyword actif
					var activeKws;
					if (keyword) {
						// Appelé depuis fetchScanForKeyword — keyword explicitement connu
						activeKws = [keyword];
					} else {
						// Appelé depuis polling — utilise selectedMapKeywords ou selectedKeyword
						activeKws = (selectedMapKeywords && selectedMapKeywords.length)
							? selectedMapKeywords.slice()
							: (selectedKeyword ? [selectedKeyword] : []);
					}

					if (activeKws.length === 0) return;

					var totalPoints = 0, top3Points = 0, kwWithData = 0;
					activeKws.forEach(function(kw) {
						var key = String(kw || '').trim().toLowerCase();
						var d = scanCache[key];
						if (!d || !Array.isArray(d.map_points) || d.map_points.length === 0) return;
						kwWithData++;
						d.map_points.forEach(function(p) {
							totalPoints++;
							var r = (p && p.rank !== null && p.rank !== undefined)
								? Number(p.rank) : null;
							if (r !== null && r >= 1 && r <= 3) top3Points++;
						});
					});

					if (kwWithData === 0 || totalPoints === 0) {
						badge.innerHTML = '<span class="avi-hm-coverage-none">'
							+ (hm_t('coverage_score_none') || 'No scan yet')
							+ '</span>';
						return;
					}

					var pct = Math.round((top3Points / totalPoints) * 1000) / 10;
					var kwLabel = keyword
						? String(keyword)
						: (activeKws.length ? activeKws[0] : '');

					badge.innerHTML = '<span class="avi-hm-coverage-icon">◎</span>'
						+ '<span class="avi-hm-coverage-value">' + pct + '%</span>'
						+ '<span class="avi-hm-coverage-lbl">'
						+ (hm_t('coverage_score_label') || 'Top 3 Coverage')
						+ '</span>'
						+ (kwLabel
							? '<span class="avi-hm-coverage-sep">·</span>'
							  + '<span class="avi-hm-coverage-kw">'
							  + String(kwLabel).replace(/[<>&"]/g, '')
							  + '</span>'
							: '');
				}
				function updateDrawerMetrics(keyword) {
					if (!root) return;
					var key = String(keyword || '').trim().toLowerCase();
					var d = scanCache[key];
					if (!d) return;

					// Top 3 Territory — calcul depuis map_points (même logique que updateCoverageBadge)
					var territoryEl = root.querySelector('[data-avi-hm-metric="territory"]');
					if (territoryEl && Array.isArray(d.map_points) && d.map_points.length > 0) {
						var total = d.map_points.length;
						var top3 = d.map_points.filter(function(p) {
							var r = (p && p.rank !== null && p.rank !== undefined) ? Number(p.rank) : null;
							return r !== null && r >= 1 && r <= 3;
						}).length;
						var pct = Math.round((top3 / total) * 1000) / 10;
						territoryEl.textContent = pct + '%';
					}

					// Average Rank — depuis d.avg_rank retourné par /heatmap/latest
					var avgRankEl = root.querySelector('[data-avi-hm-metric="avg-rank"]');
					if (avgRankEl) {
						if (d.avg_rank !== null && d.avg_rank !== undefined && Number(d.avg_rank) > 0) {
							var ar = Math.round(Number(d.avg_rank) * 10) / 10;
							avgRankEl.textContent = ar;
						} else {
							avgRankEl.textContent = '—';
						}
					}

					// Status — depuis d.status retourné par /heatmap/latest
					var statusEl = root.querySelector('[data-avi-hm-metric="status"]');
					if (statusEl && d.status) {
						var _statusMap = {
							completed:  { en: 'Completed',  fr: 'Terminé',    es: 'Completado', de: 'Abgeschlossen', 'pt-br': 'Concluído',   tr: 'Tamamlandı'  },
							processing: { en: 'Processing', fr: 'En cours',   es: 'En curso',   de: 'In Bearbeitung','pt-br': 'Processando', tr: 'İşleniyor'   },
							pending:    { en: 'Pending',     fr: 'En attente', es: 'Pendiente',  de: 'Ausstehend',   'pt-br': 'Pendente',    tr: 'Beklemede'   },
							failed:     { en: 'Failed',      fr: 'Échoué',     es: 'Fallido',    de: 'Fehlgeschlagen','pt-br': 'Falhou',     tr: 'Başarısız'   }
						};
						var _sKey = String(d.status).toLowerCase();
						var _sLng = getHmLang();
						var _sMap = _statusMap[_sKey];
						statusEl.textContent = _sMap ? (_sMap[_sLng] || _sMap.en) : (String(d.status).charAt(0).toUpperCase() + String(d.status).slice(1));
					}
				}
				function updateScanDateOverlay(keyword) {
					if (!root) return;
					var key = String(keyword || '').trim().toLowerCase();
					var d = scanCache[key];
					var dateStr = d && d.scan_date ? String(d.scan_date) : null;

					// Si la date provient du payload PHP initial mais le keyword
					// ne correspond pas au keyword scanné en DB, ne pas l'afficher
					if (dateStr && d && !d.scan_id) {
						// scan_id absent = données viennent du payload PHP init,
						// pas d'un fetch /heatmap/latest réel
						var kwMatch = String(d.keyword || '').trim().toLowerCase() === key;
						if (!kwMatch) dateStr = null;
					}

					// L'overlay va AVANT .avi-hm-map-wrapper, pas dedans
					var overlay = root.querySelector('[data-avi-hm-scan-date-overlay]');
					if (!overlay) {
						var mapWrapper = root.querySelector('.avi-hm-map-wrapper');
						if (!mapWrapper || !mapWrapper.parentNode) return;
						overlay = document.createElement('div');
						overlay.setAttribute('data-avi-hm-scan-date-overlay', '');
						overlay.className = 'avi-hm-scan-date-overlay';
						// Inséré AVANT la carte, pas dedans
						mapWrapper.parentNode.insertBefore(overlay, mapWrapper);
					}

					if (!dateStr) {
						overlay.style.display = 'none';
						return;
					}

					var lang = (typeof getHmLang === 'function') ? getHmLang() : 'en';
					var locale = lang === 'fr' ? 'fr-FR' : lang === 'es' ? 'es-ES' : 'en-GB';
					var label = lang === 'fr' ? 'Dernier scan'
						: lang === 'es' ? 'Último escaneo'
						: lang === 'de' ? 'Letzter Scan'
						: lang === 'pt-br' ? 'Último scan'
						: lang === 'tr' ? 'Son tarama'
						: 'Last scan';
					var formatted = dateStr;
					try {
						var dt = new Date(dateStr);
						if (!isNaN(dt.getTime())) {
							formatted = dt.toLocaleDateString(locale, {
								day: 'numeric', month: 'long', year: 'numeric'
							});
						}
					} catch(e) {}

					var prevLabel = lang === 'fr' ? 'Scan précédent'
						: lang === 'es' ? 'Escaneo anterior'
						: lang === 'de' ? 'Vorheriger Scan'
						: lang === 'pt-br' ? 'Scan anterior'
						: lang === 'tr' ? 'Önceki tarama'
						: 'Previous scan';
					var trendEntry = trendCache[key];
					var prevFormatted = '';
					if (trendEntry && trendEntry.prevScanId) {
						// On cherche la date du scan N-1 dans evolutionScans si disponible,
						// sinon on affiche juste l'ID
						var prevDate = null;
						if (typeof evolutionScans !== 'undefined' && Array.isArray(evolutionScans)) {
							var prevScanEntry = evolutionScans.filter(function(s) {
								return s.scan_id === trendEntry.prevScanId;
							})[0];
							if (prevScanEntry && prevScanEntry.scan_date) {
								prevDate = prevScanEntry.scan_date;
							}
						}
						if (prevDate) {
							try {
								var prevDt = new Date(prevDate);
								if (!isNaN(prevDt.getTime())) {
									prevFormatted = prevDt.toLocaleDateString(locale, {
										day: 'numeric', month: 'long', year: 'numeric'
									});
								}
							} catch(e) {}
						}
					}
					overlay.textContent = label + ' : ' + formatted
						+ (prevFormatted ? '  ·  ' + prevLabel + ' : ' + prevFormatted : '');
					overlay.style.display = 'block';
				}
				// Fonction 1 : récupère les points du scan N-1 pour un keyword
				function fetchPreviousScanPoints(keyword) {
					var key = String(keyword || '').trim().toLowerCase();
					if (trendCache[key]) return Promise.resolve(trendCache[key]);

					return fetch(historyUrl(keyword), {
						method: 'GET',
						credentials: 'same-origin',
						headers: restHeaders()
					})
					.then(function(r) { return r.json().catch(function() { return {}; }); })
					.then(function(data) {
						var scans = Array.isArray(data.scans) ? data.scans : [];
						// Trier par date desc, prendre le 2e (index 1 = scan N-1)
						scans.sort(function(a, b) {
							return new Date(b.scan_date || 0) - new Date(a.scan_date || 0);
						});
						var prevScan = scans.length >= 2 ? scans[1] : null;
						if (!prevScan || !prevScan.scan_id) {
							trendCache[key] = null;
							return null;
						}
						// Fetch les points du scan N-1
						return fetch(scanByIdUrl(prevScan.scan_id), {
							method: 'GET',
							credentials: 'same-origin',
							headers: restHeaders()
						})
						.then(function(r) { return r.json().catch(function() { return {}; }); })
						.then(function(scanData) {
							var pts = Array.isArray(scanData.map_points) ? scanData.map_points : [];
							var pointMap = {};
							pts.forEach(function(p) {
								if (p && p.point_id) {
									pointMap[p.point_id] = (p.rank !== null && p.rank !== undefined)
										? Number(p.rank) : null;
								}
							});
							var result = { prevScanId: prevScan.scan_id, points: pointMap };
							trendCache[key] = result;
							return result;
						});
					})
					.catch(function() {
						trendCache[key] = null;
						return null;
					});
				}

				// Fonction 2 : calcule la tendance point par point
				// currentPoints = data.map_points depuis scanCache (avec point_id)
				// prevData = résultat de fetchPreviousScanPoints (trendCache[key])
				// Retourne un objet { point_id: { delta, dir } }
				// dir = 'up' | 'down' | 'same' | 'new'
				function computeTrendData(currentPoints, prevData) {
					var result = {};
					if (!Array.isArray(currentPoints)) return result;
					var prevPoints = (prevData && prevData.points) ? prevData.points : {};
					currentPoints.forEach(function(p) {
						if (!p || !p.point_id) return;
						var pid = p.point_id;
						var currRank = (p.rank !== null && p.rank !== undefined)
							? Number(p.rank) : null;
						if (!(pid in prevPoints)) {
							result[pid] = { delta: null, dir: 'new' };
							return;
						}
						var prevRank = prevPoints[pid];
						if (currRank === null && prevRank === null) {
							result[pid] = { delta: 0, dir: 'same' };
						} else if (currRank === null) {
							result[pid] = { delta: null, dir: 'down' };
						} else if (prevRank === null) {
							result[pid] = { delta: null, dir: 'up' };
						} else {
							var delta = prevRank - currRank; // positif = amélioration
							result[pid] = {
								delta: delta,
								dir: delta > 0 ? 'up' : delta < 0 ? 'down' : 'same'
							};
						}
					});
					return result;
				}

				function renderTrendOverlay(keyword, trends) {
					if (!leafletMap) return;
					var key = String(keyword || '').trim().toLowerCase();

					// Supprime l'ancien overlay pour ce keyword
					if (trendLayers[key]) {
						try { leafletMap.removeLayer(trendLayers[key]); } catch(e) {}
					}
					if (!trends || typeof trends !== 'object') return;

					var layer = window.L.layerGroup();
					var data = scanCache[key];
					var pts = data && Array.isArray(data.map_points) ? data.map_points : [];

					pts.forEach(function(p) {
						if (!p || !p.point_id || !isFinite(Number(p.lat))
							|| !isFinite(Number(p.lng))) return;
						var trend = trends[p.point_id];
						if (!trend) return;

						var dir = trend.dir;
						var delta = trend.delta;
						var html = '';

						if (dir === 'up') {
							html = '<div class="avi-hm-trend avi-hm-trend--up" title="'
								+ (hm_t('trend_up_title') || 'Improved vs previous scan') + '">'
								+ '<span class="avi-hm-trend-arrow">↑</span>'
								+ '<span class="avi-hm-trend-val">'
								+ (delta !== null ? '+' + delta : '') + '</span>'
								+ '</div>';
						} else if (dir === 'down') {
							html = '<div class="avi-hm-trend avi-hm-trend--down" title="'
								+ (hm_t('trend_down_title') || 'Declined vs previous scan') + '">'
								+ '<span class="avi-hm-trend-arrow">↓</span>'
								+ '<span class="avi-hm-trend-val">'
								+ (delta !== null ? delta : '') + '</span>'
								+ '</div>';
						} else if (dir === 'same') {
							html = '<div class="avi-hm-trend avi-hm-trend--same" title="'
								+ (hm_t('trend_same_title') || 'Stable vs previous scan') + '">'
								+ '<span class="avi-hm-trend-arrow">=</span>'
								+ '</div>';
						} else if (dir === 'new') {
							html = '<div class="avi-hm-trend avi-hm-trend--new" title="'
								+ (hm_t('trend_new_title') || 'New scan point') + '">'
								+ '<span class="avi-hm-trend-arrow">★</span>'
								+ '</div>';
						}

						if (!html) return;

						// Badge positionné en haut à droite du cercle (offset -16px, -16px)
						var icon = window.L.divIcon({
							className: '',
							html: html,
							iconSize: [32, 20],
							iconAnchor: [-6, 26]
						});

						window.L.marker([Number(p.lat), Number(p.lng)], {
							icon: icon,
							interactive: false,
							keyboard: false,
							zIndexOffset: 500
						}).addTo(layer);
					});

					trendLayers[key] = layer;
					layer.addTo(leafletMap);

					// Légende trend — injectée dans la barre concurrents (même ligne, discrète)
					var trendLegendEl = root.querySelector('.avi-hm-trend-legend');
					if (!trendLegendEl) {
						var compBar = root.querySelector('[data-avi-heatmap-competitors-bar]');
						if (compBar) {
							trendLegendEl = document.createElement('div');
							trendLegendEl.className = 'avi-hm-trend-legend';
							trendLegendEl.innerHTML =
								'<span class="avi-hm-trend-legend-item" '
								+ 'title="' + (hm_t('trend_up_title') || 'Improved vs previous scan') + '">'
								+ '<span class="avi-hm-trend avi-hm-trend--up" '
								+ 'style="animation:none;pointer-events:none;cursor:help">↑+N</span>'
								+ '</span>'
								+ '<span class="avi-hm-trend-legend-item" '
								+ 'title="' + (hm_t('trend_down_title') || 'Declined vs previous scan') + '">'
								+ '<span class="avi-hm-trend avi-hm-trend--down" '
								+ 'style="pointer-events:none;cursor:help">↓-N</span>'
								+ '</span>'
								+ '<span class="avi-hm-trend-legend-item" '
								+ 'title="' + (hm_t('trend_same_title') || 'Stable vs previous scan') + '">'
								+ '<span class="avi-hm-trend avi-hm-trend--same" '
								+ 'style="pointer-events:none;cursor:help">=</span>'
								+ '</span>'
								+ '<span class="avi-hm-trend-legend-item" '
								+ 'title="' + (hm_t('trend_new_title') || 'New scan point') + '">'
								+ '<span class="avi-hm-trend avi-hm-trend--new" '
								+ 'style="pointer-events:none;cursor:help">★</span>'
								+ '</span>'
								+ '<span class="avi-hm-trend-legend-sep">·</span>'
								+ '<span class="avi-hm-trend-legend-label" '
								+ 'title="' + (hm_t('trend_legend_title') || 'Trend vs previous scan') + '">Δ</span>';
							compBar.appendChild(trendLegendEl);
						}
					}
				}

				// Supprime tous les overlays tendance (appelé au changement de keyword
				// ou re-rendu carte)
				function clearTrendOverlays() {
					if (!leafletMap) return;
					Object.keys(trendLayers).forEach(function(k) {
						try { leafletMap.removeLayer(trendLayers[k]); } catch(e) {}
					});
					trendLayers = {};
					trendCache = {};
				}
				function updateModalKpis(){
					var active = selectedMapKeywords.length ? selectedMapKeywords.slice(0, 5) : (selectedKeyword ? [selectedKeyword] : []);
					var rankSum = 0;
					var territorySum = 0;
					var rankCount = 0;
					var territoryCount = 0;
					active.forEach(function(keyword){
						var data = scanCache[String(keyword || '').toLowerCase()];
						var avg = data && data.avg_rank !== null && data.avg_rank !== undefined ? Number(data.avg_rank) : NaN;
						var territory = data && data.territory_pct !== null && data.territory_pct !== undefined ? Number(data.territory_pct) : NaN;
						if (isFinite(avg)) {
							rankSum += avg;
							rankCount++;
						}
						if (isFinite(territory)) {
							territorySum += territory;
							territoryCount++;
						}
					});
					var first = active.length ? active[0] : '';
					keywordLabels.forEach(function(el){
						el.textContent = first || hm_t('no_keyword');
					});
					if (modalAvgRankEl) modalAvgRankEl.textContent = rankCount ? formatMetric(rankSum / rankCount, '') : '—';
					if (modalTerritoryEl) modalTerritoryEl.textContent = territoryCount ? formatMetric(territorySum / territoryCount, '%') : '—';
					var gridLabelEl = root.querySelector('[data-avi-heatmap-modal-grid-label]');
					if (gridLabelEl && active.length) {
						var firstKw = active[0];
						var firstCache = scanCache[String(firstKw || '').toLowerCase()];
						if (firstCache && firstCache['grid_size']) {
							var firstGridSize = firstCache.grid_size;
							gridLabelEl.textContent = firstGridSize + 'x' + firstGridSize;
						}
					}
					if (modalSelectionNoteEl) {
						var activeLabel = active.length > 1
							? active.length + ' ' + (getHmLang() === 'fr' ? 'Mots-clés actifs'
								: getHmLang() === 'es' ? 'KW activos'
								: getHmLang() === 'de' ? 'Aktive Keywords'
								: getHmLang() === 'pt-br' ? 'Keywords ativas'
								: getHmLang() === 'tr' ? 'Aktif anahtar kelimeler'
								: 'Active Keywords')
							: '';
						modalSelectionNoteEl.textContent = activeLabel;
					}
					updateModalScanCostText();
				}
				function updateDrawerKeywordButtons(){
					var lowers = selectedMapKeywords.map(function(item){ return String(item || '').toLowerCase(); });
					var maxReached = selectedMapKeywords.length >= 5;
					keywordButtons.forEach(function(btn){
						var keyword = String(btn.getAttribute('data-heatmap-keyword') || '').trim();
						var index = lowers.indexOf(keyword.toLowerCase());
						var selected = index >= 0;
						btn.classList.toggle('is-selected-map', selected);
						btn.style.setProperty('--avi-heatmap-keyword-color', selected ? haloColors[index] : '#cbd5e1');
						var isDisabled = !selected && maxReached;
						btn.disabled = isDisabled;
						btn.style.opacity = isDisabled ? '0.4' : '';
						btn.style.cursor = isDisabled ? 'not-allowed' : '';
						btn.title = isDisabled
							? (getHmLang() === 'fr' ? 'Maximum 5 mots-clés sélectionnés'
								: getHmLang() === 'es' ? 'Máximo 5 palabras clave seleccionadas'
								: getHmLang() === 'de' ? 'Maximal 5 Keywords ausgewählt'
								: getHmLang() === 'pt-br' ? 'Máximo 5 palavras-chave selecionadas'
								: getHmLang() === 'tr' ? 'Maksimum 5 anahtar kelime seçildi'
								: 'Maximum 5 keywords selected')
							: '';
					});
					var counterEl = root.querySelector(
						'#avi-hm-kw-counter-' + profileId + ' .avi-hm-kw-counter-val'
					);
					if (counterEl) {
						counterEl.textContent = selectedMapKeywords.length;
					}
					refreshCreditState();
				}
				function updateIntelPanel(competitors, scanData) {
					var panelWrap = root.querySelector('[data-avi-hm-intel-panel]');
					var panel = root.querySelector('[data-avi-hm-intel-body]');
					if (!panel) return;
					if (panelWrap) {
						panelWrap.classList.toggle('has-competitors', !!(competitors && competitors.length));
					}
					if (!competitors || !competitors.length) {
						panel.innerHTML =
							'<p class="avi-hm-intel-empty">'
							+ (hm_t('intel_no_scan') ||
								'Launch a scan to see competitor analysis.')
							+ '</p>';
						return;
					}

					/* Top 5 triés par threat */
					var top5 = competitors.slice(0, 5);
					var groups = {
						high: [],
						medium: [],
						low: []
					};
					top5.forEach(function(c) {
						if (c.best_rank <= 3) {
							groups.high.push(c);
						} else if (c.best_rank <= 10) {
							groups.medium.push(c);
						} else {
							groups.low.push(c);
						}
					});

					var totalPoints = scanData && scanData.grid_size
						? scanData.grid_size * scanData.grid_size
						: 0;

					function buildGroup(label, emoji, cls, items) {
						if (!items.length) return '';
						var headerRow =
							'<div class="avi-hm-intel-row ' +
							'avi-hm-intel-row--header">' +
							'<span class="avi-hm-intel-col-pos">' +
								(hm_t('intel_col_pos') || 'Pos.') +
							'</span>' +
							'<span class="avi-hm-intel-col-name">' +
								(hm_t('intel_col_name') || 'Competitor') +
							'</span>' +
							'<span class="avi-hm-intel-col-rating">' +
								(hm_t('intel_col_rating') || '⭐ Rat.') +
							'</span>' +
							'<span class="avi-hm-intel-col-coverage">' +
								(hm_t('intel_col_coverage') || '📍 Cov.') +
							'</span>' +
							'</div>';
						var rows = items.map(function(c) {
							var pct = totalPoints > 0
								? Math.round((c.count / totalPoints) * 100)
								: 0;
							return '<div class="avi-hm-intel-row">'
								+ '<span class="avi-hm-intel-rank">'
									+ '#' + c.best_rank + '</span>'
								+ '<span class="avi-hm-intel-name">'
									+ String(c.title || '')
										.replace(/[<>&"]/g, '')
										.substring(0, 28)
									+ (c.category
										? '<em>' +
											String(c.category)
												.replace(/[<>&"]/g, '') +
											'</em>'
										: '')
								+ '</span>'
								+ '<span class="avi-hm-intel-rating">'
									+ (c.rating != null
										? '⭐' + (Math.round(c.rating * 10) / 10)
											.toFixed(1)
										: '—')
								+ '</span>'
								+ '<span class="avi-hm-intel-coverage">'
									+ pct + '%'
								+ '</span>'
							+ '</div>';
						}).join('');

						return '<details class="avi-hm-intel-group '
							+ cls + '" open>'
							+ '<summary>'
								+ emoji + ' ' + label
								+ ' (' + items.length + ')'
							+ '</summary>'
							+ headerRow
							+ rows
						+ '</details>';
					}

					panel.innerHTML =
						buildGroup(
							hm_t('threat_high') || 'High threat',
							'🔴', 'group--high', groups.high
						)
						+ buildGroup(
							hm_t('threat_medium') ||
								'Medium threat',
							'🟡', 'group--medium', groups.medium
						)
						+ buildGroup(
							hm_t('threat_low') || 'Low threat',
							'🟢', 'group--low', groups.low
						)
						+ (competitors.length > 5
							? '<div class="avi-hm-intel-footer">'
								+ '<p class="avi-hm-intel-more">'
								+ (hm_t('intel_see_all') ||
									'See all in Competitors tab')
								+ ' →</p>'
								+ '<button type="button" '
									+ 'class="avi-hm-intel-expand-btn" '
									+ 'data-avi-hm-intel-expand '
									+ 'title="' +
									(hm_t('intel_expand_tooltip') ||
										'Expand competitor panel') +
									'">⤢</button>'
								+ '</div>'
							: '<div class="avi-hm-intel-footer">'
								+ '<button type="button" '
									+ 'class="avi-hm-intel-expand-btn" '
									+ 'data-avi-hm-intel-expand '
									+ 'title="' +
									(hm_t('intel_expand_tooltip') ||
										'Expand competitor panel') +
									'">⤢</button>'
								+ '</div>');
				}
				var compTabListenersBound = false;
				var activeSorts = ['threat'];
				var currentSort = {
					col: 'threat',
					dir: 'asc'
				};
				var currentCompExportData = null;
				function renderCompetitorsTab() {
					var body = root.querySelector(
						'[data-avi-hm-comp-tab-body]'
					);
					var kwSelector = root.querySelector(
						'[data-avi-hm-comp-kw-selector]'
					);
					if (!body) return;
					/* Reset état tri au chargement de l'onglet */
					activeSorts = ['threat'];

					/* Sélecteur KW */
					var compKwState = {
						kw: selectedMapKeywords.length
							? selectedMapKeywords[0]
							: selectedKeyword
					};

					if (kwSelector) {
						if (selectedMapKeywords.length > 1) {
							kwSelector.style.display = '';
							kwSelector.innerHTML =
								selectedMapKeywords.map(
									function(kw, i) {
										return '<button type="button" '
											+ 'class="avi-hm-comp-kw-pill'
											+ (i === 0
												? ' is-active' : '')
											+ '" data-comp-kw="'
											+ kw.replace(/"/g, '&quot;')
											+ '">'
											+ String(kw).substring(0, 25)
											+ '</button>';
									}
								).join('');
							kwSelector.querySelectorAll(
								'[data-comp-kw]'
							).forEach(function(btn) {
								btn.addEventListener('click',
									function() {
										kwSelector.querySelectorAll(
											'[data-comp-kw]'
										).forEach(function(b) {
											b.classList.remove('is-active');
										});
										btn.classList.add('is-active');
										compKwState.kw = btn.getAttribute(
											'data-comp-kw'
										);
										/* renderCompetitorsTab garde ce listener KW local. */
										buildCompetitorsTable(
											compKwState.kw, body
										);
									}
								);
							});
						} else {
							kwSelector.style.display = 'none';
						}
					}

					buildCompetitorsTable(compKwState.kw, body);

					/* Boutons de tri */
					if (!compTabListenersBound) {
						compTabListenersBound = true;
						body.addEventListener('click',
							function(ev) {
								var th = ev.target.closest(
									'.sortable-col'
								);
								if (!th) return;
								var col = th.getAttribute(
									'data-sort-col'
								);
								if (currentSort.col === col) {
									currentSort.dir =
										currentSort.dir === 'asc'
											? 'desc' : 'asc';
								} else {
									currentSort.col = col;
									currentSort.dir = 'asc';
								}
								buildCompetitorsTable(
									compKwState.kw, body
								);
							}
						);
					}
				}

				function buildCompetitorsTable(keyword, body) {
					if (!body) return;
					var key = String(keyword || '')
						.trim().toLowerCase();
					var data = scanCache[key];
					var compPanelRoot = document.querySelector('[data-avi-heatmap-panel="competitors"]');
					var scanDateForExport = compPanelRoot ? (compPanelRoot.getAttribute('data-scan-date') || '') : '';
					var competitors = data &&
						Array.isArray(data.competitors)
						? data.competitors.slice()
						: [];

					if (!competitors.length) {
						currentCompExportData = null;
						body.innerHTML =
							'<p class="avi-hm-comp-tab-empty">'
							+ (hm_t('comp_tab_empty') ||
								'Launch a scan and activate competitors to see the analysis.')
							+ '</p>';
						return;
					}

					var totalPoints = data && data.grid_size
						? data.grid_size * data.grid_size : 0;

					/* Tri colonne unique */
					competitors.sort(function(a, b) {
						var col = currentSort.col;
						var dir = currentSort.dir === 'asc'
							? 1 : -1;
						var aRank = typeof a.best_rank ===
							'number' ? a.best_rank : 999;
						var bRank = typeof b.best_rank ===
							'number' ? b.best_rank : 999;
						var aCount = typeof a.count ===
							'number' ? a.count : 0;
						var bCount = typeof b.count ===
							'number' ? b.count : 0;
						var aRating = typeof a.rating ===
							'number' ? a.rating : -1;
						var bRating = typeof b.rating ===
							'number' ? b.rating : -1;
						var aReviews = typeof a.reviews_count ===
							'number' ? a.reviews_count : -1;
						var bReviews = typeof b.reviews_count ===
							'number' ? b.reviews_count : -1;

						var diff = 0;
						if (col === 'threat' || col === 'rank') {
							diff = aRank - bRank;
						} else if (col === 'coverage') {
							diff = aCount - bCount;
						} else if (col === 'rating') {
							diff = aRating - bRating;
						} else if (col === 'reviews') {
							diff = aReviews - bReviews;
						} else if (col === 'name') {
							diff = String(a.title || '')
								.localeCompare(
									String(b.title || '')
								);
						}
						return diff * dir;
					});

					/* Threat label */
					function getThreat(c) {
						if (c.best_rank <= 3) {
							return {
								emoji: '🔴',
								label: hm_t('threat_high') ||
									'High',
								cls: 'threat--high'
							};
						} else if (c.best_rank <= 10) {
							return {
								emoji: '🟡',
								label: hm_t('threat_medium') ||
									'Medium',
								cls: 'threat--medium'
							};
						}
						return {
							emoji: '🟢',
							label: hm_t('threat_low') ||
								'Low',
							cls: 'threat--low'
						};
					}

					/* Header tableau */
					function sortArrow(col) {
						if (currentSort.col !== col) {
							return '<span class="avi-hm-sort-arrow">'
								+ '↕</span>';
						}
						return '<span class="avi-hm-sort-arrow '
							+ 'is-active">'
							+ (currentSort.dir === 'asc'
								? '▲' : '▼')
							+ '</span>';
					}

					var html =
						'<table class="avi-hm-comp-table">'
						+ '<thead>'
							+ '<tr>'
								+ '<th class="col-threat '
									+ 'sortable-col" data-sort-col="threat">'
									+ (hm_t('col_threat') ||
										'Threat')
									+ sortArrow('threat')
									+ ' <span class="avi-hm-sort-tip" '
									+ 'title="'
									+ (hm_t('tip_threat') ||
										'Threat level based on best rank position')
									+ '">ℹ️</span>'
								+ '</th>'
								+ '<th class="col-rank sortable-col" '
									+ 'data-sort-col="rank">'
									+ (hm_t('intel_col_pos') ||
										'Pos.')
									+ sortArrow('rank')
									+ ' <span class="avi-hm-sort-tip" '
									+ 'title="'
									+ (hm_t('tip_rank') ||
										'Best rank position across all scanned points')
									+ '">ℹ️</span>'
								+ '</th>'
								+ '<th class="col-name sortable-col" '
									+ 'data-sort-col="name">'
									+ (hm_t('intel_col_name') ||
										'Competitor')
									+ sortArrow('name')
									+ ' <span class="avi-hm-sort-tip" title="'
									+ (hm_t('tip_competitor') ||
										'Business competing with you in local search results')
									+ '">ℹ️</span>'
								+ '</th>'
								+ '<th class="col-category">'
									+ (hm_t('col_category') ||
										'Category')
									+ ' <span class="avi-hm-sort-tip" title="'
									+ (hm_t('tip_category') ||
										'Primary Google Business Profile category')
									+ '">ℹ️</span>'
								+ '</th>'
								+ '<th class="col-rating sortable-col" '
									+ 'data-sort-col="rating">'
									+ (hm_t('intel_col_rating') ||
										'⭐ Rating')
									+ sortArrow('rating')
									+ ' <span class="avi-hm-sort-tip" '
									+ 'title="'
									+ (hm_t('tip_rating') ||
										'Average Google rating')
									+ '">ℹ️</span>'
								+ '</th>'
								+ '<th class="col-reviews sortable-col" '
									+ 'data-sort-col="reviews">'
									+ (hm_t('col_reviews') ||
										'💬 Reviews')
									+ sortArrow('reviews')
									+ ' <span class="avi-hm-sort-tip" '
									+ 'title="'
									+ (hm_t('tip_reviews') ||
										'Total number of Google reviews')
									+ '">ℹ️</span>'
								+ '</th>'
								+ '<th class="col-coverage sortable-col" '
									+ 'data-sort-col="coverage">'
									+ (hm_t('intel_col_coverage') ||
										'📍 Coverage')
									+ sortArrow('coverage')
									+ ' <span class="avi-hm-sort-tip" '
									+ 'title="'
									+ (hm_t('tip_coverage') ||
										'Percentage of scanned points where this competitor appears')
									+ '">ℹ️</span>'
								+ '</th>'
								+ '<th class="col-gbp">GBP'
									+ ' <span class="avi-hm-sort-tip" title="'
									+ (hm_t('tip_gbp') ||
										'Click to open the Google Business Profile')
									+ '">ℹ️</span>'
								+ '</th>'
							+ '</tr>'
						+ '</thead>'
						+ '<tbody>';

					competitors.forEach(function(c) {
						var threat = getThreat(c);
						var pct = totalPoints > 0
							? Math.round(
								(c.count / totalPoints) * 100
							)
							: 0;
						var gbpUrl = c.place_id
							? 'https://www.google.com/maps/'
								+ 'place/?q=place_id:'
								+ encodeURIComponent(
									String(c.place_id)
								)
							: null;

						html += '<tr class="avi-hm-comp-row '
							+ threat.cls + '">'
							+ '<td class="col-threat">'
								+ '<span class="avi-hm-threat-badge '
									+ threat.cls + '">'
									+ threat.emoji + ' '
									+ threat.label
								+ '</span>'
							+ '</td>'
							+ '<td class="col-rank">'
								+ '<strong>#' + c.best_rank
								+ '</strong>'
							+ '</td>'
							+ '<td class="col-name">'
								+ '<span class="comp-name">'
									+ String(c.title || '')
										.replace(/[<>&"]/g, '')
								+ '</span>'
							+ '</td>'
							+ '<td class="col-category">'
								+ (c.category
									? '<em class="comp-cat">'
										+ String(c.category)
											.replace(/[<>&"]/g, '')
										+ '</em>'
									: '<span class="comp-null">—</span>')
							+ '</td>'
							+ '<td class="col-rating">'
								+ (c.rating != null
									? '⭐ ' + (Math.round(
										c.rating * 10) / 10
									).toFixed(1)
									: '<span class="comp-null">—</span>')
							+ '</td>'
							+ '<td class="col-reviews">'
								+ (c.reviews_count != null
									? c.reviews_count
									: '<span class="comp-null">—</span>')
							+ '</td>'
							+ '<td class="col-coverage">'
								+ '<span class="comp-cov-pct">'
									+ pct + '%</span>'
								+ '<span class="comp-cov-pts">'
									+ ' (' + c.count + '/'
									+ totalPoints + ')'
								+ '</span>'
							+ '</td>'
							+ '<td class="col-gbp">'
								+ (gbpUrl
									? '<button type="button" '
										+ 'class="avi-hm-comp-gbp-btn" '
										+ 'data-gbp-url="'
										+ gbpUrl.replace(/"/g, '&quot;')
										+ '">↗</button>'
									: '—')
							+ '</td>'
						+ '</tr>';
					});

					currentCompExportData = {
						keyword: key,
						scanDate: scanDateForExport,
						rows: competitors.slice()
					};

					html += '</tbody></table>';
					body.innerHTML = html;
				}

				function exportCompetitorsToExcel() {
					if (typeof XLSX === 'undefined') {
						alert('Export library not loaded, please refresh the page.');
						return;
					}
					if (!currentCompExportData ||
						!currentCompExportData.rows ||
						!currentCompExportData.rows.length) {
						alert('No data to export.');
						return;
					}
					var data = scanCache[currentCompExportData.keyword] || {};
					var totalPoints = data && data.grid_size
						? data.grid_size * data.grid_size : 0;
					var headers = ['Threat', 'Position', 'Competitor',
						'Category', 'Rating', 'Reviews', 'Coverage'];
					var rows = currentCompExportData.rows.map(function(c) {
						var threatLabel = c.best_rank <= 3
							? (hm_t('threat_high') || 'High')
							: c.best_rank <= 10
							? (hm_t('threat_medium') || 'Medium')
							: (hm_t('threat_low') || 'Low');
						return [
							threatLabel,
							c.best_rank != null ? c.best_rank : '',
							String(c.title || ''),
							c.category || '',
							c.rating != null
								? Math.round(c.rating * 10) / 10 : '',
							c.reviews_count != null
								? c.reviews_count : '',
							String(c.count || 0) + '/' + totalPoints
						];
					});
					var ws = XLSX.utils.aoa_to_sheet([headers].concat(rows));
					var wb = XLSX.utils.book_new();
					XLSX.utils.book_append_sheet(wb, ws, 'Competitors');
					var filename = 'competitor-analysis-' +
						(currentCompExportData.keyword || 'export')
							.replace(/[^a-z0-9]/gi, '-') + '.xlsx';
					XLSX.writeFile(wb, filename);
				}

				function exportCompetitorsToPDF() {
					if (typeof jspdf === 'undefined' ||
						!window.jspdf || !window.jspdf.jsPDF) {
						alert('Export library not loaded, please refresh the page.');
						return;
					}
					if (!currentCompExportData ||
						!currentCompExportData.rows ||
						!currentCompExportData.rows.length) {
						alert('No data to export.');
						return;
					}
					var jsPDFCtor = window.jspdf.jsPDF;
					var doc = new jsPDFCtor({
						orientation: 'landscape',
						unit: 'pt',
						format: 'a4'
					});
					doc.setFontSize(14);
					doc.setFont(undefined, 'bold');
					doc.text('Competitor Analysis', 40, 32);
					doc.setFontSize(10);
					doc.setFont(undefined, 'normal');
					doc.setTextColor(100);
					doc.text(
						'Keyword: ' +
						(currentCompExportData.keyword || '-') +
						'   |   Scan date: ' +
						(currentCompExportData.scanDate || '-'),
						40,
						48
					);
					doc.setTextColor(0);
					var data = scanCache[currentCompExportData.keyword] || {};
					var totalPoints = data && data.grid_size
						? data.grid_size * data.grid_size : 0;
					var headers = ['Threat', 'Position', 'Competitor',
						'Category', 'Rating', 'Reviews', 'Coverage'];
					var rows = currentCompExportData.rows.map(function(c) {
						var threatLabel = c.best_rank <= 3
							? (hm_t('threat_high') || 'High')
							: c.best_rank <= 10
							? (hm_t('threat_medium') || 'Medium')
							: (hm_t('threat_low') || 'Low');
						return [
							threatLabel,
							c.best_rank != null ? c.best_rank : '',
							String(c.title || ''),
							c.category || '',
							c.rating != null
								? Math.round(c.rating * 10) / 10 : '',
							c.reviews_count != null
								? c.reviews_count : '',
							String(c.count || 0) + '/' + totalPoints
						];
					});
					doc.autoTable({
						head: [headers],
						body: rows,
						startY: 60,
						styles: { fontSize: 9 },
						headStyles: {
							fillColor: [40, 40, 40],
							textColor: 255
						},
						margin: { left: 40, right: 40 }
					});
					var filename = 'competitor-analysis-' +
						(currentCompExportData.keyword || 'export')
							.replace(/[^a-z0-9]/gi, '-') + '.pdf';
					doc.save(filename);
				}
				function enableOpenMapButton(){
					var openMapBtn = root.querySelector('.avi-hm-open-map-btn');
					if (openMapBtn) {
						openMapBtn.removeAttribute('disabled');
						openMapBtn.classList.remove('is-disabled');
						openMapBtn.setAttribute('data-avi-heatmap-modal-open', '');
						var hintEl = openMapBtn.querySelector('.avi-hm-open-map-hint');
						if (hintEl) {
							hintEl.remove();
						}
					}
				}
				function updateModalKeywordButtons(){
					modalKeywordButtons.forEach(function(btn){
						var keyword = String(btn.getAttribute('data-avi-heatmap-modal-keyword') || '').trim();
						var index = selectedMapKeywords.map(function(item){ return item.toLowerCase(); }).indexOf(keyword.toLowerCase());
						var selected = index >= 0;
						btn.classList.toggle('is-selected', selected);
						btn.disabled = !selected && selectedMapKeywords.length >= 5;
						btn.style.setProperty('--avi-heatmap-keyword-color', selected ? haloColors[index] : '#cbd5e1');
					});
				}
				function fetchScanForKeyword(keyword){
					var key = String(keyword || '').trim().toLowerCase();
					if (!key) return Promise.resolve(null);
					// Anti-régression #Sprint43-fetchScan-guard
					// Cache valide = map_points ET competitors présents
					// Empêche retour cache vide après modification backend status=completed
					// Garde-fou obligatoire : toute modif backend doit vérifier ce guard
					if (scanCache[key]
						&& Array.isArray(scanCache[key].map_points)
						&& scanCache[key].map_points.length > 0
						&& Array.isArray(scanCache[key].competitors)
						&& scanCache[key].competitors.length > 0) {
						return Promise.resolve(scanCache[key]);
					}

					return fetch(latestUrl(keyword), {
						method: 'GET',
						credentials: 'same-origin',
						headers: restHeaders()
					}).then(function(response){
						return response.json().catch(function(){ return {}; }).then(function(data){
							if (!response.ok) return null;
							data = data || {};
							data.map_points = Array.isArray(data.map_points) ? data.map_points : [];
							data.competitors = Array.isArray(data.competitors)
								? data.competitors : [];
							scanCache[key] = data;
							updateCoverageBadge(keyword);
							updateDrawerMetrics(keyword);
							updateScanDateOverlay(keyword);
							fetchPreviousScanPoints(keyword).then(function(prevData) {
								var key = String(keyword || '').trim().toLowerCase();
								var currentData = scanCache[key];
								if (!currentData || !Array.isArray(currentData.map_points)) return;
								var trends = computeTrendData(currentData.map_points, prevData);
								scanCache[key].trendData = trends;
								// renderTrendOverlay appelé depuis renderSelectedMap() après init carte
							});
							return data;
						});
					}).catch(function(){
						return null;
					});
				}
				function initLeafletMap(){
					if (!mapContainer) return false;
					if (!hasCenter()) {
						setMapOverlay(hm_t('center_not_found') || 'Geographic center not found for this profile.', true);
						return false;
					}
					if (!window.L) {
						setMapOverlay(hm_t('leaflet_not_loaded') || 'Map unavailable: Leaflet is not loaded.', true);
						return false;
					}
					if (!leafletMap) {
						leafletRenderer = window.L.canvas({ padding: 0.5 });
						leafletMap = window.L.map(mapContainer, {
							renderer: leafletRenderer,
							zoomControl: true
						});
						window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
							maxZoom: 19,
							attribution: '&copy; OpenStreetMap contributors'
						}).addTo(leafletMap);
						leafletLayer = window.L.layerGroup().addTo(leafletMap);
						leafletMap.setView([Number(config.center.lat), Number(config.center.lng)], 13);
					}
					requestAnimationFrame(function(){
						leafletMap.invalidateSize();
						setTimeout(function(){ leafletMap.invalidateSize(); }, 100);
					});
					return true;
				}
				function haloRadius(zone, data){
					var radiusKm = Number((data && data.radius_km) || config.default_radius_km || 5);
					var gridSize = Number((data && data.grid_size) || config.default_grid_size || 7);
					var spacing = gridSize > 1 ? (radiusKm * 1000 * 2) / (gridSize - 1) : radiusKm * 1000;
					if (zone === 'top3') return spacing * 0.58;
					if (zone === 'found') return spacing * 0.43;
					if (zone === 'weak') return spacing * 0.30;
					return 0;
				}
				function haloOpacity(zone){
					if (zone === 'top3') return 0.28;
					if (zone === 'found') return 0.20;
					if (zone === 'weak') return 0.13;
					return 0;
				}
				function metersToPixels(latlng, meters) {
					if (!leafletMap) return 0;
					var p1 = leafletMap.latLngToContainerPoint(latlng);
					var latOffset = meters / 111320;
					var p2 = leafletMap.latLngToContainerPoint([latlng.lat + latOffset, latlng.lng]);
					return Math.abs(p1.y - p2.y);
				}
				function heatmapProjectedSpacingPx(latlng, spacingMeters) {
					if (!leafletMap) return 0;
					return metersToPixels(latlng, spacingMeters);
				}
				function heatmapRingRadiusPx(latlng, spacingMeters, ratio, minPx, maxPx) {
					var spacingPx = heatmapProjectedSpacingPx(latlng, spacingMeters);
					if (!spacingPx) return minPx;
					var capped = Math.floor(spacingPx * ratio);
					return Math.max(minPx, Math.min(maxPx, capped));
				}
				function findHeatmapHoverTarget(e) {
					var mouse = leafletMap.latLngToContainerPoint(e.latlng);
					var best = null;
					var bestScore = Infinity;
					heatmapHitTargets.forEach(function(target) {
						var center = leafletMap.latLngToContainerPoint(target.latlng);
						var distance = mouse.distanceTo(center);
						if (target.type === 'center') {
							if (distance <= target.radiusPx && distance < bestScore) {
								best = target;
								bestScore = distance;
							}
							return;
						}
						if (target.type === 'ring') {
							var ringPx = target.radiusPx || 0;
							var tolerance = 10;
							var score = Math.abs(distance - ringPx);
							if (score <= tolerance && score < bestScore) {
								best = target;
								bestScore = score;
							}
						}
					});
					return best;
				}
				function bindHeatmapMouseTracking() {
					if (!leafletMap || leafletMap._aviHeatmapMouseTrackingBound) return;
					leafletMap._aviHeatmapMouseTrackingBound = true;
					if (!heatmapHoverPopup) {
						heatmapHoverPopup = window.L.popup({ closeButton: false, offset: [0, -8] });
					}
					leafletMap.on('mousemove', function(e) {
						if (heatmapCompetitorPopupOpen) return;
						var target = findHeatmapHoverTarget(e);
						if (!target) {
							heatmapHoverKey = '';
							if (heatmapHoverPopup) heatmapHoverPopup.remove();
							return;
						}
						var key = target.keyword + '|' + target.rankText + '|' + target.type + '|' +
							target.latlng.lat + '|' + target.latlng.lng;
						if (key !== heatmapHoverKey) {
							heatmapHoverKey = key;
							if (heatmapHoverPopup) heatmapHoverPopup.setContent(heatmapPopupHtml(target));
						}
						if (heatmapHoverPopup) heatmapHoverPopup.setLatLng(e.latlng).openOn(leafletMap);
					});
					leafletMap.on('mouseout', function() {
						heatmapHoverKey = '';
						if (heatmapHoverPopup) heatmapHoverPopup.remove();
					});
				}
				function bindHeatmapZoomRefresh(){
					if (!leafletMap || leafletMap._aviHeatmapZoomRefreshBound) return;
					leafletMap._aviHeatmapZoomRefreshBound = true;
					if (!leafletMap._aviHeatmapZoomRefreshHandler) {
						leafletMap._aviHeatmapZoomRefreshHandler = function(){
							heatmapIsZoomRefresh = true;
							renderSelectedMap();
						};
						leafletMap.on('zoomend', leafletMap._aviHeatmapZoomRefreshHandler);
					}
				}
				function renderMapPoints(keyword, haloColor, kwIndex, totalKwOverride){
					return fetchScanForKeyword(keyword).then(function(data){
						var points = data && Array.isArray(data.map_points) ? data.map_points : [];
						var rendered = [];
						var hitTargets = [];
						points.forEach(function(point){
							if (!validPoint(point)) return;
							var zone = normalizeZone(point);
							var bgColor;
							if (zone === 'top3')    bgColor = '#16a34a';
							if (zone === 'found')   bgColor = '#84cc16';
							if (zone === 'weak')    bgColor = '#f97316';
							if (zone === 'missing') bgColor = '#64748b';
							if (!bgColor) bgColor = '#64748b';
							var radius = 16;
							var radiusKm = Number((data && data.radius_km) || config.default_radius_km || 5);
							var gridSize = Number((data && data.grid_size) || config.default_grid_size || 7);
							var spacing = gridSize > 1 ? (radiusKm * 1000 * 2) / (gridSize - 1) : radiusKm * 1000;
							var ring1Radius = spacing * 0.30;
							var ring2Radius = spacing * 0.48;
							if (ring1Radius < 80) ring1Radius = 80;
							if (ring2Radius < 140) ring2Radius = 140;
							var totalKw = totalKwOverride !== undefined ? Number(totalKwOverride) : selectedMapKeywords.length;
							var latlng = window.L.latLng(Number(point.lat), Number(point.lng));
							var circleMarkerFactory = window.L.circleMarker;
							var divIconFactory = window.L.divIcon;
							var tooltipContent = '<div style="font-size:.75rem;font-weight:800;color:#0f172a;padding:2px 4px">' +
								(point.rank !== null && point.rank !== undefined ? '#' + point.rank : '>20') +
								'<br><span style="color:#64748b;font-weight:600">' +
								String(keyword || '').replace(/[<>&]/g, '') + '</span></div>';
							var ring1Px = Math.round(radius * 2.0);
							var ring2Px = Math.round(radius * 3.0);
							if (totalKw >= 2 && kwIndex === 1) {
								var ring1 = circleMarkerFactory(
									[Number(point.lat), Number(point.lng)],
									{
										renderer: leafletRenderer,
										radius: ring1Px,
										color: bgColor,
										weight: 3,
										opacity: 0.85,
										fillColor: bgColor,
										fillOpacity: 0,
										interactive: false
									}
								);
								ring1.addTo(leafletLayer);
							}
							if (totalKw >= 3 && kwIndex === 2) {
								var ring2 = circleMarkerFactory(
									[Number(point.lat), Number(point.lng)],
									{
										renderer: leafletRenderer,
										radius: ring2Px,
										color: bgColor,
										weight: 2.5,
										opacity: 0.75,
										fillColor: bgColor,
										fillOpacity: 0,
										interactive: false
									}
								);
								ring2.addTo(leafletLayer);
							}
							if (totalKw >= 4 && kwIndex === 3) {
								var ring3Px = Math.round(radius * 4.0);
								var ring3 = circleMarkerFactory(
									[Number(point.lat), Number(point.lng)],
									{
										renderer: leafletRenderer,
										radius: ring3Px,
										color: bgColor,
										weight: 2.5,
										opacity: 0.65,
										fillColor: bgColor,
										fillOpacity: 0,
										interactive: false
									}
								);
								ring3.addTo(leafletLayer);
							}
							if (totalKw >= 5 && kwIndex === 4) {
								var ring4Px = Math.round(radius * 5.0);
								var ring4 = circleMarkerFactory(
									[Number(point.lat), Number(point.lng)],
									{
										renderer: leafletRenderer,
										radius: ring4Px,
										color: bgColor,
										weight: 2.5,
										opacity: 0.55,
										fillColor: bgColor,
										fillOpacity: 0,
										interactive: false
									}
								);
								ring4.addTo(leafletLayer);
							}
							var circle = circleMarkerFactory(
								[Number(point.lat), Number(point.lng)],
								{
									renderer: leafletRenderer,
									radius: radius,
									color: '#fff',
									weight: 2,
									opacity: 1,
									fillColor: (bgColor),
									fillOpacity: 1
								}
							);
							circle.addTo(leafletLayer);
							if (kwIndex === 0 || typeof kwIndex === 'undefined') {
								var labelText = point.rank !== null && point.rank !== undefined
									? String(point.rank) : '>20';
								var fontSize = labelText.length > 2 ? '9px' : '11px';
								var kwIdx = typeof kwIndex !== 'undefined' ? kwIndex : 0;
								var offsetX = kwIdx === 0 ? 0 : (kwIdx === 1 ? -8 : 8);
								var offsetY = kwIdx === 0 ? 0 : (kwIdx === 1 ? -8 : -8);
								var divLabel = window.L.marker(
									[Number(point.lat), Number(point.lng)],
									{
										interactive: false,
										icon: divIconFactory({
											className: '',
											html: '<span style="display:flex;align-items:center;' +
												'justify-content:center;width:100%;height:100%;' +
												'font-size:' + fontSize + ';font-weight:900;' +
												'color:#fff;pointer-events:none;' +
												'text-shadow:0 1px 2px rgba(0,0,0,0.4);">' +
												labelText + '</span>',
											iconSize: [radius * 2, radius * 2],
											iconAnchor: [radius - offsetX, radius - offsetY]
										})
									}
								);
								divLabel.addTo(leafletLayer);
							}
							var rankText0 = point.rank !== null && point.rank !== undefined
								? '#' + point.rank : '>20';
							if (kwIndex === 0 || typeof kwIndex === 'undefined') {
								hitTargets.push({
									type: 'center', keyword: keyword, rankText: rankText0,
									bgColor: bgColor, latlng: latlng, radiusPx: radius
								});
							}
							if (kwIndex === 1) {
								hitTargets.push({
									type: 'ring', keyword: keyword, rankText: rankText0,
									bgColor: bgColor, latlng: latlng, radiusPx: ring1Px
								});
							}
							if (kwIndex === 2) {
								hitTargets.push({
									type: 'ring', keyword: keyword, rankText: rankText0,
									bgColor: bgColor, latlng: latlng, radiusPx: ring2Px
								});
							}
							if (kwIndex === 3) {
								var ring3PxHit = Math.round(radius * 4.0);
								hitTargets.push({
									type: 'ring', keyword: keyword, rankText: rankText0,
									bgColor: bgColor, latlng: latlng, radiusPx: ring3PxHit
								});
							}
							if (kwIndex === 4) {
								var ring4PxHit = Math.round(radius * 5.0);
								hitTargets.push({
									type: 'ring', keyword: keyword, rankText: rankText0,
									bgColor: bgColor, latlng: latlng, radiusPx: ring4PxHit
								});
							}
							rendered.push([Number(point.lat), Number(point.lng)]);
						});
						return { keyword: keyword, data: data, points: rendered, hitTargets: hitTargets };
					});
				}
				function renderCompetitors(competitors, scanData){
					// Anti-régression #Sprint43-Frontend5-bataille
					// Nouvelle logique : 1 marqueur par concurrent par point de grille
					// Colorisation selon rang concurrent vs rang profil sur CE point précis
					// Labels : DANGER / Menace / En lutte / Repoussé
					// Autorisé Anthony — Sprint 43
					competitorMarkers.forEach(function(m){
						if (leafletLayer) leafletLayer.removeLayer(m);
						if (leafletLayer && m._aviCompLabel) leafletLayer.removeLayer(m._aviCompLabel);
					});
					competitorMarkers = [];
					// Anti-régression #Sprint43-pids-reset-no-zoom
					// Vider _activeCompPids uniquement hors zoom (changement keyword)
					// Au zoom : conserver les pids pour restauration pins après clearLayers
					if (!heatmapIsZoomRefresh) {
						_activeCompPids = {};
					}
					if (!leafletMap || !leafletLayer) return;
					if (!scanData || !Array.isArray(scanData.map_points) || !scanData.map_points.length) return;

					// Anti-régression #Sprint43-C12-compindex-full
					// Index concurrents : competitors[] globaux + tous competitors_at_point
					// Permet d'afficher DANGER/Menace même si absents de la liste agrégée
					// Les faux positifs sont exclus par catégorie/distance dans _aggregate_competitors
					var compIndex = {};
					if (Array.isArray(competitors)) {
						competitors.forEach(function(c){ if (c && c.place_id) compIndex[String(c.place_id)] = c; });
					}
					// Anti-régression #Sprint43-C12-compindex-coords
					// Enrichir compIndex avec competitors_at_point
					// Stocker lat_center/lng_center depuis comp.lat/lng GBP réel
					if (scanData && Array.isArray(scanData.map_points)) {
						scanData.map_points.forEach(function(point){
							if (!point || !Array.isArray(point.competitors_at_point)) return;
							point.competitors_at_point.forEach(function(comp){
								if (!comp || !comp.place_id) return;
								var pid = String(comp.place_id);
								if (!compIndex[pid]) {
									// Créer entrée avec lat_center/lng_center depuis coordonnées GBP
									compIndex[pid] = {
										place_id: pid,
										title: comp.title || '',
										category: comp.category || null,
										rating: comp.rating || null,
										reviews_count: comp.reviews_count || null,
										best_rank: comp.rank || null,
										count: 1,
										lat_center: comp.lat || null,
										lng_center: comp.lng || null,
										dist_km: null
									};
								}
							});
						});
					}

					// Palette bataille — _battleStyle au scope module (§16 + sidebar §23)

					// Anti-régression #Sprint43-Bug-one-pin
					// 1 pin par concurrent = meilleur point sur la grille (rank le plus bas)
					// Position = point.lat/lng du meilleur point scané
					// Autorisé Anthony — Sprint 43
					var _bestByPid = {};
					_lastBestByPid = {};
					_allPointsByPid = {}; // Anti-régression #Sprint43-C12-zone-reset
					// Anti-régression #Sprint43-C12-zone-cleanup-array
					// Nettoyage compatible tableaux de layers (multi-polygones)
					Object.keys(_compZoneLayers).forEach(function(pid){
						var layers = _compZoneLayers[pid];
						if (!leafletMap || !layers) return;
						(Array.isArray(layers) ? layers : [layers]).forEach(function(l){ leafletMap.removeLayer(l); });
					});
					_compZoneLayers = {};
					if (!heatmapIsZoomRefresh) { _activeCompZones = {}; }
					scanData.map_points.forEach(function(point){
						if (!point || !Array.isArray(point.competitors_at_point)) return;
						var profileRank = (point.rank != null && point.rank <= 20) ? point.rank : null;
						point.competitors_at_point.forEach(function(comp){
							if (!comp) return;
							var pid = String(comp.place_id || '');
							if (!pid || !compIndex[pid]) return;
							var compRank = (comp.rank != null && comp.rank <= 20) ? comp.rank : null;
							// Stocker tous les points pour polygone zone
							if (!_allPointsByPid[pid]) _allPointsByPid[pid] = [];
							_allPointsByPid[pid].push({ lat: Number(point.lat), lng: Number(point.lng), compRank: compRank, profileRank: profileRank });
							if (!_bestByPid[pid]) {
								_bestByPid[pid] = { point: point, comp: comp, profileRank: profileRank, compRank: compRank };
							} else {
								// Garder le point avec le meilleur rank concurrent
								var prev = _bestByPid[pid];
								var prevRank = prev.compRank != null ? prev.compRank : 999;
								var curRank = compRank != null ? compRank : 999;
								if (curRank < prevRank) {
									_bestByPid[pid] = { point: point, comp: comp, profileRank: profileRank, compRank: compRank };
								}
							}
						});
					});

					_lastBestByPid = _bestByPid; // Anti-régression #Sprint43-C12-bestbypid-store
					var centerLat = config.center ? Number(config.center.lat) : null;
					var centerLng = config.center ? Number(config.center.lng) : null;
					var lang = getHmLang();
					Object.keys(_bestByPid).forEach(function(pid){
						var best = _bestByPid[pid];
						var point = best.point;
						var comp = best.comp;
						var profileRank = best.profileRank;
						var compRank = best.compRank;
						var style = _battleStyle(profileRank, compRank);
						if (!style) return;
						// Anti-régression #Sprint43-C12-pin-gbp-coords
						// Utiliser lat/lng GBP réel du concurrent depuis competitors_at_point
						// best.comp.lat/lng = position réelle de l'établissement (DataForSEO)
						// Position unique par business → pas de superposition
						var lat = Number(best.comp.lat != null ? best.comp.lat : NaN);
						var lng = Number(best.comp.lng != null ? best.comp.lng : NaN);
						if (isNaN(lat) || isNaN(lng)) return;
						if (centerLat != null && centerLng != null) {
							var _dLat = (lat - centerLat) * Math.PI / 180;
							var _dLng = (lng - centerLng) * Math.PI / 180;
							var _a = Math.sin(_dLat/2)*Math.sin(_dLat/2) + Math.cos(centerLat*Math.PI/180)*Math.cos(lat*Math.PI/180)*Math.sin(_dLng/2)*Math.sin(_dLng/2);
							var _distKm = 6371 * 2 * Math.atan2(Math.sqrt(_a), Math.sqrt(1-_a));
							if (_distKm > 50) return;
						}
						var rkTxt = compRank != null ? String(compRank) : '>20';
						var pulseClass = style.pulse === 'fast' ? 'avi-comp-pulse-fast' : style.pulse === 'slow' ? 'avi-comp-pulse-slow' : '';
						// Anti-régression #Sprint43-C12-pin-grad-unique — id gradient SVG unique par place_id complet
						var _gradId = 'avi-pin-grad-' + pid.replace(/[^a-zA-Z0-9]/g, '_');
						var _colorMap = {
							'#dc2626': ['#ef4444', '#991b1b'],
							'#f97316': ['#fb923c', '#c2410c'],
							'#f59e0b': ['#fbbf24', '#b45309'],
							'#94a3b8': ['#cbd5e1', '#475569']
						};
						var _cols = _colorMap[style.color] || [style.color, style.color];
						var _pulseAnim = '';
						if (style.pulse === 'fast') {
							_pulseAnim = '<animateTransform attributeName="transform" type="scale" values="1;1.22;1" dur="0.8s" repeatCount="3" additive="sum" transformOrigin="14 40"/>' +
								'<animate attributeName="opacity" values="1;0.6;1" dur="0.8s" repeatCount="3"/>';
						} else if (style.pulse === 'slow') {
							_pulseAnim = '<animateTransform attributeName="transform" type="scale" values="1;1.14;1" dur="1.4s" repeatCount="3" additive="sum" transformOrigin="14 40"/>' +
								'<animate attributeName="opacity" values="1;0.7;1" dur="1.4s" repeatCount="3"/>';
						}
						var pinSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 36 48" width="36" height="48">' +
							'<defs><linearGradient id="' + _gradId + '" x1="0%" y1="0%" x2="0%" y2="100%">' +
							'<stop offset="0%" style="stop-color:' + _cols[0] + ';stop-opacity:1"/>' +
							'<stop offset="100%" style="stop-color:' + _cols[1] + ';stop-opacity:1"/>' +
							'</linearGradient></defs>' +
							'<g>' + _pulseAnim +
							'<path d="M14 0 C6.3 0 0 6.3 0 14 C0 24.5 14 40 14 40 C14 40 28 24.5 28 14 C28 6.3 21.7 0 14 0Z" fill="url(#' + _gradId + ')" stroke="rgba(0,0,0,0.15)" stroke-width="1.5"/>' +
							'<circle cx="14" cy="13" r="7.5" fill="rgba(255,255,255,0.97)"/>' +
							'<text x="14" y="17" text-anchor="middle" font-size="' + (rkTxt.length > 2 ? '6' : rkTxt.length > 1 ? '7' : '9') + '" font-weight="900" fill="' + _cols[1] + '" font-family="Arial,sans-serif">' + rkTxt + '</text>' +
							'</g></svg>';
						// Anti-régression #Sprint43-C12-pin-transform-fix
						// Pulse sur wrapper interne — JAMAIS sur className divIcon (Leaflet positionne via transform)
						var pinHtml = '<div class="avi-comp-pin-wrap' + (pulseClass ? ' ' + pulseClass : '') + '">' + pinSvg + '</div>';
						var marker = window.L.marker([lat, lng], {
							icon: window.L.divIcon({
								className: 'avi-comp-pin',
								html: pinHtml,
								iconSize: [28, 40],
								iconAnchor: [14, 40],
								popupAnchor: [0, -40]
							}),
							zIndexOffset: 500
						});
						var globalComp = compIndex[pid] || {};
						var title = String(comp.title || globalComp.title || hm_t('competitor') || 'Concurrent').replace(/[<>&"]/g, '');
						var category = String(comp.category || globalComp.category || '').replace(/[<>&"]/g, '');
						var rating = comp.rating != null ? comp.rating : (globalComp.rating != null ? globalComp.rating : null);
						var reviews = comp.reviews_count != null ? comp.reviews_count : (globalComp.reviews_count != null ? globalComp.reviews_count : null);
						var rankLabel = compRank != null ? '#' + compRank : '>20';
						var profRankLabel = profileRank != null ? '#' + profileRank : '>20';
						var gbpUrl = pid ? 'https://www.google.com/maps/place/?q=place_id:' + encodeURIComponent(pid) : null;
						var ratingHtml = rating != null ? '<div class="avi-hm-popup__comp-rating">⭐ ' + (Math.round(rating * 10) / 10).toFixed(1) + (reviews != null ? ' (' + reviews + ' ' + (hm_t('reviews_label') || 'avis') + ')' : '') + '</div>' : '';
						var categoryHtml = category ? '<div class="avi-hm-popup__comp-category"><span class="avi-hm-popup__cat-icon">🏷️</span><span class="avi-hm-popup__cat-label">' + category + '</span></div>' : '';
						var gbpBtnHtml = gbpUrl ? '<button type="button" class="avi-hm-popup__gbp-btn" data-gbp-url="' + gbpUrl.replace(/"/g, '&quot;') + '">' + (hm_t('view_on_google') || 'View on Google') + ' ↗</button>' : '';
						// Anti-régression #Sprint43-C12-popup-keyword
						// Popup enrichi avec keyword actif pour contextualiser la menace
						var _activeKw = selectedMapKeywords.length ? selectedMapKeywords[0] : (selectedKeyword || '');
						marker.bindPopup(
							'<div class="avi-hm-popup avi-hm-popup--competitor">' +
							(_activeKw ? '<div class="avi-hm-popup__comp-keyword">🔑 ' + String(_activeKw).replace(/[<>]/g,'') + '</div>' : '') +
							'<div class="avi-hm-popup__comp-threat ' + style.cls + '">' +
							'<span class="avi-hm-popup__threat-dot">' + style.label + '</span>' +
							'<span class="avi-hm-popup__comp-rank-pill">' + rankLabel + '</span>' +
							'<span class="avi-hm-popup__prof-rank-pill">' + (lang === 'fr' ? 'Vous : ' : lang === 'es' ? 'Usted: ' : lang === 'de' ? 'Sie: ' : lang === 'pt-br' ? 'Você: ' : lang === 'tr' ? 'Siz: ' : 'You: ') + profRankLabel + '</span>' +
							'</div>' +
							'<div class="avi-hm-popup__comp-name">' + title + '</div>' +
							ratingHtml + categoryHtml +
							(gbpBtnHtml ? '<div class="avi-hm-popup__comp-footer">' + gbpBtnHtml + '</div>' : '') +
							'</div>',
							{ autoClose: false, closeOnClick: false, keepInView: true }
						);
						marker.on('click', function(){
							heatmapCompetitorPopupOpen = true;
							if (heatmapHoverPopup) heatmapHoverPopup.remove();
							heatmapHoverKey = '';
							this.openPopup();
						});
						marker.on('popupclose', function(){ heatmapCompetitorPopupOpen = false; });
						var compLabel = null;
						marker._aviCompLabel = compLabel;
						marker._aviPlaceId = pid;
						competitorMarkers.push(marker);
					});
				}
				// Anti-régression #Sprint43-C12-convex-hull
				// Convex hull Graham scan — calcule polygone minimal englobant les points grille
				function _convexHull(points) {
					if (points.length < 3) return points;
					var pts = points.slice().sort(function(a,b){ return a.lng !== b.lng ? a.lng-b.lng : a.lat-b.lat; });
					function cross(O,A,B){ return (A.lng-O.lng)*(B.lat-O.lat)-(A.lat-O.lat)*(B.lng-O.lng); }
					var lower = [];
					for (var i=0;i<pts.length;i++){
						while(lower.length>=2 && cross(lower[lower.length-2],lower[lower.length-1],pts[i])<=0) lower.pop();
						lower.push(pts[i]);
					}
					var upper = [];
					for (var i=pts.length-1;i>=0;i--){
						while(upper.length>=2 && cross(upper[upper.length-2],upper[upper.length-1],pts[i])<=0) upper.pop();
						upper.push(pts[i]);
					}
					lower.pop(); upper.pop();
					return lower.concat(upper);
				}

				// Anti-régression #Sprint43-C12-renderCompZone-multi
				// Multi-polygones par cluster de points menaçants
				// Label géographique par cluster (Nord/Sud/Est/Ouest/Centre)
				// Autorisé Anthony — Sprint 43
				function renderCompZone(pid, show) {
					if (!leafletMap) return;
					if (_compZoneLayers[pid]) {
						if (Array.isArray(_compZoneLayers[pid])) {
							_compZoneLayers[pid].forEach(function(l){ leafletMap.removeLayer(l); });
						} else {
							leafletMap.removeLayer(_compZoneLayers[pid]);
						}
						delete _compZoneLayers[pid];
					}
					if (!show) return;
					var pts = _allPointsByPid[pid];
					if (!pts || !pts.length) return;
					var threatPts = pts.filter(function(p){
						var profAbsent = (p.profileRank == null || p.profileRank > 20);
						var compAbsent = (p.compRank == null || p.compRank > 20);
						if (compAbsent) return false;
						if (profAbsent) return true;
						return p.compRank <= p.profileRank;
					});
					if (!threatPts.length) return;

					// Couleur selon meilleur rank menaçant
					var bestRank = 999;
					threatPts.forEach(function(p){ if(p.compRank != null && p.compRank < bestRank) bestRank = p.compRank; });
					var color = bestRank <= 3 ? '#dc2626' : bestRank <= 10 ? '#f97316' : '#f59e0b';

					// Centre de la grille
					var allPts = pts;
					var cLat = allPts.reduce(function(s,p){return s+p.lat;},0)/allPts.length;
					var cLng = allPts.reduce(function(s,p){return s+p.lng;},0)/allPts.length;
					var gridSpan = 0.009; // ~700m espacement grille Pau

					// Haversine distance
					function _d(a,b){
						var R=6371000,dLat=(b.lat-a.lat)*Math.PI/180,dLng=(b.lng-a.lng)*Math.PI/180;
						var x=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(a.lat*Math.PI/180)*Math.cos(b.lat*Math.PI/180)*Math.sin(dLng/2)*Math.sin(dLng/2);
						return R*2*Math.atan2(Math.sqrt(x),Math.sqrt(1-x));
					}

					// Clustering simple par proximité
					var threshold = gridSpan * 1.5 * 111000; // ~1050m
					var clusters = [];
					var assigned = new Array(threatPts.length).fill(false);
					for (var i=0;i<threatPts.length;i++){
						if (assigned[i]) continue;
						var cluster = [threatPts[i]];
						assigned[i] = true;
						for (var j=i+1;j<threatPts.length;j++){
							if (!assigned[j] && _d(threatPts[i],threatPts[j]) < threshold) {
								cluster.push(threatPts[j]);
								assigned[j] = true;
							}
						}
						clusters.push(cluster);
					}

					// Label géographique selon position du cluster vs centre grille
					function _geoLabel(cluster, lang) {
						var clat = cluster.reduce(function(s,p){return s+p.lat;},0)/cluster.length;
						var clng = cluster.reduce(function(s,p){return s+p.lng;},0)/cluster.length;
						var dLat = clat - cLat;
						var dLng = clng - cLng;
						var threshold = gridSpan * 0.4;
						var isN = dLat > threshold, isS = dLat < -threshold;
						var isE = dLng > threshold, isW = dLng < -threshold;
						var labels = {
							fr: { N:'Nord', S:'Sud', E:'Est', W:'Ouest', C:'Centre', NE:'Nord-Est', NW:'Nord-Ouest', SE:'Sud-Est', SW:'Sud-Ouest' },
							es: { N:'Norte', S:'Sur', E:'Este', W:'Oeste', C:'Centro', NE:'Noreste', NW:'Noroeste', SE:'Sureste', SW:'Suroeste' },
							en: { N:'North', S:'South', E:'East', W:'West', C:'Centre', NE:'North-East', NW:'North-West', SE:'South-East', SW:'South-West' }
						};
						var L = labels[lang] || labels.en;
						if (!isN && !isS && !isE && !isW) return L.C;
						if (isN && isE) return L.NE;
						if (isN && isW) return L.NW;
						if (isS && isE) return L.SE;
						if (isS && isW) return L.SW;
						if (isN) return L.N;
						if (isS) return L.S;
						if (isE) return L.E;
						return L.W;
					}

					var lang = getHmLang();
					var layers = [];

					clusters.forEach(function(cluster){
						var hull = _convexHull(cluster);
						var latlngs;
						if (hull.length >= 3) {
							latlngs = hull.map(function(p){ return [p.lat, p.lng]; });
						} else if (hull.length === 2) {
							// 2 points → petit losange
							var mid = { lat:(hull[0].lat+hull[1].lat)/2, lng:(hull[0].lng+hull[1].lng)/2 };
							var d = gridSpan * 0.3;
							latlngs = [[hull[0].lat,hull[0].lng],[mid.lat+d,mid.lng],[hull[1].lat,hull[1].lng],[mid.lat-d,mid.lng]];
						} else {
							// 1 point → petit carré
							var p = hull[0]; var d = gridSpan * 0.35;
							latlngs = [[p.lat+d,p.lng-d],[p.lat+d,p.lng+d],[p.lat-d,p.lng+d],[p.lat-d,p.lng-d]];
						}
						var poly = window.L.polygon(latlngs, {
							color: color, weight: 2, opacity: 0.85,
							dashArray: '6 3', fillColor: color, fillOpacity: 0.12,
							interactive: false
						});
						poly.addTo(leafletMap);

						// Label géographique
						var label = _geoLabel(cluster, lang);
						var centLat = cluster.reduce(function(s,p){return s+p.lat;},0)/cluster.length;
						var centLng = cluster.reduce(function(s,p){return s+p.lng;},0)/cluster.length;
						var tooltip = window.L.tooltip({
							permanent: true, direction: 'center',
							className: 'avi-comp-zone-label'
						}).setContent(label).setLatLng([centLat, centLng]);
						tooltip.addTo(leafletMap);

						layers.push(poly);
						layers.push(tooltip);
					});

					_compZoneLayers[pid] = layers;
				}
				// Anti-régression #Sprint43-C12-renderBattleSidebar — §23
				// Sidebar bataille concurrents — 320px flottant gauche
				// Dépendances : _activeCompPids (§17), competitorMarkers (§16)
				// Ne jamais appeler sans competitors[] valide
				function renderBattleSidebar(sidebar, competitors, scanData) {
					if (!sidebar || !Array.isArray(competitors)) return;
					var lang = getHmLang();
					var centerLat = config.center ? Number(config.center.lat) : null;
					var centerLng = config.center ? Number(config.center.lng) : null;

					// Anti-régression #Sprint43-C12-level-battlestyle
					// Niveau basé sur _battleStyle (profileRank vs compRank) — cohérent avec pins carte
					function _level(c) {
						var pid = String(c.place_id || '');
						var best = _lastBestByPid[pid];
						var profileRank = best ? best.profileRank : null;
						var compRank = best ? best.compRank : (c.best_rank != null ? int(c.best_rank) : null);
						var style = _battleStyle(profileRank, compRank);
						var _lvlL = getHmLang();
						var _lvlMap = {
							repousse: { en: '✅ Repelled', fr: '✅ Repoussé', es: '✅ Repelido', de: '✅ Zurückgedrängt', 'pt-br': '✅ Repelido', tr: '✅ Geri püskürtüldü' },
							danger:   { en: '⚔️ DANGER',  fr: '⚔️ DANGER',  es: '⚔️ PELIGRO', de: '⚔️ GEFAHR',        'pt-br': '⚔️ PERIGO',  tr: '⚔️ TEHLİKE' },
							menace:   { en: '⚠️ Threat',  fr: '⚠️ Menace',  es: '⚠️ Amenaza', de: '⚠️ Bedrohung',     'pt-br': '⚠️ Ameaça',  tr: '⚠️ Tehdit' },
							lutte:    { en: '🔴 Fighting', fr: '🔴 En lutte', es: '🔴 En pugna', de: '🔴 Im Kampf',     'pt-br': '🔴 Em disputa', tr: '🔴 Mücadelede' }
						};
						var _lvlG = function(k) { return (_lvlMap[k][_lvlL] || _lvlMap[k].en); };
						if (!style) style = { label: _lvlG('repousse'), color: '#94a3b8' };
						if (style.cls === 'threat--danger')   return { key: 'danger',   label: _lvlG('danger'),   color: '#dc2626', bg: '#fef2f2' };
						if (style.cls === 'threat--menace')   return { key: 'menace',   label: _lvlG('menace'),   color: '#f97316', bg: '#fff7ed' };
						if (style.cls === 'threat--lutte')    return { key: 'lutte',    label: _lvlG('lutte'),    color: '#f59e0b', bg: '#fffbeb' };
						return { key: 'repousse', label: _lvlG('repousse'), color: '#94a3b8', bg: '#f8fafc' };
					}
					function int(v) { try { return parseInt(v) || 999; } catch(e) { return 999; } }

					// Distance Haversine
					function _dist(lat, lng) {
						if (centerLat == null || lat == null) return null;
						var R = 6371;
						var dLat = (lat - centerLat) * Math.PI / 180;
						var dLng = (lng - centerLng) * Math.PI / 180;
						var a = Math.sin(dLat/2)*Math.sin(dLat/2) +
							Math.cos(centerLat*Math.PI/180)*Math.cos(lat*Math.PI/180)*
							Math.sin(dLng/2)*Math.sin(dLng/2);
						return Math.round(R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a))*10)/10;
					}

					// État filtre courant
					var _currentFilter = sidebar._bsFilter || 'level';
					var _searchVal = sidebar._bsSearch || '';

					function _buildSidebar() {
						var _lvlL = getHmLang();
						var _lvlMap = {
							repousse: { en: '✅ Repelled', fr: '✅ Repoussé', es: '✅ Repelido', de: '✅ Zurückgedrängt', 'pt-br': '✅ Repelido', tr: '✅ Geri püskürtüldü' },
							danger:   { en: '⚔️ DANGER',  fr: '⚔️ DANGER',  es: '⚔️ PELIGRO', de: '⚔️ GEFAHR',        'pt-br': '⚔️ PERIGO',  tr: '⚔️ TEHLİKE' },
							menace:   { en: '⚠️ Threat',  fr: '⚠️ Menace',  es: '⚠️ Amenaza', de: '⚠️ Bedrohung',     'pt-br': '⚠️ Ameaça',  tr: '⚠️ Tehdit' },
							lutte:    { en: '🔴 Fighting', fr: '🔴 En lutte', es: '🔴 En pugna', de: '🔴 Im Kampf',     'pt-br': '🔴 Em disputa', tr: '🔴 Mücadelede' }
						};
						var _lvlG = function(k) { return (_lvlMap[k][_lvlL] || _lvlMap[k].en); };
						// Enrichir competitors avec dist + level
						var enriched = competitors.map(function(c) {
							var lat = c.lat_center != null ? Number(c.lat_center) : null;
							var lng = c.lng_center != null ? Number(c.lng_center) : null;
							return Object.assign({}, c, {
								_dist: _dist(lat, lng),
								_level: _level(c),
								_lat: lat,
								_lng: lng
							});
						});

						// Filtre recherche
						if (_searchVal) {
							var sv = _searchVal.toLowerCase();
							enriched = enriched.filter(function(c) {
								return String(c.title || '').toLowerCase().indexOf(sv) >= 0;
							});
						}

						// Tri
						if (_currentFilter === 'level') {
							var order = {danger:0, menace:1, lutte:2, repousse:3};
							enriched.sort(function(a,b){ return (order[a._level.key]||3)-(order[b._level.key]||3) || (a._dist||999)-(b._dist||999); });
						} else if (_currentFilter === 'distance') {
							enriched.sort(function(a,b){ return (a._dist||999)-(b._dist||999); });
						} else if (_currentFilter === 'az') {
							enriched.sort(function(a,b){ return String(a.title||'').localeCompare(String(b.title||'')); });
						} else if (_currentFilter === 'coverage') {
							enriched.sort(function(a,b){ return (b.count||0)-(a.count||0); });
						}

						// HTML
						var totalPoints = scanData && scanData.grid_size ? scanData.grid_size * scanData.grid_size : 0;
						var filterLabels = {
							level:    lang==='fr' ? 'Niveau' : lang==='es' ? 'Nivel' : lang==='de' ? 'Stufe' : lang==='pt-br' ? 'Nível' : lang==='tr' ? 'Seviye' : 'Level',
							distance: lang==='fr' ? 'Distance' : lang==='de' ? 'Entfernung' : lang==='pt-br' ? 'Distância' : lang==='tr' ? 'Mesafe' : 'Distance',
							az:       'A-Z',
							coverage: lang==='fr' ? 'Couverture' : lang==='es' ? 'Cobertura' : lang==='de' ? 'Abdeckung' : lang==='pt-br' ? 'Cobertura' : lang==='tr' ? 'Kapsama' : 'Coverage'
						};
						var titleText = lang==='fr' ? '⚔️ Bataille' : lang==='es' ? '⚔️ Batalla' : lang==='de' ? '⚔️ Kampf' : lang==='pt-br' ? '⚔️ Batalha' : lang==='tr' ? '⚔️ Savaş' : '⚔️ Battle';
						var searchPh  = lang==='fr' ? 'Rechercher...' : lang==='es' ? 'Buscar...' : lang==='de' ? 'Suchen...' : lang==='pt-br' ? 'Buscar...' : lang==='tr' ? 'Ara...' : 'Search...';
						var showAllTx = lang==='fr' ? '👁 Tout afficher' : lang==='es' ? '👁 Mostrar todo' : lang==='de' ? '👁 Alle anzeigen' : lang==='pt-br' ? '👁 Mostrar todos' : lang==='tr' ? '👁 Tümünü göster' : '👁 Show all';
						var hideAllTx = lang==='fr' ? '🙈 Tout masquer' : lang==='es' ? '🙈 Ocultar todo' : lang==='de' ? '🙈 Alle ausblenden' : lang==='pt-br' ? '🙈 Ocultar todos' : lang==='tr' ? '🙈 Tümünü gizle' : '🙈 Hide all';

						// Groupes pour filtre level
						var groups = _currentFilter === 'level'
							? [{key:'danger',label:_lvlG('danger'),color:'#dc2626'},{key:'menace',label:_lvlG('menace'),color:'#f97316'},{key:'lutte',label:_lvlG('lutte'),color:'#f59e0b'},{key:'repousse',label:_lvlG('repousse'),color:'#94a3b8'}]
							: null;

						// Anti-régression #Sprint43-C12-sidebar-kw-note
						var _activeKw = selectedMapKeywords.length ? selectedMapKeywords[0] : (selectedKeyword || '');
						var listHtml = '';
						if (groups) {
							groups.forEach(function(g) {
								var items = enriched.filter(function(c){ return c._level.key === g.key; });
								if (!items.length) return;
								var gId = 'bs-group-' + g.key;
								var isOpen = sidebar['_group_' + g.key] !== false;
								listHtml += '<div class="avi-hm-bs-group-header" data-bs-group="' + g.key + '" style="cursor:pointer">' +
									'<span class="avi-hm-bs-group-count" style="background:' + g.color + '">' + items.length + '</span>' +
									g.label +
									'<span style="margin-left:auto;font-size:.8rem">' + (isOpen ? '▾' : '▸') + '</span>' +
									'</div>';
								if (isOpen) {
									listHtml += '<div data-bs-group-items="' + g.key + '">';
									items.forEach(function(c){ listHtml += _itemHtml(c, totalPoints); });
									listHtml += '</div>';
								}
							});
						} else {
							enriched.forEach(function(c){ listHtml += _itemHtml(c, totalPoints); });
						}

						function _itemHtml(c, totalPoints) {
							var pid = String(c.place_id || '');
							var isActive = !!_activeCompPids[pid];
							var dist = c._dist != null ? c._dist + ' km' : '—';
							var rank = c.best_rank != null ? '#' + c.best_rank : '>20';
							var rating = c.rating != null ? '★ ' + (Math.round(c.rating*10)/10).toFixed(1) : '';
							var cov = totalPoints > 0 ? Math.round((c.count/totalPoints)*100) + '%' : '';
							var isZoneActive = !!_activeCompZones[pid];
							return '<div class="avi-hm-bs-item' + (isActive ? ' is-active' : '') + '" data-bs-pid="' + pid + '">' +
								'<span class="avi-hm-bs-badge" style="background:' + c._level.color + '"></span>' +
								'<span class="avi-hm-bs-name" title="' + String(c.title||'').replace(/"/g,'') + '">' + String(c.title||'').replace(/[<>]/g,'') + '</span>' +
								'<span class="avi-hm-bs-meta">' +
								'<span class="avi-hm-bs-rank">' + rank + '</span>' +
								'<span class="avi-hm-bs-dist">' + dist + '</span>' +
								(rating ? '<span class="avi-hm-bs-rating">' + rating + '</span>' : '') +
								(cov ? '<span class="avi-hm-bs-dist">' + cov + '</span>' : '') +
								'<button type="button" class="avi-hm-bs-zone-btn' + (isZoneActive ? ' is-active' : '') + '" data-bs-zone-pid="' + pid + '" title="' + (lang==='fr'?'Afficher zone':'Show zone') + '" style="border:none;background:' + (isZoneActive ? c._level.color : 'transparent') + ';color:' + (isZoneActive ? '#fff' : c._level.color) + ';border-radius:4px;padding:2px 5px;font-size:.65rem;cursor:pointer;margin-left:4px;border:1px solid ' + c._level.color + '">⬡</button>' +
								'</span></div>';
						}

						// Anti-régression #Sprint43-C12-zone-modal-btn
						sidebar.innerHTML =
							'<div class="avi-hm-bs-header">' +
							'<span class="avi-hm-bs-title">' + titleText + '<span class="avi-hm-bs-count">' + enriched.length + '</span></span>' +
							// Anti-régression #Sprint43-C12-zone-btn-info
							'<button type="button" class="avi-hm-bs-zone-info" data-bs-zone-info title="' + (hm_t('zone_help')||'Territory zones') + '">⬡ <span style="font-size:.6rem;vertical-align:super;opacity:.8">ℹ</span></button>' +
							'<button type="button" class="avi-hm-bs-close" data-bs-close>×</button>' +
							'</div>' +
							'<div class="avi-hm-bs-search"><input type="text" placeholder="' + searchPh + '" value="' + _searchVal.replace(/"/g,'') + '" data-bs-search/></div>' +
							'<div class="avi-hm-bs-filters">' +
							Object.keys(filterLabels).map(function(k){
								return '<button type="button" class="avi-hm-bs-filter-btn' + (k===_currentFilter?' is-active':'') + '" data-bs-filter="' + k + '">' + filterLabels[k] + '</button>';
							}).join('') +
							'</div>' +
							'<div class="avi-hm-bs-kw-note">' +
							'🔑 ' + (lang==='fr' ? 'Mot-clé actif : ' : lang==='es' ? 'Palabra clave: ' : lang==='de' ? 'Aktives Keyword: ' : lang==='pt-br' ? 'Keyword ativa: ' : lang==='tr' ? 'Aktif anahtar kelime: ' : 'Active keyword: ') +
							'<strong>' + String(_activeKw).replace(/[<>]/g,'') + '</strong>' +
							'</div>' +
							'<div class="avi-hm-bs-list">' + (listHtml || '<p style="padding:12px;color:#94a3b8;font-size:.74rem">' + (lang==='fr'?'Aucun résultat':'No results') + '</p>') + '</div>' +
							'<div class="avi-hm-bs-footer">' +
							'<button type="button" class="avi-hm-bs-footer-btn" data-bs-show-all>' + showAllTx + '</button>' +
							'<button type="button" class="avi-hm-bs-footer-btn" data-bs-hide-all>' + hideAllTx + '</button>' +
							'</div>';

						// Events
						var closeBtn = sidebar.querySelector('[data-bs-close]');
						if (closeBtn) closeBtn.addEventListener('click', function(){
							// Anti-régression #Sprint43-C12-sidebar-x-close — §23
							// Bouton × : ferme sidebar + reset pins
							sidebar.setAttribute('hidden','');
							competitorMarkers.forEach(function(m){ leafletLayer.removeLayer(m); if(m._aviCompLabel) leafletLayer.removeLayer(m._aviCompLabel); });
							_activeCompPids = {};
							var _tb = root ? root.querySelector('[data-avi-hm-competitors-toggle]') : null;
							if (_tb) { _tb.setAttribute('data-competitors-visible','0'); _tb.classList.remove('is-active'); _tb.textContent = (hm_t('show_competitors')||'Show competitors') + ' (' + competitors.length + ')'; }
						});

						sidebar.addEventListener('click', function(e){
							if (e.target.closest('[data-bs-zone-info]')) {
								// Anti-régression #Sprint43-C12-zone-modal
								// Anti-régression #Sprint43-C12-zone-modal-open
								openHmPedagoModal('hm_comp_zone');
							}
						});

						var searchInput = sidebar.querySelector('[data-bs-search]');
						if (searchInput) searchInput.addEventListener('input', function(){
							sidebar._bsSearch = this.value;
							_buildSidebar();
						});

						sidebar.querySelectorAll('[data-bs-filter]').forEach(function(btn){
							btn.addEventListener('click', function(){
								sidebar._bsFilter = btn.getAttribute('data-bs-filter');
								_currentFilter = sidebar._bsFilter;
								_buildSidebar();
							});
						});

						sidebar.querySelectorAll('.avi-hm-bs-item').forEach(function(item){
							item.addEventListener('click', function(){
								var pid = item.getAttribute('data-bs-pid');
								if (!pid) return;
								var isActive = item.classList.contains('is-active');
								if (isActive) {
									delete _activeCompPids[pid];
									item.classList.remove('is-active');
									competitorMarkers.forEach(function(m){ if(String(m._aviPlaceId||'')===pid){ leafletLayer.removeLayer(m); if(m._aviCompLabel) leafletLayer.removeLayer(m._aviCompLabel); } });
								} else {
									_activeCompPids[pid] = true;
									item.classList.add('is-active');
									competitorMarkers.forEach(function(m){ if(String(m._aviPlaceId||'')===pid){ m.addTo(leafletLayer); if(m._aviCompLabel) m._aviCompLabel.addTo(leafletLayer); } });
								}
							});
						});

						// Anti-régression #Sprint43-C12-zone-toggle
						sidebar.querySelectorAll('[data-bs-zone-pid]').forEach(function(btn){
							btn.addEventListener('click', function(e){
								e.stopPropagation();
								// Anti-régression #Sprint43-C12-zone-scroll
								// Préserver position scroll sidebar avant rebuild
								var _list = sidebar.querySelector('.avi-hm-bs-list');
								var _scrollTop = _list ? _list.scrollTop : 0;
								var zpid = btn.getAttribute('data-bs-zone-pid');
								if (_activeCompZones[zpid]) {
									delete _activeCompZones[zpid];
									renderCompZone(zpid, false);
								} else {
									_activeCompZones[zpid] = true;
									renderCompZone(zpid, true);
								}
								_buildSidebar();
								// Restaurer scroll après rebuild DOM
								var _listAfter = sidebar.querySelector('.avi-hm-bs-list');
								if (_listAfter) _listAfter.scrollTop = _scrollTop;
							});
						});

						// Anti-régression #Sprint43-C12-group-collapse — groupes collapsibles
						sidebar.querySelectorAll('[data-bs-group]').forEach(function(hdr){
							hdr.addEventListener('click', function(){
								var key = hdr.getAttribute('data-bs-group');
								sidebar['_group_' + key] = sidebar['_group_' + key] === false ? true : false;
								_buildSidebar();
							});
						});

						var showAllBtn = sidebar.querySelector('[data-bs-show-all]');
						if (showAllBtn) showAllBtn.addEventListener('click', function(){
							competitorMarkers.forEach(function(m){ m.addTo(leafletLayer); if(m._aviCompLabel) m._aviCompLabel.addTo(leafletLayer); _activeCompPids[String(m._aviPlaceId||'')] = true; });
							_buildSidebar();
						});

						var hideAllBtn = sidebar.querySelector('[data-bs-hide-all]');
						if (hideAllBtn) hideAllBtn.addEventListener('click', function(){
							competitorMarkers.forEach(function(m){ leafletLayer.removeLayer(m); if(m._aviCompLabel) leafletLayer.removeLayer(m._aviCompLabel); });
							_activeCompPids = {};
							_buildSidebar();
						});
					}

					sidebar._bsFilter = sidebar._bsFilter || 'level';
					sidebar._bsSearch = sidebar._bsSearch || '';
					_currentFilter = sidebar._bsFilter;
					_buildSidebar();
				}
				function renderSelectedMap(){
					updateModalKeywordButtons();
					updateDrawerKeywordButtons();
					updateModalKpis();
					if (!initLeafletMap() || !leafletLayer) return;
					clearTrendOverlays();
					leafletLayer.clearLayers();
					heatmapHitTargets = [];
					setMapOverlay('', false);
					var keywords = selectedMapKeywords.slice(0, 5);
					if (!keywords.length) {
						leafletMap.setView([Number(config.center.lat), Number(config.center.lng)], 13);
						setMapOverlay(hm_t('select_kw_hint') || 'Select up to 5 keywords to display local halos.', false);
						return;
					}
					// Anti-régression #Sprint43-fetchMissing
					// Fetch parallèle des keywords non encore en cache avant rendu
					// Évite que les keywords cochés sans cache soient ignorés silencieusement
					// Autorisé Anthony — Sprint 43
					var kwMissing = keywords.filter(function(kw) {
						var key = String(kw || '').trim().toLowerCase();
						return !scanCache[key]
							|| !Array.isArray(scanCache[key].map_points)
							|| scanCache[key].map_points.length === 0;
					});
					if (kwMissing.length > 0) {
						Promise.all(kwMissing.map(function(kw) {
							return fetchScanForKeyword(kw);
						})).then(function() {
							renderSelectedMap();
						});
						return;
					}
					var kwWithData = keywords.filter(function(kw) {
						var key = String(kw || '').trim().toLowerCase();
						return scanCache[key]
							&& Array.isArray(scanCache[key].map_points)
							&& scanCache[key].map_points.length > 0;
					});

					// Anti-régression #Sprint43-C2-refDate — suppression filtre _refDate global
					// Raison : latest_scan_date_global = MAX(scan_date) en DB tire des scans
					// partiels (1 point, status='completed' corrompu) qui écartent les scans
					// complets d'une date antérieure. fetchScanForKeyword() gère déjà chaque
					// keyword indépendamment avec son propre dernier scan — filtre global
					// redondant et destructif. Supprimé Sprint 43 — autorisé Anthony.

					var totalKwReady = kwWithData.length || 1;
					var keywordsToRender = kwWithData.length ? kwWithData : [];
					var keywordsReversed = keywordsToRender.slice().reverse();
					Promise.all(keywordsReversed.map(function(keyword) {
						var index = keywords.indexOf(keyword);
						return renderMapPoints(keyword, haloColors[index], index, totalKwReady);
					})).then(function(results){
						// Si aucun keyword à afficher (tous filtrés) → nettoyer la carte
						if (!results || results.length === 0) {
							clearTrendOverlays();
							if (leafletLayer) leafletLayer.clearLayers();
							heatmapHitTargets = [];
							return;
						}
						updateModalKpis();
						var bounds = [];
						var missing = [];
						results.forEach(function(result){
							if (result.points && result.points.length) {
								bounds = bounds.concat(result.points);
							} else {
								missing.push(result.keyword);
							}
						});
						heatmapHitTargets = [];
						results.forEach(function(result) {
							if (result.hitTargets && result.hitTargets.length) {
								heatmapHitTargets = heatmapHitTargets.concat(result.hitTargets);
							}
						});
						bindHeatmapMouseTracking();
						bindHeatmapZoomRefresh();
						if (modalMissingEl) {
							modalMissingEl.textContent = missing.length ? hm_t('scan_missing') + ' ' + missing.join(', ') : '';
						}
						if (!heatmapIsZoomRefresh) {
							if (bounds.length) {
								var gridSizeZ = Number((results[0] && results[0].data && results[0].data.grid_size)
									|| config.default_grid_size || 7);
								var zoomLevelZ = gridSizeZ <= 3 ? 14 : gridSizeZ <= 7 ? 13 : gridSizeZ <= 13 ? 12 : 11;
								leafletMap.setView(
									[Number(config.center.lat), Number(config.center.lng)],
									zoomLevelZ
								);
							} else {
								leafletMap.setView(
									[Number(config.center.lat), Number(config.center.lng)],
									13
								);
								if (isScanInProgress) {
									setMapOverlay(
										getHmLang() === 'fr' ? '⏳ Scan en cours...'
											: getHmLang() === 'es' ? '⏳ Escaneo en curso...'
											: getHmLang() === 'de' ? '⏳ Scan läuft...'
											: getHmLang() === 'pt-br' ? '⏳ Scan em andamento...'
											: getHmLang() === 'tr' ? '⏳ Tarama devam ediyor...'
											: '⏳ Scanning in progress...',
										false
									);
								} else {
									setMapOverlay(hm_t('launch_first_scan'), false);
								}
							}
						}
						if (!isScanInProgress) {
							setMapOverlay('', false);
						}
						/* Concurrents */
						var firstKeyword = selectedMapKeywords.length
							? selectedMapKeywords[0] : selectedKeyword;
						var firstData = firstKeyword
							? scanCache[String(firstKeyword).toLowerCase()]
							: null;
						var competitors = firstData && Array.isArray(firstData.competitors)
							? firstData.competitors : [];
						// Anti-régression #Sprint43-Frontend5-zoom-restore
						// Sauvegarde place_id actifs avant clearLayers (zoom)
						// Restauration automatique après renderCompetitors
						var _activePids = Object.assign({}, _activeCompPids);
						var renderCompetitorsFn = renderCompetitors;
						renderCompetitorsFn(competitors, firstData);
						// Anti-régression #Sprint43-zoom-restore-split
						// Restauration marqueurs IMMÉDIATEMENT après renderCompetitors
						// Restauration boutons APRÈS reconstruction DOM (voir bloc ci-dessous)
						if (Object.keys(_activePids).length) {
							competitorMarkers.forEach(function(m){
								if (m._aviPlaceId && _activePids[String(m._aviPlaceId)]) {
									m.addTo(leafletLayer);
									if (m._aviCompLabel) m._aviCompLabel.addTo(leafletLayer);
								}
							});
						}
						// Anti-régression #Sprint43-C12-zone-zoom-restore — §9
						// Restaurer polygones zones après clearLayers/zoom
						Object.keys(_activeCompZones).forEach(function(pid){
							if (_activeCompZones[pid]) renderCompZone(pid, true);
						});

						var competitorsBar = root.querySelector(
							'[data-avi-heatmap-competitors-bar]');
						var competitorsButtonsEl = root.querySelector(
							'[data-avi-heatmap-competitors-buttons]');
						if (!competitorsBar) {
							competitorsBar = document.querySelector('[data-avi-heatmap-competitors-bar]');
						}
						if (!competitorsButtonsEl) {
							competitorsButtonsEl = document.querySelector('[data-avi-heatmap-competitors-buttons]');
						}
						if (competitorsBar && competitorsButtonsEl) {
							// Anti-régression #Sprint43-zoom-state-restore
							// Sauvegarder état volet avant innerHTML='' (zoom redraw)
							var _prevToggle = competitorsButtonsEl.querySelector('[data-avi-hm-competitors-toggle]');
							var _wasVisible = _prevToggle ? _prevToggle.getAttribute('data-competitors-visible') === '1' : false;
							competitorsButtonsEl.innerHTML = '';
							if (competitors.length) {
								var toggleCompBtn = document.createElement('button');
								toggleCompBtn.type = 'button';
								toggleCompBtn.className = 'avi-hm-competitors-toggle' + (_wasVisible ? ' is-active' : '');
								toggleCompBtn.setAttribute('data-avi-hm-competitors-toggle', '');
								toggleCompBtn.textContent = _wasVisible
									? (hm_t('hide_competitors') || 'Hide competitors') + ' (' + competitors.length + ')'
									: (hm_t('show_competitors') || 'Show competitors') + ' (' + competitors.length + ')';
								toggleCompBtn.setAttribute('data-competitors-visible', _wasVisible ? '1' : '0');

								var individualBtnsWrap = document.createElement('div');
								individualBtnsWrap.className = 'avi-hm-competitors-individual';
								// Anti-régression #Sprint43-C12-hide-individual — sidebar remplace boutons individuels
								individualBtnsWrap.style.display = 'none';

								toggleCompBtn.addEventListener('click', function(){
									// Anti-régression #Sprint43-Frontend5-toggle-global
									// Show competitors → affiche boutons individuels UNIQUEMENT
									// Les marqueurs s'ajoutent via boutons individuels seulement
									// Hide competitors → masque tout + remet boutons inactifs
									var visible = toggleCompBtn.getAttribute('data-competitors-visible') === '1';
									if (visible) {
										// Anti-régression #Sprint43-C12-sidebar-close
										// Ferme la sidebar + masque tous les marqueurs
										competitorMarkers.forEach(function(m){
											leafletLayer.removeLayer(m);
											if (m._aviCompLabel) leafletLayer.removeLayer(m._aviCompLabel);
										});
										_activeCompPids = {};
										// Anti-régression #Sprint43-C12-layout-close — §23
										var _sidebar = root ? root.querySelector('[data-avi-hm-battle-sidebar]') : null;
										if (_sidebar) _sidebar.setAttribute('hidden', '');
										toggleCompBtn.setAttribute('data-competitors-visible', '0');
										toggleCompBtn.textContent = (hm_t('show_competitors') || 'Show competitors') + ' (' + competitors.length + ')';
										toggleCompBtn.classList.remove('is-active');
									} else {
										// Anti-régression #Sprint43-C12-sidebar-open
										// Ouvre la sidebar bataille au lieu de individualBtnsWrap
										toggleCompBtn.setAttribute('data-competitors-visible', '1');
										toggleCompBtn.textContent = (hm_t('hide_competitors') || 'Hide competitors') + ' (' + competitors.length + ')';
										toggleCompBtn.classList.add('is-active');
										// Anti-régression #Sprint43-C12-layout-open — §23
										var _sidebar = root ? root.querySelector('[data-avi-hm-battle-sidebar]') : null;
										if (_sidebar) {
											renderBattleSidebar(_sidebar, competitors, firstData);
											_sidebar.removeAttribute('hidden');
										}
									}
								});

								competitorsButtonsEl.appendChild(toggleCompBtn);
								competitorsButtonsEl.appendChild(individualBtnsWrap);

								// Anti-régression #Sprint43-Frontend5-toggle
								// Toggle individuel par place_id — compatible architecture multi-points
								// Par défaut : boutons inactifs, marqueurs masqués
								// Autorisé Anthony — Sprint 43
								competitors.forEach(function(c){
									var pid = String(c.place_id || '');
									var btn = document.createElement('button');
									btn.type = 'button';
									btn.className = 'avi-heatmap-competitors-btn';
									btn.textContent = String(c.title ||
										hm_t('competitor')).substring(0, 30);
									btn.setAttribute('data-competitor-pid', pid);
									btn.addEventListener('click', function(){
										btn.classList.toggle('is-active');
										var isActive = btn.classList.contains('is-active');
										if (isActive) { _activeCompPids[pid] = true; } else { delete _activeCompPids[pid]; }
										competitorMarkers.forEach(function(m){
											if (String(m._aviPlaceId || '') !== pid) return;
											if (isActive) {
												m.addTo(leafletLayer);
												if (m._aviCompLabel) m._aviCompLabel.addTo(leafletLayer);
											} else {
												leafletLayer.removeLayer(m);
												if (m._aviCompLabel) leafletLayer.removeLayer(m._aviCompLabel);
											}
										});
									});
									individualBtnsWrap.appendChild(btn);
								});
								// Anti-régression #Sprint43-zoom-restore-btns
								// Restauration boutons individuels APRÈS reconstruction DOM
								if (Object.keys(_activePids).length) {
									individualBtnsWrap.querySelectorAll('[data-competitor-pid]').forEach(function(btn){
										var pid = btn.getAttribute('data-competitor-pid');
										if (pid && _activePids[pid]) btn.classList.add('is-active');
									});
								}
								competitorsBar.style.display = 'flex';
								var contextMsg = root.querySelector('[data-avi-heatmap-competitors-context]');
								if (!contextMsg) {
									contextMsg = document.createElement('p');
									contextMsg.setAttribute('data-avi-heatmap-competitors-context', '');
									contextMsg.style.cssText = 'margin:6px 12px 4px;font-size:.72rem;' +
										'color:#64748b;font-style:italic;line-height:1.4;';
									competitorsBar.parentNode.insertBefore(
										contextMsg, competitorsBar.nextSibling);
								}
								var kwList = selectedMapKeywords.length
									? selectedMapKeywords.join(', ')
									: selectedKeyword;
								// Anti-régression #Sprint43-Frontend5-context-grid
								// Message enrichi : hint grille si peu de concurrents détectés
								var hmLang = getHmLang();
								var _gridHint = competitors.length < 5
									? (hmLang === 'fr'
										? ' Pour découvrir plus de concurrents, augmentez la taille de la grille et relancez un scan.'
										: hmLang === 'es'
										? ' Para descubrir más competidores, aumente el tamaño de la cuadrícula y vuelva a escanear.'
										: ' To discover more competitors, increase the grid size and relaunch a scan.')
									: '';
								contextMsg.textContent = hmLang === 'fr'
									? 'Ces entreprises apparaissent dans les résultats Google Maps pour : "' + kwList +
										'". Même si leur activité diffère, ils occupent des positions qui réduisent votre visibilité locale.' + _gridHint
									: hmLang === 'es'
									? 'Estas empresas aparecen en Google Maps para: "' + kwList +
										'". Aunque su actividad difiera, ocupan posiciones que reducen su visibilidad local.' + _gridHint
									: 'These businesses appear in Google Maps results for: "' + kwList +
										'". Even if their activity differs, they occupy positions that reduce your local visibility.' + _gridHint;
								contextMsg.style.display = '';
							} else {
								competitorsBar.style.display = 'none';
								var contextMsg = root.querySelector('[data-avi-heatmap-competitors-context]');
								if (!contextMsg) {
									contextMsg = document.createElement('p');
									contextMsg.setAttribute('data-avi-heatmap-competitors-context', '');
									contextMsg.style.cssText = 'margin:6px 12px 4px;font-size:.72rem;' +
										'color:#64748b;font-style:italic;line-height:1.4;';
									if (competitorsBar && competitorsBar.parentNode) {
										competitorsBar.parentNode.insertBefore(
											contextMsg, competitorsBar.nextSibling);
									}
								}
								var kwList = selectedMapKeywords.length
									? selectedMapKeywords.join(', ')
									: selectedKeyword;
								var hmLang = getHmLang();
								contextMsg.textContent = hmLang === 'fr'
									? 'Aucun concurrent identifié pour : "' + kwList + '". Relancez un scan pour obtenir les données concurrentielles.'
									: hmLang === 'es'
									? 'Ningún competidor identificado para: "' + kwList + '". Relance un escaneo para obtener datos competitivos.'
									: 'No competitors identified for: "' + kwList + '". Run a scan to get competitive data.';
								contextMsg.style.display = '';
							}
						}
						var _kwForTrend = selectedMapKeywords.length
							? selectedMapKeywords[0] : selectedKeyword;
						if (_kwForTrend) {
							var _trendKey = String(_kwForTrend).trim().toLowerCase();
							setTimeout(function() {
								fetchPreviousScanPoints(_kwForTrend).then(function(prevData) {
									var currentData = scanCache[_trendKey];
									if (!currentData || !Array.isArray(currentData.map_points)
										|| !currentData.map_points.length) return;
									var trends = computeTrendData(currentData.map_points, prevData);
									if (Object.keys(trends).length > 0) {
										scanCache[_trendKey].trendData = trends;
										renderTrendOverlay(_kwForTrend, trends);
									}
								});
							}, 300);
						}
						leafletMap.invalidateSize();
						heatmapIsZoomRefresh = false;
					});
				}
				function bindHmPedago(){
					root.querySelectorAll('[data-hm-pedago]').forEach(function(btn){
						btn.addEventListener('click', function(event){
							event.preventDefault();
							event.stopPropagation();
							openHmPedagoModal(btn.getAttribute('data-hm-pedago') || '');
						});
					});
					var overlay = root.querySelector('#aviHmPedagoOverlay');
					var cta = root.querySelector('#aviHmPedagoCta');
					if (cta) {
						cta.addEventListener('click', function(event){
							event.preventDefault();
							closeHmPedagoModal();
						});
					}
					if (overlay) {
						overlay.addEventListener('click', function(event){
							if (event.target === overlay) closeHmPedagoModal();
						});
					}
					document.addEventListener('keydown', function(event){
						if (event.key === 'Escape') closeHmPedagoModal();
					});
				}
				function bindHmDisclaimer(){
					var toggleBtn = root.querySelector('[data-avi-hm-disclaimer-toggle]');
					var body = root.querySelector('[data-avi-hm-disclaimer-body]');
					if (!toggleBtn || !body) return;
					toggleBtn.addEventListener('click', function(event){
						event.preventDefault();
						event.stopPropagation();
						var open = toggleBtn.getAttribute('aria-expanded') === 'true';
						toggleBtn.setAttribute('aria-expanded', open ? 'false' : 'true');
						body.hidden = open;
					});
				}
				function bindHmFullscreen(){
					var fsBtn = root.querySelector('[data-avi-hm-fullscreen]');
					if (!fsBtn || !mapContainer) return;
					var exitBtn = document.createElement('button');
					exitBtn.type = 'button';
					exitBtn.className = 'avi-hm-fs-exit-btn';
					exitBtn.textContent = '×';
					exitBtn.setAttribute('aria-label', 'Exit fullscreen map');
					exitBtn.addEventListener('click', function(event){
						event.preventDefault();
						if (document.exitFullscreen) document.exitFullscreen();
					});
					fsBtn.addEventListener('click', function(event){
						event.preventDefault();
						event.stopPropagation();
						if (mapContainer.requestFullscreen) {
							mapContainer.requestFullscreen();
						}
					});
					document.addEventListener('fullscreenchange', function(){
						var active = document.fullscreenElement === mapContainer;
						if (active && !mapContainer.contains(exitBtn)) {
							mapContainer.appendChild(exitBtn);
						} else if (!active && exitBtn.parentNode) {
							exitBtn.parentNode.removeChild(exitBtn);
						}
						setTimeout(function(){
							if (leafletMap) leafletMap.invalidateSize();
						}, 120);
					});
				}
				function launchHeatmapScan(){
					refreshCreditState();
					var size = selectedGridSize();
					var kwCount = selectedMapKeywords.length || 1;
					var cost = size * size * kwCount;
					var keywordsToScan = selectedMapKeywords.length
						? selectedMapKeywords.slice()
						: (selectedKeyword ? [selectedKeyword] : []);
					var scanSessionId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
						var r = Math.random() * 16 | 0;
						var v = c === 'x' ? r : (r & 0x3 | 0x8);
						return v.toString(16);
					});
					var disabled = isSubmitting || !keywordsToScan.length || !hasCenter() || cost > creditBalance;
					if (disabled) return;
					preserveFeedback = false;
					isSubmitting = true;
					setScanButtonsDisabled(true);
					var scanSendingText = hm_t('scan_sending');
					setFeedback(scanSendingText, 'loading');
					var modalFeedbackEl = root.querySelector(
						'[data-avi-heatmap-scan-feedback][style]');
					if (modalFeedbackEl) {
						modalFeedbackEl.style.display = '';
						modalFeedbackEl.textContent = scanSendingText || 'Scan request sent...';
					}
					Promise.all(keywordsToScan.map(function(kw) {
						return fetch(scanUrl(), {
							method: 'POST',
							credentials: 'same-origin',
							headers: restHeaders(),
							body: JSON.stringify({
								profile_id: Number(config.profile_id || root.getAttribute('data-profile-id') || 0),
								keyword: kw,
								center: { lat: Number(config.center.lat), lng: Number(config.center.lng) },
								grid_size: size,
								radius_km: Number(config.default_radius_km || 5),
								scan_mode: 'standard',
								scan_session_id: scanSessionId
							})
						}).then(function(response){
							return response.json().catch(function(){ return {}; }).then(function(data){
								if (!response.ok) {
									var message = (data && data.message) || (data && data.error && data.error.message) || hm_t('scan_refused');
									throw new Error(message);
								}
								return data || {};
							});
						});
					})).then(function(results){
						var totalCharged = 0;
						results.forEach(function(data) {
							var charged = Number(data.credits_charged || 0);
							if (isFinite(charged) && charged > 0) totalCharged += charged;
						});
						if (totalCharged > 0) creditBalance = Math.max(0, creditBalance - totalCharged);
						var scanIds = results.map(function(d) { return d.scan_id ? '#' + d.scan_id : ''; }).filter(Boolean).join(', ');
						var kwCount = keywordsToScan.length;
						var estimatedMinutes = kwCount * 2;
						var waitMsg = kwCount > 1
							? (getHmLang() === 'fr'
								? '🔄 ' + kwCount + ' scans lancés (' + scanIds + '). Les mots-clés apparaîtront sur la carte au fur et à mesure. Temps estimé : ~' + estimatedMinutes + ' min. Vous pouvez continuer à naviguer.'
								: getHmLang() === 'es'
								? '🔄 ' + kwCount + ' escaneos lanzados (' + scanIds + '). Las palabras clave aparecerán progresivamente. Tiempo estimado: ~' + estimatedMinutes + ' min. Puede seguir navegando.'
								: getHmLang() === 'de'
								? '🔄 ' + kwCount + ' Scans gestartet (' + scanIds + '). Keywords erscheinen nach und nach. Geschätzte Zeit: ~' + estimatedMinutes + ' Min. Sie können weiter navigieren.'
								: getHmLang() === 'pt-br'
								? '🔄 ' + kwCount + ' scans iniciados (' + scanIds + '). Keywords aparecerão progressivamente. Tempo estimado: ~' + estimatedMinutes + ' min. Você pode continuar navegando.'
								: getHmLang() === 'tr'
								? '🔄 ' + kwCount + ' tarama başlatıldı (' + scanIds + '). Anahtar kelimeler haritada kademeli olarak görünecek. Tahmini süre: ~' + estimatedMinutes + ' dk. Gezinmeye devam edebilirsiniz.'
								: '🔄 ' + kwCount + ' scans launched (' + scanIds + '). Keywords will appear on the map progressively. Estimated time: ~' + estimatedMinutes + ' min. You can keep browsing.')
							: (getHmLang() === 'fr'
								? '🔄 Scan lancé (' + scanIds + '). Résultat attendu dans ~2 min. Vous pouvez continuer à naviguer.'
								: getHmLang() === 'es'
								? '🔄 Escaneo lanzado (' + scanIds + '). Resultado esperado en ~2 min. Puede seguir navegando.'
								: getHmLang() === 'de'
								? '🔄 Scan gestartet (' + scanIds + '). Ergebnis erwartet in ~2 Min. Sie können weiter navigieren.'
								: getHmLang() === 'pt-br'
								? '🔄 Scan iniciado (' + scanIds + '). Resultado esperado em ~2 min. Você pode continuar navegando.'
								: getHmLang() === 'tr'
								? '🔄 Tarama başlatıldı (' + scanIds + '). Sonuç ~2 dk içinde bekleniyor. Gezinmeye devam edebilirsiniz.'
								: '🔄 Scan launched (' + scanIds + '). Result expected in ~2 min. You can keep browsing.');
						preserveFeedback = true;
						setFeedback(waitMsg, 'ok');
						var pendingScans = {};
						results.forEach(function(data, idx) {
							if (data && data.scan_id) {
								pendingScans[String(data.scan_id)] = {
									kw: keywordsToScan[idx] || '',
									scan_id: data.scan_id
								};
							}
						});
						isScanInProgress = true;
						if (Object.keys(pendingScans).length) {
							if (leafletLayer) leafletLayer.clearLayers();
							if (leafletMap) setMapOverlay(
								getHmLang() === 'fr' ? '⏳ Scan en cours...'
								: getHmLang() === 'es' ? '⏳ Escaneo en curso...'
								: getHmLang() === 'de' ? '⏳ Scan läuft...'
								: getHmLang() === 'pt-br' ? '⏳ Scan em andamento...'
								: getHmLang() === 'tr' ? '⏳ Tarama devam ediyor...'
								: '⏳ Scanning in progress...', false);
							selectedMapKeywords.forEach(function(kw) {
								delete scanCache[String(kw).toLowerCase()];
							});
							setScanButtonsDisabled(true);
							var totalScansLaunched = Object.keys(pendingScans).length;
							var pollAttempts = 0;
							var maxAttempts = 180;
							var pollInterval = setInterval(function() {
								pollAttempts++;
								if (pollAttempts > maxAttempts) {
									clearInterval(pollInterval);
									pollInterval = null;
									preserveFeedback = false;
									setScanButtonsDisabled(false);
									refreshCreditState();
									return;
								}
								// Anti-régression #Sprint43-polling-scan-id
								// Polling par scan_id exact au lieu de /heatmap/latest
								// Evite que le filtre status=completed retourne un ancien scan complet
								// au lieu du scan en cours (id plus élevé). Autorisé Anthony — Sprint 43
								var pendingEntries = Object.values(pendingScans);
								Promise.all(pendingEntries.map(function(entry) {
									var kwKey = String(entry.kw).toLowerCase();
									return fetch(scanByIdUrl(entry.scan_id), {
										method: 'GET',
										credentials: 'same-origin',
										headers: restHeaders()
									}).then(function(r) {
										return r.json().catch(function() { return {}; });
									}).then(function(data) {
										return { entry: entry, data: data };
									}).catch(function() { return { entry: entry, data: {} }; });
								})).then(function(pollResults) {
									var completedCount = 0;
									pollResults.forEach(function(result) {
										var d = result.data;
										var entry = result.entry;
										var kw = entry.kw;
										var sid = String(entry.scan_id);
										if (d && (d.status === 'completed' || d.status === 'partial') &&
											Array.isArray(d.map_points) && d.map_points.length > 0) {
											var dScanId = d.scan_id ? String(d.scan_id) : null;
											if (dScanId && parseInt(dScanId) >= parseInt(sid)) {
												var key = String(kw).toLowerCase();
												scanCache[key] = d;
												delete pendingScans[sid];
												completedCount++;
											}
										}
									});
									if (completedCount > 0) {
										updateCoverageBadge();
										var activeKwForDrawer = selectedMapKeywords && selectedMapKeywords.length
											? selectedMapKeywords[0] : selectedKeyword;
										if (activeKwForDrawer) updateDrawerMetrics(activeKwForDrawer);
										var remaining = Object.keys(pendingScans).length;
										var doneCount = totalScansLaunched - remaining;
										// remaining === 0: rendu final uniquement.
										if (remaining === 0) {
											isScanInProgress = false;
											renderSelectedMap();
											clearInterval(pollInterval);
											pollInterval = null;
											setScanButtonsDisabled(false);
											isSubmitting = false;
											preserveFeedback = false;
											setFeedback(
												getHmLang() === 'fr' ? '✅ Tous les scans sont terminés !'
												: getHmLang() === 'es' ? '✅ ¡Todos los escaneos completados!'
												: getHmLang() === 'de' ? '✅ Alle Scans abgeschlossen!'
												: getHmLang() === 'pt-br' ? '✅ Todos os scans concluídos!'
												: getHmLang() === 'tr' ? '✅ Tüm taramalar tamamlandı!'
												: '✅ All scans completed!', 'ok');
											enableOpenMapButton();
											setTimeout(function() {
												preserveFeedback = false;
												setFeedback('', '');
											}, 3000);
										} else {
											setFeedback(
												getHmLang() === 'fr'
													? '🔄 ' + doneCount + '/' + totalScansLaunched + ' mots-clés traités...'
													: getHmLang() === 'es'
													? '🔄 ' + doneCount + '/' + totalScansLaunched + ' palabras clave procesadas...'
													: getHmLang() === 'de'
													? '🔄 ' + doneCount + '/' + totalScansLaunched + ' Keywords verarbeitet...'
													: getHmLang() === 'pt-br'
													? '🔄 ' + doneCount + '/' + totalScansLaunched + ' keywords processadas...'
													: getHmLang() === 'tr'
													? '🔄 ' + doneCount + '/' + totalScansLaunched + ' anahtar kelime işlendi...'
													: '🔄 ' + doneCount + '/' + totalScansLaunched + ' keywords processed...', 'ok');
										}
									}
								});
							}, 5000);
						} else {
							setScanButtonsDisabled(false);
						}
						resetConfirmPanelScheduleUI();
					}).catch(function(error){
						preserveFeedback = true;
						setFeedback(error && error.message ? error.message : hm_t('scan_error'), 'error');
						setScanButtonsDisabled(false);
						isSubmitting = false;
						refreshCreditState();
						resetConfirmPanelScheduleUI();
					});
				}
				if (toggle && drawer) {
					toggle.addEventListener('click', function(){
						var isOpen = !drawer.hasAttribute('hidden');
						if (isOpen) {
							drawer.setAttribute('hidden', '');
							toggle.setAttribute('aria-expanded', 'false');
							if (radarCard) radarCard.classList.remove('radar-heatmap-open');
						} else {
							drawer.removeAttribute('hidden');
							// #Sprint43-scheduler
							// #Sprint43-scheduler fix — pas de updateScheduleNote si schedule actif rendu
							loadHeatmapSchedules();
							toggle.setAttribute('aria-expanded', 'true');
							if (radarCard) radarCard.classList.add('radar-heatmap-open');
						}
					});
				}
				designButtons.forEach(function(btn){
					btn.addEventListener('click', function(){
						applyDesign(String(btn.getAttribute('data-avi-heatmap-design') || 'grid'));
					});
				});
				keywordButtons.forEach(function(btn){
					btn.addEventListener('click', function(){
						var keyword = String(btn.getAttribute('data-heatmap-keyword') || '').trim();
						if (!keyword) return;
						var lower = keyword.toLowerCase();
						var index = selectedMapKeywords.map(function(item){ return String(item || '').toLowerCase(); }).indexOf(lower);
						if (index >= 0) {
							selectedMapKeywords.splice(index, 1);
						} else if (selectedMapKeywords.length < 5) {
							selectedMapKeywords.push(keyword);
						}
						var savedPreserveFeedback = preserveFeedback;
						setSelectedKeyword(selectedMapKeywords.length ? selectedMapKeywords[0] : '');
						(function() {
							var _kw = selectedKeyword;
							if (!_kw) return;
							var _key = String(_kw).trim().toLowerCase();
							var _cached = scanCache[_key];
							if (_cached && Array.isArray(_cached.map_points) && _cached.map_points.length > 0) {
								updateCoverageBadge(_kw);
								updateDrawerMetrics(_kw);
								updateScanDateOverlay(_kw);
								if (leafletMap) renderSelectedMapDebounced(); // Anti-régression #Sprint43-C2-redraw
							} else {
								var _badge = root ? root.querySelector('[data-avi-hm-coverage]') : null;
								if (_badge) {
									_badge.innerHTML = '<span class="avi-hm-coverage-none">'
										+ (hm_t('coverage_score_none') || 'No scan yet') + '</span>';
								}
								var _terrEl = root ? root.querySelector('[data-avi-hm-metric="territory"]') : null;
								var _rankEl = root ? root.querySelector('[data-avi-hm-metric="avg-rank"]') : null;
								var _statEl = root ? root.querySelector('[data-avi-hm-metric="status"]') : null;
								if (_terrEl) _terrEl.textContent = '—';
								if (_rankEl) _rankEl.textContent = '—';
								if (_statEl) _statEl.textContent = '—';
								fetchScanForKeyword(_kw).then(function() {
									if (leafletMap) renderSelectedMapDebounced();
								});
							}
						})();
						preserveFeedback = savedPreserveFeedback;
					});
				});
				modalKeywordButtons.forEach(function(btn){
					btn.addEventListener('click', function(){
						var keyword = String(btn.getAttribute('data-avi-heatmap-modal-keyword') || '').trim();
						if (!keyword) return;
						var lower = keyword.toLowerCase();
						var index = selectedMapKeywords.map(function(item){ return item.toLowerCase(); }).indexOf(lower);
						if (index >= 0) {
							selectedMapKeywords.splice(index, 1);
						} else if (selectedMapKeywords.length < 5) {
							selectedMapKeywords.push(keyword);
						}
						if (selectedMapKeywords.length) {
							setSelectedKeyword(selectedMapKeywords[0]);
							(function() {
								var _kw = selectedKeyword;
								if (!_kw) return;
								var _key = String(_kw).trim().toLowerCase();
								var _cached = scanCache[_key];
								if (_cached && Array.isArray(_cached.map_points) && _cached.map_points.length > 0) {
									updateCoverageBadge(_kw);
									updateDrawerMetrics(_kw);
									updateScanDateOverlay(_kw);
									if (leafletMap) renderSelectedMapDebounced(); // Anti-régression #Sprint43-C2-redraw
								} else {
									var _badge = root ? root.querySelector('[data-avi-hm-coverage]') : null;
									if (_badge) {
										_badge.innerHTML = '<span class="avi-hm-coverage-none">'
											+ (hm_t('coverage_score_none') || 'No scan yet') + '</span>';
									}
									var _terrEl = root ? root.querySelector('[data-avi-hm-metric="territory"]') : null;
									var _rankEl = root ? root.querySelector('[data-avi-hm-metric="avg-rank"]') : null;
									var _statEl = root ? root.querySelector('[data-avi-hm-metric="status"]') : null;
									if (_terrEl) _terrEl.textContent = '—';
									if (_rankEl) _rankEl.textContent = '—';
									if (_statEl) _statEl.textContent = '—';
									fetchScanForKeyword(_kw).then(function() {
										if (leafletMap) renderSelectedMapDebounced();
									});
								}
							})();
						} else {
							setSelectedKeyword('');
						}
					});
				});
				var KW_LIMIT = 11;
				function applyKeywordFilter(scope, searchVal, sortMode, showAll){
					var isDrawer = scope === 'drawer';
					var listEl = root.querySelector(
						isDrawer ? '.avi-heatmap-keyword-list' : '.avi-heatmap-modal-keywords'
					);
					var showMoreBtn = root.querySelector(
						'[data-avi-heatmap-kw-showmore="' + scope + '"]');
					var showMoreCount = showMoreBtn
						? showMoreBtn.querySelector('[data-avi-heatmap-kw-showmore-count]')
						: null;
					if (!listEl) return;
					var attr = isDrawer ? 'data-heatmap-keyword' : 'data-avi-heatmap-modal-keyword';
					var allBtns = Array.prototype.slice.call(
						listEl.querySelectorAll('[' + attr + ']'));
					var search = String(searchVal || '').trim().toLowerCase();
					var filtered = allBtns.filter(function(btn){
						if (!search) return true;
						var kw = String(btn.getAttribute(attr) || '').toLowerCase();
						return kw.indexOf(search) >= 0;
					});
					filtered.sort(function(a, b){
						if (sortMode === 'alpha') {
							var ka = String(a.getAttribute(attr) || '').toLowerCase();
							var kb = String(b.getAttribute(attr) || '').toLowerCase();
							return ka < kb ? -1 : ka > kb ? 1 : 0;
						}
						if (sortMode === 'scanned') {
							var sa = scanCache[String(a.getAttribute(attr) || '').toLowerCase()] ? 1 : 0;
							var sb = scanCache[String(b.getAttribute(attr) || '').toLowerCase()] ? 1 : 0;
							if (sb !== sa) return sb - sa;
						}
						var ra = parseInt(a.getAttribute('data-keyword-rank') || '0', 10) || 9999;
						var rb = parseInt(b.getAttribute('data-keyword-rank') || '0', 10) || 9999;
						return ra - rb;
					});
					allBtns.forEach(function(btn){ btn.style.display = 'none'; });
					filtered.forEach(function(btn){ listEl.appendChild(btn); });
					var limit = showAll ? filtered.length : KW_LIMIT;
					var hidden = 0;
					filtered.forEach(function(btn, idx){
						if (idx < limit) { btn.style.display = ''; } else { hidden++; }
					});
					if (showMoreBtn) {
						if (hidden > 0 && !showAll) {
							if (showMoreCount) showMoreCount.textContent = '(+' + hidden + ')';
							showMoreBtn.style.display = '';
						} else {
							showMoreBtn.style.display = 'none';
						}
					}
				}
				var kwState = {
					drawer: { search: '', sort: 'rank', showAll: false },
					modal:  { search: '', sort: 'rank', showAll: false }
				};
				applyKeywordFilter('drawer', '', 'rank', false);
				applyKeywordFilter('modal',  '', 'rank', false);
				['drawer', 'modal'].forEach(function(scope){
					var input = root.querySelector(
						'[data-avi-heatmap-kw-search="' + scope + '"]');
					if (input) {
						input.addEventListener('input', function(){
							kwState[scope].search = input.value;
							kwState[scope].showAll = false;
							applyKeywordFilter(
								scope, kwState[scope].search,
								kwState[scope].sort, false);
						});
					}
				});
				root.querySelectorAll('[data-avi-heatmap-kw-sort]').forEach(function(btn){
					btn.addEventListener('click', function(){
						var scope = btn.getAttribute('data-avi-heatmap-kw-sort');
						var mode = btn.getAttribute('data-sort-mode');
						kwState[scope].sort = mode;
						kwState[scope].showAll = false;
						root.querySelectorAll(
							'[data-avi-heatmap-kw-sort="' + scope + '"]'
						).forEach(function(b){
							b.classList.toggle('is-active',
								b.getAttribute('data-sort-mode') === mode);
						});
						applyKeywordFilter(
							scope, kwState[scope].search, mode, false);
					});
				});
				root.querySelectorAll('[data-avi-heatmap-kw-showmore]').forEach(function(btn){
					btn.addEventListener('click', function(){
						var scope = btn.getAttribute('data-avi-heatmap-kw-showmore');
						kwState[scope].showAll = true;
						btn.style.display = 'none';
						applyKeywordFilter(
							scope, kwState[scope].search,
							kwState[scope].sort, true);
					});
				});
				if (gridSelect) {
					gridSelect.addEventListener('change', function(){
						preserveFeedback = false;
						updateGridDescription();
						refreshCreditState();
					});
				}
				// #Sprint43-scheduler — listener fréquence
				if (frequencySelect) {
					frequencySelect.addEventListener('change', function() {
						updateScheduleNote();
					});
				}
				// #Sprint43-scheduler — confirm panel dynamique
				function buildConfirmContent(freq, costPerScan, balance) {
					var lang   = getHmLang();
					if (freq === 'once' || !freq) return null;

					var mult       = SCHEDULE_MONTHLY_MULTIPLIERS[freq] || 1;
					var freqLabel  = (SCHEDULE_FREQ_LABELS[freq] || {})[lang] || freq;
					var total      = costPerScan * mult;
					var balanceOk  = balance >= costPerScan;
					var balanceKey = balanceOk ? 'schedule_balance_ok' : 'schedule_balance_warn';
					var multiWarn  = (window._aviActiveSchedules || []).length > 0
						? '<p class="avi-hm-confirm-warn">' + hm_t('schedule_multi_warn') + '</p>'
						: '';

					return multiWarn
						+ '<p class="avi-hm-confirm-title">'
						+   hm_t('schedule_confirm_freq')
								.replace('{freq_label}', freqLabel)
								.replace('{scans}', mult)
						+ '</p>'
						+ '<p class="avi-hm-confirm-detail">'
						+   hm_t('schedule_credits_line')
								.replace('{cost}', costPerScan)
								.replace('{scans}', mult)
								.replace('{total}', total)
						+ '</p>'
						+ '<p class="avi-hm-confirm-balance ' + (balanceOk ? 'ok' : 'warn') + '">'
						+   hm_t(balanceKey).replace('{balance}', balance)
						+ '</p>';
				}
				// #Sprint43-scheduler fix — nettoyage confirm panel schedulé
				function resetConfirmPanelScheduleUI() {
					var panel = root.querySelector('[data-avi-hm-confirm-panel]');
					if (!panel) return;
					var scheduled = panel.querySelector('.avi-hm-confirm-scheduled');
					if (scheduled) scheduled.remove();
					var onetime = panel.querySelector('.avi-hm-confirm-summary');
					if (onetime) onetime.style.display = '';
				}
				scanButtons.forEach(function(btn){
					btn.addEventListener('click', function(){
						if (btn.hasAttribute('data-avi-heatmap-scan-submit')) {
							var kwCount = selectedMapKeywords.length || 1;
							var size = selectedGridSize();
							var cost = size * size * kwCount;
							var after = Math.max(0, creditBalance - cost);
							var confirmPanel = root.querySelector('[data-avi-hm-confirm-panel]');
							var titleEl = root.querySelector('[data-avi-hm-confirm-title]');
							var subEl = root.querySelector('[data-avi-hm-confirm-sub]');
							var launchBtn = root.querySelector('[data-avi-heatmap-scan-submit]');
							if (titleEl) {
								titleEl.textContent = kwCount + ' ' +
									(hm_t('kw_label_plural') || 'keyword(s)') +
									' · ' + cost + ' ' +
									(hm_t('credits_label') || 'credits');
							}
							if (subEl) {
								subEl.textContent =
									(hm_t('balance_after_label') || 'Balance after') +
									' : ' + after + ' ' +
									(hm_t('credits_label') || 'credits');
							}
							// #Sprint43-scheduler — enrichissement panel si freq programmée
							var _freq = frequencySelect ? frequencySelect.value : 'once';
							var _panel = root.querySelector('[data-avi-hm-confirm-panel]');
							if (_panel) {
								resetConfirmPanelScheduleUI();
								if (_freq !== 'once') {
									var _size    = selectedGridSize();
									var _kwCount = (selectedMapKeywords && selectedMapKeywords.length)
										|| (selectedKeyword ? 1 : 0);
									var _cost    = _size * _size * _kwCount;
									var _balance = creditBalance;
									var _html    = buildConfirmContent(_freq, _cost, _balance);
									if (_html) {
										// #Sprint43-scheduler fix — insertion sans détruire les listeners
										var _wrapper = document.createElement('div');
										_wrapper.className = 'avi-hm-confirm-scheduled';
										_wrapper.innerHTML = _html;
										_panel.insertBefore(_wrapper, _panel.firstChild);
										// #Sprint43-scheduler fix — masquer bloc one-time (.avi-hm-confirm-summary)
										var _onetime = _panel.querySelector('.avi-hm-confirm-summary');
										if (_onetime) _onetime.style.display = 'none';
									}
								}
							}
							if (confirmPanel) {
								confirmPanel.removeAttribute('hidden');
							}
							if (launchBtn) {
								launchBtn.setAttribute('hidden', '');
							}
							return;
						}
						launchHeatmapScan();
					});
				});
				var confirmOkBtn = root.querySelector('[data-avi-hm-confirm-ok]');
				var confirmCancelBtn = root.querySelector('[data-avi-hm-confirm-cancel]');
				var confirmPanel2 = root.querySelector('[data-avi-hm-confirm-panel]');
				var launchBtn2 = root.querySelector('[data-avi-heatmap-scan-submit]');
				if (confirmOkBtn) {
					confirmOkBtn.addEventListener('click', function(){
						// #Sprint43-scheduler fix — nettoyage bloc schedulé
						resetConfirmPanelScheduleUI();
						if (confirmPanel2) {
							confirmPanel2.setAttribute('hidden', '');
						}
						if (launchBtn2) {
							launchBtn2.removeAttribute('hidden');
						}
						// #Sprint43-scheduler — branchement freq
						var freq = frequencySelect ? frequencySelect.value : 'once';
						if (freq === 'once') {
							launchHeatmapScan();
						} else {
							launchHeatmapScan();
							saveHeatmapSchedule(freq);
						}
					});
				}
				if (confirmCancelBtn) {
					confirmCancelBtn.addEventListener('click', function(){
						// #Sprint43-scheduler fix — nettoyage bloc schedulé
						resetConfirmPanelScheduleUI();
						if (confirmPanel2) {
							confirmPanel2.setAttribute('hidden', '');
						}
						if (launchBtn2) {
							launchBtn2.removeAttribute('hidden');
						}
					});
				}
				root.querySelectorAll('[data-avi-heatmap-modal-open]').forEach(function(btn){
					btn.addEventListener('click', function(){
						if (modal) {
							modal.removeAttribute('hidden');
							if (document.body) document.body.classList.add('avi-heatmap-modal-open');
							renderSelectedMap();
						}
					});
				});
				root.querySelectorAll('[data-avi-heatmap-modal-close]').forEach(function(btn){
					btn.addEventListener('click', function(){
						if (modal) {
							modal.setAttribute('hidden', '');
							if (document.body) document.body.classList.remove('avi-heatmap-modal-open');
						}
					});
				});
				var _hmDashRoot = document.getElementById('avision-dashboard-root');
				var _hmLangObserver = window.MutationObserver;
				if (_hmDashRoot && _hmLangObserver) {
					new _hmLangObserver(function(mutations){
						mutations.forEach(function(m){
							if (m.attributeName === 'data-lang' || m.attributeName === 'data-language') {
								applyHeatmapI18n();
								updateGridDescription();
								refreshCreditState();
							}
						});
					}).observe(_hmDashRoot, { attributes: true, attributeFilter: ['data-lang', 'data-language'] });
				}
				/* ── Mode Évolution ── */
				var evolutionScans = [];
				var evolutionSelected = [];
				var evolutionSelectedMonth = '';
				var evolutionSelectedWeek = '';
				var evolutionSelectedDay = '';
				var evolutionRefScanId = null;
				var sessionGroups = {};
				var evolutionSortDesc = true;

				function historyUrl(keyword, sessionId) {
					var base = String(config.history_rest_url || '/wp-json/avision/v1/heatmap-history');
					var sep = base.indexOf('?') >= 0 ? '&' : '?';
					var url = base + sep
						+ 'profile_id=' + encodeURIComponent(String(config.profile_id || root.getAttribute('data-profile-id') || 0))
						+ '&keyword=' + encodeURIComponent(keyword || '')
						+ '&limit=100';
					if (sessionId) {
						url += '&session_id=' + encodeURIComponent(String(sessionId));
					}
					if (typeof window.aviAppendTestDriveParamsToWpAvisionUrl === 'function') {
						url = window.aviAppendTestDriveParamsToWpAvisionUrl(url);
					}
					return url;
				}

				function scanByIdUrl(scanId) {
					var base = String(config.latest_rest_url || '/wp-json/avision/v1/heatmap-latest');
					base = base.replace('/heatmap-latest', '/heatmap-scan-by-id/' + encodeURIComponent(String(scanId)));
					var profileId = String(config.profile_id || root.getAttribute('data-profile-id') || 0);
					var sep = base.indexOf('?') >= 0 ? '&' : '?';
					return base + sep + 'profile_id=' + encodeURIComponent(profileId);
				}

				function formatEvolutionDate(dateStr) {
					if (!dateStr) return '—';
					var parts = String(dateStr).split('-');
					if (parts.length === 3) {
						var months = {
							en: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
							fr: ['jan.','fév.','mar.','avr.','mai','juin','juil.','août','sep.','oct.','nov.','déc.'],
							es: ['ene.','feb.','mar.','abr.','may.','jun.','jul.','ago.','sep.','oct.','nov.','dic.']
						};
						var lang = getHmLang();
						var mList = months[lang] || months.en;
						var day = parseInt(parts[2], 10);
						var month = mList[parseInt(parts[1], 10) - 1] || parts[1];
						var year = parts[0];
						return day + ' ' + month + ' ' + year;
					}
					return dateStr;
				}

				function deltaHtml(current, reference, isRank) {
					if (current === null || current === undefined || reference === null || reference === undefined) return '';
					var diff = isRank
						? Math.round((reference - current) * 10) / 10
						: Math.round((current - reference) * 10) / 10;
					if (diff === 0) return '<span class="avi-hm-evolution-delta avi-hm-evolution-delta--neutral">= </span>';
					var up = diff > 0;
					var sign = up ? '↑ +' : '↓ ';
					var cls = up ? 'avi-hm-evolution-delta--up' : 'avi-hm-evolution-delta--down';
					var suffix = (!isRank && Math.abs(diff) > 0) ? '%' : '';
					return '<span class="avi-hm-evolution-delta ' + cls + '">' + sign + Math.abs(diff) + suffix + '</span>';
				}

				function renderEvolutionSelector() {
					var selectorEl = root.querySelector('[data-avi-hm-evolution-selector]');
					var loadingEl = root.querySelector('[data-avi-hm-evolution-loading]');
					if (!selectorEl) return;
					if (!evolutionScans.length) {
						selectorEl.innerHTML = '<p class="avi-hm-evolution-loading">' +
							(hm_t('evolution_no_scans') || 'No completed scans found.') + '</p>';
						return;
					}
					var evoInfoStyle = 'display:inline-flex;align-items:center;justify-content:center;' +
						'width:14px;height:14px;margin-left:4px;border-radius:999px;background:none;' +
						'border:none;color:#94a3b8;font-size:.7rem;cursor:pointer;vertical-align:middle;padding:0;font-family:inherit';
					var months = {};
					evolutionScans.forEach(function(s) {
						if (!s.scan_date) return;
						var parts = String(s.scan_date).split('-');
						if (parts.length < 2) return;
						var key = parts[0] + '-' + parts[1];
						var lang = getHmLang();
						var monthNames = {
							en: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
							fr: ['Jan','Fév','Mar','Avr','Mai','Jun','Jul','Aoû','Sep','Oct','Nov','Déc'],
							es: ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic']
						};
						var mList = monthNames[lang] || monthNames.en;
						var mIdx = parseInt(parts[1], 10) - 1;
						months[key] = (mList[mIdx] || parts[1]) + ' ' + parts[0];
					});
					var monthKeys = Object.keys(months).sort().reverse();
					var selectedMonth = evolutionSelectedMonth || '';
					if (selectedMonth && monthKeys.indexOf(selectedMonth) < 0) {
						selectedMonth = '';
						evolutionSelectedMonth = selectedMonth;
					}
					var monthFilterHtml = monthKeys.length > 1
						? '<div class="avi-hm-evo-month-filter">' +
							'<button type="button" class="avi-hm-evo-month-btn' +
							(!evolutionSelectedMonth ? ' is-active' : '') +
							'" data-avi-hm-evo-month="">' +
							(getHmLang() === 'fr' ? 'Tout' : getHmLang() === 'es' ? 'Todo' : 'All') +
							'</button>' +
							monthKeys.map(function(k) {
								return '<button type="button" class="avi-hm-evo-month-btn' +
									(k === selectedMonth ? ' is-active' : '') + '" data-avi-hm-evo-month="' + k + '">' +
									months[k] + '</button>';
							}).join('') +
							'</div>'
						: '';
					var dayScans = evolutionScans.filter(function(s) {
						if (!s.scan_date) return false;
						var dateStr = String(s.scan_date);
						return !selectedMonth || dateStr.indexOf(selectedMonth) === 0;
					});
					var weeks = {};
					dayScans.forEach(function(s) {
						if (!s.scan_date) return;
						var d = new Date(s.scan_date);
						var startOfYear = new Date(d.getFullYear(), 0, 1);
						var weekNum = Math.ceil(((d - startOfYear) / 86400000 + startOfYear.getDay() + 1) / 7);
						var weekKey = d.getFullYear() + '-W' + String(weekNum).padStart(2, '0');
						var weekStart = new Date(d);
						weekStart.setDate(d.getDate() - d.getDay() + 1);
						var weekEnd = new Date(weekStart);
						weekEnd.setDate(weekStart.getDate() + 6);
						var lang = getHmLang();
						var mNames = {
							en: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
							fr: ['jan','fév','mar','avr','mai','jun','jul','aoû','sep','oct','nov','déc'],
							es: ['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic']
						};
						var mList = mNames[lang] || mNames.en;
						weeks[weekKey] = weekStart.getDate() + ' ' + mList[weekStart.getMonth()] +
							' → ' + weekEnd.getDate() + ' ' + mList[weekEnd.getMonth()];
					});
					var weekKeys = Object.keys(weeks).sort().reverse();
					var selectedWeek = evolutionSelectedWeek || '';
					if (weekKeys.length && weeks[selectedWeek] === undefined) {
						evolutionSelectedWeek = '';
						selectedWeek = '';
					}
					if (!weekKeys.length) {
						evolutionSelectedWeek = '';
						selectedWeek = '';
					}
					var weekFilterHtml = weekKeys.length > 1
						? '<div class="avi-hm-evo-week-filter">' +
							'<button type="button" class="avi-hm-evo-week-btn' +
							(!evolutionSelectedWeek ? ' is-active' : '') +
							'" data-avi-hm-evo-week="">' +
							(getHmLang() === 'fr' ? 'Tout' : getHmLang() === 'es' ? 'Todo' : 'All') +
							'</button>' +
							weekKeys.map(function(k) {
								return '<button type="button" class="avi-hm-evo-week-btn' +
									(k === selectedWeek ? ' is-active' : '') +
									'" data-avi-hm-evo-week="' + k + '">' +
									weeks[k] + '</button>';
							}).join('') +
							'</div>'
						: '';
					var days = {};
					var weekDayScans = selectedWeek
						? dayScans.filter(function(s) {
							if (!s.scan_date) return false;
							var d = new Date(s.scan_date);
							var startOfYear = new Date(d.getFullYear(), 0, 1);
							var weekNum = Math.ceil(((d - startOfYear) / 86400000 + startOfYear.getDay() + 1) / 7);
							var wk = d.getFullYear() + '-W' + String(weekNum).padStart(2, '0');
							return wk === selectedWeek;
						})
						: dayScans;
					weekDayScans.forEach(function(s) {
						if (!s.scan_date) return;
						var parts = String(s.scan_date).split('-');
						if (parts.length < 3) return;
						var dayKey = String(s.scan_date).substring(0, 10);
						var lang = getHmLang();
						var mNames = {
							en: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
							fr: ['jan.','fév.','mar.','avr.','mai','juin','juil.','août','sep.','oct.','nov.','déc.'],
							es: ['ene.','feb.','mar.','abr.','may.','jun.','jul.','ago.','sep.','oct.','nov.','dic.']
						};
						var mList = mNames[lang] || mNames.en;
						var mIdx = parseInt(parts[1], 10) - 1;
						days[dayKey] = parseInt(parts[2], 10) + ' ' + (mList[mIdx] || parts[1]) + ' ' + parts[0];
					});
					var dayKeys = Object.keys(days).sort().reverse();
					var selectedDay = evolutionSelectedDay || '';
					if (dayKeys.length && days[selectedDay] === undefined) {
						evolutionSelectedDay = '';
						selectedDay = '';
					}
					if (!dayKeys.length) {
						evolutionSelectedDay = '';
						selectedDay = '';
					}
					var dayFilterHtml = dayKeys.length > 1
						? '<div class="avi-hm-evo-day-filter">' +
							'<button type="button" class="avi-hm-evo-day-btn' +
							(!evolutionSelectedDay ? ' is-active' : '') +
							'" data-avi-hm-evo-day="">' +
							(getHmLang() === 'fr' ? 'Tout' : getHmLang() === 'es' ? 'Todo' : 'All') +
							'</button>' +
							dayKeys.map(function(k) {
								return '<button type="button" class="avi-hm-evo-day-btn' +
									(k === selectedDay ? ' is-active' : '') + '" data-avi-hm-evo-day="' + k + '">' +
									days[k] + '</button>';
							}).join('') +
							'</div>'
						: '';
					var filteredScans = evolutionScans.filter(function(s) {
						if (!s.scan_date) return true;
						var dateStr = String(s.scan_date).substring(0, 10);
						if (selectedDay) return dateStr === selectedDay;
						if (selectedWeek) {
							var d = new Date(s.scan_date);
							var startOfYear = new Date(d.getFullYear(), 0, 1);
							var weekNum = Math.ceil(((d - startOfYear) / 86400000 + startOfYear.getDay() + 1) / 7);
							var wk = d.getFullYear() + '-W' + String(weekNum).padStart(2, '0');
							return wk === selectedWeek;
						}
						if (selectedMonth) return String(s.scan_date).indexOf(selectedMonth) === 0;
						return true;
					});
					sessionGroups = {};
					var noSessionScans = [];
					filteredScans.forEach(function(s) {
						if (s.scan_session_id) {
							if (!sessionGroups[s.scan_session_id]) sessionGroups[s.scan_session_id] = [];
							sessionGroups[s.scan_session_id].push(s);
						} else {
							noSessionScans.push(s);
						}
					});
					var sortLabel = evolutionSortDesc
						? (hm_t('evolution_sort_' + 'recent') || '↓ Most recent first')
						: (hm_t('evolution_sort_oldest') || '↑ Oldest first');
					var sortHtml = '<div class="avi-hm-evo-sort-row">' +
						'<button type="button" class="avi-hm-evo-sort-inline-btn" data-avi-hm-evo-sort>' +
						sortLabel + '</button>' +
						'</div>';
					var html = monthFilterHtml + weekFilterHtml + dayFilterHtml + sortHtml + '<div class="avi-hm-evolution-list-scroll">' +
						'<div class="avi-hm-evolution-list-header">' +
						'<span class="avi-hm-evolution-header-check"></span>' +
						'<span class="avi-hm-evolution-header-date">' + (hm_t('evolution_date_label') || 'Date') + '</span>' +
						'<span class="avi-hm-evolution-header-grid">' + (hm_t('evolution_grid_label') || 'Grid') +
							'<button type="button" style="' + evoInfoStyle + '" data-avi-hm-evo-info="grid">ℹ️</button></span>' +
						'<span class="avi-hm-evolution-header-rank">' + (hm_t('evolution_rank_label') || 'Avg rank') +
							'<button type="button" style="' + evoInfoStyle + '" data-avi-hm-evo-info="rank">ℹ️</button></span>' +
						'<span class="avi-hm-evolution-header-territory">' + (hm_t('evolution_territory_label') || 'Top 3 territory') +
							'<button type="button" style="' + evoInfoStyle + '" data-avi-hm-evo-info="territory">ℹ️</button></span>' +
						'<span class="avi-hm-evolution-header-delete"></span>' +
						'</div>';
					function buildScanItemHtml(scan) {
						var idx = filteredScans.indexOf(scan);
						if (idx < 0) idx = evolutionScans.indexOf(scan);
						var scanHtml = '';
						var isSelected = evolutionSelected.indexOf(scan.scan_id) >= 0;
						var gridLabel = scan.grid_size + '×' + scan.grid_size;
						var sessionBadge = scan.scan_session_id
							? '<span class="avi-hm-evo-session-badge" title="' +
								(getHmLang() === 'fr' ? 'Scans lancés ensemble (même session)'
								: getHmLang() === 'es' ? 'Escaneos lanzados juntos (misma sesión)'
								: 'Scans launched together (same session)') +
								'" data-session-id="' + String(scan.scan_session_id).substring(0, 8) + '">🔗</span>'
							: '';
						var rankVal = scan.avg_rank !== null && scan.avg_rank !== undefined
							? Math.round(scan.avg_rank * 10) / 10 : null;
						var terrVal = scan.territory_pct !== null && scan.territory_pct !== undefined
							? Math.round(scan.territory_pct * 10) / 10 : null;
						var rankDelta = '';
						var terrDelta = '';
						scanHtml += '<div class="avi-hm-evolution-scan-item' + (isSelected ? ' is-selected' : '') + '" ' +
							'data-avi-hm-scan-item="' + scan.scan_id + '">' +
							'<input type="checkbox" ' + (isSelected ? 'checked' : '') + ' ' +
							((!isSelected && evolutionSelected.length >= 5) ? 'disabled' : '') + ' ' +
							'data-avi-hm-scan-check="' + scan.scan_id + '">' +
							'<div class="avi-hm-evolution-scan-col avi-hm-evolution-scan-col--date">' +
							'<strong>' + formatEvolutionDate(scan.scan_date) + ' ' + sessionBadge + '</strong>' +
							'</div>' +
							'<div class="avi-hm-evolution-scan-col avi-hm-evolution-scan-col--grid">' +
							'<span class="avi-hm-evolution-scan-badge">' + gridLabel + '</span>' +
							'</div>' +
							'<div class="avi-hm-evolution-scan-col avi-hm-evolution-scan-col--rank">' +
							(rankVal !== null ? '<span class="avi-hm-evolution-scan-badge avi-hm-evolution-scan-badge--rank">' +
								rankVal + ' ' + rankDelta + '</span>' : '<span>—</span>') +
							'</div>' +
							'<div class="avi-hm-evolution-scan-col avi-hm-evolution-scan-col--territory">' +
							(terrVal !== null ? '<span class="avi-hm-evolution-scan-badge avi-hm-evolution-scan-badge--territory">' +
								terrVal + '% ' + terrDelta + '</span>' : '<span>—</span>') +
							'</div>' +
							'<button type="button" class="avi-hm-evolution-scan-delete" ' +
							'data-avi-hm-scan-delete="' + scan.scan_id + '">' +
							(hm_t('evolution_delete_btn') || 'Delete') + '</button>';
						return scanHtml + '</div>';
					}
					var sessionKeys = Object.keys(sessionGroups);
					if (sessionKeys.length) {
						sessionKeys.forEach(function(sid) {
							var group = sessionGroups[sid];
							var firstScan = group[0];
							html += '<div class="avi-hm-evo-group-header">' +
								'<span class="avi-hm-evo-group-label">🔗 ' +
								formatEvolutionDate(firstScan.scan_date) + ' · ' +
								group.length + ' ' + (getHmLang() === 'fr' ? 'mots-clés'
									: getHmLang() === 'es' ? 'palabras clave' : 'keywords') +
								'</span>' +
								'<button type="button" class="avi-hm-evo-group-select-all" ' +
								'data-avi-hm-evo-session="' + sid + '">' +
								(getHmLang() === 'fr' ? 'Tout sélectionner'
									: getHmLang() === 'es' ? 'Seleccionar todo'
									: 'Select all') +
								'</button>' +
								'</div>';
							group.forEach(function(scan) {
								html += buildScanItemHtml(scan);
							});
						});
					}
					if (noSessionScans.length) {
						if (sessionKeys.length) {
							html += '<div class="avi-hm-evo-group-header avi-hm-evo-group-header--solo">' +
								'<span class="avi-hm-evo-group-label">' +
								(getHmLang() === 'fr' ? 'Scans individuels'
									: getHmLang() === 'es' ? 'Escaneos individuales'
									: 'Individual scans') +
								'</span></div>';
						}
						noSessionScans.forEach(function(scan) {
							html += buildScanItemHtml(scan);
						});
					}
					html += '</div><div class="avi-hm-evolution-actions">' +
						(evolutionSelected.length >= 2
							? '<button type="button" class="avi-hm-focus-btn" data-avi-hm-focus-open>' +
								(hm_t('evo_focus_btn') || '⛶ Focus mode') +
								'</button>'
							: '') +
						'<button type="button" class="avi-hm-evolution-compare-btn" ' +
						'data-avi-hm-compare ' +
						(evolutionSelected.length < 2 ? 'disabled' : '') + '>' +
						(hm_t('evolution_compare_btn') || 'Compare selected scans') +
						' (' + evolutionSelected.length + ')' +
						'</button>' +
						'</div>';
					selectorEl.innerHTML = html;
					bindEvolutionSelectorEvents();
					root.querySelectorAll('[data-avi-hm-evo-month]').forEach(function(btn) {
						btn.addEventListener('click', function() {
							evolutionSelectedMonth = btn.getAttribute('data-avi-hm-evo-month') || '';
							evolutionSelectedWeek = '';
							evolutionSelectedDay = '';
							renderEvolutionSelector();
						});
					});
					root.querySelectorAll('[data-avi-hm-evo-week]').forEach(function(btn) {
						btn.addEventListener('click', function() {
							evolutionSelectedWeek = btn.getAttribute('data-avi-hm-evo-week') || '';
							evolutionSelectedDay = '';
							renderEvolutionSelector();
						});
					});
					root.querySelectorAll('[data-avi-hm-evo-day]').forEach(function(btn) {
						btn.addEventListener('click', function() {
							evolutionSelectedDay = btn.getAttribute('data-avi-hm-evo-day') || '';
							renderEvolutionSelector();
						});
					});
				}
				var refreshEvoSelector = renderEvolutionSelector;

				function bindEvolutionSelectorEvents() {
					root.querySelectorAll('[data-avi-hm-scan-check]').forEach(function(chk) {
						chk.addEventListener('change', function() {
							var scanId = parseInt(chk.getAttribute('data-avi-hm-scan-check'), 10);
							if (chk.checked) {
								if (evolutionSelected.indexOf(scanId) < 0 && evolutionSelected.length < 5) {
									evolutionSelected.push(scanId);
								}
							} else {
								evolutionSelected = evolutionSelected.filter(function(id) { return id !== scanId; });
							}
							renderEvolutionSelector();
						});
					});
					root.querySelectorAll('[data-avi-hm-scan-item]').forEach(function(item) {
						item.addEventListener('click', function(e) {
							if (e.target.type === 'checkbox' || e.target.tagName === 'BUTTON') return;
							var chk = item.querySelector('[data-avi-hm-scan-check]');
							if (!chk || chk.disabled) return;
							chk.checked = !chk.checked;
							chk.dispatchEvent(new Event('change'));
						});
					});
					var selectorContainer = root.querySelector('[data-avi-hm-evolution-selector]') || root;
					selectorContainer.querySelectorAll('[data-avi-hm-evo-info]').forEach(function(btn) {
						btn.addEventListener('click', function(e) {
							e.stopPropagation();
							var key = btn.getAttribute('data-avi-hm-evo-info');
							var lang = getHmLang();
							var tips = {
								grid: {
									en: 'Grid size defines the geographic area scanned. 3×3 = your street (9 points). 7×7 = your neighbourhood (49 points). Larger grids cost more credits but give a fuller picture.',
									fr: 'La taille de grille définit la zone géographique scannée. 3×3 = votre rue (9 points). 7×7 = votre quartier (49 points). Les grandes grilles coûtent plus de crédits mais donnent une vue plus complète.',
									es: 'El tamaño de cuadrícula define el área geográfica escaneada. 3×3 = su calle (9 puntos). 7×7 = su barrio (49 puntos). Las cuadrículas grandes cuestan más créditos pero dan una vista más completa.',
									de: 'Die Rastergröße definiert den gescannten geografischen Bereich. 3×3 = deine Straße (9 Punkte). 7×7 = dein Viertel (49 Punkte). Größere Raster kosten mehr Credits, liefern aber ein vollständigeres Bild.',
									'pt-br': 'O tamanho da grade define a área geográfica escaneada. 3×3 = sua rua (9 pontos). 7×7 = seu bairro (49 pontos). Grades maiores custam mais créditos mas oferecem uma visão mais completa.',
									tr: 'Izgara boyutu taranan coğrafi alanı tanımlar. 3×3 = sokağın (9 nokta). 7×7 = mahallen (49 nokta). Büyük ızgaralar daha fazla kredi maliyeti getirir ama daha tam bir görünüm sağlar.'
								},
								rank: {
									en: 'Average rank is the mean position across all scanned points. The lower the number, the better (rank 1 = top position). A rank of 4.9 means you appear around position 5 on average across the area.',
									fr: 'Le rang moyen est la position moyenne sur tous les points scannés. Plus le chiffre est bas, mieux c\'est (rang 1 = première position). Un rang de 4,9 signifie que vous apparaissez en moyenne vers la position 5 sur la zone.',
									es: 'El rango promedio es la posición media en todos los puntos escaneados. Cuanto más bajo el número, mejor (rango 1 = primera posición). Un rango de 4,9 significa que aparece alrededor de la posición 5 en promedio.',
									de: 'Der Durchschnittsrang ist die mittlere Position über alle gescannten Punkte. Je niedriger die Zahl, desto besser (Rang 1 = Spitzenposition). Ein Rang von 4,9 bedeutet, dass du im Durchschnitt um Position 5 erscheinst.',
									'pt-br': 'O rank médio é a posição média em todos os pontos escaneados. Quanto menor o número, melhor (rank 1 = primeira posição). Um rank de 4,9 significa que você aparece em média na posição 5 na área.',
									tr: 'Ortalama sıralama, taranan tüm noktalardaki ortalama konumdur. Sayı ne kadar düşükse o kadar iyidir (sıralama 1 = en üst konum). 4,9 sıralaması, bölgede ortalama olarak 5. konumda göründüğün anlamına gelir.'
								},
								territory: {
									en: 'Top 3 Territory shows the % of scanned points where you rank in the top 3. Important: a 3×3 grid (your street) naturally gives higher % than a 7×7 grid (your neighbourhood) — because it covers a smaller area. Always compare scans of the same grid size for meaningful results.',
									fr: 'Le Territoire Top 3 montre le % de points scannés où vous êtes dans le top 3. Important : une grille 3×3 (votre rue) donne naturellement un % plus élevé qu\'une grille 7×7 (votre quartier) car elle couvre une zone plus petite. Comparez toujours des scans de même taille de grille pour des résultats pertinents.',
									es: 'El Territorio Top 3 muestra el % de puntos escaneados donde está en el top 3. Importante: una cuadrícula 3×3 (su calle) da naturalmente un % más alto que una 7×7 (su barrio) porque cubre un área más pequeña. Compare siempre escaneos del mismo tamaño de cuadrícula para resultados relevantes.',
									de: 'Top-3-Gebiet zeigt den % der gescannten Punkte, an denen du in den Top 3 bist. Wichtig: Ein 3×3-Raster (deine Straße) ergibt natürlich einen höheren % als ein 7×7-Raster (dein Viertel) — weil es einen kleineren Bereich abdeckt. Vergleiche immer Scans gleicher Rastergröße für aussagekräftige Ergebnisse.',
									'pt-br': 'O Território Top 3 mostra o % de pontos escaneados onde você está no top 3. Importante: uma grade 3×3 (sua rua) naturalmente dá % maior que uma 7×7 (seu bairro) — porque cobre uma área menor. Sempre compare scans do mesmo tamanho de grade para resultados relevantes.',
									tr: 'Top 3 Bölgesi, ilk 3\'te yer aldığın taranan noktaların %\'sini gösterir. Önemli: 3×3 ızgara (sokağın) doğal olarak 7×7 ızgaradan (mahallen) daha yüksek % verir — çünkü daha küçük bir alanı kapsar. Anlamlı sonuçlar için her zaman aynı ızgara boyutundaki taramaları karşılaştır.'
								}
							};
							var tip = tips[key] ? (tips[key][lang] || tips[key].en) : '';
							if (!tip) return;
							var existing = document.querySelector('.avi-hm-evo-tooltip');
							if (existing) {
								existing.parentNode.removeChild(existing);
								if (existing.getAttribute('data-evo-info-key') === key) return;
							}
							var tooltip = document.createElement('div');
							tooltip.className = 'avi-hm-evo-tooltip';
							tooltip.style.cssText = 'position:fixed;z-index:100002;max-width:280px;padding:10px 12px;' +
								'border-radius:10px;background:#0f172a;color:#fff;font-size:.75rem;line-height:1.5;' +
								'font-weight:500;box-shadow:0 8px 24px rgba(0,0,0,.3);pointer-events:none;';
							var titles = {
								grid:      { en: 'Grid Size',       fr: 'Taille de grille',  es: 'Tamaño de cuadrícula', de: 'Rastergröße',              'pt-br': 'Tamanho da grade',   tr: 'Izgara boyutu' },
								rank:      { en: 'Average Rank',    fr: 'Rang moyen',        es: 'Rango promedio',        de: 'Durchschnittlicher Rang',  'pt-br': 'Rank médio',         tr: 'Ortalama sıralama' },
								territory: { en: 'Top 3 Territory', fr: 'Territoire Top 3',  es: 'Territorio Top 3',      de: 'Top-3-Gebiet',             'pt-br': 'Território Top 3',   tr: 'Top 3 Bölgesi' }
							};
							var titleText = titles[key] ? (titles[key][lang] || titles[key].en) : key;
							tooltip.innerHTML =
								'<div style="font-size:.7rem;font-weight:900;text-transform:uppercase;' +
								'letter-spacing:.06em;color:#94a3b8;margin-bottom:6px">' + titleText + '</div>' +
								'<div style="line-height:1.6">' + tip.replace(/[.] /g, '.<br><br>') + '</div>';
							tooltip.setAttribute('data-evo-info-key', key);
							var rect = btn.getBoundingClientRect();
							document.body.appendChild(tooltip);
							var tooltipLeft = Math.min(rect.left, window.innerWidth - 300);
							var tooltipTop = rect.bottom + 8;
							if (tooltipTop + 150 > window.innerHeight) {
								tooltipTop = rect.top - 158;
							}
							tooltip.style.left = tooltipLeft + 'px';
							tooltip.style.top = tooltipTop + 'px';
							setTimeout(function() {
								document.addEventListener('click', function remove() {
									var t = root.querySelector('.avi-hm-evo-tooltip') ||
										document.querySelector('.avi-hm-evo-tooltip');
									if (t) t.parentNode.removeChild(t);
									document.removeEventListener('click', remove);
								});
							}, 10);
						});
					});
					root.querySelectorAll('[data-avi-hm-evo-session]').forEach(function(btn) {
						btn.addEventListener('click', function(e) {
							e.stopPropagation();
							var sid = btn.getAttribute('data-avi-hm-evo-session');
							var group = sessionGroups[sid] || [];
							group.slice(0, Math.min(5, group.length)).forEach(function(s) {
								if (evolutionSelected.indexOf(s.scan_id) < 0 && evolutionSelected.length < 5) {
									evolutionSelected.push(s.scan_id);
								}
							});
							renderEvolutionSelector();
						});
					});
					root.querySelectorAll('[data-session-id]').forEach(function(badge) {
						badge.addEventListener('click', function(e) {
							e.stopPropagation();
							var sessionId = badge.getAttribute('data-session-id');
							if (!sessionId) return;
							var sessionScans = evolutionScans.filter(function(s) {
								return s.scan_session_id && s.scan_session_id.substring(0, 8) === sessionId;
							});
							if (!sessionScans.length) return;
							sessionScans.slice(0, Math.min(5, sessionScans.length)).forEach(function(s) {
								if (evolutionSelected.indexOf(s.scan_id) < 0) {
									evolutionSelected.push(s.scan_id);
								}
							});
							renderEvolutionSelector();
						});
					});
					var selectorEl2 = root.querySelector('[data-avi-hm-evolution-selector]') || root;
					var sortBtn = selectorEl2.querySelector('[data-avi-hm-evo-sort]');
					if (sortBtn) {
						sortBtn.addEventListener('click', function(e) {
							e.stopPropagation();
							evolutionSortDesc = !evolutionSortDesc;
							evolutionScans.reverse();
							sortBtn.textContent = evolutionSortDesc
								? (hm_t('evolution_sort_' + 'recent') || '↓ Most recent first')
								: (hm_t('evolution_sort_oldest') || '↑ Oldest first');
							renderEvolutionSelector();
						});
					}
					root.querySelectorAll('[data-avi-hm-scan-delete]').forEach(function(btn) {
						btn.addEventListener('click', function(e) {
							e.stopPropagation();
							var scanId = parseInt(btn.getAttribute('data-avi-hm-scan-delete'), 10);
							if (!window['confirm'](hm_t('evolution_delete_confirm') || 'Delete this scan?')) return;
							fetch('/wp-json/avision/v1/heatmap-scan/' + scanId, {
								method: 'DELETE',
								credentials: 'same-origin',
								headers: restHeaders()
							}).then(function(r) {
								evolutionScans = evolutionScans.filter(function(s) { return s.scan_id !== scanId; });
								evolutionSelected = evolutionSelected.filter(function(id) { return id !== scanId; });
								refreshEvoSelector();
							}).catch(function() { refreshEvoSelector(); });
						});
					});
					var compareBtn = root.querySelector('[data-avi-hm-compare]');
					if (compareBtn) {
						compareBtn.addEventListener('click', function() {
							if (evolutionSelected.length < 2) return;
							renderEvolutionComparison();
						});
					}
					var focusOpenBtn = root.querySelector('[data-avi-hm-focus-open]');
					if (focusOpenBtn) {
						focusOpenBtn.onclick = function() { openHmFocusMode(); };
					}
				}

				function fetchEvolutionHistory(keyword) {
					var selectorEl = root.querySelector('[data-avi-hm-evolution-selector]');
					if (selectorEl) {
						selectorEl.innerHTML = '<p class="avi-hm-evolution-loading">' +
							(hm_t('evolution_loading') || 'Loading scan history...') + '</p>';
					}
					evolutionScans = [];
					evolutionSelected = [];
					fetch(historyUrl(keyword), {
						method: 'GET',
						credentials: 'same-origin',
						headers: restHeaders()
					}).then(function(r) {
						return r.json().catch(function() { return {}; });
					}).then(function(data) {
						var scans = (data && Array.isArray(data.scans)) ? data.scans : [];
						var sessionIds = [];
						scans.forEach(function(s) {
							if (s.scan_session_id && sessionIds.indexOf(s.scan_session_id) < 0) {
								sessionIds.push(s.scan_session_id);
							}
						});
						if (!sessionIds.length) {
							evolutionScans = scans;
							renderEvolutionSelector();
							return;
						}
						Promise.all(sessionIds.map(function(sid) {
							return fetch(historyUrl(keyword, sid), {
								method: 'GET',
								credentials: 'same-origin',
								headers: restHeaders()
							}).then(function(r) {
								return r.json().catch(function() { return {}; });
							}).then(function(d) {
								return Array.isArray(d.scans) ? d.scans : [];
							}).catch(function() { return []; });
						})).then(function(results) {
							var allScans = scans.slice();
							results.forEach(function(sessionScans) {
								sessionScans.forEach(function(s) {
									if (!allScans.some(function(existing) { return existing.scan_id === s.scan_id; })) {
										allScans.push(s);
									}
								});
							});
							allScans.sort(function(a, b) {
								return new Date(b.scan_date) - new Date(a.scan_date);
							});
							evolutionScans = allScans;
							renderEvolutionSelector();
						});
					}).catch(function() {
						evolutionScans = [];
						renderEvolutionSelector();
					});
				}

				function bindTabSwitcher() {
					var tabs = root.querySelectorAll('[data-avi-heatmap-tab]');
					var panels = root.querySelectorAll('[data-avi-heatmap-panel]');
					tabs.forEach(function(tab) {
						tab.addEventListener('click', function() {
							var target = tab.getAttribute('data-avi-heatmap-tab');
							tabs.forEach(function(t) { t.classList.toggle('is-active', t.getAttribute('data-avi-heatmap-tab') === target); });
							panels.forEach(function(p) {
								var isTarget = p.getAttribute('data-avi-heatmap-panel') === target;
								if (isTarget) {
									p.removeAttribute('hidden');
								} else {
									p.setAttribute('hidden', '');
								}
							});
							var modalLayout = root.querySelector('.avi-heatmap-modal-layout');
							var modalInfo = root.querySelector('.avi-heatmap-modal-info');
							if (target === 'evolution') {
								if (modalInfo) modalInfo.style.display = 'none';
								if (modalLayout) modalLayout.style.gridTemplateColumns = '1fr';
								var kw = selectedMapKeywords.length ? selectedMapKeywords[0] : selectedKeyword;
								if (kw) fetchEvolutionHistory(kw);
								else {
									var selectorEl = root.querySelector('[data-avi-hm-evolution-selector]');
									if (selectorEl) selectorEl.innerHTML = '<p class="avi-hm-evolution-loading">' +
										(hm_t('evolution_select_hint') || 'Select a keyword first.') + '</p>';
								}
							}
							if (target === 'competitors') {
								if (modalInfo) {
									modalInfo.style.display = 'none';
								}
								if (modalLayout) {
									modalLayout.style.gridTemplateColumns =
										'1fr';
								}
								renderCompetitorsTab();
							}
							if (target === 'map') {
								if (modalInfo) modalInfo.style.display = '';
								if (modalLayout) modalLayout.style.gridTemplateColumns = '';
								setTimeout(function() {
									if (leafletMap) leafletMap.invalidateSize();
								}, 120);
							}
						});
					});
				}

				document.addEventListener('click', function(e) {
					var btnExcel = e.target.closest('[data-avi-hm-export="excel"]');
					if (btnExcel) {
						exportCompetitorsToExcel();
					}
					var btnPdf = e.target.closest('[data-avi-hm-export="pdf"]');
					if (btnPdf) {
						exportCompetitorsToPDF();
					}
				});

				function renderEvolutionComparison() {
					var gridEl = root.querySelector('[data-avi-hm-evolution-grid]');
					var kpisEl = root.querySelector('[data-avi-hm-evolution-kpis]');
					if (!gridEl) return;
					var selectedScans = evolutionSelected.map(function(id) {
						return evolutionScans.find(function(s) { return s.scan_id === id; });
					}).filter(Boolean);
					var refScanId = (evolutionRefScanId && selectedScans.some(function(s) {
						return s.scan_id === evolutionRefScanId;
					})) ? evolutionRefScanId : (selectedScans.length ? selectedScans[0].scan_id : null);
					var refScanForGrid = selectedScans.find(function(s) { return s.scan_id === refScanId; }) || selectedScans[0];
					var count = selectedScans.length;
					var refGridSize = refScanForGrid ? (refScanForGrid.grid_size || 0) : 0;
					var hasMixedGrids = selectedScans.some(function(s) {
						return (s.grid_size || 0) !== refGridSize;
					});
					var mixedGridWarning = hasMixedGrids
						? '<div class="avi-hm-evo-mixed-warning" data-hm-evo-mixed-warning>' +
							'⚠️ <span data-hm-i18n="evo_mixed_grids_warning">Mixed grid sizes detected. Comparisons are approximate — only center points are shared between different grid sizes.</span>' +
							'</div>'
						: '';
					gridEl.setAttribute('data-count', String(count));
					var gridHtml = mixedGridWarning;
					gridHtml += selectedScans.map(function(scan, idx) {
						var gridLabel = scan.grid_size + '×' + scan.grid_size;
						var rankVal = scan.avg_rank !== null ? Math.round(scan.avg_rank * 10) / 10 : null;
						var terrVal = scan.territory_pct !== null ? Math.round(scan.territory_pct * 10) / 10 : null;
						var isRef = scan.scan_id === refScanId;
						var ref = selectedScans.find(function(s) { return s.scan_id === refScanId; }) || selectedScans[0];
						var sameGrid = (scan.grid_size || 0) === refGridSize;
						var sessionKeywords = [];
						if (scan.scan_session_id) {
							selectedScans.forEach(function(s) {
								if (s.scan_session_id === scan.scan_session_id) {
									var kw = s.keyword || '';
									if (kw && sessionKeywords.indexOf(kw) < 0) sessionKeywords.push(kw);
								}
							});
						}
						if (!sessionKeywords.length && scan.keyword) sessionKeywords = [scan.keyword];
						var kwButtonsHtml = sessionKeywords.length > 1
							? '<div class="avi-hm-evo-mini-kw-btns" data-avi-hm-mini-kw-btns="' + scan.scan_id + '">' +
								sessionKeywords.map(function(kw, i) {
									return '<button type="button" ' +
										'class="avi-hm-evo-mini-kw-btn' + (i === 0 ? ' is-active' : '') + '" ' +
										'data-avi-hm-mini-kw="' + kw.replace(/"/g, '&quot;') + '" ' +
										'data-avi-hm-mini-scan="' + scan.scan_id + '" ' +
										'title="' + kw.replace(/"/g, '&quot;') + '">' +
										(kw.length > 20 ? kw.substring(0, 18) + '…' : kw) +
										'</button>';
								}).join('') +
								'</div>'
							: '';
						var rankD = (!isRef && rankVal !== null && ref.avg_rank !== null)
							? deltaHtml(rankVal, ref.avg_rank, true) : '';
						var terrD = (!isRef && terrVal !== null && ref.territory_pct !== null)
							? deltaHtml(terrVal, ref.territory_pct, false) : '';
						var commonPointsNote = (!isRef && !sameGrid && refGridSize > 0)
							? '<div class="avi-hm-evo-common-points">' +
								(refGridSize * refGridSize) + ' ' + hm_t('evo_common_points') +
								'</div>'
							: '';
						var refBadge = isRef
							? '<span class="avi-hm-evo-ref-badge" title="' + hm_t('evo_ref_tooltip') + '">📌 ' +
								hm_t('evo_ref_' + 'label') + '</span>'
							: '<button type="button" class="avi-hm-evo-set-ref-btn" ' +
								'data-avi-hm-set-ref="' + scan.scan_id + '" ' +
								'title="' + (getHmLang() === 'fr' ? 'Définir comme référence'
									: getHmLang() === 'es' ? 'Definir como referencia'
									: 'Set as reference') + '">📌</button>' +
								(hasMixedGrids && (scan.grid_size || 0) !== refGridSize
									? '<span class="avi-hm-evo-diff-grid-badge" title="' + hm_t('evo_diff_grid_tooltip') + '">⚠️ ' +
										scan.grid_size + '×' + scan.grid_size + '</span>'
									: '');
						return '<div class="avi-hm-evolution-mini-card">' +
							'<div class="avi-hm-evolution-mini-header">' +
							'<div class="avi-hm-evo-mini-header-top">' +
							'<div style="display:flex;align-items:center;gap:6px">' +
							'<span class="avi-hm-evo-drag-handle" ' +
							'data-avi-hm-drag-handle="' + scan.scan_id + '" ' +
							'draggable="true" ' +
							'title="' + (getHmLang() === 'fr' ? 'Glisser pour réordonner'
								: getHmLang() === 'es' ? 'Arrastrar para reordenar'
								: 'Drag to reorder') + '">≡</span>' +
							'<strong class="avi-hm-evo-mini-date">' + formatEvolutionDate(scan.scan_date) + '</strong>' +
							'</div>' +
							'<div style="display:flex;gap:5px;align-items:center;flex-shrink:0">' +
							'<span class="avi-hm-evo-mini-grid-badge">' + gridLabel + '</span>' +
							refBadge +
							(count > 1
								? '<div class="avi-hm-evo-mini-order-btns">' +
									'<button type="button" class="avi-hm-evo-mini-order-btn" ' +
									'data-avi-hm-evo-move="up" data-avi-hm-evo-scan-id="' + scan.scan_id + '" ' +
									(idx === 0 ? 'disabled' : '') + ' title="Move left">◀</button>' +
									'<button type="button" class="avi-hm-evo-mini-order-btn" ' +
									'data-avi-hm-evo-move="down" data-avi-hm-evo-scan-id="' + scan.scan_id + '" ' +
									(idx === count - 1 ? 'disabled' : '') + ' title="Move right">▶</button>' +
									'</div>'
								: '') +
							'</div>' +
							'</div>' +
							'</div>' +
							kwButtonsHtml +
							'<div class="avi-hm-evolution-mini-map" id="avi-hm-evo-map-' + scan.scan_id + '" ' +
							'data-avi-hm-focus-map data-scan-id="' + scan.scan_id + '" data-grid-size="' + (scan.grid_size || 7) + '" ' +
							'style="height:' + (scan.grid_size <= 3 ? 160 : scan.grid_size <= 7 ? 200 : scan.grid_size <= 13 ? 240 : 280) + 'px"></div>' +
							'<div class="avi-hm-evolution-mini-kpis">' +
							'<div class="avi-hm-evolution-mini-kpi avi-hm-evolution-mini-kpi--' + 'rank">' +
							'<strong>' + (rankVal !== null ? rankVal : '—') +
							(rankD ? '<br>' + rankD : (isRef ? '<br><span class="avi-hm-evo-ref-placeholder">ref.</span>' : '')) +
							'</strong>' +
							'<span>' + (hm_t('evolution_rank_label') || 'Avg rank') + '</span></div>' +
							'<div class="avi-hm-evolution-mini-kpi avi-hm-evolution-mini-kpi--' + 'territory">' +
							'<strong>' + (terrVal !== null ? terrVal + '%' : '—') +
							(terrD ? '<br>' + terrD : (isRef ? '<br><span class="avi-hm-evo-ref-placeholder">ref.</span>' : '')) +
							'</strong>' +
							'<span>' + (hm_t('evolution_territory_label') || 'Top 3 territory') + '</span></div>' +
							'<div class="avi-hm-evolution-mini-kpi avi-hm-evolution-mini-kpi--' + 'grid">' +
							'<strong>' + gridLabel + '</strong>' +
							'<span>' + (hm_t('evolution_grid_label') || 'Grid') + '</span></div>' +
							'</div>' +
							commonPointsNote +
							'</div>';
					}).join('');
					gridEl.innerHTML = gridHtml;
					var dragSrcId = null;
					gridEl.querySelectorAll('[data-avi-hm-drag-handle]').forEach(function(handle) {
						var card = handle.closest('.avi-hm-evolution-mini-card');
						if (!card) return;
						var scanId = parseInt(handle.getAttribute('data-avi-hm-drag-handle'), 10);
						handle.addEventListener('dragstart', function(e) {
							dragSrcId = scanId;
							e.dataTransfer.effectAllowed = 'move';
							e.dataTransfer.setData('text/plain', String(scanId));
							setTimeout(function() { card.classList.add('avi-hm-evo-dragging'); }, 0);
						});
						handle.addEventListener('dragend', function() {
							card.classList.remove('avi-hm-evo-dragging');
							gridEl.querySelectorAll('.avi-hm-evo-drag-over').forEach(function(el) {
								el.classList.remove('avi-hm-evo-drag-over');
							});
							dragSrcId = null;
						});
						card.addEventListener('dragover', function(e) {
							e.preventDefault();
							e.dataTransfer.dropEffect = 'move';
							gridEl.querySelectorAll('.avi-hm-evo-drag-over').forEach(function(el) {
								el.classList.remove('avi-hm-evo-drag-over');
							});
							card.classList.add('avi-hm-evo-drag-over');
						});
						card.addEventListener('dragleave', function() {
							card.classList.remove('avi-hm-evo-drag-over');
						});
						card.addEventListener('drop', function(e) {
							e.preventDefault();
							card.classList.remove('avi-hm-evo-drag-over');
							if (dragSrcId === null || dragSrcId === scanId) return;
							var srcIdx = evolutionSelected.indexOf(dragSrcId);
							var dstIdx = evolutionSelected.indexOf(scanId);
							if (srcIdx < 0 || dstIdx < 0) return;
							evolutionSelected.splice(srcIdx, 1);
							evolutionSelected.splice(dstIdx, 0, dragSrcId);
							renderEvolutionComparison();
						});
					});
					gridEl.querySelectorAll('[data-avi-hm-evo-move]').forEach(function(btn) {
						btn.addEventListener('click', function() {
							var direction = btn.getAttribute('data-avi-hm-evo-move');
							var scanId = parseInt(btn.getAttribute('data-avi-hm-evo-scan-id'), 10);
							var idx = evolutionSelected.indexOf(scanId);
							if (idx < 0) return;
							if (direction === 'up' && idx > 0) {
								var tmp = evolutionSelected[idx - 1];
								evolutionSelected[idx - 1] = evolutionSelected[idx];
								evolutionSelected[idx] = tmp;
							} else if (direction === 'down' && idx < evolutionSelected.length - 1) {
								var tmp2 = evolutionSelected[idx + 1];
								evolutionSelected[idx + 1] = evolutionSelected[idx];
								evolutionSelected[idx] = tmp2;
							}
							renderEvolutionComparison();
						});
					});
					gridEl.querySelectorAll('[data-avi-hm-set-ref]').forEach(function(btn) {
						btn.addEventListener('click', function() {
							evolutionRefScanId = parseInt(btn.getAttribute('data-avi-hm-set-ref'), 10);
							renderEvolutionComparison();
						});
					});
					gridEl.querySelectorAll('[data-avi-hm-mini-kw]').forEach(function(btn) {
						btn.addEventListener('click', function() {
							var activeKw = btn.getAttribute('data-avi-hm-mini-kw');
							var scanId = parseInt(btn.getAttribute('data-avi-hm-mini-scan') || '0', 10);
							var kwBtnsContainer = gridEl.querySelector(
								'[data-avi-hm-mini-kw-btns="' + scanId + '"]'
							);
							if (kwBtnsContainer) {
								kwBtnsContainer.querySelectorAll('[data-avi-hm-mini-kw]').forEach(function(b) {
									b.classList.toggle('is-active', b === btn);
								});
							}
							var mapEl = document.getElementById('avi-hm-evo-map-' + scanId);
							if (!mapEl || !mapEl._leafletLayer || !mapEl._leafletMap) return;
							mapEl._leafletLayer.clearLayers();
							var gridSz = parseInt(mapEl.getAttribute('data-grid-size') || '7', 10);
							var radius = gridSz <= 3 ? 9 : gridSz <= 7 ? 8 : gridSz <= 13 ? 6 : 5;
							var miniRenderer = mapEl._leafletRenderer || window.L.canvas({ padding: 0.5 });
							var scanToFetch = null;
							selectedScans.forEach(function(s) {
								if ((s.keyword || '') === activeKw &&
									s.scan_session_id === btn.closest('[data-avi-hm-mini-kw-btns]')
										?.getAttribute('data-avi-hm-mini-kw-btns') &&
									!scanToFetch) {
									scanToFetch = s;
								}
							});
							if (!scanToFetch) {
								selectedScans.forEach(function(s) {
									if ((s.keyword || '') === activeKw && !scanToFetch) scanToFetch = s;
								});
							}
							var fetchId = scanToFetch ? scanToFetch.scan_id : scanId;
							fetch(scanByIdUrl(fetchId), {
								method: 'GET',
								credentials: 'same-origin',
								headers: restHeaders()
							}).then(function(r) { return r.json().catch(function() { return {}; }); })
							.then(function(data) {
								var points = Array.isArray(data.map_points) ? data.map_points : [];
								var bounds = [];
								points.forEach(function(point) {
									if (!point || !isFinite(Number(point.lat)) || !isFinite(Number(point.lng))) return;
									var zone = normalizeZone(point);
									var bgColor = zone === 'top3' ? '#16a34a'
										: zone === 'found' ? '#84cc16'
										: zone === 'weak' ? '#f97316' : '#64748b';
									var latlng = [Number(point.lat), Number(point.lng)];
									var rankDisplay = (point.rank !== null && point.rank !== undefined && Number(point.rank) <= 20)
										? '#' + point.rank : '>20';
									window.L.circleMarker(latlng, {
										renderer: miniRenderer,
										radius: radius,
										color: '#fff',
										weight: (1.5),
										fillColor: (bgColor),
										fillOpacity: 1
									}).bindTooltip(
										'<div style="font-size:.75rem;font-weight:800;color:#0f172a;padding:2px 4px">' +
										rankDisplay + '<br><span style="color:#64748b;font-weight:600">' +
										String(activeKw).replace(/[<>&]/g, '') + '</span></div>',
										{ sticky: true, direction: 'top', offset: [0, -8] }
									).addTo(mapEl._leafletLayer);
									var fontSize = rankDisplay.length > 2 ? '7px' : (radius <= 7 ? '8px' : '9px');
									window.L.marker(latlng, {
										interactive: false,
										icon: window.L.divIcon({
											className: '',
											html: '<span style="display:flex;align-items:center;justify-content:center;' +
												'width:100%;height:100%;font-size:' + fontSize + ';font-weight:900;' +
												'color:#fff;pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,.4);">' +
												rankDisplay + '</span>',
											iconSize: [radius * 2, radius * 2],
											iconAnchor: [radius, radius]
										})
									}).addTo(mapEl._leafletLayer);
									bounds.push(latlng);
								});
								if (bounds.length) {
									try {
										mapEl._leafletMap.fitBounds(bounds, { padding: [12, 12], maxZoom: 15 });
									} catch(e) {}
								}
								mapEl._leafletMap.invalidateSize();
								var avgRank = points.length
									? Math.round((points.reduce(function(s, p) {
										return s + (Number(p.rank) || 21);
									}, 0) / points.length) * 10) / 10
									: null;
								var top3Points = points.filter(function(p) {
									return p.rank !== null && p.rank !== undefined && Number(p.rank) <= 3;
								});
								var territory = points.length
									? Math.round((top3Points.length / points.length) * 1000) / 10
									: null;
								var card = mapEl.closest('.avi-hm-evolution-mini-card');
								if (card) {
									var rankEl = card.querySelector('.avi-hm-evolution-mini-kpi--rank strong');
									var terrEl = card.querySelector('.avi-hm-evolution-mini-kpi--territory strong');
									var allCards = gridEl.querySelectorAll('.avi-hm-evolution-mini-card');
									var isRefCard = allCards.length > 0 && allCards[0] === card;
									var refRankEl = allCards.length > 0
										? allCards[0].querySelector('.avi-hm-evolution-mini-kpi--rank strong')
										: null;
									var refTerrEl = allCards.length > 0
										? allCards[0].querySelector('.avi-hm-evolution-mini-kpi--territory strong')
										: null;
									var refRank = refRankEl ? parseFloat(refRankEl.textContent) : null;
									var refTerr = refTerrEl ? parseFloat(refTerrEl.textContent) : null;
									var rankDelta = '';
									var terrDelta = '';
									if (!isRefCard && avgRank !== null && refRank !== null && !isNaN(refRank)) {
										rankDelta = deltaHtml(avgRank, refRank, true);
									}
									if (!isRefCard && territory !== null && refTerr !== null && !isNaN(refTerr)) {
										terrDelta = deltaHtml(territory, refTerr, false);
									}
									if (rankEl) rankEl.innerHTML = (avgRank !== null ? String(avgRank) : '—') +
										(rankDelta ? '<br>' + rankDelta : (isRefCard ? '<br><span class="avi-hm-evo-ref-placeholder">ref.</span>' : ''));
									if (terrEl) terrEl.innerHTML = (territory !== null ? territory + '%' : '—') +
										(terrDelta ? '<br>' + terrDelta : (isRefCard ? '<br><span class="avi-hm-evo-ref-placeholder">ref.</span>' : ''));
								}
							}).catch(function() {});
						});
					});
					var focusOpenBtn = root.querySelector('[data-avi-hm-focus-open]');
					if (focusOpenBtn) {
						focusOpenBtn.onclick = function() { openHmFocusMode(); };
					}
					if (kpisEl && selectedScans.length >= 2) {
						var first = selectedScans[0];
						var last = selectedScans[selectedScans.length - 1];
						var globalRankDelta = (first.avg_rank !== null && last.avg_rank !== null)
							? deltaHtml(last.avg_rank, first.avg_rank, true) : '';
						var globalTerrDelta = (first.territory_pct !== null && last.territory_pct !== null)
							? deltaHtml(last.territory_pct, first.territory_pct, false) : '';
						kpisEl.innerHTML =
							'<h4>📈 ' + formatEvolutionDate(first.scan_date) + ' → ' + formatEvolutionDate(last.scan_date) + '</h4>' +
							'<div class="avi-hm-evolution-kpis-values">' +
							'<div class="avi-hm-evolution-kpi-global">' +
							'<strong>' + (last.avg_rank !== null ? Math.round(last.avg_rank * 10) / 10 : '—') +
							' ' + globalRankDelta + '</strong>' +
							'<span>' + (hm_t('evolution_rank_label') || 'Avg rank') + '</span>' +
							'</div>' +
							'<div class="avi-hm-evolution-kpi-global">' +
							'<strong>' + (last.territory_pct !== null ? Math.round(last.territory_pct * 10) / 10 + '%' : '—') +
							' ' + globalTerrDelta + '</strong>' +
							'<span>' + (hm_t('evolution_territory_label') || 'Top 3 territory') + '</span>' +
							'</div>' +
							'</div>';
					}
					requestAnimationFrame(function() {
						selectedScans.forEach(function(scan) {
							var mapEl = document.getElementById('avi-hm-evo-map-' + scan.scan_id);
							if (!mapEl || !window.L || !config.center) return;
							var miniRenderer = window.L.canvas({ padding: 0.5 });
							var miniMap = window.L.map(mapEl, {
								zoomControl: !!1,
								attributionControl: false,
								renderer: miniRenderer,
								scrollWheelZoom: true,
								doubleClickZoom: true
							});
							window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
								maxZoom: 19
							}).addTo(miniMap);
							var miniLayer = window.L.layerGroup().addTo(miniMap);
							mapEl._leafletMap = miniMap;
							mapEl._leafletLayer = miniLayer;
							mapEl._leafletRenderer = miniRenderer;
							var gridSizeZ = scan.grid_size || 7;
							var radiusKmZ = scan.radius_km || 5;
							var zoomZ = gridSizeZ <= 3
								? (radiusKmZ <= 1 ? 16 : 15)
								: gridSizeZ <= 7
								? (radiusKmZ <= 3 ? 14 : 13)
								: gridSizeZ <= 13 ? 12 : 11;
							miniMap.setView([Number(config.center.lat), Number(config.center.lng)], zoomZ);
							setTimeout(function() { miniMap.invalidateSize(); }, 100);
							/* scan.keyword keeps each mini-card tied to its historical scan keyword. */
							var kwsToFetch = [scan.keyword || selectedKeyword || ''];
							var miniHaloColors = ['#10b981', '#f59e0b', '#ef4444'];
							var allBoundsEvo = [];
							Promise.all(kwsToFetch.map(function(kw, kwIdx) {
								return fetch(
									scanByIdUrl(scan.scan_id),
									{ method: 'GET', credentials: 'same-origin', headers: restHeaders() }
								).then(function(r) { return r.json().catch(function() { return {}; }); })
								.then(function(data) {
									return { kw: kw, kwIdx: kwIdx, points: Array.isArray(data.map_points) ? data.map_points : [] };
								}).catch(function() { return { kw: kw, kwIdx: kwIdx, points: [] }; });
							})).then(function(results) {
								results.forEach(function(result) {
									var kw = result.kw;
									var kwIdx = result.kwIdx;
									var points = result.points;
									var totalKws = kwsToFetch.length;
									points.forEach(function(point) {
										if (!point || !isFinite(Number(point.lat)) || !isFinite(Number(point.lng))) return;
										var zone = normalizeZone(point);
										var bgColor = zone === 'top3' ? '#16a34a'
											: zone === 'found' ? '#84cc16'
											: zone === 'weak' ? '#f97316' : '#64748b';
										var radius = gridSizeZ <= 3 ? 9 : gridSizeZ <= 7 ? 8 : gridSizeZ <= 13 ? 6 : 5;
										var latlng = [Number(point.lat), Number(point.lng)];
										/* Halos kwIdx=1 et kwIdx=2 */
										if (totalKws >= 2 && kwIdx === 1) {
											var ring1Px = Math.round(radius * 1.9);
											window.L.circleMarker(latlng, {
												renderer: miniRenderer,
												radius: ring1Px,
												color: miniHaloColors[1],
												weight: 2,
												opacity: 0.75,
												fillColor: miniHaloColors[1],
												fillOpacity: 0.10,
												interactive: false
											}).addTo(miniLayer);
										}
										if (totalKws >= 3 && kwIdx === 2) {
											var ring2Px = Math.round(radius * 2.7);
											window.L.circleMarker(latlng, {
												renderer: miniRenderer,
												radius: ring2Px,
												color: miniHaloColors[2],
												weight: 2,
												opacity: 0.65,
												fillColor: miniHaloColors[2],
												fillOpacity: 0.07,
												interactive: false
											}).addTo(miniLayer);
										}
										/* Cercle central (kwIdx=0 uniquement) */
										if (kwIdx === 0) {
											var rankDisplay = (point.rank !== null && point.rank !== undefined && Number(point.rank) <= 20)
												? '#' + point.rank : '>20';
											var kwDisplay = String(kw || '').replace(/[<>&"]/g, '');
											window.L.circleMarker(latlng, {
												renderer: miniRenderer,
												radius: radius,
												color: '#fff',
												weight: (1.5),
												fillColor: (bgColor),
												fillOpacity: 1
											}).bindTooltip(
												'<div style="font-size:.75rem;font-weight:800;color:#0f172a;padding:2px 4px">' +
												rankDisplay + '<br><span style="color:#64748b;font-weight:600">' +
												kwDisplay + '</span></div>',
												{ sticky: true, direction: 'top', offset: [0, -8] }
											).addTo(miniLayer);
											var rankLabel = rankDisplay;
											var fontSize = rankLabel.length > 2 ? '7px' : (radius <= 7 ? '8px' : '9px');
											window.L.marker(latlng, {
												interactive: false,
												icon: window.L.divIcon({
													className: '',
													html: '<span style="display:flex;align-items:center;justify-content:center;' +
														'width:100%;height:100%;font-size:' + fontSize + ';font-weight:900;' +
														'color:#fff;pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,0.4);">' +
														rankLabel + '</span>',
													iconSize: [radius * 2, radius * 2],
													iconAnchor: [radius, radius]
												})
											}).addTo(miniLayer);
											allBoundsEvo.push(latlng);
										}
									});
								});
								if (allBoundsEvo.length) {
									try {
										miniMap.fitBounds(allBoundsEvo, { padding: [12, 12], maxZoom: zoomZ + 1 });
									} catch(e) {}
								}
								miniMap.invalidateSize();
							});
						});
					});
				}
				/* ── Mode Focus ── */
				/* openHmFocusMode is wired from the dynamic evolution actions button. */
				function openHmFocusMode() {
					var overlay = root.querySelector('[data-avi-hm-focus-overlay]');
					var focusGrid = root.querySelector('[data-avi-hm-focus-grid]');
					if (!overlay || !focusGrid) return;
					var selectedScansForFocus = evolutionScans.filter(function(s) {
						return evolutionSelected.indexOf(s.scan_id) >= 0;
					}).slice().sort(function(a, b) {
						return new Date(a.scan_date) - new Date(b.scan_date);
					});
					var kw = selectedMapKeywords.length
						? selectedMapKeywords[0]
						: (selectedKeyword || '');
					var focusTitle = overlay.querySelector('[data-avi-hm-focus-title]');
					var focusSubtitle = overlay.querySelector('[data-avi-hm-focus-subtitle]');
					var focusKwBtns = overlay.querySelector('[data-avi-hm-focus-kw-btns]');
					if (focusTitle) focusTitle.textContent = kw;
					if (focusSubtitle) {
						focusSubtitle.textContent = selectedScansForFocus.length + ' ' +
							(getHmLang() === 'fr' ? 'scans comparés'
							: getHmLang() === 'es' ? 'escaneos comparados'
							: getHmLang() === 'de' ? 'Scans verglichen'
							: getHmLang() === 'pt-br' ? 'scans comparados'
							: getHmLang() === 'tr' ? 'tarama karşılaştırıldı'
							: 'scans compared');
					}
					var allFocusKeywords = [];
					selectedScansForFocus.forEach(function(s) {
						var kw = s.keyword || '';
						if (kw && allFocusKeywords.indexOf(kw) < 0) allFocusKeywords.push(kw);
					});
					if (focusKwBtns && allFocusKeywords.length > 0) {
						focusKwBtns.innerHTML = allFocusKeywords.map(function(k, i) {
							return '<button type="button" class="avi-hm-focus-kw-btn' + (i === 0 ? ' is-active' : '') + '" ' +
								'data-avi-hm-focus-kw="' + k.replace(/"/g, '&quot;') + '">' + k + '</button>';
						}).join('');
						focusKwBtns.querySelectorAll('[data-avi-hm-focus-kw]').forEach(function(btn) {
							btn.addEventListener('click', function() {
								focusKwBtns.querySelectorAll('[data-avi-hm-focus-kw]').forEach(function(b) {
									b.classList.toggle('is-active', b === btn);
								});
								var activeKw = btn.getAttribute('data-avi-hm-focus-kw');
								if (focusTitle) focusTitle.textContent = activeKw;
								/* avi-hm-focus-card-kw mirrors the active Focus header keyword. */
								focusGrid.querySelectorAll('.avi-hm-focus-card-kw').forEach(function(el) {
									el.textContent = activeKw;
									el.title = activeKw;
								});
								focusGrid.querySelectorAll('[data-avi-hm-focus-map]').forEach(function(mapEl) {
									var scanId = parseInt(mapEl.getAttribute('data-scan-id') || '0', 10);
									var gridSz = parseInt(mapEl.getAttribute('data-grid-size') || '7', 10);
									if (!scanId || !mapEl._leafletMap) return;
									var focusMap = mapEl._leafletMap;
									var focusLayer = mapEl._leafletLayer;
									if (!focusLayer) return;
									focusLayer.clearLayers();
									fetch(scanByIdUrl(scanId), {
										method: 'GET',
										credentials: 'same-origin',
										headers: restHeaders()
									}).then(function(r) { return r.json().catch(function() { return {}; }); })
									.then(function(data) {
										var points = Array.isArray(data.map_points) ? data.map_points : [];
										var focusRenderer = mapEl._leafletRenderer;
										var radius = gridSz <= 3 ? 12 : gridSz <= 7 ? 10 : 7;
										var bounds = [];
										points.forEach(function(point) {
											if (!point || !isFinite(Number(point.lat)) || !isFinite(Number(point.lng))) return;
											var zone = normalizeZone(point);
											var bgColor = zone === 'top3' ? '#16a34a'
												: zone === 'found' ? '#84cc16'
												: zone === 'weak' ? '#f97316' : '#64748b';
											var latlng = [Number(point.lat), Number(point.lng)];
											var rankDisplay = (point.rank !== null && point.rank !== undefined && Number(point.rank) <= 20)
												? '#' + point.rank : '>20';
											window.L.circleMarker(latlng, {
												renderer: focusRenderer,
												radius: radius,
												color: '#fff',
												weight: 2,
												fillColor: (bgColor),
												fillOpacity: 1
											}).bindTooltip(
												'<div style="font-size:.8rem;font-weight:800;color:#0f172a;padding:3px 6px">' +
												rankDisplay + '<br><span style="color:#64748b;font-weight:600">' +
												String(activeKw).replace(/[<>&]/g, '') + '</span></div>',
												{ sticky: true, direction: 'top', offset: [0, -10] }
											).addTo(focusLayer);
											var fontSize = rankDisplay.length > 2 ? '8px' : '10px';
											window.L.marker(latlng, {
												interactive: false,
												icon: window.L.divIcon({
													className: '',
													html: '<span style="display:flex;align-items:center;justify-content:center;' +
														'width:100%;height:100%;font-size:' + fontSize + ';font-weight:900;' +
														'color:#fff;pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,.4);">' +
														rankDisplay + '</span>',
													iconSize: [radius * 2, radius * 2],
													iconAnchor: [radius, radius]
												})
											}).addTo(focusLayer);
											bounds.push(latlng);
										});
										if (bounds.length) {
											try { focusMap.fitBounds(bounds, { padding: [16, 16] }); }
											catch(e) {}
										}
									}).catch(function() {});
								});
							});
						});
					} else if (focusKwBtns) {
						focusKwBtns.innerHTML = '';
					}
					var count = selectedScansForFocus.length;
					var cols = count <= 1 ? 1 : 2;
					focusGrid.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
					focusGrid.setAttribute('data-count', String(count));
					var refGridSizeFocus = selectedScansForFocus[0] ? (selectedScansForFocus[0].grid_size || 0) : 0;
					var hasMixedGridsFocus = selectedScansForFocus.some(function(s) {
						return (s.grid_size || 0) !== refGridSizeFocus;
					});
					focusGrid.innerHTML = selectedScansForFocus.map(function(scan, idx) {
						return '<div style="display:flex;flex-direction:column;min-height:0;border-radius:6px;overflow:hidden;background:#fff">' +
							'<div style="padding:10px 14px;background:#fff;border-bottom:1px solid #f1f5f9;flex-shrink:0;display:flex;align-items:center;justify-content:space-between">' +
							'<div>' +
							'<strong style="font-size:.9rem;font-weight:950;color:#0f172a">' + formatEvolutionDate(scan.scan_date) + '</strong>' +
							'<div class="avi-hm-focus-card-kw" ' +
							'style="font-size:.72rem;color:#64748b;font-weight:600;margin-top:2px;' +
							'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px" ' +
							'title="' + String(scan.keyword || selectedKeyword || '').replace(/"/g, '&quot;') + '">' +
							String(scan.keyword || selectedKeyword || '') + '</div>' +
							'</div>' +
							'<div style="display:flex;gap:5px;align-items:center">' +
							'<span style="padding:3px 8px;border-radius:999px;background:linear-gradient(135deg,#7c3aed,#6d28d9);color:#fff;font-size:.68rem;font-weight:900">' +
							(scan.grid_size || 7) + '×' + (scan.grid_size || 7) + '</span>' +
							(idx === 0
								? '<span style="padding:2px 7px;border-radius:999px;background:#fef3c7;color:#92400e;font-size:.62rem;font-weight:900;border:1px solid #fde68a" title="' + hm_t('evo_ref_tooltip') + '">📌 ' + hm_t('evo_ref_label') + '</span>'
								: (hasMixedGridsFocus && (scan.grid_size || 0) !== refGridSizeFocus
									? '<span style="padding:2px 7px;border-radius:999px;background:#fef3c7;color:#92400e;font-size:.62rem;font-weight:900;border:1px solid #fde68a">⚠️ ' + (scan.grid_size || 7) + '×' + (scan.grid_size || 7) + '</span>'
									: '')) +
							'</div>' +
							'</div>' +
							'<div class="avi-hm-focus-map-el" data-avi-hm-focus-map ' +
							'data-scan-id="' + scan.scan_id + '" ' +
							'data-grid-size="' + (scan.grid_size || 7) + '" ' +
							'style="flex:1;min-height:0;background:#f0ede8"></div>' +
							'<div style="display:flex;gap:0;flex-shrink:0">' +
							'<div style="flex:1;text-align:center;padding:8px 6px;border-right:1px solid #f1f5f9;background:#f0fdf4">' +
							'<strong style="display:block;font-size:.9rem;color:#15803d;font-weight:950">' +
							(scan.avg_rank !== null ? Math.round(scan.avg_rank * 10) / 10 : '—') +
							(idx > 0 && selectedScansForFocus[0].avg_rank !== null && scan.avg_rank !== null
								? ' ' + deltaHtml(scan.avg_rank, selectedScansForFocus[0].avg_rank, true) : '') +
							'</strong>' +
							'<span style="display:block;font-size:.58rem;color:#16a34a;font-weight:700;text-transform:uppercase;letter-spacing:.06em">' + (hm_t('evolution_rank_label') || 'Avg rank') + '</span>' +
							'</div>' +
							'<div style="flex:1;text-align:center;padding:8px 6px;border-right:1px solid #f1f5f9;background:#eff6ff">' +
							'<strong style="display:block;font-size:.9rem;color:#1d4ed8;font-weight:950">' +
							(scan.territory_pct !== null ? Math.round(scan.territory_pct * 10) / 10 + '%' : '—') +
							(idx > 0 && selectedScansForFocus[0].territory_pct !== null && scan.territory_pct !== null
								? ' ' + deltaHtml(scan.territory_pct, selectedScansForFocus[0].territory_pct, false) : '') +
							'</strong>' +
							'<span style="display:block;font-size:.58rem;color:#2563eb;font-weight:700;text-transform:uppercase;letter-spacing:.06em">' + (hm_t('evolution_territory_label') || 'Top 3 territory') + '</span>' +
							'</div>' +
							'<div style="flex:1;text-align:center;padding:8px 6px;background:#faf5ff">' +
							'<strong style="display:block;font-size:.9rem;color:#7c3aed;font-weight:950">' + (scan.grid_size || 7) + '×' + (scan.grid_size || 7) + '</strong>' +
							'<span style="display:block;font-size:.58rem;color:#6d28d9;font-weight:700;text-transform:uppercase;letter-spacing:.06em">' + (hm_t('evolution_grid_label') || 'Grid') + '</span>' +
							'</div>' +
							'</div>' +
							'</div>';
					}).join('');
					overlay.removeAttribute('hidden');
					if (document.body) document.body.classList.add('avi-hm-focus-open');
					setTimeout(function() {
						var focusHeader = overlay.querySelector('.avi-hm-focus-header');
						var headerH = focusHeader ? focusHeader.offsetHeight : 56;
						var navH = 52;
						var availH = overlay.offsetHeight - headerH - navH - 8;
						var mapH = Math.max(300, availH - 110);
						var focusPage = 0;
						var focusPerPage = 2;
						var focusTotal = selectedScansForFocus.length;
						var focusPages = Math.ceil(focusTotal / focusPerPage);
						function renderFocusPage(page) {
							focusPage = Math.max(0, Math.min(page, focusPages - 1));
							var cards = Array.prototype.slice.call(focusGrid.children);
							cards.forEach(function(card, idx) {
								var visible = idx >= focusPage * focusPerPage && idx < (focusPage + 1) * focusPerPage;
								card.style.display = visible ? 'flex' : 'none';
							});
							var prevBtn = overlay.querySelector('[data-avi-hm-focus-prev]');
							var nextBtn = overlay.querySelector('[data-avi-hm-focus-next]');
							var pageInfo = overlay.querySelector('[data-avi-hm-focus-page-info]');
							if (prevBtn) prevBtn.disabled = focusPage === 0;
							if (nextBtn) nextBtn.disabled = focusPage >= focusPages - 1;
							if (pageInfo) pageInfo.textContent = (focusPage + 1) + ' / ' + focusPages;
							var cols = Math.min(focusPerPage, focusTotal - focusPage * focusPerPage);
							focusGrid.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
							setTimeout(function() {
								focusGrid.querySelectorAll('[data-avi-hm-focus-map]').forEach(function(el) {
									if (el.style.display !== 'none' && el.closest('[style*="flex"]')) {
										el.style.height = mapH + 'px';
										if (el._leafletMap) el._leafletMap.invalidateSize();
									}
								});
							}, 50);
						}
						var focusNav = overlay.querySelector('.avi-hm-focus-nav');
						if (!focusNav) {
							focusNav = document.createElement('div');
							focusNav.className = 'avi-hm-focus-nav';
							focusNav.innerHTML =
								'<button type="button" class="avi-hm-focus-nav-btn" data-avi-hm-focus-prev>← ' +
								(getHmLang() === 'fr' ? 'Précédent' : getHmLang() === 'es' ? 'Anterior' : getHmLang() === 'de' ? 'Zurück' : getHmLang() === 'pt-br' ? 'Anterior' : getHmLang() === 'tr' ? 'Önceki' : 'Previous') +
								'</button>' +
								'<span class="avi-hm-focus-page-info" data-avi-hm-focus-page-info></span>' +
								'<button type="button" class="avi-hm-focus-nav-btn" data-avi-hm-focus-next>' +
								(getHmLang() === 'fr' ? 'Suivant' : getHmLang() === 'es' ? 'Siguiente' : getHmLang() === 'de' ? 'Weiter' : getHmLang() === 'pt-br' ? 'Próximo' : getHmLang() === 'tr' ? 'Sonraki' : 'Next') +
								' →</button>';
							overlay.appendChild(focusNav);
							overlay.querySelector('[data-avi-hm-focus-prev]').addEventListener('click', function() {
								renderFocusPage(focusPage - 1);
							});
							overlay.querySelector('[data-avi-hm-focus-next]').addEventListener('click', function() {
								renderFocusPage(focusPage + 1);
							});
						}
						focusGrid.style.height = availH + 'px';
						focusGrid.style.overflow = 'hidden';
						focusGrid.querySelectorAll('[data-avi-hm-focus-map]').forEach(function(el) {
							el.style.height = mapH + 'px';
							el.style.display = 'block';
						});
						overlay.querySelectorAll('[data-avi-hm-focus-per-page]').forEach(function(btn) {
							btn.addEventListener('click', function() {
								overlay.querySelectorAll('[data-avi-hm-focus-per-page]').forEach(function(b) {
									b.classList.toggle('is-active', b === btn);
								});
								focusPerPage = parseInt(btn.getAttribute('data-avi-hm-focus-per-page'), 10) || 2;
								focusPages = Math.ceil(focusTotal / focusPerPage);
								renderFocusPage(0);
							});
						});
						focusGrid.querySelectorAll('[data-avi-hm-focus-map]').forEach(function(mapEl) {
							if (window.L && mapEl._leaflet_id) return;
							var scanId = parseInt(mapEl.getAttribute('data-scan-id') || '0', 10);
							var gridSz = parseInt(mapEl.getAttribute('data-grid-size') || '7', 10);
							if (!scanId || !config.center) return;
							var focusRenderer = window.L.canvas({ padding: 0.5 });
							var focusMap = mapEl._leafletMap = window.L.map(mapEl, {
								zoomControl: true,
								attributionControl: false,
								renderer: focusRenderer,
								scrollWheelZoom: 'center'
							});
							focusMap.on('focus', function() {
								focusMap.scrollWheelZoom.enable();
							});
							focusMap.on('blur', function() {
								focusMap.scrollWheelZoom.disable();
							});
							focusMap.scrollWheelZoom.disable();
							var zoomHint = document.createElement('div');
							zoomHint.style.cssText = 'position:absolute;bottom:10px;left:50%;transform:translateX(-50%);' +
								'background:rgba(15,23,42,.7);color:#fff;font-size:.7rem;font-weight:700;' +
								'padding:4px 10px;border-radius:999px;pointer-events:none;z-index:500;' +
								'backdrop-filter:blur(4px);white-space:nowrap;';
							zoomHint.textContent = getHmLang() === 'fr' ? '🖱️ Cliquez pour zoomer'
								: getHmLang() === 'es' ? '🖱️ Haga clic para hacer zoom'
								: '🖱️ Click to zoom';
							mapEl.style.position = 'relative';
							mapEl.appendChild(zoomHint);
							focusMap.once('click', function() {
								if (zoomHint.parentNode) zoomHint.parentNode.removeChild(zoomHint);
								focusMap.scrollWheelZoom.enable();
							});
							window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
								maxZoom: 19
							}).addTo(focusMap);
							/* mapEl._leafletLayer is reused when the Focus keyword changes. */
							var focusLayer = mapEl._leafletLayer = window.L.layerGroup().addTo(focusMap);
							mapEl._leafletRenderer = focusRenderer;
							var zoomZ = gridSz <= 3 ? 15 : gridSz <= 7 ? 14 : 13;
							focusMap.setView([Number(config.center.lat), Number(config.center.lng)], zoomZ);
							setTimeout(function() { focusMap.invalidateSize(); }, 150);
							fetch(scanByIdUrl(scanId), {
								method: 'GET',
								credentials: 'same-origin',
								headers: restHeaders()
							}).then(function(r) { return r.json().catch(function() { return {}; }); })
							.then(function(data) {
								var points = Array.isArray(data.map_points) ? data.map_points : [];
								var bounds = [];
								var radius = gridSz <= 3 ? 12 : gridSz <= 7 ? 10 : 7;
								points.forEach(function(point) {
									if (!point || !isFinite(Number(point.lat)) || !isFinite(Number(point.lng))) return;
									var zone = normalizeZone(point);
									var bgColor = zone === 'top3' ? '#16a34a'
										: zone === 'found' ? '#84cc16'
										: zone === 'weak' ? '#f97316' : '#64748b';
									var latlng = [Number(point.lat), Number(point.lng)];
									var rankDisplay = (point.rank !== null && point.rank !== undefined && Number(point.rank) <= 20)
										? '#' + point.rank : '>20';
									window.L.circleMarker(latlng, {
										renderer: focusRenderer,
										radius: radius,
										color: '#fff',
										weight: 2,
										fillColor: (bgColor),
										fillOpacity: 1
									}).bindTooltip(
										'<div style="font-size:.8rem;font-weight:800;color:#0f172a;padding:3px 6px">' +
										rankDisplay + '<br><span style="color:#64748b;font-weight:600">' +
										String(data.keyword || '').replace(/[<>&]/g, '') + '</span></div>',
										{ sticky: true, direction: 'top', offset: [0, -10] }
									).addTo(focusLayer);
									var fontSize = rankDisplay.length > 2 ? '8px' : '10px';
									window.L.marker(latlng, {
										interactive: false,
										icon: window.L.divIcon({
											className: '',
											html: '<span style="display:flex;align-items:center;justify-content:center;' +
												'width:100%;height:100%;font-size:' + fontSize + ';font-weight:900;' +
												'color:#fff;pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,.4);">' +
												rankDisplay + '</span>',
											iconSize: [radius * 2, radius * 2],
											iconAnchor: [radius, radius]
										})
									}).addTo(focusLayer);
									bounds.push(latlng);
								});
								if (bounds.length) {
									try { focusMap.fitBounds(bounds, { padding: [16, 16], maxZoom: zoomZ + 1 }); }
									catch(e) {}
								}
								focusMap.invalidateSize();
							}).catch(function() {});
						});
						renderFocusPage(0);
					}, 200);
				}
				function closeHmFocusMode() {
					var overlay = root.querySelector('[data-avi-hm-focus-overlay]');
					if (overlay) overlay.setAttribute('hidden', '');
					if (document.body) document.body.classList.remove('avi-hm-focus-open');
				}
				function bindHmFocusMode() {
					var closeBtn = root.querySelector('[data-avi-hm-focus-close]');
					if (closeBtn) {
						closeBtn.addEventListener('click', closeHmFocusMode);
					}
					document.addEventListener('keydown', function(e) {
						if (e.key === 'Escape') closeHmFocusMode();
					});
				}
				applyHeatmapI18n();
				updateGridDescription();
				bindHmPedago();
				bindHmDisclaimer();
				bindHmFullscreen();
				bindTabSwitcher();
				bindHmFocusMode();
				/* Expand overlay */
				var intelOverlay = root.querySelector(
					'[data-avi-hm-intel-overlay]'
				);
				var intelOverlayBody = root.querySelector(
					'[data-avi-hm-intel-overlay-body]'
				);
				var intelOverlayClose = root.querySelector(
					'[data-avi-hm-intel-overlay-close]'
				);
				root.addEventListener('click', function(ev) {
					if (ev.target.closest(
						'[data-avi-hm-intel-expand]'
					)) {
						if (!intelOverlay ||
							!intelOverlayBody) return;
						/* Copier le contenu du panel dans l'overlay */
						var intelBody = root.querySelector(
							'[data-avi-hm-intel-body]'
						);
						if (intelBody) {
							intelOverlayBody.innerHTML =
								intelBody.innerHTML;
						}
						intelOverlay.removeAttribute('hidden');
						return;
					}
					if (ev.target.closest(
						'[data-avi-hm-intel-overlay-close]'
					)) {
						if (intelOverlay) {
							intelOverlay.setAttribute(
								'hidden', ''
							);
						}
					}
				});
				void intelOverlayClose;
				/* Listener délégué GBP — une seule fois */
				if (!root.dataset.gbpListenerAdded) {
					root.dataset.gbpListenerAdded = '1';
					document.addEventListener(
						'click',
						function(ev) {
							var btn = ev.target.closest(
								'.avi-hm-popup__gbp-btn, '
								+ '.avi-hm-comp-gbp-btn'
							);
							if (!btn) return;
							var url = btn.getAttribute('data-gbp-url');
							if (url) {
								ev.preventDefault();
								ev.stopPropagation();
								window.open(
									url,
									'_blank',
									'noopener,noreferrer'
								);
							}
						}
					);
				}
				applyDesign('points');
				updateDrawerKeywordButtons();
				refreshCreditState();
				// #Sprint43-scheduler
				// #Sprint43-scheduler fix — pas de updateScheduleNote si schedule actif rendu
				// #Sprint44-Plugin — délai boot embed token
				if (config.embed_token) { setTimeout(function(){ loadHeatmapSchedules(); }, 100); } else { loadHeatmapSchedules(); }
				if (selectedKeyword && scanCache[selectedKeyword.toLowerCase()]
					&& Array.isArray(scanCache[selectedKeyword.toLowerCase()].map_points)
					&& scanCache[selectedKeyword.toLowerCase()].map_points.length > 0) {
					updateCoverageBadge(selectedKeyword);
					updateDrawerMetrics(selectedKeyword);
					updateScanDateOverlay(selectedKeyword);
				}
				if (selectedKeyword) {
					fetchPreviousScanPoints(selectedKeyword).then(function(prevData) {
						var key = String(selectedKeyword).trim().toLowerCase();
						var currentData = scanCache[key];
						if (!currentData || !Array.isArray(currentData.map_points)
							|| currentData.map_points.length === 0) return;
						var trends = computeTrendData(currentData.map_points, prevData);
						scanCache[key].trendData = trends;
						// renderTrendOverlay appelé depuis renderSelectedMap() après init carte
					});
				}
			})();
