{"id":12741,"date":"2026-07-13T16:14:37","date_gmt":"2026-07-13T16:14:37","guid":{"rendered":"https:\/\/www.justhits.fr\/?page_id=12741"},"modified":"2026-07-17T05:59:22","modified_gmt":"2026-07-17T05:59:22","slug":"player","status":"publish","type":"page","link":"https:\/\/www.justhits.fr\/","title":{"rendered":"Player Just Hits"},"content":{"rendered":"    <div\n        id=\"justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2\"\n        class=\"jh4-player\"\n        style=\"--jh-theme-bg:#06251f;--jh-theme-orb-1-rgb:255,181,46;--jh-theme-orb-2-rgb:0,227,165;--jh-theme-overlay-1-rgb:255,123,77;--jh-theme-overlay-2-rgb:0,217,181;--jh-theme-overlay-mid-rgb:11,59,50;--jh-theme-overlay-bottom-rgb:3,22,16;--jh-theme-grid-1-rgb:99,255,211;--jh-theme-grid-2-rgb:255,194,75;--jh-theme-orb-opacity:0.3;--jh-theme-blur:90px;--jh-theme-image-opacity:0.46;--jh-theme-image-brightness:0.64;--jh-theme-image-saturation:1.8;--jh-theme-grid-opacity:0.1\"\n        data-appearance=\"tropical\"\n        data-api=\"https:\/\/www.justhits.fr\/index.php?rest_route=\/justhits\/v4\/player\"\n        data-ranking-enabled=\"1\"\n        data-vote-api=\"https:\/\/www.justhits.fr\/index.php?rest_route=\/justhits\/v4\/vote\"\n        data-bootstrap=\"https:\/\/www.justhits.fr\/index.php?rest_route=\/justhits\/v4\/bootstrap\"\n        data-bootstrap-token=\"1788125315.f00c4aa52da88d55ac6a836629fc9bf06a35f1eff7143e5f146ff6967334d005\"\n        data-refresh=\"12\"\n    >\n        <div class=\"jh4-launch-overlay\" role=\"dialog\" aria-label=\"\u00c9couter Just Hits\">\n            <div class=\"jh4-launch-card\">\n                <img\n                    class=\"jh4-launch-logo\"\n                    alt=\"Just Hits\"\n                    data-jh-secure-logo=\"transparent\"\n                >\n\n                <div class=\"jh4-launch-kicker\">EN DIRECT<\/div>\n\n                <button\n                    type=\"button\"\n                    class=\"jh4-launch-play\"\n                    aria-label=\"Lancer la radio Just Hits\"\n                >\n                    <span>\u25b6<\/span>\n                <\/button>\n\n                <strong>\u00c9COUTER JUST HITS<\/strong>\n                <small>Appuie sur lecture pour lancer la radio<\/small>\n            <\/div>\n        <\/div>\n\n        <div class=\"jh4-tooltip-portal\" role=\"tooltip\" aria-hidden=\"true\"><\/div>\n\n        <audio class=\"jh4-audio jh4-audio-a\" preload=\"auto\" playsinline><\/audio>\n        <audio class=\"jh4-audio jh4-audio-b\" preload=\"auto\" playsinline><\/audio>\n        <audio class=\"jh4-audio jh4-audio-c\" preload=\"auto\" playsinline><\/audio>\n\n        <div class=\"jh4-stream-switch-overlay\" aria-hidden=\"true\">\n            <div class=\"jh4-stream-switch-card\">\n                Changement de programme\u2026\n            <\/div>\n        <\/div>\n\n        <div class=\"jh4-bg\">\n            <img class=\"jh4-bg-image\" alt=\"\">\n            <div class=\"jh4-bg-orb jh4-bg-orb-pink\"><\/div>\n            <div class=\"jh4-bg-orb jh4-bg-orb-blue\"><\/div>\n            <div class=\"jh4-bg-overlay\"><\/div>\n            <div class=\"jh4-grid\"><\/div>\n        <\/div>\n\n        <div class=\"jh4-shell\">\n            <header class=\"jh4-header\">\n                <div class=\"jh4-live\"><i><\/i><span>EN DIRECT<\/span><\/div>\n\n                <div class=\"jh4-brand\">\n                    <img\n                        alt=\"Just Hits\"\n                        data-jh-secure-logo=\"transparent\"\n                    >\n                <\/div>\n\n                <div class=\"jh4-theme-switcher\" role=\"group\" aria-label=\"Choisir le th\u00e8me du player\">\n                    <button type=\"button\" class=\"jh4-theme-button\" data-theme-mode=\"system\" aria-label=\"Th\u00e8me syst\u00e8me\" title=\"Th\u00e8me syst\u00e8me\">\n                        <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n                            <rect x=\"3\" y=\"4\" width=\"18\" height=\"13\" rx=\"2\"><\/rect>\n                            <path d=\"M8 21h8M12 17v4\"><\/path>\n                        <\/svg>\n                    <\/button>\n                    <button type=\"button\" class=\"jh4-theme-button\" data-theme-mode=\"light\" aria-label=\"Th\u00e8me clair\" title=\"Th\u00e8me clair\">\n                        <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n                            <circle cx=\"12\" cy=\"12\" r=\"4\"><\/circle>\n                            <path d=\"M12 2v2M12 20v2M4.93 4.93l1.42 1.42M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.42-1.42M17.66 6.34l1.41-1.41\"><\/path>\n                        <\/svg>\n                    <\/button>\n                    <button type=\"button\" class=\"jh4-theme-button\" data-theme-mode=\"dark\" aria-label=\"Th\u00e8me sombre\" title=\"Th\u00e8me sombre\">\n                        <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n                            <path d=\"M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z\"><\/path>\n                        <\/svg>\n                    <\/button>\n                <\/div>\n\n                <div class=\"jh4-header-volume-wrap\">\n                    <button class=\"jh4-icon-button jh4-mute\" type=\"button\" aria-label=\"Couper ou r\u00e9activer le son\">\n                        <svg class=\"jh4-volume-svg\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n                            <path class=\"jh4-speaker\" d=\"M4 9v6h4l5 4V5L8 9H4z\"><\/path>\n                            <path class=\"jh4-wave-one\" d=\"M16 8.5c1.2 1.2 1.2 5.8 0 7\"><\/path>\n                            <path class=\"jh4-wave-two\" d=\"M19 6c3 3 3 9 0 12\"><\/path>\n                            <path class=\"jh4-muted-x\" d=\"M17 9l5 5m0-5l-5 5\"><\/path>\n                        <\/svg>\n                    <\/button>\n\n                    <div class=\"jh4-header-volume-panel\" aria-label=\"R\u00e9glage du volume\">\n                        <input\n                            type=\"range\"\n                            class=\"jh4-header-volume-slider\"\n                            min=\"0\"\n                            max=\"1\"\n                            step=\"0.01\"\n                            value=\"0.8\"\n                            aria-label=\"Volume\"\n                        >\n                    <\/div>\n                <\/div>\n            <\/header>\n\n            <main class=\"jh4-main\">\n                <section class=\"jh4-current\">\n                    <div class=\"jh4-cover-zone\">\n                        <div class=\"jh4-cover-glow\"><\/div>\n                        <div class=\"jh4-cover-stack\" aria-hidden=\"true\">\n                            <div class=\"jh4-cover-ghost jh4-cover-ghost-1\">\n                                <img alt=\"\">\n                            <\/div>\n                            <div class=\"jh4-cover-ghost jh4-cover-ghost-2\">\n                                <img alt=\"\">\n                            <\/div>\n                            <div class=\"jh4-cover-ghost jh4-cover-ghost-3\">\n                                <img alt=\"\">\n                            <\/div>\n                        <\/div>\n\n                        <div class=\"jh4-cover-box\">\n                            <img class=\"jh4-logo-fallback\" data-jh-secure-logo=\"standard\" alt=\"Just Hits\">\n                            <img class=\"jh4-current-cover\" alt=\"Pochette du titre en cours\">\n\n                            <button class=\"jh4-play jh4-cover-play\" type=\"button\" aria-label=\"\u00c9couter la radio\">\n                                <span class=\"jh4-play-symbol\">\u25b6<\/span>\n                            <\/button>\n\n                            <div class=\"jh4-bars\"><i><\/i><i><\/i><i><\/i><i><\/i><i><\/i><\/div>\n                        <\/div>\n                    <\/div>\n\n                    <div class=\"jh4-current-copy\">\n                        <h1 class=\"jh4-title jh4-tooltip-title\">Chargement du titre\u2026<\/h1>\n                        <div class=\"jh4-artist\">JUST HITS<\/div>\n                        <div class=\"jh4-album\"><\/div>\n\n                        <div\n                            class=\"jh4-current-vote\"\n                            aria-label=\"Voter pour le titre en cours\"\n                        >\n                                                            <button\n                                    type=\"button\"\n                                    class=\"jh4-current-vote-button jh4-current-vote-like\"\n                                    aria-label=\"J'aime ce titre\"\n                                >\n                                    <span class=\"jh4-heart-icon\">\u2665<\/span>\n                                <\/button>\n\n                                <button\n                                    type=\"button\"\n                                    class=\"jh4-current-vote-button jh4-current-vote-dislike\"\n                                    aria-label=\"Je n'aime pas ce titre\"\n                                >\n                                    <span class=\"jh4-broken-heart-icon\">\ud83d\udc94<\/span>\n                                <\/button>\n                                                    <\/div>\n\n                        <div class=\"jh4-progress-wrap\">\n                            <div class=\"jh4-progress\"><b><\/b><i><\/i><\/div>\n                            <div class=\"jh4-times\"><span class=\"jh4-elapsed\">EN DIRECT<\/span><span class=\"jh4-duration\">LIVE<\/span><\/div>\n                        <\/div>\n\n                        <div class=\"jh4-controls\">\n\n                            <div class=\"jh4-state\"><i><\/i><span>Radio en pause<\/span><\/div>\n                            <div class=\"jh4-request-kpi\" hidden>\n                                <svg class=\"jh4-request-kpi-icon\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n                                    <path d=\"M4 14v-2a8 8 0 0 1 16 0v2\"><\/path>\n                                    <path d=\"M4 14a2 2 0 0 1 2-2h1v7H6a2 2 0 0 1-2-2z\"><\/path>\n                                    <path d=\"M20 14a2 2 0 0 0-2-2h-1v7h1a2 2 0 0 0 2-2z\"><\/path>\n                                <\/svg>\n                                <span>Demand\u00e9 par<\/span> <strong><\/strong>\n                            <\/div>\n                        <\/div>\n\n                        <div class=\"jh4-error\" hidden><\/div>\n                    <\/div>\n                <\/section>\n\n                <aside class=\"jh4-next\">\n                    <div class=\"jh4-next-heading\">\n                        <span>PROCHAIN TITRE<\/span>\n                        <b>\u00c0 SUIVRE<\/b>\n                    <\/div>\n\n                    <div class=\"jh4-next-card\">\n                        <div class=\"jh4-next-cover-wrap\">\n                            <img class=\"jh4-next-fallback\" data-jh-secure-logo=\"standard\" alt=\"\">\n                            <img class=\"jh4-next-cover\" alt=\"\">\n                        <\/div>\n\n                        <div class=\"jh4-next-copy\">\n                            <small>JUSTE APR\u00c8S<\/small>\n\n                            <div class=\"jh4-next-title-wrap\">\n                                <strong class=\"jh4-next-title jh4-tooltip-title\" data-tooltip-container=\"parent\">Chargement\u2026<\/strong>\n                            <\/div>\n\n                            <span class=\"jh4-next-artist\">JUST HITS<\/span>\n                            <span class=\"jh4-next-requester\" hidden><\/span>\n                        <\/div>\n\n                        <div class=\"jh4-next-arrow\">\u203a<\/div>\n                    <\/div>\n\n                    <div class=\"jh4-next-pulse\"><i><\/i><span>Programm\u00e9 sur Just Hits<\/span><\/div>\n                <\/aside>\n            <\/main>\n\n            <section class=\"jh4-history\">\n                <div class=\"jh4-history-heading\">\n                    <strong>D\u00c9J\u00c0 DIFFUS\u00c9S<\/strong>\n                    <span>Les derniers titres<\/span>\n                <\/div>\n                <div class=\"jh4-history-grid\">\n                    <div class=\"jh4-loading\">Chargement des derniers titres\u2026<\/div>\n                <\/div>\n            <\/section>\n        <\/div>\n\n        <div\n            class=\"jh4-connection-strength level-3\"\n            role=\"status\"\n            aria-live=\"polite\"\n            aria-label=\"Connexion correcte pour \u00e9couter la radio\"\n            data-tooltip=\"Connexion suffisante\"\n            tabindex=\"0\"\n        >\n            <span class=\"jh4-connection-bars\" aria-hidden=\"true\">\n                <i><\/i><i><\/i><i><\/i><i><\/i>\n            <\/span>\n            <strong>Connexion correcte<\/strong>\n        <\/div>\n    <\/div>\n\n    <style>\n    html:has(#justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2), body:has(#justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2) {\n        margin: 0 !important;\n        min-height: 100%;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 {\n        --pink:#ff008c;\n        --purple:#6f26ff;\n        --blue:#008cff;\n        --cyan:#00dcff;\n        --navy:#02031a;\n        --jh-bg:var(\n            --jh-theme-bg,\n            #02031a\n        );\n        --jh-theme-bg:#02031a;\n        --jh-theme-orb-1-rgb:255,0,140;\n        --jh-theme-orb-2-rgb:0,220,255;\n        --jh-theme-overlay-1-rgb:255,0,140;\n        --jh-theme-overlay-2-rgb:0,220,255;\n        --jh-theme-overlay-mid-rgb:2,3,26;\n        --jh-theme-overlay-bottom-rgb:1,2,20;\n        --jh-theme-grid-1-rgb:0,220,255;\n        --jh-theme-grid-2-rgb:255,0,140;\n        --jh-theme-orb-opacity:.24;\n        --jh-theme-blur:95px;\n        --jh-theme-image-opacity:.50;\n        --jh-theme-image-brightness:.58;\n        --jh-theme-image-saturation:1.55;\n        --jh-theme-grid-opacity:.15;\n        --jh-panel:rgba(2,3,26,.80);\n        --jh-panel-strong:rgba(2,3,26,.92);\n        --jh-card:rgba(255,255,255,.055);\n        --jh-card-hover:rgba(255,255,255,.085);\n        --jh-text:#ffffff;\n        --jh-text-soft:rgba(255,255,255,.66);\n        --jh-border:rgba(255,255,255,.10);\n        position:relative;\n        width:100vw;\n        min-height:100svh;\n        margin-left:calc(50% - 50vw);\n        overflow:hidden;\n        color:var(--jh-text);\n        border-radius:0;\n        background:var(--jh-bg);\n        font-family:Inter,Montserrat,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;\n        isolation:isolate;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 *,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 *::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 *::after { box-sizing:border-box; }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 img {\n        -webkit-user-drag:none;\n        user-select:none;\n        -webkit-user-select:none;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 button,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 input { font:inherit; }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength {\n        position:fixed;\n        right:max(16px,env(safe-area-inset-right));\n        bottom:max(16px,env(safe-area-inset-bottom));\n        z-index:80;\n        display:flex;\n        align-items:center;\n        justify-content:center;\n        width:42px;\n        height:42px;\n        min-height:42px;\n        padding:8px;\n        border:1px solid rgba(255,255,255,.14);\n        border-radius:999px;\n        background:rgba(2,3,26,.82);\n        color:#fff;\n        box-shadow:0 12px 34px rgba(0,0,0,.32);\n        backdrop-filter:blur(14px);\n        -webkit-backdrop-filter:blur(14px);\n        cursor:help;\n        pointer-events:auto;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-bars {\n        display:flex;\n        align-items:flex-end;\n        gap:2px;\n        width:22px;\n        height:18px;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-bars i {\n        width:4px;\n        border-radius:3px 3px 1px 1px;\n        background:rgba(255,255,255,.20);\n        transition:background .2s ease,box-shadow .2s ease;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-bars i:nth-child(1){height:5px}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-bars i:nth-child(2){height:9px}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-bars i:nth-child(3){height:13px}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-bars i:nth-child(4){height:17px}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength strong {\n        display:none;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength::before {\n        content:attr(data-tooltip);\n        position:absolute;\n        right:0;\n        bottom:calc(100% + 14px);\n        width:max-content;\n        max-width:min(250px,calc(100vw - 32px));\n        padding:12px 15px;\n        border:1px solid rgba(255,255,255,.10);\n        border-radius:17px;\n        background:#03041d;\n        color:#fff;\n        box-shadow:0 16px 38px rgba(0,0,0,.38);\n        font-size:13px;\n        font-weight:850;\n        line-height:1.25;\n        white-space:normal;\n        opacity:0;\n        visibility:hidden;\n        pointer-events:none;\n        transform:translateY(7px) scale(.96);\n        transform-origin:bottom right;\n        transition:opacity .18s ease,visibility .18s ease,transform .18s ease;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength::after {\n        content:\"\";\n        position:absolute;\n        right:12px;\n        bottom:calc(100% + 7px);\n        width:14px;\n        height:14px;\n        border-right:1px solid rgba(255,255,255,.08);\n        border-bottom:1px solid rgba(255,255,255,.08);\n        background:#03041d;\n        opacity:0;\n        visibility:hidden;\n        pointer-events:none;\n        transform:rotate(45deg);\n        transition:opacity .18s ease,visibility .18s ease;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength:hover::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength:focus-visible::before {\n        opacity:1;\n        visibility:visible;\n        transform:translateY(0) scale(1);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength:hover::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength:focus-visible::after {\n        opacity:1;\n        visibility:visible;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength.level-1 .jh4-connection-bars i:nth-child(-n+1),\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength.level-2 .jh4-connection-bars i:nth-child(-n+2),\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength.level-3 .jh4-connection-bars i:nth-child(-n+3),\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength.level-4 .jh4-connection-bars i:nth-child(-n+4) {\n        background:#31df83;\n        box-shadow:0 0 8px rgba(49,223,131,.46);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength.level-2 .jh4-connection-bars i:nth-child(-n+2) {\n        background:#ffc43d;\n        box-shadow:0 0 8px rgba(255,196,61,.42);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength.level-1 .jh4-connection-bars i:nth-child(1) {\n        background:#ff596e;\n        box-shadow:0 0 8px rgba(255,89,110,.46);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-connection-strength.level-0 .jh4-connection-bars i {\n        background:#ff596e;\n        opacity:.35;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-overlay {\n        position:fixed;\n        inset:0;\n        z-index:99999;\n        display:flex;\n        align-items:center;\n        justify-content:center;\n        width:100%;\n        min-height:100dvh;\n        padding:\n            max(18px, env(safe-area-inset-top))\n            max(18px, env(safe-area-inset-right))\n            max(18px, env(safe-area-inset-bottom))\n            max(18px, env(safe-area-inset-left));\n        background:rgba(2,3,26,.42);\n        backdrop-filter:blur(9px);\n        -webkit-backdrop-filter:blur(9px);\n        opacity:1;\n        visibility:visible;\n        overflow:auto;\n        overscroll-behavior:contain;\n        transition:opacity .55s ease,visibility .55s ease,backdrop-filter .55s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-overlay.is-hidden {\n        opacity:0;\n        visibility:hidden;\n        pointer-events:none;\n        backdrop-filter:blur(0);\n        -webkit-backdrop-filter:blur(0);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-card {\n        width:min(92vw,520px);\n        max-height:calc(\n            100dvh\n            - max(36px, env(safe-area-inset-top))\n            - max(36px, env(safe-area-inset-bottom))\n        );\n        margin:auto;\n        padding:36px 28px 32px;\n        display:flex;\n        flex-direction:column;\n        align-items:center;\n        justify-content:center;\n        text-align:center;\n        overflow:auto;\n        border:1px solid rgba(255,255,255,.16);\n        border-radius:34px;\n        background:\n            radial-gradient(circle at 25% 20%,rgba(255,0,140,.22),transparent 42%),\n            radial-gradient(circle at 78% 18%,rgba(0,220,255,.18),transparent 42%),\n            rgba(2,3,26,.82);\n        box-shadow:\n            0 30px 90px rgba(0,0,0,.48),\n            inset 0 1px rgba(255,255,255,.08);\n        transform:translateY(0) scale(1);\n        transition:transform .55s cubic-bezier(.2,.8,.2,1),opacity .4s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-overlay.is-hidden .jh4-launch-card {\n        transform:translateY(18px) scale(.94);\n        opacity:0;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-logo {\n        width:min(240px,58vw);\n        height:auto;\n        object-fit:contain;\n        filter:\n            drop-shadow(0 0 18px rgba(255,0,140,.30))\n            drop-shadow(0 0 24px rgba(0,220,255,.22));\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-kicker {\n        margin-top:18px;\n        color:var(--cyan);\n        font-size:12px;\n        font-weight:900;\n        letter-spacing:.18em;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-play {\n        position:relative;\n        display:grid;\n        place-items:center;\n        width:124px;\n        height:124px;\n        margin:24px 0 20px;\n        padding:0;\n        border:2px solid rgba(255,255,255,.30);\n        border-radius:50%;\n        background:linear-gradient(135deg,var(--pink),var(--purple) 50%,var(--cyan));\n        color:#fff;\n        cursor:pointer;\n        box-shadow:\n            0 0 0 0 rgba(255,0,140,.42),\n            0 0 35px rgba(255,0,140,.42),\n            0 0 55px rgba(0,220,255,.28),\n            0 22px 50px rgba(0,0,0,.38);\n        animation:jh4LaunchPulse 1.9s infinite;\n        transition:transform .2s ease,box-shadow .2s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-play:hover {\n        transform:scale(1.06);\n        box-shadow:\n            0 0 0 14px rgba(255,0,140,.08),\n            0 0 44px rgba(255,0,140,.56),\n            0 0 68px rgba(0,220,255,.36),\n            0 24px 56px rgba(0,0,0,.44);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-play span {\n        display:flex;\n        align-items:center;\n        justify-content:center;\n        width:100%;\n        height:100%;\n        padding-left:7px;\n        font-size:46px;\n        line-height:1;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-card > strong {\n        font-size:clamp(24px,5vw,38px);\n        font-weight:950;\n        letter-spacing:-.03em;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-card > small {\n        margin-top:8px;\n        color:rgba(255,255,255,.66);\n        font-size:13px;\n    }\n\n    \/*\n     * Tooltip flottante ind\u00e9pendante.\n     * Les anciennes pseudo-tooltips sont neutralis\u00e9es afin qu\u2019elles\n     * ne soient jamais coup\u00e9es par les titres en overflow:hidden.\n     *\/\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title[data-tooltip]::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title[data-tooltip]::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title[data-tooltip]::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title[data-tooltip]::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong[data-tooltip]::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong[data-tooltip]::after {\n        display:none !important;\n        content:none !important;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 [data-tooltip] {\n        cursor:default;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-tooltip-title {\n        cursor:default;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-tooltip-portal {\n        position:fixed;\n        left:0;\n        top:0;\n        z-index:2147483647;\n        width:max-content;\n        max-width:min(460px,calc(100vw - 28px));\n        padding:12px 15px;\n        border:1px solid rgba(255,255,255,.14);\n        border-radius:14px;\n        background:rgba(2,3,26,.97);\n        color:#fff;\n        box-shadow:\n            0 18px 48px rgba(0,0,0,.42),\n            0 0 24px rgba(0,220,255,.10);\n        backdrop-filter:blur(16px);\n        -webkit-backdrop-filter:blur(16px);\n        font-size:14px;\n        font-weight:750;\n        line-height:1.35;\n        letter-spacing:0;\n        white-space:normal;\n        text-align:left;\n        opacity:0;\n        visibility:hidden;\n        pointer-events:none;\n        transform:translateY(8px) scale(.97);\n        transform-origin:center bottom;\n        transition:\n            opacity .16s ease,\n            visibility .16s ease,\n            transform .16s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-tooltip-portal::after {\n        content:\"\";\n        position:absolute;\n        left:var(--jh4-tooltip-arrow-left,50%);\n        bottom:-7px;\n        width:13px;\n        height:13px;\n        background:inherit;\n        border-right:1px solid rgba(255,255,255,.12);\n        border-bottom:1px solid rgba(255,255,255,.12);\n        transform:translateX(-50%) rotate(45deg);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-tooltip-portal.is-visible {\n        opacity:1;\n        visibility:visible;\n        transform:translateY(0) scale(1);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-tooltip-portal.is-below {\n        transform-origin:center top;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-tooltip-portal.is-below::after {\n        top:-7px;\n        bottom:auto;\n        border-right:0;\n        border-bottom:0;\n        border-left:1px solid rgba(255,255,255,.12);\n        border-top:1px solid rgba(255,255,255,.12);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-tooltip-portal {\n        background:rgba(255,255,255,.98);\n        color:#172033;\n        border-color:rgba(23,32,51,.12);\n        box-shadow:\n            0 18px 48px rgba(44,62,99,.20),\n            0 0 22px rgba(0,140,255,.08);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-tooltip-portal::after {\n        border-color:rgba(23,32,51,.10);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg { position:absolute;inset:0;z-index:-4;overflow:hidden; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-image {\n        width:100%;\n        height:100%;\n        object-fit:cover;\n        opacity:0;\n        filter:\n            blur(52px)\n            saturate(\n                var(\n                    --jh-theme-image-saturation,\n                    1.55\n                )\n            )\n            brightness(\n                var(\n                    --jh-theme-image-brightness,\n                    .58\n                )\n            );\n        transform:scale(1.22);\n        transition:\n            opacity .8s ease,\n            transform 1.1s ease,\n            filter 1.1s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-orb {\n        position:absolute;\n        width:min(42vw,620px);\n        aspect-ratio:1;\n        border-radius:50%;\n        filter:\n            blur(\n                var(\n                    --jh-theme-blur,\n                    95px\n                )\n            );\n        opacity:\n            var(\n                --jh-theme-orb-opacity,\n                .24\n            );\n        pointer-events:none;\n        will-change:transform;\n        mix-blend-mode:screen;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-orb-pink {\n        left:-8vw;\n        top:4vh;\n        background:radial-gradient(\n            circle,\n            rgba(\n                var(--jh-theme-orb-1-rgb),\n                .78\n            ) 0%,\n            rgba(\n                var(--jh-theme-orb-1-rgb),\n                .36\n            ) 42%,\n            rgba(\n                var(--jh-theme-orb-1-rgb),\n                0\n            ) 72%\n        );\n        animation:jh4BgPinkFloat 26s ease-in-out infinite alternate;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-orb-blue {\n        right:-10vw;\n        top:8vh;\n        background:radial-gradient(\n            circle,\n            rgba(\n                var(--jh-theme-orb-2-rgb),\n                .72\n            ) 0%,\n            rgba(\n                var(--jh-theme-orb-2-rgb),\n                .34\n            ) 44%,\n            rgba(\n                var(--jh-theme-orb-2-rgb),\n                0\n            ) 74%\n        );\n        animation:jh4BgBlueFloat 32s ease-in-out infinite alternate;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-image.on {\n        opacity:\n            var(\n                --jh-theme-image-opacity,\n                .50\n            );\n        animation:jh4BgImageDrift 38s ease-in-out infinite alternate;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.track-transition .jh4-bg-image {\n        opacity:.12;transform:scale(1.34) rotate(.6deg);filter:blur(70px) saturate(1.8) brightness(.45);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-overlay {\n        position:absolute;inset:0;\n        background:\n            radial-gradient(\n                circle at 12% 18%,\n                rgba(\n                    var(--jh-theme-overlay-1-rgb),\n                    .32\n                ),\n                transparent 34%\n            ),\n            radial-gradient(\n                circle at 86% 16%,\n                rgba(\n                    var(--jh-theme-overlay-2-rgb),\n                    .28\n                ),\n                transparent 35%\n            ),\n            linear-gradient(\n                90deg,\n                rgba(\n                    var(--jh-theme-overlay-mid-rgb),\n                    .94\n                ),\n                rgba(\n                    var(--jh-theme-overlay-mid-rgb),\n                    .72\n                ) 52%,\n                rgba(\n                    var(--jh-theme-overlay-mid-rgb),\n                    .84\n                )\n            ),\n            linear-gradient(\n                0deg,\n                rgba(\n                    var(--jh-theme-overlay-bottom-rgb),\n                    .99\n                ),\n                transparent 62%\n            );\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-grid {\n        position:absolute;\n        inset:0;\n        opacity:\n            var(\n                --jh-theme-grid-opacity,\n                .15\n            );\n        background-image:\n            linear-gradient(\n                rgba(\n                    var(--jh-theme-grid-1-rgb),\n                    .08\n                ) 1px,\n                transparent 1px\n            ),\n            linear-gradient(\n                90deg,\n                rgba(\n                    var(--jh-theme-grid-2-rgb),\n                    .07\n                ) 1px,\n                transparent 1px\n            );\n        background-size:72px 72px;\n        mask-image:\n            linear-gradient(\n                to bottom,\n                #000,\n                transparent 80%\n            );\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-shell {\n        width:min(1600px,100%);\n        min-height:100svh;\n        margin:auto;\n        padding:\n            clamp(18px,2.2vw,38px)\n            clamp(18px,2.2vw,38px)\n            clamp(18px,2.2vw,38px);\n        display:flex;\n        flex-direction:column;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header {\n        position:relative;\n        z-index:5;\n        display:flex;\n        align-items:center;\n        justify-content:space-between;\n        gap:20px;\n        padding-top:clamp(26px,3.2vh,48px);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-live {\n        display:flex;align-items:center;gap:10px;padding:12px 18px;border-radius:999px;\n        background:linear-gradient(135deg,var(--pink),#c90072);\n        box-shadow:0 0 26px rgba(255,0,140,.38);\n        font-size:12px;font-weight:900;letter-spacing:.14em;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-live i {\n        width:9px;height:9px;border-radius:50%;background:#fff;box-shadow:0 0 12px #fff;\n        animation:jh4Pulse 1.35s infinite;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-brand {\n        position:absolute;left:50%;transform:translateX(-50%);\n        display:flex;align-items:center;gap:13px;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-brand img {\n        width:220px;\n        height:125px;\n        object-fit:contain;\n        object-position:center;\n        border-radius:0;\n        border:0;\n        background:transparent !important;\n        box-shadow:none;\n        image-rendering:auto;\n        transform-origin:center;\n        filter:\n            drop-shadow(0 0 20px rgba(255,0,140,.30))\n            drop-shadow(0 0 30px rgba(0,140,255,.24));\n        transition:\n            transform .7s cubic-bezier(.2,.8,.2,1),\n            filter .7s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-brand.is-logo-animated img {\n        animation:jh4LogoMoment 2.8s cubic-bezier(.2,.8,.2,1);\n    }\n#justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-theme-switcher {\n        display:flex;\n        align-items:center;\n        gap:6px;\n        margin-left:auto;\n        margin-right:10px;\n        padding:5px;\n        border:1px solid var(--jh-border);\n        border-radius:999px;\n        background:var(--jh-panel);\n        backdrop-filter:blur(12px);\n        -webkit-backdrop-filter:blur(12px);\n        box-shadow:0 10px 28px rgba(0,0,0,.14);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-theme-button {\n        display:grid;\n        place-items:center;\n        width:34px;\n        height:34px;\n        padding:0;\n        border:0;\n        border-radius:50%;\n        background:transparent;\n        color:var(--jh-text-soft);\n        cursor:pointer;\n        transition:transform .18s ease,color .18s ease,background .18s ease,box-shadow .18s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-theme-button:hover {\n        transform:translateY(-1px);\n        color:var(--jh-text);\n        background:rgba(255,255,255,.08);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-theme-button.is-active {\n        color:#fff;\n        background:linear-gradient(135deg,var(--pink),var(--purple),var(--cyan));\n        box-shadow:0 0 18px rgba(0,220,255,.24);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-theme-button svg {\n        width:18px;\n        height:18px;\n        fill:none;\n        stroke:currentColor;\n        stroke-width:1.8;\n        stroke-linecap:round;\n        stroke-linejoin:round;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] {\n        \/*\n         * Le mode clair conserve d\u00e9sormais l'ambiance s\u00e9lectionn\u00e9e.\n         * Les couleurs saisonni\u00e8res sont \u00e9claircies plut\u00f4t que remplac\u00e9es\n         * par le fond clair rose\/bleu historique.\n         *\/\n        --jh-bg:var(\n            --jh-theme-bg,\n            #f4f7fb\n        );\n        --jh-panel:rgba(255,255,255,.80);\n        --jh-panel-strong:rgba(255,255,255,.94);\n        --jh-card:rgba(255,255,255,.78);\n        --jh-card-hover:rgba(255,255,255,.96);\n        --jh-text:#172033;\n        --jh-text-soft:rgba(23,32,51,.70);\n        --jh-border:rgba(23,32,51,.12);\n        color:var(--jh-text);\n        background:\n            radial-gradient(\n                circle at 15% 12%,\n                rgba(\n                    var(--jh-theme-overlay-1-rgb),\n                    .19\n                ),\n                transparent 34%\n            ),\n            radial-gradient(\n                circle at 84% 14%,\n                rgba(\n                    var(--jh-theme-overlay-2-rgb),\n                    .21\n                ),\n                transparent 36%\n            ),\n            linear-gradient(\n                145deg,\n                rgba(\n                    var(--jh-theme-overlay-1-rgb),\n                    .08\n                ),\n                rgba(255,255,255,.93) 34%,\n                rgba(\n                    var(--jh-theme-overlay-2-rgb),\n                    .09\n                ) 68%,\n                rgba(255,255,255,.96)\n            );\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-bg-image {\n        opacity:\n            calc(\n                var(\n                    --jh-theme-image-opacity,\n                    .50\n                ) * .42\n            );\n        filter:\n            blur(58px)\n            saturate(\n                max(\n                    1,\n                    var(\n                        --jh-theme-image-saturation,\n                        1.55\n                    )\n                )\n            )\n            brightness(1.18);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"]\n    .jh4-bg-orb {\n        opacity:\n            calc(\n                var(\n                    --jh-theme-orb-opacity,\n                    .24\n                ) * .78\n            );\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-bg-overlay {\n        background:\n            radial-gradient(\n                circle at 12% 18%,\n                rgba(\n                    var(--jh-theme-overlay-1-rgb),\n                    .17\n                ),\n                transparent 34%\n            ),\n            radial-gradient(\n                circle at 86% 16%,\n                rgba(\n                    var(--jh-theme-overlay-2-rgb),\n                    .16\n                ),\n                transparent 35%\n            ),\n            linear-gradient(\n                90deg,\n                rgba(255,255,255,.80),\n                rgba(255,255,255,.58) 52%,\n                rgba(255,255,255,.72)\n            ),\n            linear-gradient(\n                0deg,\n                rgba(248,250,255,.92),\n                transparent 64%\n            );\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-title,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-artist,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-title,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-history-copy strong {\n        color:var(--jh-text);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-album,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-artist,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-request-kpi,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-history-copy span,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-state,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-times,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-brand small {\n        color:var(--jh-text-soft);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next {\n        border-color:rgba(0,140,255,.18);\n        background:\n            linear-gradient(145deg,rgba(255,0,140,.08),rgba(0,217,255,.08)),\n            rgba(255,255,255,.80);\n        box-shadow:0 24px 70px rgba(44,62,99,.14),inset 0 1px rgba(255,255,255,.85);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-card,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-history,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-history-item {\n        background:rgba(255,255,255,.76);\n        border-color:rgba(23,32,51,.10);\n        box-shadow:0 14px 36px rgba(44,62,99,.10);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-vote-button,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-current-vote-button {\n        color:#172033;\n        background:rgba(255,255,255,.78);\n        border-color:rgba(23,32,51,.16);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-heading span,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-history-heading span,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-pulse {\n        color:rgba(40,52,78,.62);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-heading b,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-history-heading strong {\n        color:#e90083;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-copy small {\n        color:#00a9c8;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-pulse i {\n        background:#35c9df;\n        box-shadow:0 0 10px rgba(53,201,223,.48);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-progress {\n        background:rgba(31,42,68,.18);\n        box-shadow:inset 0 1px 2px rgba(31,42,68,.08);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-progress b {\n        box-shadow:\n            0 0 14px rgba(255,0,140,.34),\n            0 0 20px rgba(0,174,220,.24);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-progress i {\n        border-color:#ffffff;\n        background:#13c9ed;\n        box-shadow:\n            0 0 0 2px rgba(19,201,237,.13),\n            0 0 12px rgba(19,201,237,.65);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-times {\n        color:rgba(31,42,68,.70);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-history-time {\n        color:rgba(31,42,68,.58);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-launch-overlay {\n        background:rgba(236,242,250,.42);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-launch-card {\n        background:\n            radial-gradient(circle at 25% 20%,rgba(255,0,140,.10),transparent 42%),\n            radial-gradient(circle at 78% 18%,rgba(0,220,255,.10),transparent 42%),\n            rgba(255,255,255,.92);\n        color:#172033;\n        border-color:rgba(23,32,51,.12);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-wrap {\n        position:relative;\n        display:flex;\n        align-items:center;\n        justify-content:center;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-wrap::after {\n        content:\"\";\n        position:absolute;\n        top:100%;\n        right:0;\n        width:220px;\n        height:16px;\n        z-index:19;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-panel {\n        position:absolute;\n        top:calc(100% + 12px);\n        right:0;\n        width:190px;\n        padding:14px 16px;\n        border:1px solid var(--jh-border);\n        border-radius:16px;\n        background:var(--jh-panel-strong);\n        box-shadow:0 18px 45px rgba(0,0,0,.26);\n        backdrop-filter:blur(16px);\n        -webkit-backdrop-filter:blur(16px);\n        opacity:0;\n        visibility:hidden;\n        transform:translateY(-6px) scale(.96);\n        transform-origin:top right;\n        pointer-events:none;\n        transition:\n            opacity .20s ease,\n            visibility .20s ease,\n            transform .20s ease;\n        z-index:20;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-wrap:hover .jh4-header-volume-panel,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-wrap:focus-within .jh4-header-volume-panel {\n        opacity:1;\n        visibility:visible;\n        transform:translateY(0) scale(1);\n        pointer-events:auto;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-wrap.is-open .jh4-header-volume-panel {\n        opacity:1;\n        visibility:visible;\n        transform:translateY(0) scale(1);\n        pointer-events:auto;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-slider {\n        width:100%;\n        height:5px;\n        margin:0;\n        accent-color:var(--cyan);\n        cursor:pointer;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-icon-button {\n        display:grid;\n        place-items:center;\n        width:48px;\n        height:48px;\n        padding:0;\n        border:1px solid rgba(255,255,255,.85);\n        border-radius:50%;\n        background:#fff;\n        color:#02031a;\n        cursor:pointer;\n        box-shadow:0 10px 28px rgba(0,0,0,.24);\n        transition:transform .2s ease,box-shadow .2s ease,background .2s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-icon-button:hover {\n        transform:translateY(-2px);\n        box-shadow:0 14px 34px rgba(0,0,0,.32);\n        background:#f7f7fb;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-volume-svg { width:25px;height:25px;overflow:visible; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-volume-svg path {\n        fill:#02031a;\n        stroke:#02031a;\n        stroke-width:1.8;\n        stroke-linecap:round;\n        stroke-linejoin:round;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-volume-svg .jh4-speaker { stroke:none; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-volume-svg .jh4-wave-one,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-volume-svg .jh4-wave-two,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-volume-svg .jh4-muted-x { fill:none; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-volume-svg .jh4-muted-x { display:none; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-mute.is-muted .jh4-wave-one,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-mute.is-muted .jh4-wave-two { display:none; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-mute.is-muted .jh4-muted-x { display:block; }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-main {\n        \/*\n         * Le contenu principal ne remplit plus artificiellement toute\n         * la hauteur disponible. La zone \u00ab D\u00e9j\u00e0 diffus\u00e9s \u00bb remonte ainsi\n         * naturellement et reste visible sans devoir faire d\u00e9filer la page.\n         *\/\n        flex:0 0 auto;\n        display:grid;\n        grid-template-columns:minmax(0,1.55fr) minmax(360px,.75fr);\n        align-items:center;\n        gap:clamp(28px,4vw,70px);\n        padding:clamp(22px,3vh,42px) 0;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current {\n        display:grid;grid-template-columns:minmax(280px,440px) minmax(0,1fr);\n        align-items:center;gap:clamp(34px,5vw,80px);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-zone { position:relative;display:grid;place-items:center; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-glow {\n        position:absolute;inset:4%;border-radius:50%;\n        background:linear-gradient(135deg,var(--pink),var(--purple) 48%,var(--cyan));\n        filter:blur(70px);opacity:.58;\n        transition:transform .7s ease,opacity .7s ease;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.track-transition .jh4-cover-glow { transform:scale(1.25);opacity:.85; }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-stack {\n        position:absolute;\n        inset:0;\n        display:grid;\n        place-items:center;\n        pointer-events:none;\n        z-index:0;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-ghost {\n        position:absolute;\n        width:min(100%,420px);\n        aspect-ratio:1;\n        overflow:hidden;\n        border-radius:30px;\n        border:1px solid rgba(255,255,255,.10);\n        background:rgba(2,3,26,.72);\n        box-shadow:0 24px 60px rgba(0,0,0,.34);\n        opacity:0;\n        transform:translate3d(0,0,0) scale(.95);\n        transition:\n            transform .85s cubic-bezier(.2,.8,.2,1),\n            opacity .7s ease,\n            filter .85s ease;\n        will-change:transform,opacity,filter;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-ghost img {\n        width:100%;\n        height:100%;\n        object-fit:cover;\n        display:block;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-ghost-1 {\n        transform:translate3d(-30px,18px,0) rotate(-6deg) scale(.93);\n        opacity:.40;\n        filter:saturate(.9) brightness(.72);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-ghost-2 {\n        transform:translate3d(30px,16px,0) rotate(6deg) scale(.92);\n        opacity:.30;\n        filter:saturate(.8) brightness(.65);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-ghost-3 {\n        transform:translate3d(0,34px,0) rotate(-1.5deg) scale(.90);\n        opacity:.18;\n        filter:saturate(.75) brightness(.58);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.cover-stack-burst .jh4-cover-ghost-1 {\n        animation:jh4StackOne 1s cubic-bezier(.2,.8,.2,1);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.cover-stack-burst .jh4-cover-ghost-2 {\n        animation:jh4StackTwo 1s cubic-bezier(.2,.8,.2,1);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.cover-stack-burst .jh4-cover-ghost-3 {\n        animation:jh4StackThree 1s cubic-bezier(.2,.8,.2,1);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-box {\n        position:relative;width:min(100%,420px);aspect-ratio:1;overflow:hidden;border-radius:32px;\n        border:1px solid rgba(0,220,255,.24);background:var(--navy);\n        box-shadow:0 38px 90px rgba(0,0,0,.58),0 0 32px rgba(0,140,255,.16);\n        transition:transform .65s cubic-bezier(.2,.8,.2,1),opacity .4s ease,filter .65s ease;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.track-transition .jh4-cover-box {\n        transform:scale(.86) rotate(-2deg);opacity:.2;filter:blur(8px);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-logo-fallback,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-cover {\n        position:absolute;inset:0;width:100%;height:100%;object-fit:cover;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-logo-fallback { z-index:1; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-cover {\n        z-index:2;opacity:0;transform:scale(1.08);\n        transition:opacity .65s ease,transform .9s cubic-bezier(.2,.8,.2,1);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-cover.on { opacity:1;transform:scale(1); }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bars {\n        position:absolute;right:18px;bottom:18px;z-index:4;\n        width:55px;height:48px;padding:11px;display:flex;align-items:flex-end;justify-content:center;gap:4px;\n        border-radius:16px;background:rgba(2,3,26,.78);border:1px solid rgba(0,220,255,.2);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bars i {\n        width:4px;height:7px;border-radius:5px;background:linear-gradient(to top,var(--pink),var(--cyan));\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-playing .jh4-bars i:nth-child(1){animation:jh4Bars .6s .1s infinite alternate}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-playing .jh4-bars i:nth-child(2){animation:jh4Bars .8s .25s infinite alternate}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-playing .jh4-bars i:nth-child(3){animation:jh4Bars .55s .35s infinite alternate}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-playing .jh4-bars i:nth-child(4){animation:jh4Bars .75s .15s infinite alternate}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-playing .jh4-bars i:nth-child(5){animation:jh4Bars .65s .4s infinite alternate}\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-copy {\n        min-width:0;transition:transform .55s ease,opacity .4s ease,filter .55s ease;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.track-transition .jh4-current-copy {\n        transform:translateY(18px);opacity:.1;filter:blur(7px);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong {\n        position:relative;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title[data-tooltip]::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title[data-tooltip]::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong[data-tooltip]::after {\n        content:attr(data-tooltip);\n        position:absolute;\n        left:50%;\n        bottom:calc(100% + 14px);\n        z-index:999;\n        width:max-content;\n        max-width:min(420px,80vw);\n        padding:12px 15px;\n        border:1px solid rgba(255,255,255,.14);\n        border-radius:14px;\n        background:rgba(2,3,26,.96);\n        color:#fff;\n        box-shadow:0 16px 44px rgba(0,0,0,.38),0 0 22px rgba(0,220,255,.10);\n        backdrop-filter:blur(14px);\n        -webkit-backdrop-filter:blur(14px);\n        font-size:14px;\n        font-weight:700;\n        line-height:1.35;\n        letter-spacing:0;\n        white-space:normal;\n        text-align:left;\n        opacity:0;\n        visibility:hidden;\n        pointer-events:none;\n        transform:translate(-50%,8px) scale(.96);\n        transform-origin:bottom center;\n        transition:opacity .18s ease,visibility .18s ease,transform .18s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title[data-tooltip]::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title[data-tooltip]::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong[data-tooltip]::before {\n        content:\"\";\n        position:absolute;\n        left:50%;\n        bottom:calc(100% + 7px);\n        z-index:1000;\n        width:12px;\n        height:12px;\n        background:rgba(2,3,26,.96);\n        border-right:1px solid rgba(255,255,255,.12);\n        border-bottom:1px solid rgba(255,255,255,.12);\n        opacity:0;\n        visibility:hidden;\n        pointer-events:none;\n        transform:translateX(-50%) rotate(45deg);\n        transition:opacity .18s ease,visibility .18s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title[data-tooltip]:hover::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title[data-tooltip]:hover::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong[data-tooltip]:hover::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title[data-tooltip]:focus-visible::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title[data-tooltip]:focus-visible::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong[data-tooltip]:focus-visible::after {\n        opacity:1;\n        visibility:visible;\n        transform:translate(-50%,0) scale(1);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title[data-tooltip]:hover::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title[data-tooltip]:hover::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong[data-tooltip]:hover::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title[data-tooltip]:focus-visible::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title[data-tooltip]:focus-visible::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong[data-tooltip]:focus-visible::before {\n        opacity:1;\n        visibility:visible;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-title[data-tooltip]::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-title[data-tooltip]::after,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-history-copy strong[data-tooltip]::after {\n        background:rgba(255,255,255,.98);\n        color:#172033;\n        border-color:rgba(23,32,51,.12);\n        box-shadow:0 16px 44px rgba(44,62,99,.18),0 0 20px rgba(0,140,255,.08);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-title[data-tooltip]::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-title[data-tooltip]::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-history-copy strong[data-tooltip]::before {\n        background:rgba(255,255,255,.98);\n        border-color:rgba(23,32,51,.10);\n    }\n\n#justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title {\n        margin:0;\n        max-width:780px;\n        max-height:2.05em;\n        display:-webkit-box;\n        -webkit-box-orient:vertical;\n        -webkit-line-clamp:2;\n        overflow:hidden;\n        font-size:clamp(44px,5.2vw,86px);\n        font-weight:950;\n        line-height:1.02;\n        letter-spacing:-.058em;\n        text-wrap:balance;\n        word-break:normal;\n        overflow-wrap:anywhere;\n        cursor:default;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-artist { margin-top:19px;font-size:clamp(21px,2vw,30px);font-weight:760; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-album {\n        min-height:20px;\n        margin-top:8px;\n        color:rgba(255,255,255,.66);\n        font-size:13px;\n        font-weight:650;\n        letter-spacing:.01em;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-album[hidden] { display:none !important; }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-progress-wrap { max-width:720px;margin-top:32px; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-progress { position:relative;height:5px;border-radius:999px;background:rgba(255,255,255,.16); }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-progress b {\n        display:block;width:0;height:100%;border-radius:inherit;\n        background:linear-gradient(90deg,var(--pink),var(--purple),var(--cyan));\n        box-shadow:0 0 18px rgba(255,0,140,.5),0 0 24px rgba(0,220,255,.34);\n        transition:width 1s linear;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-progress i {\n        position:absolute;top:50%;left:0;width:14px;height:14px;border:3px solid #fff;border-radius:50%;\n        background:var(--cyan);box-shadow:0 0 15px rgba(0,220,255,.95);\n        transform:translate(-50%,-50%);transition:left 1s linear;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-times {\n        display:flex;justify-content:space-between;margin-top:10px;\n        color:rgba(255,255,255,.46);font-size:10px;font-weight:700;letter-spacing:.05em;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-controls {\n        display:flex;\n        align-items:center;\n        gap:12px;\n        margin-top:20px;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-play {\n        display:grid;\n        place-items:center;\n        width:86px;\n        height:86px;\n        padding:0;\n        border:1px solid rgba(255,255,255,.30);\n        border-radius:50%;\n        background:linear-gradient(135deg,var(--pink),var(--purple) 48%,var(--cyan));\n        box-shadow:\n            0 0 26px rgba(255,0,140,.52),\n            0 0 40px rgba(0,220,255,.34),\n            0 18px 44px rgba(0,0,0,.46);\n        color:#fff;\n        cursor:pointer;\n        transition:transform .2s ease,box-shadow .2s ease,opacity .2s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-play {\n        position:absolute;\n        left:50%;\n        top:50%;\n        z-index:5;\n        transform:translate(-50%,-50%);\n        opacity:.96;\n        backdrop-filter:blur(4px);\n        -webkit-backdrop-filter:blur(4px);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-play:hover {\n        transform:translate(-50%,-50%) scale(1.06);\n        box-shadow:\n            0 0 34px rgba(255,0,140,.66),\n            0 0 52px rgba(0,220,255,.42),\n            0 22px 50px rgba(0,0,0,.50);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-play-symbol {\n        display:flex;\n        align-items:center;\n        justify-content:center;\n        width:100%;\n        height:100%;\n        font-size:27px;\n        line-height:1;\n        transform:translateX(2px);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-playing .jh4-play-symbol { transform:none;font-size:23px; }\n#justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-state {\n        display:flex;align-items:center;gap:8px;color:rgba(255,255,255,.62);font-size:12px;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-state i { width:8px;height:8px;border-radius:50%;background:#777; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-playing .jh4-state i { background:#2ee879;box-shadow:0 0 14px #2ee879; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-error { margin-top:13px;color:#ff9bcb;font-size:12px; }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next {\n        align-self:stretch;display:flex;flex-direction:column;justify-content:center;\n        padding:clamp(16px,2vw,26px) clamp(20px,2.6vw,34px);border:1px solid rgba(0,217,255,.22);border-radius:32px;\n        background:linear-gradient(145deg,rgba(255,0,140,.17),rgba(0,217,255,.10)),rgba(2,3,25,.78);\n        backdrop-filter:blur(24px);box-shadow:0 30px 80px rgba(0,0,0,.38),inset 0 1px rgba(255,255,255,.07);\n        position:relative;overflow:hidden;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next::before {\n        content:\"\";position:absolute;width:240px;height:240px;right:-80px;top:-80px;border-radius:50%;\n        background:radial-gradient(circle,rgba(0,220,255,.25),transparent 68%);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-heading { position:relative;z-index:1;margin-bottom:12px; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-heading span {\n        display:block;color:var(--cyan);font-size:15px;font-weight:850;letter-spacing:.16em;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-heading b {\n        display:block;margin-top:5px;color:var(--pink);font-size:clamp(28px,3vw,48px);font-weight:950;line-height:1;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-card {\n        position:relative;z-index:1;display:grid;grid-template-columns:150px minmax(0,1fr) auto;\n        align-items:center;gap:20px;padding:14px 18px;border-radius:26px;\n        background:rgba(255,255,255,.085);border:1px solid rgba(255,255,255,.15);\n        box-shadow:0 20px 50px rgba(0,0,0,.28);\n        transition:transform .45s ease,box-shadow .45s ease,opacity .28s ease,filter .28s ease;\n        will-change:transform,opacity,filter;\n    }\n    \/* Transition discr\u00e8te du prochain titre : sortie vers la gauche,\n       puis arriv\u00e9e depuis la droite, coh\u00e9rente avec le titre en cours. *\/\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-card.next-track-out {\n        transform:translateX(-14px);\n        opacity:.12;\n        filter:blur(3px);\n        pointer-events:none;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-card.next-track-in {\n        transform:translateX(14px);\n        opacity:.12;\n        filter:blur(3px);\n        transition:none;\n        pointer-events:none;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-card:hover { transform:translateY(-5px);box-shadow:0 28px 65px rgba(0,0,0,.38); }\n    @media (prefers-reduced-motion: reduce) {\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-card {\n            transition:box-shadow .2s ease;\n        }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-card.next-track-out,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-card.next-track-in {\n            transform:none;\n            opacity:1;\n            filter:none;\n        }\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-cover-wrap {\n        position:relative;width:150px;aspect-ratio:1;overflow:hidden;border-radius:21px;\n        border:1px solid rgba(0,220,255,.17);background:var(--navy);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-fallback,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-cover {\n        position:absolute;inset:0;width:100%;height:100%;object-fit:cover;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-fallback { z-index:1; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-cover { z-index:2;opacity:0;transform:scale(1.08);transition:opacity .6s ease,transform .8s ease; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-cover.on { opacity:1;transform:scale(1); }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-copy { min-width:0;display:flex;flex-direction:column; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-copy small { color:var(--cyan);font-size:10px;font-weight:900;letter-spacing:.13em; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title-wrap {\n        position:relative;\n        margin-top:10px;\n        max-height:2.52em;\n        padding-bottom:5px;\n        overflow:hidden;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title-wrap::after {\n        content:\"\";\n        position:absolute;\n        right:0;\n        bottom:0;\n        width:32%;\n        height:1.1em;\n        pointer-events:none;\n        background:linear-gradient(90deg,transparent,rgba(35,39,73,.98));\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title {\n        display:-webkit-box;\n        -webkit-box-orient:vertical;\n        -webkit-line-clamp:2;\n        overflow:hidden;\n        font-size:clamp(21px,2vw,33px);\n        font-weight:950;\n        line-height:1.16;\n        padding-bottom:2px;\n        word-break:normal;\n        overflow-wrap:anywhere;\n        hyphens:auto;\n        cursor:default;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-artist { margin-top:9px;color:rgba(255,255,255,.65);font-size:15px; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-requester{margin-top:7px;color:var(--cyan);font-size:12px;font-weight:800;letter-spacing:.02em}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-request-kpi{display:inline-flex;align-items:center;gap:5px;margin-left:9px;padding:6px 10px;border:1px solid color-mix(in srgb,var(--cyan) 28%,transparent);border-radius:999px;background:color-mix(in srgb,var(--cyan) 10%,transparent);color:rgba(255,255,255,.78);font-size:11px}#justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-request-kpi[hidden]{display:none!important}#justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-request-kpi-icon{width:15px;height:15px;flex:0 0 15px;fill:none;stroke:var(--cyan);stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}#justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-request-kpi strong{color:var(--cyan)}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-next-requester{color:#007fae}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-request-kpi{border-color:rgba(0,127,174,.22);background:rgba(0,127,174,.08);color:var(--jh-text-soft)}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"] .jh4-request-kpi strong{color:#007fae}\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-arrow { font-size:46px;line-height:1;color:var(--cyan); }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-pulse {\n        position:relative;z-index:1;display:flex;align-items:center;justify-content:center;gap:9px;\n        margin-top:18px;color:rgba(255,255,255,.52);font-size:11px;letter-spacing:.05em;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-pulse i {\n        width:7px;height:7px;border-radius:50%;background:var(--cyan);box-shadow:0 0 12px var(--cyan);\n        animation:jh4Pulse 1.6s infinite;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history {\n        padding:22px;border-radius:25px;background:rgba(2,3,25,.8);\n        border:1px solid rgba(255,255,255,.08);backdrop-filter:blur(20px);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-heading { display:flex;align-items:baseline;gap:12px;margin-bottom:14px; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-heading strong { color:var(--pink);font-size:12px;font-weight:900;letter-spacing:.13em; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-heading span { color:rgba(255,255,255,.43);font-size:10px;text-transform:uppercase; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-grid { display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:22px; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-item {\n        display:grid;\n        grid-template-columns:132px minmax(0,1fr);\n        align-items:center;\n        gap:16px;\n        min-width:0;\n        overflow:hidden;\n        padding:13px;\n        border-radius:15px;\n        background:rgba(255,255,255,.055);\n        border:1px solid transparent;\n        transition:transform .2s ease,background .2s ease,border-color .2s ease;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-item {\n        min-height:132px;\n        box-shadow:0 12px 28px rgba(0,0,0,.18);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-item.is-entering {\n        opacity:0;\n        transform:translateY(18px) scale(.96);\n        filter:blur(5px);\n        animation:jh4HistoryEnter .72s cubic-bezier(.2,.8,.2,1) forwards;\n        animation-delay:var(--jh4-enter-delay,0ms);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-item:hover {\n        transform:translateY(-2px);background:rgba(255,255,255,.085);border-color:rgba(0,220,255,.18);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-item {\n        position:relative;\n        overflow:hidden;\n        background:\n            linear-gradient(145deg,rgba(0,220,255,.10),rgba(111,38,255,.08) 55%,rgba(255,0,140,.10)),\n            rgba(255,255,255,.055);\n        border-color:rgba(0,220,255,.18);\n        box-shadow:0 12px 30px rgba(0,0,0,.22);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-item:nth-child(2n) {\n        background:\n            linear-gradient(145deg,rgba(255,0,140,.12),rgba(111,38,255,.08) 55%,rgba(0,220,255,.08)),\n            rgba(255,255,255,.055);\n        border-color:rgba(255,0,140,.18);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-vote {\n        display:flex;\n        align-items:center;\n        justify-content:center;\n        gap:12px;\n        width:100%;\n        margin-top:12px;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-vote-button,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote-button {\n        display:inline-flex;\n        align-items:center;\n        justify-content:center;\n        width:40px;\n        height:40px;\n        padding:0;\n        border:1px solid rgba(255,255,255,.22);\n        border-radius:50%;\n        color:#fff;\n        background:rgba(255,255,255,.08);\n        cursor:pointer;\n        font-size:20px;\n        line-height:1;\n        transition:\n            transform .18s ease,\n            background .18s ease,\n            border-color .18s ease,\n            box-shadow .18s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-vote-button:hover,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote-button:hover {\n        transform:translateY(-2px) scale(1.06);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-vote-plus:hover,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-vote-plus.is-selected,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote-like:hover,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote-like.is-selected {\n        background:rgba(255,0,140,.20);\n        border-color:rgba(255,0,140,.75);\n        box-shadow:0 0 18px rgba(255,0,140,.28);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-vote-minus:hover,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-vote-minus.is-selected,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote-dislike:hover,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote-dislike.is-selected {\n        background:rgba(255,74,104,.16);\n        border-color:rgba(255,74,104,.72);\n        box-shadow:0 0 18px rgba(255,74,104,.24);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-vote-button:disabled,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote-button:disabled {\n        opacity:.48;\n        cursor:not-allowed;\n        transform:none;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-vote-button.is-vote-animated,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote-button.is-vote-animated {\n        animation:jh4VotePop .65s cubic-bezier(.2,.8,.2,1);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote-like {\n        position:relative;\n        overflow:visible;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-item,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-vote {\n        overflow:visible !important;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-vote-plus {\n        position:relative;\n        overflow:visible;\n    }\n\n    \/*\n     * Apr\u00e8s validation, le bouton choisi reste parfaitement visible.\n     * Seul le bouton oppos\u00e9 peut rester att\u00e9nu\u00e9.\n     *\/\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-vote-plus.is-selected:disabled,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-current-vote-like.is-selected:disabled {\n        opacity:1 !important;\n        filter:none !important;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-like-sparkles {\n        position:absolute;\n        left:50%;\n        top:50%;\n        z-index:20;\n        width:0;\n        height:0;\n        overflow:visible;\n        pointer-events:none;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-like-sparkles i {\n        position:absolute;\n        left:0;\n        top:0;\n        width:5px;\n        height:5px;\n        border-radius:50%;\n        background:linear-gradient(135deg,#fff,var(--cyan));\n        box-shadow:\n            0 0 10px rgba(0,220,255,.78),\n            0 0 7px rgba(255,255,255,.75);\n        opacity:0;\n        transform:translate(-50%,-50%) scale(.25);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-vote-plus.is-sparkling\n    .jh4-history-like-sparkles i {\n        animation:\n            jh4LikeSparkle\n            .72s cubic-bezier(.2,.8,.2,1)\n            forwards;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-vote-plus.is-sparkling\n    .jh4-history-like-sparkles i:nth-child(1) {\n        --tx:-27px;\n        --ty:-21px;\n        animation-delay:0ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-vote-plus.is-sparkling\n    .jh4-history-like-sparkles i:nth-child(2) {\n        --tx:0px;\n        --ty:-32px;\n        animation-delay:35ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-vote-plus.is-sparkling\n    .jh4-history-like-sparkles i:nth-child(3) {\n        --tx:27px;\n        --ty:-21px;\n        animation-delay:70ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-vote-plus.is-sparkling\n    .jh4-history-like-sparkles i:nth-child(4) {\n        --tx:33px;\n        --ty:1px;\n        animation-delay:105ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-vote-plus.is-sparkling\n    .jh4-history-like-sparkles i:nth-child(5) {\n        --tx:23px;\n        --ty:26px;\n        animation-delay:140ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-vote-plus.is-sparkling\n    .jh4-history-like-sparkles i:nth-child(6) {\n        --tx:0px;\n        --ty:33px;\n        animation-delay:175ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-vote-plus.is-sparkling\n    .jh4-history-like-sparkles i:nth-child(7) {\n        --tx:-24px;\n        --ty:25px;\n        animation-delay:210ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-vote-plus.is-sparkling\n    .jh4-history-like-sparkles i:nth-child(8) {\n        --tx:-33px;\n        --ty:1px;\n        animation-delay:245ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"]\n    .jh4-history-like-sparkles i {\n        background:linear-gradient(135deg,#fff,#00a9c8);\n        box-shadow:\n            0 0 10px rgba(0,169,200,.50),\n            0 0 7px rgba(255,255,255,.88);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-like-sparkles {\n        position:absolute;\n        left:50%;\n        top:50%;\n        z-index:5;\n        width:0;\n        height:0;\n        pointer-events:none;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-like-sparkles i {\n        position:absolute;\n        left:0;\n        top:0;\n        width:5px;\n        height:5px;\n        border-radius:50%;\n        background:linear-gradient(135deg,#fff,var(--cyan));\n        box-shadow:\n            0 0 10px rgba(0,220,255,.78),\n            0 0 7px rgba(255,255,255,.75);\n        opacity:0;\n        transform:translate(-50%,-50%) scale(.25);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-current-vote-like.is-sparkling\n    .jh4-like-sparkles i {\n        animation:\n            jh4LikeSparkle\n            .72s cubic-bezier(.2,.8,.2,1)\n            forwards;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-current-vote-like.is-sparkling\n    .jh4-like-sparkles i:nth-child(1) {\n        --tx:-29px;\n        --ty:-22px;\n        animation-delay:0ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-current-vote-like.is-sparkling\n    .jh4-like-sparkles i:nth-child(2) {\n        --tx:0px;\n        --ty:-35px;\n        animation-delay:35ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-current-vote-like.is-sparkling\n    .jh4-like-sparkles i:nth-child(3) {\n        --tx:29px;\n        --ty:-22px;\n        animation-delay:70ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-current-vote-like.is-sparkling\n    .jh4-like-sparkles i:nth-child(4) {\n        --tx:36px;\n        --ty:1px;\n        animation-delay:105ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-current-vote-like.is-sparkling\n    .jh4-like-sparkles i:nth-child(5) {\n        --tx:25px;\n        --ty:28px;\n        animation-delay:140ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-current-vote-like.is-sparkling\n    .jh4-like-sparkles i:nth-child(6) {\n        --tx:0px;\n        --ty:36px;\n        animation-delay:175ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-current-vote-like.is-sparkling\n    .jh4-like-sparkles i:nth-child(7) {\n        --tx:-26px;\n        --ty:27px;\n        animation-delay:210ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-current-vote-like.is-sparkling\n    .jh4-like-sparkles i:nth-child(8) {\n        --tx:-36px;\n        --ty:1px;\n        animation-delay:245ms;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"]\n    .jh4-like-sparkles i {\n        background:linear-gradient(135deg,#fff,#00a9c8);\n        box-shadow:\n            0 0 10px rgba(0,169,200,.50),\n            0 0 7px rgba(255,255,255,.88);\n    }\n\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-stream-switch-overlay {\n        position:absolute;\n        inset:0;\n        z-index:50;\n        display:flex;\n        align-items:center;\n        justify-content:center;\n        pointer-events:none;\n        opacity:0;\n        visibility:hidden;\n        background:rgba(3,4,26,.22);\n        backdrop-filter:blur(8px);\n        -webkit-backdrop-filter:blur(8px);\n        transition:opacity .32s ease,visibility .32s ease;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-stream-switch-overlay.is-visible {\n        opacity:1;\n        visibility:visible;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-stream-switch-card {\n        padding:14px 22px;\n        border:1px solid rgba(255,255,255,.18);\n        border-radius:999px;\n        background:rgba(5,6,32,.82);\n        color:#fff;\n        box-shadow:\n            0 18px 50px rgba(0,0,0,.26),\n            0 0 28px rgba(0,220,255,.14);\n        font-size:11px;\n        font-weight:900;\n        letter-spacing:.1em;\n        text-transform:uppercase;\n        transform:translateY(8px) scale(.96);\n        transition:transform .34s cubic-bezier(.2,.8,.2,1);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2    .jh4-stream-switch-overlay.is-visible\n    .jh4-stream-switch-card {\n        transform:translateY(0) scale(1);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2[data-theme-resolved=\"light\"]\n    .jh4-stream-switch-card {\n        border-color:rgba(23,32,51,.12);\n        background:rgba(255,255,255,.9);\n        color:#172033;\n    }\n\n    @keyframes jh4LikeSparkle {\n        0% {\n            opacity:0;\n            transform:translate(-50%,-50%) scale(.25);\n        }\n\n        22% {\n            opacity:1;\n        }\n\n        100% {\n            opacity:0;\n            transform:\n                translate(\n                    calc(-50% + var(--tx)),\n                    calc(-50% + var(--ty))\n                )\n                scale(1.15);\n        }\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote {\n        display:flex;\n        align-items:center;\n        gap:13px;\n        margin-top:18px;\n    }\n\n    \/*\n     * Aucun espace r\u00e9serv\u00e9 lorsque le classement est d\u00e9sactiv\u00e9 et\n     * qu'aucun module de d\u00e9dicace n'a encore ajout\u00e9 de bouton.\n     * D\u00e8s qu'un bouton est inject\u00e9, la marge normale revient.\n     *\/\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote:empty {\n        margin-top:0;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote-button {\n        width:48px;\n        height:48px;\n        font-size:23px;\n        backdrop-filter:blur(10px);\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-heart-icon,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-broken-heart-icon {\n        display:flex;\n        align-items:center;\n        justify-content:center;\n        width:100%;\n        height:100%;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-cover-wrap {\n        position:relative;width:132px;height:132px;overflow:hidden;border-radius:18px;background:var(--navy);\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-cover-wrap img {\n        position:absolute;inset:0;width:100%;height:100%;object-fit:cover;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-cover-wrap .jh4-history-fallback { z-index:1; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-cover-wrap .jh4-history-cover { z-index:2; }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy {\n        min-width:0;\n        width:100%;\n        max-width:100%;\n        overflow:hidden;\n        display:flex;\n        flex-direction:column;\n        align-items:center;\n        justify-content:center;\n        text-align:center;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy > span {\n        display:block;\n        width:100%;\n        max-width:100%;\n        overflow:hidden;\n        text-overflow:ellipsis;\n        white-space:nowrap;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong {\n        display:-webkit-box;\n        width:100%;\n        max-width:100%;\n        max-height:2.42em;\n        padding:0 2px 2px;\n        overflow:hidden;\n        -webkit-box-orient:vertical;\n        -webkit-line-clamp:2;\n        font-size:18px;\n        line-height:1.18;\n        font-weight:900;\n        letter-spacing:-.01em;\n        text-overflow:ellipsis;\n        white-space:normal;\n        word-break:normal;\n        overflow-wrap:break-word;\n        hyphens:auto;\n        cursor:default;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy > span {\n        margin-top:7px;\n        color:rgba(255,255,255,.70);\n        font-size:13px;\n        line-height:1.25;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-time {\n        display:inline-flex;\n        align-items:center;\n        justify-content:center;\n        gap:5px;\n        margin-top:7px;\n        color:rgba(255,255,255,.48);\n        font-size:11px;\n        font-weight:750;\n        line-height:1;\n        letter-spacing:.03em;\n        font-variant-numeric:tabular-nums;\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-time svg {\n        width:13px;\n        height:13px;\n        fill:none;\n        stroke:currentColor;\n        stroke-width:1.8;\n        stroke-linecap:round;\n        stroke-linejoin:round;\n    }\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-loading { grid-column:1\/-1;text-align:center;color:rgba(255,255,255,.55); }\n\n    @keyframes jh4Pulse { 50% { opacity:.35;transform:scale(.72); } }\n\n    @keyframes jh4BgPinkFloat {\n        0% {\n            transform:translate3d(0,0,0) scale(1);\n        }\n        50% {\n            transform:translate3d(9vw,5vh,0) scale(1.08);\n        }\n        100% {\n            transform:translate3d(4vw,12vh,0) scale(.96);\n        }\n    }\n\n    @keyframes jh4BgBlueFloat {\n        0% {\n            transform:translate3d(0,0,0) scale(1);\n        }\n        50% {\n            transform:translate3d(-8vw,7vh,0) scale(1.06);\n        }\n        100% {\n            transform:translate3d(-3vw,14vh,0) scale(.97);\n        }\n    }\n\n    @keyframes jh4BgImageDrift {\n        0% {\n            transform:scale(1.22) translate3d(0,0,0);\n        }\n        50% {\n            transform:scale(1.26) translate3d(-1.2%,1%,0);\n        }\n        100% {\n            transform:scale(1.24) translate3d(1.1%,-.8%,0);\n        }\n    }\n\n    @keyframes jh4LogoMoment {\n        0%,100% {\n            transform:translateY(0) scale(1) rotate(0deg);\n            filter:\n                drop-shadow(0 0 20px rgba(255,0,140,.30))\n                drop-shadow(0 0 30px rgba(0,140,255,.24));\n        }\n        24% {\n            transform:translateY(-4px) scale(1.035) rotate(-1deg);\n            filter:\n                drop-shadow(0 0 30px rgba(255,0,140,.46))\n                drop-shadow(0 0 42px rgba(0,220,255,.34));\n        }\n        52% {\n            transform:translateY(0) scale(1.06) rotate(1deg);\n            filter:\n                drop-shadow(0 0 36px rgba(255,0,140,.54))\n                drop-shadow(0 0 50px rgba(0,220,255,.40));\n        }\n        76% {\n            transform:translateY(-2px) scale(1.025) rotate(-.5deg);\n        }\n    }\n\n    @keyframes jh4LaunchPulse {\n        0%,100% {\n            box-shadow:\n                0 0 0 0 rgba(255,0,140,.34),\n                0 0 35px rgba(255,0,140,.42),\n                0 0 55px rgba(0,220,255,.28),\n                0 22px 50px rgba(0,0,0,.38);\n        }\n        50% {\n            box-shadow:\n                0 0 0 18px rgba(255,0,140,0),\n                0 0 48px rgba(255,0,140,.56),\n                0 0 70px rgba(0,220,255,.36),\n                0 22px 50px rgba(0,0,0,.38);\n        }\n    }\n    @keyframes jh4Bars { to { height:25px; } }\n\n    @keyframes jh4VotePop {\n        0%,100% {\n            transform:scale(1);\n        }\n        42% {\n            transform:scale(1.24);\n            box-shadow:0 0 28px rgba(255,0,140,.48);\n        }\n        72% {\n            transform:scale(.96);\n        }\n    }\n\n    @keyframes jh4HistoryEnter {\n        0% {\n            opacity:0;\n            transform:translateY(18px) scale(.96);\n            filter:blur(5px);\n        }\n        65% {\n            opacity:1;\n            transform:translateY(-3px) scale(1.015);\n            filter:blur(0);\n        }\n        100% {\n            opacity:1;\n            transform:translateY(0) scale(1);\n            filter:blur(0);\n        }\n    }\n\n    @keyframes jh4StackOne {\n        0% { transform:translate3d(0,0,0) rotate(0deg) scale(.78);opacity:0;filter:blur(10px) saturate(1.3); }\n        45% { transform:translate3d(-48px,8px,0) rotate(-10deg) scale(.98);opacity:.58;filter:blur(0) saturate(1); }\n        100% { transform:translate3d(-30px,18px,0) rotate(-6deg) scale(.93);opacity:.40;filter:saturate(.9) brightness(.72); }\n    }\n\n    @keyframes jh4StackTwo {\n        0% { transform:translate3d(0,0,0) rotate(0deg) scale(.76);opacity:0;filter:blur(12px) saturate(1.3); }\n        55% { transform:translate3d(52px,10px,0) rotate(11deg) scale(.97);opacity:.48;filter:blur(0) saturate(1); }\n        100% { transform:translate3d(30px,16px,0) rotate(6deg) scale(.92);opacity:.30;filter:saturate(.8) brightness(.65); }\n    }\n\n    @keyframes jh4StackThree {\n        0% { transform:translate3d(0,0,0) rotate(0deg) scale(.72);opacity:0;filter:blur(14px); }\n        60% { transform:translate3d(0,50px,0) rotate(-3deg) scale(.94);opacity:.34;filter:blur(0); }\n        100% { transform:translate3d(0,34px,0) rotate(-1.5deg) scale(.90);opacity:.18;filter:saturate(.75) brightness(.58); }\n    }\n\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-page-hidden *,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-page-hidden *::before,\n    #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-page-hidden *::after {\n        animation-play-state:paused !important;\n    }\n\n    @media (prefers-reduced-motion: reduce) {\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-brand.is-logo-animated img,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-item.is-entering,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-orb,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-image.on {\n            animation:none !important;\n        }\n    }\n\n    @media (max-width:1200px) {\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-main { grid-template-columns:1fr; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next { min-height:0; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-grid { grid-template-columns:repeat(2,1fr); }\n    }\n\n    @media (max-width:800px) {\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-shell { padding:16px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header { padding-top:18px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-brand { position:static;margin-left:auto;transform:none; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-brand div { display:none; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-brand img { width:158px;height:88px;border-radius:0;object-fit:contain; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-icon-button { width:44px;height:44px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-main { padding:28px 0; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current { grid-template-columns:1fr;text-align:center; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-box { width:min(78vw,370px);margin:auto; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-play { width:72px;height:72px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-play .jh4-play-symbol { font-size:23px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-ghost { width:min(78vw,370px); }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-ghost-1 { transform:translate3d(-18px,12px,0) rotate(-5deg) scale(.94); }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-ghost-2 { transform:translate3d(18px,11px,0) rotate(5deg) scale(.93); }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-ghost-3 { transform:translate3d(0,24px,0) rotate(-1deg) scale(.91); }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-controls,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-current-vote { justify-content:center; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-progress-wrap { margin-inline:auto; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-state { display:none; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-card { grid-template-columns:105px minmax(0,1fr); }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-cover-wrap { width:105px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-arrow { display:none; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-grid { grid-template-columns:repeat(2,1fr); }\n    }\n\n    @media (max-width:520px) {\n        \/*\n         * Mode mobile all\u00e9g\u00e9 :\n         * on conserve le rendu, mais on r\u00e9duit les effets GPU co\u00fbteux.\n         *\/\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-orb {\n            width:72vw;\n            filter:blur(42px);\n            opacity:.14;\n            mix-blend-mode:normal;\n            will-change:auto;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-orb-pink,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-orb-blue,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-image.on {\n            animation:none !important;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-image {\n            filter:blur(30px) saturate(1.25) brightness(.72);\n            transform:scale(1.12);\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-bg-image.on {\n            opacity:.34;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-overlay,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-panel,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-tooltip-portal {\n            backdrop-filter:none !important;\n            -webkit-backdrop-filter:none !important;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-grid {\n            opacity:.08;\n            background-size:96px 96px;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-glow {\n            filter:blur(38px);\n            opacity:.38;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-cover-box,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-item {\n            box-shadow:none;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.track-transition .jh4-bg-image {\n            filter:blur(36px) saturate(1.35) brightness(.62);\n            transform:scale(1.16);\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2.is-playing .jh4-bars i {\n            animation-duration:1.2s !important;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-live,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-theme-switcher {\n            display:none !important;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header {\n            justify-content:center;\n            padding-top:22px;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-brand {\n            position:static;\n            left:auto;\n            margin:0 auto;\n            transform:none;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-brand img {\n            width:184px;\n            height:102px;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-wrap {\n            position:absolute;\n            right:0;\n            top:22px;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title[data-tooltip]::after,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title[data-tooltip]::after,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong[data-tooltip]::after {\n            max-width:88vw;\n            font-size:13px;\n            padding:10px 12px;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-panel {\n            width:170px;\n            right:0;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-wrap:hover .jh4-header-volume-panel {\n            opacity:0;\n            visibility:hidden;\n            transform:translateY(-6px) scale(.96);\n            pointer-events:none;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-wrap.is-open .jh4-header-volume-panel,\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-header-volume-wrap:focus-within .jh4-header-volume-panel {\n            opacity:1;\n            visibility:visible;\n            transform:translateY(0) scale(1);\n            pointer-events:auto;\n        }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-theme-switcher {\n            gap:3px;\n            margin-right:6px;\n            padding:4px;\n        }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-theme-button {\n            width:30px;\n            height:30px;\n        }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-theme-button svg {\n            width:16px;\n            height:16px;\n        }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-overlay {\n            align-items:center;\n            justify-content:center;\n            min-height:100svh;\n            padding:\n                max(14px, env(safe-area-inset-top))\n                14px\n                max(14px, env(safe-area-inset-bottom));\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-card {\n            width:min(92vw,440px);\n            max-height:calc(\n                100svh\n                - max(28px, env(safe-area-inset-top))\n                - max(28px, env(safe-area-inset-bottom))\n            );\n            margin:auto;\n            padding:26px 18px 22px;\n            border-radius:26px;\n        }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-play {\n            width:104px;\n            height:104px;\n            margin:20px 0 17px;\n        }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-launch-play span {\n            font-size:38px;\n        }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-live { padding:10px 13px;font-size:9px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-title { font-size:clamp(38px,13vw,62px); }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next { padding:18px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-heading b { font-size:34px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-card { grid-template-columns:82px minmax(0,1fr);gap:14px;padding:13px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-cover-wrap { width:82px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-title { font-size:19px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-artist { font-size:12px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-grid { grid-template-columns:1fr; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-item { grid-template-columns:82px 1fr;min-height:104px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-cover-wrap { width:82px;height:82px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy strong {\n            font-size:16px;\n            line-height:1.2;\n            max-height:2.45em;\n        }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-copy span { font-size:12px; }\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-history-time {\n            margin-top:6px;\n            font-size:10px;\n        }\n    }\n\n    @media (max-width:700px) {\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-brand img {\n            width:178px;\n            height:102px;\n            background:transparent !important;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next {\n            padding:14px 16px;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-heading {\n            margin-bottom:10px;\n        }\n\n        #justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2 .jh4-next-card {\n            padding:12px 14px;\n            gap:14px;\n        }\n    }\n    <\/style>\n\n    <script>\n    (() => {\n        const root = document.getElementById(\"justhits-v4-f99f53ae-2c36-48d9-ab06-b1e9c52cb8c2\");\n        if (!root || root.dataset.initialized === '1') return;\n        root.dataset.initialized = '1';\n\n        const q = (selector) => root.querySelector(selector);\n        const managerDecks = Array.from(root.querySelectorAll('.jh4-audio'));\n        let audio = managerDecks[0];\n        let standbyAudio = managerDecks[1] || managerDecks[0];\n        let reserveAudio = managerDecks[2] || managerDecks[0];\n        let playbackWanted = false;\n        let managerRecoveryRunning = false;\n        const streamSwitchOverlay = q('.jh4-stream-switch-overlay');\n        const streamSwitchCard = q('.jh4-stream-switch-card');\n        const rankingEnabled =\n            root.dataset.rankingEnabled === '1';\n\n        const voteApi = rankingEnabled\n            ? root.dataset.voteApi\n            : '';\n\n        const launchOverlay = q('.jh4-launch-overlay');\n        const launchPlay = q('.jh4-launch-play');\n        const themeButtons = Array.from(root.querySelectorAll('.jh4-theme-button'));\n        const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');\n        const brand = q('.jh4-brand');\n        const playButton = q('.jh4-play');\n        const playSymbol = q('.jh4-play-symbol');\n        const muteButton = q('.jh4-mute');\n        const volumeSlider = q('.jh4-header-volume-slider');\n        const headerVolumeWrap = q('.jh4-header-volume-wrap');\n        const mobileMediaQuery = window.matchMedia('(max-width: 520px)');\n        const currentCover = q('.jh4-current-cover');\n        const backgroundImage = q('.jh4-bg-image');\n        const coverGhosts = Array.from(root.querySelectorAll('.jh4-cover-ghost img'));\n        const title = q('.jh4-title');\n        const artist = q('.jh4-artist');\n        const album = q('.jh4-album');\n        const nextCover = q('.jh4-next-cover');\n        const nextCard = q('.jh4-next-card');\n        const nextTitle = q('.jh4-next-title');\n        const nextArtist = q('.jh4-next-artist');\n        const nextRequester = q('.jh4-next-requester');\n        const requestKpi = q('.jh4-request-kpi');\n        const requestKpiName = requestKpi ? requestKpi.querySelector('strong') : null;\n        const historyGrid = q('.jh4-history-grid');\n        const progressFill = q('.jh4-progress b');\n        const progressDot = q('.jh4-progress i');\n        const elapsedLabel = q('.jh4-elapsed');\n        const durationLabel = q('.jh4-duration');\n        const stateText = q('.jh4-state span');\n        const errorBox = q('.jh4-error');\n        const connectionStrength = q('.jh4-connection-strength');\n        const connectionLabel = connectionStrength\n            ? connectionStrength.querySelector('strong')\n            : null;\n        const networkInformation =\n            navigator.connection\n            || navigator.mozConnection\n            || navigator.webkitConnection\n            || null;\n        let connectionStalledUntil = 0;\n\n        const activeBufferedSeconds = () => {\n            if (!audio || !audio.buffered || !audio.buffered.length) {\n                return 0;\n            }\n\n            const position = Number(audio.currentTime || 0);\n            for (let index = 0; index < audio.buffered.length; index += 1) {\n                if (\n                    audio.buffered.start(index) <= position\n                    && audio.buffered.end(index) >= position\n                ) {\n                    return Math.max(\n                        0,\n                        audio.buffered.end(index) - position\n                    );\n                }\n            }\n\n            return 0;\n        };\n\n        const updateConnectionStrength = () => {\n            if (!connectionStrength) return;\n\n            let level = navigator.onLine ? 3 : 0;\n\n            if (navigator.onLine && networkInformation) {\n                const downlink = Number(networkInformation.downlink);\n                const effectiveType = String(\n                    networkInformation.effectiveType || ''\n                ).toLowerCase();\n\n                if (\n                    effectiveType === 'slow-2g'\n                    || effectiveType === '2g'\n                    || (Number.isFinite(downlink) && downlink < .7)\n                ) {\n                    level = 1;\n                } else if (\n                    effectiveType === '3g'\n                    || (Number.isFinite(downlink) && downlink < 2)\n                ) {\n                    level = 2;\n                } else if (\n                    effectiveType === '4g'\n                    && Number.isFinite(downlink)\n                    && downlink >= 5\n                ) {\n                    level = 4;\n                }\n            }\n\n            if (\n                navigator.onLine\n                && performance.now() < connectionStalledUntil\n            ) {\n                level = 1;\n            } else if (isPlaybackActive()) {\n                const buffered = activeBufferedSeconds();\n\n                if (buffered < .8) {\n                    level = Math.min(level, 1);\n                } else if (buffered < 3) {\n                    level = Math.min(level, 2);\n                } else if (buffered >= 12) {\n                    level = Math.max(level, 3);\n                }\n            }\n\n            const labels = [\n                'Hors connexion',\n                'Connexion faible',\n                'Connexion limit\u00e9e',\n                'Connexion suffisante',\n                'Excellente connexion',\n            ];\n            const label = labels[level] || labels[0];\n\n            connectionStrength.className =\n                `jh4-connection-strength level-${level}`;\n            connectionStrength.setAttribute(\n                'aria-label',\n                `${label} pour \u00e9couter la radio`\n            );\n            connectionStrength.setAttribute(\n                'data-tooltip',\n                label\n            );\n\n            if (connectionLabel) {\n                connectionLabel.textContent = label;\n            }\n        };\n\n        const markConnectionStalled = () => {\n            connectionStalledUntil = performance.now() + 8000;\n            updateConnectionStrength();\n        };\n\n        window.addEventListener('online', updateConnectionStrength);\n        window.addEventListener('offline', updateConnectionStrength);\n        networkInformation?.addEventListener?.(\n            'change',\n            updateConnectionStrength\n        );\n        managerDecks.forEach((deck) => {\n            deck.addEventListener('progress', updateConnectionStrength);\n            deck.addEventListener('canplay', updateConnectionStrength);\n            deck.addEventListener('playing', updateConnectionStrength);\n            deck.addEventListener('waiting', markConnectionStalled);\n            deck.addEventListener('stalled', markConnectionStalled);\n        });\n        const connectionStrengthTimer = window.setInterval(\n            updateConnectionStrength,\n            3000\n        );\n        window.setTimeout(updateConnectionStrength, 0);\n\n        \/*\n         * Dur\u00e9e d'\u00e9coute de la session courante.\n         * Le compteur avance uniquement lorsque le direct est r\u00e9ellement lu.\n         * Une pause temporaire (d\u00e9dicace, extrait ou action de l'auditeur)\n         * suspend le compteur sans remettre la dur\u00e9e \u00e0 z\u00e9ro.\n         *\/\n        let listeningElapsedMs = 0;\n        let listeningStartedAt = 0;\n        let listeningTimer = null;\n        let listeningWasPlaying = false;\n\n        const formatListeningDuration = (milliseconds) => {\n            const totalSeconds = Math.max(\n                0,\n                Math.floor((Number(milliseconds) || 0) \/ 1000)\n            );\n            const hours = Math.floor(totalSeconds \/ 3600);\n            const minutes = Math.floor((totalSeconds % 3600) \/ 60);\n            const seconds = totalSeconds % 60;\n\n            if (hours > 0) {\n                return [hours, minutes, seconds]\n                    .map((value) => String(value).padStart(2, '0'))\n                    .join(':');\n            }\n\n            return [minutes, seconds]\n                .map((value) => String(value).padStart(2, '0'))\n                .join(':');\n        };\n\n        const getListeningElapsedMs = () =>\n            listeningElapsedMs + (\n                listeningWasPlaying && listeningStartedAt\n                    ? Date.now() - listeningStartedAt\n                    : 0\n            );\n\n        const renderListeningState = () => {\n            if (!stateText) return;\n\n            if (listeningWasPlaying) {\n                stateText.textContent =\n                    `Vous \u00e9coutez le direct depuis ${formatListeningDuration(getListeningElapsedMs())}`;\n            } else {\n                stateText.textContent = 'Radio en pause';\n            }\n        };\n\n        const syncListeningTimer = (playing) => {\n            if (playing && !listeningWasPlaying) {\n                listeningWasPlaying = true;\n                listeningStartedAt = Date.now();\n            } else if (!playing && listeningWasPlaying) {\n                listeningElapsedMs += Math.max(\n                    0,\n                    Date.now() - listeningStartedAt\n                );\n                listeningStartedAt = 0;\n                listeningWasPlaying = false;\n            }\n\n            if (playing && !listeningTimer) {\n                listeningTimer = window.setInterval(\n                    renderListeningState,\n                    1000\n                );\n            } else if (!playing && listeningTimer) {\n                window.clearInterval(listeningTimer);\n                listeningTimer = null;\n            }\n\n            renderListeningState();\n        };\n        const tooltipPortal = q('.jh4-tooltip-portal');\n        const currentVoteBox = q('.jh4-current-vote');\n        const currentVoteLike = q('.jh4-current-vote-like');\n        const currentVoteDislike = q('.jh4-current-vote-dislike');\n\n        if (\n            currentVoteLike &&\n            !currentVoteLike.querySelector('.jh4-like-sparkles')\n        ) {\n            const sparkleLayer = document.createElement('span');\n            sparkleLayer.className = 'jh4-like-sparkles';\n            sparkleLayer.setAttribute('aria-hidden', 'true');\n            sparkleLayer.innerHTML =\n                '<i><\/i><i><\/i><i><\/i><i><\/i>' +\n                '<i><\/i><i><\/i><i><\/i><i><\/i>';\n\n            currentVoteLike.appendChild(sparkleLayer);\n        }\n\n        const animateLikeSparkles = () => {\n            if (!currentVoteLike) return;\n\n            currentVoteLike.classList.remove('is-sparkling');\n            void currentVoteLike.offsetWidth;\n            currentVoteLike.classList.add('is-sparkling');\n\n            window.setTimeout(() => {\n                currentVoteLike.classList.remove('is-sparkling');\n            }, 1050);\n        };\n\n        const ensureHistoryLikeSparkles = (button) => {\n            if (\n                !button ||\n                button.querySelector(\n                    '.jh4-history-like-sparkles'\n                )\n            ) {\n                return;\n            }\n\n            const layer = document.createElement('span');\n            layer.className = 'jh4-history-like-sparkles';\n            layer.setAttribute('aria-hidden', 'true');\n            layer.innerHTML =\n                '<i><\/i><i><\/i><i><\/i><i><\/i>' +\n                '<i><\/i><i><\/i><i><\/i><i><\/i>';\n\n            button.appendChild(layer);\n        };\n\n        const animateHistoryLikeSparkles = (button) => {\n            if (!button) return;\n\n            ensureHistoryLikeSparkles(button);\n\n            button.classList.remove('is-sparkling');\n            void button.offsetWidth;\n            button.classList.add('is-sparkling');\n\n            window.setTimeout(() => {\n                button.classList.remove('is-sparkling');\n            }, 1050);\n        };\n\n        let activeTrack = null;\n        let lastVolume = 0.8;\n        let transitionTimer = null;\n        let nextTransitionTimer = null;\n        let lastNextTrackKey = '';\n        let coverStackTimer = null;\n        let recentCovers = [];\n        let lastHistorySignature = '';\n        let unifiedHistory = [];\n        let metadataInitialized = false;\n        let preserveHistoryOnNextMetadata = false;\n\n        const isMobileStreamDevice = () => {\n            const userAgent =\n                navigator.userAgent ||\n                navigator.vendor ||\n                window.opera ||\n                '';\n\n            const mobileUserAgent =\n                \/Android|iPhone|iPad|iPod|IEMobile|Opera Mini|Mobile\/i\n                    .test(userAgent);\n\n            const touchTablet = (\n                navigator.maxTouchPoints > 1 &&\n                \/Macintosh\/i.test(userAgent)\n            );\n\n            return (\n                window.matchMedia('(max-width: 768px)').matches &&\n                (mobileUserAgent || touchTablet)\n            );\n        };\n\n        let selectedStream = '';\n        let activeStreamProfileId = '';\n        let activeStreamVersion = '';\n        let activeStreamProvider = 'stream';\n        let managerLiveEndpoint = '';\n        let managerSyncTimer = null;\n        let managerFadeTimer = null;\n        let managerPreloadTimer = null;\n        let managerTransitionTimer = null;\n        let managerLastLiveData = null;\n        let managerCrossfading = false;\n        let managerTransitionSettledUntil = 0;\n        let managerPreloaded = null;\n        let managerFollowingPreloaded = null;\n        const managerMemoryCache = new Map();\n        const managerMemoryCacheOrder = [];\n        const MANAGER_MEMORY_CACHE_LIMIT = 6;\n        let streamSwitching = false;\n        let secureLogo = '';\n        let secureTransparentLogo = '';\n\n        const secureConfigPromise = fetch(\n            `${root.dataset.bootstrap}?token=${\n                encodeURIComponent(\n                    root.dataset.bootstrapToken || ''\n                )\n            }`,\n            {\n                cache: 'no-store',\n                credentials: 'same-origin',\n            }\n        )\n            .then((response) => {\n                if (!response.ok) {\n                    throw new Error(\n                        `Bootstrap HTTP ${response.status}`\n                    );\n                }\n\n                return response.json();\n            })\n            .then((config) => {\n                secureLogo = String(\n                    config.logo || ''\n                );\n\n                secureTransparentLogo = String(\n                    config.logo_transparent || ''\n                );\n\n                selectedStream = (\n                    isMobileStreamDevice() &&\n                    config.mobile_stream\n                )\n                    ? String(config.mobile_stream)\n                    : String(config.stream || '');\n\n                activeStreamProfileId =\n                    String(config.profile_id || '');\n\n                activeStreamVersion =\n                    String(config.version || '');\n\n                activeStreamProvider = String(config.provider || 'stream');\n                managerLiveEndpoint = String(config.manager_live || '');\n\n                root.dataset.logo = secureLogo;\n\n                root.querySelectorAll(\n                    '[data-jh-secure-logo=\"standard\"]'\n                ).forEach((image) => {\n                    image.src = secureLogo;\n                });\n\n                root.querySelectorAll(\n                    '[data-jh-secure-logo=\"transparent\"]'\n                ).forEach((image) => {\n                    image.src =\n                        secureTransparentLogo;\n                });\n\n                if (selectedStream && activeStreamProvider !== 'manager') {\n                    audio.src = selectedStream;\n                }\n\n                return config;\n            })\n            .catch((error) => {\n                console.error(\n                    'Just Hits secure bootstrap:',\n                    error\n                );\n\n                errorBox.hidden = false;\n                errorBox.textContent =\n                    'Le player ne peut pas \u00eatre initialis\u00e9.';\n                throw error;\n            });\n\n        const ensureSecureConfig = async () => {\n            await secureConfigPromise;\n\n            if (activeStreamProvider === 'manager') {\n                if (!managerLiveEndpoint) throw new Error('Radio Manager indisponible.');\n                return managerLiveEndpoint;\n            }\n            if (!selectedStream) throw new Error('Flux radio indisponible.');\n            return selectedStream;\n        };\n\n        audio.volume = lastVolume;\n\n        const escapeHtml = (value) => String(value ?? '').replace(\/[&<>\"']\/g, (char) => ({\n            '&':'&amp;', '<':'&lt;', '>':'&gt;', '\"':'&quot;', \"'\":'&#039;'\n        })[char]);\n\n        const themeStorageKey = 'justhits_player_theme_mode';\n\n        const readSavedTheme = () => {\n            try {\n                const value = localStorage.getItem(themeStorageKey);\n                if (value) return value;\n            } catch (error) {}\n\n            const match = document.cookie.match(\n                \/(?:^|; )justhits_player_theme_mode=([^;]+)\/\n            );\n\n            return match ? decodeURIComponent(match[1]) : 'system';\n        };\n\n        const saveTheme = (mode) => {\n            try {\n                localStorage.setItem(themeStorageKey, mode);\n            } catch (error) {}\n\n            document.cookie =\n                `justhits_player_theme_mode=${encodeURIComponent(mode)};` +\n                `path=\/;max-age=31536000;SameSite=Lax`;\n        };\n\n        const resolveTheme = (mode) => {\n            if (mode === 'light' || mode === 'dark') {\n                return mode;\n            }\n            return systemThemeQuery.matches ? 'dark' : 'light';\n        };\n\n        const applyTheme = (mode, persist = true) => {\n            const safeMode = ['system', 'light', 'dark'].includes(mode)\n                ? mode\n                : 'system';\n\n            root.dataset.themeMode = safeMode;\n            root.dataset.themeResolved = resolveTheme(safeMode);\n\n            themeButtons.forEach((button) => {\n                const active = button.dataset.themeMode === safeMode;\n                button.classList.toggle('is-active', active);\n                button.setAttribute('aria-pressed', active ? 'true' : 'false');\n            });\n\n            if (persist) {\n                saveTheme(safeMode);\n            }\n        };\n\n        const initializeTheme = () => {\n            applyTheme(readSavedTheme(), false);\n        };\n\n        const scheduleLogoAnimation = () => {\n            if (!brand || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n                return;\n            }\n\n            const minDelay = 45000;\n            const maxDelay = 90000;\n            const delay = Math.floor(minDelay + Math.random() * (maxDelay - minDelay));\n\n            window.setTimeout(() => {\n                brand.classList.remove('is-logo-animated');\n                void brand.offsetWidth;\n                brand.classList.add('is-logo-animated');\n\n                window.setTimeout(() => {\n                    brand.classList.remove('is-logo-animated');\n                    scheduleLogoAnimation();\n                }, 3000);\n            }, delay);\n        };\n\n        let activeTooltipTarget = null;\n        let tooltipCloseTimer = null;\n\n        const isTooltipNeeded = (target) => {\n            if (!target || !target.isConnected) {\n                return false;\n            }\n\n            const fullText = String(\n                target.dataset.tooltip || target.textContent || ''\n            ).trim();\n\n            if (!fullText) {\n                return false;\n            }\n\n            const sizingTarget =\n                target.dataset.tooltipContainer === 'parent' &&\n                target.parentElement\n                    ? target.parentElement\n                    : target;\n\n            const targetStyle = window.getComputedStyle(target);\n            const sizingStyle = window.getComputedStyle(sizingTarget);\n            const sizingRect = sizingTarget.getBoundingClientRect();\n\n            if (sizingRect.width <= 0 || sizingRect.height <= 0) {\n                return false;\n            }\n\n            const paddingLeft =\n                parseFloat(sizingStyle.paddingLeft) || 0;\n            const paddingRight =\n                parseFloat(sizingStyle.paddingRight) || 0;\n            const paddingTop =\n                parseFloat(sizingStyle.paddingTop) || 0;\n            const paddingBottom =\n                parseFloat(sizingStyle.paddingBottom) || 0;\n\n            const availableWidth = Math.max(\n                1,\n                sizingTarget.clientWidth -\n                paddingLeft -\n                paddingRight\n            );\n\n            const availableHeight = Math.max(\n                1,\n                sizingTarget.clientHeight -\n                paddingTop -\n                paddingBottom\n            );\n\n            const measure = document.createElement('div');\n            measure.textContent = fullText;\n            measure.setAttribute('aria-hidden', 'true');\n\n            Object.assign(measure.style, {\n                position: 'fixed',\n                left: '-100000px',\n                top: '0',\n                zIndex: '-1',\n                visibility: 'hidden',\n                pointerEvents: 'none',\n                boxSizing: 'border-box',\n                width: `${availableWidth}px`,\n                minWidth: `${availableWidth}px`,\n                maxWidth: `${availableWidth}px`,\n                height: 'auto',\n                minHeight: '0',\n                maxHeight: 'none',\n                margin: '0',\n                padding: '0',\n                border: '0',\n                overflow: 'visible',\n                display: 'block',\n                whiteSpace: 'normal',\n                wordBreak: targetStyle.wordBreak,\n                overflowWrap: targetStyle.overflowWrap,\n                hyphens: targetStyle.hyphens,\n                fontFamily: targetStyle.fontFamily,\n                fontSize: targetStyle.fontSize,\n                fontWeight: targetStyle.fontWeight,\n                fontStyle: targetStyle.fontStyle,\n                fontStretch: targetStyle.fontStretch,\n                fontVariant: targetStyle.fontVariant,\n                lineHeight: targetStyle.lineHeight,\n                letterSpacing: targetStyle.letterSpacing,\n                textTransform: targetStyle.textTransform\n            });\n\n            document.body.appendChild(measure);\n\n            const naturalRect = measure.getBoundingClientRect();\n            const naturalHeight = Math.ceil(naturalRect.height);\n            const naturalWidth = Math.ceil(measure.scrollWidth);\n\n            measure.remove();\n\n            const isSingleLine =\n                targetStyle.whiteSpace === 'nowrap';\n\n            if (isSingleLine) {\n                return naturalWidth > availableWidth + 1;\n            }\n\n            return naturalHeight > availableHeight + 1;\n        };\n\n        const refreshTooltipEligibility = (scope = root) => {\n            scope.querySelectorAll('.jh4-tooltip-title[data-tooltip]').forEach((target) => {\n                const needed = isTooltipNeeded(target);\n\n                target.dataset.tooltipEnabled = needed ? '1' : '0';\n                target.classList.toggle('has-active-tooltip', needed);\n\n                if (needed) {\n                    target.setAttribute('tabindex', '0');\n                    target.setAttribute('aria-label', target.dataset.tooltip || '');\n                } else {\n                    target.removeAttribute('tabindex');\n                    target.removeAttribute('aria-label');\n\n                    if (activeTooltipTarget === target) {\n                        closeTooltipPortal();\n                    }\n                }\n            });\n        };\n\n        const positionTooltipPortal = (target) => {\n            if (!tooltipPortal || !target) return;\n\n            const anchorTarget =\n                target.dataset.tooltipContainer === 'parent' &&\n                target.parentElement\n                    ? target.parentElement\n                    : target;\n\n            const targetRect = anchorTarget.getBoundingClientRect();\n            const tooltipRect = tooltipPortal.getBoundingClientRect();\n            const padding = 14;\n            const gap = 12;\n\n            let left = targetRect.left + targetRect.width \/ 2 - tooltipRect.width \/ 2;\n            left = Math.max(\n                padding,\n                Math.min(left, window.innerWidth - tooltipRect.width - padding)\n            );\n\n            let top = targetRect.top - tooltipRect.height - gap;\n            let below = false;\n\n            if (top < padding) {\n                top = targetRect.bottom + gap;\n                below = true;\n            }\n\n            const targetCenter = targetRect.left + targetRect.width \/ 2;\n            const arrowLeft = Math.max(\n                14,\n                Math.min(tooltipRect.width - 14, targetCenter - left)\n            );\n\n            tooltipPortal.classList.toggle('is-below', below);\n            tooltipPortal.style.left = `${Math.round(left)}px`;\n            tooltipPortal.style.top = `${Math.round(top)}px`;\n            tooltipPortal.style.setProperty(\n                '--jh4-tooltip-arrow-left',\n                `${Math.round(arrowLeft)}px`\n            );\n        };\n\n        const openTooltipPortal = (target) => {\n            if (!tooltipPortal || !target) return;\n\n            const needed = isTooltipNeeded(target);\n\n            target.dataset.tooltipEnabled = needed ? '1' : '0';\n            target.classList.toggle('has-active-tooltip', needed);\n\n            if (!needed) {\n                closeTooltipPortal();\n                return;\n            }\n\n            const content = String(target.dataset.tooltip || '').trim();\n            if (!content) return;\n\n            window.clearTimeout(tooltipCloseTimer);\n            activeTooltipTarget = target;\n\n            tooltipPortal.textContent = content;\n            tooltipPortal.setAttribute('aria-hidden', 'false');\n            tooltipPortal.classList.add('is-visible');\n\n            requestAnimationFrame(() => {\n                positionTooltipPortal(target);\n            });\n        };\n\n        const closeTooltipPortal = () => {\n            if (!tooltipPortal) return;\n\n            tooltipCloseTimer = window.setTimeout(() => {\n                tooltipPortal.classList.remove('is-visible');\n                tooltipPortal.setAttribute('aria-hidden', 'true');\n                activeTooltipTarget = null;\n            }, 70);\n        };\n\n        const bindTooltipTargets = (scope = root) => {\n            scope.querySelectorAll('.jh4-tooltip-title[data-tooltip]').forEach((target) => {\n                if (target.dataset.jh4TooltipBound !== '1') {\n                    target.dataset.jh4TooltipBound = '1';\n\n                    target.addEventListener('mouseenter', () => openTooltipPortal(target));\n                    target.addEventListener('mouseleave', closeTooltipPortal);\n                    target.addEventListener('focus', () => openTooltipPortal(target));\n                    target.addEventListener('blur', closeTooltipPortal);\n                }\n            });\n\n            requestAnimationFrame(() => {\n                requestAnimationFrame(() => refreshTooltipEligibility(scope));\n            });\n        };\n\n        window.addEventListener(\n            'scroll',\n            () => {\n                if (activeTooltipTarget) {\n                    positionTooltipPortal(activeTooltipTarget);\n                }\n            },\n            { passive:true }\n        );\n\n        let tooltipResizeTimer = null;\n\n        window.addEventListener('resize', () => {\n            window.clearTimeout(tooltipResizeTimer);\n\n            tooltipResizeTimer = window.setTimeout(() => {\n                refreshTooltipEligibility(root);\n\n                if (activeTooltipTarget) {\n                    if (isTooltipNeeded(activeTooltipTarget)) {\n                        positionTooltipPortal(activeTooltipTarget);\n                    } else {\n                        closeTooltipPortal();\n                    }\n                }\n            }, 160);\n        }, { passive:true });\n\n        const animateLogoOnTrackChange = () => {\n            if (\n                !brand ||\n                window.matchMedia('(prefers-reduced-motion: reduce)').matches\n            ) {\n                return;\n            }\n\n            brand.classList.remove('is-logo-animated');\n\n            window.requestAnimationFrame(() => {\n                void brand.offsetWidth;\n                brand.classList.add('is-logo-animated');\n\n                window.setTimeout(() => {\n                    brand.classList.remove('is-logo-animated');\n                }, 2900);\n            });\n        };\n\n        const updateMediaSessionMetadata = (track) => {\n            if (\n                !('mediaSession' in navigator) ||\n                typeof window.MediaMetadata !== 'function'\n            ) {\n                return;\n            }\n\n            const coverUrl =\n                String(track?.cover_url || '').trim() ||\n                String(root.dataset.logo || '').trim();\n\n            const artwork = coverUrl\n                ? [\n                    {\n                        src: coverUrl\n                    }\n                ]\n                : [];\n\n            try {\n                navigator.mediaSession.metadata = new MediaMetadata({\n                    title: track?.title || 'JUST HITS',\n                    artist: track?.artist || 'Only the biggest hits',\n                    album: track?.album || 'JUST HITS',\n                    artwork\n                });\n            } catch (error) {\n                console.warn(\n                    'Just Hits Media Session metadata :',\n                    error\n                );\n            }\n        };\n\n        const initializeMediaSession = () => {\n            if (!('mediaSession' in navigator)) {\n                return;\n            }\n\n            const safeSetActionHandler = (action, handler) => {\n                try {\n                    navigator.mediaSession.setActionHandler(\n                        action,\n                        handler\n                    );\n                } catch (error) {\n                    \/\/ Certaines actions ne sont pas disponibles\n                    \/\/ selon le navigateur ou la version d\u2019iOS.\n                }\n            };\n\n            safeSetActionHandler('play', async () => {\n                try {\n                    if (audio.paused) {\n                        await togglePlayback();\n                    } else {\n                        await audio.play();\n                    }\n                } catch (error) {\n                    console.warn(\n                        'Just Hits Media Session play :',\n                        error\n                    );\n                }\n            });\n\n            safeSetActionHandler('pause', () => {\n                audio.pause();\n            });\n\n            \/*\n             * Le flux est en direct : on d\u00e9sactive les commandes\n             * de recherche avant\/arri\u00e8re lorsqu\u2019elles sont prises en charge.\n             *\/\n            safeSetActionHandler('seekbackward', null);\n            safeSetActionHandler('seekforward', null);\n            safeSetActionHandler('seekto', null);\n            safeSetActionHandler('previoustrack', null);\n            safeSetActionHandler('nexttrack', null);\n\n            updateMediaSessionMetadata(activeTrack);\n        };\n\n        const updateMediaSessionPlaybackState = () => {\n            if (!('mediaSession' in navigator)) {\n                return;\n            }\n\n            try {\n                navigator.mediaSession.playbackState = audio.paused\n                    ? 'paused'\n                    : 'playing';\n            } catch (error) {\n                \/\/ Certains navigateurs ignorent cette propri\u00e9t\u00e9.\n            }\n        };\n\n        const formatBroadcastTime = (value) => {\n            if (!value) return '';\n\n            const date = new Date(value);\n            if (!Number.isFinite(date.getTime())) return '';\n\n            return new Intl.DateTimeFormat('fr-FR', {\n                hour: '2-digit',\n                minute: '2-digit',\n                hour12: false\n            }).format(date);\n        };\n\n        const formatTime = (seconds) => {\n            if (!Number.isFinite(seconds) || seconds < 0) return '--:--';\n            const minutes = Math.floor(seconds \/ 60);\n            const rest = Math.floor(seconds % 60);\n            return `${String(minutes).padStart(2,'0')}:${String(rest).padStart(2,'0')}`;\n        };\n\n        const normalizeTrackText = (value) => String(value || '')\n            .normalize('NFD')\n            .replace(\/[\\u0300-\\u036f]\/g, '')\n            .toLowerCase()\n            .replace(\/[^a-z0-9]+\/g, ' ')\n            .trim();\n\n        const displayAlbum = (track) => {\n            const albumName = String(track?.album || '').trim();\n            const trackTitle = String(track?.title || '').trim();\n\n            if (\n                !albumName ||\n                normalizeTrackText(albumName) === normalizeTrackText(trackTitle)\n            ) {\n                album.textContent = '';\n                album.hidden = true;\n                return;\n            }\n\n            album.textContent = `Album : ${albumName}`;\n            album.hidden = false;\n        };\n\n        const setImage = (image, url, visibleClass = 'on') => {\n            image.classList.remove(visibleClass);\n            if (!url) {\n                image.removeAttribute('src');\n                return;\n            }\n\n            image.onload = () => image.classList.add(visibleClass);\n            image.onerror = () => {\n                image.classList.remove(visibleClass);\n                image.removeAttribute('src');\n            };\n\n            \/\/ Ne pas ajouter de param\u00e8tre aux URL sign\u00e9es ou aux data URI.\n            image.src = url;\n        };\n\n        const runTrackTransition = (callback) => {\n            clearTimeout(transitionTimer);\n            root.classList.add('track-transition');\n\n            transitionTimer = window.setTimeout(() => {\n                callback();\n                requestAnimationFrame(() => {\n                    requestAnimationFrame(() => root.classList.remove('track-transition'));\n                });\n            }, 430);\n        };\n\n        const refreshCoverStack = (newCoverUrl) => {\n            if (newCoverUrl) {\n                recentCovers = [\n                    newCoverUrl,\n                    ...recentCovers.filter((url) => url && url !== newCoverUrl)\n                ].slice(0, 4);\n            }\n\n            coverGhosts.forEach((image, index) => {\n                const source = recentCovers[index + 1] || recentCovers[0] || (root.dataset.logo || secureLogo || '');\n\n                image.onload = () => {\n                    image.style.opacity = '1';\n                };\n\n                image.onerror = () => {\n                    image.src = (root.dataset.logo || secureLogo || '');\n                };\n\n                image.src = source;\n            });\n\n            clearTimeout(coverStackTimer);\n            root.classList.remove('cover-stack-burst');\n            void root.offsetWidth;\n            root.classList.add('cover-stack-burst');\n\n            coverStackTimer = window.setTimeout(() => {\n                root.classList.remove('cover-stack-burst');\n            }, 1100);\n        };\n\n        const syncCurrentVote = (track) => {\n            \/*\n             * Lorsque le snippet de classement est d\u00e9sactiv\u00e9, les boutons\n             * n'existent pas. On quitte imm\u00e9diatement afin de ne jamais\n             * interrompre le rafra\u00eechissement du player.\n             *\n             * Le conteneur reste volontairement pr\u00e9sent, car les modules\n             * de d\u00e9dicaces audio et \u00e9crites y ajoutent leurs propres boutons.\n             *\/\n            if (\n                !rankingEnabled ||\n                !currentVoteBox ||\n                !currentVoteLike ||\n                !currentVoteDislike ||\n                !track\n            ) {\n                return;\n            }\n\n            const key = track.vote_key || '';\n            currentVoteBox.dataset.key = key;\n            currentVoteBox.dataset.title = track.title || '';\n            currentVoteBox.dataset.artist = track.artist || '';\n            currentVoteBox.dataset.airing =\n                track.started_at || track.end_at || '';\n\n            currentVoteLike.classList.remove('is-selected');\n            currentVoteDislike.classList.remove('is-selected');\n            currentVoteLike.disabled = false;\n            currentVoteDislike.disabled = false;\n\n            const previousVote = localStorage.getItem(\n                voteStorageKey(\n                    key,\n                    currentVoteBox.dataset.airing || ''\n                )\n            );\n\n            if (previousVote === 'plus') {\n                currentVoteLike.classList.add('is-selected');\n                currentVoteLike.disabled = true;\n                currentVoteDislike.disabled = true;\n            } else if (previousVote === 'minus') {\n                currentVoteDislike.classList.add('is-selected');\n                currentVoteLike.disabled = true;\n                currentVoteDislike.disabled = true;\n            }\n        };\n\n        const showCurrent = (track) => {\n            const oldKey = activeTrack ? `${activeTrack.title}|${activeTrack.artist}` : '';\n            const newKey = track ? `${track.title}|${track.artist}` : '';\n\n            if (oldKey === newKey && activeTrack) {\n                \/\/ La pochette peut \u00eatre arriv\u00e9e lors d'une r\u00e9ponse suivante.\n                if (\n                    track.cover_url &&\n                    currentCover.getAttribute('src') !== track.cover_url\n                ) {\n                    setImage(currentCover, track.cover_url);\n                    setImage(backgroundImage, track.cover_url);\n                }\n\n                activeTrack = track;\n                updateMediaSessionMetadata(track);\n                return;\n            }\n\n            runTrackTransition(() => {\n                activeTrack = track;\n\n                if (!track) {\n                    title.textContent = 'JUST HITS';\n                    artist.textContent = 'Only the biggest hits';\n                    album.textContent = '';\n                    album.hidden = true;\n                    currentCover.classList.remove('on');\n                    backgroundImage.classList.remove('on');\n                    if (requestKpi && requestKpiName) {\n                        requestKpi.hidden = true;\n                        requestKpiName.textContent = '';\n                    }\n                    updateMediaSessionMetadata(null);\n                    return;\n                }\n\n                title.textContent = track.title || 'Titre inconnu';\n                title.setAttribute('data-tooltip', track.title || 'Titre inconnu');\n                artist.textContent = track.artist || 'JUST HITS';\n                const requestedBy = String(track.requested_by || '').replace(\/^[#@\\s]+\/, '').trim();\n                if (requestKpi && requestKpiName) {\n                    requestKpi.hidden = !requestedBy;\n                    requestKpiName.textContent = requestedBy || '';\n                }\n\n                bindTooltipTargets(root);\n\n                requestAnimationFrame(() => {\n                    requestAnimationFrame(() => {\n                        refreshTooltipEligibility(root);\n                    });\n                });\n                displayAlbum(track);\n                setImage(currentCover, track.cover_url);\n                setImage(backgroundImage, track.cover_url);\n                refreshCoverStack(track.cover_url || (root.dataset.logo || secureLogo || ''));\n                syncCurrentVote(track);\n                updateMediaSessionMetadata(track);\n\n                document.title = `${track.artist || 'JUST HITS'} \u2013 ${track.title || 'En direct'}`;\n                animateLogoOnTrackChange();\n            });\n        };\n\n        const showNext = (track) => {\n            const nextKey = track\n                ? [track.artist || '', track.title || '', track.cover_url || ''].join('|')\n                : '__unavailable__';\n\n            const updateNextContent = () => {\n                if (!track) {\n                    nextTitle.textContent = 'Prochain titre indisponible';\n                    nextTitle.setAttribute(\n                        'data-tooltip',\n                        'Prochain titre indisponible'\n                    );\n                    nextArtist.textContent = 'JUST HITS';\n                    if (nextRequester) { nextRequester.hidden = true; nextRequester.textContent = ''; }\n                    nextCover.classList.remove('on');\n                    nextCover.removeAttribute('src');\n                    return;\n                }\n\n                nextTitle.textContent = track.title || 'Titre \u00e0 venir';\n                nextTitle.setAttribute('data-tooltip', track.title || 'Titre \u00e0 venir');\n                nextArtist.textContent = track.artist || 'JUST HITS';\n                const requestedBy = String(track.requested_by || '').replace(\/^[#@\\s]+\/, '').trim();\n                if (nextRequester) {\n                    nextRequester.hidden = !requestedBy;\n                    nextRequester.textContent = requestedBy ? ('Demand\u00e9 par ' + requestedBy) : '';\n                }\n\n                bindTooltipTargets(root);\n\n                requestAnimationFrame(() => {\n                    requestAnimationFrame(() => {\n                        refreshTooltipEligibility(root);\n                    });\n                });\n\n                window.setTimeout(() => {\n                    refreshTooltipEligibility(root);\n                }, 120);\n\n                setImage(nextCover, track.cover_url);\n            };\n\n            \/* Pas d'animation au premier chargement ni si les m\u00e9tadonn\u00e9es\n               re\u00e7ues correspondent toujours au m\u00eame prochain titre. *\/\n            if (!nextCard || !lastNextTrackKey || nextKey === lastNextTrackKey) {\n                updateNextContent();\n                lastNextTrackKey = nextKey;\n                return;\n            }\n\n            clearTimeout(nextTransitionTimer);\n            nextCard.classList.remove('next-track-in');\n            nextCard.classList.add('next-track-out');\n\n            nextTransitionTimer = window.setTimeout(() => {\n                updateNextContent();\n                lastNextTrackKey = nextKey;\n\n                nextCard.classList.remove('next-track-out');\n                nextCard.classList.add('next-track-in');\n\n                requestAnimationFrame(() => {\n                    requestAnimationFrame(() => {\n                        nextCard.classList.remove('next-track-in');\n                    });\n                });\n            }, 230);\n        };\n\n        const historyTrackKey = (track) => {\n            if (!track) return '';\n\n            return [\n                String(track.title || '').trim().toLowerCase(),\n                String(track.artist || '').trim().toLowerCase(),\n                String(\n                    track.started_at ||\n                    track.end_at ||\n                    ''\n                ).trim()\n            ].join('|');\n        };\n\n        const sameHistoryTrack = (first, second) => {\n            if (!first || !second) return false;\n\n            return (\n                String(first.title || '').trim().toLowerCase() ===\n                    String(second.title || '').trim().toLowerCase() &&\n                String(first.artist || '').trim().toLowerCase() ===\n                    String(second.artist || '').trim().toLowerCase()\n            );\n        };\n\n        const mergeUnifiedHistory = (\n            tracks,\n            prepend = false\n        ) => {\n            const incoming = Array.isArray(tracks)\n                ? tracks.filter(Boolean)\n                : [];\n\n            const combined = prepend\n                ? [...incoming, ...unifiedHistory]\n                : [...unifiedHistory, ...incoming];\n\n            const seen = new Set();\n            const merged = [];\n\n            combined.forEach((track) => {\n                const key = historyTrackKey(track);\n\n                if (!key || seen.has(key)) return;\n\n                seen.add(key);\n                merged.push(track);\n            });\n\n            unifiedHistory = merged.slice(0, 12);\n\n            return unifiedHistory.slice(0, 4);\n        };\n\n        const pushPreviousCurrentToHistory = (\n            previousTrack,\n            nextTrack\n        ) => {\n            if (\n                !previousTrack ||\n                sameHistoryTrack(previousTrack, nextTrack)\n            ) {\n                return;\n            }\n\n            mergeUnifiedHistory(\n                [{\n                    ...previousTrack,\n                    end_at: new Date().toISOString()\n                }],\n                true\n            );\n        };\n\n        const showHistory = (tracks) => {\n            const safeTracks = Array.isArray(tracks) ? tracks.slice(0, 4) : [];\n            const historySignature = safeTracks\n                .map((track) => `${track.title || ''}|${track.artist || ''}|${track.cover_url || ''}`)\n                .join('||');\n\n            if (historySignature === lastHistorySignature) {\n                return;\n            }\n\n            lastHistorySignature = historySignature;\n\n            if (!safeTracks.length) {\n                historyGrid.innerHTML = '<div class=\"jh4-loading\">Aucun historique disponible pour le moment.<\/div>';\n                return;\n            }\n\n            historyGrid.innerHTML = safeTracks.map((track, index) => {\n                const cover = track.cover_url\n                    ? `<img decoding=\"async\" class=\"jh4-history-cover\" src=\"${escapeHtml(track.cover_url)}\" alt=\"\" loading=\"lazy\" referrerpolicy=\"no-referrer\">`\n                    : '';\n\n                return `\n                    <article\n                        class=\"jh4-history-item is-entering\"\n                        style=\"--jh4-enter-delay:${index * 90}ms\"\n                    >\n                        <div class=\"jh4-history-cover-wrap\">\n                            <img decoding=\"async\" class=\"jh4-history-fallback\" src=\"${escapeHtml((root.dataset.logo || secureLogo || ''))}\" alt=\"\">\n                            ${cover}\n                        <\/div>\n                        <div class=\"jh4-history-copy\">\n                            <strong class=\"jh4-tooltip-title\" data-tooltip-container=\"self\" data-tooltip=\"${escapeHtml(track.title || 'Titre inconnu')}\">${escapeHtml(track.title || 'Titre inconnu')}<\/strong>\n                            <span>${escapeHtml(track.artist || 'JUST HITS')}<\/span>\n\n                            ${\n                                formatBroadcastTime(track.started_at || track.end_at)\n                                    ? `<small class=\"jh4-history-time\">\n                                        <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n                                            <circle cx=\"12\" cy=\"12\" r=\"8\"><\/circle>\n                                            <path d=\"M12 8v5l3 2\"><\/path>\n                                        <\/svg>\n                                        ${escapeHtml(\n                                            formatBroadcastTime(\n                                                track.started_at || track.end_at\n                                            )\n                                        )}\n                                    <\/small>`\n                                    : ''\n                            }\n\n                            ${\n                                rankingEnabled\n                                    ? `\n                                        <div class=\"jh4-history-vote\"\n                                             data-title=\"${escapeHtml(track.title || '')}\"\n                                             data-artist=\"${escapeHtml(track.artist || '')}\"\n                                             data-key=\"${escapeHtml(track.vote_key || '')}\"\n                                             data-airing=\"${escapeHtml(track.started_at || track.end_at || '')}\">\n                                            <button\n                                                type=\"button\"\n                                                class=\"jh4-vote-button jh4-vote-plus\"\n                                                aria-label=\"J'aime ce titre\"\n                                            >\u2665<\/button>\n\n                                            <button\n                                                type=\"button\"\n                                                class=\"jh4-vote-button jh4-vote-minus\"\n                                                aria-label=\"Je n'aime pas ce titre\"\n                                            >\ud83d\udc94<\/button>\n                                        <\/div>\n                                    `\n                                    : ''\n                            }\n                        <\/div>\n                    <\/article>\n                `;\n            }).join('');\n\n            bindVoteButtons();\n            bindTooltipTargets(historyGrid);\n\n            requestAnimationFrame(() => {\n                requestAnimationFrame(() => refreshTooltipEligibility(historyGrid));\n            });\n\n            window.setTimeout(() => {\n                historyGrid.querySelectorAll('.jh4-history-item.is-entering').forEach((item) => {\n                    item.classList.remove('is-entering');\n                    item.style.removeProperty('--jh4-enter-delay');\n                });\n            }, 1200);\n        };\n\n        const voteStorageKey = (key, airing = '') => {\n            const occurrence = String(airing || 'same-track').trim();\n            return `justhits_vote_${key}_${occurrence}`;\n        };\n\n        const submitVote = async (container, direction, button) => {\n            if (!rankingEnabled || !voteApi) {\n                return;\n            }\n\n            const key = container.dataset.key;\n            const airing = container.dataset.airing || '';\n            const previousVote = localStorage.getItem(\n                voteStorageKey(key, airing)\n            );\n\n            if (previousVote) {\n                return;\n            }\n\n            const buttons = container.querySelectorAll('.jh4-vote-button');\n            buttons.forEach((item) => item.disabled = true);\n\n            try {\n                const response = await fetch(voteApi, {\n                    method: 'POST',\n                    headers: {\n                        'Content-Type': 'application\/json'\n                    },\n                    body: JSON.stringify({\n                        title: container.dataset.title,\n                        artist: container.dataset.artist,\n                        direction\n                    })\n                });\n\n                if (!response.ok) {\n                    throw new Error(`HTTP ${response.status}`);\n                }\n\n                await response.json();\n                button.classList.add('is-selected');\n                localStorage.setItem(\n                    voteStorageKey(key, airing),\n                    direction\n                );\n\n                button.classList.add('is-vote-animated');\n\n                if (direction === 'plus') {\n                    if (button === currentVoteLike) {\n                        animateLikeSparkles();\n                    } else if (\n                        button.classList.contains(\n                            'jh4-vote-plus'\n                        )\n                    ) {\n                        animateHistoryLikeSparkles(button);\n                    }\n                }\n\n                window.setTimeout(() => {\n                    button.classList.remove('is-vote-animated');\n                }, 700);\n            } catch (error) {\n                console.error('Just Hits vote:', error);\n                buttons.forEach((item) => item.disabled = false);\n            }\n        };\n\n        const bindVoteButtons = () => {\n            root.querySelectorAll('.jh4-history-vote').forEach((container) => {\n                const key = container.dataset.key;\n                const airing = container.dataset.airing || '';\n                const previousVote = localStorage.getItem(\n                    voteStorageKey(key, airing)\n                );\n                const plus = container.querySelector('.jh4-vote-plus');\n                const minus = container.querySelector('.jh4-vote-minus');\n\n                ensureHistoryLikeSparkles(plus);\n\n                if (previousVote === 'plus') {\n                    plus.classList.add('is-selected');\n                    plus.disabled = true;\n                    minus.disabled = true;\n                } else if (previousVote === 'minus') {\n                    minus.classList.add('is-selected');\n                    plus.disabled = true;\n                    minus.disabled = true;\n                }\n\n                plus.addEventListener('click', () => submitVote(container, 'plus', plus));\n                minus.addEventListener('click', () => submitVote(container, 'minus', minus));\n            });\n        };\n\n        const updateProgress = () => {\n            if (!activeTrack) {\n                progressFill.style.width = '0%';\n                progressDot.style.left = '0%';\n                elapsedLabel.textContent = 'EN DIRECT';\n                durationLabel.textContent = 'LIVE';\n                return;\n            }\n\n            let duration = Number(activeTrack.duration || 0);\n            let elapsed = 0;\n\n            if (activeTrack.started_at) {\n                const start = new Date(activeTrack.started_at).getTime();\n                if (Number.isFinite(start)) {\n                    elapsed = Math.max(0, (Date.now() - start) \/ 1000);\n                }\n            }\n\n            if (!duration && activeTrack.started_at && activeTrack.end_at) {\n                const start = new Date(activeTrack.started_at).getTime();\n                const end = new Date(activeTrack.end_at).getTime();\n                if (Number.isFinite(start) && Number.isFinite(end) && end > start) {\n                    duration = (end - start) \/ 1000;\n                }\n            }\n\n            if (duration <= 0) {\n                const simulated = ((Date.now() \/ 1000) % 180) \/ 180 * 100;\n                progressFill.style.width = `${simulated}%`;\n                progressDot.style.left = `${simulated}%`;\n                elapsedLabel.textContent = 'EN DIRECT';\n                durationLabel.textContent = 'LIVE';\n                return;\n            }\n\n            elapsed = Math.min(elapsed, duration);\n            const percent = Math.max(0, Math.min(100, elapsed \/ duration * 100));\n            progressFill.style.width = `${percent}%`;\n            progressDot.style.left = `${percent}%`;\n            elapsedLabel.textContent = formatTime(elapsed);\n            durationLabel.textContent = formatTime(duration);\n        };\n\n\n        const fadeStreamAudio = (\n            from,\n            to,\n            duration\n        ) => new Promise(resolve => {\n            const started = performance.now();\n            const safeFrom = Math.max(0, Math.min(1, Number(from) || 0));\n            const safeTo = Math.max(0, Math.min(1, Number(to) || 0));\n\n            audio.volume = safeFrom;\n\n            const timer = window.setInterval(() => {\n                const progress = Math.min(\n                    1,\n                    (performance.now() - started) \/ duration\n                );\n\n                audio.volume =\n                    safeFrom +\n                    (safeTo - safeFrom) * progress;\n\n                if (progress >= 1) {\n                    window.clearInterval(timer);\n                    audio.volume = safeTo;\n                    resolve();\n                }\n            }, 30);\n        });\n\n        const normalizeStreamUrl = (value) => {\n            try {\n                const url = new URL(\n                    String(value || ''),\n                    window.location.href\n                );\n\n                \/*\n                 * Les param\u00e8tres ajout\u00e9s pour contourner le cache\n                 * ne doivent pas emp\u00eacher la comparaison des flux.\n                 *\/\n                url.searchParams.delete('live');\n                url.searchParams.delete('_');\n                url.searchParams.delete('t');\n\n                return (\n                    url.origin +\n                    url.pathname\n                ).replace(\/\\\/+$\/, '');\n            } catch (error) {\n                return String(value || '')\n                    .split('?')[0]\n                    .replace(\/\\\/+$\/, '');\n            }\n        };\n\n        const waitForAudioStart = (\n            timeout = 7000,\n            deck = audio\n        ) => new Promise((resolve, reject) => {\n            let finished = false;\n\n            const cleanup = () => {\n                deck.removeEventListener(\n                    'playing',\n                    handlePlaying\n                );\n\n                deck.removeEventListener(\n                    'canplay',\n                    handleCanPlay\n                );\n\n                deck.removeEventListener(\n                    'error',\n                    handleError\n                );\n\n                window.clearTimeout(timer);\n            };\n\n            const succeed = () => {\n                if (finished) return;\n                finished = true;\n                cleanup();\n                resolve();\n            };\n\n            const fail = () => {\n                if (finished) return;\n                finished = true;\n                cleanup();\n\n                reject(\n                    new Error(\n                        'Le nouveau flux audio ne d\u00e9marre pas.'\n                    )\n                );\n            };\n\n            const handlePlaying = succeed;\n            const handleCanPlay = succeed;\n            const handleError = fail;\n\n            const timer = window.setTimeout(\n                fail,\n                timeout\n            );\n\n            deck.addEventListener(\n                'playing',\n                handlePlaying,\n                {\n                    once: true\n                }\n            );\n\n            deck.addEventListener(\n                'canplay',\n                handleCanPlay,\n                {\n                    once: true\n                }\n            );\n\n            deck.addEventListener(\n                'error',\n                handleError,\n                {\n                    once: true\n                }\n            );\n\n            if (\n                audio.readyState >=\n                HTMLMediaElement.HAVE_FUTURE_DATA\n            ) {\n                succeed();\n            }\n        });\n\n        const switchScheduledStream = async config => {\n            if (streamSwitching) return;\n\n            const nextStream = (\n                isMobileStreamDevice() &&\n                config.mobile_stream\n            )\n                ? String(config.mobile_stream)\n                : String(config.stream || '');\n\n            const nextId =\n                String(config.profile_id || '');\n\n            const nextVersion =\n                String(config.version || '');\n\n            const nextProvider = String(config.provider || 'stream');\n            const nextManagerLive = String(config.manager_live || '');\n\n            const actualStream = normalizeStreamUrl(\n                audio.currentSrc ||\n                audio.src ||\n                selectedStream\n            );\n\n            const expectedStream =\n                normalizeStreamUrl(nextStream);\n\n            \/*\n             * On ne consid\u00e8re le flux comme correct que si :\n             * - le profil est le bon ;\n             * - sa version est la bonne ;\n             * - l'\u00e9l\u00e9ment audio lit r\u00e9ellement la bonne URL.\n             *\n             * Ainsi, si une premi\u00e8re bascule \u00e9choue, le player\n             * retentera automatiquement 5 secondes plus tard.\n             *\/\n            const nextSourceAvailable = nextProvider === 'manager'\n                ? Boolean(nextManagerLive)\n                : Boolean(nextStream);\n\n            if (\n                !nextSourceAvailable ||\n                (\n                    nextId === activeStreamProfileId &&\n                    nextVersion === activeStreamVersion &&\n                    nextProvider === activeStreamProvider &&\n                    (nextProvider === 'manager' || actualStream === expectedStream)\n                )\n            ) {\n                return;\n            }\n\n            streamSwitching = true;\n\n            const wasPlaying =\n                !audio.paused &&\n                !audio.ended;\n\n            const previousStream =\n                selectedStream ||\n                audio.currentSrc ||\n                audio.src ||\n                '';\n\n            const previousId =\n                activeStreamProfileId;\n\n            const previousVersion =\n                activeStreamVersion;\n\n            const previousProvider = activeStreamProvider;\n            const previousManagerLive = managerLiveEndpoint;\n\n            const previousMuted =\n                Boolean(audio.muted);\n\n            const targetVolume =\n                Math.max(\n                    0,\n                    Math.min(\n                        1,\n                        Number(lastVolume) || .75\n                    )\n                );\n\n            try {\n                streamSwitchCard.textContent =\n                    config.profile_name\n                        ? `Place \u00e0 ${config.profile_name}`\n                        : 'Changement de programme\u2026';\n\n                streamSwitchOverlay.classList.add(\n                    'is-visible'\n                );\n\n                if (wasPlaying) {\n                    await fadeStreamAudio(\n                        audio.muted\n                            ? 0\n                            : audio.volume,\n                        0,\n                        700\n                    );\n                }\n\n                audio.pause();\n\n                \/*\n                 * Arr\u00eat r\u00e9el de l'ancien flux r\u00e9seau.\n                 *\/\n                audio.removeAttribute('src');\n                audio.load();\n\n                preserveHistoryOnNextMetadata = true;\n                updateProgress();\n\n                audio.muted = false;\n                audio.volume = wasPlaying ? 0 : targetVolume;\n                activeStreamProvider = nextProvider;\n                managerLiveEndpoint = nextManagerLive;\n\n                if (nextProvider === 'manager') {\n                    if (!managerLiveEndpoint) {\n                        throw new Error('API du Radio Manager absente.');\n                    }\n\n                    try {\n                        await syncManagerPlayback(wasPlaying);\n\n                        if (wasPlaying) {\n                            await fadeStreamAudio(0, targetVolume, 900);\n                        }\n                    } catch (managerError) {\n                        \/*\n                         * Chrome\/Safari peuvent refuser audio.play() lorsqu'une\n                         * bascule planifi\u00e9e remplace compl\u00e8tement la source. La\n                         * source Manager reste n\u00e9anmoins valid\u00e9e : un clic sur le\n                         * bouton central reprend alors imm\u00e9diatement le direct,\n                         * sans relancer la transition toutes les secondes.\n                         *\/\n                        const autoplayBlocked =\n                            managerError &&\n                            (managerError.name === 'NotAllowedError' ||\n                             \/play\\(\\)|autoplay|user gesture|interaction\/i.test(\n                                 String(managerError.message || managerError)\n                             ));\n\n                        if (!autoplayBlocked) {\n                            throw managerError;\n                        }\n\n                        console.warn(\n                            'Just Hits Manager autoplay bloqu\u00e9 :',\n                            managerError\n                        );\n\n                        audio.pause();\n                        launchOverlay?.classList.remove('is-hidden');\n                        launchOverlay?.setAttribute('aria-hidden', 'false');\n                        if (launchPlay) launchPlay.disabled = false;\n                    }\n                } else {\n                    const separator = nextStream.includes('?') ? '&' : '?';\n                    const freshStream = `${nextStream}${separator}live=${Date.now()}`;\n                    audio.src = freshStream;\n                    audio.load();\n                    if (wasPlaying) {\n                        const started = waitForAudioStart();\n                        await audio.play();\n                        await started;\n                        await fadeStreamAudio(0, targetVolume, 900);\n                    }\n                }\n\n                \/*\n                 * On m\u00e9morise le nouveau profil seulement apr\u00e8s\n                 * confirmation du chargement du bon flux.\n                 *\/\n                selectedStream = nextStream;\n                activeStreamProfileId = nextId;\n                activeStreamVersion = nextVersion;\n                activeStreamProvider = nextProvider;\n                managerLiveEndpoint = nextManagerLive;\n                audio.muted = previousMuted;\n\n                await loadMetadata();\n\n                window.setTimeout(\n                    () => {\n                        streamSwitchOverlay.classList.remove(\n                            'is-visible'\n                        );\n                    },\n                    450\n                );\n            } catch (error) {\n                console.error(\n                    'Just Hits stream switch:',\n                    error\n                );\n\n                \/*\n                 * La bascule n'est pas valid\u00e9e : le contr\u00f4le p\u00e9riodique\n                 * pourra la retenter. On remet l'ancien flux si possible.\n                 *\/\n                selectedStream = previousStream;\n                activeStreamProfileId = previousId;\n                activeStreamVersion = previousVersion;\n                activeStreamProvider = previousProvider;\n                managerLiveEndpoint = previousManagerLive;\n\n                try {\n                    if (previousStream && previousProvider !== 'manager') {\n                        const separator =\n                            previousStream.includes('?')\n                                ? '&'\n                                : '?';\n\n                        audio.src =\n                            `${previousStream}${separator}` +\n                            `resume=${Date.now()}`;\n\n                        audio.load();\n\n                        if (wasPlaying) {\n                            audio.volume = 0;\n                            audio.muted = false;\n\n                            await audio.play();\n\n                            await fadeStreamAudio(\n                                0,\n                                targetVolume,\n                                650\n                            );\n\n                            audio.muted =\n                                previousMuted;\n                        }\n                    }\n                } catch (restoreError) {\n                    console.warn(\n                        'Just Hits restore previous stream:',\n                        restoreError\n                    );\n                }\n\n                streamSwitchCard.textContent =\n                    'Connexion au direct impossible';\n\n                errorBox.hidden = false;\n                errorBox.textContent = error && error.message\n                    ? String(error.message)\n                    : 'Le nouveau flux ne peut pas \u00eatre charg\u00e9.';\n\n                window.setTimeout(\n                    () => {\n                        streamSwitchOverlay.classList.remove(\n                            'is-visible'\n                        );\n                    },\n                    900\n                );\n            } finally {\n                streamSwitching = false;\n            }\n        };\n\n        const checkScheduledStream = async () => {\n            try {\n                const url = new URL(\n                    root.dataset.bootstrap,\n                    window.location.href\n                );\n\n                url.searchParams.set(\n                    'token',\n                    root.dataset.bootstrapToken || ''\n                );\n\n                url.searchParams.set(\n                    '_refresh',\n                    String(Date.now())\n                );\n\n                const response = await fetch(\n                    url.toString(),\n                    {\n                        cache:'no-store',\n                        credentials:'same-origin'\n                    }\n                );\n\n                if (!response.ok) return;\n\n                await switchScheduledStream(\n                    await response.json()\n                );\n            } catch (error) {\n                console.warn('Just Hits scheduler:', error);\n            }\n        };\n\n        const loadMetadata = async () => {\n            try {\n                const separator =\n                    root.dataset.api.includes('?')\n                        ? '&'\n                        : '?';\n\n                const response = await fetch(\n                    `${root.dataset.api}${separator}t=${Date.now()}`,\n                    {\n                        cache: 'no-store',\n                        credentials: 'same-origin'\n                    }\n                );\n\n                if (!response.ok) {\n                    throw new Error(\n                        `HTTP ${response.status}`\n                    );\n                }\n\n                const data = await response.json();\n                const incomingCurrent =\n                    data.current || null;\n\n                const previousCurrent =\n                    activeTrack\n                        ? { ...activeTrack }\n                        : null;\n\n                if (!metadataInitialized) {\n                    unifiedHistory = [];\n\n                    mergeUnifiedHistory(\n                        data.history || [],\n                        false\n                    );\n\n                    metadataInitialized = true;\n                } else if (\n                    preserveHistoryOnNextMetadata\n                ) {\n                    \/*\n                     * Le dernier titre de l'ancien flux devient le\n                     * premier titre de l'historique commun.\n                     * L'ancien historique du nouveau flux est ignor\u00e9.\n                     *\/\n                    pushPreviousCurrentToHistory(\n                        previousCurrent,\n                        incomingCurrent\n                    );\n\n                    preserveHistoryOnNextMetadata = false;\n                } else {\n                    pushPreviousCurrentToHistory(\n                        previousCurrent,\n                        incomingCurrent\n                    );\n\n                    mergeUnifiedHistory(\n                        data.history || [],\n                        false\n                    );\n                }\n\n                showCurrent(incomingCurrent);\n                showNext(data.next || null);\n                showHistory(\n                    unifiedHistory.slice(0, 4)\n                );\n\n                errorBox.hidden = true;\n                errorBox.textContent = '';\n                updateProgress();\n            } catch (error) {\n                console.error(\n                    'Just Hits metadata:',\n                    error\n                );\n\n                errorBox.hidden = false;\n                errorBox.textContent =\n                    'Les informations des titres sont momentan\u00e9ment indisponibles.';\n            }\n        };\n\n        const hideLaunchOverlay = () => {\n            if (!launchOverlay) return;\n\n            \/*\n             * L'accessibilit\u00e9 et les interactions sont d\u00e9sactiv\u00e9es\n             * imm\u00e9diatement. L'animation visuelle peut ensuite se terminer\n             * sans attendre le chargement du direct par le Manager.\n             *\/\n            launchOverlay.setAttribute('aria-hidden', 'true');\n            launchOverlay.classList.add('is-hidden');\n\n            if (\n                document.activeElement\n                && launchOverlay.contains(document.activeElement)\n            ) {\n                document.activeElement.blur();\n            }\n        };\n\n        const startFromLaunchOverlay = async () => {\n            if (!launchPlay) return;\n\n            launchPlay.disabled = true;\n            playbackWanted = true;\n\n            \/*\n             * La popup se ferme au clic. Elle ne d\u00e9pend plus du temps de\n             * r\u00e9ponse de l'API Manager, du t\u00e9l\u00e9chargement du MP3 ou du seek\n             * vers la position exacte du direct.\n             *\/\n            hideLaunchOverlay();\n\n            try {\n                await ensureSecureConfig();\n                if (activeStreamProvider === 'manager') {\n                    await syncManagerPlayback(true);\n                } else {\n                    const stream = selectedStream;\n                    const separator = stream.includes('?') ? '&' : '?';\n                    audio.src = `${stream}${separator}live=${Date.now()}`;\n                    audio.load();\n                    await audio.play();\n                }\n                errorBox.hidden = true;\n                errorBox.textContent = '';\n                updatePlaybackUI();\n                updateMediaSessionPlaybackState();\n            } catch (error) {\n                console.error('Just Hits launch playback:', error);\n                playbackWanted = false;\n                launchPlay.disabled = false;\n                errorBox.hidden = false;\n                errorBox.textContent =\n                    'La lecture n\u2019a pas d\u00e9marr\u00e9. Utilise le bouton lecture du player pour r\u00e9essayer.';\n                updatePlaybackUI();\n            }\n        };\n\n        const fetchManagerLive = async () => {\n            if (!managerLiveEndpoint) {\n                throw new Error('API du Radio Manager absente.');\n            }\n\n            const separator = managerLiveEndpoint.includes('?') ? '&' : '?';\n            const response = await fetch(\n                `${managerLiveEndpoint}${separator}_=${Date.now()}`,\n                {cache:'no-store', credentials:'same-origin'}\n            );\n\n            let data = {};\n            try {\n                data = await response.json();\n            } catch (error) {\n                throw new Error('R\u00e9ponse invalide du Radio Manager.');\n            }\n\n            if (!response.ok || !data.ok || !data.current || !data.current.url) {\n                throw new Error(data.message || 'Programmation du Manager indisponible.');\n            }\n\n            \/*\n             * Conserve l'instant exact de r\u00e9ception. Les op\u00e9rations de\n             * chargement peuvent durer plusieurs secondes : utiliser ensuite\n             * directement `server_time` retarderait le D\u00e9but, le Fondu, le\n             * Mix et la Fin d'autant.\n             *\/\n            data._client_received_ms = performance.now();\n\n            return data;\n        };\n\n        const managerServerNow = (data = {}) => {\n            const receivedAt = Number(data._client_received_ms);\n            const elapsed = Number.isFinite(receivedAt)\n                ? Math.max(0, (performance.now() - receivedAt) \/ 1000)\n                : 0;\n\n            return Number(data.server_time || Date.now() \/ 1000) + elapsed;\n        };\n\n        const managerPlaybackTarget = (data = {}) => Math.max(\n            0,\n            Number(data.offset || 0)\n                + Math.max(\n                    0,\n                    managerServerNow(data)\n                        - Number(data.server_time || managerServerNow(data))\n                )\n        );\n\n        const waitForManagerMedia = (deck = audio, timeout = 12000, wantedTime = 0) => new Promise((resolve, reject) => {\n            const hasBufferedTarget = () => {\n                if (!deck || deck.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return false;\n                if (!Number.isFinite(Number(wantedTime)) || Number(wantedTime) <= 0) {\n                    return deck.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA || deck.buffered.length > 0;\n                }\n                for (let index = 0; index < deck.buffered.length; index += 1) {\n                    if (deck.buffered.start(index) <= wantedTime && deck.buffered.end(index) >= wantedTime + .35) {\n                        return true;\n                    }\n                }\n                return deck.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA;\n            };\n\n            if (hasBufferedTarget()) {\n                resolve();\n                return;\n            }\n\n            let finished = false;\n            const cleanup = () => {\n                deck.removeEventListener('loadedmetadata', check);\n                deck.removeEventListener('loadeddata', check);\n                deck.removeEventListener('canplay', check);\n                deck.removeEventListener('canplaythrough', check);\n                deck.removeEventListener('progress', check);\n                deck.removeEventListener('error', failure);\n                window.clearTimeout(timer);\n            };\n            const success = () => {\n                if (finished) return;\n                finished = true;\n                cleanup();\n                resolve();\n            };\n            const check = () => {\n                if (hasBufferedTarget()) success();\n            };\n            const failure = () => {\n                if (finished) return;\n                finished = true;\n                cleanup();\n                reject(new Error('Le fichier du Radio Manager ne peut pas \u00eatre pr\u00e9charg\u00e9.'));\n            };\n            const timer = window.setTimeout(() => {\n                if (deck.readyState >= HTMLMediaElement.HAVE_METADATA) success();\n                else failure();\n            }, timeout);\n\n            deck.addEventListener('loadedmetadata', check);\n            deck.addEventListener('loadeddata', check);\n            deck.addEventListener('canplay', check);\n            deck.addEventListener('canplaythrough', check);\n            deck.addEventListener('progress', check);\n            deck.addEventListener('error', failure, {once:true});\n        });\n\n\n        const managerDeckFadeTimers = new WeakMap();\n\n        const cancelManagerDeckFade = (deck) => {\n            const activeFade =\n                managerDeckFadeTimers.get(deck);\n\n            if (!activeFade) {\n                return;\n            }\n\n            window.clearInterval(activeFade.timer);\n            managerDeckFadeTimers.delete(deck);\n\n            \/*\n             * R\u00e9sout aussi la promesse de l\u2019ancien fondu afin de ne pas\n             * bloquer une transition qui attendait sa fin.\n             *\/\n            try {\n                activeFade.resolve(false);\n            } catch (error) {}\n        };\n\n        const fadeDeck = (\n            deck,\n            from,\n            to,\n            duration\n        ) => new Promise((resolve) => {\n            cancelManagerDeckFade(deck);\n\n            const started = performance.now();\n            const safeFrom = Math.max(\n                0,\n                Math.min(1, Number(from) || 0)\n            );\n            const safeTo = Math.max(\n                0,\n                Math.min(1, Number(to) || 0)\n            );\n\n            deck.volume = safeFrom;\n\n            const timer = window.setInterval(() => {\n                const progress = Math.min(\n                    1,\n                    (\n                        performance.now() -\n                        started\n                    ) \/ Math.max(80, duration)\n                );\n\n                deck.volume =\n                    safeFrom +\n                    (safeTo - safeFrom) * progress;\n\n                if (progress >= 1) {\n                    window.clearInterval(timer);\n                    managerDeckFadeTimers.delete(deck);\n                    deck.volume = safeTo;\n                    resolve(true);\n                }\n            }, 25);\n\n            managerDeckFadeTimers.set(\n                deck,\n                {\n                    timer,\n                    resolve,\n                }\n            );\n        });\n\n        const releaseManagerMemoryEntry = (key) => {\n            const entry = managerMemoryCache.get(key);\n            if (!entry) return;\n            if (entry.objectUrl) {\n                try { URL.revokeObjectURL(entry.objectUrl); } catch (error) {}\n            }\n            managerMemoryCache.delete(key);\n            const index = managerMemoryCacheOrder.indexOf(key);\n            if (index >= 0) managerMemoryCacheOrder.splice(index, 1);\n        };\n\n        const rememberManagerMemoryEntry = (key, entry) => {\n            if (managerMemoryCache.has(key)) releaseManagerMemoryEntry(key);\n            managerMemoryCache.set(key, entry);\n            managerMemoryCacheOrder.push(key);\n            while (managerMemoryCacheOrder.length > MANAGER_MEMORY_CACHE_LIMIT) {\n                releaseManagerMemoryEntry(managerMemoryCacheOrder[0]);\n            }\n            return entry;\n        };\n\n        const fetchManagerMp3ToMemory = async (url) => {\n            const absoluteUrl = new URL(String(url), window.location.href).toString();\n            const key = normalizeStreamUrl(absoluteUrl);\n            const cached = managerMemoryCache.get(key);\n            if (cached && cached.objectUrl) return cached;\n\n            const controller = new AbortController();\n            const timeout = window.setTimeout(() => controller.abort(), 25000);\n            try {\n                const response = await fetch(absoluteUrl, {\n                    credentials: 'same-origin',\n                    cache: 'force-cache',\n                    signal: controller.signal,\n                });\n                if (!response.ok) throw new Error(`T\u00e9l\u00e9chargement MP3 impossible (${response.status}).`);\n                const blob = await response.blob();\n                if (!blob || blob.size < 1024) throw new Error('Fichier audio vide ou incomplet.');\n                const objectUrl = URL.createObjectURL(blob);\n                return rememberManagerMemoryEntry(key, {\n                    key,\n                    originalUrl: absoluteUrl,\n                    objectUrl,\n                    size: blob.size,\n                    createdAt: Date.now(),\n                });\n            } finally {\n                window.clearTimeout(timeout);\n            }\n        };\n\n        const managerDeckOriginalUrl = (deck) => normalizeStreamUrl(\n            (deck && deck.dataset && deck.dataset.jhManagerOriginalUrl)\n                ? deck.dataset.jhManagerOriginalUrl\n                : (deck ? (deck.currentSrc || deck.src || '') : '')\n        );\n\n        const loadManagerDeckFromMemory = async (deck, item, cueIn = 0) => {\n            const originalUrl = new URL(String(item.url || ''), window.location.href).toString();\n            \/*\n             * Ne t\u00e9l\u00e9charge jamais le MP3 complet avant la lecture.\n             *\n             * Un morceau de 8 \u00e0 10 Mo pouvait auparavant retarder le bouton\n             * Play de plusieurs secondes. Avec trois platines, les trois\n             * t\u00e9l\u00e9chargements complets concurrents pouvaient \u00e9galement\n             * saturer la connexion et perturber le direct.\n             *\n             * L'\u00e9l\u00e9ment audio charge maintenant directement l'URL : le\n             * navigateur utilise le streaming HTTP et les requ\u00eates partielles.\n             * Les platines suivantes restent pr\u00e9charg\u00e9es par `preload=auto`,\n             * sans cr\u00e9er une copie int\u00e9grale du fichier en m\u00e9moire.\n             *\/\n            const memoryEntry = null;\n            const playbackUrl = originalUrl;\n\n            deck.pause();\n            deck.preload = 'auto';\n            deck.dataset.jhManagerOriginalUrl = originalUrl;\n            deck.src = playbackUrl;\n            deck.load();\n            await waitForManagerMedia(deck, 18000, cueIn);\n            try {\n                if (Math.abs((deck.currentTime || 0) - cueIn) > .08) deck.currentTime = cueIn;\n            } catch (error) {}\n            return {originalUrl, playbackUrl, memoryEntry};\n        };\n\n        const managerItemCueIn = (item = {}) => (\n            item.cue_in !== '' && item.cue_in != null\n                ? Math.max(0, Number(item.cue_in) || 0)\n                : 0\n        );\n\n        const clearManagerContinuityTimers = () => {\n            clearTimeout(managerPreloadTimer);\n            clearTimeout(managerTransitionTimer);\n            managerPreloadTimer = null;\n            managerTransitionTimer = null;\n        };\n\n        const preloadManagerNext = async (data) => {\n            const nextItem = (data && data.next) || {};\n            if (!nextItem.url || standbyAudio === audio) return null;\n\n            const wantedUrl = new URL(String(nextItem.url), window.location.href).toString();\n            const expectedUrl = normalizeStreamUrl(wantedUrl);\n            const cueIn = managerItemCueIn(nextItem);\n            const deck = standbyAudio;\n            const loadedUrl = managerDeckOriginalUrl(deck);\n\n            if (!managerPreloaded || managerPreloaded.url !== expectedUrl || managerPreloaded.deck !== deck || loadedUrl !== expectedUrl) {\n                const loaded = await loadManagerDeckFromMemory(deck, nextItem, cueIn);\n                managerPreloaded = {\n                    url: expectedUrl,\n                    deck,\n                    item: nextItem,\n                    ready: false,\n                    objectUrl: loaded.playbackUrl,\n                    memoryEntry: loaded.memoryEntry || null,\n                };\n            }\n\n            await waitForManagerMedia(deck, 18000, cueIn);\n            try {\n                if (Math.abs((deck.currentTime || 0) - cueIn) > .08) deck.currentTime = cueIn;\n            } catch (error) {}\n            deck.muted = audio.muted;\n            deck.volume = 0;\n            managerPreloaded.ready = true;\n            managerPreloaded.item = nextItem;\n            return managerPreloaded;\n        };\n\n        const preloadManagerFollowing = async (data) => {\n            const item = (data && data.after_next) || {};\n            if (!item.url || reserveAudio === audio || reserveAudio === standbyAudio) {\n                return null;\n            }\n\n            const wantedUrl = new URL(String(item.url), window.location.href).toString();\n            const expectedUrl = normalizeStreamUrl(wantedUrl);\n            const cueIn = managerItemCueIn(item);\n            const deck = reserveAudio;\n            const loadedUrl = managerDeckOriginalUrl(deck);\n\n            if (\n                !managerFollowingPreloaded\n                || managerFollowingPreloaded.url !== expectedUrl\n                || managerFollowingPreloaded.deck !== deck\n                || loadedUrl !== expectedUrl\n            ) {\n                const loaded = await loadManagerDeckFromMemory(deck, item, cueIn);\n                managerFollowingPreloaded = {\n                    url: expectedUrl,\n                    deck,\n                    item,\n                    ready: false,\n                    objectUrl: loaded.playbackUrl,\n                    memoryEntry: loaded.memoryEntry || null,\n                };\n            }\n\n            await waitForManagerMedia(deck, 18000, cueIn);\n            deck.muted = audio.muted;\n            deck.volume = 0;\n            managerFollowingPreloaded.ready = true;\n            return managerFollowingPreloaded;\n        };\n\n        \/*\n         * Apr\u00e8s une transition locale, l'ancienne r\u00e9ponse API contient encore\n         * l'ancien couple current\/next. On attend que le serveur confirme le\n         * nouveau titre courant, puis on pr\u00e9pare imm\u00e9diatement le suivant.\n         * C'est indispensable pour les jingles de quelques secondes.\n         *\/\n        const refreshManagerChainAfterTransition = async (\n            expectedDeck,\n            attempt = 0\n        ) => {\n            if (\n                !playbackWanted\n                || activeStreamProvider !== 'manager'\n                || expectedDeck !== audio\n            ) {\n                return false;\n            }\n\n            try {\n                const fresh = await fetchManagerLive();\n                const freshUrl = normalizeStreamUrl(\n                    new URL(\n                        String(fresh.current?.url || ''),\n                        window.location.href\n                    ).toString()\n                );\n                const activeUrl = managerDeckOriginalUrl(expectedDeck);\n\n                if (freshUrl && activeUrl && freshUrl === activeUrl) {\n                    managerLastLiveData = fresh;\n                    await syncManagerPlayback(true);\n                    return true;\n                }\n            } catch (error) {\n                console.warn(\n                    'Just Hits Manager pr\u00e9paration de la cha\u00eene :',\n                    error\n                );\n            }\n\n            if (attempt < 8) {\n                window.setTimeout(() => {\n                    refreshManagerChainAfterTransition(\n                        expectedDeck,\n                        attempt + 1\n                    ).catch(() => {});\n                }, 180);\n            }\n\n            return false;\n        };\n\n        const startScheduledManagerTransition = async (data) => {\n            if (managerCrossfading || !playbackWanted || activeStreamProvider !== 'manager') return;\n            const nextItem = (data && data.next) || {};\n            if (!nextItem.url) {\n                scheduleManagerRecovery(100);\n                return;\n            }\n\n            managerCrossfading = true;\n            clearTimeout(managerFadeTimer);\n            const oldDeck = audio;\n            const targetVolume = Math.max(.01, Math.min(1, Number(lastVolume) || .8));\n\n            try {\n                const prepared = await preloadManagerNext(data);\n                if (!prepared || !prepared.deck) throw new Error('Titre suivant non pr\u00e9charg\u00e9.');\n                const nextDeck = prepared.deck;\n                const preparedFollowing = managerFollowingPreloaded;\n                const previousReserveDeck = reserveAudio;\n                const cueIn = managerItemCueIn(nextItem);\n\n                try {\n                    if (Math.abs((nextDeck.currentTime || 0) - cueIn) > .15) nextDeck.currentTime = cueIn;\n                } catch (error) {}\n                nextDeck.muted = oldDeck.muted;\n                nextDeck.volume = 0;\n                await nextDeck.play();\n                await waitForAudioStart(5000, nextDeck);\n\n                const currentItem = (data && data.current) || {};\n                const currentDuration = Math.max(.1, Number(currentItem.duration) || .1);\n                const currentCueIn = managerItemCueIn(currentItem);\n                const currentCueOut = currentItem.cue_out !== '' && currentItem.cue_out != null\n                    ? Math.max(currentCueIn, Math.min(currentDuration, Number(currentItem.cue_out) || currentDuration))\n                    : currentDuration;\n                const currentMixPoint = currentItem.mix_point !== ''\n                    && currentItem.mix_point != null\n                    ? Math.max(\n                        currentCueIn,\n                        Math.min(\n                            currentCueOut,\n                            Number(currentItem.mix_point) || currentCueOut\n                        )\n                    )\n                    : currentCueOut;\n                \/*\n                 * La transition est d\u00e9clench\u00e9e au point de mix. Sa dur\u00e9e\n                 * restante d\u00e9pend donc uniquement des points du morceau,\n                 * jamais de l'horloge locale de l'appareil.\n                 *\/\n                const remainingMs = Math.max(\n                    80,\n                    (currentCueOut - currentMixPoint) * 1000\n                );\n\n                await Promise.all([\n                    fadeDeck(oldDeck, oldDeck.volume, 0, remainingMs),\n                    fadeDeck(nextDeck, 0, targetVolume, remainingMs)\n                ]);\n\n                oldDeck.pause();\n                oldDeck.removeAttribute('src');\n                delete oldDeck.dataset.jhManagerOriginalUrl;\n                oldDeck.load();\n                audio = nextDeck;\n                \/*\n                 * Rotation A \u2192 B \u2192 C :\n                 * - B devient active ;\n                 * - C, d\u00e9j\u00e0 charg\u00e9e, devient la prochaine ;\n                 * - A devient la r\u00e9serve \u00e0 remplir.\n                 *\/\n                standbyAudio = previousReserveDeck;\n                reserveAudio = oldDeck;\n                managerPreloaded = (\n                    preparedFollowing\n                    && preparedFollowing.deck === standbyAudio\n                ) ? preparedFollowing : null;\n                managerFollowingPreloaded = null;\n                managerTransitionSettledUntil = performance.now() + 3500;\n                managerLastLiveData = {\n                    ...data,\n                    current: nextItem,\n                    started_at: Number(data.next_start || data.server_time || Date.now() \/ 1000),\n                    offset: cueIn\n                };\n\n                \/*\n                 * Le jingle est maintenant la platine active. Sa musique\n                 * suivante est r\u00e9cup\u00e9r\u00e9e tout de suite, sans attendre le\n                 * contr\u00f4le p\u00e9riodique.\n                 *\/\n                window.setTimeout(() => {\n                    refreshManagerChainAfterTransition(nextDeck)\n                        .catch(() => scheduleManagerRecovery(120));\n                }, 140);\n            } catch (error) {\n                console.warn('Just Hits Manager transition locale:', error);\n                managerPreloaded = null;\n                managerFollowingPreloaded = null;\n                scheduleManagerRecovery(80);\n            } finally {\n                managerCrossfading = false;\n                updatePlaybackUI();\n            }\n        };\n\n        const scheduleManagerContinuity = (data) => {\n            clearManagerContinuityTimers();\n            const serverNow = managerServerNow(data);\n            const nextStart = Number(data.next_start || 0);\n            if (!nextStart || !data.next || !data.next.url) return;\n\n            const millisecondsUntilNext = Math.max(0, (nextStart - serverNow) * 1000);\n            const preloadLead = Math.min(60000, Math.max(20000, millisecondsUntilNext * .65));\n            managerPreloadTimer = window.setTimeout(() => {\n                Promise.allSettled([\n                    preloadManagerNext(data),\n                    preloadManagerFollowing(data)\n                ]).then(results => {\n                    results.forEach(result => {\n                        if (result.status === 'rejected') {\n                            console.warn('Just Hits Manager pr\u00e9chargement:', result.reason);\n                        }\n                    });\n                });\n            }, Math.max(0, millisecondsUntilNext - preloadLead));\n\n            managerTransitionTimer = window.setTimeout(() => {\n                startScheduledManagerTransition(data).catch(error => {\n                    console.warn('Just Hits Manager encha\u00eenement:', error);\n                    scheduleManagerRecovery(100);\n                });\n            }, Math.max(0, millisecondsUntilNext - 90));\n        };\n\n        const managerCrossfadeTo = async (data, wantedUrl, target) => {\n            const oldDeck = audio;\n            const nextDeck = standbyAudio;\n            const previousReserveDeck = reserveAudio;\n            const targetVolume = Math.max(0, Math.min(1, Number(lastVolume) || .8));\n            managerCrossfading = true;\n            try {\n                await loadManagerDeckFromMemory(nextDeck, data.current || {url: wantedUrl}, target);\n                target = managerPlaybackTarget(data);\n                try {\n                    nextDeck.currentTime = Math.min(target, Number.isFinite(nextDeck.duration) && nextDeck.duration > 0 ? Math.max(0, nextDeck.duration - .1) : target);\n                } catch (error) {}\n                nextDeck.muted = oldDeck.muted;\n                nextDeck.volume = 0;\n                await nextDeck.play();\n\n                const previous = managerLastLiveData || {};\n                const previousItem = previous.current || {};\n                const previousDuration = Math.max(.1, Number(previousItem.duration) || .1);\n                const previousCue = previousItem.cue_in !== '' && previousItem.cue_in != null ? Math.max(0, Number(previousItem.cue_in) || 0) : 0;\n                const previousOut = previousItem.cue_out !== '' && previousItem.cue_out != null ? Math.max(previousCue, Math.min(previousDuration, Number(previousItem.cue_out) || previousDuration)) : previousDuration;\n                const previousEnd = Number(previous.started_at || 0) + (previousOut - previousCue);\n                \/* Dur\u00e9e exacte entre le point de mix et le cue_out du titre sortant.\n                 * Aucun minimum\/maximum arbitraire : les r\u00e9glages du titre sont\n                 * appliqu\u00e9s tels qu'ils sont enregistr\u00e9s dans le Manager. *\/\n                const remainingMs = Math.max(\n                    80,\n                    (previousEnd - managerServerNow(data)) * 1000\n                );\n\n                await Promise.all([\n                    fadeDeck(oldDeck, oldDeck.volume, 0, remainingMs),\n                    fadeDeck(nextDeck, 0, targetVolume, remainingMs)\n                ]);\n\n                oldDeck.pause();\n                oldDeck.removeAttribute('src');\n                delete oldDeck.dataset.jhManagerOriginalUrl;\n                oldDeck.load();\n                audio = nextDeck;\n                standbyAudio = previousReserveDeck;\n                reserveAudio = oldDeck;\n                managerPreloaded = (\n                    managerFollowingPreloaded\n                    && managerFollowingPreloaded.deck === standbyAudio\n                ) ? managerFollowingPreloaded : null;\n                managerFollowingPreloaded = null;\n            } finally {\n                managerCrossfading = false;\n            }\n        };\n\n        const syncManagerPlayback = async (shouldPlay = false) => {\n            if (managerCrossfading) return managerLastLiveData;\n            const data = await fetchManagerLive();\n            const item = data.current || {};\n            const wantedUrl = new URL(String(item.url || ''), window.location.href).toString();\n            const currentUrl = managerDeckOriginalUrl(audio);\n            const expectedUrl = normalizeStreamUrl(wantedUrl);\n            \/*\n             * L'offset re\u00e7u correspond \u00e0 l'instant de la r\u00e9ponse. Ajoute le\n             * temps r\u00e9ellement \u00e9coul\u00e9 pendant la pr\u00e9paration de la platine\n             * afin de ne jamais d\u00e9marrer quelques secondes derri\u00e8re le live.\n             *\/\n            let target = managerPlaybackTarget(data);\n            const wasPlaying = shouldPlay || playbackWanted || isPlaybackActive();\n\n            if (!currentUrl || currentUrl !== expectedUrl) {\n                if (wasPlaying && currentUrl && standbyAudio !== audio) {\n                    await managerCrossfadeTo(data, wantedUrl, target);\n                } else {\n                    await loadManagerDeckFromMemory(audio, item, target);\n                }\n            }\n\n            target = managerPlaybackTarget(data);\n\n            if (!managerCrossfading && Number.isFinite(target)) {\n                const currentPosition = Number(audio.currentTime || 0);\n                const drift = target - currentPosition;\n                const transitionJustFinished =\n                    performance.now() < managerTransitionSettledUntil;\n\n                \/*\n                 * Une petite d\u00e9rive ne doit jamais provoquer un saut audible.\n                 * Apr\u00e8s un encha\u00eenement, toute correction arri\u00e8re est bloqu\u00e9e :\n                 * c'\u00e9tait elle qui faisait r\u00e9p\u00e9ter une seconde des jingles.\n                 *\/\n                const shouldSeekForward = drift > 3.5;\n                const shouldSeekBackward =\n                    drift < -8\n                    && !transitionJustFinished;\n                const playbackCanBeRepositioned =\n                    audio.paused\n                    || !playbackWanted;\n\n                if (\n                    playbackCanBeRepositioned\n                    && (shouldSeekForward || shouldSeekBackward)\n                ) {\n                    try {\n                        audio.currentTime = Math.min(\n                            target,\n                            Number.isFinite(audio.duration) && audio.duration > 0\n                                ? Math.max(0, audio.duration - 0.1)\n                                : target\n                        );\n                    } catch (error) {\n                        console.warn('Just Hits Manager seek:', error);\n                    }\n                }\n            }\n\n            if ((shouldPlay || playbackWanted) && audio.paused) {\n                audio.muted = false;\n                if (audio.volume <= 0.001) {\n                    audio.volume = Math.max(0.01, Math.min(1, Number(lastVolume) || 0.8));\n                }\n                await audio.play();\n                await waitForAudioStart(8000);\n            }\n\n            managerLastLiveData = data;\n\n            \/\/ Respect strictement les points du Manager :\n            \/\/ - d\u00e9but de la baisse au fade_start ;\n            \/\/ - d\u00e9marrage du titre suivant au mix_point\/next_start ;\n            \/\/ - extinction compl\u00e8te au cue_out.\n            clearTimeout(managerFadeTimer);\n            const duration = Math.max(.1, Number(item.duration) || .1);\n            const cueIn = item.cue_in !== '' && item.cue_in != null\n                ? Math.max(0, Number(item.cue_in) || 0)\n                : 0;\n            const fadeStart = item.fade_start !== '' && item.fade_start != null\n                ? Math.max(cueIn, Math.min(duration, Number(item.fade_start) || cueIn))\n                : null;\n            const cueOut = item.cue_out !== '' && item.cue_out != null\n                ? Math.max(cueIn, Math.min(duration, Number(item.cue_out) || duration))\n                : duration;\n\n            if (fadeStart !== null && cueOut > fadeStart && !audio.paused) {\n                const fadeAt = Number(data.started_at || 0) + (fadeStart - cueIn);\n                const fadeDelay = Math.max(\n                    0,\n                    (fadeAt - managerServerNow(data)) * 1000\n                );\n                const fadeDuration = Math.max(80, (cueOut - fadeStart) * 1000);\n                const scheduledDeck = audio;\n                managerFadeTimer = window.setTimeout(() => {\n                    if (scheduledDeck === audio && !scheduledDeck.paused && !managerCrossfading) {\n                        fadeDeck(scheduledDeck, scheduledDeck.volume, 0, fadeDuration).catch(() => {});\n                    }\n                }, fadeDelay);\n            }\n\n            scheduleManagerContinuity(data);\n\n            clearTimeout(managerSyncTimer);\n            const delay = Math.max(\n                1200,\n                (Number(data.next_start || 0) - managerServerNow(data)) * 1000 + 900\n            );\n            managerSyncTimer = setTimeout(() => {\n                syncManagerPlayback(playbackWanted).catch(error => {\n                    console.warn('Just Hits Manager resynchronisation:', error);\n                    if (playbackWanted) scheduleManagerRecovery();\n                });\n            }, delay);\n            return data;\n        };\n\n        const isDeckReallyPlaying = (deck) => Boolean(\n            deck\n            && !deck.paused\n            && !deck.ended\n            && deck.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA\n        );\n\n        const isPlaybackActive = () => managerDecks.some(isDeckReallyPlaying);\n\n        const pauseAllDecks = () => {\n            managerDecks.forEach((deck) => {\n                if (!deck.paused) deck.pause();\n            });\n        };\n\n        window.addEventListener('pagehide', () => {\n            managerMemoryCacheOrder.slice().forEach(releaseManagerMemoryEntry);\n        }, {once:true});\n\n\n        const scheduleManagerRecovery = (delay = 350) => {\n            if (!playbackWanted || activeStreamProvider !== 'manager' || managerRecoveryRunning) return;\n            managerRecoveryRunning = true;\n            window.setTimeout(async () => {\n                try {\n                    if (playbackWanted && activeStreamProvider === 'manager' && !isPlaybackActive()) {\n                        await syncManagerPlayback(true);\n                    }\n                } catch (error) {\n                    console.warn('Just Hits Manager reprise automatique:', error);\n                    if (playbackWanted) {\n                        managerRecoveryRunning = false;\n                        scheduleManagerRecovery(1200);\n                        return;\n                    }\n                }\n                managerRecoveryRunning = false;\n            }, Math.max(100, Number(delay) || 350));\n        };\n\n        const togglePlayback = async () => {\n            if (!isPlaybackActive()) {\n                playbackWanted = true;\n                try {\n                    await ensureSecureConfig();\n                    if (activeStreamProvider === 'manager') {\n                        await syncManagerPlayback(true);\n                    } else {\n                        const stream = selectedStream;\n                        const separator = stream.includes('?') ? '&' : '?';\n                        audio.src = `${stream}${separator}live=${Date.now()}`;\n                        audio.load();\n                        await audio.play();\n                    }\n                    errorBox.hidden = true;\n                    updatePlaybackUI();\n                } catch (error) {\n                    console.error('Just Hits playback:', error);\n                    if (activeStreamProvider === 'manager') scheduleManagerRecovery(600);\n                    errorBox.hidden = false;\n                    errorBox.textContent = 'Appuie \u00e0 nouveau sur le bouton lecture.';\n                    updatePlaybackUI();\n                }\n            } else {\n                playbackWanted = false;\n                clearTimeout(managerSyncTimer);\n                clearTimeout(managerFadeTimer);\n                clearManagerContinuityTimers();\n                managerPreloaded = null;\n                managerFollowingPreloaded = null;\n                pauseAllDecks();\n                updatePlaybackUI();\n            }\n        };\n\n        const updatePlaybackUI = () => {\n            const playing = isPlaybackActive();\n            root.classList.toggle('is-playing', playing);\n            playSymbol.textContent = playing ? '\u2161' : '\u25b6';\n            playButton.setAttribute('aria-label', playing ? 'Mettre en pause' : '\u00c9couter la radio');\n            playButton.setAttribute('aria-pressed', playing ? 'true' : 'false');\n            syncListeningTimer(playing);\n        };\n\n        \/*\n         * Passerelle officielle utilis\u00e9e par le module ind\u00e9pendant\n         * \u00ab Extraits des titres d\u00e9j\u00e0 diffus\u00e9s \u00bb.\n         *\n         * Le module ne clique plus artificiellement sur le bouton Play\n         * et ne manipule plus directement les sources des deux platines.\n         *\/\n        let previewControlRunning = false;\n        let previewControlWasPlaying = false;\n        let previewControlTargetVolume = Math.max(\n            .01,\n            Math.min(1, Number(lastVolume) || .8)\n        );\n\n        const previewPlayingDecks = () =>\n            managerDecks.filter(isDeckReallyPlaying);\n\n        const previewFadeDecks = async (\n            decks,\n            fromResolver,\n            toResolver,\n            duration\n        ) => {\n            const safeDecks = decks.filter(Boolean);\n\n            if (!safeDecks.length) {\n                return false;\n            }\n\n            await Promise.all(\n                safeDecks.map((deck) => {\n                    const from = Math.max(\n                        0,\n                        Math.min(\n                            1,\n                            Number(fromResolver(deck)) || 0\n                        )\n                    );\n\n                    const to = Math.max(\n                        0,\n                        Math.min(\n                            1,\n                            Number(toResolver(deck)) || 0\n                        )\n                    );\n\n                    return fadeDeck(\n                        deck,\n                        from,\n                        to,\n                        Math.max(80, Number(duration) || 0)\n                    );\n                })\n            );\n\n            return true;\n        };\n\n        root.jhPreviewAudioControl = {\n            isPlaying() {\n                return isPlaybackActive();\n            },\n\n            async fadeOutAndPause(duration = 850) {\n                if (previewControlRunning) {\n                    return false;\n                }\n\n                const decks = previewPlayingDecks();\n\n                previewControlWasPlaying =\n                    decks.length > 0;\n\n                if (!previewControlWasPlaying) {\n                    return false;\n                }\n\n                previewControlRunning = true;\n\n                try {\n                    \/*\n                     * Le niveau choisi par l\u2019auditeur est la seule\n                     * r\u00e9f\u00e9rence. On ne reprend jamais le volume temporaire\n                     * d\u2019une platine en plein encha\u00eenement, car il peut \u00eatre\n                     * sup\u00e9rieur et provoquer une pouss\u00e9e au retour.\n                     *\/\n                    previewControlTargetVolume = Math.max(\n                        .01,\n                        Math.min(\n                            1,\n                            Number(lastVolume) ||\n                            Number(decks[0]?.volume) ||\n                            .8\n                        )\n                    );\n\n                    playbackWanted = false;\n                    clearTimeout(managerSyncTimer);\n                    clearTimeout(managerFadeTimer);\n                    clearManagerContinuityTimers();\n\n                    decks.forEach((deck) => {\n                        cancelManagerDeckFade(deck);\n                    });\n\n                    await previewFadeDecks(\n                        decks,\n                        (deck) => deck.volume,\n                        () => 0,\n                        duration\n                    );\n\n                    managerPreloaded = null;\n                    managerFollowingPreloaded = null;\n\n                    decks.forEach((deck) => {\n                        try {\n                            deck.pause();\n                            deck.volume = 0;\n                        } catch (error) {}\n                    });\n\n                    updatePlaybackUI();\n                    updateVolumeUI();\n\n                    return true;\n                } finally {\n                    previewControlRunning = false;\n                }\n            },\n\n            async resumeWithFade(duration = 1100) {\n                if (\n                    previewControlRunning ||\n                    !previewControlWasPlaying\n                ) {\n                    return false;\n                }\n\n                previewControlRunning = true;\n\n                try {\n                    const target = Math.max(\n                        .01,\n                        Math.min(\n                            1,\n                            Number(previewControlTargetVolume) ||\n                            Number(lastVolume) ||\n                            .8\n                        )\n                    );\n\n                    \/*\n                     * Pendant la pr\u00e9paration, playbackWanted reste faux :\n                     * syncManagerPlayback() peut ainsi charger et recaler\n                     * la bonne source sans appeler play() ni r\u00e9tablir le\n                     * volume tout seul.\n                     *\/\n                    playbackWanted = false;\n\n                    managerDecks.forEach((deck) => {\n                        cancelManagerDeckFade(deck);\n                        deck.muted = true;\n                        deck.volume = 0;\n                    });\n\n                    await ensureSecureConfig();\n\n                    if (activeStreamProvider === 'manager') {\n                        await syncManagerPlayback(false);\n                    } else {\n                        const stream = selectedStream;\n                        const separator =\n                            stream.includes('?') ? '&' : '?';\n\n                        audio.pause();\n                        audio.src =\n                            `${stream}${separator}live=${Date.now()}`;\n                        audio.load();\n                    }\n\n                    \/*\n                     * Le direct d\u00e9marre r\u00e9ellement muet et \u00e0 z\u00e9ro. Il est\n                     * seulement rendu audible une fois la lecture native\n                     * confirm\u00e9e, puis le fade-in est appliqu\u00e9.\n                     *\/\n                    playbackWanted = true;\n                    audio.muted = true;\n                    audio.volume = 0;\n\n                    await audio.play();\n\n                    const started = await waitForAudioStart(\n                        8000,\n                        audio\n                    );\n\n                    if (!started && !isPlaybackActive()) {\n                        throw new Error(\n                            'Le direct ne red\u00e9marre pas.'\n                        );\n                    }\n\n                    const decks = previewPlayingDecks();\n\n                    decks.forEach((deck) => {\n                        cancelManagerDeckFade(deck);\n                        deck.volume = 0;\n                        deck.muted = false;\n                    });\n\n                    await previewFadeDecks(\n                        decks,\n                        () => 0,\n                        () => target,\n                        duration\n                    );\n\n                    decks.forEach((deck) => {\n                        deck.volume = target;\n                        deck.muted = false;\n                    });\n\n                    lastVolume = target;\n                    previewControlWasPlaying = false;\n\n                    errorBox.hidden = true;\n                    updatePlaybackUI();\n                    updateVolumeUI();\n\n                    return true;\n                } catch (error) {\n                    playbackWanted = false;\n\n                    managerDecks.forEach((deck) => {\n                        cancelManagerDeckFade(deck);\n\n                        if (deck === audio) {\n                            deck.muted = false;\n                        } else {\n                            deck.muted = true;\n                            deck.volume = 0;\n                        }\n                    });\n\n                    updatePlaybackUI();\n                    throw error;\n                } finally {\n                    previewControlRunning = false;\n                }\n            }\n        };\n\n        const updateVolumeUI = () => {\n            muteButton.classList.toggle('is-muted', audio.muted || audio.volume === 0);\n        };\n\n        root.addEventListener('contextmenu', (event) => {\n            if (event.target instanceof HTMLImageElement) {\n                event.preventDefault();\n            }\n        });\n\n        root.addEventListener('dragstart', (event) => {\n            if (event.target instanceof HTMLImageElement) {\n                event.preventDefault();\n            }\n        });\n\n        themeButtons.forEach((button) => {\n            button.addEventListener('click', () => {\n                applyTheme(button.dataset.themeMode || 'system');\n            });\n        });\n\n        systemThemeQuery.addEventListener('change', () => {\n            if ((root.dataset.themeMode || 'system') === 'system') {\n                applyTheme('system', false);\n            }\n        });\n\n        if (launchPlay) {\n            launchPlay.addEventListener('click', startFromLaunchOverlay);\n        }\n\n        if (currentVoteLike && currentVoteBox) {\n            currentVoteLike.addEventListener(\n                'click',\n                () => submitVote(currentVoteBox, 'plus', currentVoteLike)\n            );\n        }\n\n        if (currentVoteDislike && currentVoteBox) {\n            currentVoteDislike.addEventListener(\n                'click',\n                () => submitVote(currentVoteBox, 'minus', currentVoteDislike)\n            );\n        }\n\n        playButton.addEventListener('click', togglePlayback);\n\n        muteButton.addEventListener('click', (event) => {\n            if (mobileMediaQuery.matches) {\n                event.stopPropagation();\n\n                const isOpen =\n                    headerVolumeWrap?.classList.contains('is-open');\n\n                if (!isOpen) {\n                    headerVolumeWrap?.classList.add('is-open');\n                    return;\n                }\n\n                if (audio.muted || audio.volume === 0) {\n                    audio.muted = false;\n                    audio.volume = lastVolume || 0.8;\n                    volumeSlider.value = String(audio.volume);\n                } else {\n                    lastVolume = audio.volume || 0.8;\n                    audio.muted = true;\n                }\n\n                updateVolumeUI();\n                return;\n            }\n\n            if (audio.muted || audio.volume === 0) {\n                audio.muted = false;\n                audio.volume = lastVolume || 0.8;\n                volumeSlider.value = String(audio.volume);\n            } else {\n                lastVolume = audio.volume || 0.8;\n                audio.muted = true;\n            }\n\n            updateVolumeUI();\n        });\n\n        headerVolumeWrap?.addEventListener('click', (event) => {\n            if (mobileMediaQuery.matches) {\n                event.stopPropagation();\n            }\n        });\n\n        document.addEventListener('click', (event) => {\n            if (\n                mobileMediaQuery.matches &&\n                headerVolumeWrap &&\n                !headerVolumeWrap.contains(event.target)\n            ) {\n                headerVolumeWrap.classList.remove('is-open');\n            }\n        });\n\n        mobileMediaQuery.addEventListener('change', () => {\n            if (!mobileMediaQuery.matches && headerVolumeWrap) {\n                headerVolumeWrap.classList.remove('is-open');\n            }\n        });\n\n        volumeSlider.addEventListener('input', (event) => {\n            const volume = Number(event.target.value);\n            audio.muted = false;\n            audio.volume = volume;\n            if (managerCrossfading && standbyAudio && standbyAudio !== audio) standbyAudio.volume = volume;\n            if (volume > 0) lastVolume = volume;\n            updateVolumeUI();\n        });\n\n        managerDecks.forEach(deck => {\n            deck.addEventListener('play', () => {\n                playbackWanted = true;\n                window.requestAnimationFrame(() => {\n                    updatePlaybackUI();\n                    updateMediaSessionPlaybackState();\n                    hideLaunchOverlay();\n                });\n            });\n\n            deck.addEventListener('playing', () => {\n                window.requestAnimationFrame(() => {\n                    updatePlaybackUI();\n                    updateMediaSessionPlaybackState();\n                    hideLaunchOverlay();\n                });\n            });\n\n            deck.addEventListener('pause', () => {\n                window.requestAnimationFrame(() => {\n                    updatePlaybackUI();\n                    updateMediaSessionPlaybackState();\n                });\n            });\n\n            deck.addEventListener('ended', () => {\n                window.requestAnimationFrame(() => {\n                    updatePlaybackUI();\n                    updateMediaSessionPlaybackState();\n                });\n                if (activeStreamProvider === 'manager' && playbackWanted) {\n                    \/*\n                     * Une fin de fichier ne passe plus d'abord par le secours\n                     * temporis\u00e9. Le passage courant est r\u00e9cup\u00e9r\u00e9 imm\u00e9diatement.\n                     *\/\n                    window.setTimeout(() => {\n                        syncManagerPlayback(true).catch(error => {\n                            console.warn(\n                                'Just Hits Manager reprise apr\u00e8s fin :',\n                                error\n                            );\n                            scheduleManagerRecovery(180);\n                        });\n                    }, 40);\n                }\n            });\n            deck.addEventListener('volumechange', updateVolumeUI);\n            deck.addEventListener('error', () => {\n                if (deck !== audio) return;\n                errorBox.hidden = false;\n                errorBox.textContent = 'Le flux radio ne peut pas \u00eatre charg\u00e9 pour le moment.';\n                updatePlaybackUI();\n                if (activeStreamProvider === 'manager' && playbackWanted) {\n                    scheduleManagerRecovery(900);\n                }\n            });\n        });\n\n        initializeTheme();\n        initializeMediaSession();\n        bindTooltipTargets(root);\n\n        window.setTimeout(() => {\n            refreshTooltipEligibility(root);\n        }, 250);\n\n        if (document.fonts && document.fonts.ready) {\n            document.fonts.ready.then(() => {\n                refreshTooltipEligibility(root);\n            });\n        }\n\n        refreshCoverStack((root.dataset.logo || secureLogo || ''));\n        loadMetadata();\n        window.setInterval(() => {\n            if (activeStreamProvider === 'manager' && playbackWanted) {\n                syncManagerPlayback(true).catch(error => {\n                    console.error(error);\n                    scheduleManagerRecovery(700);\n                });\n            }\n        }, 12000);\n        updateProgress();\n        updatePlaybackUI();\n        updateVolumeUI();\n\n        const refreshMs =\n            Math.max(8, Number(root.dataset.refresh || 12)) * 1000;\n\n        const isMobileDevice = window.matchMedia(\n            '(max-width: 780px), (hover: none) and (pointer: coarse)'\n        ).matches;\n\n        \/*\n         * Sur mobile, une progression toutes les 2 secondes est visuellement\n         * suffisante et r\u00e9duit les r\u00e9veils JavaScript.\n         *\/\n        const progressRefreshMs = isMobileDevice ? 2000 : 1000;\n\n        let metadataTimer = null;\n        let progressTimer = null;\n        let playerDestroyed = false;\n\n        const scheduleMetadataRefresh = () => {\n            window.clearTimeout(metadataTimer);\n\n            if (\n                playerDestroyed ||\n                !document.body.contains(root)\n            ) {\n                return;\n            }\n\n            \/*\n             * Ne pas interrompre le suivi lorsque l\u2019onglet passe en arri\u00e8re-plan.\n             * Les navigateurs mobiles peuvent ralentir les timers, mais ils ne sont\n             * plus volontairement stopp\u00e9s par le player.\n             *\/\n            metadataTimer = window.setTimeout(async () => {\n                await loadMetadata();\n                scheduleMetadataRefresh();\n            }, refreshMs);\n        };\n\n        const scheduleProgressRefresh = () => {\n            window.clearTimeout(progressTimer);\n\n            if (\n                playerDestroyed ||\n                !document.body.contains(root)\n            ) {\n                return;\n            }\n\n            progressTimer = window.setTimeout(() => {\n                updateProgress();\n                scheduleProgressRefresh();\n            }, progressRefreshMs);\n        };\n\n        const stopBackgroundWork = () => {\n            window.clearTimeout(metadataTimer);\n            window.clearTimeout(progressTimer);\n            metadataTimer = null;\n            progressTimer = null;\n        };\n\n        const resumeBackgroundWork = () => {\n            if (\n                playerDestroyed ||\n                !document.body.contains(root)\n            ) {\n                return;\n            }\n\n            root.classList.remove('is-page-hidden');\n\n            \/*\n             * Synchronisation imm\u00e9diate lors du retour au premier plan afin de\n             * rattraper une \u00e9ventuelle suspension impos\u00e9e par iOS ou Android.\n             *\/\n            checkScheduledStream();\n            loadMetadata();\n            updateProgress();\n            scheduleMetadataRefresh();\n            scheduleProgressRefresh();\n        };\n\n        const handleVisibilityChange = () => {\n            if (document.hidden) {\n                root.classList.add('is-page-hidden');\n\n                \/*\n                 * On conserve les timers actifs en arri\u00e8re-plan. Le syst\u00e8me peut\n                 * les ralentir, mais le player ne les coupe plus lui-m\u00eame.\n                 *\/\n                scheduleMetadataRefresh();\n                scheduleProgressRefresh();\n            } else {\n                resumeBackgroundWork();\n            }\n        };\n\n        document.addEventListener(\n            'visibilitychange',\n            handleVisibilityChange\n        );\n\n        \/*\n         * iOS peut suspendre compl\u00e8tement une page mise en cache. Dans ce cas,\n         * une synchronisation imm\u00e9diate est effectu\u00e9e d\u00e8s sa restauration.\n         *\/\n        const handlePageHide = () => {\n            root.classList.add('is-page-hidden');\n        };\n\n        window.addEventListener('pagehide', handlePageHide);\n        window.addEventListener('pageshow', resumeBackgroundWork);\n\n        scheduleMetadataRefresh();\n        scheduleProgressRefresh();\n\n        \/*\n         * Contr\u00f4le chaque seconde pour respecter pr\u00e9cis\u00e9ment les\n         * heures, minutes et secondes d\u00e9finies dans le back-office.\n         *\/\n        const streamScheduleTimer = window.setInterval(\n            checkScheduledStream,\n            1000\n        );\n\n        window.addEventListener(\n            'focus',\n            checkScheduledStream\n        );\n\n        const observer = new MutationObserver(() => {\n            if (!document.body.contains(root)) {\n                playerDestroyed = true;\n                stopBackgroundWork();\n\n                document.removeEventListener(\n                    'visibilitychange',\n                    handleVisibilityChange\n                );\n\n                window.removeEventListener(\n                    'pagehide',\n                    handlePageHide\n                );\n\n                window.removeEventListener(\n                    'pageshow',\n                    resumeBackgroundWork\n                );\n\n                window.removeEventListener(\n                    'focus',\n                    checkScheduledStream\n                );\n\n                window.clearInterval(\n                    streamScheduleTimer\n                );\n                window.clearInterval(\n                    connectionStrengthTimer\n                );\n\n                window.removeEventListener(\n                    'online',\n                    updateConnectionStrength\n                );\n\n                window.removeEventListener(\n                    'offline',\n                    updateConnectionStrength\n                );\n\n                networkInformation?.removeEventListener?.(\n                    'change',\n                    updateConnectionStrength\n                );\n\n                observer.disconnect();\n            }\n        });\n\n        observer.observe(document.body, {\n            childList:true,\n            subtree:true\n        });\n    })();\n    <\/script>\n    \n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_canvas","meta":{"footnotes":""},"class_list":["post-12741","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.justhits.fr\/index.php?rest_route=\/wp\/v2\/pages\/12741","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.justhits.fr\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.justhits.fr\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.justhits.fr\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.justhits.fr\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=12741"}],"version-history":[{"count":3,"href":"https:\/\/www.justhits.fr\/index.php?rest_route=\/wp\/v2\/pages\/12741\/revisions"}],"predecessor-version":[{"id":12885,"href":"https:\/\/www.justhits.fr\/index.php?rest_route=\/wp\/v2\/pages\/12741\/revisions\/12885"}],"wp:attachment":[{"href":"https:\/\/www.justhits.fr\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=12741"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}