{"id":23813964,"date":"2026-08-28T13:46:22","date_gmt":"2026-08-28T13:46:22","guid":{"rendered":"https:\/\/socialhackerslab.com\/?page_id=23813964"},"modified":"2026-08-28T13:46:23","modified_gmt":"2026-08-28T13:46:23","slug":"mapping-of-projects-and-funding","status":"publish","type":"page","link":"https:\/\/socialhackerslab.com\/en\/mapping-of-projects-and-funding\/","title":{"rendered":"Mapping of projects and funding"},"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: #ffffff;\r\n            stroke-width: 1.8px;\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        \/* Success story card \u2014 a single example association (Action Bomaye),\r\n           shown as a fixed corner card so it never competes with the wheel's\r\n           own layout math. Deliberately not the full association browser. *\/\r\n        #shl-roue-wp .success-story{\r\n            position: fixed;\r\n            left: 1.1rem;\r\n            bottom: 1.1rem;\r\n            z-index: 12;\r\n            width: 280px;\r\n            max-width: calc(100vw - 2.2rem);\r\n            padding: 0.9rem 1rem 1rem;\r\n            border-radius: 16px;\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            backdrop-filter: blur(6px);\r\n            -webkit-backdrop-filter: blur(6px);\r\n        }\r\n        #shl-roue-wp .success-story-eyebrow{\r\n            display: block;\r\n            font-size: 0.66rem;\r\n            font-weight: 850;\r\n            letter-spacing: 0.09em;\r\n            text-transform: uppercase;\r\n            color: #2f8d69;\r\n            margin-bottom: 0.3rem;\r\n        }\r\n        #shl-roue-wp .success-story-name{\r\n            display: flex;\r\n            align-items: baseline;\r\n            justify-content: space-between;\r\n            gap: 0.5rem;\r\n            margin-bottom: 0.35rem;\r\n        }\r\n        #shl-roue-wp .success-story-name strong{\r\n            font-size: 1rem;\r\n            font-weight: 850;\r\n            color: var(--text-color);\r\n        }\r\n        #shl-roue-wp .success-story-name a{\r\n            font-size: 0.72rem;\r\n            font-weight: 700;\r\n            color: var(--accent-color);\r\n            text-decoration: none;\r\n            white-space: nowrap;\r\n        }\r\n        #shl-roue-wp .success-story-name a:hover{ text-decoration: underline; }\r\n        #shl-roue-wp .success-story-desc{\r\n            font-size: 0.78rem;\r\n            line-height: 1.42;\r\n            color: var(--muted-text);\r\n            margin: 0 0 0.55rem;\r\n        }\r\n        #shl-roue-wp .success-story-track{\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 0.4rem;\r\n            font-size: 0.72rem;\r\n            font-weight: 750;\r\n            color: var(--text-color);\r\n        }\r\n        #shl-roue-wp .success-story-track span.figure{\r\n            padding: 0.15rem 0.45rem;\r\n            border-radius: 999px;\r\n            background: rgba(47, 141, 105, 0.14);\r\n            color: #226b4e;\r\n        }\r\n        #shl-roue-wp .success-story-track span.arrow{ color: var(--muted-text); }\r\n\r\n        \/* ------------------------------------------------------------------\r\n           EXAMPLE PROJECT (Action Bomaye)\r\n           This English edition deliberately carries ONE worked example instead\r\n           of the full association directory of the French wheel. The example is\r\n           plotted on the wheel exactly where its theme sits, and opens a detail\r\n           panel with its real EU funding track record.\r\n           ------------------------------------------------------------------ *\/\r\n\r\n        \/* On\/off switch that reveals the example marker on the wheel. *\/\r\n        #shl-roue-wp .example-switch-group{\r\n            display: inline-flex;\r\n            align-items: center;\r\n            justify-content: space-between;\r\n            gap: 0.7rem;\r\n            padding: 0.6rem 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            width: fit-content;\r\n            cursor: pointer;\r\n            font: inherit;\r\n            color: var(--text-color);\r\n            transition: border-color 0.2s ease, transform 0.15s ease;\r\n        }\r\n        #shl-roue-wp .example-switch-group:hover{\r\n            border-color: rgba(36, 71, 109, 0.28);\r\n            transform: translateY(-1px);\r\n        }\r\n        #shl-roue-wp .example-switch-label{\r\n            font-size: 0.82rem;\r\n            font-weight: 700;\r\n            white-space: nowrap;\r\n        }\r\n        #shl-roue-wp .example-switch{\r\n            position: relative;\r\n            flex: none;\r\n            width: 38px;\r\n            height: 22px;\r\n            border-radius: 999px;\r\n            background: rgba(36, 71, 109, 0.2);\r\n            transition: background-color 0.22s ease;\r\n        }\r\n        #shl-roue-wp .example-switch-thumb{\r\n            position: absolute;\r\n            top: 2px;\r\n            left: 2px;\r\n            width: 18px;\r\n            height: 18px;\r\n            border-radius: 50%;\r\n            background: #ffffff;\r\n            box-shadow: 0 2px 5px rgba(15, 23, 42, 0.28);\r\n            transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);\r\n        }\r\n        #shl-roue-wp .example-switch-group.active .example-switch{ background: #2f8d69; }\r\n        #shl-roue-wp .example-switch-group.active .example-switch-thumb{ transform: translateX(16px); }\r\n\r\n        \/* Marker plotted on the wheel, at the sub-theme the example belongs to. *\/\r\n        #shl-roue-wp .example-marker{\r\n            cursor: pointer;\r\n            transition: opacity 0.3s ease-out;\r\n        }\r\n        #shl-roue-wp .example-marker-inner{\r\n            transform: scale(0);\r\n            transform-origin: 0 0;\r\n            transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);\r\n        }\r\n        #shl-roue-wp .example-marker-inner.popped{ transform: scale(1); }\r\n        #shl-roue-wp .example-marker-circle{\r\n            stroke: rgba(255, 255, 255, 0.95);\r\n            stroke-width: 2px;\r\n            vector-effect: non-scaling-stroke;\r\n            transition: stroke-width 0.2s cubic-bezier(0.4, 0, 0.2, 1);\r\n            filter: drop-shadow(0 4px 10px rgba(15, 23, 42, 0.28));\r\n        }\r\n        #shl-roue-wp .example-marker:hover .example-marker-circle{ stroke-width: 3.5px; }\r\n        #shl-roue-wp .example-marker-halo{\r\n            fill: none;\r\n            stroke-width: 2px;\r\n            vector-effect: non-scaling-stroke;\r\n            transform-origin: 0 0;\r\n            animation: exampleHalo 2.6s ease-out infinite;\r\n            pointer-events: none;\r\n        }\r\n        @keyframes exampleHalo{\r\n            0%   { transform: scale(1);   opacity: 0.55; }\r\n            70%  { transform: scale(2.1); opacity: 0; }\r\n            100% { transform: scale(2.1); opacity: 0; }\r\n        }\r\n        #shl-roue-wp .example-marker-mono{\r\n            pointer-events: none;\r\n            font-family: var(--font-family);\r\n            font-weight: 850;\r\n            text-anchor: middle;\r\n            dominant-baseline: central;\r\n            fill: #ffffff;\r\n            letter-spacing: 0.02em;\r\n        }\r\n\r\n        \/* Detail panel. Fixed overlay so it never feeds into the wheel's own\r\n           layout maths (unlike the funding legend, which is a flex sibling). *\/\r\n        #shl-roue-wp .example-panel{\r\n            position: fixed;\r\n            top: 0;\r\n            right: 0;\r\n            bottom: 0;\r\n            width: min(420px, 92vw);\r\n            z-index: 40;\r\n            display: flex;\r\n            flex-direction: column;\r\n            background: rgba(255, 253, 251, 0.97);\r\n            backdrop-filter: blur(10px);\r\n            -webkit-backdrop-filter: blur(10px);\r\n            border-left: 1px solid var(--border-color);\r\n            box-shadow: -22px 0 55px rgba(36, 71, 109, 0.14);\r\n            transform: translateX(102%);\r\n            transition: transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);\r\n            overflow: hidden;\r\n        }\r\n        #shl-roue-wp .example-panel.open{ transform: translateX(0); }\r\n        #shl-roue-wp .example-panel-close{\r\n            position: absolute;\r\n            top: 0.9rem;\r\n            right: 0.9rem;\r\n            width: 32px;\r\n            height: 32px;\r\n            border: 1px solid var(--border-color);\r\n            border-radius: 50%;\r\n            background: #ffffff;\r\n            color: var(--text-color);\r\n            font-size: 1rem;\r\n            line-height: 1;\r\n            cursor: pointer;\r\n            display: flex;\r\n            align-items: center;\r\n            justify-content: center;\r\n            transition: background-color 0.2s ease, transform 0.15s ease;\r\n        }\r\n        \/* The body scrolls under this button, so carve a small halo of panel\r\n           background around it to keep the glyph legible over content. *\/\r\n        #shl-roue-wp .example-panel-close{\r\n            box-shadow: 0 0 0 7px rgba(255, 253, 251, 0.95), 0 2px 8px rgba(36, 71, 109, 0.12);\r\n        }\r\n        #shl-roue-wp .example-panel-close:hover{ background: #f2ece8; transform: scale(1.06); }\r\n        #shl-roue-wp .example-panel-body{\r\n            flex: 1;\r\n            overflow-y: auto;\r\n            padding: 1.6rem 1.5rem 2rem;\r\n        }\r\n        #shl-roue-wp .example-tag{\r\n            display: inline-block;\r\n            padding: 0.28rem 0.7rem;\r\n            border-radius: 999px;\r\n            font-size: 0.68rem;\r\n            font-weight: 800;\r\n            letter-spacing: 0.06em;\r\n            text-transform: uppercase;\r\n            color: #ffffff;\r\n        }\r\n        #shl-roue-wp .example-title{\r\n            margin: 0.7rem 0 0.15rem;\r\n            font-size: 1.55rem;\r\n            font-weight: 850;\r\n            line-height: 1.1;\r\n            color: #13293f;\r\n        }\r\n        #shl-roue-wp .example-path{\r\n            display: flex;\r\n            flex-wrap: wrap;\r\n            align-items: center;\r\n            gap: 0.4rem;\r\n            font-size: 0.8rem;\r\n            font-weight: 650;\r\n            color: var(--muted-text);\r\n            margin-bottom: 1rem;\r\n        }\r\n        #shl-roue-wp .example-path .sep{ color: #cbd5e1; }\r\n        #shl-roue-wp .example-meta{\r\n            display: grid;\r\n            grid-template-columns: 1fr 1fr;\r\n            gap: 0.55rem;\r\n            margin-bottom: 1.05rem;\r\n        }\r\n        #shl-roue-wp .example-meta div{\r\n            padding: 0.6rem 0.7rem;\r\n            border-radius: 12px;\r\n            background: rgba(255, 255, 255, 0.8);\r\n            border: 1px solid var(--border-color);\r\n        }\r\n        #shl-roue-wp .example-meta dt{\r\n            margin: 0 0 0.15rem;\r\n            font-size: 0.63rem;\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 .example-meta dd{\r\n            margin: 0;\r\n            font-size: 0.83rem;\r\n            font-weight: 750;\r\n            line-height: 1.3;\r\n            color: var(--text-color);\r\n        }\r\n        #shl-roue-wp .example-section-title{\r\n            margin: 1.35rem 0 0.6rem;\r\n            font-size: 0.7rem;\r\n            font-weight: 850;\r\n            letter-spacing: 0.09em;\r\n            text-transform: uppercase;\r\n            color: var(--muted-text);\r\n        }\r\n        #shl-roue-wp .example-desc{\r\n            margin: 0;\r\n            font-size: 0.88rem;\r\n            line-height: 1.55;\r\n            color: var(--text-color);\r\n        }\r\n        #shl-roue-wp .example-desc + .example-desc{ margin-top: 0.6rem; }\r\n        #shl-roue-wp .example-total{\r\n            display: flex;\r\n            align-items: baseline;\r\n            gap: 0.55rem;\r\n            padding: 0.75rem 0.9rem;\r\n            border-radius: 14px;\r\n            background: rgba(47, 141, 105, 0.12);\r\n            border: 1px solid rgba(47, 141, 105, 0.24);\r\n            margin-bottom: 0.8rem;\r\n        }\r\n        #shl-roue-wp .example-total .amount{\r\n            font-size: 1.4rem;\r\n            font-weight: 850;\r\n            color: #226b4e;\r\n            line-height: 1;\r\n        }\r\n        #shl-roue-wp .example-total .caption{\r\n            font-size: 0.76rem;\r\n            font-weight: 650;\r\n            color: #2f6b53;\r\n        }\r\n        #shl-roue-wp .example-grant{\r\n            display: block;\r\n            width: 100%;\r\n            text-align: left;\r\n            text-decoration: none;\r\n            padding: 0.8rem 0.9rem;\r\n            border-radius: 14px;\r\n            background: #ffffff;\r\n            border: 1px solid var(--border-color);\r\n            margin-bottom: 0.55rem;\r\n            transition: border-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;\r\n        }\r\n        #shl-roue-wp a.example-grant:hover{\r\n            border-color: rgba(36, 71, 109, 0.3);\r\n            transform: translateY(-1px);\r\n            box-shadow: 0 10px 24px rgba(36, 71, 109, 0.1);\r\n        }\r\n        #shl-roue-wp .example-grant-head{\r\n            display: flex;\r\n            align-items: baseline;\r\n            gap: 0.5rem;\r\n            margin-bottom: 0.28rem;\r\n        }\r\n        #shl-roue-wp .example-grant-year{\r\n            font-size: 0.68rem;\r\n            font-weight: 850;\r\n            letter-spacing: 0.06em;\r\n            padding: 0.14rem 0.45rem;\r\n            border-radius: 999px;\r\n            background: rgba(36, 71, 109, 0.1);\r\n            color: var(--text-color);\r\n        }\r\n        #shl-roue-wp .example-grant-amount{\r\n            margin-left: auto;\r\n            font-size: 0.95rem;\r\n            font-weight: 850;\r\n            color: #226b4e;\r\n        }\r\n        #shl-roue-wp .example-grant-name{\r\n            font-size: 0.86rem;\r\n            font-weight: 800;\r\n            color: var(--text-color);\r\n        }\r\n        #shl-roue-wp .example-grant-sub{\r\n            font-size: 0.76rem;\r\n            font-weight: 600;\r\n            line-height: 1.4;\r\n            color: var(--muted-text);\r\n        }\r\n        #shl-roue-wp .example-grant-link{\r\n            display: inline-block;\r\n            margin-top: 0.35rem;\r\n            font-size: 0.72rem;\r\n            font-weight: 750;\r\n            color: var(--accent-color);\r\n        }\r\n        #shl-roue-wp .example-contact{\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 0.6rem;\r\n            padding: 0.75rem 0.9rem;\r\n            border-radius: 14px;\r\n            background: #ffffff;\r\n            border: 1px solid var(--border-color);\r\n            text-decoration: none;\r\n            color: var(--text-color);\r\n            transition: border-color 0.2s ease, transform 0.15s ease;\r\n        }\r\n        #shl-roue-wp .example-contact:hover{\r\n            border-color: rgba(36, 71, 109, 0.3);\r\n            transform: translateY(-1px);\r\n        }\r\n        #shl-roue-wp .example-contact .label{\r\n            display: block;\r\n            font-size: 0.63rem;\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 .example-contact .value{\r\n            display: block;\r\n            font-size: 0.84rem;\r\n            font-weight: 750;\r\n        }\r\n        #shl-roue-wp .example-contact .chev{ margin-left: auto; color: var(--muted-text); }\r\n\r\n        \/* Corner card is now the second entry point into the panel. *\/\r\n        #shl-roue-wp button.success-story{\r\n            display: block;\r\n            text-align: left;\r\n            font: inherit;\r\n            color: inherit;\r\n            cursor: pointer;\r\n            transition: transform 0.15s ease, border-color 0.2s ease, box-shadow 0.2s ease;\r\n        }\r\n        #shl-roue-wp button.success-story:hover{\r\n            transform: translateY(-2px);\r\n            border-color: rgba(36, 71, 109, 0.3);\r\n            box-shadow: 0 16px 38px rgba(36, 71, 109, 0.13);\r\n        }\r\n        #shl-roue-wp .success-story-cta{\r\n            display: block;\r\n            margin-top: 0.5rem;\r\n            font-size: 0.72rem;\r\n            font-weight: 800;\r\n            color: var(--accent-color);\r\n        }\r\n\r\n        @media (max-width: 640px){\r\n            \/* Panel becomes a bottom sheet on phones. *\/\r\n            #shl-roue-wp .example-panel{\r\n                top: auto;\r\n                left: 0;\r\n                right: 0;\r\n                height: 78vh;\r\n                width: auto;\r\n                border-left: none;\r\n                border-top: 1px solid var(--border-color);\r\n                border-radius: 22px 22px 0 0;\r\n                transform: translateY(102%);\r\n                box-shadow: 0 -22px 55px rgba(36, 71, 109, 0.18);\r\n            }\r\n            #shl-roue-wp .example-panel.open{ transform: translateY(0); }\r\n            #shl-roue-wp .example-panel-body{ padding: 1.4rem 1.15rem 2rem; }\r\n            #shl-roue-wp .example-meta{ grid-template-columns: 1fr; }\r\n            \/* Needs the button selector to beat #shl-roue-wp button.success-story above. *\/\r\n            #shl-roue-wp button.success-story{ display: none; }\r\n        }\r\n\r\n        @media (max-width: 640px){\r\n            #shl-roue-wp .success-story{ display: none; }\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: 2px;\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=\"Mapping of incubated projects\">\r\n<span class=\"map-title-eyebrow\">Mapping<\/span>\r\n<strong class=\"map-title-main\">Mapping of incubated projects<\/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\">Projects<\/button>\r\n<button aria-pressed=\"false\" class=\"toggle-btn\" data-mode=\"category\" type=\"button\">Funding<\/button>\r\n<\/div>\r\n<div class=\"filter-group\" hidden=\"\" id=\"funding-filters\">\r\n<span class=\"filter-label\">Funding categories<\/span>\r\n<button aria-pressed=\"false\" class=\"filter-btn\" data-group=\"Sectoral Programmes\" style=\"--filter-color:#cc4f70;\" type=\"button\">\r\n<span class=\"filter-dot\"><\/span>Sectoral Programmes\r\n<\/button>\r\n<button aria-pressed=\"false\" class=\"filter-btn\" data-group=\"Territorial Cooperation\" style=\"--filter-color:#d4a14c;\" type=\"button\">\r\n<span class=\"filter-dot\"><\/span>Territorial Cooperation\r\n<\/button>\r\n<button aria-pressed=\"false\" class=\"filter-btn\" data-group=\"Structural Funds\" style=\"--filter-color:#1c95a6;\" type=\"button\">\r\n<span class=\"filter-dot\"><\/span>Structural Funds\r\n<\/button>\r\n<\/div>\r\n<button class=\"example-switch-group active\" id=\"example-toggle\" type=\"button\" role=\"switch\" aria-checked=\"true\">\r\n<span class=\"example-switch-label\">Show example project<\/span>\r\n<span class=\"example-switch\" aria-hidden=\"true\"><span class=\"example-switch-thumb\"><\/span><\/span>\r\n<\/button>\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=\"European programmes\"><\/aside>\r\n<div class=\"tooltip\" id=\"tooltip\"><\/div>\r\n<\/main>\r\n<button class=\"success-story\" id=\"success-story-card\" type=\"button\" aria-label=\"Open the Action Bomaye example\">\r\n<span class=\"success-story-eyebrow\">Success story<\/span>\r\n<div class=\"success-story-name\">\r\n<strong>Action Bomaye<\/strong>\r\n<\/div>\r\n<p class=\"success-story-desc\">Marseille association using cinema and immersive tech to include marginalised youth. Incubated by SHL, now running EU-funded projects with Greece, Spain and Italy.<\/p>\r\n<div class=\"success-story-track\">\r\n<span class=\"figure\">\u20ac60k<\/span>\r\n<span class=\"arrow\">2024 \u2192 2025<\/span>\r\n<span class=\"figure\">\u20ac250k<\/span>\r\n<\/div>\r\n<span class=\"success-story-cta\">See the full case \u2192<\/span>\r\n<\/button>\r\n<aside class=\"example-panel\" id=\"example-panel\" aria-label=\"Action Bomaye \u2014 project details\" aria-hidden=\"true\">\r\n<button class=\"example-panel-close\" id=\"example-panel-close\" type=\"button\" aria-label=\"Close\">\u2715<\/button>\r\n<div class=\"example-panel-body\" id=\"example-panel-body\"><\/div>\r\n<\/aside>\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 Wheel: D3 not loaded or container not measurable.');\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: [\"Social Cohesion\", \"& Citizenship\"],\r\n                name: \"Social Cohesion & Citizenship\", color: \"#2b6f9f\", light: \"#79b3d5\", lightAlt: \"#5291ba\", accent: \"#d6eaf4\", accentAlt: \"#a7cee4\", items: [\r\n                    { name: \"Anti-discrimination\", subs: [\"awareness campaigns\", \"equality training\", \"inclusion programmes\"] },\r\n                    { name: \"Civic Participation\", subs: [\"citizen debates\", \"popular universities\", \"participatory budgeting\"] },\r\n                    { name: \"Social Inclusion\", subs: [\"intergenerational projects\", \"support for vulnerable groups\", \"social integration\"] },\r\n                    { name: \"Access to Rights\", subs: [\"social mediation\", \"legal advice points\", \"resident information\"] }\r\n                ]\r\n            },\r\n            {\r\n                titleLines: [\"Youth, Education\", \"& Skills\"],\r\n                name: \"Youth, Education & Skills\", color: \"#c49432\", light: \"#d9bc63\", lightAlt: \"#cea84a\", accent: \"#f4e7bd\", accentAlt: \"#e6d190\", items: [\r\n                    { name: \"Youth\", subs: [\"youth exchanges\", \"European volunteering\", \"inclusive sports projects\"] },\r\n                    { name: \"Education\", subs: [\"innovative education projects\", \"tackling early school leaving\", \"European teaching projects\"] },\r\n                    { name: \"Mobility\", subs: [\"European mobility schemes\", \"study visits\", \"professional exchanges\"] },\r\n                    { name: \"Employability\", subs: [\"vocational training\", \"youth entrepreneurship\", \"support into employment\"] }\r\n                ]\r\n            },\r\n            {\r\n                titleLines: [\"Culture &\", \"Territorial Creativity\"],\r\n                name: \"Culture & Territorial Creativity\", color: \"#74518d\", light: \"#b197c8\", lightAlt: \"#9274aa\", accent: \"#e6daee\", accentAlt: \"#cbb8db\", items: [\r\n                    { name: \"Cultural Mediation\", subs: [\"art workshops\", \"culture-education projects\", \"outreach to residents\"] },\r\n                    { name: \"Territorial Heritage\", subs: [\"local heritage\", \"cultural tourism\", \"place-identity projects\"] },\r\n                    { name: \"Artistic Projects\", subs: [\"artist residencies\", \"collaborative creations\", \"art & neighbourhoods projects\"] },\r\n                    { name: \"Culture\", subs: [\"cultural festivals\", \"arts events\", \"participatory cultural projects\"] }\r\n                ]\r\n            },\r\n            {\r\n                titleLines: [\"Ecological Transition\", \"& Social\", \"Innovation\"],\r\n                name: \"Ecological Transition & Social Innovation\", color: \"#2f8d69\", light: \"#83ccb3\", lightAlt: \"#59ac8e\", accent: \"#d2ebe2\", accentAlt: \"#aadbca\", items: [\r\n                    { name: \"Ecological Transition\", subs: [\"climate projects\", \"environmental awareness\", \"zero-waste initiatives\"] },\r\n                    { name: \"Social Innovation\", subs: [\"social experimentation\", \"community third places\", \"new local services\"] },\r\n                    { name: \"Digital Projects\", subs: [\"digital inclusion\", \"collaborative platforms\", \"civic digital projects\"] },\r\n                    { name: \"New Economic Models\", subs: [\"social & solidarity economy\", \"social entrepreneurship\", \"short local supply chains\"] }\r\n                ]\r\n            }\r\n        ];\r\n\r\n        const hierarchyData = {\r\n            name: \"All domains\",\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        \/\/ Anneaux de financement \u2014 v5. Les programmes qui couvrent EXACTEMENT\r\n        \/\/ les m\u00eames quadrants de la roue (m\u00eame \u00ab espace \u00bb th\u00e9matique) sont\r\n        \/\/ fusionn\u00e9s en un seul anneau, \u00e0 l'int\u00e9rieur de leur cat\u00e9gorie. Le label\r\n        \/\/ liste alors les programmes regroup\u00e9s, s\u00e9par\u00e9s par \u00ab \/ \u00bb. Moins\r\n        \/\/ d'anneaux \u21d2 traits plus \u00e9pais et plus lisibles.\r\n        \/\/\r\n        \/\/ Chaque CAT\u00c9GORIE garde sa famille de couleur (rose = Programmes\r\n        \/\/ sectoriels, or = Territorial Cooperation, turquoise = Fonds\r\n        \/\/ structurels). \u00c0 l'int\u00e9rieur d'une famille, on \u00e9carte nettement les\r\n        \/\/ teintes (du fonc\u00e9 au clair) pour qu'on distingue chaque programme \u2014\r\n        \/\/ c'est ce contraste interne qui manquait avant.\r\n        const fundingLayers = [\r\n            \/\/ \u2014 Sectoral Programmes : famille rose \/ framboise \u2014\r\n            {\r\n                mode: \"category\",\r\n                group: \"Sectoral Programmes\",\r\n                label: \"ERASMUS +\",\r\n                color: \"#f0a8bb\",\r\n                segments: [[1, 89], [91, 89], [181, 89], [271, 89]]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Sectoral Programmes\",\r\n                label: \"CERV\",\r\n                color: \"#d5587d\",\r\n                segments: [[1, 89], [271, 89]]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Sectoral Programmes\",\r\n                label: \"Creative Europe\",\r\n                color: \"#a8254e\",\r\n                segments: [[181, 89], [271, 89]]\r\n            },\r\n            \/\/ \u2014 Territorial Cooperation : famille or \/ ambre \u2014\r\n            {\r\n                mode: \"category\",\r\n                group: \"Territorial Cooperation\",\r\n                label: \"URBACT IV \/ City-to-City \/ EUI\",\r\n                color: \"#d4a14c\",\r\n                segments: [[1, 89], [91, 89], [181, 89], [271, 89]]\r\n            },\r\n            \/\/ \u2014 Structural Funds : famille turquoise (du fonc\u00e9 au clair) \u2014\r\n            {\r\n                \/\/ FSE+, FAMI et INTERREG couvrent les deux m\u00eames quadrants.\r\n                mode: \"category\",\r\n                group: \"Structural Funds\",\r\n                label: \"ESF+ \/ AMIF \/ INTERREG\",\r\n                color: \"#0d6f81\",\r\n                segments: [[1, 89], [271, 89]]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Structural Funds\",\r\n                label: \"LIFE\",\r\n                color: \"#1c95a6\",\r\n                segments: [[1, 89]]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Structural Funds\",\r\n                label: \"ERDF \/ EAFRD\",\r\n                color: \"#34b5c4\",\r\n                segments: [[271, 89]]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Structural Funds\",\r\n                label: \"EU4Health\",\r\n                color: \"#62cdd8\",\r\n                segments: [[1, 89], [91, 89]]\r\n            },\r\n            {\r\n                mode: \"category\",\r\n                group: \"Structural Funds\",\r\n                label: \"Horizon Europe\",\r\n                color: \"#95dee6\",\r\n                segments: [[1, 89], [91, 89], [181, 89], [271, 89]]\r\n            }\r\n        ];\r\n\r\n        \/\/ ------------------------------------------------------------------\r\n        \/\/ EXAMPLE PROJECT \u2014 Action Bomaye\r\n        \/\/ The French wheel plots the whole incubated portfolio (loaded from a\r\n        \/\/ Google Sheet). This English edition carries a single worked example\r\n        \/\/ instead, so a partner reads one complete story rather than a\r\n        \/\/ directory. Figures below come from the SHL mapping sheet and the two\r\n        \/\/ Erasmus+ project records.\r\n        \/\/ ------------------------------------------------------------------\r\n        const exampleProject = {\r\n            name: \"Action Bomaye\",\r\n            \/\/ Where it sits on the wheel \u2014 must match the labels in D above.\r\n            domain: \"Culture & Territorial Creativity\",\r\n            item: \"Artistic Projects\",\r\n            sub: \"art & neighbourhoods projects\",\r\n            monogram: \"AB\",\r\n            city: \"Marseille, France\",\r\n            territory: \"Aix-Marseille-Provence Metropolis\",\r\n            audience: \"Young people from priority neighbourhoods, NEETs and young migrants\",\r\n            incubated: \"Incubated by Social Hackers Lab\",\r\n            website: \"https:\/\/action-bomaye.fr\/\",\r\n            paragraphs: [\r\n                \"Action Bomaye gives marginalised young people a voice through film. Short-film workshops take them through writing, acting, directing and public screenings \u2014 building creative and technical skills, and the confidence that comes with telling your own story in front of an audience.\",\r\n                \"The 2025 phase extends that method into immersive technology: 3D and virtual reality, used both as a craft and as a way into the jobs of the future. Skills are recognised through digital badges, so what young people gain is documented and portable.\"\r\n            ],\r\n            \/\/ Two consecutive Erasmus+ grants: the small-scale partnership acted\r\n            \/\/ as the proof of concept for the larger cooperation partnership.\r\n            grants: [\r\n                {\r\n                    year: \"2024\",\r\n                    programme: \"Erasmus+ \u2014 Small-scale partnership\",\r\n                    reference: \"KA210-YOU \u00b7 2024-1-FR02-KA210-YOU-000245785\",\r\n                    partners: \"Partner country: Greece\",\r\n                    amount: 60000,\r\n                    amountLabel: \"\u20ac60,000\",\r\n                    url: \"https:\/\/erasmus-plus.ec.europa.eu\/projects\/search\/details\/2024-1-FR02-KA210-YOU-000245785\"\r\n                },\r\n                {\r\n                    year: \"2025\",\r\n                    programme: \"Erasmus+ \u2014 Cooperation partnership\",\r\n                    reference: \"KA220-YOU \u00b7 2025-2-FR02-KA220-YOU-000387407\",\r\n                    partners: \"Partner countries: Spain, Greece, Italy\",\r\n                    amount: 250000,\r\n                    amountLabel: \"\u20ac250,000\",\r\n                    url: \"https:\/\/erasmus-plus.ec.europa.eu\/projects\/search\/details\/2025-2-FR02-KA220-YOU-000387407\"\r\n                }\r\n            ]\r\n        };\r\n\r\n        \/\/ 2. STATE\r\n        let selectedNode = null;\r\n        let fundingMode = \"none\";\r\n        \/\/ The example marker starts visible: it is the point of this edition.\r\n        let showExample = true;\r\n        let exampleOpen = false;\r\n        const activeFundingCategories = {\r\n            \"Sectoral Programmes\": false,\r\n            \"Territorial Cooperation\": false,\r\n            \"Structural Funds\": 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                \/\/ v5 \u2014 La roue garde quasiment toute sa taille (le contenu\r\n                \/\/ int\u00e9rieur reste lisible) : on conserve la m\u00eame r\u00e9serve qu'avant.\r\n                \/\/ Ce sont les anneaux qui deviennent plus \u00e9pais en remplissant\r\n                \/\/ toute cette couronne (cf. fundingRingData), et non la roue qui\r\n                \/\/ r\u00e9tr\u00e9cit.\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        \/\/ Example marker sits on top of everything else on the wheel.\r\n        const exampleGroup = g.append(\"g\").attr(\"class\", \"example\");\r\n\r\n        \/\/ Resolve the wheel node the example belongs to. If the taxonomy labels\r\n        \/\/ ever drift, this returns null and the marker simply never renders \u2014\r\n        \/\/ the wheel itself keeps working.\r\n        const exampleNode = root.descendants().find(d =>\r\n            d.depth === 3 &&\r\n            d.data.name === exampleProject.sub &&\r\n            d.parent.data.name === exampleProject.item &&\r\n            d.parent.parent.data.name === exampleProject.domain\r\n        ) || null;\r\n\r\n        const exampleColor = exampleNode ? exampleNode.parent.parent.data.color : \"#74518d\";\r\n\r\n        let exampleMarkerNode = null;\r\n        if (exampleNode) {\r\n            const marker = exampleGroup.append(\"g\")\r\n                .attr(\"class\", \"example-marker\")\r\n                .attr(\"role\", \"button\")\r\n                .attr(\"tabindex\", \"0\")\r\n                .attr(\"aria-label\", `${exampleProject.name} \u2014 open project details`)\r\n                .style(\"opacity\", 0)\r\n                .style(\"pointer-events\", \"none\");\r\n            const inner = marker.append(\"g\").attr(\"class\", \"example-marker-inner\");\r\n            inner.append(\"circle\")\r\n                .attr(\"class\", \"example-marker-halo\")\r\n                .attr(\"stroke\", exampleColor);\r\n            inner.append(\"circle\")\r\n                .attr(\"class\", \"example-marker-circle\")\r\n                .attr(\"fill\", exampleColor);\r\n            inner.append(\"text\")\r\n                .attr(\"class\", \"example-marker-mono\")\r\n                .text(exampleProject.monogram);\r\n            marker\r\n                .on(\"mouseover\", event => {\r\n                    tooltip.style.opacity = 1;\r\n                    tooltip.innerHTML = `<strong>${escapeHTML(exampleProject.name)}<\/strong><br>${escapeHTML(exampleProject.sub)}`;\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                .on(\"click\", event => {\r\n                    event.stopPropagation();\r\n                    openExamplePanel();\r\n                })\r\n                .on(\"keydown\", event => {\r\n                    if (event.key !== \"Enter\" && event.key !== \" \") return;\r\n                    event.preventDefault();\r\n                    openExamplePanel();\r\n                });\r\n            exampleMarkerNode = marker.node();\r\n        }\r\n\r\n        function exampleMarkerRadius() {\r\n            const tier = screenTier(width, height);\r\n            return tier === \"compactPhone\" ? 12 : tier === \"phone\" ? 14 : 17;\r\n        }\r\n\r\n        function updateExampleMarker(box) {\r\n            if (!exampleMarkerNode || !exampleNode) return;\r\n            const cur = box || exampleNode.current || exampleNode;\r\n            const angle = (cur.x0 + cur.x1) \/ 2;\r\n            const zoomed = Boolean(selectedNode);\r\n            \/\/ Zoomed out the wheel already fills its box, so the marker rides\r\n            \/\/ inside the outer band; zoomed in there is room to sit outside it,\r\n            \/\/ which reads better against the enlarged labels.\r\n            const offset = isCompactLayout(width) ? 14 : 22;\r\n            const r = zoomed\r\n                ? (cur.y1 != null ? cur.y1 : exampleNode.y1) + offset\r\n                : ((cur.y0 + cur.y1) \/ 2);\r\n            const x = Math.sin(angle) * r;\r\n            const y = -Math.cos(angle) * r;\r\n            exampleMarkerNode.setAttribute(\"transform\", `translate(${x},${y})`);\r\n\r\n            const rad = exampleMarkerRadius();\r\n            const circle = exampleMarkerNode.querySelector(\".example-marker-circle\");\r\n            const halo = exampleMarkerNode.querySelector(\".example-marker-halo\");\r\n            const mono = exampleMarkerNode.querySelector(\".example-marker-mono\");\r\n            if (circle) circle.setAttribute(\"r\", rad);\r\n            if (halo) halo.setAttribute(\"r\", rad);\r\n            if (mono) mono.style.fontSize = Math.round(rad * 0.78) + \"px\";\r\n        }\r\n\r\n        function isExampleVisible(box) {\r\n            if (!exampleMarkerNode || !exampleNode || !showExample) return false;\r\n            if (isFundingSelectionActive()) return false;\r\n            const cur = box || exampleNode.current || exampleNode;\r\n            return cur.visible !== false;\r\n        }\r\n\r\n        \/\/ pop === true replays the scale-in animation (used after a zoom or a\r\n        \/\/ toggle); otherwise the marker just snaps to its new place.\r\n        function refreshExampleMarker(pop) {\r\n            if (!exampleMarkerNode || !exampleNode) return;\r\n            const show = isExampleVisible();\r\n            const inner = exampleMarkerNode.querySelector(\".example-marker-inner\");\r\n            exampleMarkerNode.style.opacity = show ? 1 : 0;\r\n            exampleMarkerNode.style.pointerEvents = show ? \"all\" : \"none\";\r\n            if (show) updateExampleMarker();\r\n            if (!inner) return;\r\n            if (!show) {\r\n                inner.classList.remove(\"popped\");\r\n                return;\r\n            }\r\n            if (pop) {\r\n                \/\/ Force a synchronous style flush rather than waiting on rAF:\r\n                \/\/ in a background tab rAF is throttled and the marker would sit\r\n                \/\/ at scale(0) until the tab is looked at again.\r\n                inner.classList.remove(\"popped\");\r\n                void inner.getBoundingClientRect();\r\n            }\r\n            inner.classList.add(\"popped\");\r\n        }\r\n\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            \/\/ v5 \u2014 Anneaux \u00e9pais : les noms sont dans la liste lat\u00e9rale\r\n            \/\/ (#funding-legend), donc chaque trait peut remplir toute la couronne\r\n            \/\/ r\u00e9serv\u00e9e. On laisse l'\u00e9paisseur grandir jusqu'\u00e0 un plafond g\u00e9n\u00e9reux\r\n            \/\/ pour des traits bien visibles, sans qu'ils deviennent disproportionn\u00e9s.\r\n            const ringThickness = compact\r\n                ? Math.max(4, Math.min(screenTier(width, height) === \"phone\" ? 9 : 12, fitThickness))\r\n                : Math.max(6, Math.min(20, 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 = [\"Sectoral Programmes\", \"Territorial Cooperation\", \"Structural Funds\"];\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.95 : 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\", \"Back\")\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(\"Back\");\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(\"Back\");\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 ? \"Back\" : \"Overview\");\r\n            centerControl.select(\"title\").text(showBack ? \"Back\" : \"Overview\");\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        refreshExampleMarker(true);\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            \/\/ Hide the example marker for the length of the zoom, then pop it\r\n            \/\/ back in on its new arc once the geometry has settled.\r\n            exampleGroup.style('opacity', 0);\r\n            if (exampleMarkerNode) {\r\n                exampleMarkerNode.style.pointerEvents = \"none\";\r\n                const innerEl = exampleMarkerNode.querySelector(\".example-marker-inner\");\r\n                if (innerEl) innerEl.classList.remove(\"popped\");\r\n            }\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                    exampleGroup.style('opacity', 1);\r\n                    refreshExampleMarker(true);\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            refreshExampleMarker(false);\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 = 'Choose a category';\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                refreshExampleMarker(true);\r\n            }\r\n        });\r\n\r\n        \/\/ ------------------------------------------------------------------\r\n        \/\/ EXAMPLE PANEL\r\n        \/\/ ------------------------------------------------------------------\r\n        const examplePanel = document.getElementById('example-panel');\r\n        const examplePanelBody = document.getElementById('example-panel-body');\r\n        const examplePanelClose = document.getElementById('example-panel-close');\r\n        const exampleToggle = document.getElementById('example-toggle');\r\n        const successCard = document.getElementById('success-story-card');\r\n\r\n        function renderExamplePanel() {\r\n            const p = exampleProject;\r\n            const total = p.grants.reduce((sum, grant) => sum + (grant.amount || 0), 0);\r\n            const totalLabel = \"\u20ac\" + total.toLocaleString(\"en-GB\");\r\n            const websiteDisplay = p.website.replace(\/^https?:\\\/\\\/\/i, \"\").replace(\/\\\/$\/, \"\");\r\n\r\n            const grants = p.grants.map(grant => `\r\n                <a class=\"example-grant\" href=\"${escapeHTML(grant.url)}\" target=\"_blank\" rel=\"noopener noreferrer\">\r\n                    <span class=\"example-grant-head\">\r\n                        <span class=\"example-grant-year\">${escapeHTML(grant.year)}<\/span>\r\n                        <span class=\"example-grant-amount\">${escapeHTML(grant.amountLabel)}<\/span>\r\n                    <\/span>\r\n                    <span class=\"example-grant-name\">${escapeHTML(grant.programme)}<\/span><br>\r\n                    <span class=\"example-grant-sub\">${escapeHTML(grant.partners)}<br>${escapeHTML(grant.reference)}<\/span>\r\n                    <span class=\"example-grant-link\">Open the EU project record \u2197<\/span>\r\n                <\/a>\r\n            `).join(\"\");\r\n\r\n            const paragraphs = p.paragraphs\r\n                .map(text => `<p class=\"example-desc\">${escapeHTML(text)}<\/p>`)\r\n                .join(\"\");\r\n\r\n            examplePanelBody.innerHTML = `\r\n                <span class=\"example-tag\" style=\"background-color: ${escapeHTML(exampleColor)}\">${escapeHTML(p.domain)}<\/span>\r\n                <h2 class=\"example-title\">${escapeHTML(p.name)}<\/h2>\r\n                <div class=\"example-path\">\r\n                    <span>${escapeHTML(p.item)}<\/span>\r\n                    <span class=\"sep\">\u2022<\/span>\r\n                    <span>${escapeHTML(p.sub)}<\/span>\r\n                <\/div>\r\n                <dl class=\"example-meta\">\r\n                    <div><dt>Based in<\/dt><dd>${escapeHTML(p.city)}<\/dd><\/div>\r\n                    <div><dt>Territory<\/dt><dd>${escapeHTML(p.territory)}<\/dd><\/div>\r\n                    <div><dt>Target group<\/dt><dd>${escapeHTML(p.audience)}<\/dd><\/div>\r\n                    <div><dt>Support<\/dt><dd>${escapeHTML(p.incubated)}<\/dd><\/div>\r\n                <\/dl>\r\n                <h3 class=\"example-section-title\">The project<\/h3>\r\n                ${paragraphs}\r\n                <h3 class=\"example-section-title\">EU funding secured<\/h3>\r\n                <div class=\"example-total\">\r\n                    <span class=\"amount\">${escapeHTML(totalLabel)}<\/span>\r\n                    <span class=\"caption\">across ${p.grants.length} Erasmus+ grants, ${escapeHTML(p.grants[0].year)}\u2013${escapeHTML(p.grants[p.grants.length - 1].year)}<\/span>\r\n                <\/div>\r\n                ${grants}\r\n                <h3 class=\"example-section-title\">Find out more<\/h3>\r\n                <a class=\"example-contact\" href=\"${escapeHTML(p.website)}\" target=\"_blank\" rel=\"noopener noreferrer\">\r\n                    <span>\r\n                        <span class=\"label\">Website<\/span>\r\n                        <span class=\"value\">${escapeHTML(websiteDisplay)}<\/span>\r\n                    <\/span>\r\n                    <span class=\"chev\">\u2192<\/span>\r\n                <\/a>\r\n            `;\r\n        }\r\n\r\n        function openExamplePanel() {\r\n            if (!examplePanel) return;\r\n            if (!examplePanelBody.childElementCount) renderExamplePanel();\r\n            exampleOpen = true;\r\n            examplePanel.classList.add('open');\r\n            examplePanel.setAttribute('aria-hidden', 'false');\r\n            tooltip.style.opacity = 0;\r\n            examplePanelClose.focus();\r\n        }\r\n\r\n        function closeExamplePanel() {\r\n            if (!examplePanel) return;\r\n            exampleOpen = false;\r\n            examplePanel.classList.remove('open');\r\n            examplePanel.setAttribute('aria-hidden', 'true');\r\n        }\r\n\r\n        examplePanelClose.addEventListener('click', closeExamplePanel);\r\n        successCard.addEventListener('click', openExamplePanel);\r\n        document.addEventListener('keydown', event => {\r\n            if (event.key === 'Escape' && exampleOpen) closeExamplePanel();\r\n        });\r\n\r\n        \/\/ Toggle: hides both the wheel marker and the corner card, so the wheel\r\n        \/\/ can be shown on its own when the example is not the point.\r\n        exampleToggle.addEventListener('click', () => {\r\n            showExample = !showExample;\r\n            exampleToggle.classList.toggle('active', showExample);\r\n            exampleToggle.setAttribute('aria-checked', showExample ? 'true' : 'false');\r\n            successCard.style.display = showExample ? '' : 'none';\r\n            if (!showExample) closeExamplePanel();\r\n            refreshExampleMarker(true);\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-23813964","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Mapping of projects and funding - 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\/mapping-of-projects-and-funding\/\" \/>\n<meta property=\"og:locale\" content=\"en_GB\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Mapping of projects and funding - Social Hackers Lab\" \/>\n<meta property=\"og:url\" content=\"https:\/\/socialhackerslab.com\/en\/mapping-of-projects-and-funding\/\" \/>\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-08-28T13:46:23+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/socialhackerslab.com\/wp-content\/uploads\/2024\/10\/Social-Hacker-Lab-1.png\" \/>\n\t<meta property=\"og:image:width\" content=\"250\" \/>\n\t<meta property=\"og:image:height\" content=\"140\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/mapping-of-projects-and-funding\\\/\",\"url\":\"https:\\\/\\\/socialhackerslab.com\\\/mapping-of-projects-and-funding\\\/\",\"name\":\"Mapping of projects and funding - Social Hackers Lab\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/#website\"},\"datePublished\":\"2026-08-28T13:46:22+00:00\",\"dateModified\":\"2026-08-28T13:46:23+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/mapping-of-projects-and-funding\\\/#breadcrumb\"},\"inLanguage\":\"en-GB\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/socialhackerslab.com\\\/mapping-of-projects-and-funding\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/socialhackerslab.com\\\/mapping-of-projects-and-funding\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Accueil\",\"item\":\"https:\\\/\\\/socialhackerslab.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Mapping of projects and funding\"}]},{\"@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":"Mapping of projects and funding - 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\/mapping-of-projects-and-funding\/","og_locale":"en_GB","og_type":"article","og_title":"Mapping of projects and funding - Social Hackers Lab","og_url":"https:\/\/socialhackerslab.com\/en\/mapping-of-projects-and-funding\/","og_site_name":"Social Hackers Lab","article_publisher":"https:\/\/www.facebook.com\/profile.php?id=61561806211570","article_modified_time":"2026-08-28T13:46:23+00:00","og_image":[{"width":250,"height":140,"url":"https:\/\/socialhackerslab.com\/wp-content\/uploads\/2024\/10\/Social-Hacker-Lab-1.png","type":"image\/png"}],"twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/socialhackerslab.com\/mapping-of-projects-and-funding\/","url":"https:\/\/socialhackerslab.com\/mapping-of-projects-and-funding\/","name":"Mapping of projects and funding - Social Hackers Lab","isPartOf":{"@id":"https:\/\/socialhackerslab.com\/#website"},"datePublished":"2026-08-28T13:46:22+00:00","dateModified":"2026-08-28T13:46:23+00:00","breadcrumb":{"@id":"https:\/\/socialhackerslab.com\/mapping-of-projects-and-funding\/#breadcrumb"},"inLanguage":"en-GB","potentialAction":[{"@type":"ReadAction","target":["https:\/\/socialhackerslab.com\/mapping-of-projects-and-funding\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/socialhackerslab.com\/mapping-of-projects-and-funding\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Accueil","item":"https:\/\/socialhackerslab.com\/"},{"@type":"ListItem","position":2,"name":"Mapping of projects and funding"}]},{"@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\/23813964","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=23813964"}],"version-history":[{"count":1,"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/pages\/23813964\/revisions"}],"predecessor-version":[{"id":23813965,"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/pages\/23813964\/revisions\/23813965"}],"wp:attachment":[{"href":"https:\/\/socialhackerslab.com\/en\/wp-json\/wp\/v2\/media?parent=23813964"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}