JVChat Mobile

Version mobile de JVChat (fork SVR) — chat seul, optimisé Firefox Android.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name           JVChat Mobile
// @description    Version mobile de JVChat (fork SVR) — chat seul, optimisé Firefox Android.
// @author         DarkWizounet
// @namespace      JVChat-Mobile
// @license        MIT
// @version        7.8.7
// @match          https://*.jeuxvideo.com/forums/42-*
// @match          https://*.jeuxvideo.com/forums/1-*
// @match          https://*.jeuxvideo.com/forums/0-42-*
// @match          https://*.jeuxvideo.com/forums/0-19163-*
// @grant          GM_xmlhttpRequest
// @connect        esports-api.lolesports.com
// @connect        feed.lolesports.com
// @connect        api.bo3.gg
// ==/UserScript==

const _DBG = false;
function dbg(...a) { if (_DBG) console.log('%c[JVChat]', 'color:#00e5ff;font-weight:bold', ...a); }
function err(...a) { if (_DBG) console.error('%c[JVChat]', 'color:#ff1744;font-weight:bold', ...a); }

// ─────────────────────────────────────────────────────────────
// CACHE AVATARS (en mémoire, évite les rechargements réseau)
// ─────────────────────────────────────────────────────────────
const _avatarCache = new Map(); // url → url (cache simple pour éviter les re-renders)

function cachedAvatar(url) {
    if (!url) return Promise.resolve(url);
    if (!_avatarCache.has(url)) _avatarCache.set(url, url);
    return Promise.resolve(_avatarCache.get(url));
}

// ─────────────────────────────────────────────────────────────
// STYLES
// ─────────────────────────────────────────────────────────────
function addStyle() {
    const style = document.createElement('style');
    style.textContent = `
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

        :root {
            --jvc-bg:          #18181b;
            --jvc-surface-1:   #1f1f23;
            --jvc-surface-2:   #27272a;
            --jvc-surface-3:   #303035;
            --jvc-border:      #2e2e33;
            --jvc-divider:     #242428;
            --jvc-text:        #e4e4e7;
            --jvc-text-dim:    #a1a1aa;
            --jvc-text-muted:  #71717a;
            --jvc-orange:      #e84d1a;
            --jvc-orange-deep: #c73f13;
            --jvc-green:       #4ade80;
            --jvc-blue:        #7fb4ff;
            --jvc-red:         #f87171;
            --jvc-header-h:    52px;
            --jvc-safe-bottom: env(safe-area-inset-bottom, 0px);
        }

        *, *::before, *::after { box-sizing: border-box; }

        html, body {
            margin: 0; padding: 0;
            overflow: hidden;
            background: var(--jvc-bg);
            height: 100%;
            overscroll-behavior: none;
        }

        body * { font-family: 'Inter', system-ui, sans-serif; }

        /* ── LAYOUT ── */
        #jvcm-app {
            display: flex;
            flex-direction: column;
            height: 100vh; height: -webkit-fill-available; height: 100dvh;
            background: var(--jvc-bg);
            overflow: hidden;
            position: relative;
        }

        /* ── HEADER ── */
        #jvcm-header {
            flex: 0 0 auto;
            min-height: var(--jvc-header-h);
            display: flex;
            align-items: center;
            gap: 0.6rem;
            padding: 0 0.9rem;
            background: var(--jvc-surface-1);
            border-bottom: 1px solid var(--jvc-border);
            z-index: 10;
        }

        #jvcm-topic-title {
            flex: 1;
            font-size: 0.82rem; font-weight: 600;
            color: var(--jvc-text);
            white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
            cursor: pointer;
        }
        #jvcm-topic-title:active { opacity: 0.7; }

        #jvcm-meta { display: flex; align-items: center; gap: 0.35rem; flex-shrink: 0; }

        .jvcm-chip {
            display: inline-flex; align-items: center; gap: 0.3rem;
            padding: 0.2rem 0.5rem;
            background: var(--jvc-surface-2); border: 1px solid var(--jvc-border);
            border-radius: 4px; font-size: 0.65rem; font-weight: 500;
            color: var(--jvc-text-dim); font-variant-numeric: tabular-nums; white-space: nowrap;
        }
        .jvcm-chip--live::before {
            content: ''; width: 5px; height: 5px; border-radius: 50%;
            background: var(--jvc-green); box-shadow: 0 0 4px var(--jvc-green);
        }

        #jvcm-home-btn {
            background: none; border: none; color: var(--jvc-text-muted);
            font-size: 1.2rem; cursor: pointer; padding: 0.2rem 0.3rem;
            line-height: 1; -webkit-tap-highlight-color: transparent; flex-shrink: 0;
        }
        #jvcm-home-btn:active { color: var(--jvc-text); }

        #jvcm-settings-btn {
            background: none; border: none; color: var(--jvc-text-muted);
            font-size: 1.1rem; cursor: pointer; padding: 0.2rem 0.3rem;
            line-height: 1; -webkit-tap-highlight-color: transparent; flex-shrink: 0;
        }
        #jvcm-settings-btn:active { color: var(--jvc-text); }

        /* ── LOADING ── */
        #jvcm-loading {
            position: absolute; inset: var(--jvc-header-h) 0 0 0;
            display: flex; flex-direction: column; align-items: center; justify-content: center;
            gap: 0.75rem; background: var(--jvc-bg); z-index: 50;
        }
        #jvcm-loading.hidden { display: none; }
        .jvcm-spinner {
            width: 28px; height: 28px;
            border: 3px solid var(--jvc-surface-3);
            border-top-color: var(--jvc-orange);
            border-radius: 50%; animation: jvcm-spin 0.7s linear infinite;
        }
        @keyframes jvcm-spin { to { transform: rotate(360deg); } }
        .jvcm-loading-label { font-size: 0.75rem; color: var(--jvc-text-muted); }

        /* ── MESSAGES ── */
        #jvcm-messages {
            flex: 1; overflow-y: auto; overflow-x: hidden;
            overscroll-behavior: contain; display: flex; flex-direction: column;
            padding: 0.4rem 0 0.5rem; -webkit-overflow-scrolling: touch; min-height: 0;
        }
        #jvcm-messages::-webkit-scrollbar { width: 3px; }
        #jvcm-messages::-webkit-scrollbar-track { background: transparent; }
        #jvcm-messages::-webkit-scrollbar-thumb { background: var(--jvc-surface-3); border-radius: 2px; }

        /* ── SÉPARATEUR NOUVEAUX MESSAGES ── */
        .jvcm-new-sep {
            flex: 0 0 auto;
            display: flex; align-items: center; justify-content: center;
            padding: 0.45rem 1rem; font-size: 0.68rem; font-weight: 700;
            color: #fff; background: var(--jvc-orange);
            letter-spacing: 0.06em; text-transform: uppercase;
            cursor: pointer; border: none; width: 100%;
            -webkit-tap-highlight-color: transparent;
        }
        .jvcm-new-sep:active { opacity: 0.85; }
        .jvcm-new-sep::before, .jvcm-new-sep::after { display: none; }

        .jvcm-message {
            display: flex; gap: 0.6rem; padding: 0.5rem 0.75rem;
            border-bottom: 1px solid var(--jvc-divider); position: relative;
            transition: background 120ms ease, transform 80ms ease;
            -webkit-tap-highlight-color: transparent; user-select: none;
        }
        .jvcm-message:nth-child(even) { background: rgba(255,255,255,0.016); }
        .jvcm-message.is-pressing     { background: rgba(232,77,26,0.08); }
        .jvcm-message.mentions-me     { background: rgba(232,77,26,0.06) !important; border-left: 2px solid var(--jvc-orange); }

        /* Grouped : masquer seulement le pseudo */
        .jvcm-message.is-grouped { padding-top: 0.1rem; padding-bottom: 0.1rem; border-bottom: none; }
        .jvcm-message.is-grouped .jvcm-username { display: none; }
        .jvcm-message.is-grouped .jvcm-avatar { opacity: 0.5; }

        /* Swipe hint */
        .jvcm-swipe-hint {
            position: absolute; left: 0.5rem; top: 50%;
            transform: translateY(-50%) scale(0.6); opacity: 0; font-size: 1rem;
            pointer-events: none; transition: opacity 120ms ease, transform 120ms ease;
        }
        .jvcm-message.swipe-ready .jvcm-swipe-hint { opacity: 1; transform: translateY(-50%) scale(1); }

        .jvcm-avatar {
            width: 34px; height: 34px; border-radius: 50%;
            border: 1px solid var(--jvc-border); object-fit: cover; flex-shrink: 0; margin-top: 2px;
        }

        .jvcm-msg-body { flex: 1; min-width: 0; }

        .jvcm-msg-header {
            display: flex; align-items: baseline; gap: 0.45rem; margin-bottom: 0.15rem;
        }

        .jvcm-username { font-size: 0.82rem; font-weight: 700; color: oklch(72% 0.16 var(--pseudo-h, 220)); }
        .jvcm-userrank { font-size: 0.58rem; color: var(--jvc-text-muted); font-weight: 400; margin-left: 0.25rem; opacity: 0.8; }

        .jvcm-date { font-size: 0.62rem; color: var(--jvc-text-muted); font-variant-numeric: tabular-nums; cursor: default; }
        .jvcm-date .jvcm-date-abs { display: none; }
        .jvcm-date:hover .jvcm-date-rel, .jvcm-date.show-abs .jvcm-date-rel { display: none; }
        .jvcm-date:hover .jvcm-date-abs, .jvcm-date.show-abs .jvcm-date-abs { display: inline; }

        .jvcm-msg-status { font-size: 0.62rem; margin-left: 0.2rem; }
        .jvcm-msg-status.pending { color: var(--jvc-text-muted); }
        .jvcm-msg-status.ok      { color: var(--jvc-green); }
        .jvcm-msg-status.fail    { color: var(--jvc-red); cursor: pointer; }

        .jvcm-content { font-size: 0.88rem; line-height: 1.5; color: var(--jvc-text); word-break: break-word; overflow-wrap: anywhere; }
        .jvcm-content p { margin: 0 0 0.2rem; }
        .jvcm-content p:last-child { margin-bottom: 0; }
        .jvcm-content a { color: var(--jvc-blue); text-decoration: none; }

        .jvcm-content blockquote, .jvcm-content .message__blockquote {
            margin: 0.3rem 0 0.4rem; padding: 0.35rem 0.6rem;
            background: var(--jvc-surface-2); border-left: 3px solid var(--jvc-border);
            border-radius: 0 3px 3px 0; font-size: 0.78rem; color: var(--jvc-text-dim);
            cursor: pointer; transition: border-color 150ms ease, background 150ms ease;
        }
        .jvcm-content blockquote:hover, .jvcm-content .message__blockquote:hover {
            border-left-color: var(--jvc-orange); background: var(--jvc-surface-3);
        }
        .jvcm-content blockquote p { margin: 0; }

        .jvcm-content .message__blockquote blockquote > *:not(.jvcm-collapse-btn) { display: none; }
        .jvcm-content .message__blockquote.is-open blockquote > *:not(.jvcm-collapse-btn) { display: revert; }
        .jvcm-content .message__blockquote.is-open blockquote > .jvcm-collapse-btn { display: none; }

        .jvcm-collapse-btn {
            display: inline-flex; align-items: center; padding: 0.2rem 0.5rem;
            background: transparent; border: 1px dashed var(--jvc-border);
            border-radius: 999px; color: var(--jvc-text-muted);
            font-size: 0.65rem; font-family: inherit; cursor: pointer; margin: 0.1rem 0;
        }

        @keyframes jvcm-flash { 0%,100%{background:transparent} 30%{background:rgba(232,77,26,0.22)} }
        .jvcm-message.quote-flash { animation: jvcm-flash 900ms ease; }

        /* Animation d'entrée pour les nouveaux messages */
        @keyframes jvcm-msg-in {
            from { opacity: 0; transform: translateY(6px); }
            to   { opacity: 1; transform: translateY(0); }
        }
        .jvcm-message.is-new { animation: jvcm-msg-in 250ms ease; }

        .jvcm-content img { max-width: 100%; height: auto; border-radius: 3px; }

        .jvcm-message.is-deleted .jvcm-msg-body { opacity: 0.25; filter: grayscale(1); }
        .jvcm-message.is-deleted::after {
            content: 'Message supprimé'; position: absolute; top: 50%; left: 50%;
            transform: translate(-50%,-50%); font-size: 0.72rem; color: var(--jvc-text-muted);
            font-weight: 600; pointer-events: none;
        }

        /* ── SCROLL BUTTON ── */

        /* ── INPUT ── */
        #jvcm-input-wrap {
            flex: 0 0 auto; display: flex; flex-direction: column;
            background: var(--jvc-surface-1); border-top: 1px solid var(--jvc-border); z-index: 10;
        }

        /* Compteur de caractères */
        #jvcm-char-count {
            font-size: 0.62rem; font-variant-numeric: tabular-nums;
            text-align: right; padding: 0.2rem 0.75rem 0;
            color: var(--jvc-text-muted); display: none;
        }
        #jvcm-char-count.warn   { color: #fbbf24; display: block; }
        #jvcm-char-count.danger { color: var(--jvc-red); display: block; font-weight: 600; }

        #jvcm-input-bar {
            display: flex; align-items: flex-end; gap: 0.5rem;
            padding: 0.5rem 0.75rem; padding-bottom: calc(0.5rem + var(--jvc-safe-bottom));
        }

        #jvcm-textarea {
            flex: 1; min-height: 36px; max-height: 120px;
            padding: 0.45rem 0.7rem; background: var(--jvc-surface-2);
            border: 1px solid var(--jvc-border); border-radius: 18px;
            color: var(--jvc-text); font-size: 0.9rem; font-family: inherit;
            line-height: 1.4; resize: none; overflow-y: auto; outline: none;
            -webkit-appearance: none; transition: border-color 150ms ease;
        }
        #jvcm-textarea:focus { border-color: var(--jvc-orange); }
        #jvcm-textarea::placeholder { color: var(--jvc-text-muted); }

        #jvcm-send-btn {
            width: 36px; height: 36px; flex-shrink: 0; display: grid; place-items: center;
            background: var(--jvc-orange); border: none; border-radius: 50%; cursor: pointer;
            -webkit-tap-highlight-color: transparent; transition: background 120ms ease, transform 100ms ease;
        }
        #jvcm-send-btn:active   { background: var(--jvc-orange-deep); transform: scale(0.92); }
        #jvcm-send-btn:disabled { opacity: 0.5; pointer-events: none; }
        #jvcm-send-btn svg { width: 16px; height: 16px; fill: #fff; }

        #jvcm-clear-btn {
            width: 36px; height: 36px; flex-shrink: 0;
            display: none; place-items: center;
            background: var(--jvc-surface-3); border: 1px solid var(--jvc-border);
            border-radius: 50%; cursor: pointer; color: var(--jvc-text-muted);
            font-size: 1rem; line-height: 1;
            -webkit-tap-highlight-color: transparent;
            transition: background 120ms ease, color 120ms ease;
        }
        #jvcm-clear-btn.visible { display: grid; }
        #jvcm-clear-btn:active { background: var(--jvc-surface-2); color: var(--jvc-text); }

        /* ── ACTIONS INLINE ── */
        .jvcm-actions { display: none; flex-direction: column; align-items: flex-end; gap: 0.2rem; margin-left: auto; flex-shrink: 0; }
        .jvcm-message.is-active .jvcm-actions { display: flex; }
        .jvcm-actions-row { display: flex; align-items: center; gap: 0.25rem; }

        .jvcm-action-btn {
            display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.28rem 0.6rem;
            background: var(--jvc-surface-2); border: 1px solid var(--jvc-border);
            border-radius: 4px; color: var(--jvc-text-dim); font-size: 0.72rem;
            font-weight: 500; font-family: inherit; cursor: pointer;
            -webkit-tap-highlight-color: transparent; transition: background 100ms ease, color 100ms ease;
        }
        .jvcm-action-btn:active { background: var(--jvc-surface-3); color: var(--jvc-text); }
        .jvcm-action-btn.danger { color: var(--jvc-red); border-color: rgba(248,113,113,0.3); }
        .jvcm-action-btn.danger:active { background: rgba(127,29,29,0.4); }

        /* ── TOAST ── */
        .jvcm-toast {
            position: fixed; top: calc(var(--jvc-header-h) + 8px); left: 50%;
            transform: translateX(-50%); z-index: 99999;
            padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.8rem;
            font-weight: 500; color: #fff; background: #1a1a1f;
            border: 1px solid var(--jvc-border); box-shadow: 0 4px 20px rgba(0,0,0,0.5);
            white-space: nowrap; animation: jvcm-toast-in 200ms ease; pointer-events: none;
        }
        .jvcm-toast.error   { background: #7f1d1d; border-color: var(--jvc-red); }
        .jvcm-toast.success { background: #14532d; border-color: var(--jvc-green); }
        .jvcm-toast.info    { background: #1e3a5f; border-color: var(--jvc-blue); }
        .jvcm-toast.warn    { background: #78350f; border-color: #fbbf24; }
        @keyframes jvcm-toast-in {
            from { opacity: 0; transform: translate(-50%, -8px); }
            to   { opacity: 1; transform: translate(-50%, 0); }
        }

        /* ── MODAL GÉNÉRIQUE (report + confirm delete) ── */
        .jvcm-modal-overlay {
            position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 1000;
            display: flex; align-items: center; justify-content: center; padding: 1rem;
        }
        .jvcm-modal-overlay.hidden { display: none; }

        .jvcm-modal {
            background: var(--jvc-surface-1); border: 1px solid var(--jvc-border);
            border-radius: 8px; width: 100%; max-width: 360px;
            display: flex; flex-direction: column; overflow: hidden;
        }
        .jvcm-modal-header {
            display: flex; align-items: center; justify-content: space-between;
            padding: 0.875rem 1rem; border-bottom: 1px solid var(--jvc-border);
            font-size: 0.9rem; font-weight: 600; color: var(--jvc-text);
        }
        .jvcm-modal-close {
            background: none; border: none; color: var(--jvc-text-muted);
            font-size: 1.3rem; cursor: pointer; line-height: 1; padding: 0.2rem;
        }
        .jvcm-modal-body { padding: 1rem; font-size: 0.85rem; color: var(--jvc-text-dim); line-height: 1.5; }
        .jvcm-modal-body strong { color: var(--jvc-text); }
        .jvcm-modal-footer {
            padding: 0.75rem 1rem; border-top: 1px solid var(--jvc-border);
            display: flex; gap: 0.5rem; justify-content: flex-end;
        }
        .jvcm-modal-cancel {
            padding: 0.45rem 1rem; background: var(--jvc-surface-2);
            border: 1px solid var(--jvc-border); border-radius: 5px;
            color: var(--jvc-text-dim); font-family: inherit; font-size: 0.82rem; cursor: pointer;
        }
        .jvcm-modal-confirm {
            padding: 0.45rem 1rem; background: var(--jvc-orange); border: none;
            border-radius: 5px; color: #fff; font-family: inherit;
            font-size: 0.82rem; font-weight: 600; cursor: pointer; transition: background 150ms ease;
        }
        .jvcm-modal-confirm:hover { background: var(--jvc-orange-deep); }
        .jvcm-modal-confirm.danger { background: #991b1b; }
        .jvcm-modal-confirm.danger:hover { background: #7f1d1d; }

        /* ── SETTINGS ── */
        #jvcm-settings {
            position: fixed; inset: 0; background: var(--jvc-bg); z-index: 200;
            display: flex; flex-direction: column;
            transform: translateY(100%);
            transition: transform 300ms cubic-bezier(.4,0,.2,1);
            touch-action: pan-y;
        }
        #jvcm-settings.open { transform: translateY(0); }

        #jvcm-settings-header {
            display: flex; align-items: center; padding: 0 1rem;
            min-height: var(--jvc-header-h);
            background: var(--jvc-surface-1); border-bottom: 1px solid var(--jvc-border);
            gap: 0.75rem; font-size: 0.9rem; font-weight: 600; color: var(--jvc-text);
            cursor: grab;
        }
        /* Poignée de drag sur le settings panel */
        #jvcm-settings-header::before {
            content: ''; display: block; width: 32px; height: 4px;
            background: var(--jvc-border); border-radius: 2px;
            position: absolute; top: 6px; left: 50%; transform: translateX(-50%);
        }
        #jvcm-settings-close {
            margin-left: auto; background: none; border: none;
            color: var(--jvc-text-muted); font-size: 1.4rem;
            cursor: pointer; line-height: 1; padding: 0.2rem 0.4rem;
        }
        #jvcm-settings-body {
            flex: 1; overflow-y: auto; padding: 1rem;
            display: flex; flex-direction: column; gap: 0.75rem;
        }
        .jvcm-setting-row {
            display: flex; align-items: center; justify-content: space-between;
            padding: 0.75rem 0.875rem; background: var(--jvc-surface-1);
            border: 1px solid var(--jvc-border); border-radius: 6px; gap: 1rem;
        }
        .jvcm-setting-label { font-size: 0.85rem; color: var(--jvc-text); }
        .jvcm-setting-desc  { font-size: 0.7rem; color: var(--jvc-text-muted); margin-top: 0.2rem; }

        .jvcm-toggle { position: relative; width: 40px; height: 22px; flex-shrink: 0; }
        .jvcm-toggle input { opacity: 0; width: 0; height: 0; position: absolute; }
        .jvcm-toggle-track {
            position: absolute; inset: 0; background: var(--jvc-surface-3);
            border-radius: 999px; border: 1px solid var(--jvc-border);
            transition: background 200ms ease; cursor: pointer;
        }
        .jvcm-toggle-track::after {
            content: ''; position: absolute; top: 2px; left: 2px;
            width: 16px; height: 16px; background: var(--jvc-text-muted);
            border-radius: 50%; transition: transform 200ms ease, background 200ms ease;
        }
        .jvcm-toggle input:checked + .jvcm-toggle-track { background: var(--jvc-orange); border-color: var(--jvc-orange); }
        .jvcm-toggle input:checked + .jvcm-toggle-track::after { transform: translateX(18px); background: #fff; }

        /* ── MODALE MATCHS ── */
        #jvcm-matches-overlay {
            position: fixed; inset: 0;
            background: rgba(0,0,0,0.75); z-index: 500;
            display: flex; align-items: flex-end; justify-content: center;
            padding: 0;
        }
        #jvcm-matches-overlay.hidden { display: none; }

        #jvcm-matches-panel {
            background: var(--jvc-bg);
            border: 1px solid var(--jvc-border);
            border-radius: 12px 12px 0 0;
            width: 100%; max-width: 520px;
            max-height: 85dvh;
            display: flex; flex-direction: column;
            overflow: hidden;
        }

        #jvcm-matches-handle {
            width: 36px; height: 4px;
            background: var(--jvc-border);
            border-radius: 2px;
            margin: 10px auto 0;
            flex-shrink: 0;
        }

        #jvcm-matches-header {
            display: flex; align-items: center;
            padding: 0.75rem 1rem 0.5rem;
            gap: 0.5rem; flex-shrink: 0;
        }
        #jvcm-matches-title {
            font-size: 0.9rem; font-weight: 700; color: var(--jvc-text); flex: 1;
        }
        #jvcm-matches-close {
            background: none; border: none; color: var(--jvc-text-muted);
            font-size: 1.3rem; cursor: pointer; line-height: 1; padding: 0.2rem;
        }
        #jvcm-matches-refresh {
            background: none; border: none; color: var(--jvc-text-muted);
            font-size: 1rem; cursor: pointer; padding: 0.2rem 0.4rem;
            border-radius: 4px; transition: color 150ms ease;
        }
        #jvcm-matches-refresh:active { color: var(--jvc-orange); }
        #jvcm-matches-refresh.spinning { animation: jvcm-spin 0.7s linear infinite; color: var(--jvc-orange); }

        /* Sélection ligues */
        #jvcm-leagues-wrap {
            padding: 0 1rem 0.5rem;
            display: flex; flex-wrap: wrap; gap: 0.35rem;
            flex-shrink: 0;
            border-bottom: 1px solid var(--jvc-border);
            padding-bottom: 0.6rem;
        }
        .jvcm-league-btn {
            padding: 0.25rem 0.6rem;
            background: var(--jvc-surface-2);
            border: 1px solid var(--jvc-border);
            border-radius: 999px;
            color: var(--jvc-text-muted);
            font-size: 0.68rem; font-weight: 600;
            font-family: inherit; cursor: pointer;
            transition: all 150ms ease;
            -webkit-tap-highlight-color: transparent;
        }
        .jvcm-league-btn.active {
            background: var(--jvc-orange);
            border-color: var(--jvc-orange);
            color: #fff;
        }

        /* Liste des matchs */
        #jvcm-matches-list {
            flex: 1; overflow-y: auto; padding: 0.5rem 0;
        }

        .jvcm-match-row { padding: 0.55rem 1rem; border-bottom: 1px solid var(--jvc-divider); display: flex; flex-direction: column; gap: 0.25rem; }
        .jvcm-match-row:last-child { border-bottom: none; }
        .jvcm-match-row[data-state="inProgress"] {
            background: rgba(239, 68, 68, 0.04);
            border-left: 2px solid rgba(239, 68, 68, 0.5);
        }

        /* Score game par game */
        .jvcm-match-games {
            display: flex; align-items: center; gap: 0.25rem;
            margin-top: 0.15rem; flex-wrap: wrap;
        }
        .jvcm-game-dot {
            width: 22px; height: 22px;
            border-radius: 4px;
            font-size: 0.62rem; font-weight: 700;
            display: grid; place-items: center;
            background: var(--jvc-surface-3);
            color: var(--jvc-text-muted);
            cursor: default;
            transition: opacity 150ms ease;
        }
        .jvcm-game-dot.clickable {
            cursor: pointer;
            border: 1px solid var(--jvc-border);
        }
        .jvcm-game-dot.clickable:active { opacity: 0.7; }
        .jvcm-game-dot.selected {
            border-color: var(--jvc-orange);
            box-shadow: 0 0 0 1px var(--jvc-orange);
        }
        .jvcm-game-dot.win  { background: rgba(74,222,128,0.2); color: var(--jvc-green); }
        .jvcm-game-dot.loss { background: rgba(248,113,113,0.15); color: var(--jvc-red); }
        .jvcm-game-dot.win-blue { background: rgba(100,149,237,0.25); color: #7fb4ff; border-color: rgba(100,149,237,0.4); }
        .jvcm-game-dot.win-red  { background: rgba(248,113,113,0.2); color: var(--jvc-red); border-color: rgba(248,113,113,0.4); }
        .jvcm-game-dot.live { background: rgba(239,68,68,0.2); color: #ef4444; animation: jvcm-breathe 2.4s ease-in-out infinite; }

        .jvcm-match-row-head {
            display: flex; align-items: center; gap: 0.4rem;
            font-size: 0.58rem; font-weight: 700;
            letter-spacing: 0.08em; text-transform: uppercase;
        }
        .jvcm-match-badge-live     { color: #ef4444; display: flex; align-items: center; gap: 0.3rem; }
        .jvcm-match-badge-upcoming { color: #fbbf24; text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 0.65rem; }
        .jvcm-match-badge-done     { color: var(--jvc-text-muted); }
        .jvcm-match-dot {
            width: 6px; height: 6px; border-radius: 50%;
            background: currentColor;
            animation: jvcm-breathe 2.4s ease-in-out infinite;
        }
        @keyframes jvcm-breathe {
            0%, 100% { opacity: 1; }
            50%       { opacity: 0.3; }
        }
        .jvcm-match-stage {
            color: var(--jvc-text-muted); font-weight: 500;
            font-size: 0.6rem; text-transform: none; letter-spacing: 0;
            overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
        }
        .jvcm-match-row-body {
            display: flex; align-items: baseline;
            gap: 0.3rem; font-size: 0.82rem;
            font-weight: 700; color: var(--jvc-text);
        }
        .jvcm-match-team {
            flex: 1; min-width: 0; overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap;
        }
        .jvcm-match-team:last-child { text-align: right; }
        .jvcm-match-team.winner { color: var(--jvc-green); }
        .jvcm-match-team.loser  { color: var(--jvc-text-muted); }
        .jvcm-match-score {
            flex-shrink: 0; font-size: 0.78rem;
            font-weight: 800; padding: 0 0.4rem;
            font-variant-numeric: tabular-nums;
        }
        .jvcm-match-vs {
            flex-shrink: 0; font-size: 0.65rem;
            color: var(--jvc-text-muted); padding: 0 0.4rem;
        }

        #jvcm-matches-empty {
            text-align: center; padding: 2rem 1rem;
            color: var(--jvc-text-muted); font-size: 0.82rem;
        }
        #jvcm-matches-loading {
            text-align: center; padding: 2rem 1rem;
            color: var(--jvc-text-muted); font-size: 0.82rem;
            display: flex; flex-direction: column; align-items: center; gap: 0.75rem;
        }

        /* Stats game terminée (expansion au clic sur dot) */
        .jvcm-game-stats {
            display: none; flex-direction: column;
            margin-top: 0.35rem;
            background: rgba(255,255,255,0.03);
            border: 1px solid var(--jvc-border);
            border-radius: 6px; overflow: hidden;
        }
        .jvcm-game-stats.open { display: flex; }
        .jvcm-game-stats-header {
            padding: 0.3rem 0.5rem;
            font-size: 0.6rem; font-weight: 700;
            color: var(--jvc-text-muted);
            text-transform: uppercase; letter-spacing: 0.08em;
            border-bottom: 1px solid var(--jvc-border);
            display: flex; align-items: center; gap: 0.4rem;
        }
        .jvcm-game-stats-loading {
            padding: 0.6rem; text-align: center;
            font-size: 0.65rem; color: var(--jvc-text-muted);
        }

        /* Détail par lane */
        .jvcm-lane-detail {
            display: none; flex-direction: column;
            border-top: 1px solid rgba(239,68,68,0.15);
        }
        .jvcm-lane-detail.open { display: flex; }
        .jvcm-lane-row {
            display: flex; align-items: center;
            padding: 0.35rem 0.5rem; gap: 0.3rem;
            border-bottom: 1px solid var(--jvc-divider);
            font-size: 0.62rem; font-variant-numeric: tabular-nums;
        }
        .jvcm-lane-row:last-child { border-bottom: none; }
        .jvcm-lane-badge {
            width: 20px; text-align: center; flex-shrink: 0;
            font-size: 0.6rem; color: var(--jvc-text-muted); font-weight: 600;
        }
        .jvcm-lane-player {
            flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.05rem;
        }
        .jvcm-lane-player.right { align-items: flex-end; text-align: right; }
        .jvcm-lane-name     { font-weight: 600; color: var(--jvc-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 80px; }
        .jvcm-lane-champ    { color: var(--jvc-text-muted); font-size: 0.58rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 80px; }
        .jvcm-lane-kda      { color: var(--jvc-text-dim); font-size: 0.6rem; white-space: nowrap; }
        .jvcm-lane-gold     { color: #fbbf24; font-size: 0.6rem; font-weight: 600; }
        .jvcm-lane-mid-col  { display: flex; flex-direction: column; align-items: center; gap: 0.1rem; flex-shrink: 0; padding: 0 0.2rem; }
        .jvcm-lane-cs       { color: var(--jvc-text-muted); font-size: 0.58rem; font-style: italic; }
        .jvcm-lane-level    { font-size: 0.6rem; color: var(--jvc-text-muted); background: var(--jvc-surface-3); border-radius: 3px; padding: 0.05rem 0.25rem; }

        /* Streams */
        .jvcm-match-streams {
            display: flex; align-items: center; gap: 0.35rem;
            margin-top: 0.4rem; flex-wrap: wrap;
        }
        .jvcm-stream-btn {
            display: inline-flex; align-items: center; gap: 0.3rem;
            padding: 0.28rem 0.65rem;
            background: #9147ff; border: none; border-radius: 4px;
            color: #fff; font-size: 0.7rem; font-weight: 600;
            font-family: inherit; cursor: pointer;
            -webkit-tap-highlight-color: transparent;
            transition: background 120ms ease;
            text-decoration: none;
        }
        .jvcm-stream-btn:active { background: #7b35d9; }
        .jvcm-stream-select {
            padding: 0.28rem 0.5rem;
            background: #9147ff; border: none; border-radius: 4px;
            color: #fff; font-size: 0.7rem; font-weight: 600;
            font-family: inherit; cursor: pointer;
            -webkit-appearance: none; outline: none;
        }

        /* Fenêtre temporelle */
        #jvcm-time-range {
            padding: 0.5rem 1rem 0.4rem;
            border-bottom: 1px solid var(--jvc-border);
            display: flex; gap: 1rem; flex-shrink: 0;
        }
        .jvcm-range-group {
            flex: 1; display: flex; flex-direction: column; gap: 0.25rem;
        }
        .jvcm-range-label {
            font-size: 0.6rem; font-weight: 600; color: var(--jvc-text-muted);
            text-transform: uppercase; letter-spacing: 0.08em;
            display: flex; justify-content: space-between;
        }
        .jvcm-range-label span { color: var(--jvc-text); font-variant-numeric: tabular-nums; }
        .jvcm-range-input {
            -webkit-appearance: none; appearance: none;
            width: 100%; height: 4px;
            background: var(--jvc-surface-3); border-radius: 2px; outline: none;
        }
        .jvcm-range-input::-webkit-slider-thumb {
            -webkit-appearance: none; width: 16px; height: 16px;
            border-radius: 50%; background: var(--jvc-orange); cursor: pointer;
        }
        .jvcm-range-input::-moz-range-thumb {
            width: 16px; height: 16px; border: none;
            border-radius: 50%; background: var(--jvc-orange); cursor: pointer;
        }

        /* Bouton matchs dans le header */
        #jvcm-matches-btn {
            background: none; border: none; color: var(--jvc-text-muted);
            font-size: 1rem; cursor: pointer; padding: 0.2rem 0.3rem;
            line-height: 1; -webkit-tap-highlight-color: transparent; flex-shrink: 0;
            position: relative;
        }
        #jvcm-matches-btn:active { color: var(--jvc-orange); }
        .jvcm-live-badge {
            position: absolute; top: -2px; right: -2px;
            min-width: 14px; height: 14px; padding: 0 3px;
            background: #ef4444; color: #fff;
            border-radius: 999px; font-size: 0.5rem; font-weight: 700;
            display: flex; align-items: center; justify-content: center;
            pointer-events: none;
        }
    `;
    document.head.appendChild(style);
}

// ─────────────────────────────────────────────────────────────
// SETTINGS
// ─────────────────────────────────────────────────────────────
class Settings {
    constructor() {
        this._defs = [
            { key: 'send_on_enter',  label: 'Envoyer avec Entrée',     desc: 'Sinon, Entrée = saut de ligne',                      default: false },
            { key: 'swipe_to_quote', label: 'Swipe droite pour citer', desc: 'Glisser un message vers la droite',                  default: true  },
            { key: 'notifications',  label: 'Notifications navigateur',desc: 'Alerte quand un nouveau message arrive (hors onglet)',default: false },
            { key: 'vibration',      label: 'Vibration sur mention',   desc: 'Vibre quand quelqu\'un te cite (Android uniquement)', default: true  },
        ];
    }
    get(key) {
        const raw = localStorage.getItem(`jvcm:setting:${key}`);
        const def = this._defs.find(d => d.key === key);
        try { return raw !== null ? JSON.parse(raw) : (def?.default ?? false); } catch { return def?.default ?? false; }
    }
    set(key, val) { try { localStorage.setItem(`jvcm:setting:${key}`, JSON.stringify(val)); } catch {} }
    get defs() { return this._defs; }
}

// ─────────────────────────────────────────────────────────────
// HORODATAGE RELATIF
// ─────────────────────────────────────────────────────────────
function parseJVCDate(str) {
    if (!str) return null;
    const short = str.match(/(\d{2}):(\d{2}):(\d{2})$/);
    if (short) {
        const now = new Date();
        return new Date(now.getFullYear(), now.getMonth(), now.getDate(), +short[1], +short[2], +short[3]).getTime();
    }
    const long = str.match(/(\d+)\s+(\w+)\s+(\d{4}).*?(\d{2}):(\d{2}):(\d{2})/);
    if (long) {
        const months = { janvier:0,février:1,mars:2,avril:3,mai:4,juin:5,juillet:6,août:7,septembre:8,octobre:9,novembre:10,décembre:11 };
        return new Date(+long[3], months[long[2].toLowerCase()] ?? 0, +long[1], +long[4], +long[5], +long[6]).getTime();
    }
    return null;
}

function relativeTime(ts) {
    if (!ts) return '';
    const s = Math.floor((Date.now() - ts) / 1000);
    if (s < 60)  return "à l'instant";
    const m = Math.floor(s / 60);
    if (m < 60)  return `il y a ${m} min`;
    const h = Math.floor(m / 60);
    if (h < 24)  return `il y a ${h}h`;
    return `il y a ${Math.floor(h / 24)}j`;
}

// ─────────────────────────────────────────────────────────────
// UTILITAIRES
// ─────────────────────────────────────────────────────────────
function pseudoHue(pseudo) {
    let h = 0;
    for (let i = 0; i < pseudo.length; i++) h = ((h << 5) - h + pseudo.charCodeAt(i)) | 0;
    return Math.abs(h) % 360;
}

function jvCake(str) {
    const base16 = '0A12B34C56D78E9F';
    const s = str.split(' ')[1];
    let lien = '';
    for (let i = 0; i < s.length; i += 2)
        lien += String.fromCharCode(base16.indexOf(s.charAt(i)) * 16 + base16.indexOf(s.charAt(i + 1)));
    return lien;
}

function reverseMessage(node, isInit, isUl) {
    let quote = '', prevIsP = false, startsWithSpoil = false;
    for (const child of node.childNodes) {
        const name = child.nodeName;
        switch (name) {
            case 'P':      quote += reverseMessage(child) + '\n\n'; break;
            case 'STRONG': quote += "'''" + reverseMessage(child) + "'''"; break;
            case 'EM':     quote += "''" + reverseMessage(child) + "''"; break;
            case 'U':      quote += '<u>' + reverseMessage(child) + '</u>'; break;
            case 'S':      quote += '<s>' + reverseMessage(child) + '</s>'; break;
            case 'BR':     quote += '\n'; break;
            case 'UL':     quote += reverseMessage(child, false, true) + '\n\n'; break;
            case 'OL':     quote += reverseMessage(child, false, false) + '\n\n'; break;
            case 'LI':     quote += (isUl ? '* ' : '# ') + reverseMessage(child) + '\n'; break;
            case 'IMG':    quote += child.alt; break;
            case 'A':      quote += child.href ? child.href : reverseMessage(child); break;
            case 'CODE':   quote += '<code>' + child.textContent + '</code>'; break;
            case 'PRE':    quote += reverseMessage(child) + '\n\n'; break;
            case 'BLOCKQUOTE':
                quote += (prevIsP ? quote.trimEnd() + '\n' : '') + reverseMessage(child).replace(/^/gm, '> ') + '\n\n';
                break;
            case 'DIV': case 'SPAN': {
                const cl = child.classList;
                if (cl.contains('message__spoil')) {
                    if (!quote) startsWithSpoil = true;
                    quote += '<spoil>' + reverseMessage(child) + '</spoil>\n\n';
                } else if (cl.contains('message__spoilContent')) {
                    quote += reverseMessage(child);
                }
                break;
            }
            case '#text':
                if (!isInit || child.textContent.trim() !== '') {
                    quote += child.textContent;
                    if (isInit && !quote.endsWith('\n')) quote += '\n';
                }
                break;
        }
        prevIsP = (name === 'P');
    }
    quote = quote.replace(/(\n){3,}/g, '\n\n');
    if (startsWithSpoil && isInit) quote = '\n' + quote.trimEnd();
    else quote = quote.trim();
    if (isInit) quote = quote.replace(/^/gm, '> ');
    return quote;
}

function updateFavicon(count) {
    const link = document.querySelector('link[rel*="icon"]') || (() => {
        const l = document.createElement('link'); l.rel = 'icon'; document.head.appendChild(l); return l;
    })();
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.onload = () => {
        const canvas = document.createElement('canvas');
        canvas.width = canvas.height = 16;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, 16, 16);
        if (count > 0) {
            const label = count > 99 ? '99+' : String(count);
            ctx.fillStyle = '#e84d1a';
            ctx.fillRect(0, 0, ctx.measureText(label).width + 4, 11);
            ctx.fillStyle = '#fff';
            ctx.font = 'bold 9px sans-serif';
            ctx.textBaseline = 'bottom';
            ctx.fillText(label, 2, 11);
        }
        link.href = canvas.toDataURL();
    };
    img.onerror = () => {};
    img.src = link.href || '/favicon.ico';
}

// ─────────────────────────────────────────────────────────────
// NOTIFICATIONS
// ─────────────────────────────────────────────────────────────
async function requestNotifPermission() {
    if (!('Notification' in window)) return false;
    if (Notification.permission === 'granted') return true;
    if (Notification.permission === 'denied') return false;
    return (await Notification.requestPermission()) === 'granted';
}

function sendNotif(title, body) {
    if (!('Notification' in window) || Notification.permission !== 'granted') return;
    const n = new Notification(title, { body, icon: '/favicon.ico', tag: 'jvchat' });
    n.onclick = () => { window.focus(); n.close(); };
    setTimeout(() => n.close(), 6000);
}

// ─────────────────────────────────────────────────────────────
// PARSER
// ─────────────────────────────────────────────────────────────
function parsePage(doc) {
    const titleEl = doc.querySelector('#title-display-container');
    const title = titleEl ? titleEl.textContent.trim() : '';

    let lastPage = 1;
    for (const a of doc.querySelectorAll('.pagination .pagination__navigation a')) {
        const p = parseInt(a.textContent.trim());
        if (!isNaN(p) && p > lastPage) lastPage = p;
    }
    const cur = doc.querySelector('.pagination__item--current');
    if (cur) { const p = parseInt(cur.textContent); if (!isNaN(p) && p > lastPage) lastPage = p; }
    for (const a of doc.querySelectorAll('.bloc-liste-num-page span a')) {
        const p = parseInt(a.textContent.trim());
        if (!isNaN(p) && p > lastPage) lastPage = p;
    }

    const messages = [];
    for (const msg of doc.querySelectorAll('#listMessages .messageUser.js-hybrid-component, #listMessages .messageUser, .bloc-message-forum')) {
        if (msg.children.length === 0) continue;
        const msgEl   = msg.querySelector('.messageUser__msg, .bloc-contenu');
        const dateEl  = msg.querySelector('.messageUser__date, .bloc-date-msg');
        const labelEl = msg.querySelector('.messageUser__label, .bloc-pseudo-msg');
        if (!msgEl || !dateEl || !labelEl) continue;
        let avatarUrl = 'https://image.jeuxvideo.com/avatar/default.jpg';
        const av = msg.querySelector('img.avatar__image, .bloc-avatar img');
        if (av) avatarUrl = av.src;
        const username  = labelEl.textContent.trim();
        const rankEl    = msg.querySelector('.messageUser__memberGroup, .messageUser__rank, .bloc-grade-msg, .grade-pseudo');
        const userRank  = rankEl?.textContent.trim() || '';
        const id = msg.id ? msg.id.split('-').pop() : msg.getAttribute('data-id');
        messages.push({ id, page: lastPage, username, userRank, avatarUrl, content: msgEl.innerHTML, creationDate: dateEl.textContent.trim() });
    }

    const connectedEl = doc.querySelector('#forums-info-app .sideCardForum__headerExtra .sideCardForum__Link');
    const connectedCount = connectedEl ? parseInt(connectedEl.textContent.trim()) || 0 : 0;

    return { title, lastPage, messages, connectedCount };
}

function getPayload(doc) {
    for (const sc of doc.getElementsByTagName('script')) {
        const txt = sc.textContent || '';
        const m = txt.match(/(?:window\.)?jvc\.forumsAppPayload\s*=\s*['"]([^'"]+)['"]/);
        if (m) { try { return JSON.parse(atob(m[1])); } catch (e) { err('payload:', e); } }
    }
    return null;
}

// ─────────────────────────────────────────────────────────────
// API
// ─────────────────────────────────────────────────────────────
class JVCAPI {
    constructor(viewId, forumId, topicId, topicTitle) {
        Object.assign(this, { viewId, forumId, topicId, topicTitle, payload: null });
    }
    _url(page) {
        return `https://www.jeuxvideo.com/forums/${this.viewId}-${this.forumId}-${this.topicId}-${page}-0-1-0-${this.topicTitle}.htm`;
    }
    async getPageDocument(page) {
        const res = await fetch(this._url(page));
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return new DOMParser().parseFromString(await res.text(), 'text/html');
    }
    async getMessage(id) {
        try {
            const res = await fetch(`https://www.jeuxvideo.com/forums/message/${id}`);
            return res.ok ? await res.text() : null;
        } catch { return null; }
    }
    async postMessage(text) {
        const fd = new FormData();
        fd.set('text', text); fd.set('topicId', this.topicId); fd.set('forumId', this.forumId);
        fd.set('group', 1); fd.set('messageId', 'undefined'); fd.set('ajax_hash', this.payload.ajaxToken);
        for (const k in this.payload.formSession) fd.append(k, this.payload.formSession[k]);
        const res = await fetch('https://www.jeuxvideo.com/forums/message/add', {
            credentials: 'include', method: 'POST', mode: 'cors', body: fd
        });
        if (!res.ok) throw new Error(await res.text());
        const data = await res.json();
        if (data.errors) {
            const msgs = Object.values(data.errors);
            if (msgs.length) throw new Error(msgs.join(' | '));
        }
    }
    async deleteMessage(msgId) {
        const url = `https://www.jeuxvideo.com${this.payload.topicActions.deleteMessageUrl}&ids=${msgId}`;
        const res = await fetch(url, { credentials: 'include', method: 'POST', mode: 'cors' });
        const data = await res.json();
        if (data.errors?.length) throw new Error(data.errors.join(' | '));
    }
    async getEditForm(msgId) {
        const url = `https://www.jeuxvideo.com/forums/message/edit/form-values?id_message=${msgId}&ajax_hash=${this.payload.ajaxToken}`;
        const res = await fetch(url, { credentials: 'include', headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json' } });
        if (!res.ok) throw new Error('Echec récupération');
        const data = await res.json();
        if (data.errors) {
            const msgs = Array.isArray(data.errors) ? data.errors : Object.values(data.errors);
            if (msgs.length) throw new Error(msgs.join(' | '));
        }
        const jvcode = data.jvcode || data.text || '';
        if (!jvcode) throw new Error("Délai d'édition dépassé");
        const fs = {};
        for (const k in data) if (k.startsWith('fs_')) fs[k] = data[k];
        if (data.edit_form_session) Object.assign(fs, data.edit_form_session);
        return { jvcode, formSession: fs };
    }
    async updateMessage(msgId, text, formSession) {
        const fd = new FormData();
        fd.set('text', text); fd.set('topicId', this.topicId); fd.set('forumId', this.forumId);
        fd.set('group', 1); fd.set('messageId', msgId); fd.set('ajax_hash', this.payload.ajaxToken);
        fd.set('resetFormAfterSuccess', 'false');
        for (const k in formSession) fd.append(k, formSession[k]);
        const res = await fetch('https://www.jeuxvideo.com/forums/message/edit', {
            method: 'POST', credentials: 'include',
            headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json' }, body: fd
        });
        const data = await res.json();
        if (data.errors) {
            const msgs = Object.values(data.errors);
            if (msgs.length) throw new Error(msgs.join(' | '));
        }
        return data.html;
    }
}

// ─────────────────────────────────────────────────────────────
// LONG PRESS
// ─────────────────────────────────────────────────────────────
function attachLongPress(el, onLongPress, delay = 420) {
    let timer = null, fired = false, active = false, startX, startY;
    const cancel = () => { clearTimeout(timer); timer = null; active = false; };

    el.addEventListener('touchstart', (e) => {
        fired = false; active = true;
        startX = e.touches[0].clientX; startY = e.touches[0].clientY;
        timer = setTimeout(() => { fired = true; onLongPress(e); }, delay);
    }, { passive: true });
    el.addEventListener('touchmove', (e) => {
        if (!active) return;
        if (Math.abs(e.touches[0].clientX - startX) > 8 || Math.abs(e.touches[0].clientY - startY) > 8) cancel();
    }, { passive: true });
    el.addEventListener('touchend',    cancel);
    el.addEventListener('touchcancel', cancel);

    el.addEventListener('mousedown', (e) => {
        if (e.button !== 0) return;
        fired = false; active = true;
        startX = e.clientX; startY = e.clientY;
        timer = setTimeout(() => { fired = true; onLongPress(e); }, delay);
        const onMove = (ev) => {
            if (Math.abs(ev.clientX - startX) > 8 || Math.abs(ev.clientY - startY) > 8) { cancel(); cleanup(); }
        };
        const onUp = () => { cancel(); cleanup(); };
        const cleanup = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
        document.addEventListener('mousemove', onMove);
        document.addEventListener('mouseup',   onUp);
    });
    el.addEventListener('contextmenu', (e) => { if (fired) { e.preventDefault(); fired = false; } });
}

// ─────────────────────────────────────────────────────────────
// MATCHS ESPORT
// ─────────────────────────────────────────────────────────────

const LOLESPORTS_API  = 'https://esports-api.lolesports.com/persisted/gw';
const LOLESPORTS_KEY  = '0TvQnueqKa5mxJntVWt0w4LpLfEkrV1Ta8rQBb9Z';
const MATCHES_TTL     = 60_000;
const BO3GG_API       = 'https://api.bo3.gg/api/v1';
const BO3GG_CACHE_TTL = 12 * 3600 * 1000;
const BO3GG_BLOCK_KEY = 'jvcm:bo3_blocked_until';
const BO3GG_BLOCK_MS  = 30 * 60_000;

// Toutes les ligues disponibles avec leur label affiché
const ALL_LEAGUES = [
    // Tier 1
    { slug: 'lec',             label: 'LEC',         group: 'Tier 1' },
    { slug: 'lck',             label: 'LCK',         group: 'Tier 1' },
    { slug: 'lpl',             label: 'LPL',         group: 'Tier 1' },
    { slug: 'lcp',             label: 'LCP',         group: 'Tier 1' },
    { slug: 'lcs',             label: 'LCS',         group: 'Tier 1' },
    { slug: 'cblol',           label: 'CBLOL',       group: 'Tier 1' },
    { slug: 'worlds',          label: 'Worlds',      group: 'Tier 1' },
    { slug: 'msi',             label: 'MSI',         group: 'Tier 1' },
    { slug: 'first_stand',     label: 'First Stand', group: 'Tier 1' },
    { slug: 'ewc',             label: 'EWC',         group: 'Tier 1' },
    // Tier 2
    { slug: 'emea_masters',    label: 'EMEA Masters',group: 'Tier 2'   },
    // ERL
    { slug: 'lfl',             label: 'LFL',         group: 'Tier 2'    },
    { slug: 'primeleague',     label: 'Prime League',group: 'Tier 2'    },
    { slug: 'superliga',       label: 'Superliga',   group: 'Tier 2'    },
    { slug: 'nlc',             label: 'NLC',         group: 'Tier 2'    },
    { slug: 'pg_nationals',    label: 'PG Nationals',group: 'Tier 2'    },
    { slug: 'ultraliga',       label: 'Ultraliga',   group: 'Tier 2'    },
    { slug: 'tal',             label: 'TAL',         group: 'Tier 2'    },
];

const BO3GG_TOURNAMENTS = {
    ewc: [
        'esports-world-cup-2026-emea-qualifier',
        'esports-world-cup-2026-asia-pacific-qualifier',
        'esports-world-cup-2026-korea-qualifier',
        'esports-world-cup-2026',
    ],
};

const LEAGUES_STORAGE_KEY = 'jvcm:selected-leagues';
const LEAGUES_TTL = 7 * 24 * 3600_000; // 7 jours

function getSelectedLeagues() {
    try {
        const raw = localStorage.getItem(LEAGUES_STORAGE_KEY);
        if (raw) {
            const { slugs, ts } = JSON.parse(raw);
            if (Date.now() - ts < LEAGUES_TTL) return slugs;
            // Expiré : supprimer et retourner le défaut
            try { localStorage.removeItem(LEAGUES_STORAGE_KEY); } catch {}
        }
    } catch {}
    return ['lec', 'lck', 'lpl', 'lfl'];
}

function setSelectedLeagues(slugs) {
    try {
        localStorage.setItem(LEAGUES_STORAGE_KEY, JSON.stringify({ slugs, ts: Date.now() }));
    } catch {}
}

function gmFetch(url, headers = {}) {
    return new Promise((resolve, reject) => {
        const fn = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest
                 : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest
                 : null;
        // Fallback fetch natif (iOS Safari, contextes sans GM_xmlhttpRequest)
        if (!fn) {
            fetch(url, { headers })
                .then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
                .then(resolve).catch(reject);
            return;
        }
        fn({
            method: 'GET', url, headers, timeout: 10_000,
            onload: r => {
                if (r.status >= 200 && r.status < 300) {
                    try { resolve(JSON.parse(r.responseText)); }
                    catch (e) { reject(new Error('JSON parse error')); }
                } else { reject(new Error(`HTTP ${r.status}`)); }
            },
            onerror:   () => reject(new Error('network error')),
            ontimeout: () => reject(new Error('timeout')),
        });
    });
}

function _matchEsc(s) {
    return String(s ?? '').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
}

function _matchFormatStart(iso) {
    const d = new Date(iso);
    if (isNaN(d.getTime())) return '';
    const diff = d.getTime() - Date.now();
    const hh = String(d.getHours()).padStart(2,'0');
    const mm = String(d.getMinutes()).padStart(2,'0');
    if (diff < 0) return `Imminent (${hh}:${mm})`;
    const mins = Math.round(diff / 60_000);
    if (mins < 60) return `Dans ${mins} min`;
    const h = Math.floor(mins / 60);
    const r = mins % 60;
    if (h < 24) return `Dans ${h}h${r > 0 ? String(r).padStart(2,'0') : ''}`;
    const today    = new Date(); today.setHours(0,0,0,0);
    const tomorrow = new Date(today.getTime() + 86_400_000);
    if (d >= today && d < tomorrow)    return `Aujourd'hui ${hh}:${mm}`;
    if (d >= tomorrow)                 return `Demain ${hh}:${mm}`;
    return d.toLocaleDateString('fr-FR',{weekday:'short',day:'numeric',month:'short'}) + ` ${hh}:${mm}`;
}

function _matchFormatPast(iso) {
    const d = new Date(iso);
    if (isNaN(d.getTime())) return '';
    const hh = String(d.getHours()).padStart(2,'0');
    const mm = String(d.getMinutes()).padStart(2,'0');
    const today = new Date(); today.setHours(0,0,0,0);
    const yesterday = new Date(today.getTime() - 86_400_000);
    if (d >= today)      return `Aujourd'hui ${hh}h${mm}`;
    if (d >= yesterday)  return `Hier ${hh}h${mm}`;
    return d.toLocaleDateString('fr-FR',{weekday:'short',day:'numeric',month:'short'}) + ` ${hh}h${mm}`;
}

async function fetchLolesportsMatches(allowedSlugs, pastDays = 1, futureDays = 7) {
    const allowed  = new Set(allowedSlugs.map(s => s.toLowerCase()));
    const headers  = { 'x-api-key': LOLESPORTS_KEY };
    const now      = Date.now();
    // L'API getSchedule retourne max ~48h dans le passé — on plafonne à 2j
    const agoMs    = now - Math.min(pastDays, 2) * 24 * 3600_000;
    const futureMs = now + Math.min(futureDays, 7) * 24 * 3600_000;
    const isOk     = e => allowed.has((e.league?.slug || '').toLowerCase());

    const [liveData, schedData] = await Promise.all([
        gmFetch(`${LOLESPORTS_API}/getLive?hl=fr-FR`, headers).catch(() => null),
        gmFetch(`${LOLESPORTS_API}/getSchedule?hl=fr-FR`, headers).catch(() => null),
    ]);

    const liveEvents  = liveData?.data?.schedule?.events || [];
    const schedEvents = schedData?.data?.schedule?.events || [];

    const filtered = schedEvents.filter(e => {
        if (e.type !== 'match' || !isOk(e)) return false;
        const t = new Date(e.startTime).getTime();
        if (e.state === 'completed')  return t >= agoMs;
        if (e.state === 'inProgress') return true;
        if (e.state === 'unstarted')  return t <= futureMs;
        return false;
    });

    const seen = new Set(); const matches = [];
    for (const ev of [...liveEvents, ...filtered]) {
        const id = ev.match?.id;
        if (!id || seen.has(id) || !isOk(ev)) continue;
        seen.add(id);
        matches.push({
            matchId: id, startTime: ev.startTime, state: ev.state,
            blockName: ev.blockName || '',
            league: { name: ev.league?.name || '', slug: ev.league?.slug || '' },
            teams: (ev.match?.teams || []).map(t => ({
                name: t.name, code: t.code,
                wins: t.result?.gameWins || 0, outcome: t.result?.outcome || null,
            })),
            strategy: ev.match?.strategy || {},
            games: (ev.match?.games || []).map(g => ({
                number: g.number,
                state: g.state,
                teams: (g.teams || []).map(t => ({ id: t.id, side: t.side })),
            })),
            matchUrl: `https://lolesports.com/match/${id}`,
            streams: (ev.streams || [])
                .filter(s => s.provider === 'twitch')
                .map(s => s.parameter)
                .filter(Boolean),
            source: 'lolesports',
        });
    }
    return matches;
}

function _bo3CollectMatches(tournament) {
    const out = [];
    for (const stage of (tournament?.stages || [])) {
        for (const round of (stage?.rounds || [])) {
            for (const m of (round?.matches || [])) out.push({ ...m, _stage: stage, _round: round });
        }
    }
    return out;
}

function _bo3NormalizeMatch(m) {
    if (!m?.start_date) return null;
    const ts = new Date(m.start_date).getTime();
    if (!ts || isNaN(ts)) return null;
    const t1 = { name: m.team1?.name || 'TBD', code: (m.team1?.acronym?.trim()) || m.team1?.name || 'TBD' };
    const t2 = { name: m.team2?.name || 'TBD', code: (m.team2?.acronym?.trim()) || m.team2?.name || 'TBD' };
    const s1 = parseInt(m.team1_score, 10) || 0;
    const s2 = parseInt(m.team2_score, 10) || 0;
    let state, o1 = null, o2 = null;
    const status = (m.status || '').toLowerCase();
    if (status === 'finished' || status === 'defwin' || m.winner_team_id) {
        state = 'completed';
        if (m.winner_team_id === m.team1_id)      { o1='win'; o2='loss'; }
        else if (m.winner_team_id === m.team2_id) { o1='loss'; o2='win'; }
        else if (s1 > s2) { o1='win'; o2='loss'; }
        else if (s2 > s1) { o1='loss'; o2='win'; }
    } else if (['started','live','in_progress'].includes(status)) {
        state = 'inProgress';
    } else {
        state = 'unstarted';
    }
    return {
        matchId: `bo3-${m.id || ts}`, startTime: new Date(ts).toISOString(),
        state, blockName: m._stage?.title || m._round?.name || '',
        league: { name: 'EWC 2026', slug: 'ewc' },
        teams: [
            { name: t1.name, code: t1.code, wins: s1, outcome: o1 },
            { name: t2.name, code: t2.code, wins: s2, outcome: o2 },
        ],
        strategy: { count: parseInt(m.bo_type, 10) || 3 },
        source: 'bo3gg',
    };
}

async function fetchBo3ggTournament(slug) {
    const cacheKey = `jvcm:bo3:${slug}`;
    try {
        const raw = localStorage.getItem(cacheKey);
        if (raw) {
            const c = JSON.parse(raw);
            if (Date.now() - c.ts < BO3GG_CACHE_TTL) return c.matches;
        }
    } catch {}
    const data = await gmFetch(`${BO3GG_API}/tournaments/${encodeURIComponent(slug)}`);
    if (!data || data.error) throw new Error(data?.error?.message || 'bo3.gg error');
    const matches = _bo3CollectMatches(data).map(_bo3NormalizeMatch).filter(Boolean);
    try { localStorage.setItem(cacheKey, JSON.stringify({ ts: Date.now(), matches })); } catch {}
    return matches;
}

async function fetchBo3ggMatches(allowedSlugs) {
    const blockedUntil = parseInt(localStorage.getItem(BO3GG_BLOCK_KEY) || '0', 10);
    if (Date.now() < blockedUntil) return [];
    const slugs = new Set();
    for (const league of allowedSlugs) {
        if (BO3GG_TOURNAMENTS[league]) for (const s of BO3GG_TOURNAMENTS[league]) slugs.add(s);
    }
    if (!slugs.size) return [];
    const all = []; let hadError = false;
    for (const slug of slugs) {
        try { all.push(...await fetchBo3ggTournament(slug)); }
        catch (e) { err('bo3.gg:', e.message); hadError = true; }
    }
    if (hadError && !all.length) localStorage.setItem(BO3GG_BLOCK_KEY, String(Date.now() + BO3GG_BLOCK_MS));
    const now = Date.now();
    const min = now - 48 * 3600_000;
    const max = now + 7 * 24 * 3600_000;
    const seen = new Set();
    return all.filter(m => {
        const t = new Date(m.startTime).getTime();
        if (t < min || t > max) return false;
        const k = `${m.teams[0]?.code}-${m.teams[1]?.code}-${m.startTime}`;
        if (seen.has(k)) return false;
        seen.add(k); return true;
    });
}

async function fetchMatches(allowedSlugs, pastDays = 1, futureDays = 7) {
    const cacheKey = `jvcm:matches:${[...allowedSlugs].sort().join(',')}_p${pastDays}_f${futureDays}`;
    try {
        const raw = localStorage.getItem(cacheKey);
        if (raw) {
            const c = JSON.parse(raw);
            if (Date.now() - c.ts < MATCHES_TTL) return c.data;
        }
    } catch {}

    let matches = await fetchLolesportsMatches(allowedSlugs, pastDays, futureDays).catch(e => { err('lolesports:', e.message); return []; });

    const wantsExtra = allowedSlugs.some(l => BO3GG_TOURNAMENTS[l]);
    if (wantsExtra) {
        const extra = await fetchBo3ggMatches(allowedSlugs).catch(() => []);
        if (extra.length) {
            const keyOf = m => `${m.teams[0]?.code}-${m.teams[1]?.code}-${m.startTime}`;
            const seen = new Set(matches.map(keyOf));
            for (const m of extra) { const k = keyOf(m); if (!seen.has(k)) { matches.push(m); seen.add(k); } }
        }
    }

    const order = { inProgress: 0, unstarted: 1, completed: 2 };
    matches.sort((a, b) => {
        const d = (order[a.state] ?? 9) - (order[b.state] ?? 9);
        return d !== 0 ? d : new Date(a.startTime) - new Date(b.startTime);
    });

    try { localStorage.setItem(cacheKey, JSON.stringify({ ts: Date.now(), data: matches })); } catch {}
    return matches;
}

// Streams Twitch de fallback par ligue slug (quand l'API ne retourne rien)
const LEAGUE_STREAMS = {
    lec:          ['lec', 'otplol_', 'caedrel'],
    lck:          ['lck'],
    lpl:          ['lpl'],
    lcp:          ['lcp'],
    lfl:          ['otplol_', 'solary'],
    lcs:          ['lcsofficial'],
    cblol:        ['cblol'],
    emea_masters: ['emeamasters'],
    primeleague:  ['primeleague'],
    superliga:    ['lvpes'],
    nlc:          ['nlcofficial'],
    pg_nationals: ['pgnationals'],
    ultraliga:    ['ultraliga'],
    tal:          ['talofficial'],
    worlds:       ['lec', 'riotgames'],
    msi:          ['lec', 'riotgames'],
    first_stand:  ['lec'],
    ewc:          ['lec'],
};

function getStreamsForMatch(m) {
    // Priorité : streams déclarés par l'API, sinon fallback par ligue
    const declared = (m.streams || []).filter(Boolean);
    if (declared.length) return declared;
    return LEAGUE_STREAMS[m.league?.slug?.toLowerCase()] || [];
}

function openTwitch(channel) {
    // Tente d'ouvrir l'app Twitch native, fallback sur le navigateur
    const appUrl     = `twitch://stream/${channel}`;
    const browserUrl = `https://www.twitch.tv/${channel}`;
    const start = Date.now();
    window.location.href = appUrl;
    setTimeout(() => {
        if (Date.now() - start < 1500) window.open(browserUrl, '_blank');
    }, 800);
}


function _attachDotHandlers(matchRow, matchId) {
    // Les dots sont purement visuels (gagnant de chaque game)
    // Les stats des games terminées ne sont pas disponibles via l'API publique
}

function renderMatchRow(m) {
    const [a, b] = m.teams?.length >= 2 ? m.teams : [m.teams?.[0] || {}, {}];
    const isLive = m.state === 'inProgress';
    const isDone = m.state === 'completed';
    const bo = m.strategy?.count ? `BO${m.strategy.count}` : '';
    const leagueLabel = (m.league?.slug || '').toUpperCase();
    const stageParts = [leagueLabel, m.blockName, bo, isDone ? _matchFormatPast(m.startTime) : ''].filter(Boolean).map(_matchEsc).join(' · ');

    let badge;
    if (isLive) {
        badge = `<span class="jvcm-match-badge-live"><span class="jvcm-match-dot"></span>LIVE</span>`;
    } else if (isDone) {
        badge = `<span class="jvcm-match-badge-done">Terminé</span>`;
    } else {
        badge = `<span class="jvcm-match-badge-upcoming">${_matchEsc(_matchFormatStart(m.startTime))}</span>`;
    }

    const winA = a.outcome === 'win', winB = b.outcome === 'win';
    const clA  = isDone ? (winA ? 'winner' : winB ? 'loser' : '') : '';
    const clB  = isDone ? (winB ? 'winner' : winA ? 'loser' : '') : '';
    const middle = (isLive || isDone)
        ? `<span class="jvcm-match-score">${a.wins ?? 0} – ${b.wins ?? 0}</span>`
        : `<span class="jvcm-match-vs">vs</span>`;

    // Streams
    const streams = getStreamsForMatch(m);
    let streamsHtml = '';
    if (streams.length === 1) {
        streamsHtml = `<div class="jvcm-match-streams"><button class="jvcm-stream-btn" data-channel="${_matchEsc(streams[0])}">▶ ${_matchEsc(streams[0])}</button></div>`;
    } else if (streams.length > 1) {
        const opts = streams.map(s => `<option value="${_matchEsc(s)}">${_matchEsc(s)}</option>`).join('');
        streamsHtml = `<div class="jvcm-match-streams"><select class="jvcm-stream-select" data-stream-select><option value="">▶ Regarder sur Twitch</option>${opts}</select></div>`;
    }

    // Dots games (LIVE + terminés)
    let gamesHtml = '';
    if ((isLive || isDone) && m.games?.length) {
        let blueWins = 0, redWins = 0;
        const dots = m.games.map(g => {
            const st        = (g.state || '').toLowerCase();
            const completed = st === 'completed' || st === 'finished';
            const live      = st === 'inprogress' || st === 'in_progress';
            let dotClass = 'jvcm-game-dot';
            if (live) dotClass += ' live';
            if (completed) {
                dotClass += ' clickable';
                if (g.teams?.[0]?.result?.gameWins > blueWins)     { dotClass += ' win-blue'; blueWins = g.teams[0].result.gameWins; }
                else if (g.teams?.[1]?.result?.gameWins > redWins) { dotClass += ' win-red';  redWins  = g.teams[1].result.gameWins; }
            }
            const label = live ? '▶' : `G${g.number}`;
            return `<span class="${dotClass}" data-game-num="${g.number}" title="Game ${g.number}">${label}</span>`;
        }).join('');
        gamesHtml = `<div class="jvcm-match-games">${dots}</div>`;
    }

    // Lien lolesports
    const leagueSlug = m.league?.slug || '';
    const lolLink = leagueSlug
        ? `<a href="https://lolesports.com/schedule?leagues=${_matchEsc(leagueSlug)}" target="_blank" style="font-size:0.62rem;color:var(--jvc-text-muted);text-decoration:none;margin-left:auto;flex-shrink:0;padding:0.1rem 0.3rem;border:1px solid var(--jvc-border);border-radius:3px;">lolesports ↗</a>`
        : '';

    return `
        <div class="jvcm-match-row" data-state="${m.state}" data-match-id="${_matchEsc(m.matchId)}">
            <div class="jvcm-match-row-head">
                ${badge}
                <span class="jvcm-match-stage">${stageParts}</span>
                ${lolLink}
            </div>
            <div class="jvcm-match-row-body">
                <span class="jvcm-match-team ${clA}">${_matchEsc(a.code || a.name || 'TBD')}</span>
                ${middle}
                <span class="jvcm-match-team ${clB}">${_matchEsc(b.code || b.name || 'TBD')}</span>
            </div>
            ${gamesHtml}
            ${streamsHtml}
        </div>
    `;
}

// HTML partagé de la modale matchs — identique partout
function _matchesModalHTML() {
    return `
        <div id="jvcm-matches-overlay" class="hidden">
            <div id="jvcm-matches-panel">
                <div id="jvcm-matches-handle"></div>
                <div id="jvcm-matches-header">
                    <span id="jvcm-matches-title">🏆 Matchs esport</span>
                    <button id="jvcm-matches-refresh" title="Actualiser">⟳</button>
                    <button id="jvcm-matches-close">×</button>
                </div>
                <div id="jvcm-leagues-wrap"></div>
                <div id="jvcm-time-range">
                    <div class="jvcm-range-group">
                        <div class="jvcm-range-label">Passé <span id="jvcm-past-val">1j</span></div>
                        <input type="range" class="jvcm-range-input" id="jvcm-past-range" min="0" max="2" value="1" step="1">
                    </div>
                    <div class="jvcm-range-group">
                        <div class="jvcm-range-label">Futur <span id="jvcm-future-val">7j</span></div>
                        <input type="range" class="jvcm-range-input" id="jvcm-future-range" min="0" max="7" value="7" step="1">
                    </div>
                </div>
                <div id="jvcm-matches-loading" class="hidden">
                    <div class="jvcm-spinner"></div>
                    <span>Chargement des matchs…</span>
                </div>
                <div id="jvcm-matches-empty" class="hidden">Aucun match trouvé pour les ligues sélectionnées.</div>
                <div id="jvcm-matches-list"></div>
            </div>
        </div>`;
}

// ─────────────────────────────────────────────────────────────
// MATCHES UI — logique partagée entre JVChat et standalone
// ─────────────────────────────────────────────────────────────
function setupMatchesUI() {
    const overlay     = document.getElementById('jvcm-matches-overlay');
    const list        = document.getElementById('jvcm-matches-list');
    const loading     = document.getElementById('jvcm-matches-loading');
    const empty       = document.getElementById('jvcm-matches-empty');
    const leaguesWrap = document.getElementById('jvcm-leagues-wrap');
    const refreshBtn  = document.getElementById('jvcm-matches-refresh');

    loading.style.display = 'none';
    empty.style.display   = 'none';
    list.style.display    = 'block';

    const showLoading = () => { loading.style.display='flex'; empty.style.display='none'; list.style.display='none'; };
    const showEmpty   = (msg) => { loading.style.display='none'; empty.style.display='block'; list.style.display='none'; if (msg) empty.textContent = msg; };
    const showList    = () => { loading.style.display='none'; empty.style.display='none'; list.style.display='block'; };

    let selected = getSelectedLeagues();

    const renderLeagueBtns = () => {
        leaguesWrap.innerHTML = '';
        const groups = [...new Set(ALL_LEAGUES.map(l => l.group))];
        for (const group of groups) {
            const label = document.createElement('span');
            label.style.cssText = 'font-size:0.58rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--jvc-text-muted);width:100%;margin-top:0.3rem;font-family:Inter,system-ui,sans-serif;';
            label.textContent = group;
            leaguesWrap.appendChild(label);
            for (const league of ALL_LEAGUES.filter(l => l.group === group)) {
                const btn = document.createElement('button');
                btn.className = 'jvcm-league-btn' + (selected.includes(league.slug) ? ' active' : '');
                btn.textContent = league.label;
                btn.addEventListener('click', () => {
                    if (selected.includes(league.slug)) {
                        if (selected.length === 1) return;
                        selected = selected.filter(s => s !== league.slug);
                    } else {
                        selected = [...selected, league.slug];
                    }
                    setSelectedLeagues(selected);
                    renderLeagueBtns();
                    loadMatches();
                });
                leaguesWrap.appendChild(btn);
            }
        }
    };

    let _lastSelectedGame = {};
    let _pastDays   = 1;
    let _futureDays = 7;

    // Sliders fenêtre temporelle
    const pastRange   = document.getElementById('jvcm-past-range');
    const futureRange = document.getElementById('jvcm-future-range');
    const pastVal     = document.getElementById('jvcm-past-val');
    const futureVal   = document.getElementById('jvcm-future-val');

    const onRangeChange = () => {
        _pastDays   = parseInt(pastRange?.value   || '1');
        _futureDays = parseInt(futureRange?.value || '7');
        if (pastVal)   pastVal.textContent   = _pastDays   === 0 ? 'auj.' : `${_pastDays}j`;
        if (futureVal) futureVal.textContent = _futureDays === 0 ? 'auj.' : `${_futureDays}j`;
        // Invalider le cache pour forcer un rechargement avec les nouveaux paramètres
        const cacheKey = `jvcm:matches:${[...selected].sort().join(',')}_p${_pastDays}_f${_futureDays}`;
        try { localStorage.removeItem(cacheKey); } catch {}
        loadMatches();
    };

    let rangeTimer = null;
    const debouncedChange = () => { clearTimeout(rangeTimer); rangeTimer = setTimeout(onRangeChange, 400); };
    pastRange?.addEventListener('input', () => {
        _pastDays = parseInt(pastRange.value, 10) || 1;
        if (pastVal) pastVal.textContent = _pastDays === 0 ? 'auj.' : `${_pastDays}j`;
        debouncedChange();
    });
    futureRange?.addEventListener('input', () => {
        _futureDays = parseInt(futureRange.value, 10) || 7;
        if (futureVal) futureVal.textContent = _futureDays === 0 ? 'auj.' : `${_futureDays}j`;
        debouncedChange();
    }); // matchId → gameNum mémorisé

    const _renderSection = (label, matchesArr) => {
        if (!matchesArr.length) return '';
        const header = `<div style="padding:0.3rem 1rem;font-size:0.58rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--jvc-text-muted);background:var(--jvc-surface-2);border-bottom:1px solid var(--jvc-divider);">${label}</div>`;
        const html = matchesArr.map(m => {
            const div = document.createElement('div');
            div.innerHTML = renderMatchRow(m);
            return div.firstElementChild?.outerHTML || '';
        }).join('');
        return header + html;
    };

    const loadMatches = async () => {
        list.innerHTML = '';
        showLoading();
        refreshBtn.classList.add('spinning');
        try {
            const matches = await fetchMatches(selected, _pastDays, _futureDays);
            const now     = Date.now();
            const agoMs   = now - _pastDays   * 24 * 3600_000;
            const futMs   = now + _futureDays * 24 * 3600_000;
            const today   = new Date(); today.setHours(0,0,0,0);
            const tomorrow = new Date(today.getTime() + 86_400_000);
            const nextWeek = new Date(today.getTime() + 7 * 86_400_000);

            const live      = matches.filter(m => m.state === 'inProgress');
            const todayUp   = matches.filter(m => m.state === 'unstarted' && new Date(m.startTime) >= today    && new Date(m.startTime) < tomorrow);
            const weekUp    = matches.filter(m => m.state === 'unstarted' && new Date(m.startTime) >= tomorrow && new Date(m.startTime) < nextWeek);
            const completed = matches.filter(m => m.state === 'completed'  && new Date(m.startTime).getTime() >= agoMs).slice(0, 20);

            // Mettre à jour le badge 🏆 dans le header JVChat
            const matchesBtn = document.getElementById('jvcm-matches-btn');
            if (matchesBtn) {
                const existing = matchesBtn.querySelector('.jvcm-live-badge');
                if (live.length > 0) {
                    if (!existing) {
                        const badge = document.createElement('span');
                        badge.className = 'jvcm-live-badge';
                        badge.textContent = live.length;
                        matchesBtn.appendChild(badge);
                    } else {
                        existing.textContent = live.length;
                    }
                } else if (existing) {
                    existing.remove();
                }
            }

            refreshBtn.classList.remove('spinning');

            const total = live.length + todayUp.length + weekUp.length + completed.length;
            if (!total) { showEmpty('Aucun match trouvé pour les ligues sélectionnées.'); return; }

            // Injecter par sections
            let html = '';
            if (live.length)      html += _renderSection('🔴 En direct',       live);
            if (todayUp.length)   html += _renderSection("📅 Aujourd'hui",      todayUp);
            if (weekUp.length)    html += _renderSection('📆 Cette semaine',    weekUp);
            if (completed.length) html += _renderSection('✅ Récemment terminés', completed);
            list.innerHTML = html;

            // Brancher streams
            for (const btn of list.querySelectorAll('.jvcm-stream-btn[data-channel]')) {
                btn.addEventListener('click', () => openTwitch(btn.dataset.channel));
            }
            for (const sel of list.querySelectorAll('.jvcm-stream-select[data-stream-select]')) {
                sel.addEventListener('change', () => { if (sel.value) { openTwitch(sel.value); sel.value = ''; } });
            }
            showList();


            // Dots async pour les matchs terminés sans games dans le payload
            for (const row of list.querySelectorAll('.jvcm-match-row[data-state="completed"]')) {
                const matchId = row.dataset.matchId;
                if (!matchId) continue;
                // Si les dots sont déjà présents (payload avait m.games), skip
                if (row.querySelector('.jvcm-match-games')) continue;

                // Charger les games via getEventDetails
                gmFetch(`${LOLESPORTS_API}/getEventDetails?hl=fr-FR&id=${matchId}`, { 'x-api-key': LOLESPORTS_KEY })
                    .then(details => {
                        const games = details?.data?.event?.match?.games || [];
                        if (!games.length) return;
                        const teams = details?.data?.event?.match?.teams || [];
                        const t1Wins = teams[0]?.result?.gameWins ?? 0;
                        const t2Wins = teams[1]?.result?.gameWins ?? 0;

                        let blueW = 0, redW = 0;
                        const dots = games.map(g => {
                            const st = (g.state || '').toLowerCase();
                            const completed = st === 'completed' || st === 'finished';
                            let cls = 'jvcm-game-dot' + (completed ? ' clickable' : '');
                            if (completed) {
                                // Déduire le gagnant game par game depuis les wins cumulés
                                const newBlue = teams[0]?.result?.gameWins ?? 0;
                                const newRed  = teams[1]?.result?.gameWins ?? 0;
                                if (g.number <= t1Wins)      cls += ' win-blue';
                                else if (g.number <= t1Wins + t2Wins) cls += ' win-red';
                            }
                            return `<span class="${cls}" data-game-num="${g.number}" title="Game ${g.number}">G${g.number}</span>`;
                        }).join('');

                        const gamesDiv = document.createElement('div');
                        gamesDiv.className = 'jvcm-match-games';
                        gamesDiv.innerHTML = dots;

                        // Insérer avant le bloc streams
                        const streamsEl = row.querySelector('.jvcm-match-streams');
                        if (streamsEl) row.insertBefore(gamesDiv, streamsEl);
                        else row.appendChild(gamesDiv);

                        // Brancher les dots
                        _attachDotHandlers(row, matchId);
                    }).catch(() => {});
            }



            // Refresh auto toutes les 30s si des matchs LIVE
        } catch (e) {
            refreshBtn.classList.remove('spinning');
            showEmpty('Erreur de chargement. Réessayez.');
            err('fetchMatches:', e);
        }
    };

    const open  = () => {
        overlay.style.display = 'flex';
        overlay.classList.remove('hidden');
        renderLeagueBtns();
        loadMatches();
    };
    const close = () => {
        overlay.style.display = 'none';
        overlay.classList.add('hidden');

    };

    document.getElementById('jvcm-matches-close').addEventListener('click', close);
    overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });

    refreshBtn.addEventListener('click', () => {
        if (refreshBtn.classList.contains('spinning')) return;
        const cacheKey = `jvcm:matches:${[...selected].sort().join(',')}_p${_pastDays}_f${_futureDays}`;
        try { localStorage.removeItem(cacheKey); } catch {}
        loadMatches();
    });

    // Swipe bas pour fermer
    const handle = document.getElementById('jvcm-matches-handle');
    if (handle) {
        let startY = 0;
        handle.addEventListener('touchstart', (e) => { startY = e.touches[0].clientY; }, { passive: true });
        handle.addEventListener('touchend',   (e) => { if (e.changedTouches[0].clientY - startY > 60) close(); }, { passive: true });
    }

    return { open, close };
}
// ─────────────────────────────────────────────────────────────
class JVChatMobile {
    constructor() {
        this.messages        = [];
        this.lastPage        = 1;
        this.payload         = null;
        this.connectedUser   = null;
        this.autoScroll      = true;
        this.unreadOnScroll  = 0;
        this.unreadTotal     = 0;
        this.refreshRate     = 6000;
        this.isTabActive     = true;
        this._cycle          = 0;
        this._retryDelay     = 6000;
        this._baseTitle      = '';
        this._newSepInserted = false;
        this._relTimerID     = null;
        this.settings        = new Settings();
        // Confirmation suppression
        this._pendingDelete  = null;
    }

    async start() {
        addStyle();
        const info = this._parseURL(location.href);
        this.viewId = info.viewId; this.forumId = info.forumId;
        this.topicId = info.topicId; this.topicTitle = info.title;
        this.api = new JVCAPI(this.viewId, this.forumId, this.topicId, this.topicTitle);

        this._buildUI();
        await this._loadInitial();

        let lastRefresh = performance.now();
        this._resetPollTimer = () => { lastRefresh = 0; }; // remet le timer à 0 pour forcer un poll immédiat
        setInterval(() => {
            if (performance.now() - lastRefresh >= this.refreshRate) {
                lastRefresh = performance.now();
                this._poll();
            }
        }, 50);

        this._relTimerID = setInterval(() => this._updateRelativeTimes(), 60_000);

        document.addEventListener('visibilitychange', () => {
            this.isTabActive = document.visibilityState === 'visible';
            if (this.isTabActive) {
                this.unreadTotal = 0;
                updateFavicon(0);
                if (this._baseTitle) document.title = this._baseTitle;
                this.autoScroll = true;
                this.unreadOnScroll = 0;
                this._newSepInserted = false;
                    this._scrollToBottom();
            }
        });

        if (window.visualViewport) {
            window.visualViewport.addEventListener('resize', () => this._onViewportResize());
        }
    }

    _parseURL(url) {
        const m = url.match(/^https:\/\/www\.jeuxvideo\.com\/(?:recherche\/)?forums\/(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(.*?)\.htm/);
        if (!m) throw new Error('URL invalide');
        return { viewId: +m[1], forumId: +m[2], topicId: +m[3], title: m[8] };
    }

    _buildUI() {
        for (const el of document.body.children) el.style.display = 'none';
        document.documentElement.style.overflow = 'hidden';
        document.body.style.cssText = 'margin:0;padding:0;overflow:hidden;background:var(--jvc-bg,#18181b)';

        document.body.insertAdjacentHTML('beforeend', `
            <div id="jvcm-app">
                <header id="jvcm-header">
                    <button id="jvcm-home-btn" title="Retour à la liste des topics">⌂</button>
                    <span id="jvcm-topic-title" title="Fermer JVChat">Chargement…</span>
                    <div id="jvcm-meta">
                        <span class="jvcm-chip jvcm-chip--live" id="jvcm-connected">–</span>
                    </div>
                    <button id="jvcm-matches-btn" title="Matchs esport">🏆</button>
                    <button id="jvcm-settings-btn" title="Paramètres">⚙</button>
                </header>

                <div id="jvcm-loading">
                    <div class="jvcm-spinner"></div>
                    <span class="jvcm-loading-label">Chargement des messages…</span>
                </div>

                <div id="jvcm-messages"></div>


                <div id="jvcm-input-wrap">
                    <div id="jvcm-input-bar">
                        <textarea id="jvcm-textarea" rows="1" placeholder="Votre message…"></textarea>
                        <button id="jvcm-clear-btn" aria-label="Vider le message" title="Vider">×</button>
                        <button id="jvcm-send-btn" aria-label="Envoyer">
                            <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
                        </button>
                    </div>
                </div>
            </div>

            <!-- Modale matchs esport -->
            ${_matchesModalHTML()}

            <div id="jvcm-settings">
                <div id="jvcm-settings-header">
                    ⚙ Paramètres
                    <button id="jvcm-settings-close">×</button>
                </div>
                <div id="jvcm-settings-body"></div>
            </div>

            <!-- Modal générique (report + confirm delete) -->
            <div id="jvcm-modal-overlay" class="jvcm-modal-overlay hidden">
                <div class="jvcm-modal" id="jvcm-modal">
                    <div class="jvcm-modal-header">
                        <span id="jvcm-modal-title"></span>
                        <button class="jvcm-modal-close" id="jvcm-modal-close">×</button>
                    </div>
                    <div class="jvcm-modal-body" id="jvcm-modal-body"></div>
                    <div class="jvcm-modal-footer">
                        <button class="jvcm-modal-cancel" id="jvcm-modal-cancel">Annuler</button>
                        <button class="jvcm-modal-confirm" id="jvcm-modal-confirm">Confirmer</button>
                    </div>
                </div>
            </div>
        `);

        this.$messages  = document.getElementById('jvcm-messages');
        this.$textarea  = document.getElementById('jvcm-textarea');
        this.$sendBtn   = document.getElementById('jvcm-send-btn');
        this.$loading   = document.getElementById('jvcm-loading');


        // Bouton home → retour liste des topics
        const forumLink = document.querySelector('.spreadContainer nav.breadcrumb > a[href^="/forums/0-"]')
                       || document.querySelector('nav.breadcrumb a[href*="/forums/0-"]')
                       || document.querySelector('a[href*="/forums/0-19163"]')
                       || document.querySelector('a[href*="/forums/0-42"]');
        const forumUrl  = forumLink?.href || null;
        const homeBtn   = document.getElementById('jvcm-home-btn');
        if (forumUrl) {
            homeBtn.addEventListener('click', () => { window.location.href = forumUrl; });
        } else {
            homeBtn.style.display = 'none';
        }

        // Fermer JVChat
        document.getElementById('jvcm-topic-title').addEventListener('click', () => {
            clearInterval(this._relTimerID);
            document.getElementById('jvcm-app')?.remove();
            document.getElementById('jvcm-settings')?.remove();
            document.getElementById('jvcm-modal-overlay')?.remove();
            document.documentElement.style.overflow = '';
            document.body.style.cssText = '';
            for (const el of document.body.children) el.style.display = '';
            location.reload();
        });

        // Fermer actions si clic ailleurs
        document.addEventListener('click', (e) => {
            if (!e.target.closest('.jvcm-message')) {
                for (const m of document.querySelectorAll('.jvcm-message.is-active')) m.classList.remove('is-active');
            }
        });

        // Textarea + compteur de caractères
        const clearBtn = document.getElementById('jvcm-clear-btn');
        this.$clearBtn = clearBtn;
        clearBtn.addEventListener('click', () => {
            this.$textarea.value = '';
            this.$textarea.style.height = '';
            clearBtn.classList.remove('visible');
            this.$textarea.focus();
        });

        this.$textarea.addEventListener('input', () => {
            this.$textarea.style.height = 'auto';
            this.$textarea.style.height = Math.min(this.$textarea.scrollHeight, 120) + 'px';
            clearBtn.classList.toggle('visible', this.$textarea.value.length > 0);
        });
        this.$sendBtn.addEventListener('click', () => this._send());
        this.$textarea.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey && this.settings.get('send_on_enter')) {
                e.preventDefault(); this._send();
            }
        });

        // Scroll
        this.$messages.addEventListener('scroll', () => {
            const atBottom = this.$messages.scrollHeight - this.$messages.scrollTop <= this.$messages.clientHeight + 2;
            this.autoScroll = atBottom;
            if (atBottom) {
                this.unreadOnScroll = 0;
                    // Retirer le séparateur "nouveaux messages" quand on arrive en bas
                document.getElementById('jvcm-new-sep')?.remove();
                this._newSepInserted = false;
            }
        }, { passive: true });

        // Settings panel + swipe bas pour fermer
        this._buildSettings();
        document.getElementById('jvcm-settings-btn').addEventListener('click', () => {
            document.getElementById('jvcm-settings').classList.add('open');
        });
        document.getElementById('jvcm-settings-close').addEventListener('click', () => {
            document.getElementById('jvcm-settings').classList.remove('open');
        });
        this._setupSettingsSwipe();

        // Modale matchs
        this._setupMatchesModal();

        // Modal générique
        this._setupModal();
    }

    _setupMatchesModal() {
        const { open } = setupMatchesUI();
        document.getElementById('jvcm-matches-btn').addEventListener('click', open);
    }

    _setupSettingsSwipe() {
        const panel = document.getElementById('jvcm-settings');
        const header = document.getElementById('jvcm-settings-header');
        let startY = 0, dragging = false;

        header.addEventListener('touchstart', (e) => {
            startY = e.touches[0].clientY;
            dragging = true;
            panel.style.transition = 'none';
        }, { passive: true });

        header.addEventListener('touchmove', (e) => {
            if (!dragging) return;
            const dy = e.touches[0].clientY - startY;
            if (dy > 0) panel.style.transform = `translateY(${dy}px)`;
        }, { passive: true });

        header.addEventListener('touchend', (e) => {
            if (!dragging) return;
            dragging = false;
            panel.style.transition = '';
            const dy = e.changedTouches[0].clientY - startY;
            if (dy > 80) {
                panel.classList.remove('open');
                panel.style.transform = '';
            } else {
                panel.style.transform = '';
            }
        });
    }

    _setupModal() {
        const overlay = document.getElementById('jvcm-modal-overlay');
        const close = () => {
            overlay.classList.add('hidden');
            this._pendingDelete = null;
            document.getElementById('jvcm-modal-confirm').onclick = null;
        };
        document.getElementById('jvcm-modal-close').addEventListener('click', close);
        document.getElementById('jvcm-modal-cancel').addEventListener('click', close);
        overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
    }

    _showModal({ title, body, confirmLabel, confirmClass = '', onConfirm }) {
        document.getElementById('jvcm-modal-title').textContent = title;
        document.getElementById('jvcm-modal-body').innerHTML = body;
        const btn = document.getElementById('jvcm-modal-confirm');
        btn.textContent = confirmLabel;
        btn.className = 'jvcm-modal-confirm ' + confirmClass;
        btn.onclick = () => {
            document.getElementById('jvcm-modal-overlay').classList.add('hidden');
            onConfirm();
        };
        document.getElementById('jvcm-modal-overlay').classList.remove('hidden');
    }

    _buildSettings() {
        const body = document.getElementById('jvcm-settings-body');
        for (const def of this.settings.defs) {
            const id = `jvcm-s-${def.key}`;
            const checked = this.settings.get(def.key) ? 'checked' : '';
            body.insertAdjacentHTML('beforeend', `
                <div class="jvcm-setting-row">
                    <div>
                        <div class="jvcm-setting-label">${def.label}</div>
                        ${def.desc ? `<div class="jvcm-setting-desc">${def.desc}</div>` : ''}
                    </div>
                    <label class="jvcm-toggle">
                        <input type="checkbox" id="${id}" ${checked}>
                        <span class="jvcm-toggle-track"></span>
                    </label>
                </div>
            `);
            document.getElementById(id).addEventListener('change', async (e) => {
                const val = e.target.checked;
                if (def.key === 'notifications' && val) {
                    const ok = await requestNotifPermission();
                    if (!ok) { e.target.checked = false; this._toast('Permission refusée', 'error'); return; }
                }
                this.settings.set(def.key, val);
            });
        }
    }

    _onViewportResize() {
        const app = document.getElementById('jvcm-app');
        if (!app) return;
        app.style.height = window.visualViewport.height + 'px';
        if (this.autoScroll) this._scrollToBottom();
    }

    // ── Chargement initial ──
    async _loadInitial() {
        this.$loading.classList.remove('hidden');
        try {
            const currentPage = parsePage(document);
            this.lastPage = currentPage.lastPage;

            const lastDoc = await this.api.getPageDocument(this.lastPage);
            const lastParsed = parsePage(lastDoc);

            const loadPages = async (finalPage, finalDoc, finalParsed) => {
                this.payload = getPayload(finalDoc);
                this.api.payload = this.payload;
                this.connectedUser = this._getConnectedUser(finalDoc);
                this._updateHeader(finalParsed);
                if (finalPage >= 2) {
                    const prevDoc = await this.api.getPageDocument(finalPage - 1);
                    for (const m of parsePage(prevDoc).messages) this._addMessage(m, false);
                }
                for (const m of finalParsed.messages) this._addMessage(m, false);
            };

            if (lastParsed.lastPage > this.lastPage) {
                this.lastPage = lastParsed.lastPage;
                const realDoc = await this.api.getPageDocument(this.lastPage);
                await loadPages(this.lastPage, realDoc, parsePage(realDoc));
            } else {
                await loadPages(this.lastPage, lastDoc, lastParsed);
            }

            this._recheckMentions();
            this._baseTitle = document.title;
            this.$loading.classList.add('hidden');
            this._scrollToBottom(true);
        } catch (e) {
            err('loadInitial:', e);
            this.$loading.classList.add('hidden');
            this._toast('Erreur de chargement — nouvelle tentative dans 10s', 'error');
            setTimeout(() => this._loadInitial(), 10000);
        }
    }

    // ── Polling ──
    async _poll() {
        const c = (this._cycle = (this._cycle || 0) + 1);
        try {
            const doc = await this.api.getPageDocument(this.lastPage);
            const page = parsePage(doc);
            this.payload = getPayload(doc);
            this.api.payload = this.payload;
            this.lastPage = page.lastPage;
            this.connectedUser = this._getConnectedUser(doc);
            this._updateHeader(page);

            for (const m of page.messages) {
                if (!this.messages.find(x => x.id === m.id)) this._addMessage(m, true);
            }

            if (page.messages.length >= 20) {
                try {
                    const nextDoc = await this.api.getPageDocument(this.lastPage + 1);
                    const next = parsePage(nextDoc);
                    if (next.lastPage > this.lastPage) {
                        this.lastPage = next.lastPage;
                        for (const m of next.messages) {
                            if (!this.messages.find(x => x.id === m.id)) this._addMessage(m, true);
                        }
                    }
                } catch {}
            }

            const recent = this.messages.filter(m => m.page >= this.lastPage - 1).slice(-20);
            for (const m of recent) {
                if (m.isDeleted) continue;
                if (!page.messages.find(x => x.id === m.id)) {
                    const raw = await this.api.getMessage(m.id);
                    if (!raw) { m.element?.classList.add('is-deleted'); m.isDeleted = true; }
                }
            }

            if (this.autoScroll) this._scrollToBottom();
            if (this._retryDelay > 6000) { this._retryDelay = 6000; this.refreshRate = 6000; }
        } catch (e) {
            err(`Cycle #${c}:`, e);
            this._retryDelay = Math.min(this._retryDelay * 2, 48000);
            this.refreshRate = this._retryDelay;
            this._toast(`Reconnexion dans ${this._retryDelay / 1000}s…`, 'info');
            setTimeout(() => { this.refreshRate = 6000; }, this._retryDelay + 500);
        }
    }

    _updateHeader(page) {
        const t = document.getElementById('jvcm-topic-title');
        const c = document.getElementById('jvcm-connected');
        if (t) t.textContent = page.title || '';
        if (c) {
            const n = this.payload?.forumInfo?.header?.btnVal ?? page.connectedCount;
            c.textContent = `${n} connecté${n !== 1 ? 's' : ''}`;
        }
    }

    _getConnectedUser(doc) {
        if (!doc.querySelector('.headerAccount__pm')) return null;
        const username = doc.querySelector('.headerAccount__pseudo')?.textContent.trim();
        const avatarUrl = doc.querySelector('.headerAccount__avatar')?.style.backgroundImage
            .slice(5, -2).replace('/avatar-md/', '/avatar/');
        return { username, avatarUrl };
    }

    // ── Ajout d'un message ──
    _addMessage(message, isNew) {
        if (this.messages.find(m => m.id === message.id)) return;

        // Séparateur "nouveaux messages"
        if (isNew && !this._newSepInserted && !this.autoScroll) {
            this._newSepInserted = true;
            this._pendingNewCount = 1;
            const sep = document.createElement('button');
            sep.className = 'jvcm-new-sep'; sep.id = 'jvcm-new-sep';
            sep.textContent = '↓ Nouveaux messages';
            sep.addEventListener('click', () => {
                this._scrollToBottom();
                this.autoScroll = true;
                sep.remove();
                this._newSepInserted = false;
            });
            // Insérer entre $messages et la zone de saisie, pas dans le scroll
            const inputWrap = document.getElementById('jvcm-input-wrap');
            if (inputWrap) inputWrap.insertAdjacentElement('beforebegin', sep);
            else this.$messages.appendChild(sep);
        } else if (isNew && this._newSepInserted) {
            this._pendingNewCount = (this._pendingNewCount || 1) + 1;
            const sep = document.getElementById('jvcm-new-sep');
            if (sep) sep.textContent = `${this._pendingNewCount} nouveaux messages`;
        }

        if (isNew && !this.autoScroll) { this.unreadOnScroll++; this._updateScrollBtn(); }

        if (isNew && !this.isTabActive) {
            this.unreadTotal++;
            updateFavicon(this.unreadTotal);
            document.title = `(${this.unreadTotal}) ${this._baseTitle || document.title.replace(/^\(\d+\)\s*/, '')}`;
            if (this.settings.get('notifications')) {
                sendNotif(`${message.username} — JVChat`, message.content.replace(/<[^>]+>/g, '').slice(0, 80));
            }
        }

        const prev = this.messages[this.messages.length - 1];
        const grouped = prev && prev.username === message.username;
        const hue = pseudoHue(message.username);
        const ts = parseJVCDate(message.creationDate);
        const relTxt = ts ? relativeTime(ts) : message.creationDate.slice(-8);
        const absTxt = message.creationDate.slice(-8);
        const isOwn = this.connectedUser && this.connectedUser.username === message.username;

        // Avatar : afficher l'URL originale immédiatement, remplacer par le blob caché en arrière-plan
        const avatarSrc = _avatarCache.get(message.avatarUrl) || message.avatarUrl;

        const el = document.createElement('article');
        el.className = 'jvcm-message' + (grouped ? ' is-grouped' : '');
        el.dataset.id = message.id;
        if (ts) el.dataset.ts = ts;

        el.innerHTML = `
            <span class="jvcm-swipe-hint">💬</span>
            <img class="jvcm-avatar" src="${avatarSrc}" alt="" loading="lazy">
            <div class="jvcm-msg-body">
                <div class="jvcm-msg-header">
                    <span class="jvcm-username" style="--pseudo-h:${hue}">${message.username}</span>
                        ${message.userRank ? `<span class="jvcm-userrank">${message.userRank}</span>` : ''}
                    <span class="jvcm-date" title="${message.creationDate}">
                        <span class="jvcm-date-rel">${relTxt}</span>
                        <span class="jvcm-date-abs">${absTxt}</span>
                    </span>
                    <div class="jvcm-actions">
                        <div class="jvcm-actions-row">
                            <button class="jvcm-action-btn jvcm-btn-quote">💬 Citer</button>
                            <button class="jvcm-action-btn jvcm-btn-copy">🔗</button>
                        </div>
                        <div class="jvcm-actions-row">
                            ${isOwn ? `<button class="jvcm-action-btn jvcm-btn-edit">✏️ Éditer</button>` : ''}
                            ${isOwn ? `<button class="jvcm-action-btn danger jvcm-btn-delete">🗑️</button>` : ''}
                            ${!isOwn ? `<button class="jvcm-action-btn danger jvcm-btn-report">🚩</button>` : ''}
                        </div>
                    </div>
                </div>
                <div class="jvcm-content txt-msg text-enrichi-forum">${message.content}</div>
            </div>
        `;

        // Toggle date abs/rel
        el.querySelector('.jvcm-date')?.addEventListener('click', (e) => {
            e.stopPropagation();
            el.querySelector('.jvcm-date').classList.toggle('show-abs');
        });

        // Fix JvCare
        for (const jvc of el.querySelectorAll('.JvCare')) {
            const a = document.createElement('a');
            a.href = jvCake(jvc.getAttribute('class')); a.target = '_blank';
            a.innerHTML = jvc.innerHTML; jvc.replaceWith(a);
        }

        // Fix img-shack
        for (const img of el.querySelectorAll('.img-shack')) {
            const parent = img.parentElement;
            const a = document.createElement('a');
            a.href = jvCake(parent.getAttribute('class')); a.target = '_blank';
            a.appendChild(img.cloneNode(true)); parent.replaceWith(a);
        }

        // Citations imbriquées
        for (const bq of el.querySelectorAll('.text-enrichi-forum > blockquote > blockquote')) {
            const btn = document.createElement('button');
            btn.className = 'jvcm-collapse-btn'; btn.textContent = '⟲ Voir la citation';
            bq.prepend(btn);
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                btn.closest('.message__blockquote, blockquote').classList.toggle('is-open');
            });
        }

        // Scroll vers citation
        for (const bq of el.querySelectorAll('.jvcm-content > blockquote, .jvcm-content > .message__blockquote')) {
            bq.addEventListener('click', (e) => {
                if (e.target.closest('.jvcm-collapse-btn')) return;
                e.stopPropagation();
                this._scrollToQuoteOrigin(bq);
            });
        }

        this._checkMention(el, message);

        // Tap → toggle actions
        el.addEventListener('click', (e) => {
            if (e.target.closest('a, button, input, textarea, .jvcm-date, blockquote')) return;
            if (window.getSelection?.().toString().length > 0) return;
            const wasActive = el.classList.contains('is-active');
            for (const other of document.querySelectorAll('.jvcm-message.is-active')) other.classList.remove('is-active');
            if (!wasActive) el.classList.add('is-active');
        });

        attachLongPress(el, () => {
            el.classList.add('is-pressing');
            setTimeout(() => el.classList.remove('is-pressing'), 300);
            if (!el.classList.contains('is-active')) {
                for (const other of document.querySelectorAll('.jvcm-message.is-active')) other.classList.remove('is-active');
                el.classList.add('is-active');
            }
        });

        if (this.settings.get('swipe_to_quote')) this._attachSwipeQuote(el, message);

        // Bouton copier lien du message
        el.querySelector('.jvcm-btn-copy')?.addEventListener('click', (e) => {
            e.stopPropagation();
            el.classList.remove('is-active');
            const url = `https://www.jeuxvideo.com/forums/message/${message.id}`;
            const doCopy = () => {
                if (navigator.clipboard?.writeText) {
                    return navigator.clipboard.writeText(url);
                }
                // Fallback iOS Safari / anciens navigateurs
                const ta = document.createElement('textarea');
                ta.value = url;
                ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;';
                document.body.appendChild(ta);
                ta.focus(); ta.select();
                try { document.execCommand('copy'); } catch {}
                ta.remove();
                return Promise.resolve();
            };
            doCopy().then(() => this._toast('Lien copié', 'success')).catch(() => this._toast(url, 'info'));
        });

        // Bouton citer
        el.querySelector('.jvcm-btn-quote')?.addEventListener('click', (e) => {
            e.stopPropagation();
            this._quoteMessage(message);
            el.classList.remove('is-active');
        });

        // Bouton éditer
        el.querySelector('.jvcm-btn-edit')?.addEventListener('click', async (e) => {
            e.stopPropagation();
            const btn = el.querySelector('.jvcm-btn-edit');
            btn.disabled = true; btn.textContent = '…';
            try {
                await this._editMessage(message);
                el.classList.remove('is-active');
            } catch {
                btn.style.display = 'none';
                this._toast("Édition non disponible", 'error');
            }
        });

        // Bouton supprimer — confirmation avant suppression
        el.querySelector('.jvcm-btn-delete')?.addEventListener('click', (e) => {
            e.stopPropagation();
            el.classList.remove('is-active');
            this._showModal({
                title: '🗑️ Supprimer le message',
                body: `Supprimer définitivement ce message de <strong>${message.username}</strong> ? Cette action est irréversible.`,
                confirmLabel: 'Supprimer',
                confirmClass: 'danger',
                onConfirm: () => this._deleteMessage(message),
            });
        });

        // Bouton signaler
        el.querySelector('.jvcm-btn-report')?.addEventListener('click', (e) => {
            e.stopPropagation();
            el.classList.remove('is-active');
            this._showModal({
                title: '🚩 Faire un signalement',
                body: `Signaler le message de <strong>${message.username}</strong> ?<br><br>
                       Le formulaire JVC (avec captcha) s'ouvrira dans un nouvel onglet.`,
                confirmLabel: 'Signaler',
                confirmClass: '',
                onConfirm: () => {
                    window.open(`https://www.jeuxvideo.com/forums/message/${message.id}`, '_blank');
                    this._toast('Cliquez sur ⚠ dans le message pour signaler', 'info');
                },
            });
        });

        this.$messages.appendChild(el);
        message.element = el;
        this.messages.push(message);

        // Animation d'entrée pour les nouveaux messages
        if (isNew) {
            requestAnimationFrame(() => el.classList.add('is-new'));
        }

        // Forcer le chargement des images lazy et des backgrounds spoil JVC
        for (const img of el.querySelectorAll('img[loading="lazy"]')) {
            img.removeAttribute('loading'); // désactiver le lazy loading natif
        }
        for (const img of el.querySelectorAll('img[data-src], img[data-original], img[data-lazy]')) {
            img.src = img.dataset.src || img.dataset.original || img.dataset.lazy || img.src;
        }
        // Images spoil : data-src-background sur <span> → transformer en <img>
        for (const span of el.querySelectorAll('[data-src-background]')) {
            const src = span.dataset.srcBackground;
            if (!src) continue;
            const img = document.createElement('img');
            img.src = src;
            img.style.cssText = 'max-width:100%;display:block;border-radius:4px;';
            span.replaceWith(img);
        }

        // Mettre en cache l'avatar en arrière-plan et remplacer si l'URL change
        if (!_avatarCache.has(message.avatarUrl)) {
            cachedAvatar(message.avatarUrl).then(cached => {
                if (cached !== message.avatarUrl) {
                    const img = el.querySelector('.jvcm-avatar');
                    if (img) img.src = cached;
                }
            }).catch(() => {}); // CORS safe - pas de fetch
        }
    }

    _scrollToQuoteOrigin(bqNode) {
        const raw = bqNode.textContent.replace(/\s+/g, ' ').trim();
        const timeStr  = raw.match(/\d{2}:\d{2}:\d{2}/)?.[0] ?? null;
        const bodyMatch = raw.match(/\d{2}:\d{2}:\d{2}\s*:\s*(.+)/);
        const fragment  = bodyMatch ? bodyMatch[1].trim().slice(0, 60).toLowerCase() : null;

        if (!fragment) { this._toast('Impossible d\'identifier le message cité', 'info'); return; }

        let bestMatch = null;
        for (const m of this.messages) {
            if (!m.element) continue;
            const msgText = (m.element.querySelector('.jvcm-content')?.textContent || '')
                .replace(/\s+/g, ' ').trim().toLowerCase();
            if (msgText.includes(fragment)) {
                if (timeStr && m.creationDate && !m.creationDate.includes(timeStr)) continue;
                bestMatch = m.element; break;
            }
        }
        if (!bestMatch) {
            for (const m of this.messages) {
                if (!m.element) continue;
                const msgText = (m.element.querySelector('.jvcm-content')?.textContent || '')
                    .replace(/\s+/g, ' ').trim().toLowerCase();
                if (msgText.includes(fragment)) { bestMatch = m.element; break; }
            }
        }

        if (!bestMatch) { this._toast('Message original introuvable dans le chat chargé', 'info'); return; }

        this.autoScroll = false;

        const current = this.$messages.scrollTop;
        const cRect   = this.$messages.getBoundingClientRect();
        const tRect   = bestMatch.getBoundingClientRect();
        const center  = current + tRect.top - cRect.top
                      - (this.$messages.clientHeight / 2)
                      + (bestMatch.offsetHeight / 2);

        this.$messages.scrollTo({ top: Math.max(0, center), behavior: 'smooth' });
        bestMatch.classList.add('quote-flash');
        setTimeout(() => bestMatch.classList.remove('quote-flash'), 900);
    }

    _checkMention(el, message) {
        if (!this.connectedUser) return;
        const bq = el.querySelector('.jvcm-content > blockquote, .jvcm-content > .message__blockquote');
        if (bq && bq.textContent.toLowerCase().includes(this.connectedUser.username.toLowerCase())) {
            el.classList.add('mentions-me');
            if (navigator.vibrate && this.settings.get('vibration')) navigator.vibrate([80, 40, 80]);
        }
    }

    _recheckMentions() {
        for (const m of this.messages) {
            if (m.element && !m.element.classList.contains('mentions-me')) this._checkMention(m.element, m);
        }
    }

    _updateRelativeTimes() {
        for (const el of this.$messages.querySelectorAll('[data-ts]')) {
            const ts = parseInt(el.dataset.ts);
            if (!ts) continue;
            const rel = el.querySelector('.jvcm-date-rel');
            if (rel) rel.textContent = relativeTime(ts);
        }
    }

    _attachSwipeQuote(el, message) {
        let startX = 0, startY = 0, swiping = false;
        const THRESHOLD = 70;

        el.addEventListener('touchstart', (e) => {
            startX = e.touches[0].clientX; startY = e.touches[0].clientY; swiping = false;
        }, { passive: true });

        el.addEventListener('touchmove', (e) => {
            const dx = e.touches[0].clientX - startX;
            const dy = Math.abs(e.touches[0].clientY - startY);
            if (!swiping && Math.abs(dx) > 10 && dy < 20) swiping = true;
            if (!swiping || dx <= 0) return;
            el.style.transform = `translateX(${Math.min(dx, THRESHOLD * 1.4) * 0.45}px)`;
            el.classList.toggle('swipe-ready', dx >= THRESHOLD);
        }, { passive: true });

        const endSwipe = () => {
            if (swiping && el.classList.contains('swipe-ready')) {
                this._quoteMessage(message);
                this._toast('Citation ajoutée', 'info');
            }
            el.style.transform = ''; el.classList.remove('swipe-ready'); swiping = false;
        };
        el.addEventListener('touchend',    endSwipe, { passive: true });
        el.addEventListener('touchcancel', endSwipe, { passive: true });
    }

    // ── Envoi optimiste ──
    async _send() {
        const text = this.$textarea.value.trim();
        if (!text || !this.connectedUser) return;

        this.$textarea.value = '';
        this.$textarea.style.height = '';
        this.$clearBtn?.classList.remove('visible');
        this.$sendBtn.disabled = true;

        try {
            await this.api.postMessage(text);
            await this._poll();
            this._resetPollTimer?.();
        } catch (e) {
            // En cas d'erreur, remettre le texte dans le textarea
            this.$textarea.value = text;
            this.$textarea.dispatchEvent(new Event('input'));
            this._toast(e.message, 'error');
        } finally {
            this.$sendBtn.disabled = false;
        }
    }

    _quoteMessage(message) {
        const contentEl = message.element?.querySelector('.jvcm-content');
        if (!contentEl) return;
        const txt = reverseMessage(contentEl, true);
        const quote = `> Le ${message.creationDate} ${message.username} a écrit :\n${txt}\n\n`;
        const ta = this.$textarea;
        const before = ta.value.slice(0, ta.selectionStart ?? ta.value.length);
        const prefix = (before.length > 0 && !before.endsWith('\n\n')) ? '\n\n' : '';
        this._insertAtCursor(prefix + quote);
        this.$textarea.focus();
    }

    _insertAtCursor(text) {
        const ta = this.$textarea;
        const start = ta.selectionStart ?? ta.value.length;
        const end   = ta.selectionEnd   ?? ta.value.length;
        ta.value = ta.value.slice(0, start) + text + ta.value.slice(end);
        ta.selectionStart = ta.selectionEnd = start + text.length;
        ta.dispatchEvent(new Event('input', { bubbles: true }));
    }

    async _deleteMessage(message) {
        try {
            await this.api.deleteMessage(message.id);
            message.element?.classList.add('is-deleted');
            message.isDeleted = true;
        } catch (e) {
            this._toast(e.message, 'error');
        }
    }

    async _editMessage(message) {
        const { jvcode, formSession } = await this.api.getEditForm(message.id);
        const contentEl = message.element?.querySelector('.jvcm-content');
        if (!contentEl) return;
        const backup = contentEl.innerHTML;
        contentEl.innerHTML = `
            <textarea class="jvcm-edit-ta" style="width:100%;min-height:80px;background:var(--jvc-surface-2);color:var(--jvc-text);border:1px solid var(--jvc-border);border-radius:6px;padding:0.4rem 0.6rem;font-family:inherit;font-size:0.85rem;resize:vertical"></textarea>
            <div style="display:flex;gap:0.4rem;margin-top:0.4rem">
                <button class="jvcm-edit-cancel" style="flex:1;padding:0.35rem;background:var(--jvc-surface-3);border:1px solid var(--jvc-border);border-radius:5px;color:var(--jvc-text-dim);font-family:inherit;font-size:0.8rem;cursor:pointer">Annuler</button>
                <button class="jvcm-edit-submit" style="flex:1;padding:0.35rem;background:var(--jvc-orange);border:none;border-radius:5px;color:#fff;font-family:inherit;font-size:0.8rem;font-weight:600;cursor:pointer">Modifier</button>
            </div>
        `;
        const ta = contentEl.querySelector('.jvcm-edit-ta');
        ta.value = jvcode; ta.focus();
        contentEl.querySelector('.jvcm-edit-cancel').addEventListener('click', () => { contentEl.innerHTML = backup; });
        contentEl.querySelector('.jvcm-edit-submit').addEventListener('click', async () => {
            try {
                contentEl.innerHTML = await this.api.updateMessage(message.id, ta.value, formSession);
                this._toast('Message modifié', 'success');
            } catch (e) {
                this._toast(e.message, 'error');
                contentEl.innerHTML = backup;
            }
        });
    }

    _scrollToBottom(force = false) {
        if (!force) {
            requestAnimationFrame(() => {
                this.$messages.scrollTo({ top: this.$messages.scrollHeight, behavior: 'smooth' });
            });
            return;
        }
        const imgs = [...this.$messages.querySelectorAll('img')].filter(i => !i.complete);
        if (imgs.length === 0) {
            requestAnimationFrame(() => this.$messages.scrollTo({ top: this.$messages.scrollHeight, behavior: 'instant' }));
            return;
        }
        let settled = false;
        const doScroll = () => {
            if (settled) return; settled = true;
            this.$messages.scrollTo({ top: this.$messages.scrollHeight, behavior: 'instant' });
        };
        let loaded = 0;
        for (const img of imgs) {
            const done = () => { if (++loaded >= imgs.length) doScroll(); };
            img.addEventListener('load', done, { once: true });
            img.addEventListener('error', done, { once: true });
        }
        setTimeout(doScroll, 1200);
    }



    _toast(msg, type = 'info') {
        const el = document.createElement('div');
        el.className = `jvcm-toast ${type}`;
        el.textContent = msg;
        document.body.appendChild(el);
        setTimeout(() => el.remove(), 4000);
    }
}

// ─────────────────────────────────────────────────────────────
// DÉTECTION FORUM LOL
// ─────────────────────────────────────────────────────────────
const LOL_KEYWORDS = /league\s*of\s*legends|\blol\b|lec|lck|lpl|lfl|esport/i;
const LOL_FORUM_IDS = new Set(['19163', '42']); // IDs des forums LoL sur JVC

function isLolContext() {
    // Priorité 1 : ID du forum dans l'URL
    const forumIdMatch = location.href.match(/\/forums\/(?:0-)?(\d+)-/);
    if (forumIdMatch && LOL_FORUM_IDS.has(forumIdMatch[1])) return true;
    // Priorité 2 : mots-clés dans le fil d'Ariane / titre / nom du forum
    const breadcrumb = document.querySelector('.spreadContainer nav.breadcrumb, .breadcrumb')?.textContent || '';
    const title      = document.title || '';
    const forumName  = document.querySelector('.spreadContainer nav.breadcrumb > a[href^="/forums/0-"]')?.textContent || '';
    return LOL_KEYWORDS.test(breadcrumb + title + forumName);
}

// ─────────────────────────────────────────────────────────────
// MODALE MATCHS STANDALONE (utilisable hors JVChat)
// ─────────────────────────────────────────────────────────────
function injectStandaloneMatchesModal() {
    if (document.getElementById('jvcm-matches-overlay')) return setupMatchesUI(); // déjà injecté, rebrancher

    // Injecter les variables CSS + styles de base si JVChat n'est pas actif
    if (!document.getElementById('jvcm-style')) {
        const style = document.createElement('style');
        style.id = 'jvcm-standalone-style';
        style.textContent = `
            @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
            :root {
                --jvc-bg:#18181b; --jvc-surface-1:#1f1f23; --jvc-surface-2:#27272a;
                --jvc-surface-3:#303035; --jvc-border:#2e2e33; --jvc-divider:#242428;
                --jvc-text:#e4e4e7; --jvc-text-dim:#a1a1aa; --jvc-text-muted:#71717a;
                --jvc-orange:#e84d1a; --jvc-green:#4ade80; --jvc-red:#f87171; --jvc-blue:#7fb4ff;
            }
            @keyframes jvcm-spin    { to { transform: rotate(360deg); } }
            @keyframes jvcm-breathe { 0%,100%{opacity:1} 50%{opacity:0.3} }
            .jvcm-spinner { width:28px;height:28px;border:3px solid var(--jvc-surface-3);border-top-color:var(--jvc-orange);border-radius:50%;animation:jvcm-spin 0.7s linear infinite; }
            /* Modale structure */
            #jvcm-matches-overlay { position:fixed;inset:0;background:rgba(0,0,0,0.75);z-index:9001;display:flex;align-items:flex-end;justify-content:center; }
            #jvcm-matches-overlay.hidden { display:none; }
            #jvcm-matches-panel { background:var(--jvc-bg);border:1px solid var(--jvc-border);border-radius:12px 12px 0 0;width:100%;max-width:520px;max-height:85dvh;display:flex;flex-direction:column;overflow:hidden;font-family:Inter,system-ui,sans-serif; }
            #jvcm-matches-handle { width:36px;height:4px;background:var(--jvc-border);border-radius:2px;margin:10px auto 0;flex-shrink:0; }
            #jvcm-matches-header { display:flex;align-items:center;padding:0.75rem 1rem 0.5rem;gap:0.5rem;flex-shrink:0; }
            #jvcm-matches-title { font-size:0.9rem;font-weight:700;color:var(--jvc-text);flex:1; }
            #jvcm-matches-refresh { background:none;border:none;color:var(--jvc-text-muted);font-size:1rem;cursor:pointer;padding:0.2rem 0.4rem;border-radius:4px; }
            #jvcm-matches-close { background:none;border:none;color:var(--jvc-text-muted);font-size:1.3rem;cursor:pointer;line-height:1;padding:0.2rem; }
            #jvcm-leagues-wrap { padding:0 1rem 0.6rem;display:flex;flex-wrap:wrap;gap:0.35rem;flex-shrink:0;border-bottom:1px solid var(--jvc-border); }
            #jvcm-matches-loading { display:none;text-align:center;padding:2rem 1rem;color:var(--jvc-text-muted);font-size:0.82rem;flex-direction:column;align-items:center;gap:0.75rem; }
            #jvcm-matches-loading.hidden { display:none; }
            #jvcm-matches-empty { display:none;text-align:center;padding:2rem 1rem;color:var(--jvc-text-muted);font-size:0.82rem; }
            #jvcm-matches-list { flex:1;overflow-y:auto;padding:0.5rem 0; }
            /* Sliders */
            #jvcm-time-range { padding:0.5rem 1rem 0.4rem;border-bottom:1px solid var(--jvc-border);display:flex;gap:1rem;flex-shrink:0; }
            .jvcm-range-group { flex:1;display:flex;flex-direction:column;gap:0.25rem; }
            .jvcm-range-label { font-size:0.6rem;font-weight:600;color:var(--jvc-text-muted);text-transform:uppercase;letter-spacing:0.08em;display:flex;justify-content:space-between; }
            .jvcm-range-label span { color:var(--jvc-text);font-variant-numeric:tabular-nums; }
            .jvcm-range-input { -webkit-appearance:none;appearance:none;width:100%;height:4px;background:var(--jvc-surface-3);border-radius:2px;outline:none; }
            .jvcm-range-input::-webkit-slider-thumb { -webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:var(--jvc-orange);cursor:pointer; }
            #jvcm-matches-refresh.spinning { animation:jvcm-spin 0.7s linear infinite;color:var(--jvc-orange); }
            .jvcm-league-btn { padding:0.25rem 0.6rem;border-radius:999px;font-size:0.68rem;font-weight:600;font-family:Inter,system-ui,sans-serif;cursor:pointer;background:var(--jvc-surface-2);border:1px solid var(--jvc-border);color:var(--jvc-text-muted);transition:all 150ms ease; }
            .jvcm-league-btn.active { background:var(--jvc-orange);border-color:var(--jvc-orange);color:#fff; }
            .jvcm-match-row { padding:0.55rem 1rem;border-bottom:1px solid var(--jvc-divider);display:flex;flex-direction:column;gap:0.25rem;font-family:Inter,system-ui,sans-serif; }
            .jvcm-match-row:last-child { border-bottom:none; }
            .jvcm-match-row[data-state="inProgress"] { background:rgba(239,68,68,0.04);border-left:2px solid rgba(239,68,68,0.5); }
            .jvcm-match-row-head { display:flex;align-items:center;gap:0.4rem;font-size:0.58rem;font-weight:700;letter-spacing:0.08em;text-transform:uppercase; }
            .jvcm-match-badge-live { color:#ef4444;display:flex;align-items:center;gap:0.3rem; }
            .jvcm-match-badge-upcoming { color:#fbbf24;text-transform:none;letter-spacing:0;font-weight:500;font-size:0.65rem; }
            .jvcm-match-badge-done { color:var(--jvc-text-muted); }
            .jvcm-match-dot { width:6px;height:6px;border-radius:50%;background:currentColor;animation:jvcm-breathe 2.4s ease-in-out infinite; }
            .jvcm-match-stage { color:var(--jvc-text-muted);font-weight:500;font-size:0.6rem;text-transform:none;letter-spacing:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
            .jvcm-match-row-body { display:flex;align-items:baseline;gap:0.3rem;font-size:0.82rem;font-weight:700;color:var(--jvc-text); }
            .jvcm-match-team { flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
            .jvcm-match-team:last-child { text-align:right; }
            .jvcm-match-team.winner { color:var(--jvc-green); }
            .jvcm-match-team.loser  { color:var(--jvc-text-muted); }
            .jvcm-match-score { flex-shrink:0;font-size:0.78rem;font-weight:800;padding:0 0.4rem;font-variant-numeric:tabular-nums; }
            .jvcm-match-vs { flex-shrink:0;font-size:0.65rem;color:var(--jvc-text-muted);padding:0 0.4rem; }
            .jvcm-match-games { display:flex;align-items:center;gap:0.25rem;margin-top:0.15rem;flex-wrap:wrap; }
            .jvcm-game-dot { width:22px;height:22px;border-radius:4px;font-size:0.62rem;font-weight:700;display:grid;place-items:center;background:var(--jvc-surface-3);color:var(--jvc-text-muted);cursor:default;transition:opacity 150ms ease; }
            .jvcm-game-dot.clickable { cursor:pointer;border:1px solid var(--jvc-border); }
            .jvcm-game-dot.clickable:active { opacity:0.7; }
            .jvcm-game-dot.selected { border-color:var(--jvc-orange);box-shadow:0 0 0 1px var(--jvc-orange); }
            .jvcm-game-dot.live  { background:rgba(239,68,68,0.2);color:#ef4444;animation:jvcm-breathe 2.4s ease-in-out infinite; }
            .jvcm-game-dot.win   { background:rgba(74,222,128,0.2);color:var(--jvc-green); }
            .jvcm-game-dot.loss  { background:rgba(248,113,113,0.15);color:var(--jvc-red); }
            .jvcm-game-dot.win-blue { background:rgba(100,149,237,0.25);color:#7fb4ff;border-color:rgba(100,149,237,0.4); }
            .jvcm-game-dot.win-red  { background:rgba(248,113,113,0.2);color:var(--jvc-red);border-color:rgba(248,113,113,0.4); }
            .jvcm-match-streams { display:flex;align-items:center;gap:0.35rem;margin-top:0.4rem;flex-wrap:wrap; }
            .jvcm-stream-btn { display:inline-flex;align-items:center;gap:0.3rem;padding:0.28rem 0.65rem;background:#9147ff;border:none;border-radius:4px;color:#fff;font-size:0.7rem;font-weight:600;font-family:inherit;cursor:pointer;text-decoration:none; }
            .jvcm-stream-btn:active { background:#7b35d9; }
            .jvcm-stream-select { padding:0.28rem 0.5rem;background:#9147ff;border:none;border-radius:4px;color:#fff;font-size:0.7rem;font-weight:600;font-family:inherit;cursor:pointer;-webkit-appearance:none;outline:none; }
            /* Stats game */
            .jvcm-game-stats { display:none;flex-direction:column;margin-top:0.35rem;background:rgba(255,255,255,0.03);border:1px solid var(--jvc-border);border-radius:6px;overflow:hidden; }
            .jvcm-game-stats.open { display:flex; }
            .jvcm-game-stats-header { padding:0.3rem 0.5rem;font-size:0.6rem;font-weight:700;color:var(--jvc-text-muted);text-transform:uppercase;letter-spacing:0.08em;border-bottom:1px solid var(--jvc-border); }
            .jvcm-game-stats-loading { padding:0.6rem;text-align:center;font-size:0.65rem;color:var(--jvc-text-muted); }
            /* Lane detail */
            .jvcm-lane-detail { display:none;flex-direction:column;border-top:1px solid rgba(239,68,68,0.15); }
            .jvcm-lane-detail.open { display:flex; }
            .jvcm-lane-row { display:flex;align-items:center;padding:0.35rem 0.5rem;gap:0.3rem;border-bottom:1px solid var(--jvc-divider);font-size:0.62rem;font-variant-numeric:tabular-nums; }
            .jvcm-lane-row:last-child { border-bottom:none; }
            .jvcm-lane-badge { width:20px;text-align:center;flex-shrink:0;font-size:0.6rem;color:var(--jvc-text-muted);font-weight:600; }
            .jvcm-lane-player { flex:1;min-width:0;display:flex;flex-direction:column;gap:0.05rem; }
            .jvcm-lane-player.right { align-items:flex-end;text-align:right; }
            .jvcm-lane-name { font-weight:600;color:var(--jvc-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80px; }
            .jvcm-lane-champ { color:var(--jvc-text-muted);font-size:0.58rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80px; }
            .jvcm-lane-kda { color:var(--jvc-text-dim);font-size:0.6rem;white-space:nowrap; }
            .jvcm-lane-gold { color:#fbbf24;font-size:0.6rem;font-weight:600; }
            .jvcm-lane-cs { color:var(--jvc-text-muted);font-size:0.58rem;font-style:italic; }
            .jvcm-lane-mid-col { display:flex;flex-direction:column;align-items:center;gap:0.1rem;flex-shrink:0;padding:0 0.2rem; }
            .jvcm-lane-level { font-size:0.6rem;color:var(--jvc-text-muted);background:var(--jvc-surface-3);border-radius:3px;padding:0.05rem 0.25rem; }
        `;
        document.head.appendChild(style);
    }

document.body.insertAdjacentHTML('beforeend', _matchesModalHTML());



    return setupMatchesUI();
}

// ─────────────────────────────────────────────────────────────
// BOOT
// ─────────────────────────────────────────────────────────────
function boot() {
    // Injecter Material Symbols pour l'icône JVChat
    if (!document.querySelector('link[href*="material-symbols"]')) {
        const link = document.createElement('link');
        link.rel = 'stylesheet';
        link.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&icon_names=chat';
        document.head.appendChild(link);
    }
    // Liste des topics : /forums/0-FORUMID-...
    // Page topic      : /forums/42-FORUMID-TOPICID-... ou /forums/1-...
    const isTopicPage = !!location.href.match(/\/forums\/(?:42|1)-\d+/);
    const lol = isLolContext();

    if (isTopicPage) {
        // ── Page topic : bouton JVChat + bouton 🏆 dans la navbar ──

        // Chercher le bouton Actualiser par classe OU par texte OU par icône
        let refreshIcon = document.querySelector('.buttonsNavbar .buttonsNavbar__icon.icon-refresh')
                       || document.querySelector('[class*="icon-refresh"]');
        if (!refreshIcon) {
            const allBtns = document.querySelectorAll('button, [class*="buttonsNavbar"] > *');
            for (const b of allBtns) {
                if (b.textContent.trim().includes('Actualiser') || b.querySelector('[class*="refresh"]')) {
                    refreshIcon = b; break;
                }
            }
        }
        if (!refreshIcon) {
            // Dernier fallback : chercher la navbar et prendre le dernier bouton
            const navbar2 = document.querySelector('.buttonsNavbar, [class*="buttonsNavbar"]');
            if (navbar2) refreshIcon = navbar2.lastElementChild;
        }
        if (!refreshIcon) return;

        // Bouton JVChat
        let matchesModal = null;

        // Créer une deuxième ligne sous la navbar JVC
        const navbar = refreshIcon.closest('.buttonsNavbar, [class*="navbar"], nav')
                    || refreshIcon.closest('div[class]')?.parentElement
                    || refreshIcon.parentElement;
        if (!navbar) return;

        const row2 = document.createElement('div');
        row2.id = 'jvcm-navbar-row2';
        row2.style.cssText = `
            display: flex; align-items: center; justify-content: center;
            background: ${getComputedStyle(navbar).backgroundColor || '#1a1a1f'};
            border-top: 1px solid rgba(255,255,255,0.08);
            padding: 4px 0;
            gap: 0;
        `;
        navbar.insertAdjacentElement('afterend', row2);

        const makeBtn = (icon, label, onClick) => {
            const btn = document.createElement('button');
            btn.style.cssText = `
                flex: 1; display: flex; flex-direction: column; align-items: center;
                justify-content: center; gap: 2px; padding: 6px 4px;
                background: none; border: none; cursor: pointer;
                color: inherit; font-family: inherit;
                -webkit-tap-highlight-color: transparent;
            `;
            btn.innerHTML = `<span style="font-size:1.1rem;line-height:1;">${icon}</span><span style="font-size:0.6rem;">${label}</span>`;
            btn.addEventListener('click', onClick);
            return btn;
        };

        const jvcBtn = makeBtn(
            '<span class="material-symbols-outlined" style="font-size:1.1rem;line-height:1;">chat</span>',
            'JVChat',
            () => {
                row2.remove();
                if (matchesModal) document.getElementById('jvcm-matches-overlay')?.remove();
                new JVChatMobile().start().catch(e => console.error('[JVChat Mobile]', e));
            }
        );
        row2.appendChild(jvcBtn);

        if (lol) {
            const sep = document.createElement('span');
            sep.style.cssText = 'width:1px;background:rgba(255,255,255,0.1);align-self:stretch;margin:4px 0;';
            row2.appendChild(sep);
            const matchBtn = makeBtn('🏆', 'Matchs', () => {
                if (!matchesModal) matchesModal = injectStandaloneMatchesModal();
                matchesModal.open();
            });
            row2.appendChild(matchBtn);
        }

    } else {
        // ── Liste des topics : bouton 🏆 dans la navbar du bas ──
        if (!lol) return;

        const injectIntoBottomNav = () => {
            const allBtns = document.querySelectorAll('.buttonsNavbar__button, [class*="navbar"] button, .forum-actions button');
            let refreshBtn = null;
            for (const b of allBtns) {
                if (b.textContent.includes('Actualiser') || b.querySelector('[class*="refresh"], [class*="icon-refresh"]')) {
                    refreshBtn = b; break;
                }
            }

            if (!refreshBtn) {
                if (document.getElementById('jvcm-float-btn')) return;
                injectStandaloneMatchesModal();
                const floatBtn = document.createElement('button');
                floatBtn.id = 'jvcm-float-btn';
                floatBtn.title = 'Matchs esport';
                floatBtn.textContent = '🏆';
                document.body.appendChild(floatBtn);
                let mm = null;
                floatBtn.addEventListener('click', () => {
                    if (!mm) mm = injectStandaloneMatchesModal();
                    mm.open();
                });
                return;
            }

            if (document.getElementById('jvcm-nav-matches-btn')) return;

            const matchBtn = document.createElement('button');
            matchBtn.id = 'jvcm-nav-matches-btn';
            matchBtn.type = 'button';
            matchBtn.className = refreshBtn.className;
            matchBtn.removeAttribute('style');
            const labelClass = refreshBtn.querySelector('div')?.className || '';
            matchBtn.innerHTML = `<i style="font-style:normal;animation:none;transform:none;">🏆</i><div class="${labelClass}">Matchs</div>`;

            // Insérer : Actualiser [spacer] [Matchs]
            const spBefore = document.createElement('span');
            spBefore.className = 'buttonsNavbar__space';
            refreshBtn.insertAdjacentElement('afterend', spBefore);
            spBefore.insertAdjacentElement('afterend', matchBtn);

            let mm = null;
            matchBtn.addEventListener('click', () => {
                if (!mm) mm = injectStandaloneMatchesModal();
                mm.open();
            });
        };

        if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', injectIntoBottomNav, { once: true });
        else injectIntoBottomNav();
    }
}

if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true });
else boot();