{"id":23813670,"date":"2026-05-20T17:24:15","date_gmt":"2026-05-20T17:24:15","guid":{"rendered":"https:\/\/socialhackerslab.com\/?page_id=23813670"},"modified":"2026-06-04T15:04:31","modified_gmt":"2026-06-04T15:04:31","slug":"carte-interactive","status":"publish","type":"page","link":"https:\/\/socialhackerslab.com\/en\/carte-interactive\/","title":{"rendered":"carte-interactive"},"content":{"rendered":"<style>\r\n@import url('https:\/\/fonts.googleapis.com\/css2?family=Manrope:wght@400;500;600;700;800;900&display=swap');\r\n\r\n#shl-map-container {\r\n    --bg-color: #fbfaf8;\r\n    --text-color: #24476d;\r\n    --muted-text: #6d7580;\r\n    --border-color: #e7dfd9;\r\n    --accent-color: #6d4f84;\r\n    --partner-fill: #4a90c8;\r\n    --font-family: 'Manrope', sans-serif;\r\n\r\n    \/* Fullscreen uniquement l\u00e0 o\u00f9 le shortcode est pos\u00e9 *\/\r\n    position: fixed !important;\r\n    top: 0 !important;\r\n    left: 0 !important;\r\n    right: 0 !important;\r\n    bottom: 0 !important;\r\n    width: 100vw !important;\r\n    height: 100vh !important;\r\n    max-width: none !important;\r\n    min-height: 0 !important;\r\n    margin: 0 !important;\r\n    background: var(--bg-color) !important;\r\n    border: none !important;\r\n    border-radius: 0 !important;\r\n    overflow: hidden !important;\r\n    display: flex !important;\r\n    flex-direction: row !important;\r\n    font-family: var(--font-family) !important;\r\n    box-shadow: none !important;\r\n    z-index: 999999 !important;\r\n    isolation: isolate !important;\r\n    clear: both !important;\r\n}\r\n\r\nbody.admin-bar #shl-map-container {\r\n    top: 32px !important;\r\n    height: calc(100vh - 32px) !important;\r\n}\r\n\r\n@media (max-width: 782px) {\r\n    body.admin-bar #shl-map-container {\r\n        top: 46px !important;\r\n        height: calc(100vh - 46px) !important;\r\n    }\r\n}\r\n\r\nbody:has(#shl-map-container) {\r\n    overflow: hidden !important;\r\n}\r\n\r\n#shl-map-container,\r\n#shl-map-container * {\r\n    box-sizing: border-box !important;\r\n}\r\n\r\n#shl-map-container * {\r\n    margin: 0;\r\n    padding: 0;\r\n}\r\n\r\n#shl-map-container svg text {\r\n    font-family: 'Manrope', sans-serif !important;\r\n    text-shadow: none !important;\r\n    text-transform: none !important;\r\n}\r\n\r\n#shl-map-container .shl-country-text {\r\n    fill: #ffffff !important;\r\n    color: #ffffff !important;\r\n    font-size: 13px !important;\r\n    font-weight: 800 !important;\r\n    dominant-baseline: central !important;\r\n    letter-spacing: 0.05em !important;\r\n    paint-order: stroke fill !important;\r\n    stroke: rgba(36, 71, 109, 0.55) !important;\r\n    stroke-width: 3.2px !important;\r\n    stroke-linejoin: round !important;\r\n}\r\n\r\n#shl-map-container .shl-country-text-on {\r\n    fill: #ffffff !important;\r\n    color: #ffffff !important;\r\n    font-size: 13px !important;\r\n    font-weight: 800 !important;\r\n    dominant-baseline: central !important;\r\n    letter-spacing: 0.05em !important;\r\n    paint-order: stroke fill !important;\r\n    stroke: rgba(36, 71, 109, 0.55) !important;\r\n    stroke-width: 3.2px !important;\r\n    stroke-linejoin: round !important;\r\n}\r\n\r\n#shl-map-container .shl-marseille-label {\r\n    fill: #24476d !important;\r\n    color: #24476d !important;\r\n    font-size: 13px !important;\r\n    font-weight: 800 !important;\r\n    dominant-baseline: central !important;\r\n    letter-spacing: 0.03em !important;\r\n    paint-order: stroke fill !important;\r\n    stroke: rgba(255, 255, 255, 0.92) !important;\r\n    stroke-width: 4px !important;\r\n    stroke-linejoin: round !important;\r\n}\r\n\r\n#shl-map-container .carte-map-area {\r\n    flex: 1 1 auto !important;\r\n    position: relative !important;\r\n    height: 100% !important;\r\n    min-width: 0 !important;\r\n    background: #ffffff !important;\r\n    overflow: hidden !important;\r\n}\r\n\r\n#shl-map-container .carte-map-area svg {\r\n    display: block !important;\r\n    width: 100% !important;\r\n    height: 100% !important;\r\n}\r\n\r\n#shl-map-container .carte-map-header {\r\n    position: absolute !important;\r\n    top: 1.1rem !important;\r\n    right: 1.4rem !important;\r\n    z-index: 20 !important;\r\n    text-align: right !important;\r\n    pointer-events: none !important;\r\n}\r\n#shl-map-container .carte-map-header-eyebrow {\r\n    font-size: 0.74rem !important;\r\n    font-weight: 800 !important;\r\n    letter-spacing: 0.12em !important;\r\n    text-transform: uppercase !important;\r\n    color: var(--text-color) !important;\r\n}\r\n\r\n#shl-map-container .carte-sidebar {\r\n    width: 400px !important;\r\n    min-width: 340px !important;\r\n    max-width: 44% !important;\r\n    height: 100% !important;\r\n    background: rgba(255,251,247,0.96) !important;\r\n    border-left: 1px solid var(--border-color) !important;\r\n    position: relative !important;\r\n    overflow: hidden !important;\r\n    flex: 0 0 400px !important;\r\n}\r\n\r\n#shl-map-container .slide-panel {\r\n    position: absolute !important;\r\n    inset: 0 !important;\r\n    overflow-y: auto !important;\r\n    padding: 1.5rem !important;\r\n    background: #fbfaf8 !important;\r\n    transition: transform 0.38s cubic-bezier(0.22, 1, 0.36, 1) !important;\r\n    will-change: transform !important;\r\n}\r\n#shl-map-container .sp-list { transform: translateX(0); }\r\n#shl-map-container .sp-detail { transform: translateX(100%); }\r\n#shl-map-container .sp-list.sp-exit { transform: translateX(-28%); }\r\n#shl-map-container .sp-detail.sp-enter { transform: translateX(0); }\r\n\r\n#shl-map-container .carte-map-tooltip {\r\n    position: absolute !important;\r\n    z-index: 25 !important;\r\n    min-width: 132px !important;\r\n    padding: 0.55rem 0.65rem !important;\r\n    border-radius: 10px !important;\r\n    background: rgba(255, 255, 255, 0.96) !important;\r\n    border: 1px solid rgba(231, 223, 217, 0.95) !important;\r\n    box-shadow: 0 10px 28px rgba(36, 71, 109, 0.12) !important;\r\n    opacity: 0 !important;\r\n    transform: translateY(6px) !important;\r\n    pointer-events: none !important;\r\n    transition: opacity 0.16s ease, transform 0.16s ease !important;\r\n}\r\n#shl-map-container .carte-map-tooltip.visible { opacity: 1 !important; transform: translateY(0) !important; }\r\n#shl-map-container .carte-tooltip-title { font-size: 0.84rem !important; font-weight: 800 !important; color: var(--text-color) !important; line-height: 1.1 !important; }\r\n#shl-map-container .carte-tooltip-meta { margin-top: 0.2rem !important; font-size: 0.7rem !important; font-weight: 700 !important; color: var(--accent-color) !important; }\r\n\r\n#shl-map-container .sidebar-header { margin-bottom: 1.2rem !important; }\r\n#shl-map-container .sidebar-eyebrow { font-size: 0.66rem !important; font-weight: 800 !important; letter-spacing: 0.12em !important; text-transform: uppercase !important; color: var(--muted-text) !important; }\r\n#shl-map-container .sidebar-title { font-size: 1.35rem !important; font-weight: 800 !important; color: var(--text-color) !important; margin-top: 0.25rem !important; line-height: 1.15 !important; }\r\n#shl-map-container .sidebar-subtitle { font-size: 0.78rem !important; color: var(--muted-text) !important; font-weight: 600 !important; margin-top: 0.3rem !important; }\r\n\r\n#shl-map-container .carte-stats { display: grid !important; grid-template-columns: 1fr 1fr 1fr !important; gap: 0.55rem !important; margin-bottom: 1.4rem !important; }\r\n#shl-map-container .carte-stat-card { background: white !important; border: 1px solid var(--border-color) !important; border-radius: 12px !important; padding: 0.7rem 0.55rem !important; text-align: center !important; }\r\n#shl-map-container .carte-stat-value { font-size: 1.45rem !important; font-weight: 800 !important; color: var(--text-color) !important; line-height: 1 !important; }\r\n#shl-map-container .carte-stat-label { font-size: 0.6rem !important; color: var(--muted-text) !important; font-weight: 700 !important; text-transform: uppercase !important; letter-spacing: 0.05em !important; margin-top: 4px !important; }\r\n\r\n#shl-map-container .carte-filter {\r\n    display: flex !important;\r\n    flex-wrap: wrap !important;\r\n    gap: 0.35rem !important;\r\n    margin-bottom: 1.05rem !important;\r\n}\r\n#shl-map-container .carte-filter-chip {\r\n    background: white !important;\r\n    border: 1.5px solid var(--border-color) !important;\r\n    color: var(--text-color) !important;\r\n    font-family: var(--font-family) !important;\r\n    font-size: 0.66rem !important;\r\n    font-weight: 800 !important;\r\n    text-transform: uppercase !important;\r\n    letter-spacing: 0.05em !important;\r\n    padding: 0.32rem 0.65rem !important;\r\n    border-radius: 999px !important;\r\n    cursor: pointer !important;\r\n    transition: 0.16s ease !important;\r\n    line-height: 1.1 !important;\r\n}\r\n#shl-map-container .carte-filter-chip:hover {\r\n    border-color: var(--accent-color) !important;\r\n    color: var(--accent-color) !important;\r\n}\r\n#shl-map-container .carte-filter-chip.active {\r\n    background: var(--accent-color) !important;\r\n    border-color: var(--accent-color) !important;\r\n    color: white !important;\r\n}\r\n\r\n#shl-map-container .carte-section-title { font-size: 0.66rem !important; font-weight: 800 !important; text-transform: uppercase !important; letter-spacing: 0.09em !important; color: var(--muted-text) !important; margin-bottom: 0.65rem !important; }\r\n#shl-map-container .asso-list { display: flex !important; flex-direction: column !important; gap: 0.38rem !important; }\r\n\r\n#shl-map-container .asso-card {\r\n    display: flex !important;\r\n    align-items: flex-start !important;\r\n    gap: 0.7rem !important;\r\n    background: white !important;\r\n    border: 1.5px solid var(--border-color) !important;\r\n    border-radius: 11px !important;\r\n    padding: 0.65rem 0.8rem !important;\r\n    cursor: pointer !important;\r\n    transition: 0.18s ease !important;\r\n}\r\n#shl-map-container .asso-card:hover { border-color: var(--accent-color) !important; transform: translateY(-1px) !important; box-shadow: 0 5px 12px rgba(36, 71, 109, 0.07) !important; }\r\n#shl-map-container .asso-card.c-selected { border-color: var(--accent-color) !important; box-shadow: 0 5px 14px rgba(109, 79, 132, 0.14) !important; }\r\n#shl-map-container .asso-chip {\r\n    width: 16px !important;\r\n    height: 16px !important;\r\n    border-radius: 50% !important;\r\n    flex: 0 0 16px !important;\r\n    margin-top: 2px !important;\r\n    box-shadow: 0 0 0 2px white, 0 0 0 3px rgba(0,0,0,0.07) !important;\r\n}\r\n#shl-map-container .asso-body { flex: 1 !important; min-width: 0 !important; }\r\n#shl-map-container .asso-name { font-weight: 800 !important; font-size: 0.86rem !important; color: var(--text-color) !important; line-height: 1.15 !important; }\r\n#shl-map-container .asso-hub { font-size: 0.66rem !important; color: var(--accent-color) !important; font-weight: 800 !important; text-transform: uppercase !important; letter-spacing: 0.08em !important; margin-top: 2px !important; }\r\n#shl-map-container .asso-countries { font-size: 0.72rem !important; color: var(--muted-text) !important; font-style: italic !important; margin-top: 2px !important; font-weight: 500 !important; }\r\n\r\n#shl-map-container .detail-total { display: flex !important; justify-content: space-between !important; align-items: baseline !important; gap: 1rem !important; background: white !important; border: 1px solid var(--border-color) !important; border-radius: 12px !important; padding: 0.8rem 1rem !important; margin-bottom: 1.1rem !important; }\r\n#shl-map-container .detail-total-label { font-size: 0.66rem !important; font-weight: 800 !important; text-transform: uppercase !important; letter-spacing: 0.09em !important; color: var(--muted-text) !important; }\r\n#shl-map-container .detail-total-value { font-size: 1.25rem !important; font-weight: 800 !important; color: var(--text-color) !important; }\r\n\r\n#shl-map-container .project-card { background: white !important; border: 1px solid var(--border-color) !important; border-radius: 12px !important; padding: 0.85rem 1rem !important; margin-bottom: 0.55rem !important; position: relative !important; }\r\n#shl-map-container .project-card-head { display: flex !important; justify-content: space-between !important; align-items: baseline !important; gap: 0.7rem !important; margin-bottom: 0.45rem !important; }\r\n#shl-map-container .project-year-badge { background: var(--text-color) !important; color: white !important; font-size: 0.66rem !important; font-weight: 800 !important; padding: 0.18rem 0.5rem !important; border-radius: 6px !important; letter-spacing: 0.06em !important; }\r\n#shl-map-container .project-amount-big { font-size: 1.05rem !important; font-weight: 800 !important; color: var(--text-color) !important; }\r\n#shl-map-container .project-detail { font-size: 0.78rem !important; font-weight: 700 !important; color: var(--accent-color) !important; text-transform: uppercase !important; letter-spacing: 0.05em !important; line-height: 1.3 !important; margin-bottom: 0.45rem !important; }\r\n#shl-map-container .project-countries { display: flex !important; align-items: center !important; flex-wrap: wrap !important; gap: 0.35rem !important; font-size: 0.76rem !important; color: var(--text-color) !important; font-weight: 600 !important; margin-bottom: 0.55rem !important; }\r\n#shl-map-container .project-flag { font-size: 1.1rem !important; line-height: 1 !important; }\r\n#shl-map-container .project-link { display: inline-flex !important; align-items: center !important; gap: 0.25rem !important; font-size: 0.74rem !important; font-weight: 700 !important; color: var(--accent-color) !important; text-decoration: none !important; padding-top: 0.15rem !important; border-top: 1px dashed var(--border-color) !important; margin-top: 0.45rem !important; }\r\n#shl-map-container .project-link:hover { text-decoration: underline !important; }\r\n#shl-map-container .project-no-link { font-size: 0.7rem !important; color: var(--muted-text) !important; font-style: italic !important; margin-top: 0.3rem !important; }\r\n\r\n\/* Bloc compact de drapeaux pour assos avec beaucoup de pays *\/\r\n#shl-map-container .flag-grid { display: flex !important; flex-wrap: wrap !important; gap: 0.45rem !important; background: white !important; border: 1px solid var(--border-color) !important; border-radius: 12px !important; padding: 0.7rem 0.85rem !important; margin-bottom: 0.65rem !important; }\r\n#shl-map-container .flag-grid .flag-item { display: inline-flex !important; align-items: center !important; gap: 0.3rem !important; background: rgba(74,144,200,0.07) !important; border-radius: 6px !important; padding: 0.18rem 0.42rem !important; font-size: 0.72rem !important; font-weight: 700 !important; color: var(--text-color) !important; cursor: default !important; }\r\n#shl-map-container .flag-grid .flag-item .carte-flag { font-size: 1rem !important; }\r\n\r\n#shl-map-container .panel-back { display: inline-flex !important; align-items: center !important; gap: 0.45rem !important; background: none !important; border: none !important; font-family: var(--font-family) !important; font-size: 0.8rem !important; font-weight: 700 !important; color: var(--muted-text) !important; cursor: pointer !important; padding: 0 !important; margin-bottom: 1.5rem !important; }\r\n#shl-map-container .panel-back:hover { color: var(--accent-color) !important; }\r\n#shl-map-container .detail-header { display: flex !important; align-items: center !important; gap: 0.8rem !important; margin-bottom: 1.25rem !important; padding-bottom: 1.1rem !important; border-bottom: 1px solid var(--border-color) !important; }\r\n#shl-map-container .detail-chip { width: 26px !important; height: 26px !important; border-radius: 50% !important; flex: 0 0 26px !important; box-shadow: 0 0 0 3px white, 0 0 0 4px rgba(0,0,0,0.07) !important; }\r\n#shl-map-container .detail-title { font-size: 1.3rem !important; font-weight: 800 !important; color: var(--text-color) !important; line-height: 1.1 !important; }\r\n#shl-map-container .detail-meta { font-size: 0.74rem !important; color: var(--muted-text) !important; margin-top: 4px !important; font-weight: 600 !important; }\r\n\r\n#shl-map-container .detail-items-title { font-size: 0.66rem !important; font-weight: 800 !important; text-transform: uppercase !important; letter-spacing: 0.09em !important; color: var(--muted-text) !important; margin-bottom: 0.7rem !important; }\r\n#shl-map-container .country-pill { display: flex !important; align-items: center !important; gap: 0.6rem !important; background: white !important; border: 1px solid var(--border-color) !important; border-radius: 11px !important; padding: 0.7rem 0.9rem !important; margin-bottom: 0.45rem !important; }\r\n#shl-map-container .country-pill .carte-flag { font-size: 1.35rem !important; line-height: 1 !important; }\r\n#shl-map-container .country-pill .country-name { font-weight: 700 !important; font-size: 0.92rem !important; color: var(--text-color) !important; }\r\n\r\n#shl-map-container .shl-map-error {\r\n    position: absolute !important;\r\n    inset: 1rem !important;\r\n    z-index: 50 !important;\r\n    display: none;\r\n    align-items: center !important;\r\n    justify-content: center !important;\r\n    text-align: center !important;\r\n    padding: 1rem !important;\r\n    border-radius: 12px !important;\r\n    background: rgba(255, 255, 255, 0.92) !important;\r\n    color: #24476d !important;\r\n    font-weight: 800 !important;\r\n    line-height: 1.4 !important;\r\n}\r\n\r\n@keyframes carte-fade-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }\r\n#shl-map-container .carte-stat-card { opacity: 0; animation: carte-fade-up 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; }\r\n#shl-map-container .asso-card { opacity: 0; animation: carte-fade-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards; }\r\n\r\n@media (max-width: 1200px) {\r\n    #shl-map-container .carte-sidebar {\r\n        width: 360px !important;\r\n        min-width: 320px !important;\r\n        max-width: 42% !important;\r\n        flex-basis: 360px !important;\r\n    }\r\n    #shl-map-container .slide-panel { padding: 1.2rem !important; }\r\n    #shl-map-container .carte-stat-value { font-size: 1.2rem !important; }\r\n    #shl-map-container .carte-stat-label { font-size: 0.55rem !important; }\r\n}\r\n\r\n@media (max-width: 900px) {\r\n    #shl-map-container {\r\n        flex-direction: column !important;\r\n        height: 100vh !important;\r\n        min-height: 0 !important;\r\n        max-height: none !important;\r\n    }\r\n    #shl-map-container .carte-map-area {\r\n        width: 100% !important;\r\n        height: 46% !important;\r\n        min-height: 280px !important;\r\n        flex: 0 0 46% !important;\r\n    }\r\n    #shl-map-container .carte-sidebar {\r\n        width: 100% !important;\r\n        min-width: 0 !important;\r\n        max-width: none !important;\r\n        height: 54% !important;\r\n        flex: 0 0 54% !important;\r\n        border-left: none !important;\r\n        border-top: 1px solid var(--border-color) !important;\r\n    }\r\n    #shl-map-container .slide-panel { padding: 1rem !important; }\r\n    #shl-map-container .carte-stats {\r\n        grid-template-columns: repeat(3, minmax(0, 1fr)) !important;\r\n        gap: 0.4rem !important;\r\n        margin-bottom: 0.9rem !important;\r\n    }\r\n    #shl-map-container .carte-stat-card { padding: 0.55rem 0.3rem !important; }\r\n    #shl-map-container .carte-stat-value { font-size: 1.1rem !important; }\r\n    #shl-map-container .carte-map-header { top: 0.6rem !important; right: 0.8rem !important; }\r\n    #shl-map-container .carte-map-header-eyebrow { font-size: 0.6rem !important; }\r\n}\r\n\r\n@media (max-width: 900px) {\r\n    body.admin-bar #shl-map-container {\r\n        height: calc(100vh - 32px) !important;\r\n    }\r\n}\r\n@media (max-width: 782px) {\r\n    body.admin-bar #shl-map-container {\r\n        height: calc(100vh - 46px) !important;\r\n    }\r\n}\r\n<\/style>\r\n\r\n<div id=\"shl-map-container\">\r\n    <div class=\"carte-map-area\" id=\"carte-map-area\">\r\n        <div class=\"shl-map-error\" id=\"shl-map-error\"><\/div>\r\n        <div class=\"carte-map-header\">\r\n            <div class=\"carte-map-header-eyebrow\">NOS PARTENAIRES<\/div>\r\n        <\/div>\r\n        <div class=\"carte-map-tooltip\" id=\"carte-map-tooltip\"><\/div>\r\n    <\/div>\r\n    <div class=\"carte-sidebar\">\r\n        <div class=\"slide-panel sp-list\" id=\"carte-list-panel\">\r\n            <div class=\"sidebar-header\">\r\n                <div class=\"sidebar-eyebrow\">Our European partners<\/div>\r\n                <div class=\"sidebar-title\">Cartographie des coop\u00e9rations et mobilit\u00e9s europ\u00e9ennes<\/div>\r\n            <\/div>\r\n            <div class=\"carte-stats\">\r\n                <div class=\"carte-stat-card\"><div class=\"carte-stat-value\" id=\"stat-asso\">0<\/div><div class=\"carte-stat-label\">Associations<\/div><\/div>\r\n                <div class=\"carte-stat-card\"><div class=\"carte-stat-value\" id=\"stat-pays\">0<\/div><div class=\"carte-stat-label\">Pays partenaires<\/div><\/div>\r\n                <div class=\"carte-stat-card\"><div class=\"carte-stat-value\" id=\"stat-part\">0<\/div><div class=\"carte-stat-label\">Partenariats<\/div><\/div>\r\n            <\/div>\r\n            <div class=\"carte-filter\" id=\"carte-filter\">\r\n                <button type=\"button\" class=\"carte-filter-chip active\" data-cat=\"tous\">Tous<\/button>\r\n                <button type=\"button\" class=\"carte-filter-chip\" data-cat=\"cooperation\">Coop\u00e9ration<\/button>\r\n                <button type=\"button\" class=\"carte-filter-chip\" data-cat=\"mobilite\">Mobilit\u00e9<\/button>\r\n                <button type=\"button\" class=\"carte-filter-chip\" data-cat=\"individuel\">Individuelle<\/button>\r\n            <\/div>\r\n            <div class=\"carte-section-title\">Associations<\/div>\r\n            <div class=\"asso-list\" id=\"asso-list\"><\/div>\r\n        <\/div>\r\n        <div class=\"slide-panel sp-detail\" id=\"carte-detail-panel\"><\/div>\r\n    <\/div>\r\n<\/div>\r\n\r\n<script>\r\n(function() {\r\n    const ROOT_ID = 'shl-map-container';\r\n    let started = false;\r\n\r\n    function showError(message) {\r\n        const errorEl = document.getElementById('shl-map-error');\r\n        if (!errorEl) return;\r\n        errorEl.style.display = 'flex';\r\n        errorEl.innerHTML = message;\r\n        console.error(message.replace(\/<[^>]*>\/g, ' '));\r\n    }\r\n\r\n    function loadScriptOnce(src, globalName) {\r\n        return new Promise(function(resolve, reject) {\r\n            if (window[globalName]) { resolve(); return; }\r\n\r\n            const existing = Array.from(document.scripts).find(s => s.src === src);\r\n            if (existing) {\r\n                const timer = setInterval(function() {\r\n                    if (window[globalName]) {\r\n                        clearInterval(timer);\r\n                        resolve();\r\n                    }\r\n                }, 50);\r\n                setTimeout(function() {\r\n                    clearInterval(timer);\r\n                    window[globalName] ? resolve() : reject(new Error(globalName + ' non disponible'));\r\n                }, 8000);\r\n                return;\r\n            }\r\n\r\n            const script = document.createElement('script');\r\n            script.src = src;\r\n            script.async = true;\r\n            script.onload = function() {\r\n                const timer = setInterval(function() {\r\n                    if (window[globalName]) {\r\n                        clearInterval(timer);\r\n                        resolve();\r\n                    }\r\n                }, 50);\r\n                setTimeout(function() {\r\n                    clearInterval(timer);\r\n                    window[globalName] ? resolve() : reject(new Error(globalName + ' non disponible apr\u00e8s chargement'));\r\n                }, 8000);\r\n            };\r\n            script.onerror = function() { reject(new Error('Impossible de charger ' + src)); };\r\n            document.head.appendChild(script);\r\n        });\r\n    }\r\n\r\n    function waitForContainer() {\r\n        return new Promise(function(resolve) {\r\n            let tries = 0;\r\n            function check() {\r\n                const mapEl = document.getElementById('carte-map-area');\r\n                const root = document.getElementById(ROOT_ID);\r\n                if (mapEl && root && mapEl.clientWidth > 30 && mapEl.clientHeight > 30) {\r\n                    resolve();\r\n                    return;\r\n                }\r\n                tries += 1;\r\n                if (tries > 240) {\r\n                    resolve();\r\n                    return;\r\n                }\r\n                requestAnimationFrame(check);\r\n            }\r\n            check();\r\n        });\r\n    }\r\n\r\n    function initDependencies() {\r\n        if (started) return;\r\n        started = true;\r\n        Promise.all([\r\n            loadScriptOnce('https:\/\/d3js.org\/d3.v7.min.js', 'd3'),\r\n            loadScriptOnce('https:\/\/cdn.jsdelivr.net\/npm\/topojson-client@3\/dist\/topojson-client.min.js', 'topojson'),\r\n            waitForContainer()\r\n        ]).then(initCarteApp).catch(function(err) {\r\n            showError('Carte non disponible : les biblioth\u00e8ques D3 \/ TopoJSON ne se chargent pas.<br><small>' + err.message + '<\/small>');\r\n        });\r\n    }\r\n\r\n    if (document.readyState === 'loading') {\r\n        document.addEventListener('DOMContentLoaded', initDependencies);\r\n    } else {\r\n        initDependencies();\r\n    }\r\n\r\n    function initCarteApp() {\r\n        const mapEl = document.getElementById('carte-map-area');\r\n        if (!mapEl) return showError('Zone de carte introuvable.');\r\n\r\n        \/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n        \/\/ \u25ba CONFIG : coller ici le lien Google Sheets (partage lecteur,\r\n        \/\/   format \/spreadsheets\/d\/...\/edit). Laisser vide pour la donn\u00e9e\r\n        \/\/   par d\u00e9faut (fallback ci-dessous).\r\n        const partnersCsvUrl = \"https:\/\/docs.google.com\/spreadsheets\/d\/1mqcnn5G_8KOjnpHVKbSH_7whB0Au5NUa\/edit?gid=1797200142#gid=1797200142\";\r\n        \/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n        const countryInfo = {\r\n            BE: { name: 'Belgique',   flag: '\ud83c\udde7\ud83c\uddea', lon: 4.47,  lat: 50.50, iso: 56  },\r\n            ES: { name: 'Espagne',    flag: '\ud83c\uddea\ud83c\uddf8', lon: -3.7,  lat: 40.4,  iso: 724 },\r\n            IT: { name: 'Italie',     flag: '\ud83c\uddee\ud83c\uddf9', lon: 12.6,  lat: 42.6,  iso: 380 },\r\n            PT: { name: 'Portugal',   flag: '\ud83c\uddf5\ud83c\uddf9', lon: -8.2,  lat: 39.4,  iso: 620 },\r\n            GR: { name: 'Gr\u00e8ce',      flag: '\ud83c\uddec\ud83c\uddf7', lon: 22.0,  lat: 38.5,  iso: 300 },\r\n            IE: { name: 'Irlande',    flag: '\ud83c\uddee\ud83c\uddea', lon: -8.0,  lat: 53.0,  iso: 372 },\r\n            RO: { name: 'Roumanie',   flag: '\ud83c\uddf7\ud83c\uddf4', lon: 25.0,  lat: 46.0,  iso: 642 },\r\n            TR: { name: 'Turquie',    flag: '\ud83c\uddf9\ud83c\uddf7', lon: 35.2,  lat: 39.0,  iso: 792 },\r\n            BG: { name: 'Bulgarie',   flag: '\ud83c\udde7\ud83c\uddec', lon: 25.5,  lat: 42.7,  iso: 100 },\r\n            PL: { name: 'Pologne',    flag: '\ud83c\uddf5\ud83c\uddf1', lon: 19.1,  lat: 52.1,  iso: 616 },\r\n            DK: { name: 'Danemark',   flag: '\ud83c\udde9\ud83c\uddf0', lon: 10.2,  lat: 56.2,  iso: 208 },\r\n            LU: { name: 'Luxembourg', flag: '\ud83c\uddf1\ud83c\uddfa', lon: 6.13,  lat: 49.8,  iso: 442 },\r\n            LV: { name: 'Lettonie',   flag: '\ud83c\uddf1\ud83c\uddfb', lon: 24.6,  lat: 56.9,  iso: 428 },\r\n            SE: { name: 'Su\u00e8de',      flag: '\ud83c\uddf8\ud83c\uddea', lon: 15.0,  lat: 60.1,  iso: 752 },\r\n            CZ: { name: 'Tch\u00e9quie',   flag: '\ud83c\udde8\ud83c\uddff', lon: 15.5,  lat: 49.8,  iso: 203 },\r\n            CY: { name: 'Chypre',     flag: '\ud83c\udde8\ud83c\uddfe', lon: 33.4,  lat: 35.1,  iso: 196 },\r\n            HU: { name: 'Hongrie',    flag: '\ud83c\udded\ud83c\uddfa', lon: 19.5,  lat: 47.2,  iso: 348 },\r\n            DE: { name: 'Allemagne',  flag: '\ud83c\udde9\ud83c\uddea', lon: 10.5,  lat: 51.2,  iso: 276 },\r\n            LT: { name: 'Lituanie',   flag: '\ud83c\uddf1\ud83c\uddf9', lon: 23.9,  lat: 55.2,  iso: 440 },\r\n            NO: { name: 'Norv\u00e8ge',    flag: '\ud83c\uddf3\ud83c\uddf4', lon: 9.5,   lat: 61.0,  iso: 578 },\r\n            RS: { name: 'Serbie',     flag: '\ud83c\uddf7\ud83c\uddf8', lon: 21.0,  lat: 44.0,  iso: 688 },\r\n            MT: { name: 'Malte',      flag: '\ud83c\uddf2\ud83c\uddf9', lon: 14.5,  lat: 35.9,  iso: 470 },\r\n            EE: { name: 'Estonie',    flag: '\ud83c\uddea\ud83c\uddea', lon: 25.0,  lat: 58.6,  iso: 233 },\r\n            FI: { name: 'Finlande',   flag: '\ud83c\uddeb\ud83c\uddee', lon: 24.9,  lat: 60.5,  iso: 246 },\r\n            NL: { name: 'Pays-Bas',   flag: '\ud83c\uddf3\ud83c\uddf1', lon: 5.3,   lat: 52.2,  iso: 528 }\r\n        };\r\n\r\n        const territoryHubs = {\r\n            'METROPOLE AIX MARSEILLE': { name: 'Marseille',  lon: 5.37,  lat: 43.30, primary: true },\r\n            \/\/ Hub symbolique : les territoires ultramarins sont affich\u00e9s pr\u00e8s de la France\r\n            \/\/ pour garder un cadrage Europe lisible.\r\n            'OUTRE MER':               { name: 'Outre-mer',  lon: -5.25, lat: 46.70 },\r\n            'BIARRITZ':                { name: 'Biarritz',   lon: -1.56, lat: 43.48 },\r\n            'STRASBOURG':              { name: 'Strasbourg', lon: 7.75,  lat: 48.58 },\r\n            'PARIS':                   { name: 'Paris',      lon: 2.35,  lat: 48.85 }\r\n        };\r\n        const defaultHubKey = 'METROPOLE AIX MARSEILLE';\r\n\r\n        const colorPalette = [\r\n            '#1a2050', '#c63a3a', '#7ed3cc', '#e0b540', '#a02a8c', '#4a90c8',\r\n            '#6b8e23', '#c87030', '#2e8b57', '#b09de0', '#a02060', '#8a85d4',\r\n            '#4055a0', '#5d7e3a', '#d96a5e', '#6e9bb8', '#a86d3a', '#3d6c8a',\r\n            '#b94a73', '#7c5a4a', '#5e8a76', '#f4c890', '#8e3d7a', '#4f7b6c'\r\n        ];\r\n\r\n        \/\/ Pays mis en avant sur la carte m\u00eame s'ils n'ont pas (encore) de ligne\r\n        const showcaseExtra = ['PL', 'BG', 'DK'];\r\n        const franceISO = 250;\r\n\r\n        \/\/ \u2500\u2500 Helpers de chargement (m\u00eames patterns que la Roue) \u2500\u2500\r\n        function canonicalKey(s) {\r\n            return String(s || '')\r\n                .toLowerCase()\r\n                .normalize('NFD').replace(\/[\\u0300-\\u036f]\/g, '')\r\n                .replace(\/[^a-z0-9]+\/g, '');\r\n        }\r\n        function readRowValue(row, candidates) {\r\n            if (!row) return '';\r\n            const map = {};\r\n            Object.keys(row).forEach(k => { map[canonicalKey(k)] = row[k]; });\r\n            for (const cand of candidates) {\r\n                const v = map[canonicalKey(cand)];\r\n                if (v !== undefined && String(v).trim() !== '') return String(v).trim();\r\n            }\r\n            return '';\r\n        }\r\n        function parseAmount(str) {\r\n            if (str == null) return 0;\r\n            const cleaned = String(str)\r\n                .replace(\/[\\u00a0\\u202f\\s]\/g, '')\r\n                .replace(\/[\u20ac$]\/g, '')\r\n                .replace(\/,\/g, '.')\r\n                .replace(\/[^\\d.-]\/g, '');\r\n            const n = parseFloat(cleaned);\r\n            return isFinite(n) ? n : 0;\r\n        }\r\n        function formatAmount(n) {\r\n            if (!n) return '';\r\n            try { return new Intl.NumberFormat('fr-FR').format(Math.round(n)) + ' \u20ac'; }\r\n            catch (_) { return Math.round(n) + ' \u20ac'; }\r\n        }\r\n        const countryAliases = (() => {\r\n            const map = {};\r\n            Object.entries(countryInfo).forEach(([code, info]) => {\r\n                map[canonicalKey(info.name)] = code;\r\n            });\r\n            \/\/ Variantes orthographiques fr\u00e9quentes (sans accents\/majuscules) :\r\n            const extras = {\r\n                'grece':'GR','gr\u00e8ce':'GR','espagne':'ES','italie':'IT','portugal':'PT',\r\n                'irlande':'IE','allemagne':'DE','pologne':'PL','pologn':'PL',\r\n                'danemark':'DK','belgique':'BE','luxembourg':'LU','bulgarie':'BG',\r\n                'roumanie':'RO','turquie':'TR','suede':'SE','su\u00e8de':'SE','suedee':'SE',\r\n                'norvege':'NO','norv\u00e8ge':'NO','hongrie':'HU','tchequie':'CZ',\r\n                'republiquetcheque':'CZ','rtcheque':'CZ','chypre':'CY','lettonie':'LV',\r\n                'lituanie':'LT','serbie':'RS',\r\n                'malte':'MT','malta':'MT','estonie':'EE','estonia':'EE',\r\n                'finlande':'FI','finland':'FI',\r\n                'paysbas':'NL','paysb':'NL','hollande':'NL','netherlands':'NL','nederland':'NL',\r\n                'budapest':'HU','bruxelles':'BE','brussels':'BE',\r\n                'tenerif':'ES','tenerife':'ES','canaries':'ES'\r\n            };\r\n            Object.entries(extras).forEach(([k, v]) => { map[canonicalKey(k)] = v; });\r\n            return map;\r\n        })();\r\n        function parseCountries(str) {\r\n            if (!str) return [];\r\n            return String(str)\r\n                .replace(\/\\s+(et|and)\\s+\/gi, ',')\r\n                .split(\/[,;\/]\/)\r\n                .map(s => canonicalKey(s))\r\n                .map(s => countryAliases[s] || null)\r\n                .filter(Boolean);\r\n        }\r\n        function uniqueList(items) {\r\n            const seen = new Set();\r\n            return (items || []).filter(item => {\r\n                const key = String(item || '').trim();\r\n                if (!key || seen.has(key)) return false;\r\n                seen.add(key);\r\n                return true;\r\n            });\r\n        }\r\n        function extractLinks(str) {\r\n            const matches = String(str || '').match(\/https?:\\\/\\\/[^\\s<>\"']+\/gi) || [];\r\n            return uniqueList(matches.map(url => url.replace(\/[),.;]+$\/g, '')));\r\n        }\r\n        function stripLinks(str) {\r\n            return String(str || '').replace(\/https?:\\\/\\\/[^\\s<>\"']+\/gi, '').trim();\r\n        }\r\n        function detectTerritory(raw) {\r\n            const k = canonicalKey(raw);\r\n            if (!k) return defaultHubKey;\r\n            if (\r\n                k.includes('outremer') ||\r\n                k.includes('mayotte') ||\r\n                k.includes('nouvellecaledonie') ||\r\n                k.includes('guadeloupe') ||\r\n                k.includes('martinique') ||\r\n                k.includes('reunion') ||\r\n                k.includes('guyane') ||\r\n                k.includes('polynesie') ||\r\n                k.includes('wallis') ||\r\n                k.includes('futuna') ||\r\n                k.includes('saintmartin') ||\r\n                k.includes('saintbarthelemy')\r\n            ) return 'OUTRE MER';\r\n            if (k.includes('biarritz')) return 'BIARRITZ';\r\n            if (k.includes('strasbourg')) return 'STRASBOURG';\r\n            if (k.includes('paris')) return 'PARIS';\r\n            const aliasMap = {};\r\n            Object.keys(territoryHubs).forEach(key => { aliasMap[canonicalKey(key)] = key; });\r\n            \/\/ alias suppl\u00e9mentaires\r\n            aliasMap[canonicalKey('Aix Marseille')] = 'METROPOLE AIX MARSEILLE';\r\n            aliasMap[canonicalKey('Marseille')]     = 'METROPOLE AIX MARSEILLE';\r\n            aliasMap[canonicalKey('AMP')]           = 'METROPOLE AIX MARSEILLE';\r\n            return aliasMap[k] || defaultHubKey;\r\n        }\r\n\r\n        function toCsvUrl(rawUrl) {\r\n            const value = String(rawUrl || '').trim();\r\n            if (!value) return '';\r\n            if (!value.includes('docs.google.com\/spreadsheets\/')) return value;\r\n            const gidMatch = value.match(\/[?#&]gid=(\\d+)\/);\r\n            const gid = gidMatch ? gidMatch[1] : '0';\r\n            const sheetMatch = value.match(\/\\\/spreadsheets\\\/d\\\/([^\/]+)\/);\r\n            \/\/ Cas d'une URL \"normale\" \/edit, \/view, ou bare sheet ID\r\n            if (sheetMatch && !value.includes('\/spreadsheets\/d\/e\/')) {\r\n                return `https:\/\/docs.google.com\/spreadsheets\/d\/${sheetMatch[1]}\/export?format=csv&gid=${gid}`;\r\n            }\r\n            \/\/ Cas d'une URL publi\u00e9e \/pub\r\n            const published = value.replace('\/pubhtml', '\/pub');\r\n            if (\/[?&](output|format)=csv\\b\/i.test(published)) return published;\r\n            return `${published}${published.includes('?') ? '&' : '?'}output=csv`;\r\n        }\r\n        function isGoogleSheetUrl(rawUrl) {\r\n            return String(rawUrl || '').includes('docs.google.com\/spreadsheets\/');\r\n        }\r\n        function toGoogleVizUrl(rawUrl, callbackName) {\r\n            const value = String(rawUrl || '').trim();\r\n            const publishedMatch = value.match(\/\\\/spreadsheets\\\/d\\\/e\\\/([^\/]+)\/);\r\n            const privateMatch  = value.match(\/\\\/spreadsheets\\\/d\\\/([^\/]+)\/);\r\n            const id = publishedMatch?.[1] || privateMatch?.[1];\r\n            if (!id) return '';\r\n            const base = publishedMatch\r\n                ? `https:\/\/docs.google.com\/spreadsheets\/d\/e\/${id}\/gviz\/tq`\r\n                : `https:\/\/docs.google.com\/spreadsheets\/d\/${id}\/gviz\/tq`;\r\n            const gidMatch = value.match(\/[?#&]gid=(\\d+)\/);\r\n            const params = [\r\n                `tqx=${encodeURIComponent('out:json;responseHandler:' + callbackName)}`,\r\n                'headers=1',\r\n                `_=${Date.now()}`\r\n            ];\r\n            if (gidMatch) params.push(`gid=${encodeURIComponent(gidMatch[1])}`);\r\n            return `${base}?${params.join('&')}`;\r\n        }\r\n        function googleVizResponseToRows(response) {\r\n            if (!response || response.status !== 'ok') {\r\n                throw new Error(response?.errors?.[0]?.detailed_message || 'Google Sheet non lisible.');\r\n            }\r\n            const table = response.table || {};\r\n            const headers = (table.cols || []).map((col, index) => col.label || col.id || ('Colonne ' + (index + 1)));\r\n            return (table.rows || []).map(row => {\r\n                const out = {};\r\n                headers.forEach((header, index) => {\r\n                    const cell = row.c?.[index];\r\n                    out[header] = cell ? (cell.f ?? cell.v ?? '') : '';\r\n                });\r\n                return out;\r\n            });\r\n        }\r\n        function loadGoogleSheetViaGviz(rawUrl) {\r\n            return new Promise((resolve, reject) => {\r\n                const callbackName = '__sheetCallback_' + Date.now() + '_' + Math.floor(Math.random() * 100000);\r\n                const script = document.createElement('script');\r\n                const cleanup = () => { delete window[callbackName]; script.remove(); };\r\n                const timeout = window.setTimeout(() => {\r\n                    cleanup();\r\n                    reject(new Error('Timeout pendant le chargement Google Sheet.'));\r\n                }, 15000);\r\n                window[callbackName] = response => {\r\n                    window.clearTimeout(timeout);\r\n                    try { resolve(googleVizResponseToRows(response)); }\r\n                    catch (e) { reject(e); }\r\n                    finally { cleanup(); }\r\n                };\r\n                script.onerror = () => { window.clearTimeout(timeout); cleanup(); reject(new Error('Impossible de charger la sheet (JSONP).')); };\r\n                script.src = toGoogleVizUrl(rawUrl, callbackName);\r\n                if (!script.src) { cleanup(); reject(new Error('URL Google Sheets non reconnue.')); return; }\r\n                document.head.appendChild(script);\r\n            });\r\n        }\r\n        function loadRowsFromSheet(rawUrl) {\r\n            if (!rawUrl) return Promise.resolve(null);\r\n            \/\/ Strat\u00e9gie : d'abord CSV direct ; si CORS\/login\/erreur, on retombe sur le gviz JSONP.\r\n            if (isGoogleSheetUrl(rawUrl)) {\r\n                return loadGoogleSheetViaGviz(rawUrl).catch(err => {\r\n                    console.warn('gviz a \u00e9chou\u00e9, on tente CSV direct.', err);\r\n                    const csvUrl = toCsvUrl(rawUrl);\r\n                    return d3.csv(`${csvUrl}${csvUrl.includes('?') ? '&' : '?'}_=${Date.now()}`);\r\n                });\r\n            }\r\n            const csvUrl = toCsvUrl(rawUrl);\r\n            return d3.csv(`${csvUrl}${csvUrl.includes('?') ? '&' : '?'}_=${Date.now()}`);\r\n        }\r\n\r\n        \/\/ \u2500\u2500 Fallback (donn\u00e9e hardcod\u00e9e si pas de lien CSV) \u2500\u2500\r\n        const fallbackAssociations = [\r\n            { name: 'Hatoup!',                                              countries: ['IT', 'GR', 'ES'], territoire: defaultHubKey },\r\n            { name: 'Centre social La Farandole',                           countries: ['TR', 'IE', 'ES'], territoire: defaultHubKey },\r\n            { name: 'Club de plong\u00e9e et arch\u00e9ologie de Port de Bouc',       countries: ['GR'],             territoire: defaultHubKey },\r\n            { name: 'Acelem',                                               countries: ['BE'],             territoire: defaultHubKey },\r\n            { name: 'Z\u00c9MEN',                                                countries: ['ES'],             territoire: defaultHubKey },\r\n            { name: 'Association FIFRELIN',                                 countries: ['BE'],             territoire: defaultHubKey },\r\n            { name: \"Apprendre l'anglais\",                                  countries: ['TR', 'RO'],       territoire: defaultHubKey },\r\n            { name: 'Association culture sans fronti\u00e8re Marseille',         countries: ['TR', 'IE'],       territoire: defaultHubKey },\r\n            { name: 'AASEC - Tichadou',                                     countries: ['GR'],             territoire: defaultHubKey },\r\n            { name: 'Provence Campus',                                      countries: ['GR'],             territoire: defaultHubKey },\r\n            { name: 'ASC Littoral',                                         countries: ['IE'],             territoire: defaultHubKey },\r\n            { name: 'Happy Day',                                            countries: ['BE', 'ES'],       territoire: defaultHubKey },\r\n            { name: 'Terre Ludique',                                        countries: ['PT'],             territoire: defaultHubKey },\r\n            { name: 'Effi-science',                                         countries: ['BE'],             territoire: defaultHubKey }\r\n        ];\r\n\r\n        function parseYearFromHorodateur(raw) {\r\n            if (raw == null) return '';\r\n            const s = String(raw).trim();\r\n            if (!s) return '';\r\n            const n = parseFloat(s.replace(',', '.'));\r\n            if (isFinite(n)) {\r\n                if (n >= 1000 && n <= 9999) return String(Math.round(n));\r\n                if (n > 30000) {\r\n                    \/\/ Excel serial (jours depuis 1899-12-30)\r\n                    const d = new Date(Date.UTC(1899, 11, 30) + Math.round(n) * 86400000);\r\n                    return String(d.getUTCFullYear());\r\n                }\r\n            }\r\n            const ddmmyyyy = s.match(\/(\\d{1,2})[\\\/\\.\\-](\\d{1,2})[\\\/\\.\\-](\\d{2,4})\/);\r\n            if (ddmmyyyy) {\r\n                const y = ddmmyyyy[3];\r\n                return y.length === 2 ? '20' + y : y;\r\n            }\r\n            const yyyymmdd = s.match(\/(\\d{4})[\\\/\\.\\-]\\d{1,2}[\\\/\\.\\-]\\d{1,2}\/);\r\n            if (yyyymmdd) return yyyymmdd[1];\r\n            const any = s.match(\/\\b(20\\d{2})\\b\/);\r\n            if (any) return any[1];\r\n            return '';\r\n        }\r\n\r\n        function rowsToAssociations(rows) {\r\n            if (!rows || !rows.length) return [];\r\n            let headers = Object.keys(rows[0] || {});\r\n\r\n            \/\/ \u2500\u2500 FIX format SHL Cartographie \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n            \/\/ La 1re colonne contient l'ann\u00e9e du d\u00e9p\u00f4t mais n'a pas de nom\r\n            \/\/ (cellule A1 vide ou un simple espace). Si on d\u00e9tecte\r\n            \/\/ \"Nom de l'association\" \u00e0 c\u00f4t\u00e9, on renomme la 1re colonne en\r\n            \/\/ \"Horodateur\" pour d\u00e9clencher parseShlFormRows.\r\n            const firstKey = headers[0];\r\n            const firstKeyBlank = firstKey != null && canonicalKey(firstKey) === '';\r\n            const hasAssoName = headers.some(h => canonicalKey(h) === 'nomdelassociation');\r\n            if (firstKeyBlank && hasAssoName) {\r\n                rows = rows.map(r => {\r\n                    const nr = { 'Horodateur': r[firstKey] };\r\n                    Object.keys(r).forEach(k => { if (k !== firstKey) nr[k] = r[k]; });\r\n                    return nr;\r\n                });\r\n                headers = Object.keys(rows[0]);\r\n                console.log('[Carte Partenaire] 1re colonne renomm\u00e9e en \"Horodateur\".');\r\n            }\r\n            \/\/ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n            \/\/ D\u00e9tecte un \u00e9ventuel titre en 1re ligne (les vrais en-t\u00eates seraient en ligne 2).\r\n            const hasTerritoire = headers.some(h => canonicalKey(h) === canonicalKey('Territoire'));\r\n            const hasHorodateur = headers.some(h => canonicalKey(h) === canonicalKey('Horodateur'));\r\n            const hasStructure = headers.some(h => canonicalKey(h) === canonicalKey('Structure'));\r\n            if (!hasTerritoire && !hasHorodateur && rows.length > 1) {\r\n                const firstRow = rows[0];\r\n                const isHeaderRow = Object.values(firstRow).some(v =>\r\n                    canonicalKey(v) === canonicalKey('Territoire') || canonicalKey(v) === canonicalKey('Structure') || canonicalKey(v) === canonicalKey('Horodateur'));\r\n                if (isHeaderRow) {\r\n                    const newHeaders = headers.map(h => firstRow[h] || h);\r\n                    rows = rows.slice(1).map(row => {\r\n                        const out = {};\r\n                        headers.forEach((oldKey, i) => { out[newHeaders[i]] = row[oldKey]; });\r\n                        return out;\r\n                    });\r\n                }\r\n            }\r\n\r\n            \/\/ Format formulaire SHL (Horodateur + Nom de l'association) : 1 soumission = 1 projet,\r\n            \/\/ lignes suivantes sans nom = pays additionnels du m\u00eame projet.\r\n            if (hasHorodateur && !hasStructure) {\r\n                return parseShlFormRows(rows);\r\n            }\r\n\r\n            \/\/ Format \"carte\" (Territoire \/ Structure \/ 2024-2025 - d\u00e9tail\/montant\/lien)\r\n            const grouped = new Map();\r\n            rows.forEach(row => {\r\n                const structure = readRowValue(row, ['Structure', 'Association', 'Nom']);\r\n                if (!structure) return;\r\n                const territoireRaw = readRowValue(row, ['Territoire']);\r\n                const territoire = detectTerritory(territoireRaw);\r\n                const countries = parseCountries(readRowValue(row, ['Pays \/ partenaires', 'Pays partenaires', 'Pays']));\r\n                const detail2024 = readRowValue(row, ['2024 - d\u00e9tail', '2024 detail', '2024d\u00e9tail', '2024 - detail']);\r\n                const amount2024 = parseAmount(readRowValue(row, ['2024 - montant (\u20ac)', '2024 - montant', '2024 montant', 'Montant 2024']));\r\n                const link2024   = readRowValue(row, ['2024 - lien', '2024 lien', 'Lien 2024']);\r\n                const detail2025 = readRowValue(row, ['2025 - d\u00e9tail', '2025 detail', '2025d\u00e9tail', '2025 - detail']);\r\n                const amount2025 = parseAmount(readRowValue(row, ['2025 - montant (\u20ac)', '2025 - montant', '2025 montant', 'Montant 2025']));\r\n                const link2025   = readRowValue(row, ['2025 - lien', '2025 lien', 'Lien 2025']);\r\n                const detail2026 = readRowValue(row, ['2026 - d\u00e9tail', '2026 detail', '2026d\u00e9tail', '2026 - detail']);\r\n                const amount2026 = parseAmount(readRowValue(row, ['2026 - montant (\u20ac)', '2026 - montant', '2026 montant', 'Montant 2026']));\r\n                const link2026   = readRowValue(row, ['2026 - lien', '2026 lien', 'Lien 2026']);\r\n                const notes      = readRowValue(row, ['Notes', 'Note']);\r\n                const totalLine  = parseAmount(readRowValue(row, ['Total ligne (\u20ac)', 'Total ligne', 'Total']));\r\n\r\n                const key = structure.trim().toLowerCase();\r\n                if (!grouped.has(key)) {\r\n                    grouped.set(key, { name: structure.trim(), territoire, countries: new Set(), projects: [], totalAmount: 0, notes: '', categories: new Set() });\r\n                }\r\n                const a = grouped.get(key);\r\n                countries.forEach(c => a.countries.add(c));\r\n                if (detail2024 || amount2024 || link2024) {\r\n                    a.projects.push({ year: '2024', detail: detail2024, amount: amount2024, link: link2024, countries, category: 'autre' });\r\n                    a.totalAmount += amount2024;\r\n                }\r\n                if (detail2025 || amount2025 || link2025) {\r\n                    a.projects.push({ year: '2025', detail: detail2025, amount: amount2025, link: link2025, countries, category: 'autre' });\r\n                    a.totalAmount += amount2025;\r\n                }\r\n                if (detail2026 || amount2026 || link2026) {\r\n                    a.projects.push({ year: '2026', detail: detail2026, amount: amount2026, link: link2026, countries, category: 'autre' });\r\n                    a.totalAmount += amount2026;\r\n                }\r\n                if (!a.totalAmount && totalLine) a.totalAmount = totalLine;\r\n                if (notes && !a.notes) a.notes = notes;\r\n            });\r\n            return Array.from(grouped.values())\r\n                .filter(a => a.countries.size > 0)\r\n                .map((a, i) => ({\r\n                    ...a,\r\n                    id: 'a-' + i,\r\n                    color: colorPalette[i % colorPalette.length],\r\n                    countries: Array.from(a.countries),\r\n                    categories: Array.from(a.categories)\r\n                }))\r\n                .sort((a, b) => a.name.localeCompare(b.name, 'fr'));\r\n        }\r\n\r\n        \/\/ Cat\u00e9gorise un Type de projet brut en cl\u00e9 de filtre.\r\n        \/\/ Retourne 'individuel' | 'cooperation' | 'mobilite' | 'autre'.\r\n        function categorizeType(typeProjet) {\r\n            const k = canonicalKey(typeProjet);\r\n            if (!k) return 'autre';\r\n            if (k.indexOf('individuelle') >= 0 || k.indexOf('individuel') >= 0) return 'individuel';\r\n            if (k.indexOf('cooperation') >= 0 || k.indexOf('coop') >= 0) return 'cooperation';\r\n            if (k.indexOf('mobilite') >= 0) return 'mobilite';\r\n            return 'autre';\r\n        }\r\n\r\n        function parseShlFormRows(rows) {\r\n            const grouped = new Map();\r\n            let ctx = null;            \/\/ dernier contexte projet (asso, ann\u00e9e\u2026)\r\n            let currentProject = null; \/\/ projet \"en cours\" dont les lignes suivantes peuvent \u00e9tendre les pays\r\n\r\n            function flushPending() {\r\n                if (currentProject && currentProject._asso) {\r\n                    const a = currentProject._asso;\r\n                    delete currentProject._asso;\r\n                    a.projects.push(currentProject);\r\n                    a.totalAmount += (currentProject.amount || 0);\r\n                    if (currentProject.category) a.categories.add(currentProject.category);\r\n                    currentProject = null;\r\n                }\r\n            }\r\n\r\n            rows.forEach(row => {\r\n                const rawType = readRowValue(row, ['Type de projet']);\r\n                const cat = categorizeType(rawType);\r\n\r\n                \/\/ Pour les mobilit\u00e9s individuelles : le formulaire est d\u00e9tourn\u00e9\r\n                \/\/  \u2192 \"Nom\" = Public cible, \"Th\u00e8me\" = Description.\r\n                const isIndividuel = cat === 'individuel';\r\n\r\n                const assoNameStd = readRowValue(row, [\"Nom de l'association\", 'Nom de lassociation', 'Nom association', 'Association', 'Nom']);\r\n                const assoNameAlt = isIndividuel ? readRowValue(row, ['Public cible']) : '';\r\n                const assoName = assoNameStd || assoNameAlt;\r\n\r\n                const isNew = !!assoName;\r\n                if (isNew) {\r\n                    flushPending();\r\n                    const sousThema = readRowValue(row, ['Sous-th\u00e9matique', 'Sous thematique', 'Soustematique']);\r\n                    const thematique = readRowValue(row, ['Th\u00e9matique', 'Thematique']);\r\n                    const domaine = readRowValue(row, ['Domaine']);\r\n                    const description = readRowValue(row, ['Description']);\r\n                    const detailProjet = readRowValue(row, ['D\u00e9tail du projet', 'Detail du projet', 'D\u00e9tail', 'Detail']);\r\n                    \/\/ Les fiches Erasmus+ sont parfois coll\u00e9es dans \"D\u00e9tail du projet\",\r\n                    \/\/ seules ou sur plusieurs lignes : on les extrait toutes en liens.\r\n                    const projectLinks = extractLinks(detailProjet);\r\n                    const detailText = stripLinks(detailProjet);\r\n                    ctx = {\r\n                        name: assoName.trim(),\r\n                        year: parseYearFromHorodateur(readRowValue(row, ['Horodateur'])),\r\n                        territoire: detectTerritory(readRowValue(row, ['Zone', 'Territoire'])),\r\n                        amount: parseAmount(readRowValue(row, ['Montant'])),\r\n                        typeProjet: rawType,\r\n                        category: cat,\r\n                        \/\/ Pour les individuelles, le \"th\u00e8me\" est la Description.\r\n                        theme: isIndividuel ? (description || detailText) : (sousThema || thematique || domaine || detailText),\r\n                        notesLong: isIndividuel ? '' : (detailText || description),\r\n                        links: projectLinks\r\n                    };\r\n                }\r\n                if (!ctx) return;\r\n\r\n                const countries = parseCountries(readRowValue(row, ['Pays partenaire', 'Pays partenaires', 'Pays \/ partenaires', 'Pays']));\r\n                if (countries.length === 0 && !isNew) return;\r\n\r\n                const key = ctx.name.toLowerCase();\r\n                if (!grouped.has(key)) {\r\n                    grouped.set(key, {\r\n                        name: ctx.name,\r\n                        territoire: ctx.territoire,\r\n                        countries: new Set(),\r\n                        projects: [],\r\n                        totalAmount: 0,\r\n                        notes: '',\r\n                        categories: new Set()\r\n                    });\r\n                }\r\n                const a = grouped.get(key);\r\n                countries.forEach(c => a.countries.add(c));\r\n\r\n                if (isNew) {\r\n                    const detailParts = [];\r\n                    if (ctx.typeProjet) detailParts.push(ctx.typeProjet.trim());\r\n                    if (ctx.theme && (!ctx.typeProjet || canonicalKey(ctx.theme) !== canonicalKey(ctx.typeProjet))) {\r\n                        detailParts.push(ctx.theme.trim());\r\n                    }\r\n                    const detailLabel = detailParts.join(' \u00b7 ');\r\n\r\n                    currentProject = {\r\n                        year: ctx.year || '',\r\n                        detail: detailLabel,\r\n                        amount: ctx.amount,\r\n                        link: ctx.links[0] || '',\r\n                        links: ctx.links,\r\n                        countries: [...countries],\r\n                        typeProjet: ctx.typeProjet,\r\n                        category: ctx.category,\r\n                        _asso: a\r\n                    };\r\n                    if (!a.notes && ctx.notesLong) a.notes = ctx.notesLong;\r\n                } else if (currentProject) {\r\n                    countries.forEach(c => {\r\n                        if (!currentProject.countries.includes(c)) currentProject.countries.push(c);\r\n                    });\r\n                }\r\n            });\r\n            flushPending();\r\n\r\n            return Array.from(grouped.values())\r\n                .filter(a => a.countries.size > 0)\r\n                .map((a, i) => ({\r\n                    ...a,\r\n                    id: 'a-' + i,\r\n                    color: colorPalette[i % colorPalette.length],\r\n                    countries: Array.from(a.countries),\r\n                    categories: Array.from(a.categories)\r\n                }))\r\n                .sort((a, b) => a.name.localeCompare(b.name, 'fr'));\r\n        }\r\n\r\n        function fallbackToAssociations() {\r\n            return fallbackAssociations.map((a, i) => ({\r\n                ...a,\r\n                id: 'a-' + i,\r\n                color: colorPalette[i % colorPalette.length],\r\n                projects: [],\r\n                totalAmount: 0,\r\n                notes: '',\r\n                categories: []\r\n            }));\r\n        }\r\n\r\n        \/\/ \u00c9tat partag\u00e9 : associations actives, pays li\u00e9s\r\n        let allAssociations = [];\r\n        let associations = [];\r\n        let partnerISOs = new Set();\r\n        let selectedAsso = null;\r\n        let selectedTerritory = null;\r\n        let currentFilter = 'tous';\r\n\r\n        function setAssociations(list) {\r\n            allAssociations = list;\r\n            applyCurrentFilter();\r\n        }\r\n\r\n        function applyCurrentFilter() {\r\n            associations = allAssociations.filter(a => {\r\n                if (currentFilter === 'tous') return true;\r\n                return (a.categories || []).indexOf(currentFilter) !== -1;\r\n            });\r\n            const linked = new Set();\r\n            associations.forEach(a => a.countries.forEach(c => linked.add(countryInfo[c]?.iso)));\r\n            partnerISOs = new Set([...linked, ...showcaseExtra.map(c => countryInfo[c].iso)].filter(Boolean));\r\n            updateStats();\r\n            renderAssoList();\r\n        }\r\n\r\n        function setFilter(cat) {\r\n            if (cat === currentFilter) return;\r\n            currentFilter = cat;\r\n            const root = document.getElementById('shl-map-container');\r\n            if (root) {\r\n                root.querySelectorAll('.carte-filter-chip').forEach(b => {\r\n                    b.classList.toggle('active', b.dataset.cat === cat);\r\n                });\r\n            }\r\n            selectedAsso = null;\r\n            selectedTerritory = null;\r\n            applyCurrentFilter();\r\n            renderMap(!europeFeatures || !europeFeatures.length);\r\n        }\r\n\r\n        function updateStats() {\r\n            document.getElementById('stat-asso').textContent = associations.length;\r\n            document.getElementById('stat-pays').textContent = new Set(associations.flatMap(a => a.countries)).size;\r\n            document.getElementById('stat-part').textContent = associations.reduce((s, a) => s + a.countries.length, 0);\r\n        }\r\n\r\n        function renderAssoList() {\r\n            const listEl = document.getElementById('asso-list');\r\n            listEl.innerHTML = '';\r\n            associations.forEach((a, i) => {\r\n                const card = document.createElement('div');\r\n                card.className = 'asso-card';\r\n                card.dataset.id = a.id;\r\n                card.style.animationDelay = `${i * 25}ms`;\r\n                const countryLabels = a.countries.map(c => countryInfo[c]?.name || c).join(' \u00b7 ');\r\n                const hubName = territoryHubs[a.territoire]?.name || '';\r\n                const hubLine = hubName && hubName !== 'Marseille' ? `<div class=\"asso-hub\">${hubName}<\/div>` : '';\r\n                card.innerHTML = `\r\n                    <span class=\"asso-chip\" style=\"background:${a.color}\"><\/span>\r\n                    <div class=\"asso-body\">\r\n                        <div class=\"asso-name\">${a.name}<\/div>\r\n                        ${hubLine}\r\n                        <div class=\"asso-countries\">${countryLabels}<\/div>\r\n                    <\/div>\r\n                `;\r\n                card.onmouseenter = () => focusAsso(a);\r\n                card.onmouseleave = () => resetMapFocus();\r\n                card.onclick = () => showAssoDetail(a);\r\n                listEl.appendChild(card);\r\n            });\r\n        }\r\n\r\n        function showAssoDetail(asso) {\r\n            document.querySelectorAll('#shl-map-container .asso-card').forEach(c => c.classList.remove('c-selected'));\r\n            const listCard = document.querySelector(`#shl-map-container .asso-card[data-id=\"${asso.id}\"]`);\r\n            if (listCard) listCard.classList.add('c-selected');\r\n\r\n            const hubName = territoryHubs[asso.territoire]?.name || '';\r\n            const meta = [`${asso.countries.length} pays partenaire${asso.countries.length > 1 ? 's' : ''}`];\r\n            if (hubName) meta.unshift(hubName);\r\n\r\n            const projectsHTML = asso.projects.length ? `\r\n                <div class=\"detail-items-title\">${asso.projects.length} projet${asso.projects.length > 1 ? 's' : ''}<\/div>\r\n                ${asso.projects.map(p => {\r\n                    const projCountries = (p.countries || [])\r\n                        .map(code => {\r\n                            const c = countryInfo[code];\r\n                            return c ? `<span class=\"flag-item\"><span class=\"carte-flag\">${c.flag}<\/span>${c.name}<\/span>` : '';\r\n                        }).filter(Boolean).join('');\r\n                    const head = `\r\n                        <div class=\"project-card-head\">\r\n                            <span class=\"project-year-badge\">${p.year}<\/span>\r\n                            ${p.amount ? `<span class=\"project-amount-big\">${formatAmount(p.amount)}<\/span>` : ''}\r\n                        <\/div>\r\n                    `;\r\n                    return `\r\n                        <div class=\"project-card\">\r\n                            ${head}\r\n                            ${p.detail ? `<div class=\"project-detail\">${p.detail}<\/div>` : ''}\r\n                            ${projCountries ? `<div class=\"project-countries\">${projCountries}<\/div>` : ''}\r\n                            ${uniqueList([...(p.links || []), p.link]).map((link, index, links) =>\r\n                                `<a class=\"project-link\" href=\"${link}\" target=\"_blank\" rel=\"noopener\">Fiche Erasmus+${links.length > 1 ? ' ' + (index + 1) : ''} \u2197<\/a>`\r\n                            ).join('')}\r\n                        <\/div>\r\n                    `;\r\n                }).join('')}\r\n            ` : '';\r\n\r\n            const totalHTML = asso.totalAmount ? `\r\n                <div class=\"detail-total\">\r\n                    <span class=\"detail-total-label\">Total financement<\/span>\r\n                    <span class=\"detail-total-value\">${formatAmount(asso.totalAmount)}<\/span>\r\n                <\/div>\r\n            ` : '';\r\n\r\n            \/\/ Liste compacte si beaucoup de pays, pills sinon\r\n            const manyCountries = asso.countries.length > 5;\r\n            const countriesHTML = manyCountries\r\n                ? `<div class=\"flag-grid\">${asso.countries.map(code => {\r\n                    const c = countryInfo[code];\r\n                    return c ? `<span class=\"flag-item\"><span class=\"carte-flag\">${c.flag}<\/span>${c.name}<\/span>` : '';\r\n                }).filter(Boolean).join('')}<\/div>`\r\n                : asso.countries.map(code => {\r\n                    const c = countryInfo[code];\r\n                    return `<div class=\"country-pill\"><span class=\"carte-flag\">${c?.flag || ''}<\/span><span class=\"country-name\">${c?.name || code}<\/span><\/div>`;\r\n                }).join('');\r\n\r\n            const dp = document.getElementById('carte-detail-panel');\r\n            dp.innerHTML = `\r\n                <button class=\"panel-back\" id=\"carte-back\">\u2190 Retour<\/button>\r\n                <div class=\"detail-header\">\r\n                    <span class=\"detail-chip\" style=\"background:${asso.color}\"><\/span>\r\n                    <div>\r\n                        <div class=\"detail-title\">${asso.name}<\/div>\r\n                        <div class=\"detail-meta\">${meta.join(' \u00b7 ')}<\/div>\r\n                    <\/div>\r\n                <\/div>\r\n                ${totalHTML}\r\n                <div class=\"detail-items-title\">${asso.countries.length} pays partenaire${asso.countries.length > 1 ? 's' : ''}<\/div>\r\n                ${countriesHTML}\r\n                ${projectsHTML}\r\n            `;\r\n            document.getElementById('carte-back').onclick = () => {\r\n                document.getElementById('carte-list-panel').classList.remove('sp-exit');\r\n                dp.classList.remove('sp-enter');\r\n                document.querySelectorAll('#shl-map-container .asso-card').forEach(c => c.classList.remove('c-selected'));\r\n                selectedAsso = null;\r\n                selectedTerritory = null;\r\n                resetMapFocus();\r\n            };\r\n            document.getElementById('carte-list-panel').classList.add('sp-exit');\r\n            dp.scrollTop = 0;\r\n            dp.classList.add('sp-enter');\r\n            selectedAsso = asso;\r\n            selectedTerritory = null;\r\n            focusAsso(asso);\r\n        }\r\n\r\n        const mapTooltip = document.getElementById('carte-map-tooltip');\r\n        const mapSvg = d3.select(mapEl).append('svg');\r\n        const flows = new Map();    \/\/ assoId -> array of paths\r\n        const dots = new Map();     \/\/ countryCode -> circle group\r\n        let destinationByCode = new Map();\r\n        let currentW = 0, currentH = 0;\r\n        let europeFeatures = null;\r\n        let firstRender = true;\r\n\r\n        function showAssoTooltip(asso, anchor) {\r\n            mapTooltip.innerHTML = `\r\n                <div class=\"carte-tooltip-title\">${asso.name}<\/div>\r\n                <div class=\"carte-tooltip-meta\">${asso.countries.map(c => countryInfo[c].name).join(' \u00b7 ')}<\/div>\r\n            `;\r\n            const x = anchor ? anchor[0] : currentW \/ 2;\r\n            const y = anchor ? anchor[1] : currentH \/ 2;\r\n            mapTooltip.style.left = `${Math.max(12, Math.min(currentW - 200, x + 14))}px`;\r\n            mapTooltip.style.top = `${Math.max(12, Math.min(currentH - 78, y - 38))}px`;\r\n            mapTooltip.classList.add('visible');\r\n        }\r\n        function hideMapTooltip() { mapTooltip.classList.remove('visible'); }\r\n\r\n        function setTooltipPosition(anchor) {\r\n            const x = anchor ? anchor[0] : currentW \/ 2;\r\n            const y = anchor ? anchor[1] : currentH \/ 2;\r\n            mapTooltip.style.left = `${Math.max(12, Math.min(currentW - 220, x + 14))}px`;\r\n            mapTooltip.style.top = `${Math.max(12, Math.min(currentH - 92, y - 38))}px`;\r\n            mapTooltip.classList.add('visible');\r\n        }\r\n\r\n        function focusAsso(asso) {\r\n            document.querySelectorAll('#shl-map-container .asso-card').forEach(c => {\r\n                c.classList.toggle('c-selected', c.dataset.id === asso.id);\r\n            });\r\n            flows.forEach((paths, id) => {\r\n                const dim = id !== asso.id;\r\n                paths.forEach(p => p.attr('stroke-opacity', dim ? 0.07 : 0.95).attr('stroke-width', dim ? p.attr('data-w') : (+p.attr('data-w') + 0.6)));\r\n            });\r\n            const focused = new Set(asso.countries);\r\n            dots.forEach((g, code) => {\r\n                g.select('circle.dot').attr('opacity', focused.has(code) ? 1 : 0.35);\r\n                g.select('circle.halo').attr('opacity', focused.has(code) ? 0.28 : 0.05);\r\n            });\r\n            \/\/ tooltip ancr\u00e9 sur le 1er pays partenaire\r\n            const firstCode = asso.countries[0];\r\n            const dest = destinationByCode.get(firstCode);\r\n            showAssoTooltip(asso, dest);\r\n        }\r\n\r\n        function focusTerritory(territoryKey, anchor) {\r\n            const territoryAssos = associations.filter(a => a.territoire === territoryKey);\r\n            const focusedIds = new Set(territoryAssos.map(a => a.id));\r\n            const focusedCountries = new Set(territoryAssos.flatMap(a => a.countries));\r\n            const hub = territoryHubs[territoryKey] || { name: territoryKey };\r\n            const linkCount = territoryAssos.reduce((sum, a) => sum + a.countries.length, 0);\r\n\r\n            document.querySelectorAll('#shl-map-container .asso-card').forEach(c => {\r\n                c.classList.toggle('c-selected', focusedIds.has(c.dataset.id));\r\n            });\r\n            flows.forEach((paths, id) => {\r\n                const dim = !focusedIds.has(id);\r\n                paths.forEach(p => p.attr('stroke-opacity', dim ? 0.06 : 0.95).attr('stroke-width', dim ? p.attr('data-w') : (+p.attr('data-w') + 0.8)));\r\n            });\r\n            dots.forEach((g, code) => {\r\n                g.select('circle.dot').attr('opacity', focusedCountries.has(code) ? 1 : 0.3);\r\n                g.select('circle.halo').attr('opacity', focusedCountries.has(code) ? 0.28 : 0.04);\r\n            });\r\n            mapTooltip.innerHTML = `\r\n                <div class=\"carte-tooltip-title\">${hub.name}<\/div>\r\n                <div class=\"carte-tooltip-meta\">${territoryAssos.length} association${territoryAssos.length > 1 ? 's' : ''} \u00b7 ${linkCount} lien${linkCount > 1 ? 's' : ''}<\/div>\r\n            `;\r\n            setTooltipPosition(anchor);\r\n        }\r\n\r\n        function resetMapFocus() {\r\n            if (selectedAsso) {\r\n                focusAsso(selectedAsso);\r\n                return;\r\n            }\r\n            if (selectedTerritory) {\r\n                const hub = territoryHubs[selectedTerritory];\r\n                const anchor = hub ? destinationByCode.get('__hub_' + selectedTerritory) : null;\r\n                focusTerritory(selectedTerritory, anchor);\r\n                return;\r\n            }\r\n            document.querySelectorAll('#shl-map-container .asso-card').forEach(c => c.classList.remove('c-selected'));\r\n            flows.forEach(paths => paths.forEach(p => p.attr('stroke-opacity', 0.7).attr('stroke-width', +p.attr('data-w'))));\r\n            dots.forEach(g => { g.select('circle.dot').attr('opacity', 1); g.select('circle.halo').attr('opacity', 0.12); });\r\n            hideMapTooltip();\r\n        }\r\n\r\n        function closeDetailPanel() {\r\n            const dp = document.getElementById('carte-detail-panel');\r\n            const lp = document.getElementById('carte-list-panel');\r\n            if (lp) lp.classList.remove('sp-exit');\r\n            if (dp) dp.classList.remove('sp-enter');\r\n        }\r\n\r\n        function isEurope(feature) {\r\n            const europeSet = new Set([8,20,40,56,70,100,191,196,203,208,233,246,250,276,300,348,352,372,380,392,428,440,442,470,492,498,499,528,578,616,620,642,643,688,703,705,724,752,756,792,804,807,826,10]);\r\n            return feature && feature.geometry && europeSet.has(+feature.id);\r\n        }\r\n\r\n        function renderFallbackMap() {\r\n            europeFeatures = [];\r\n            renderMap(true);\r\n        }\r\n\r\n        function renderMap(fallbackOnly) {\r\n            const rect = mapEl.getBoundingClientRect();\r\n            const w = Math.max(320, rect.width || mapEl.clientWidth || 700);\r\n            const h = Math.max(300, rect.height || mapEl.clientHeight || 600);\r\n            currentW = w;\r\n            currentH = h;\r\n\r\n            hideMapTooltip(); flows.clear(); dots.clear(); destinationByCode.clear();\r\n            mapSvg.selectAll('*').remove();\r\n            mapSvg.attr('viewBox', `0 0 ${w} ${h}`).attr('preserveAspectRatio', 'xMidYMid meet');\r\n\r\n            const animate = firstRender;\r\n            firstRender = false;\r\n\r\n            const projection = d3.geoMercator().center([14, 53]).scale(Math.min(w, h) * 1.15).translate([w \/ 2, h \/ 2 - Math.min(h, w) * 0.08]);\r\n            const geoPath = d3.geoPath().projection(projection);\r\n            \/\/ Projeter chaque hub (Marseille principal + autres territoires)\r\n            const hubPts = {};\r\n            Object.keys(territoryHubs).forEach(k => {\r\n                hubPts[k] = projection([territoryHubs[k].lon, territoryHubs[k].lat]);\r\n                if (hubPts[k]) destinationByCode.set('__hub_' + k, hubPts[k]);\r\n            });\r\n            const marseille = hubPts[defaultHubKey];\r\n\r\n            if (!fallbackOnly && europeFeatures && europeFeatures.length) {\r\n                mapSvg.selectAll('path.c-feat').data(europeFeatures).join('path')\r\n                    .attr('class', 'c-feat')\r\n                    .attr('d', geoPath)\r\n                    .attr('stroke', 'white')\r\n                    .attr('stroke-width', 0.7)\r\n                    .attr('fill', d => {\r\n                        const id = +d.id;\r\n                        if (id === franceISO) return '#4a90c8';\r\n                        if (partnerISOs.has(id)) return '#7ab1d3';\r\n                        return '#e9e4dd';\r\n                    })\r\n                    .attr('opacity', 0.95);\r\n\r\n                if (animate) {\r\n                    mapSvg.selectAll('path.c-feat').attr('opacity', 0).transition().duration(500).delay((d, i) => i * 4).ease(d3.easeCubicOut).attr('opacity', 0.95);\r\n                }\r\n            } else {\r\n                mapSvg.append('rect').attr('x', 0).attr('y', 0).attr('width', w).attr('height', h).attr('fill', '#f0eee9');\r\n            }\r\n\r\n            \/\/ \u00c9tiquettes pays \u2014 France toujours visible + tous les pays partenaires li\u00e9s\r\n            const labelOverrides = {\r\n                FR: { label: 'FRANCE', lon: 2.5, lat: 47.3, big: true },\r\n                BE: { label: 'BELGIQUE', small: true },\r\n                ES: { label: 'ESPAGNE', lon: -3.7, lat: 40.0, big: true },\r\n                PT: { label: 'PORTUGAL', small: true },\r\n                IT: { label: 'ITALIE' },\r\n                IE: { label: 'IRLANDE', lon: -8.0, lat: 53.4, small: true },\r\n                GR: { label: 'GR\u00c8CE', lon: 21.8, lat: 39.0 },\r\n                TR: { label: 'TURQUIE', big: true },\r\n                RO: { label: 'ROUMANIE', big: true },\r\n                BG: { label: 'BULGARIE' },\r\n                PL: { label: 'POLOGNE', big: true },\r\n                DK: { label: 'DANEMARK', lon: 10.0, lat: 56.0, small: true },\r\n                LU: { label: 'LUXEMBOURG', lon: 6.13, lat: 49.8, small: true },\r\n                LV: { label: 'LETTONIE', small: true },\r\n                SE: { label: 'SU\u00c8DE' },\r\n                CZ: { label: 'TCH\u00c9QUIE', small: true },\r\n                CY: { label: 'CHYPRE', small: true },\r\n                HU: { label: 'HONGRIE', small: true },\r\n                DE: { label: 'ALLEMAGNE', big: true },\r\n                LT: { label: 'LITUANIE', small: true },\r\n                NO: { label: 'NORV\u00c8GE', small: true },\r\n                RS: { label: 'SERBIE', small: true },\r\n                MT: { label: 'MALTE', small: true },\r\n                EE: { label: 'ESTONIE', small: true },\r\n                FI: { label: 'FINLANDE' },\r\n                NL: { label: 'PAYS-BAS', small: true }\r\n            };\r\n\r\n            const labelsToShow = new Set();\r\n            labelsToShow.add('FR');\r\n            associations.forEach(a => a.countries.forEach(c => labelsToShow.add(c)));\r\n            showcaseExtra.forEach(c => labelsToShow.add(c));\r\n\r\n            const labelLayer = mapSvg.append('g').attr('class', 'country-labels');\r\n            labelsToShow.forEach(code => {\r\n                const meta = labelOverrides[code];\r\n                if (!meta) return;\r\n                const info = code === 'FR' ? { lon: meta.lon, lat: meta.lat } : countryInfo[code];\r\n                if (!info) return;\r\n                const lon = meta.lon != null ? meta.lon : info.lon;\r\n                const lat = meta.lat != null ? meta.lat : info.lat;\r\n                const p = projection([lon, lat]);\r\n                if (!p) return;\r\n                const t = labelLayer.append('text').attr('class', 'shl-country-text').attr('x', p[0]).attr('y', p[1]).attr('text-anchor', 'middle').text(meta.label);\r\n                if (meta.big) t.style('font-size', '15px').style('font-weight', '900');\r\n                if (meta.small) t.style('font-size', '11px').style('font-weight', '800').style('stroke-width', '2.4px');\r\n                if (animate) t.style('opacity', 0).transition().duration(400).delay(700).style('opacity', 1);\r\n            });\r\n\r\n            \/\/ Pr\u00e9-calcule les destinations (pays partenaires)\r\n            associations.forEach(a => {\r\n                a.countries.forEach(code => {\r\n                    if (!destinationByCode.has(code) && countryInfo[code]) {\r\n                        destinationByCode.set(code, projection([countryInfo[code].lon, countryInfo[code].lat]));\r\n                    }\r\n                });\r\n            });\r\n\r\n            \/\/ Regroupe par (territoire, pays) pour offset les lignes\r\n            const perOriginCountry = new Map();\r\n            associations.forEach(a => {\r\n                a.countries.forEach(code => {\r\n                    const key = `${a.territoire}|${code}`;\r\n                    if (!perOriginCountry.has(key)) perOriginCountry.set(key, []);\r\n                    perOriginCountry.get(key).push(a.id);\r\n                });\r\n            });\r\n\r\n            \/\/ Point de passage g\u00e9ographique pour chaque pays \u2014 courbe esth\u00e9tique\r\n            const viaByCountry = {\r\n                BE: { lon: 5.2,  lat: 47.5 },\r\n                ES: { lon: 0.5,  lat: 41.5 },\r\n                IT: { lon: 9.5,  lat: 43.8 },\r\n                PT: { lon: -2.0, lat: 37.5 },\r\n                GR: { lon: 16.5, lat: 36.0 },\r\n                IE: { lon: -11.0,lat: 49.5 },\r\n                RO: { lon: 17.5, lat: 47.0 },\r\n                TR: { lon: 22.0, lat: 33.5 },\r\n                BG: { lon: 19.0, lat: 41.5 },\r\n                PL: { lon: 14.5, lat: 50.5 },\r\n                DK: { lon: 8.5,  lat: 52.0 },\r\n                LU: { lon: 5.5,  lat: 48.5 },\r\n                LV: { lon: 18.0, lat: 55.0 },\r\n                SE: { lon: 11.0, lat: 56.0 },\r\n                CZ: { lon: 12.0, lat: 49.5 },\r\n                CY: { lon: 28.0, lat: 34.0 },\r\n                HU: { lon: 16.0, lat: 47.5 },\r\n                DE: { lon: 8.0,  lat: 50.0 },\r\n                LT: { lon: 19.0, lat: 54.5 },\r\n                NO: { lon: 7.5,  lat: 58.0 },\r\n                RS: { lon: 18.5, lat: 45.0 },\r\n                MT: { lon: 14.5, lat: 37.0 },\r\n                EE: { lon: 22.0, lat: 57.0 },\r\n                FI: { lon: 18.0, lat: 55.0 },\r\n                NL: { lon: 4.5,  lat: 50.0 }\r\n            };\r\n\r\n            const flowGroup = mapSvg.append('g').attr('class', 'flows');\r\n            let pathCount = 0;\r\n            associations.forEach((asso, ai) => {\r\n                const list = [];\r\n                const origin = hubPts[asso.territoire] || hubPts[defaultHubKey];\r\n                if (!origin) return;\r\n                asso.countries.forEach(code => {\r\n                    const dest = destinationByCode.get(code);\r\n                    if (!dest) return;\r\n                    const via = viaByCountry[code];\r\n                    let cx, cy;\r\n                    if (via) {\r\n                        const viaPt = projection([via.lon, via.lat]);\r\n                        cx = viaPt[0]; cy = viaPt[1];\r\n                    } else {\r\n                        cx = (origin[0] + dest[0]) \/ 2;\r\n                        cy = (origin[1] + dest[1]) \/ 2 - 30;\r\n                    }\r\n                    const dx = dest[0] - origin[0], dy = dest[1] - origin[1];\r\n                    const dist = Math.max(1, Math.hypot(dx, dy));\r\n                    const nx = -dy \/ dist, ny = dx \/ dist;\r\n                    const peers = perOriginCountry.get(`${asso.territoire}|${code}`) || [asso.id];\r\n                    const peerIdx = peers.indexOf(asso.id);\r\n                    const peerCount = peers.length;\r\n                    const spread = peerCount > 1 ? 14 : 0;\r\n                    const k = (peerIdx - (peerCount - 1) \/ 2) * spread;\r\n                    cx += nx * k;\r\n                    cy += ny * k;\r\n                    const curve = `M${origin[0]},${origin[1]} Q${cx},${cy} ${dest[0]},${dest[1]}`;\r\n                    const sw = 1.6;\r\n                    const flow = flowGroup.append('path')\r\n                        .attr('d', curve)\r\n                        .attr('fill', 'none')\r\n                        .attr('stroke', asso.color)\r\n                        .attr('stroke-width', sw)\r\n                        .attr('stroke-opacity', 0.65)\r\n                        .attr('stroke-linecap', 'round')\r\n                        .attr('data-w', sw)\r\n                        .style('cursor', 'pointer')\r\n                        .on('mouseenter', () => focusAsso(asso))\r\n                        .on('mouseleave', resetMapFocus)\r\n                        .on('click', () => showAssoDetail(asso));\r\n                    if (animate) {\r\n                        const totalLen = flow.node().getTotalLength();\r\n                        flow.attr('stroke-dasharray', `${totalLen} ${totalLen}`).attr('stroke-dashoffset', totalLen)\r\n                            .transition().duration(900).delay(300 + pathCount * 18).ease(d3.easeCubicInOut)\r\n                            .attr('stroke-dashoffset', 0)\r\n                            .on('end', function(){ d3.select(this).attr('stroke-dasharray', null).attr('stroke-dashoffset', null); });\r\n                    }\r\n                    list.push(flow);\r\n                    pathCount++;\r\n                });\r\n                flows.set(asso.id, list);\r\n            });\r\n\r\n            \/\/ Pins des hubs (uniquement ceux utilis\u00e9s par au moins une asso)\r\n            const usedHubs = new Set(associations.map(a => a.territoire));\r\n            Object.keys(territoryHubs).forEach(key => {\r\n                if (!usedHubs.has(key)) return;\r\n                const pt = hubPts[key];\r\n                if (!pt) return;\r\n                const hub = territoryHubs[key];\r\n                const isPrimary = !!hub.primary;\r\n                const pinGroup = mapSvg.append('g').attr('class', 'hub-pin');\r\n                const bindHubFocus = selection => {\r\n                    selection\r\n                        .style('cursor', 'pointer')\r\n                        .on('mouseenter', () => focusTerritory(key, pt))\r\n                        .on('mouseleave', resetMapFocus)\r\n                        .on('click', event => {\r\n                            if (event) event.stopPropagation();\r\n                            selectedAsso = null;\r\n                            selectedTerritory = selectedTerritory === key ? null : key;\r\n                            closeDetailPanel();\r\n                            if (selectedTerritory) focusTerritory(key, pt);\r\n                            else resetMapFocus();\r\n                        });\r\n                };\r\n                pinGroup.append('circle')\r\n                    .attr('cx', pt[0]).attr('cy', pt[1])\r\n                    .attr('r', isPrimary ? 9 : 6)\r\n                    .attr('fill', 'white')\r\n                    .attr('stroke', '#24476d').attr('stroke-width', 2);\r\n                pinGroup.append('circle')\r\n                    .attr('cx', pt[0]).attr('cy', pt[1])\r\n                    .attr('r', isPrimary ? 3 : 2)\r\n                    .attr('fill', '#24476d');\r\n                const labelOffsetX = 12, labelOffsetY = isPrimary ? 10 : 4;\r\n                const mlabel = mapSvg.append('g').attr('transform', `translate(${pt[0] + labelOffsetX}, ${pt[1] + labelOffsetY})`);\r\n                if (isPrimary) {\r\n                    mlabel.append('text').attr('class', 'shl-marseille-label').attr('x', 0).attr('y', 0).text('M\u00e9tropole');\r\n                    mlabel.append('text').attr('class', 'shl-marseille-label').attr('x', 0).attr('y', 15).text('Aix Marseille');\r\n                } else {\r\n                    mlabel.append('text').attr('class', 'shl-marseille-label').attr('x', 0).attr('y', 0).style('font-size', '11px').text(hub.name);\r\n                }\r\n                bindHubFocus(pinGroup);\r\n                bindHubFocus(mlabel);\r\n                if (animate) {\r\n                    pinGroup.style('opacity', 0).transition().duration(450).delay(180).style('opacity', 1);\r\n                    mlabel.style('opacity', 0).transition().duration(450).delay(280).style('opacity', 1);\r\n                }\r\n            });\r\n\r\n            if (selectedAsso) focusAsso(selectedAsso);\r\n            else if (selectedTerritory) focusTerritory(selectedTerritory, destinationByCode.get('__hub_' + selectedTerritory));\r\n\r\n            \/\/ SHL watermark (style image fournie)\r\n            mapSvg.append('text')\r\n                .attr('x', w - 24).attr('y', h - 24)\r\n                .attr('text-anchor', 'end')\r\n                .attr('font-family', 'Manrope, sans-serif')\r\n                .attr('font-weight', '900')\r\n                .attr('font-size', '28px')\r\n                .attr('fill', '#1a2050')\r\n                .attr('opacity', 0.85)\r\n                .text('SHL');\r\n        }\r\n\r\n        function fetchWorldAtlas() {\r\n            const urls = [\r\n                'https:\/\/cdn.jsdelivr.net\/npm\/world-atlas@2\/countries-110m.json',\r\n                'https:\/\/unpkg.com\/world-atlas@2\/countries-110m.json'\r\n            ];\r\n            return urls.reduce((promise, url) => {\r\n                return promise.catch(() => fetch(url, { cache: 'force-cache' }).then(r => {\r\n                    if (!r.ok) throw new Error('HTTP ' + r.status);\r\n                    return r.json();\r\n                }));\r\n            }, Promise.reject());\r\n        }\r\n\r\n        \/\/ Petite banni\u00e8re de statut CSV (visible en haut \u00e0 droite de la carte)\r\n        function showStatus(msg, kind) {\r\n            let el = document.getElementById('shl-map-status');\r\n            if (!el) {\r\n                el = document.createElement('div');\r\n                el.id = 'shl-map-status';\r\n                el.style.cssText = 'position:absolute;top:1.1rem;left:1.1rem;z-index:30;padding:0.55rem 0.8rem;border-radius:10px;font-family:Manrope,sans-serif;font-size:0.74rem;font-weight:700;box-shadow:0 6px 18px rgba(36,71,109,0.12);pointer-events:auto;max-width:60%;line-height:1.35;';\r\n                mapEl.appendChild(el);\r\n            }\r\n            const colors = {\r\n                info:    { bg: 'rgba(74,144,200,0.12)', border: '1px solid rgba(74,144,200,0.4)', fg: '#24476d' },\r\n                ok:      { bg: 'rgba(46,139,87,0.12)',  border: '1px solid rgba(46,139,87,0.45)', fg: '#1f5e3a' },\r\n                error:   { bg: 'rgba(198,58,58,0.10)',  border: '1px solid rgba(198,58,58,0.5)',  fg: '#8a2222' }\r\n            }[kind] || {};\r\n            el.style.background = colors.bg || '';\r\n            el.style.border = colors.border || '';\r\n            el.style.color = colors.fg || '';\r\n            el.innerHTML = msg;\r\n        }\r\n        function hideStatus() { const el = document.getElementById('shl-map-status'); if (el) el.remove(); }\r\n\r\n        \/\/ Filtre par type de projet (chips dans la sidebar).\r\n        const filterEl = document.getElementById('carte-filter');\r\n        if (filterEl) {\r\n            filterEl.addEventListener('click', e => {\r\n                const btn = e.target.closest('.carte-filter-chip');\r\n                if (btn && btn.dataset && btn.dataset.cat) setFilter(btn.dataset.cat);\r\n            });\r\n        }\r\n\r\n        \/\/ 1) Donn\u00e9es : on initialise imm\u00e9diatement avec le fallback\u2026\r\n        setAssociations(fallbackToAssociations());\r\n        \/\/ \u2026puis on tente le CSV en parall\u00e8le.\r\n        if (partnersCsvUrl) {\r\n            console.log('[Carte Partenaire] Chargement CSV depuis :', partnersCsvUrl);\r\n            showStatus('Chargement des donn\u00e9es CSV\u2026', 'info');\r\n            loadRowsFromSheet(partnersCsvUrl)\r\n                .then(rows => {\r\n                    console.log('[Carte Partenaire] CSV charg\u00e9,', rows ? rows.length : 0, 'lignes brutes.');\r\n                    if (!rows || !rows.length) {\r\n                        showStatus('CSV charg\u00e9 mais vide. V\u00e9rifie la feuille et le partage lecteur.', 'error');\r\n                        return;\r\n                    }\r\n                    const parsed = rowsToAssociations(rows);\r\n                    console.log('[Carte Partenaire]', parsed.length, 'associations agr\u00e9g\u00e9es.');\r\n                    if (!parsed.length) {\r\n                        showStatus(`CSV charg\u00e9 (${rows.length} lignes) mais aucune asso n'a pu \u00eatre interpr\u00e9t\u00e9e.<br><small>V\u00e9rifie les colonnes : Territoire, Structure, Pays \/ partenaires.<\/small>`, 'error');\r\n                        return;\r\n                    }\r\n                    setAssociations(parsed);\r\n                    renderMap(!europeFeatures || !europeFeatures.length);\r\n                    showStatus(`${parsed.length} associations charg\u00e9es depuis le CSV.`, 'ok');\r\n                    setTimeout(hideStatus, 2500);\r\n                })\r\n                .catch(err => {\r\n                    console.error('[Carte Partenaire] Erreur CSV :', err);\r\n                    showStatus(`Impossible de charger le CSV. <br><small>${(err && err.message) || err}<\/small><br><small>V\u00e9rifie que la feuille est partag\u00e9e en \u00ab Lecteur \u00b7 n'importe qui avec le lien \u00bb.<\/small>`, 'error');\r\n                });\r\n        }\r\n\r\n        \/\/ 2) Fond de carte\r\n        fetchWorldAtlas().then(world => {\r\n            const countries = topojson.feature(world, world.objects.countries);\r\n            europeFeatures = countries.features.filter(isEurope);\r\n            renderMap(false);\r\n        }).catch(err => {\r\n            console.warn('Fond de carte indisponible, affichage simplifi\u00e9.', err);\r\n            renderFallbackMap();\r\n        });\r\n\r\n        let resizeRAF = null;\r\n        const ro = 'ResizeObserver' in window ? new ResizeObserver(() => {\r\n            if (resizeRAF) cancelAnimationFrame(resizeRAF);\r\n            resizeRAF = requestAnimationFrame(() => { resizeRAF = null; renderMap(!europeFeatures || !europeFeatures.length); });\r\n        }) : null;\r\n        if (ro) ro.observe(mapEl);\r\n        window.addEventListener('resize', () => {\r\n            if (resizeRAF) cancelAnimationFrame(resizeRAF);\r\n            resizeRAF = requestAnimationFrame(() => { resizeRAF = null; renderMap(!europeFeatures || !europeFeatures.length); });\r\n        });\r\n    }\r\n})();\r\n<\/script>","protected":false},"excerpt":{"rendered":"","protected":false},"author":64,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_et_pb_use_builder":"off","_et_pb_old_content":"","_et_gb_content_width":"","footnotes":""},"class_list":["post-23813670","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.9 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>carte-interactive - Social Hackers Lab<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/socialhackerslab.com\/en\/carte-interactive\/\" \/>\n<meta property=\"og:locale\" content=\"en_GB\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"carte-interactive - Social Hackers Lab\" \/>\n<meta property=\"og:url\" content=\"https:\/\/socialhackerslab.com\/en\/carte-interactive\/\" \/>\n<meta property=\"og:site_name\" content=\"Social Hackers Lab\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/profile.php?id=61561806211570\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-04T15:04:31+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/socialhackerslab.com\/wp-content\/uploads\/2024\/10\/Social-Hacker-Lab-1.png\" \/>\n\t<meta property=\"og:image:width\" content=\"250\" \/>\n\t<meta property=\"og:image:height\" content=\"140\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/carte-interactive\\\/\",\"url\":\"https:\\\/\\\/socialhackerslab.com\\\/carte-interactive\\\/\",\"name\":\"carte-interactive - Social Hackers Lab\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/#website\"},\"datePublished\":\"2026-05-20T17:24:15+00:00\",\"dateModified\":\"2026-06-04T15:04:31+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/carte-interactive\\\/#breadcrumb\"},\"inLanguage\":\"en-GB\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/socialhackerslab.com\\\/carte-interactive\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/carte-interactive\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Accueil\",\"item\":\"https:\\\/\\\/socialhackerslab.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"carte-interactive\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/#website\",\"url\":\"https:\\\/\\\/socialhackerslab.com\\\/\",\"name\":\"SocialHackersLab\",\"description\":\"Hacking Social pour une soci\u00e9t\u00e9 \u00e9galitaire\",\"publisher\":{\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/socialhackerslab.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-GB\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/#organization\",\"name\":\"Social Hackers Lab\",\"url\":\"https:\\\/\\\/socialhackerslab.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-GB\",\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/socialhackerslab.com\\\/wp-content\\\/uploads\\\/2024\\\/10\\\/BW-SHL.png\",\"contentUrl\":\"https:\\\/\\\/socialhackerslab.com\\\/wp-content\\\/uploads\\\/2024\\\/10\\\/BW-SHL.png\",\"width\":3431,\"height\":3431,\"caption\":\"Social Hackers Lab\"},\"image\":{\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/profile.php?id=61561806211570\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/socialhackerslab\\\/\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"carte-interactive - Social Hackers Lab","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/socialhackerslab.com\/en\/carte-interactive\/","og_locale":"en_GB","og_type":"article","og_title":"carte-interactive - Social Hackers Lab","og_url":"https:\/\/socialhackerslab.com\/en\/carte-interactive\/","og_site_name":"Social Hackers Lab","article_publisher":"https:\/\/www.facebook.com\/profile.php?id=61561806211570","article_modified_time":"2026-06-04T15:04:31+00:00","og_image":[{"width":250,"height":140,"url":"https:\/\/socialhackerslab.com\/wp-content\/uploads\/2024\/10\/Social-Hacker-Lab-1.png","type":"image\/png"}],"twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/socialhackerslab.com\/carte-interactive\/","url":"https:\/\/socialhackerslab.com\/carte-interactive\/","name":"carte-interactive - Social Hackers Lab","isPartOf":{"@id":"https:\/\/socialhackerslab.com\/#website"},"datePublished":"2026-05-20T17:24:15+00:00","dateModified":"2026-06-04T15:04:31+00:00","breadcrumb":{"@id":"https:\/\/socialhackerslab.com\/carte-interactive\/#breadcrumb"},"inLanguage":"en-GB","potentialAction":[{"@type":"ReadAction","target":["https:\/\/socialhackerslab.com\/carte-interactive\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/socialhackerslab.com\/carte-interactive\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Accueil","item":"https:\/\/socialhackerslab.com\/"},{"@type":"ListItem","position":2,"name":"carte-interactive"}]},{"@type":"WebSite","@id":"https:\/\/socialhackerslab.com\/#website","url":"https:\/\/socialhackerslab.com\/","name":"SocialHackersLab","description":"Hacking Social pour une soci\u00e9t\u00e9 \u00e9galitaire","publisher":{"@id":"https:\/\/socialhackerslab.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/socialhackerslab.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-GB"},{"@type":"Organization","@id":"https:\/\/socialhackerslab.com\/#organization","name":"Social Hackers Lab","url":"https:\/\/socialhackerslab.com\/","logo":{"@type":"ImageObject","inLanguage":"en-GB","@id":"https:\/\/socialhackerslab.com\/#\/schema\/logo\/image\/","url":"https:\/\/socialhackerslab.com\/wp-content\/uploads\/2024\/10\/BW-SHL.png","contentUrl":"https:\/\/socialhackerslab.com\/wp-content\/uploads\/2024\/10\/BW-SHL.png","width":3431,"height":3431,"caption":"Social Hackers Lab"},"image":{"@id":"https:\/\/socialhackerslab.com\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/profile.php?id=61561806211570","https:\/\/www.linkedin.com\/company\/socialhackerslab\/"]}]}},"_links":{"self":[{"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/pages\/23813670","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/users\/64"}],"replies":[{"embeddable":true,"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/comments?post=23813670"}],"version-history":[{"count":2,"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/pages\/23813670\/revisions"}],"predecessor-version":[{"id":23813678,"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/pages\/23813670\/revisions\/23813678"}],"wp:attachment":[{"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/media?parent=23813670"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}