{"id":23813681,"date":"2026-05-26T12:54:01","date_gmt":"2026-05-26T12:54:01","guid":{"rendered":"https:\/\/socialhackerslab.com\/?page_id=23813681"},"modified":"2026-05-26T12:54:04","modified_gmt":"2026-05-26T12:54:04","slug":"outil-de-cartographie-des-projets","status":"publish","type":"page","link":"https:\/\/socialhackerslab.com\/en\/outil-de-cartographie-des-projets\/","title":{"rendered":"outil-de-cartographie-des-projets"},"content":{"rendered":"<script src=\"https:\/\/d3js.org\/d3.v7.min.js\"><\/script>\r\n\r\n<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\/* WPCode Lite \/ WordPress plein \u00e9cran *\/\r\nhtml:has(#shl-roue-wp), body:has(#shl-roue-wp){\r\n    margin: 0 !important;\r\n    padding: 0 !important;\r\n    overflow: hidden !important;\r\n}\r\n\/* \u00c9l\u00e9ments natifs du th\u00e8me WordPress (hors du container) \u2192 on les masque\r\n   uniquement sur les pages qui h\u00e9bergent la roue, via :has(). *\/\r\nbody:has(#shl-roue-wp) .entry-title,\r\nbody:has(#shl-roue-wp) .page-title,\r\nbody:has(#shl-roue-wp) h1.elementor-heading-title,\r\nbody:has(#shl-roue-wp) .nv-page-title,\r\nbody:has(#shl-roue-wp) .site-header,\r\nbody:has(#shl-roue-wp) .site-footer,\r\nbody:has(#shl-roue-wp) header.site-header,\r\nbody:has(#shl-roue-wp) footer.site-footer{\r\n    display: none !important;\r\n}\r\n#shl-roue-wp {\r\n    --bg-color: #fbfaf8;\r\n    --text-color: #24476d;\r\n    --muted-text: #6d7580;\r\n    --sidebar-bg: rgba(255, 251, 247, 0.92);\r\n    --border-color: #e7dfd9;\r\n    --accent-color: #6d4f84;\r\n    --font-family: 'Manrope', sans-serif;\r\n    --chip-bg: rgba(255, 255, 255, 0.84);\r\n    --chip-shadow: 0 10px 30px rgba(36, 71, 109, 0.08);\r\n\r\n    position: fixed !important;\r\n    inset: 0 !important;\r\n    width: 100vw !important;\r\n    height: 100vh !important;\r\n    z-index: 9999999 !important;\r\n    font-family: var(--font-family) !important;\r\n    background:\r\n        radial-gradient(circle at top, rgba(255, 255, 255, 0.95), rgba(251, 250, 248, 0.98) 44%, rgba(244, 240, 235, 0.98)),\r\n        linear-gradient(135deg, rgba(109, 79, 132, 0.04), rgba(46, 128, 98, 0.03)) !important;\r\n    background-color: var(--bg-color) !important;\r\n    color: var(--text-color) !important;\r\n    overflow: hidden !important;\r\n    display: flex !important;\r\n    flex-direction: column !important;\r\n}\r\n#shl-roue-wp * {\r\n    box-sizing: border-box;\r\n}\r\n#shl-roue-wp #app-container {\r\n    height: 100%;\r\n    width: 100%;\r\n}\r\n\r\n#shl-roue-wp #app-container{\r\n            display: flex;\r\n            flex-direction: column;\r\n            height: 100%;\r\n            width: 100%;\r\n            max-width: none;\r\n            margin: 0 auto;\r\n            position: relative;\r\n        }\r\n\r\n        #shl-roue-wp .header{\r\n            position: absolute;\r\n            top: 1.05rem;\r\n            left: 1.1rem;\r\n            right: auto;\r\n            width: fit-content;\r\n            max-width: calc(100vw - 2.2rem);\r\n            display: block;\r\n            padding: 0;\r\n            background: transparent;\r\n            border-bottom: none;\r\n            z-index: 10;\r\n        }\r\n\r\n        #shl-roue-wp .map-title{\r\n            max-width: min(360px, calc(100vw - 2.2rem));\r\n            margin-bottom: 1.25rem;\r\n            text-align: left;\r\n            color: var(--text-color);\r\n            pointer-events: none;\r\n        }\r\n\r\n        #shl-roue-wp .map-title-eyebrow{\r\n            display: block;\r\n            margin-bottom: 0.18rem;\r\n            color: var(--muted-text);\r\n            font-size: 0.72rem;\r\n            font-weight: 850;\r\n            letter-spacing: 0.09em;\r\n            line-height: 1.15;\r\n            text-transform: uppercase;\r\n        }\r\n\r\n        #shl-roue-wp .map-title-main{\r\n            display: block;\r\n            color: #13293f;\r\n            font-size: clamp(1.45rem, 2.1vw, 2.15rem);\r\n            font-weight: 850;\r\n            line-height: 1.04;\r\n            letter-spacing: 0;\r\n        }\r\n\r\n        #shl-roue-wp .toolbar{\r\n            display: flex;\r\n            flex-direction: column;\r\n            align-items: flex-start;\r\n            gap: 0.55rem;\r\n            width: fit-content;\r\n            pointer-events: auto;\r\n        }\r\n\r\n        #shl-roue-wp .filter-group{\r\n            display: inline-flex;\r\n            flex-direction: column;\r\n            gap: 0.35rem;\r\n            padding: 0.55rem 0.6rem 0.65rem;\r\n            border-radius: 18px;\r\n            background: var(--chip-bg);\r\n            box-shadow: var(--chip-shadow);\r\n            border: 1px solid rgba(231, 223, 217, 0.9);\r\n            min-width: 200px;\r\n            opacity: 1;\r\n            transform: translateY(0);\r\n            transition: opacity 0.35s ease, transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);\r\n        }\r\n\r\n        #shl-roue-wp .filter-group[hidden]{\r\n            display: none;\r\n        }\r\n\r\n        #shl-roue-wp .filter-group.entering{\r\n            opacity: 0;\r\n            transform: translateY(-6px);\r\n        }\r\n\r\n        #shl-roue-wp .filter-label{\r\n            font-size: 0.7rem;\r\n            text-transform: uppercase;\r\n            letter-spacing: 0.06em;\r\n            color: var(--muted-text);\r\n            font-weight: 700;\r\n            padding: 0.2rem 0.45rem 0.1rem;\r\n        }\r\n\r\n        #shl-roue-wp .filter-btn{\r\n            border: 1px solid rgba(36, 71, 109, 0.14);\r\n            border-radius: 12px;\r\n            padding: 0.55rem 0.7rem;\r\n            background: rgba(255, 255, 255, 0.55);\r\n            color: var(--text-color);\r\n            font: inherit;\r\n            font-size: 0.78rem;\r\n            font-weight: 600;\r\n            cursor: pointer;\r\n            text-align: left;\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 0.5rem;\r\n            transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.15s ease;\r\n        }\r\n\r\n        #shl-roue-wp .filter-btn:hover{\r\n            transform: translateY(-1px);\r\n            background: rgba(255, 255, 255, 0.92);\r\n        }\r\n\r\n        #shl-roue-wp .filter-dot{\r\n            width: 10px;\r\n            height: 10px;\r\n            border-radius: 999px;\r\n            background: transparent;\r\n            border: 1.5px solid var(--filter-color, currentColor);\r\n            transition: background-color 0.2s ease;\r\n            flex: none;\r\n        }\r\n\r\n        #shl-roue-wp .filter-btn.active{\r\n            background: rgba(255, 255, 255, 0.96);\r\n            border-color: var(--filter-color, currentColor);\r\n            color: var(--filter-color, var(--text-color));\r\n        }\r\n\r\n        #shl-roue-wp .filter-btn.active .filter-dot{\r\n            background: var(--filter-color, currentColor);\r\n        }\r\n\r\n        #shl-roue-wp .toggle-group{\r\n            display: inline-flex;\r\n            flex-direction: row;\r\n            flex-wrap: nowrap;\r\n            gap: 0.5rem;\r\n            padding: 0.4rem;\r\n            border-radius: 22px;\r\n            background: var(--chip-bg);\r\n            box-shadow: var(--chip-shadow);\r\n            border: 1px solid rgba(231, 223, 217, 0.9);\r\n        }\r\n\r\n        #shl-roue-wp .zone-select-group{\r\n            display: inline-flex;\r\n            align-items: center;\r\n            gap: 0.55rem;\r\n            padding: 0.45rem 0.55rem 0.45rem 0.75rem;\r\n            border-radius: 18px;\r\n            background: var(--chip-bg);\r\n            box-shadow: var(--chip-shadow);\r\n            border: 1px solid rgba(231, 223, 217, 0.9);\r\n            min-width: 220px;\r\n            width: fit-content;\r\n            max-width: calc(100vw - 2rem);\r\n        }\r\n\r\n        #shl-roue-wp .zone-select-group[hidden]{\r\n            display: none;\r\n        }\r\n\r\n        #shl-roue-wp .zone-select-label{\r\n            font-size: 0.72rem;\r\n            text-transform: uppercase;\r\n            letter-spacing: 0.06em;\r\n            color: var(--muted-text);\r\n            font-weight: 800;\r\n            white-space: nowrap;\r\n        }\r\n\r\n        #shl-roue-wp .zone-select{\r\n            width: auto;\r\n            min-width: 130px;\r\n            max-width: 300px;\r\n            border: 1px solid rgba(36, 71, 109, 0.16);\r\n            border-radius: 999px;\r\n            padding: 0.55rem 2rem 0.55rem 0.8rem;\r\n            background: rgba(255, 255, 255, 0.86);\r\n            color: var(--text-color);\r\n            font: inherit;\r\n            font-size: 0.82rem;\r\n            font-weight: 750;\r\n            cursor: pointer;\r\n            outline: none;\r\n            transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;\r\n        }\r\n\r\n        #shl-roue-wp .zone-select:hover, #shl-roue-wp .zone-select:focus{\r\n            background: white;\r\n            border-color: rgba(36, 71, 109, 0.34);\r\n            box-shadow: 0 8px 18px rgba(36, 71, 109, 0.1);\r\n        }\r\n\r\n        #shl-roue-wp .toggle-btn{\r\n            border: none;\r\n            border-radius: 999px;\r\n            padding: 0.78rem 1.05rem;\r\n            background: transparent;\r\n            color: var(--text-color);\r\n            font: inherit;\r\n            font-size: 0.9rem;\r\n            font-weight: 700;\r\n            cursor: pointer;\r\n            transition: transform 0.2s ease, background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;\r\n        }\r\n\r\n        #shl-roue-wp .toggle-btn:hover{\r\n            transform: translateY(-1px);\r\n            background: rgba(255, 255, 255, 0.68);\r\n        }\r\n\r\n        #shl-roue-wp .toggle-btn.active{\r\n            color: white;\r\n            box-shadow: 0 12px 24px rgba(36, 71, 109, 0.18);\r\n        }\r\n\r\n        #shl-roue-wp .toggle-btn[data-mode=\"none\"].active{ background: #24476d; }\r\n        #shl-roue-wp .toggle-btn[data-mode=\"category\"].active{ background: #d39836; }\r\n\r\n        #shl-roue-wp .main-content{ display: flex; flex: 1; position: relative; overflow: hidden; }\r\n\r\n        #shl-roue-wp #chart-container{\r\n            flex: 1;\r\n            display: flex;\r\n            justify-content: center;\r\n            align-items: center;\r\n            position: relative;\r\n            padding: 1.5rem 1.25rem 1.25rem;\r\n            transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);\r\n            contain: layout style;\r\n        }\r\n        #shl-roue-wp #chart-container svg{\r\n            width: 100%;\r\n            height: 100%;\r\n            display: block;\r\n            touch-action: manipulation;\r\n        }\r\n\r\n        \/* v4 \u2014 L\u00e9gende verticale des programmes europ\u00e9ens. Une fine colonne \u00e0\r\n           droite de la roue : pastille color\u00e9e + nom, empil\u00e9s verticalement,\r\n           regroup\u00e9s par cat\u00e9gorie. Appara\u00eet uniquement quand au moins une\r\n           cat\u00e9gorie de financement est active. *\/\r\n        #shl-roue-wp .funding-legend{\r\n            display: none;\r\n            flex: 0 0 clamp(200px, 18vw, 270px);\r\n            flex-direction: column;\r\n            align-self: center;\r\n            max-height: calc(100% - 2rem);\r\n            margin: 0 1.1rem 0 0.5rem;\r\n            padding: 0.5rem 0.5rem 0.5rem 0;\r\n            overflow-y: auto;\r\n            scrollbar-width: none;\r\n            -ms-overflow-style: none;\r\n        }\r\n        #shl-roue-wp .funding-legend::-webkit-scrollbar{ display: none; }\r\n        #shl-roue-wp .funding-legend.is-active{\r\n            display: flex;\r\n            animation: fundingLegendIn 0.34s cubic-bezier(0.22, 1, 0.36, 1);\r\n        }\r\n        @keyframes fundingLegendIn{\r\n            from { opacity: 0; transform: translateX(10px); }\r\n            to { opacity: 1; transform: translateX(0); }\r\n        }\r\n        #shl-roue-wp .funding-legend-cat{\r\n            margin-bottom: 1.15rem;\r\n        }\r\n        #shl-roue-wp .funding-legend-cat:last-child{ margin-bottom: 0; }\r\n        #shl-roue-wp .funding-legend-cat-title{\r\n            margin: 0 0 0.45rem;\r\n            font-size: 0.72rem;\r\n            font-weight: 850;\r\n            letter-spacing: 0.08em;\r\n            text-transform: uppercase;\r\n            color: var(--muted-text);\r\n        }\r\n        #shl-roue-wp .funding-legend-list{\r\n            list-style: none;\r\n            margin: 0;\r\n            padding: 0;\r\n            display: flex;\r\n            flex-direction: column;\r\n            gap: 0.42rem;\r\n        }\r\n        #shl-roue-wp .funding-legend-item{\r\n            display: flex;\r\n            align-items: baseline;\r\n            gap: 0.65rem;\r\n            font-size: 0.9rem;\r\n            font-weight: 650;\r\n            line-height: 1.3;\r\n            color: var(--text-color);\r\n            overflow-wrap: anywhere;\r\n        }\r\n        #shl-roue-wp .funding-legend-dot{\r\n            flex: none;\r\n            width: 10px;\r\n            height: 10px;\r\n            border-radius: 999px;\r\n            background: var(--dot-color, currentColor);\r\n            transform: translateY(2px);\r\n        }\r\n        @media (max-width: 900px){\r\n            #shl-roue-wp .main-content{ flex-direction: column; }\r\n            #shl-roue-wp .funding-legend{\r\n                flex: 0 0 auto;\r\n                align-self: stretch;\r\n                max-height: 36vh;\r\n                margin: 0 0.8rem 0.6rem;\r\n                padding: 0.7rem 0.9rem 0.9rem;\r\n            }\r\n            #shl-roue-wp .funding-legend-item{\r\n                font-size: 0.84rem;\r\n            }\r\n        }\r\n\r\n        #shl-roue-wp path.arc{\r\n            cursor: pointer;\r\n            transition: opacity 0.3s ease-out;\r\n            shape-rendering: geometricPrecision;\r\n            vector-effect: non-scaling-stroke;\r\n            stroke: rgba(255, 255, 255, 0.9);\r\n            stroke-width: 1.6px;\r\n        }\r\n        #shl-roue-wp path.arc:hover{ opacity: 0.85 !important; }\r\n\r\n        #shl-roue-wp path.funding-arc{\r\n            transition: opacity 0.3s ease-out;\r\n            stroke: rgba(255, 255, 255, 0.92);\r\n            stroke-width: 1.1px;\r\n            vector-effect: non-scaling-stroke;\r\n            cursor: help;\r\n        }\r\n\r\n        #shl-roue-wp .arcs, #shl-roue-wp .texts{\r\n            will-change: transform;\r\n        }\r\n\r\n        #shl-roue-wp text.label{\r\n            pointer-events: none;\r\n            font-family: var(--font-family);\r\n            font-weight: 700;\r\n            fill: #ffffff;\r\n            text-anchor: middle;\r\n            dominant-baseline: middle;\r\n            text-rendering: geometricPrecision;\r\n            paint-order: stroke fill;\r\n            stroke: rgba(24, 38, 52, 0.16);\r\n            stroke-width: 1.35px;\r\n            stroke-linejoin: round;\r\n            letter-spacing: 0.005em;\r\n            transition: opacity 0.5s ease, font-size 0.6s cubic-bezier(0.4, 0, 0.2, 1);\r\n        }\r\n\r\n        #shl-roue-wp text.label.domain-label{\r\n            font-weight: 740;\r\n            letter-spacing: 0;\r\n            stroke: rgba(15, 28, 40, 0.12);\r\n            stroke-width: 1.1px;\r\n        }\r\n\r\n        #shl-roue-wp text.domain-title-text{\r\n            pointer-events: none;\r\n            font-family: var(--font-family);\r\n            font-style: normal;\r\n            font-weight: 850;\r\n            letter-spacing: 0.01em;\r\n            fill: #ffffff;\r\n            text-anchor: middle;\r\n            dominant-baseline: middle;\r\n            text-rendering: geometricPrecision;\r\n            paint-order: stroke fill;\r\n            stroke: rgba(15, 28, 40, 0.22);\r\n            stroke-width: 1.05px;\r\n        }\r\n\r\n        #shl-roue-wp text.label.item-label{\r\n            font-weight: 700;\r\n            stroke-width: 1.5px;\r\n        }\r\n\r\n        #shl-roue-wp text.center-text{\r\n            pointer-events: none;\r\n            font-family: var(--font-family);\r\n            font-weight: 700;\r\n            fill: var(--text-color);\r\n            text-anchor: middle;\r\n            transition: opacity 0.5s ease;\r\n        }\r\n\r\n        #shl-roue-wp .center-control circle{\r\n            cursor: pointer;\r\n            stroke: rgba(36, 71, 109, 0.12);\r\n            stroke-width: 1.5px;\r\n            filter: drop-shadow(0 10px 20px rgba(36, 71, 109, 0.12));\r\n            transition: fill 0.2s ease, stroke 0.2s ease, filter 0.2s ease;\r\n        }\r\n\r\n        #shl-roue-wp .center-control{\r\n            outline: none;\r\n        }\r\n\r\n        #shl-roue-wp .center-control:hover circle{\r\n            fill: #f8fafc;\r\n            stroke: rgba(36, 71, 109, 0.3);\r\n            filter: drop-shadow(0 12px 24px rgba(36, 71, 109, 0.18));\r\n        }\r\n\r\n        #shl-roue-wp .center-control:focus circle{\r\n            stroke: rgba(36, 71, 109, 0.42);\r\n        }\r\n\r\n        #shl-roue-wp text.center-back-icon, #shl-roue-wp text.center-back-label{\r\n            pointer-events: none;\r\n            font-family: var(--font-family);\r\n            fill: var(--text-color);\r\n            text-anchor: middle;\r\n            dominant-baseline: middle;\r\n            opacity: 0;\r\n            transition: opacity 0.25s ease;\r\n        }\r\n\r\n        #shl-roue-wp text.center-back-icon{\r\n            font-size: 22px;\r\n            font-weight: 900;\r\n        }\r\n\r\n        #shl-roue-wp text.center-back-label{\r\n            font-size: 11px;\r\n            font-weight: 800;\r\n        }\r\n\r\n        #shl-roue-wp .tooltip{\r\n            position: absolute;\r\n            pointer-events: none;\r\n            background: rgba(15, 23, 42, 0.9);\r\n            color: white;\r\n            padding: 8px 12px;\r\n            border-radius: 6px;\r\n            font-size: 0.85rem;\r\n            font-weight: 600;\r\n            opacity: 0;\r\n            transition: opacity 0.2s;\r\n            box-shadow: 0 4px 10px rgba(0,0,0,0.2);\r\n            z-index: 100;\r\n            max-width: 250px;\r\n            text-align: center;\r\n        }\r\n\r\n        @media (prefers-reduced-motion: reduce) {\r\n            #shl-roue-wp *{\r\n                animation-duration: 0.01ms !important;\r\n                animation-iteration-count: 1 !important;\r\n                transition-duration: 0.01ms !important;\r\n                scroll-behavior: auto !important;\r\n            }\r\n        }\r\n\r\n\r\n        @media (prefers-reduced-motion: reduce) {\r\n            #shl-roue-wp path.arc, #shl-roue-wp text.label, #shl-roue-wp text.center-text, #shl-roue-wp .tooltip{\r\n                transition: none !important;\r\n                animation: none !important;\r\n            }\r\n        }\r\n\r\n            @media (max-width: 900px), (pointer: coarse) and (max-width: 1180px) {\r\n            #shl-roue-wp .header{\r\n                top: 0.65rem;\r\n                left: 0.65rem;\r\n                right: auto;\r\n            }\r\n            #shl-roue-wp .map-title{\r\n                max-width: min(330px, calc(100vw - 1.3rem));\r\n                margin-bottom: 0.8rem;\r\n            }\r\n            #shl-roue-wp .map-title-eyebrow{\r\n                font-size: 0.64rem;\r\n            }\r\n            #shl-roue-wp .map-title-main{\r\n                font-size: clamp(1.05rem, 5.2vw, 1.55rem);\r\n            }\r\n            #shl-roue-wp .toolbar{\r\n                width: 100%;\r\n                flex-direction: column;\r\n                align-items: stretch;\r\n                gap: 0.4rem;\r\n            }\r\n            #shl-roue-wp .toggle-group{\r\n                width: 100%;\r\n                max-width: 460px;\r\n                flex-direction: row;\r\n                border-radius: 18px;\r\n                gap: 0.35rem;\r\n                padding: 0.32rem;\r\n            }\r\n            #shl-roue-wp .zone-select-group{\r\n                width: fit-content;\r\n                max-width: calc(100vw - 1.3rem);\r\n                min-width: 0;\r\n                align-self: flex-start;\r\n            }\r\n            #shl-roue-wp .filter-group{\r\n                width: 100%;\r\n                max-width: 460px;\r\n                min-width: 0;\r\n                padding: 0.4rem 0.5rem 0.5rem;\r\n            }\r\n            #shl-roue-wp .filter-btn{\r\n                font-size: 0.74rem;\r\n                padding: 0.5rem 0.6rem;\r\n            }\r\n            #shl-roue-wp .toggle-btn{\r\n                flex: 1 1 0;\r\n                min-width: 0;\r\n                padding: 0.68rem 0.58rem;\r\n                font-size: 0.78rem;\r\n                white-space: normal;\r\n                line-height: 1.1;\r\n            }\r\n            #shl-roue-wp path.funding-arc{\r\n                stroke-width: 1.4px;\r\n            }\r\n            #shl-roue-wp .tooltip{\r\n                display: none;\r\n            }\r\n        }\r\n\r\n        @media (max-width: 520px) {\r\n            #shl-roue-wp .toggle-btn{\r\n                font-size: 0.72rem;\r\n                padding: 0.6rem 0.42rem;\r\n            }\r\n            #shl-roue-wp text.label{\r\n                stroke-width: 1px;\r\n            }\r\n            #shl-roue-wp text.label.item-label{\r\n                stroke-width: 1.15px;\r\n            }\r\n        }\r\n\r\n\r\n\/* Bouclier anti-th\u00e8me WordPress pour les textes SVG *\/\r\n#shl-roue-wp svg text, \r\n#shl-roue-wp svg tspan, \r\n#shl-roue-wp svg textPath {\r\n    font-family: 'Manrope', sans-serif !important;\r\n    text-shadow: none !important;\r\n    text-transform: none !important;\r\n    line-height: normal !important;\r\n}\r\n#shl-roue-wp text.label,\r\n#shl-roue-wp text.domain-title-text {\r\n    fill: #ffffff !important;\r\n    color: #ffffff !important;\r\n}\r\n#shl-roue-wp text.center-text, \r\n#shl-roue-wp text.center-back-icon, \r\n#shl-roue-wp text.center-back-label {\r\n    fill: #24476d !important;\r\n    color: #24476d !important;\r\n}\r\n\r\n#shl-roue-wp .header {\r\n    z-index: 2147483000 !important;\r\n    overflow: visible !important;\r\n    isolation: isolate !important;\r\n    pointer-events: none !important;\r\n    right: auto !important;\r\n    width: fit-content !important;\r\n    max-width: calc(100vw - 2.2rem) !important;\r\n}\r\n\r\n#shl-roue-wp .toolbar {\r\n    overflow: visible !important;\r\n    isolation: isolate !important;\r\n    pointer-events: auto !important;\r\n}\r\n\r\n\/* Bouclier anti-th\u00e8me : .main-content et .toolbar sont des noms de classe\r\n   tr\u00e8s courants dans les th\u00e8mes WordPress, on neutralise leurs paddings. *\/\r\n#shl-roue-wp .main-content,\r\n#shl-roue-wp .toolbar {\r\n    margin: 0 !important;\r\n    padding: 0 !important;\r\n}\r\n\r\n#shl-roue-wp .toggle-group,\r\n#shl-roue-wp .filter-group {\r\n    position: relative !important;\r\n    z-index: 12 !important;\r\n    pointer-events: auto !important;\r\n}\r\n\r\n<\/style>\r\n\r\n<div id=\"shl-roue-wp\">\r\n<div id=\"app-container\">\r\n<header class=\"header\">\r\n<div class=\"toolbar\">\r\n<div class=\"map-title\" aria-label=\"Cartographie des projets incub\u00e9s\">\r\n<span class=\"map-title-eyebrow\">Cartographie<\/span>\r\n<strong class=\"map-title-main\">Cartographie des projets incub\u00e9s<\/strong>\r\n<\/div>\r\n<div class=\"toggle-group\" id=\"funding-toggle\">\r\n<button aria-pressed=\"true\" class=\"toggle-btn active\" data-mode=\"none\" type=\"button\">Projets<\/button>\r\n<button aria-pressed=\"false\" class=\"toggle-btn\" data-mode=\"category\" type=\"button\">Financements<\/button>\r\n<\/div>\r\n<div class=\"filter-group\" hidden=\"\" id=\"funding-filters\">\r\n<span class=\"filter-label\">Cat\u00e9gories de financement<\/span>\r\n<button aria-pressed=\"false\" class=\"filter-btn\" data-group=\"Programmes sectoriels\" style=\"--filter-color:#c95c74;\" type=\"button\">\r\n<span class=\"filter-dot\"><\/span>Programmes sectoriels\r\n<\/button>\r\n<button aria-pressed=\"false\" class=\"filter-btn\" data-group=\"Coop\u00e9ration territoriale\" style=\"--filter-color:#d4a14c;\" type=\"button\">\r\n<span class=\"filter-dot\"><\/span>Coop\u00e9ration territoriale\r\n<\/button>\r\n<button aria-pressed=\"false\" class=\"filter-btn\" data-group=\"Fonds structurels\" style=\"--filter-color:#3eb5c5;\" type=\"button\">\r\n<span class=\"filter-dot\"><\/span>Fonds structurels\r\n<\/button>\r\n<\/div>\r\n<\/div>\r\n<\/header>\r\n<main class=\"main-content\">\r\n<div id=\"chart-container\"><\/div>\r\n<aside class=\"funding-legend\" id=\"funding-legend\" aria-label=\"Programmes europ\u00e9ens\"><\/aside>\r\n<div class=\"tooltip\" id=\"tooltip\"><\/div>\r\n<\/main>\r\n<\/div>\r\n<\/div>\r\n\r\n<script>\r\n(function () {\r\n    if (window.__shlRoueWPCodeLoaded) return;\r\n    window.__shlRoueWPCodeLoaded = true;\r\n\r\n    let __shlRoueAttempts = 0;\r\n    function __shlRoueBoot() {\r\n        const container = document.querySelector('#shl-roue-wp #chart-container');\r\n        if (typeof d3 !== 'undefined' && container && container.clientWidth > 30 && container.clientHeight > 30) {\r\n            __shlRoueInit();\r\n            return;\r\n        }\r\n        if (__shlRoueAttempts < 180) {\r\n            __shlRoueAttempts++;\r\n            setTimeout(__shlRoueBoot, 50);\r\n            return;\r\n        }\r\n        console.error('SHL Roue : D3 non charg\u00e9 ou conteneur non mesurable.');\r\n    }\r\n\r\n    if (document.readyState === 'loading') {\r\n        document.addEventListener('DOMContentLoaded', __shlRoueBoot);\r\n    } else {\r\n        __shlRoueBoot();\r\n    }\r\n\r\n    function __shlRoueInit() {\r\n\r\n        \/\/ 1. DATA\r\n        const D = [\r\n            {\r\n                titleLines: [\"Coh\u00e9sion sociale\", \"& citoyennet\u00e9\"],\r\n                name: \"Coh\u00e9sion sociale & citoyennet\u00e9\", color: \"#2b6f9f\", light: \"#79b3d5\", lightAlt: \"#5291ba\", accent: \"#d6eaf4\", accentAlt: \"#a7cee4\", items: [\r\n                    { name: \"Lutte contre les discriminations\", subs: [\"campagnes de sensibilisation\", \"formations \u00e0 l\u2019\u00e9galit\u00e9\", \"programmes d\u2019inclusion\"] },\r\n                    { name: \"Participation citoyenne\", subs: [\"d\u00e9bats citoyens\", \"universit\u00e9s populaires\", \"budgets participatifs\"] },\r\n                    { name: \"Inclusion sociale\", subs: [\"projets interg\u00e9n\u00e9rationnels\", \"accompagnement des publics vuln\u00e9rables\", \"insertion sociale\"] },\r\n                    { name: \"Acc\u00e8s aux droits\", subs: [\"m\u00e9diation sociale\", \"permanences juridiques\", \"information des habitants\"] }\r\n                ]\r\n            },\r\n            {\r\n                titleLines: [\"Jeunesse, \u00e9ducation\", \"et comp\u00e9tences\"],\r\n                name: \"Jeunesse, \u00e9ducation et comp\u00e9tences\", color: \"#c49432\", light: \"#d9bc63\", lightAlt: \"#cea84a\", accent: \"#f4e7bd\", accentAlt: \"#e6d190\", items: [\r\n                    { name: \"Jeunesse\", subs: [\"\u00e9changes de jeunes\", \"volontariat europ\u00e9en\", \"projets sportifs inclusifs\"] },\r\n                    { name: \"\u00c9ducation\", subs: [\"projets \u00e9ducatifs innovants\", \"lutte contre d\u00e9crochage scolaire\", \"projets p\u00e9dagogiques europ\u00e9ens\"] },\r\n                    { name: \"Mobilit\u00e9\", subs: [\"mobilit\u00e9s europ\u00e9ennes\", \"visites d\u2019\u00e9tude\", \"\u00e9changes professionnels\"] },\r\n                    { name: \"Insertion\", subs: [\"formation professionnelle\", \"entrepreneuriat des jeunes\", \"accompagnement vers l\u2019emploi\"] }\r\n                ]\r\n            },\r\n            {\r\n                titleLines: [\"Culture & cr\u00e9ativit\u00e9\", \"territoriale\"],\r\n                name: \"Culture et cr\u00e9ativit\u00e9 territoriale\", color: \"#74518d\", light: \"#b197c8\", lightAlt: \"#9274aa\", accent: \"#e6daee\", accentAlt: \"#cbb8db\", items: [\r\n                    { name: \"M\u00e9diation culturelle\", subs: [\"ateliers artistiques\", \"projets culture-\u00e9ducation\", \"m\u00e9diation aupr\u00e8s des habitants\"] },\r\n                    { name: \"Valorisation des territoires\", subs: [\"patrimoine local\", \"tourisme culturel\", \"projets d\u2019identit\u00e9 territoriale\"] },\r\n                    { name: \"Projets artistiques\", subs: [\"r\u00e9sidences d\u2019artistes\", \"cr\u00e9ations collaboratives\", \"projets art et quartiers\"] },\r\n                    { name: \"Culture\", subs: [\"festivals culturels\", \"\u00e9v\u00e9nements artistiques\", \"projets culturels participatifs\"] }\r\n                ]\r\n            },\r\n            {\r\n                titleLines: [\"Transition \u00e9cologique\", \"et innovation\", \"sociale\"],\r\n                name: \"Transition \u00e9cologique et innovation sociale\", color: \"#2f8d69\", light: \"#83ccb3\", lightAlt: \"#59ac8e\", accent: \"#d2ebe2\", accentAlt: \"#aadbca\", items: [\r\n                    { name: \"Transition \u00e9cologique\", subs: [\"projets climat\", \"sensibilisation environnementale\", \"initiatives z\u00e9ro d\u00e9chet\"] },\r\n                    { name: \"Innovation sociale\", subs: [\"exp\u00e9rimentations sociales\", \"tiers-lieux\", \"nouveaux services locaux\"] },\r\n                    { name: \"Projets num\u00e9riques\", subs: [\"inclusion num\u00e9rique\", \"plateformes collaboratives\", \"projets num\u00e9riques citoyens\"] },\r\n                    { name: \"Nouveaux mod\u00e8les \u00e9conomiques\", subs: [\"\u00e9conomie sociale et solidaire\", \"entrepreneuriat social\", \"circuits courts locaux\"] }\r\n                ]\r\n            }\r\n        ];\r\n\r\n        const hierarchyData = {\r\n            name: \"Tous les domaines\",\r\n            color: \"#ffffff\",\r\n            children: D.map(domain => {\r\n                let subRunningIndex = 0;\r\n                return {\r\n                    name: domain.name,\r\n                    titleLines: domain.titleLines,\r\n                    color: domain.color,\r\n                    light: domain.light,\r\n                    accent: domain.accent,\r\n                    children: domain.items.map((item, itemIndex) => ({\r\n                        name: item.name,\r\n                        color: itemIndex % 2 === 0 ? domain.lightAlt : domain.light,\r\n                        children: item.subs.map(sub => ({\r\n                            name: sub,\r\n                            color: subRunningIndex++ % 2 === 0 ? domain.accentAlt : domain.accent,\r\n                            value: 1\r\n                        }))\r\n                    }))\r\n                };\r\n            })\r\n        };\r\n\r\n        const fundingLayers = [\r\n            {\r\n                mode: \"category\",\r\n                group: \"Programmes sectoriels\",\r\n                label: \"ERASMUS +\",\r\n                color: \"#e8a1b1\",\r\n                segments: [[1, 89], [91, 89], [181, 89], [271, 89]],\r\n                labelAngle: -45,\r\n                items: [\"Jeunesse\", \"formation\", \"mobilit\u00e9s europ\u00e9ennes\"]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Programmes sectoriels\",\r\n                label: \"CERV\",\r\n                color: \"#da7c91\",\r\n                segments: [[1, 89], [271, 89]],\r\n                labelAngle: 12,\r\n                items: [\"Citoyennet\u00e9\", \"droits\", \"valeurs europ\u00e9ennes\"]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Programmes sectoriels\",\r\n                label: \"Europe Creative\",\r\n                shortLabel: \"Creative\",\r\n                color: \"#c95c74\",\r\n                segments: [[181, 89], [271, 89]],\r\n                labelAngle: -22,\r\n                items: [\"Culture\", \"cr\u00e9ation\", \"coop\u00e9ration artistique\"]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Coop\u00e9ration territoriale\",\r\n                label: \"URBACT IV \/ CITY-TO-CITY \/ EUI\",\r\n                shortLabel: \"URBACT \/ C2C\",\r\n                color: \"#d4a14c\",\r\n                segments: [[1, 89], [91, 89], [181, 89], [271, 89]],\r\n                labelAngle: 30,\r\n                items: [\"R\u00e9seaux urbains\", \"\u00e9changes entre villes\", \"innovation publique locale\"]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Fonds structurels\",\r\n                label: \"FSE +\",\r\n                color: \"#1b8a9e\",\r\n                segments: [[1, 89], [271, 89]],\r\n                labelAngle: -40,\r\n                items: [\"Emploi\", \"formation\", \"inclusion sociale\"]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Fonds structurels\",\r\n                label: \"LIFE\",\r\n                color: \"#2da0b2\",\r\n                segments: [[1, 89]],\r\n                labelAngle: 40,\r\n                items: [\"Environnement\", \"climat\", \"transition \u00e9cologique\"]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Fonds structurels\",\r\n                label: \"FAMI\",\r\n                color: \"#3eb5c5\",\r\n                segments: [[1, 89], [271, 89]],\r\n                labelAngle: -40,\r\n                items: [\"Asile\", \"migration\", \"int\u00e9gration\"]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Fonds structurels\",\r\n                label: \"FEDER \/ FEADER\",\r\n                shortLabel: \"FEDER \/ FEADER\",\r\n                color: \"#54c4d2\",\r\n                segments: [[271, 89]],\r\n                labelAngle: -40,\r\n                items: [\"D\u00e9veloppement r\u00e9gional\", \"ruralit\u00e9\", \"territoires\"]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Fonds structurels\",\r\n                label: \"INTERREG\",\r\n                color: \"#70d0dc\",\r\n                segments: [[1, 89], [271, 89]],\r\n                labelAngle: -40,\r\n                items: [\"Coop\u00e9ration transfrontali\u00e8re et territoriale\"]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Fonds structurels\",\r\n                label: \"EU4Health\",\r\n                color: \"#7fd6e1\",\r\n                segments: [[1, 89], [91, 89]],\r\n                labelAngle: 40,\r\n                items: [\"Sant\u00e9\", \"pr\u00e9vention\", \"syst\u00e8mes de soin\"]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Fonds structurels\",\r\n                label: \"HORIZON EUROPE\",\r\n                shortLabel: \"HORIZON\",\r\n                color: \"#8edce6\",\r\n                segments: [[1, 89], [91, 89], [181, 89], [271, 89]],\r\n                labelAngle: -40,\r\n                items: [\"Recherche\", \"innovation\", \"coop\u00e9rations europ\u00e9ennes\"]\r\n            }\r\n        ];\r\n\r\n        \/\/ 2. STATE\r\n        let selectedNode = null;\r\n        let fundingMode = \"none\";\r\n        const activeFundingCategories = {\r\n            \"Programmes sectoriels\": false,\r\n            \"Coop\u00e9ration territoriale\": false,\r\n            \"Fonds structurels\": false\r\n        };\r\n\r\n        \/\/ 3. SETUP\r\n        const chartContainer = document.getElementById('chart-container');\r\n        const tooltip = document.getElementById('tooltip');\r\n\r\n        function getChartSize() {\r\n            return {\r\n                width: chartContainer.clientWidth || window.innerWidth,\r\n                height: chartContainer.clientHeight || window.innerHeight\r\n            };\r\n        }\r\n\r\n        function isCompactLayout(w = width) {\r\n            const coarsePointer = window.matchMedia && window.matchMedia(\"(pointer: coarse)\").matches;\r\n            return w <= 900 || (coarsePointer && w <= 1180);\r\n        }\r\n\r\n        function screenTier(w = width, h = window.innerHeight) {\r\n            if (w <= 560) return \"phone\";\r\n            if (w <= 900) return \"tablet\";\r\n            if (w <= 1180 || h <= 760) return \"laptop\";\r\n            if (w >= 1500) return \"wide\";\r\n            return \"desktop\";\r\n        }\r\n\r\n        function getUsableChartWidth(w = width) {\r\n            return Math.max(300, w);\r\n        }\r\n\r\n        function getChartCenterX(w = width) {\r\n            return getUsableChartWidth(w) \/ 2;\r\n        }\r\n\r\n        function hasActiveFundingFilters() {\r\n            return Object.values(activeFundingCategories).some(v => v);\r\n        }\r\n\r\n        function isFundingSelectionActive() {\r\n            \/\/ Renvoyer true en mode financement avec filtres actifs :\r\n            \/\/   1. cache le depth-3 (petits arcs) pour que les anneaux fins\r\n            \/\/      n'aillent pas par-dessus du contenu,\r\n            \/\/   2. d\u00e9clenche la branche d\u00e9di\u00e9e de getResponsiveRadius() qui ne\r\n            \/\/      r\u00e9serve plus qu'un petit liser\u00e9 (anneaux fins en v4).\r\n            return fundingMode === \"category\" && hasActiveFundingFilters() && !selectedNode;\r\n        }\r\n\r\n        function getResponsiveRadius(w, h) {\r\n            const usableWidth = getUsableChartWidth(w);\r\n            const visibleHeight = Math.max(180, h);\r\n            const margin = Math.min(w, h) < 520 ? 10 : 22;\r\n            if (isFundingSelectionActive()) {\r\n                \/\/ v4 \u2014 Anneaux fins (~4-7 px \u00d7 ~11 couches) : la roue garde\r\n                \/\/ quasiment toute sa taille, on ne r\u00e9serve qu'un petit liser\u00e9\r\n                \/\/ pour que les rubans ne soient pas coup\u00e9s sur les bords.\r\n                const compact = isCompactLayout(w);\r\n                const ringsReserve = compact ? 30 : 64;\r\n                const fromHeight = visibleHeight \/ 2 - margin - ringsReserve;\r\n                const fromWidth = usableWidth \/ 2 - margin - ringsReserve;\r\n                return Math.max(80, Math.min(fromHeight, fromWidth));\r\n            }\r\n            if (isCompactLayout(w)) {\r\n                const topReserve = screenTier(w, h) === \"phone\" ? 68 : 78;\r\n                const bottomReserve = 16;\r\n                const availableH = Math.max(180, visibleHeight - topReserve - bottomReserve);\r\n                return Math.max(86, Math.min(w \/ 2 - margin - 10, availableH \/ 2 - margin));\r\n            }\r\n            const verticalReserve = 22;\r\n            const horizontalReserve = 44;\r\n            const fromHeight = h \/ 2 - margin - verticalReserve;\r\n            const fromWidth = usableWidth \/ 2 - margin - horizontalReserve;\r\n            return Math.max(40, Math.min(fromHeight, fromWidth));\r\n        }\r\n\r\n        function getZoomRadius() {\r\n            const topSpace = getZoomTopSpace();\r\n            const usableWidth = getUsableChartWidth(width);\r\n            const visibleHeight = Math.max(180, height);\r\n            const sideReserve = isCompactLayout(width) ? 24 : Math.min(usableWidth * 0.18, 360);\r\n            const widthRadius = usableWidth \/ 2 - sideReserve;\r\n            const heightRadius = isCompactLayout(width)\r\n                ? visibleHeight - topSpace - 32\r\n                : visibleHeight - topSpace - 34;\r\n            if (isCompactLayout(width)) {\r\n                return Math.max(72, Math.min(widthRadius, heightRadius));\r\n            }\r\n            return Math.max(radius, Math.min(widthRadius, heightRadius));\r\n        }\r\n\r\n        function getZoomTopSpace() {\r\n            if (isCompactLayout(width)) return Math.min(Math.max(height * 0.13, 78), 112);\r\n            return Math.min(Math.max(height * 0.12, 52), 104);\r\n        }\r\n\r\n        function getActiveRadius(isZoomed) {\r\n            return isZoomed ? getZoomRadius() : radius;\r\n        }\r\n\r\n        function getChartCenterY(isZoomed) {\r\n            const visibleHeight = Math.max(180, height);\r\n            if (!isZoomed) {\r\n                return visibleHeight \/ 2;\r\n            }\r\n            const activeRadius = getActiveRadius(true);\r\n            const topSpace = getZoomTopSpace();\r\n            const bottomMargin = isCompactLayout(width) ? 18 : (Math.min(height, width) < 520 ? 16 : 28);\r\n            const targetY = activeRadius + topSpace;\r\n            return Math.min(Math.max(targetY, visibleHeight \/ 2), visibleHeight - bottomMargin);\r\n        }\r\n\r\n        let { width, height } = getChartSize();\r\n        let radius = getResponsiveRadius(width, height);\r\n\r\n        const svg = d3.select(\"#chart-container\")\r\n            .append(\"svg\")\r\n            .attr(\"viewBox\", `0 0 ${width} ${height}`)\r\n            .style(\"font\", \"12px 'Manrope'\")\r\n            .style(\"overflow\", \"visible\");\r\n\r\n        const g = svg.append(\"g\").attr(\"transform\", `translate(${getChartCenterX()},${getChartCenterY(false)})`);\r\n        const partition = d3.partition().size([2 * Math.PI, radius]);\r\n        \/\/ Preserve the data order so the four main themes stay in their intended quadrants:\r\n        \/\/ Coh\u00e9sion top-right, Jeunesse bottom-right, Culture bottom-left, Transition top-left.\r\n        const root = d3.hierarchy(hierarchyData).sum(d => d.value);\r\n        partition(root);\r\n        applyRadialLayout(root, radius);\r\n\r\n        function applyRadialLayout(rootNode, activeRadius) {\r\n            const depthBands = [0, 0.22, 0.54, 0.79, 1];\r\n            rootNode.each(d => {\r\n                const start = depthBands[d.depth] ?? depthBands[depthBands.length - 2];\r\n                const end = depthBands[d.depth + 1] ?? depthBands[depthBands.length - 1];\r\n                d.y0 = start * activeRadius;\r\n                d.y1 = end * activeRadius;\r\n            });\r\n        }\r\n\r\n\r\n        const arc = d3.arc()\r\n            .startAngle(d => d.x0)\r\n            .endAngle(d => d.x1)\r\n            .padAngle(d => Math.min((d.x1 - d.x0) \/ 2, 0.005))\r\n            .padRadius(radius \/ 2)\r\n            .innerRadius(d => d.y0)\r\n            .outerRadius(d => d.y1 - 1);\r\n\r\n        const arcsGroup = g.append(\"g\").attr(\"class\", \"arcs\");\r\n        const textPathsGroup = g.append(\"defs\").attr(\"class\", \"text-paths\");\r\n        const textGroup = g.append(\"g\").attr(\"class\", \"texts\").attr(\"pointer-events\", \"none\");\r\n        const domainTitleGroup = g.append(\"g\").attr(\"class\", \"domain-titles\").attr(\"pointer-events\", \"none\");\r\n        const fundingGroup = g.append(\"g\").attr(\"class\", \"funding\");\r\n\r\n        function fundingBandData(mode) {\r\n            return fundingRingData(mode).flatMap(layer => {\r\n                return layer.segments.map(([start, span], segmentIndex) => {\r\n                    const startAngle = (start * Math.PI) \/ 180;\r\n                    const endAngle = ((start + span) * Math.PI) \/ 180;\r\n                    return {\r\n                        ...layer,\r\n                        segmentIndex,\r\n                        pieceIndex: 0,\r\n                        startAngle,\r\n                        endAngle,\r\n                        midAngle: (startAngle + endAngle) \/ 2\r\n                    };\r\n                });\r\n            });\r\n        }\r\n\r\n        function getDomainQuadrantSegments() {\r\n            const domains = (root && root.children) || [];\r\n            const padding = 1;\r\n            return domains.map(d => {\r\n                const x0Deg = (d.x0 * 180) \/ Math.PI;\r\n                const x1Deg = (d.x1 * 180) \/ Math.PI;\r\n                return [x0Deg + padding, (x1Deg - x0Deg) - 2 * padding];\r\n            });\r\n        }\r\n\r\n        function fundingRingData(mode) {\r\n            if (mode !== \"category\") return [];\r\n            const layers = fundingLayers.filter(layer => layer.mode === mode);\r\n            const compact = isCompactLayout(width);\r\n            const ringGap = compact ? Math.max(2, Math.min(4, radius * 0.05)) : Math.max(4, Math.min(7, radius * 0.025));\r\n            const interRingGap = compact ? Math.max(1, Math.min(2, radius * 0.018)) : Math.max(1.5, Math.min(3, radius * 0.012));\r\n            const visibleHeight = Math.max(180, height);\r\n            const outerLimit = Math.min(getUsableChartWidth(width), visibleHeight) \/ 2 - (compact ? 8 : 14);\r\n            \/\/ When in funding mode without a selected branch, depth 3 of the wheel is hidden,\r\n            \/\/ so rings can start from the depth 2 outer edge (0.79 \u00d7 radius) instead of the\r\n            \/\/ wheel's full radius. This gives the rings more room without changing their order.\r\n            const wheelOuterForRings = !selectedNode ? radius * 0.79 : radius;\r\n            const fitThickness = (outerLimit - wheelOuterForRings - ringGap - Math.max(0, layers.length - 1) * interRingGap) \/ Math.max(1, layers.length);\r\n            \/\/ v4 \u2014 Anneaux fins : les noms des programmes sont \u00e9crits dans une liste\r\n            \/\/ lat\u00e9rale (#funding-legend), plus \u00e0 l'int\u00e9rieur des arcs. On peut donc\r\n            \/\/ afficher des \u00ab traits \u00bb beaucoup plus minces et lib\u00e9rer du rayon pour\r\n            \/\/ la roue elle-m\u00eame.\r\n            const ringThickness = compact\r\n                ? Math.max(2.5, Math.min(screenTier(width, height) === \"phone\" ? 4 : 5.5, fitThickness))\r\n                : Math.max(4, Math.min(7, fitThickness));\r\n            const ringStep = ringThickness + interRingGap;\r\n            const firstInner = wheelOuterForRings + ringGap;\r\n            const quadrants = getDomainQuadrantSegments();\r\n            return layers.map((layer, layerIndex) => {\r\n                const inner = firstInner + layerIndex * ringStep;\r\n                const outer = inner + ringThickness;\r\n                const segments = quadrants.length === 4\r\n                    ? layer.segments.map(([start, span]) => {\r\n                        const qIndex = Math.floor((((start + span \/ 2) % 360) + 360) % 360 \/ 90) % 4;\r\n                        return quadrants[qIndex];\r\n                    })\r\n                    : layer.segments;\r\n                return {\r\n                    ...layer,\r\n                    segments,\r\n                    ringIndex: layerIndex,\r\n                    innerRadius: inner,\r\n                    outerRadius: outer,\r\n                    labelRadius: inner + ringThickness \/ 2\r\n                };\r\n            });\r\n        }\r\n\r\n        function fundingTooltip(d) {\r\n            return `<strong>${d.label}<\/strong><br><em>${d.group}<\/em>`;\r\n        }\r\n\r\n        \/\/ v4 \u2014 L\u00e9gende verticale : pastille color\u00e9e + nom du programme,\r\n        \/\/ regroup\u00e9s par cat\u00e9gorie. Affich\u00e9e uniquement quand au moins une\r\n        \/\/ cat\u00e9gorie est coch\u00e9e (sinon retir\u00e9e pour laisser la roue respirer).\r\n        \/\/ Retourne true si la visibilit\u00e9 de la l\u00e9gende a chang\u00e9 \u2014 utile pour\r\n        \/\/ d\u00e9clencher un reflow de la roue dans la foul\u00e9e.\r\n        let __legendActive = false;\r\n\r\n        function escapeHTML(value) {\r\n            return String(value ?? \"\").replace(\/[&<>\"']\/g, char => ({\r\n                \"&\": \"&amp;\",\r\n                \"<\": \"&lt;\",\r\n                \">\": \"&gt;\",\r\n                '\"': \"&quot;\",\r\n                \"'\": \"&#39;\"\r\n            }[char]));\r\n        }\r\n\r\n        function renderFundingLegend() {\r\n            const legendEl = document.getElementById('funding-legend');\r\n            if (!legendEl) return false;\r\n            const categories = [\"Programmes sectoriels\", \"Coop\u00e9ration territoriale\", \"Fonds structurels\"];\r\n            const activeCats = fundingMode === \"category\"\r\n                ? categories.filter(cat => activeFundingCategories[cat])\r\n                : [];\r\n            const shouldBeActive = activeCats.length > 0;\r\n            const previouslyActive = __legendActive;\r\n            __legendActive = shouldBeActive;\r\n\r\n            if (!shouldBeActive) {\r\n                legendEl.classList.remove('is-active');\r\n                legendEl.innerHTML = \"\";\r\n                return previouslyActive !== shouldBeActive;\r\n            }\r\n\r\n            legendEl.innerHTML = activeCats.map(cat => {\r\n                const progs = fundingLayers.filter(l => l.mode === \"category\" && l.group === cat);\r\n                const items = progs.map(p => `\r\n                    <li class=\"funding-legend-item\">\r\n                        <span class=\"funding-legend-dot\" style=\"--dot-color:${p.color};\"><\/span>\r\n                        <span>${escapeHTML(p.label)}<\/span>\r\n                    <\/li>`).join(\"\");\r\n                return `\r\n                <section class=\"funding-legend-cat\">\r\n                    <h3 class=\"funding-legend-cat-title\">${escapeHTML(cat)}<\/h3>\r\n                    <ul class=\"funding-legend-list\">${items}<\/ul>\r\n                <\/section>`;\r\n            }).join(\"\");\r\n            legendEl.classList.add('is-active');\r\n            return previouslyActive !== shouldBeActive;\r\n        }\r\n\r\n        function labelOpacity(d, isZoomed, box = d.current) {\r\n            if (!box.visible) return 0;\r\n            if (box.x1 - box.x0 < 0.08) return 0;\r\n            if (d.depth === 1) return 0;\r\n            if (isCompactLayout(width)) {\r\n                if (d.depth === 3) return isZoomed ? 1 : 0;\r\n                return d.depth === 2 ? 1 : 0;\r\n            }\r\n            if (!isZoomed && d.depth >= 3) return 0;\r\n            return 1;\r\n        }\r\n\r\n        \/\/ =========================================================\r\n        \/\/ TEXT PATH BUILDER \u2014 fix: keep text readable on bottom half\r\n        \/\/ =========================================================\r\n        \/\/ Key insight: the baseline path goes clockwise for top-half segments\r\n        \/\/ (text reads left-to-right, upright), and counter-clockwise for bottom-half\r\n        \/\/ (text still reads left-to-right, upright but now \"below\" the curve).\r\n        \/\/ When the text is \"below\" the curve (bottom half), its baseline is at the top\r\n        \/\/ of the text, so the effective center of the glyph is at (r - fontSize\/2 roughly).\r\n        \/\/ To keep the visual baseline centered in the same way, shift the baseline radius\r\n        \/\/ outward on the bottom half by a small amount equal to the text x-height.\r\n        function isBottomHalf(x0, x1) {\r\n            const mid = (x0 + x1) \/ 2;\r\n            return (mid > Math.PI \/ 2 && mid < 3 * Math.PI \/ 2);\r\n        }\r\n\r\n        function buildTextPath(x0, x1, y0, y1, lineOffset = 0) {\r\n            if (x1 - x0 <= 0.0001) return `M 0 0 L 0 0`;\r\n            const bottom = isBottomHalf(x0, x1);\r\n            \/\/ Flip lineOffset sign on bottom half so multi-line text still stacks top-to-bottom\r\n            \/\/ relative to the reader's view (not relative to the circle geometry)\r\n            const effectiveOffset = bottom ? -lineOffset : lineOffset;\r\n            const r = Math.max(1, (y0 + y1) \/ 2 + effectiveOffset);\r\n\r\n            \/\/ For the top half, we draw the arc sweeping clockwise (sweep=1) from x0 to x1:\r\n            \/\/   \u2192 text sits on the outer side of the baseline (above the path in SVG y terms)\r\n            \/\/   \u2192 glyphs are upright, reads left-to-right from the viewer's perspective.\r\n            \/\/ For the bottom half, we draw counter-clockwise (sweep=0) from x1 to x0:\r\n            \/\/   \u2192 this effectively flips the baseline so the glyphs are upright for the viewer too.\r\n            let p0x, p0y, p1x, p1y, sweep;\r\n            if (!bottom) {\r\n                \/\/ Top half: path goes x0 \u2192 x1, sweep clockwise\r\n                sweep = 1;\r\n                p0x = r * Math.sin(x0); p0y = -r * Math.cos(x0);\r\n                p1x = r * Math.sin(x1); p1y = -r * Math.cos(x1);\r\n            } else {\r\n                \/\/ Bottom half: path goes x1 \u2192 x0, sweep counter-clockwise\r\n                \/\/ This makes the \"top\" of the text (baseline + ascender) face outward,\r\n                \/\/ toward the viewer, so characters stay upright.\r\n                sweep = 0;\r\n                p0x = r * Math.sin(x1); p0y = -r * Math.cos(x1);\r\n                p1x = r * Math.sin(x0); p1y = -r * Math.cos(x0);\r\n            }\r\n            const largeArc = (x1 - x0 > Math.PI) ? 1 : 0;\r\n            return `M ${p0x} ${p0y} A ${r} ${r} 0 ${largeArc} ${sweep} ${p1x} ${p1y}`;\r\n        }\r\n\r\n        function textArcPath(d, lineOffset = 0) {\r\n            return buildTextPath(d.x0, d.x1, d.y0, d.y1, lineOffset);\r\n        }\r\n\r\n        function labelLineConfig(d, box = d.current) {\r\n            const r = (box.y0 + box.y1) \/ 2;\r\n            const span = Math.max(0.001, box.x1 - box.x0);\r\n            const chordLen = Math.max(1, 2 * r * Math.sin(span \/ 2));\r\n            if (isCompactLayout(width)) {\r\n                if (d.depth === 2) {\r\n                    return {\r\n                        maxChars: chordLen < 150 ? 10 : 12,\r\n                        maxLines: 2\r\n                    };\r\n                }\r\n                if (d.depth === 3) {\r\n                    return {\r\n                        maxChars: 12,\r\n                        maxLines: 4\r\n                    };\r\n                }\r\n                return {\r\n                    maxChars: 10,\r\n                    maxLines: 1\r\n                };\r\n            }\r\n            if (d.depth === 1) {\r\n                return {\r\n                    maxChars: chordLen < 170 ? 13 : 15,\r\n                    maxLines: 3\r\n                };\r\n            }\r\n            if (d.depth === 2) {\r\n                return {\r\n                    maxChars: chordLen < 120 ? 12 : 14,\r\n                    maxLines: 3\r\n                };\r\n            }\r\n            return {\r\n                maxChars: chordLen < 92 ? 10 : 12,\r\n                maxLines: 4\r\n            };\r\n        }\r\n\r\n        function labelLines(name, d, box = d.current) {\r\n            const { maxChars, maxLines } = labelLineConfig(d, box);\r\n            const words = name.split(\/\\s+\/).filter(Boolean);\r\n            const lines = [];\r\n            let current = \"\";\r\n\r\n            words.forEach(word => {\r\n                const next = current ? `${current} ${word}` : word;\r\n                if (next.length > maxChars && current) {\r\n                    lines.push(current);\r\n                    current = word;\r\n                } else {\r\n                    current = next;\r\n                }\r\n            });\r\n            if (current) lines.push(current);\r\n\r\n            if (lines.length <= maxLines) return lines;\r\n            const kept = lines.slice(0, maxLines);\r\n            while (kept.length > 1 && kept[kept.length - 1].length < 4) {\r\n                const overflow = kept.pop();\r\n                kept[kept.length - 1] = `${kept[kept.length - 1]} ${overflow}`;\r\n            }\r\n            return kept;\r\n        }\r\n\r\n        function labelLineHeight(d, fontSize) {\r\n            \/\/ Hauteur de ligne proportionnelle \u00e0 la police pour \u00e9viter les chevauchements\r\n            \/\/ quand la roue est r\u00e9tr\u00e9cie (sidebar ouverte, \u00e9cran laptop, etc.).\r\n            if (d && d.depth === 2) return Math.max(fontSize * 1.22, fontSize + 2.5);\r\n            if (d && d.depth === 3) return Math.max(fontSize * 1.28, fontSize + 2.8);\r\n            return Math.max(fontSize * 1.22, fontSize + 2.5);\r\n        }\r\n\r\n        function labelLineOffset(lineIndex, lineCount, fontSize, d = null) {\r\n            const lineHeight = labelLineHeight(d, fontSize);\r\n            return ((lineCount - 1) \/ 2 - lineIndex) * lineHeight;\r\n        }\r\n\r\n        function computeLabelFontSize(d, isZoomed, box = d.current) {\r\n            const r = (box.y0 + box.y1) \/ 2;\r\n            const span = Math.max(0.001, box.x1 - box.x0);\r\n            const arcLen = Math.max(1, 2 * r * Math.sin(span \/ 2));\r\n            const longestLine = Math.max(...labelLines(d.data.name, d, box).map(line => line.length));\r\n            if (isCompactLayout(width)) {\r\n                const compactRadius = getActiveRadius(isZoomed);\r\n                const compactBase = Math.max(6.5, Math.min(11.5, compactRadius * 0.044));\r\n                if (d.depth === 2) {\r\n                    const maxFs = compactBase * 0.98;\r\n                    const minFs = compactBase * 0.66;\r\n                    const widthFactor = 0.58;\r\n                    const usableWidth = 0.8;\r\n                    const estW = longestLine * (maxFs * widthFactor);\r\n                    if (estW <= arcLen * usableWidth) return maxFs;\r\n                    return Math.max(minFs, (arcLen * usableWidth) \/ (longestLine * widthFactor));\r\n                }\r\n                if (d.depth === 3) {\r\n                    const maxFs = compactBase * 0.72;\r\n                    const minFs = compactBase * 0.48;\r\n                    const widthFactor = 0.54;\r\n                    const usableWidth = 0.82;\r\n                    const estW = longestLine * (maxFs * widthFactor);\r\n                    if (estW <= arcLen * usableWidth) return maxFs;\r\n                    return Math.max(minFs, (arcLen * usableWidth) \/ (longestLine * widthFactor));\r\n                }\r\n                return 0;\r\n            }\r\n            const activeRadius = getActiveRadius(isZoomed);\r\n            const fsBase = Math.max(7, Math.min(16, activeRadius * 0.040));\r\n            const depthMaxScale = isZoomed\r\n                ? (d.depth === 1 ? 1.18 : (d.depth === 2 ? 1.02 : 0.88))\r\n                : (d.depth === 1 ? 0.98 : (d.depth === 2 ? 0.84 : 0.74));\r\n            const depthMinScale = isZoomed\r\n                ? (d.depth === 1 ? 0.78 : (d.depth === 2 ? 0.74 : 0.66))\r\n                : (d.depth === 1 ? 0.74 : (d.depth === 2 ? 0.50 : 0.60));\r\n            const maxFs = fsBase * depthMaxScale;\r\n            const minFs = fsBase * depthMinScale;\r\n            const widthFactor = d.depth === 1 ? 0.57 : (d.depth === 2 ? 0.58 : 0.54);\r\n            const estW = longestLine * (maxFs * widthFactor);\r\n            const usableWidth = d.depth === 1 ? 0.9 : (d.depth === 2 ? 0.78 : 0.82);\r\n            if (estW <= arcLen * usableWidth) return maxFs;\r\n            return Math.max(minFs, (arcLen * usableWidth) \/ (longestLine * widthFactor));\r\n        }\r\n\r\n        function getUniformDepth2FontSize(isZoomed, useTarget = false) {\r\n            if (isCompactLayout(width)) {\r\n                if (!isZoomed) return 0;\r\n                const compactRadius = getActiveRadius(isZoomed);\r\n                const compactBase = Math.max(6.5, Math.min(11.5, compactRadius * 0.044));\r\n                return compactBase * 0.92;\r\n            }\r\n            let min = Infinity;\r\n            (typeof labelData !== \"undefined\" ? labelData : []).forEach(d => {\r\n                if (d.depth !== 2) return;\r\n                const box = useTarget ? (d.target || d.current) : (d.current || d);\r\n                if (!box || box.x1 - box.x0 < 0.0001) return;\r\n                if (useTarget && box.visible === false) return;\r\n                const fs = computeLabelFontSize(d, isZoomed, box);\r\n                if (fs < min) min = fs;\r\n            });\r\n            if (isFinite(min)) return min;\r\n            const activeRadius = getActiveRadius(isZoomed);\r\n            const fsBase = Math.max(7, Math.min(16, activeRadius * 0.040));\r\n            return isZoomed ? fsBase * 1.02 : fsBase * 0.84;\r\n        }\r\n\r\n        function invalidateUniformFontCache() {}\r\n\r\n        function effectiveLabelFontSize(d, isZoomed, box = d.current) {\r\n            if (d.depth === 2) {\r\n                const useTarget = box === d.target;\r\n                return getUniformDepth2FontSize(isZoomed, useTarget);\r\n            }\r\n            return computeLabelFontSize(d, isZoomed, box);\r\n        }\r\n\r\n        function labelCartesianPosition(d, box = d.current) {\r\n            const angle = (box.x0 + box.x1) \/ 2;\r\n            const radiusFactor = d.depth === 1 ? 0.54 : (d.depth === 2 ? 0.52 : 0.5);\r\n            const r = box.y0 + (box.y1 - box.y0) * radiusFactor;\r\n            return polarPoint(angle, r);\r\n        }\r\n\r\n        function wrapDomainTitle(d, box = d.current, fontSize = 11.6) {\r\n            const r = (box.y0 + box.y1) \/ 2;\r\n            const arcLen = Math.max(1, (box.x1 - box.x0) * r);\r\n            const sourceLines = d.data.titleLines || [d.data.name];\r\n            const sourceText = sourceLines.join(\" \");\r\n            const maxChars = Math.max(8, Math.floor((arcLen * 0.76) \/ (fontSize * 0.56)));\r\n            const maxLines = arcLen < 150 ? 4 : 3;\r\n            const lines = [];\r\n            let current = \"\";\r\n\r\n            sourceText.split(\/\\s+\/).forEach(word => {\r\n                const next = current ? `${current} ${word}` : word;\r\n                if (next.length > maxChars && current && lines.length < maxLines - 1) {\r\n                    lines.push(current);\r\n                    current = word;\r\n                } else {\r\n                    current = next;\r\n                }\r\n            });\r\n            if (current) lines.push(current);\r\n\r\n            return lines.slice(0, maxLines);\r\n        }\r\n\r\n        function domainTitleLayout(d, box = d.current) {\r\n            const activeRadius = getActiveRadius(Boolean(selectedNode));\r\n            const fsBase = Math.max(6.5, Math.min(14, activeRadius * 0.038));\r\n\r\n            if (isCompactLayout(width)) {\r\n                const lines = (d.data.titleLines || [d.data.name]).slice(0, 2);\r\n                return {\r\n                    fontSize: fsBase * 0.92,\r\n                    lines,\r\n                    lineCount: lines.length\r\n                };\r\n            }\r\n            const initialLineCount = (d.data.titleLines || [d.data.name]).length;\r\n            const baseTitleFs = fsBase * (initialLineCount >= 3 ? 0.88 : 0.96);\r\n            const lines = wrapDomainTitle(d, box, baseTitleFs);\r\n            const adjustedFontSize = lines.length >= 4 ? baseTitleFs * 0.84 : (lines.length >= 3 ? baseTitleFs * 0.90 : baseTitleFs);\r\n            return {\r\n                fontSize: adjustedFontSize,\r\n                lines,\r\n                lineCount: lines.length\r\n            };\r\n        }\r\n\r\n        function shouldShowDomainTitle(d) {\r\n            if (!d.current.visible) return false;\r\n            if (!selectedNode) return true;\r\n            const activeDomain = selectedNode.depth === 1\r\n                ? selectedNode\r\n                : selectedNode.ancestors().find(node => node.depth === 1);\r\n            return activeDomain === d;\r\n        }\r\n\r\n        function writeMultilineSvgText(textSelection, lines, fontSize, lineHeight) {\r\n            textSelection.selectAll(\"*\").remove();\r\n            textSelection\r\n                .attr(\"text-anchor\", \"middle\")\r\n                .attr(\"dominant-baseline\", \"middle\")\r\n                .style(\"font-size\", `${fontSize}px`);\r\n\r\n            const offset = (lines.length - 1) \/ 2;\r\n            textSelection.selectAll(\"tspan\")\r\n                .data(lines)\r\n                .join(\"tspan\")\r\n                .attr(\"x\", 0)\r\n                .attr(\"y\", (line, index) => (index - offset) * lineHeight)\r\n                .text(line => line);\r\n        }\r\n\r\n        function writeCurvedSvgText(textSelection, lines, pathIdPrefix) {\r\n            textSelection.selectAll(\"*\").remove();\r\n            textSelection\r\n                .attr(\"text-anchor\", \"middle\")\r\n                .attr(\"dominant-baseline\", \"central\")\r\n                .attr(\"transform\", null);\r\n\r\n            lines.forEach((line, idx) => {\r\n                textSelection.append(\"textPath\")\r\n                    .attr(\"href\", `#${pathIdPrefix}-${idx}`)\r\n                    .attr(\"startOffset\", \"50%\")\r\n                    .text(line);\r\n            });\r\n        }\r\n\r\n        function updateLabelText(labelNode, d, fontSize) {\r\n            const lines = labelLines(d.data.name, d, d.current);\r\n            const text = d3.select(labelNode);\r\n            if (d.depth === 1) {\r\n                text.selectAll(\"*\").remove();\r\n                return;\r\n            }\r\n            \/\/ Update arc paths for each line\r\n            const idx = d.labelIndex;\r\n            const lineCount = lines.length;\r\n            for (let li = 0; li < 4; li++) {\r\n                const pathNode = document.getElementById(`tp-${idx}-${li}`);\r\n                if (!pathNode) continue;\r\n                if (li >= lineCount) {\r\n                    pathNode.setAttribute(\"d\", \"M0,0\");\r\n                } else {\r\n                    pathNode.setAttribute(\"d\", buildTextPath(\r\n                        d.current.x0, d.current.x1,\r\n                        d.current.y0, d.current.y1,\r\n                        labelLineOffset(li, lineCount, fontSize, d)\r\n                    ));\r\n                }\r\n            }\r\n            writeCurvedSvgText(text, lines, `tp-${idx}`);\r\n            labelNode.dataset.lineCount = lineCount;\r\n            labelNode.dataset.fontSize = fontSize;\r\n        }\r\n\r\n        function renderDomainTitles() {\r\n            const domainNodes = labelData.filter(d => d.depth === 1);\r\n            const titlePathData = domainNodes.flatMap(d => {\r\n                const layout = domainTitleLayout(d, d.current);\r\n                return [0, 1, 2, 3].map(lineIndex => ({ d, lineIndex, active: lineIndex < layout.lines.length }));\r\n            });\r\n\r\n            textPathsGroup.selectAll(\"path.domain-title-path\")\r\n                .data(titlePathData, item => `${item.d.labelIndex}-${item.lineIndex}`)\r\n                .join(\"path\")\r\n                .attr(\"class\", \"domain-title-path\")\r\n                .attr(\"id\", item => `dtp-${item.d.labelIndex}-${item.lineIndex}`)\r\n                .attr(\"d\", item => {\r\n                    if (!item.active) return \"M0,0\";\r\n                    const layout = domainTitleLayout(item.d, item.d.current);\r\n                    return buildTextPath(\r\n                        item.d.current.x0,\r\n                        item.d.current.x1,\r\n                        item.d.current.y0,\r\n                        item.d.current.y1,\r\n                        labelLineOffset(item.lineIndex, layout.lineCount, layout.fontSize, item.d)\r\n                    );\r\n                });\r\n\r\n            const boxes = domainTitleGroup.selectAll(\"text.domain-title-text\")\r\n                .data(domainNodes, d => d.data.name)\r\n                .join(\"text\")\r\n                .attr(\"class\", \"domain-title-text\");\r\n\r\n            boxes.each(function (d) {\r\n                const layout = domainTitleLayout(d, d.current);\r\n                const visible = shouldShowDomainTitle(d);\r\n                const text = d3.select(this);\r\n                const lines = layout.lines;\r\n                text.style(\"opacity\", visible ? 1 : 0)\r\n                    .style(\"font-size\", `${layout.fontSize}px`);\r\n\r\n                writeCurvedSvgText(text, lines, `dtp-${d.labelIndex}`);\r\n            });\r\n        }\r\n\r\n        function updateLabelPaths(d, index, fontSize) {\r\n            const labelNode = labelNodes?.[index];\r\n            if (labelNode) updateLabelText(labelNode, d, fontSize);\r\n        }\r\n\r\n        root.each(d => {\r\n            d.target = { x0: d.x0, x1: d.x1, y0: d.y0, y1: d.y1, visible: true };\r\n            d.current = { x0: d.x0, x1: d.x1, y0: d.y0, y1: d.y1, visible: true };\r\n        });\r\n\r\n        \/\/ DRAW\r\n        const labelData = root.descendants().filter(d => d.depth > 0);\r\n        labelData.forEach((d, i) => d.labelIndex = i);\r\n\r\n        const path = arcsGroup.selectAll(\"path.arc\")\r\n            .data(labelData)\r\n            .join(\"path\")\r\n            .attr(\"class\", \"arc\")\r\n            .attr(\"fill\", d => d.data.color)\r\n            .attr(\"fill-opacity\", 1)\r\n            .attr(\"d\", d => arc(d.current))\r\n            .on(\"click\", (event, d) => {\r\n                clickedArc(event, d);\r\n            })\r\n            .on(\"mouseover\", function (event, d) {\r\n                if (d.current.x1 - d.current.x0 < 0.05) {\r\n                    tooltip.style.opacity = 1;\r\n                    tooltip.innerHTML = d.data.name;\r\n                }\r\n                d3.select(this).attr(\"fill-opacity\", 1);\r\n            })\r\n            .on(\"mousemove\", function (event) {\r\n                queueTooltipPosition(event.pageX + 15, event.pageY - 15);\r\n            })\r\n            .on(\"mouseout\", function (event, d) {\r\n                tooltip.style.opacity = 0;\r\n                d3.select(this).attr(\"fill-opacity\", 1);\r\n            });\r\n\r\n        const tp = textPathsGroup.selectAll(\"path.tp\")\r\n            .data(labelData.flatMap((d, nodeIndex) => [0, 1, 2, 3].map(lineIndex => ({ d, nodeIndex, lineIndex }))))\r\n            .join(\"path\")\r\n            .attr(\"class\", \"tp\")\r\n            .attr(\"id\", item => `tp-${item.nodeIndex}-${item.lineIndex}`)\r\n            .attr(\"d\", item => {\r\n                if (item.d.depth === 1) return \"M0,0\";\r\n                const fs = effectiveLabelFontSize(item.d, false);\r\n                const lineCount = labelLines(item.d.data.name, item.d, item.d.current).length;\r\n                if (item.lineIndex >= lineCount) return \"M0,0\";\r\n                return textArcPath(item.d.current, labelLineOffset(item.lineIndex, lineCount, fs, item.d));\r\n            });\r\n\r\n        const label = textGroup.selectAll(\"text.label\")\r\n            .data(labelData)\r\n            .join(\"text\")\r\n            .attr(\"class\", d => d.depth === 1 ? \"label domain-label\" : (d.depth === 2 ? \"label item-label\" : \"label\"))\r\n            .style(\"opacity\", d => labelOpacity(d, false, d.current))\r\n            .style(\"font-size\", d => effectiveLabelFontSize(d, false) + \"px\")\r\n            .each(function (d) {\r\n                updateLabelText(this, d, effectiveLabelFontSize(d, false));\r\n            });\r\n        renderDomainTitles();\r\n\r\n        function polarPoint(angle, r) {\r\n            return {\r\n                x: r * Math.sin(angle),\r\n                y: -r * Math.cos(angle)\r\n            };\r\n        }\r\n\r\n        const fundingArc = d3.arc()\r\n            .startAngle(d => d.startAngle)\r\n            .endAngle(d => d.endAngle)\r\n            .innerRadius(d => d.innerRadius)\r\n            .outerRadius(d => d.outerRadius);\r\n\r\n        function renderFunding(mode = fundingMode) {\r\n            const data = fundingBandData(mode);\r\n            fundingGroup.selectAll(\"path.funding-arc\")\r\n                .data(data, d => `${d.mode}-${d.label}-${d.segmentIndex}-${d.pieceIndex}`)\r\n                .join(\r\n                    enter => enter.append(\"path\")\r\n                        .attr(\"class\", \"funding-arc\")\r\n                        .attr(\"fill\", d => d.color)\r\n                        .attr(\"opacity\", 0)\r\n                        .attr(\"d\", fundingArc)\r\n                        .on(\"mouseover\", function (event, d) {\r\n                            tooltip.style.opacity = 1;\r\n                            tooltip.innerHTML = fundingTooltip(d);\r\n                            queueTooltipPosition(event.pageX + 15, event.pageY - 15);\r\n                        })\r\n                        .on(\"mousemove\", event => queueTooltipPosition(event.pageX + 15, event.pageY - 15))\r\n                        .on(\"mouseout\", () => tooltip.style.opacity = 0),\r\n                    update => update,\r\n                    exit => exit.remove()\r\n                )\r\n                .attr(\"fill\", d => d.color)\r\n                .attr(\"d\", fundingArc)\r\n                .on(\"click\", null);\r\n\r\n            updateFundingVisibility();\r\n            renderFundingLegend();\r\n        }\r\n\r\n        function updateFundingVisibility() {\r\n            const inFundingMode = fundingMode === \"category\" && !selectedNode;\r\n            fundingGroup.style(\"pointer-events\", inFundingMode ? \"all\" : \"none\");\r\n\r\n            const ringData = fundingRingData(fundingMode);\r\n            const ringIndexByLabel = new Map(ringData.map((r, i) => [r.label, i]));\r\n\r\n            fundingGroup.selectAll(\"path.funding-arc\")\r\n                .each(function (d) {\r\n                    const groupActive = !!activeFundingCategories[d.group];\r\n                    const visible = inFundingMode && groupActive;\r\n                    const ringIdx = ringIndexByLabel.get(d.label) ?? 0;\r\n                    const sel = d3.select(this);\r\n                    sel.style(\"pointer-events\", visible ? \"all\" : \"none\");\r\n                    sel.interrupt()\r\n                        .transition()\r\n                        .duration(420)\r\n                        .delay(visible ? ringIdx * 45 : 0)\r\n                        .attr(\"opacity\", visible ? 0.82 : 0);\r\n                });\r\n        }\r\n\r\n        const centerControl = g.append(\"g\")\r\n            .attr(\"class\", \"center-control\")\r\n            .attr(\"role\", \"button\")\r\n            .attr(\"tabindex\", \"0\")\r\n            .attr(\"aria-label\", \"Retour\")\r\n            .on(\"click\", () => {\r\n                const target = selectedNode ? (selectedNode.parent || root) : root;\r\n                clickedArc(null, target);\r\n            })\r\n            .on(\"keydown\", event => {\r\n                if (event.key !== \"Enter\" && event.key !== \" \") return;\r\n                event.preventDefault();\r\n                const target = selectedNode ? (selectedNode.parent || root) : root;\r\n                clickedArc(null, target);\r\n            });\r\n\r\n        centerControl.append(\"title\").text(\"Retour\");\r\n\r\n        const centerCircle = centerControl.append(\"circle\")\r\n            .attr(\"r\", radius * 0.15)\r\n            .attr(\"fill\", \"#ffffff\");\r\n\r\n        const centerText = centerControl.append(\"text\")\r\n            .attr(\"class\", \"center-text\")\r\n            .attr(\"dy\", \"0.35em\")\r\n            .text(\"\");\r\n\r\n        const centerBackIcon = centerControl.append(\"text\")\r\n            .attr(\"class\", \"center-back-icon\")\r\n            .attr(\"dy\", \"-0.15em\")\r\n            .text(\"\\u2190\");\r\n\r\n        const centerBackLabel = centerControl.append(\"text\")\r\n            .attr(\"class\", \"center-back-label\")\r\n            .attr(\"dy\", \"1.25em\")\r\n            .text(\"Retour\");\r\n\r\n        const logoDefs = svg.append(\"defs\");\r\n        const logoClipPath = logoDefs.append(\"clipPath\").attr(\"id\", \"center-logo-clip\");\r\n        const logoClipCircle = logoClipPath.append(\"circle\").attr(\"cx\", 0).attr(\"cy\", 0);\r\n\r\n        const centerLogo = centerControl.append(\"image\")\r\n            .attr(\"href\", \"https:\/\/refonte.socialhackerslab.com\/wp-content\/uploads\/2026\/04\/SHL-1-scaled.png\")\r\n            .attr(\"preserveAspectRatio\", \"xMidYMid meet\")\r\n            .attr(\"clip-path\", \"url(#center-logo-clip)\")\r\n            .attr(\"pointer-events\", \"none\");\r\n\r\n        function updateCenterControl(isZoomed, controlRadius) {\r\n            const showBack = Boolean(isZoomed);\r\n            const showLabel = showBack && controlRadius >= 30;\r\n            centerText.style(\"opacity\", showBack ? 0 : 1);\r\n            centerBackIcon\r\n                .attr(\"dy\", showLabel ? \"-0.15em\" : \"0.12em\")\r\n                .style(\"opacity\", showBack ? 1 : 0)\r\n                .style(\"font-size\", `${Math.max(18, Math.min(26, controlRadius * 0.62))}px`);\r\n            centerBackLabel\r\n                .style(\"opacity\", showLabel ? 1 : 0)\r\n                .style(\"font-size\", `${Math.max(9, Math.min(12, controlRadius * 0.28))}px`);\r\n            const logoSize = controlRadius * 1.8;\r\n            logoClipCircle.attr(\"r\", controlRadius * 0.92);\r\n            centerLogo\r\n                .attr(\"x\", -logoSize \/ 2)\r\n                .attr(\"y\", -logoSize \/ 2)\r\n                .attr(\"width\", logoSize)\r\n                .attr(\"height\", logoSize)\r\n                .style(\"opacity\", showBack ? 0 : 1);\r\n            centerControl.attr(\"aria-label\", showBack ? \"Retour\" : \"Vue d'ensemble\");\r\n            centerControl.select(\"title\").text(showBack ? \"Retour\" : \"Vue d'ensemble\");\r\n        }\r\n        updateCenterControl(false, radius * 0.15);\r\n\r\n        \/\/ Fast manual arc path builder (avoids d3.arc() recompute per frame)\r\n        const PAD_ANGLE = 0.005;\r\n        function buildArcPath(x0, x1, y0, y1) {\r\n            const span = x1 - x0;\r\n            if (!Number.isFinite(span) || !Number.isFinite(y0) || !Number.isFinite(y1)) return \"M0,0\";\r\n            if (span <= 1e-5 || span >= (2 * Math.PI - 1e-4)) return \"M0,0\";\r\n            const pa = Math.min(span \/ 2, PAD_ANGLE);\r\n            const a0 = x0 + pa \/ 2;\r\n            const a1 = x1 - pa \/ 2;\r\n            const r0 = Math.max(0, y0);\r\n            const r1 = Math.max(0, y1 - 1);\r\n            if (r1 <= r0 || a1 <= a0) return \"M0,0\";\r\n            const cos0 = Math.cos(a0 - Math.PI \/ 2), sin0 = Math.sin(a0 - Math.PI \/ 2);\r\n            const cos1 = Math.cos(a1 - Math.PI \/ 2), sin1 = Math.sin(a1 - Math.PI \/ 2);\r\n            const x0o = r1 * cos0, y0o = r1 * sin0;\r\n            const x1o = r1 * cos1, y1o = r1 * sin1;\r\n            const x0i = r0 * cos0, y0i = r0 * sin0;\r\n            const x1i = r0 * cos1, y1i = r0 * sin1;\r\n            const largeArc = (a1 - a0) > Math.PI ? 1 : 0;\r\n            return `M${x0o},${y0o}A${r1},${r1} 0 ${largeArc},1 ${x1o},${y1o}L${x1i},${y1i}A${r0},${r0} 0 ${largeArc},0 ${x0i},${y0i}Z`;\r\n        }\r\n\r\n        const animatedNodes = path.data();\r\n        const pathNodes = path.nodes();\r\n        const tpNodes = tp.nodes();\r\n        const labelNodes = label.nodes();\r\n\r\n        const nodeIndexMap = new Map(animatedNodes.map((node, index) => [node, index]));\r\n\r\n        \/\/ Batch tooltip position writes\r\n        let tooltipFrame = null;\r\n        let tooltipX = 0;\r\n        let tooltipY = 0;\r\n        function queueTooltipPosition(x, y) {\r\n            tooltipX = x;\r\n            tooltipY = y;\r\n            if (tooltipFrame) return;\r\n            tooltipFrame = requestAnimationFrame(() => {\r\n                tooltip.style.left = tooltipX + 'px';\r\n                tooltip.style.top = tooltipY + 'px';\r\n                tooltipFrame = null;\r\n            });\r\n        }\r\n\r\n        function easePolyInOut(t) {\r\n            return 0.5 - Math.cos(Math.PI * t) \/ 2;\r\n        }\r\n\r\n        function nearestEquivalentAngle(angle, reference) {\r\n            const turn = 2 * Math.PI;\r\n            return angle + Math.round((reference - angle) \/ turn) * turn;\r\n        }\r\n\r\n        function zoomGlobalStart(visualStart, node) {\r\n            const domainName = node.ancestors().find(d => d.depth === 1)?.data.name || \"\";\r\n            if (domainName.includes(\"Jeunesse\")) return visualStart;\r\n            return nearestEquivalentAngle(visualStart, node.x0);\r\n        }\r\n\r\n        let currentAnimation = null;\r\n        const MIN_FRAME_MS = 1000 \/ 60;\r\n        let lastFrameTime = 0;\r\n\r\n        \/\/ Once all initial textPaths exist in the DOM, recompute them using the proper\r\n        \/\/ orientation-aware logic (the initial draw already uses buildTextPath which is\r\n        \/\/ now correct, but let's make sure everything is in sync).\r\n        invalidateUniformFontCache();\r\n        labelData.forEach((d, i) => {\r\n            const fs = effectiveLabelFontSize(d, false);\r\n            updateLabelPaths(d, i, fs);\r\n        });\r\n        renderDomainTitles();\r\n        renderFunding();\r\n\r\n        function clickedArc(event, p) {\r\n            selectedNode = p === root ? null : p;\r\n\r\n            const isZoomed = p !== root;\r\n            const visualStart = isZoomed ? -Math.PI \/ 2 : 0;\r\n            const globalStart = isZoomed ? zoomGlobalStart(visualStart, p) : 0;\r\n            const globalEnd = isZoomed ? globalStart + Math.PI : 2 * Math.PI;\r\n            const globalRange = globalEnd - globalStart;\r\n            const activeRadius = getActiveRadius(isZoomed);\r\n            const innerHole = activeRadius * 0.12;\r\n            const duration = 760;\r\n            g.transition()\r\n                .duration(duration)\r\n                .ease(d3.easeCubicInOut)\r\n                .attr(\"transform\", `translate(${getChartCenterX()},${getChartCenterY(isZoomed)})`);\r\n\r\n            root.each(d => {\r\n                if (!isZoomed) {\r\n                    d.target = { x0: d.x0, x1: d.x1, y0: d.y0, y1: d.y1, visible: true };\r\n                } else {\r\n                    const isDescendant = d.ancestors().includes(p);\r\n                    if (isDescendant) {\r\n                        const scale = globalRange \/ (p.x1 - p.x0);\r\n                        const yScale = (activeRadius - innerHole) \/ (radius - p.y0);\r\n                        d.target = {\r\n                            x0: globalStart + (d.x0 - p.x0) * scale,\r\n                            x1: globalStart + (d.x1 - p.x0) * scale,\r\n                            y0: innerHole + (d.y0 - p.y0) * yScale,\r\n                            y1: innerHole + (d.y1 - p.y0) * yScale,\r\n                            visible: true\r\n                        };\r\n                    } else {\r\n                        d.target = { x0: globalStart, x1: globalStart, y0: d.y0, y1: d.y1, visible: false };\r\n                    }\r\n                }\r\n            });\r\n\r\n            const animated = animatedNodes;\r\n            const starts = new Array(animated.length);\r\n            const needsMove = new Array(animated.length);\r\n            const EPS = 1e-4;\r\n            for (let i = 0; i < animated.length; i++) {\r\n                const d = animated[i];\r\n                starts[i] = { x0: d.current.x0, x1: d.current.x1, y0: d.current.y0, y1: d.current.y1 };\r\n                needsMove[i] = (\r\n                    Math.abs(d.current.x0 - d.target.x0) > EPS ||\r\n                    Math.abs(d.current.x1 - d.target.x1) > EPS ||\r\n                    Math.abs(d.current.y0 - d.target.y0) > EPS ||\r\n                    Math.abs(d.current.y1 - d.target.y1) > EPS\r\n                );\r\n            }\r\n\r\n            svg.node().style.shapeRendering = \"optimizeSpeed\";\r\n\r\n            const hideOuterDepth = isFundingSelectionActive();\r\n            for (let i = 0; i < pathNodes.length; i++) {\r\n                const d = animated[i];\r\n                const targetVisible = d.target.visible;\r\n                const hiddenForFunding = hideOuterDepth && d.depth === 3;\r\n                const vis = targetVisible && !hiddenForFunding;\r\n                pathNodes[i].style.opacity = vis ? 1 : 0;\r\n                pathNodes[i].style.pointerEvents = vis ? \"all\" : \"none\";\r\n                pathNodes[i].style.fillOpacity = 1;\r\n                if (!targetVisible) pathNodes[i].setAttribute(\"d\", \"M0,0\");\r\n            }\r\n\r\n            for (let i = 0; i < labelNodes.length; i++) {\r\n                const d = animated[i];\r\n                const fs = effectiveLabelFontSize(d, isZoomed, d.target);\r\n                labelNodes[i].style.fontSize = fs + \"px\";\r\n                labelNodes[i].style.opacity = labelOpacity(d, isZoomed, d.target);\r\n                labelNodes[i].dataset.fontSize = fs;\r\n            }\r\n\r\n            textGroup.style('opacity', 0);\r\n            domainTitleGroup.style('opacity', 0);\r\n\r\n            centerCircle.attr(\"r\", isZoomed ? innerHole : radius * 0.22);\r\n            updateCenterControl(isZoomed, isZoomed ? innerHole : radius * 0.22);\r\n            updateFundingVisibility();\r\n\r\n            if (currentAnimation) cancelAnimationFrame(currentAnimation);\r\n\r\n            const DURATION = duration;\r\n            const startTime = performance.now();\r\n\r\n            function tick(now) {\r\n                if (now - lastFrameTime < MIN_FRAME_MS && now - startTime < DURATION) {\r\n                    currentAnimation = requestAnimationFrame(tick);\r\n                    return;\r\n                }\r\n                lastFrameTime = now;\r\n\r\n                const elapsed = now - startTime;\r\n                const progress = Math.min(elapsed \/ DURATION, 1);\r\n                const k = easePolyInOut(progress);\r\n\r\n                for (let i = 0; i < animated.length; i++) {\r\n                    if (!needsMove[i]) continue;\r\n                    const d = animated[i];\r\n                    const s = starts[i];\r\n                    const t = d.target;\r\n                    const x0 = s.x0 + (t.x0 - s.x0) * k;\r\n                    const x1 = s.x1 + (t.x1 - s.x1) * k;\r\n                    const y0 = s.y0 + (t.y0 - s.y0) * k;\r\n                    const y1 = s.y1 + (t.y1 - s.y1) * k;\r\n                    d.current.x0 = x0;\r\n                    d.current.x1 = x1;\r\n                    d.current.y0 = y0;\r\n                    d.current.y1 = y1;\r\n                    if (!d.target.visible) {\r\n                        d.current.visible = false;\r\n                        pathNodes[i].setAttribute(\"d\", \"M0,0\");\r\n                        continue;\r\n                    }\r\n                    d.current.visible = true;\r\n                    pathNodes[i].setAttribute(\"d\", buildArcPath(x0, x1, y0, y1));\r\n                }\r\n\r\n                if (progress < 1) {\r\n                    currentAnimation = requestAnimationFrame(tick);\r\n                } else {\r\n                    currentAnimation = null;\r\n                    svg.node().style.shapeRendering = \"geometricPrecision\";\r\n                    for (let i = 0; i < animated.length; i++) {\r\n                        if (!needsMove[i]) continue;\r\n                        const d = animated[i];\r\n                        if (!d.target.visible) {\r\n                            d.current.x0 = d.target.x0;\r\n                            d.current.x1 = d.target.x1;\r\n                            d.current.y0 = d.target.y0;\r\n                            d.current.y1 = d.target.y1;\r\n                            d.current.visible = false;\r\n                            pathNodes[i].setAttribute(\"d\", \"M0,0\");\r\n                            continue;\r\n                        }\r\n                        d.current.x0 = d.target.x0;\r\n                        d.current.x1 = d.target.x1;\r\n                        d.current.y0 = d.target.y0;\r\n                        d.current.y1 = d.target.y1;\r\n                        d.current.visible = true;\r\n                        pathNodes[i].setAttribute(\"d\", buildArcPath(d.target.x0, d.target.x1, d.target.y0, d.target.y1));\r\n                    }\r\n                    for (let i = 0; i < animated.length; i++) {\r\n                        updateLabelPaths(animated[i], i, Number(labelNodes[i].dataset.fontSize || 13));\r\n                    }\r\n                    textGroup.style('opacity', 1);\r\n                    renderDomainTitles();\r\n                    domainTitleGroup.style('opacity', 1);\r\n                }\r\n            }\r\n\r\n            currentAnimation = requestAnimationFrame(tick);\r\n        }\r\n\r\n        function redrawForCurrentSize() {\r\n            const size = getChartSize();\r\n            width = size.width;\r\n            height = size.height;\r\n            radius = getResponsiveRadius(width, height);\r\n\r\n            svg.attr(\"viewBox\", `0 0 ${width} ${height}`);\r\n            g.interrupt().attr(\"transform\", `translate(${getChartCenterX()},${getChartCenterY(Boolean(selectedNode))})`);\r\n\r\n            partition.size([2 * Math.PI, radius])(root);\r\n            applyRadialLayout(root, radius);\r\n            const activeNode = selectedNode || root;\r\n            root.each(d => {\r\n                d.current = { x0: d.x0, x1: d.x1, y0: d.y0, y1: d.y1, visible: true };\r\n                d.target = { ...d.current };\r\n            });\r\n\r\n            pathNodes.forEach((node, i) => {\r\n                const d = animatedNodes[i];\r\n                node.setAttribute(\"d\", buildArcPath(d.current.x0, d.current.x1, d.current.y0, d.current.y1));\r\n            });\r\n            animatedNodes.forEach((d, i) => {\r\n                const fs = effectiveLabelFontSize(d, Boolean(selectedNode), d.current);\r\n                labelNodes[i].style.fontSize = fs + \"px\";\r\n                labelNodes[i].style.opacity = labelOpacity(d, Boolean(selectedNode), d.current);\r\n                labelNodes[i].dataset.fontSize = fs;\r\n                updateLabelPaths(d, i, fs);\r\n            });\r\n            renderDomainTitles();\r\n            renderFunding();\r\n\r\n            centerCircle.attr(\"r\", getActiveRadius(Boolean(selectedNode)) * 0.12);\r\n            updateCenterControl(Boolean(selectedNode), getActiveRadius(Boolean(selectedNode)) * 0.12);\r\n            clickedArc(null, activeNode);\r\n        }\r\n\r\n        const fundingFilters = document.getElementById('funding-filters');\r\n        const fundingToggle = document.getElementById('funding-toggle');\r\n        fundingFilters.querySelector('.filter-label').textContent = 'Choisir un type';\r\n\r\n        function setFundingFiltersVisible(show) {\r\n            if (show) {\r\n                fundingFilters.removeAttribute('hidden');\r\n                fundingFilters.classList.add('entering');\r\n                requestAnimationFrame(() => fundingFilters.classList.remove('entering'));\r\n            } else {\r\n                fundingFilters.setAttribute('hidden', '');\r\n            }\r\n        }\r\n\r\n        function resetFundingFilters() {\r\n            Object.keys(activeFundingCategories).forEach(k => { activeFundingCategories[k] = false; });\r\n            fundingFilters.querySelectorAll('.filter-btn').forEach(btn => {\r\n                btn.classList.remove('active');\r\n                btn.setAttribute('aria-pressed', 'false');\r\n            });\r\n        }\r\n        resetFundingFilters();\r\n\r\n        fundingToggle.addEventListener('click', event => {\r\n            const button = event.target.closest('.toggle-btn');\r\n            if (!button) return;\r\n            fundingMode = button.dataset.mode;\r\n            fundingToggle.querySelectorAll('.toggle-btn').forEach(btn => {\r\n                const isActive = btn === button;\r\n                btn.classList.toggle('active', isActive);\r\n                btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');\r\n            });\r\n            if (fundingMode === \"category\") {\r\n                setFundingFiltersVisible(true);\r\n            } else {\r\n                setFundingFiltersVisible(false);\r\n                resetFundingFilters();\r\n            }\r\n            \/\/ Synchronise la l\u00e9gende AVANT le reflow pour que la largeur du\r\n            \/\/ chart soit d\u00e9j\u00e0 \u00e0 jour quand la roue se recalcule.\r\n            renderFundingLegend();\r\n            redrawForCurrentSize();\r\n        });\r\n\r\n        \/\/ Variante \"sans associations\" : le toggle a \u00e9t\u00e9 retir\u00e9 du DOM ;\r\n        \/\/ showAssociations reste false en permanence.\r\n\r\n\r\n        fundingFilters.addEventListener('click', event => {\r\n            const btn = event.target.closest('.filter-btn');\r\n            if (!btn) return;\r\n            const group = btn.dataset.group;\r\n            if (!(group in activeFundingCategories)) return;\r\n            activeFundingCategories[group] = !activeFundingCategories[group];\r\n            btn.classList.toggle('active', activeFundingCategories[group]);\r\n            btn.setAttribute('aria-pressed', activeFundingCategories[group] ? 'true' : 'false');\r\n            \/\/ v4 \u2014 On rend la l\u00e9gende AVANT le reflow : si elle appara\u00eet\/dispara\u00eet\r\n            \/\/ la largeur du chart change (flex layout), et la roue doit \u00eatre\r\n            \/\/ recalcul\u00e9e pour s'adapter \u00e0 ce nouveau gabarit.\r\n            const legendVisibilityChanged = renderFundingLegend();\r\n            if (legendVisibilityChanged) {\r\n                redrawForCurrentSize();\r\n            } else {\r\n                updateFundingVisibility();\r\n            }\r\n        });\r\n\r\n        let resizeRAF = null;\r\n        window.addEventListener('resize', () => {\r\n            if (resizeRAF) cancelAnimationFrame(resizeRAF);\r\n            resizeRAF = requestAnimationFrame(() => {\r\n                redrawForCurrentSize(); \r\n                resizeRAF = null;\r\n            });\r\n        });\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-23813681","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>outil-de-cartographie-des-projets - 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\/outil-de-cartographie-des-projets\/\" \/>\n<meta property=\"og:locale\" content=\"en_GB\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"outil-de-cartographie-des-projets - Social Hackers Lab\" \/>\n<meta property=\"og:url\" content=\"https:\/\/socialhackerslab.com\/en\/outil-de-cartographie-des-projets\/\" \/>\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-05-26T12:54:04+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<meta name=\"twitter:label1\" content=\"Estimated reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/outil-de-cartographie-des-projets\\\/\",\"url\":\"https:\\\/\\\/socialhackerslab.com\\\/outil-de-cartographie-des-projets\\\/\",\"name\":\"outil-de-cartographie-des-projets - Social Hackers Lab\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/#website\"},\"datePublished\":\"2026-05-26T12:54:01+00:00\",\"dateModified\":\"2026-05-26T12:54:04+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/outil-de-cartographie-des-projets\\\/#breadcrumb\"},\"inLanguage\":\"en-GB\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/socialhackerslab.com\\\/outil-de-cartographie-des-projets\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/outil-de-cartographie-des-projets\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Accueil\",\"item\":\"https:\\\/\\\/socialhackerslab.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"outil-de-cartographie-des-projets\"}]},{\"@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":"outil-de-cartographie-des-projets - 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\/outil-de-cartographie-des-projets\/","og_locale":"en_GB","og_type":"article","og_title":"outil-de-cartographie-des-projets - Social Hackers Lab","og_url":"https:\/\/socialhackerslab.com\/en\/outil-de-cartographie-des-projets\/","og_site_name":"Social Hackers Lab","article_publisher":"https:\/\/www.facebook.com\/profile.php?id=61561806211570","article_modified_time":"2026-05-26T12:54:04+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","twitter_misc":{"Estimated reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/socialhackerslab.com\/outil-de-cartographie-des-projets\/","url":"https:\/\/socialhackerslab.com\/outil-de-cartographie-des-projets\/","name":"outil-de-cartographie-des-projets - Social Hackers Lab","isPartOf":{"@id":"https:\/\/socialhackerslab.com\/#website"},"datePublished":"2026-05-26T12:54:01+00:00","dateModified":"2026-05-26T12:54:04+00:00","breadcrumb":{"@id":"https:\/\/socialhackerslab.com\/outil-de-cartographie-des-projets\/#breadcrumb"},"inLanguage":"en-GB","potentialAction":[{"@type":"ReadAction","target":["https:\/\/socialhackerslab.com\/outil-de-cartographie-des-projets\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/socialhackerslab.com\/outil-de-cartographie-des-projets\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Accueil","item":"https:\/\/socialhackerslab.com\/"},{"@type":"ListItem","position":2,"name":"outil-de-cartographie-des-projets"}]},{"@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\/23813681","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=23813681"}],"version-history":[{"count":1,"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/pages\/23813681\/revisions"}],"predecessor-version":[{"id":23813683,"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/pages\/23813681\/revisions\/23813683"}],"wp:attachment":[{"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/media?parent=23813681"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}