RSWARE — Omoggle 1v1

Score spoof, camera spoof, mic spoof, country spoof, PRO bypass, enemy score, ban bypass, mass report

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         RSWARE — Omoggle 1v1
// @namespace    http://tampermonkey.net/
// @version      9.3
// @description  Score spoof, camera spoof, mic spoof, country spoof, PRO bypass, enemy score, ban bypass, mass report
// @author       you
// @match        *://*.omoggle.com/*
// @match        *://omoggle.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    /* ══════════════════════════════════════════════════════════════════
       STATE
    ══════════════════════════════════════════════════════════════════ */
    const CFG = {
        scoreSpoof: false, displayWin: false, liveSpoof: false,
        scoreVal: 10, enemySpoof: false, enemyVal: 1.0,
    };
    const DYN = { enabled: false, min: 7.0, max: 10.0, cur: 10.0, timer: null };
    const CAM = { enabled: false, mode: 'black', img: null, color: '#111111' };
    const MIC = { enabled: false, mode: 'silent', freq: 440 };
    const SPF = { country: false, code: 'US', pro: false, ban: false };
    const BAN_STATE = { lastUrl: null, lastProto: null, reconnectTimer: null };
    const RPT = { running: false, sent: 0, total: 0, timer: null, lastUrl: null, lastHeaders: {}, lastBody: null };
    const MATCH = { opponentId: null, opponentUsername: null, matchId: null };

    /* ══════════════════════════════════════════════════════════════════
       CONFIG PERSISTENCE
    ══════════════════════════════════════════════════════════════════ */
    const CONF_KEY = 'rsware_omoggle_v93';
    let _saveTimer = null;

    function saveConf() {
        try {
            localStorage.setItem(CONF_KEY, JSON.stringify({
                v: 93,
                score: { spoof: CFG.scoreSpoof, val: CFG.scoreVal, displayWin: CFG.displayWin, liveSpoof: CFG.liveSpoof,
                         dynEnabled: DYN.enabled, dynMin: DYN.min, dynMax: DYN.max, dynInt: 3 },
                enemy: { spoof: CFG.enemySpoof, val: CFG.enemyVal },
                cam:   { enabled: CAM.enabled, mode: CAM.mode, color: CAM.color },
                mic:   { enabled: MIC.enabled, mode: MIC.mode, freq: MIC.freq },
                spf:   { country: SPF.country, code: SPF.code, pro: SPF.pro, ban: SPF.ban },
            }));
            const el = document.getElementById('rsw-conf-status');
            if (el) { el.textContent = 'SAVED'; el.style.color = '#0f0'; setTimeout(function(){ if(el){el.textContent='AUTO';el.style.color='#333';} }, 1400); }
        } catch(_) {}
    }

    function autoSave() { clearTimeout(_saveTimer); _saveTimer = setTimeout(saveConf, 700); }

    function loadConf() { try { const r=localStorage.getItem(CONF_KEY); return r?JSON.parse(r):null; } catch(_){ return null; } }

    function applyConf(c) {
        if (!c || c.v !== 93) return;
        if (c.score) {
            CFG.scoreSpoof  = !!c.score.spoof;
            CFG.scoreVal    = c.score.val != null ? c.score.val : 10;
            CFG.displayWin  = !!c.score.displayWin;
            CFG.liveSpoof   = !!c.score.liveSpoof;
            DYN.enabled     = !!c.score.dynEnabled;
            DYN.min         = c.score.dynMin != null ? c.score.dynMin : 7;
            DYN.max         = c.score.dynMax != null ? c.score.dynMax : 10;
        }
        if (c.enemy) { CFG.enemySpoof = !!c.enemy.spoof; CFG.enemyVal = c.enemy.val != null ? c.enemy.val : 1.0; }
        if (c.cam)   { CAM.enabled = !!c.cam.enabled; CAM.mode = c.cam.mode||'black'; CAM.color = c.cam.color||'#111111'; }
        if (c.mic)   { MIC.enabled = !!c.mic.enabled; MIC.mode = c.mic.mode||'silent'; MIC.freq = c.mic.freq||440; }
        if (c.spf)   { SPF.country = !!c.spf.country; SPF.code = c.spf.code||'US'; SPF.pro = !!c.spf.pro; SPF.ban = !!c.spf.ban; }
    }

    // Restore saved state before hooks run
    applyConf(loadConf());

    let hookedWsRef = null, floodTimer = null, pktCount = 0;
    const logs = [];

    function log(msg) {
        const t = new Date().toLocaleTimeString('en-GB', { hour12: false });
        logs.push(t + '  ' + msg);
        if (logs.length > 400) logs.shift();
        const el = document.getElementById('rsw-log');
        if (el) { el.textContent = logs.join('\n'); el.scrollTop = el.scrollHeight; }
        const c = document.getElementById('rsw-cnt');
        if (c) c.textContent = pktCount;
    }

    /* ══════════════════════════════════════════════════════════════════
       DYNAMIC RANGE
    ══════════════════════════════════════════════════════════════════ */
    function dynGet() { return DYN.enabled ? DYN.cur : CFG.scoreVal; }
    function dynTick() {
        DYN.cur = Math.round((DYN.min + Math.random() * (DYN.max - DYN.min)) * 10) / 10;
        // UI is updated by the 250ms ticker — no DOM access needed here
        log('[DYN] tick → ' + DYN.cur.toFixed(1));
    }
    function dynStart(ms) {
        if (DYN.timer) clearInterval(DYN.timer);
        dynTick(); // fire immediately so first value is set right away
        DYN.timer = setInterval(dynTick, Math.max(100, ms));
    }
    function dynStop() {
        if (DYN.timer) { clearInterval(DYN.timer); DYN.timer = null; }
        DYN.cur = CFG.scoreVal;
    }

    /* ══════════════════════════════════════════════════════════════════
       CAMERA CANVAS — set up at document-start before MediaPipe loads
    ══════════════════════════════════════════════════════════════════ */
    const camCanvas = document.createElement('canvas');
    camCanvas.width = 640; camCanvas.height = 480;
    const camCtx = camCanvas.getContext('2d');

    (function camLoop() {
        if (CAM.mode === 'image' && CAM.img) {
            camCtx.drawImage(CAM.img, 0, 0, 640, 480);
        } else {
            camCtx.fillStyle = CAM.color || '#000';
            camCtx.fillRect(0, 0, 640, 480);
        }
        const prev = document.getElementById('rsw-cam-prev');
        if (prev) {
            const pc = prev.getContext('2d');
            pc.drawImage(camCanvas, 0, 0, prev.width, prev.height);
        }
        requestAnimationFrame(camLoop);
    })();

    /* getUserMedia hook */
    let _micCtx = null;
    // keep hard refs so AudioNodes aren't GC'd
    const _micNodes = [];

    function createMicTrack() {
        // Resume or create AudioContext — must happen inside user-gesture chain (getUserMedia satisfies this)
        if (!_micCtx) {
            _micCtx = new (window.AudioContext || window.webkitAudioContext)();
        }
        if (_micCtx.state === 'suspended') _micCtx.resume();

        const dst = _micCtx.createMediaStreamDestination();
        _micNodes.length = 0; // clear old nodes

        if (MIC.mode === 'tone') {
            const osc  = _micCtx.createOscillator();
            const gain = _micCtx.createGain();
            osc.type = 'sine';
            osc.frequency.value = MIC.freq || 440;
            gain.gain.value = 0.4;
            osc.connect(gain); gain.connect(dst);
            osc.start();
            _micNodes.push(osc, gain, dst);
            log('[MIC] tone track active  ' + (MIC.freq || 440) + 'Hz');

        } else if (MIC.mode === 'noise') {
            // White noise via ScriptProcessor (works in all browsers)
            const bufSize = 2048;
            const sp = _micCtx.createScriptProcessor(bufSize, 0, 1);
            sp.onaudioprocess = function (e) {
                const out = e.outputBuffer.getChannelData(0);
                for (let i = 0; i < bufSize; i++) out[i] = (Math.random() * 2 - 1) * 0.35;
            };
            const gain = _micCtx.createGain(); gain.gain.value = 1;
            sp.connect(gain); gain.connect(dst);
            _micNodes.push(sp, gain, dst);
            log('[MIC] noise track active');

        } else {
            // silent — oscillator at 0 gain
            const osc  = _micCtx.createOscillator();
            const gain = _micCtx.createGain(); gain.gain.value = 0;
            osc.connect(gain); gain.connect(dst);
            osc.start();
            _micNodes.push(osc, gain, dst);
            log('[MIC] silent track active');
        }

        const track = dst.stream.getAudioTracks()[0];
        if (!track) { log('[MIC] ERROR: no audio track returned'); }
        return track;
    }

    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        const _gum = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
        navigator.mediaDevices.getUserMedia = async function (constraints) {
            let real;
            try { real = await _gum(constraints); } catch (e) { throw e; }
            if (!CAM.enabled && !MIC.enabled) return real;
            const tracks = [];
            if (constraints && constraints.video) {
                if (CAM.enabled) {
                    real.getVideoTracks().forEach(t => t.stop());
                    tracks.push(...camCanvas.captureStream(30).getVideoTracks());
                    log('[CAM] video track spoofed  mode=' + CAM.mode);
                } else { tracks.push(...real.getVideoTracks()); }
            }
            if (constraints && constraints.audio) {
                if (MIC.enabled) {
                    real.getAudioTracks().forEach(t => t.stop());
                    const t = createMicTrack();
                    if (t) tracks.push(t);
                } else { tracks.push(...real.getAudioTracks()); }
            }
            // If no tracks were added (e.g. site only asked video and cam is off) fall back
            if (tracks.length === 0) return real;
            return new MediaStream(tracks);
        };
    }

    /* ══════════════════════════════════════════════════════════════════
       COUNTRY SPOOF
    ══════════════════════════════════════════════════════════════════ */
    const COUNTRIES = {
        US:{ name:'United States', lang:'en-US', tz:'America/New_York',   lat:40.71, lon:-74.01 },
        GB:{ name:'United Kingdom',lang:'en-GB', tz:'Europe/London',      lat:51.51, lon:-0.13  },
        DE:{ name:'Germany',       lang:'de-DE', tz:'Europe/Berlin',      lat:52.52, lon:13.40  },
        FR:{ name:'France',        lang:'fr-FR', tz:'Europe/Paris',       lat:48.86, lon:2.35   },
        JP:{ name:'Japan',         lang:'ja-JP', tz:'Asia/Tokyo',         lat:35.69, lon:139.69 },
        KR:{ name:'South Korea',   lang:'ko-KR', tz:'Asia/Seoul',         lat:37.57, lon:126.98 },
        CN:{ name:'China',         lang:'zh-CN', tz:'Asia/Shanghai',      lat:31.23, lon:121.47 },
        BR:{ name:'Brazil',        lang:'pt-BR', tz:'America/Sao_Paulo',  lat:-23.55,lon:-46.63 },
        RU:{ name:'Russia',        lang:'ru-RU', tz:'Europe/Moscow',      lat:55.75, lon:37.62  },
        IN:{ name:'India',         lang:'hi-IN', tz:'Asia/Kolkata',       lat:28.61, lon:77.21  },
        AU:{ name:'Australia',     lang:'en-AU', tz:'Australia/Sydney',   lat:-33.87,lon:151.21 },
        CA:{ name:'Canada',        lang:'en-CA', tz:'America/Toronto',    lat:43.65, lon:-79.38 },
        IT:{ name:'Italy',         lang:'it-IT', tz:'Europe/Rome',        lat:41.90, lon:12.50  },
        ES:{ name:'Spain',         lang:'es-ES', tz:'Europe/Madrid',      lat:40.42, lon:-3.70  },
        NL:{ name:'Netherlands',   lang:'nl-NL', tz:'Europe/Amsterdam',   lat:52.37, lon:4.90   },
        SE:{ name:'Sweden',        lang:'sv-SE', tz:'Europe/Stockholm',   lat:59.33, lon:18.07  },
        NO:{ name:'Norway',        lang:'nb-NO', tz:'Europe/Oslo',        lat:59.91, lon:10.75  },
        PL:{ name:'Poland',        lang:'pl-PL', tz:'Europe/Warsaw',      lat:52.23, lon:21.01  },
        TR:{ name:'Turkey',        lang:'tr-TR', tz:'Europe/Istanbul',    lat:39.93, lon:32.86  },
        MX:{ name:'Mexico',        lang:'es-MX', tz:'America/Mexico_City',lat:19.43, lon:-99.13 },
    };

    function applyCountrySpoof() {
        const c = COUNTRIES[SPF.code] || COUNTRIES.US;
        try {
            Object.defineProperty(navigator, 'language',  { configurable:true, get:()=>c.lang });
            Object.defineProperty(navigator, 'languages', { configurable:true, get:()=>[c.lang, c.lang.split('-')[0]] });
        } catch(_) {}
        try {
            if (navigator.geolocation) {
                const _og = navigator.geolocation.getCurrentPosition.bind(navigator.geolocation);
                navigator.geolocation.getCurrentPosition = function (ok, err, opts) {
                    if (SPF.country) ok({ coords:{ latitude:c.lat, longitude:c.lon, accuracy:50, altitude:null, altitudeAccuracy:null, heading:null, speed:null }, timestamp:Date.now() });
                    else _og(ok, err, opts);
                };
            }
        } catch(_) {}
        log('[SPF] country → ' + c.name);
    }

    /* ══════════════════════════════════════════════════════════════════
       PRO BYPASS — 4-layer attack
       1. fetch() hook        — patches API responses before JS sees them
       2. XHR hook            — same for XMLHttpRequest
       3. localStorage hook   — patches cached Supabase session on every read
       4. CSS injection       — removes paywall blur/lock overlays from DOM
    ══════════════════════════════════════════════════════════════════ */

    // Supabase project ID → the localStorage key they use for the session
    const SB_LS_KEY = 'sb-echqrtrlzdhzyuhtueoj-auth-token';

    // Deep-patch any JS object to look like a PRO user.
    // Only touches fields that are clearly subscription/plan related.
    function deepPatchPro(o, depth) {
        if (!o || typeof o !== 'object' || depth > 6) return;
        depth = (depth || 0);

        // Always unconditionally set known PRO fields
        o.is_pro        = true;
        o.isPro         = true;
        o.hasPro        = true;
        o.pro           = true;
        o.plan          = 'pro';
        o.tier          = 'pro';
        o.role          = o.role || 'authenticated';   // don't destroy real role
        o.subscription  = { plan:'pro', status:'active', active:true,
                            cancel_at_period_end:false, current_period_end: 9999999999 };

        // Supabase user_metadata / app_metadata — create if missing
        if (!o.user_metadata || typeof o.user_metadata !== 'object') o.user_metadata = {};
        o.user_metadata.plan   = 'pro';
        o.user_metadata.is_pro = true;
        o.user_metadata.tier   = 'pro';

        if (!o.app_metadata || typeof o.app_metadata !== 'object') o.app_metadata = {};
        o.app_metadata.plan   = 'pro';
        o.app_metadata.is_pro = true;

        // Recurse into common wrapper keys only (avoid recursing everything)
        ['data','user','profile','account','session','me','viewer'].forEach(function(k) {
            if (o[k] && typeof o[k] === 'object') deepPatchPro(o[k], depth + 1);
        });
        if (Array.isArray(o)) o.forEach(function(item) { deepPatchPro(item, depth + 1); });
    }

    /* ══════════════════════════════════════════════════════════════════
       BAN BYPASS — strip ban fields from any object
    ══════════════════════════════════════════════════════════════════ */
    const BAN_WORDS = ['ban','suspend','restrict','block','mute','shadow'];
    function isBanMsg(s) {
        if (!s || typeof s !== 'string') return false;
        const lc = s.toLowerCase();
        return BAN_WORDS.some(function(w) { return lc.includes(w); });
    }

    function deepPatchBan(o, depth) {
        if (!o || typeof o !== 'object' || depth > 6) return;
        // Wipe every ban-related flag unconditionally
        o.banned        = false;  o.is_banned     = false;  o.isBanned      = false;
        o.suspended     = false;  o.is_suspended  = false;  o.isSuspended   = false;
        o.restricted    = false;  o.is_restricted = false;  o.isRestricted  = false;
        o.blocked       = false;  o.is_blocked    = false;
        o.shadow_banned = false;  o.shadowBanned  = false;
        o.banned_at        = null; o.ban_expires     = null;
        o.banned_until     = null; o.suspend_until   = null;
        o.ban_reason       = null; o.banReason       = null;
        o.suspension_reason = null;
        // Status fields: turn "banned"/"suspended" → "active"
        if (typeof o.status  === 'string' && isBanMsg(o.status))  o.status  = 'active';
        if (typeof o.state   === 'string' && isBanMsg(o.state))   o.state   = 'active';
        if (typeof o.account_status === 'string' && isBanMsg(o.account_status)) o.account_status = 'active';
        // Recurse into common wrapper keys
        ['data','user','profile','account','session','me','viewer'].forEach(function(k) {
            if (o[k] && typeof o[k] === 'object') deepPatchBan(o[k], depth + 1);
        });
        if (Array.isArray(o)) o.forEach(function(item) { deepPatchBan(item, depth + 1); });
    }

    /* ══════════════════════════════════════════════════════════════════
       OPPONENT DETECTION
    ══════════════════════════════════════════════════════════════════ */
    const OPP_ID_FIELDS   = ['opponent_id','opponentId','opponent_user_id','challengerId','player2_id','p2_id','p2Id','enemy_id','enemyId','other_user_id','otherUserId'];
    const OPP_NAME_FIELDS = ['opponent_username','opponentUsername','opponent_name','opponentName','challengerUsername','p2Username','p2_username','enemyUsername','enemy_username','other_username','otherUsername','opponent_display_name'];
    const MATCH_ID_FIELDS = ['match_id','matchId','room_id','roomId','game_id','gameId','battle_id','session_id'];

    function updateOpponentUI() {
        const val = MATCH.opponentUsername || MATCH.opponentId || '--';
        const el  = document.getElementById('rsw-rpt-opp-val'); if (el) { el.textContent = val; el.style.color = val !== '--' ? '#0f0' : '#444'; }
        const el2 = document.getElementById('rsw-rpt-opp-id');  if (el2) el2.textContent = MATCH.opponentId || '--';
        // Auto-fill report target if currently empty
        const tgt = document.getElementById('rsw-rpt-target');
        if (tgt && !tgt.value && (MATCH.opponentId || MATCH.opponentUsername))
            tgt.value = MATCH.opponentId || MATCH.opponentUsername;
    }

    function getMyId() {
        try {
            for (let i=0;i<localStorage.length;i++) {
                const k=localStorage.key(i); if(!k) continue;
                if (k.includes('auth-token')||k.includes('supabase')||k.startsWith('sb-')) {
                    const obj=JSON.parse(localStorage.getItem(k)||'');
                    const user=obj&&(obj.user||obj.session&&obj.session.user);
                    if (user&&user.id) return user.id;
                }
            }
        } catch(_){}
        return null;
    }

    function captureOpponent(obj, depth) {
        if (!obj || typeof obj !== 'object' || depth > 8) return;
        const me   = CFG.myUsername;
        const myId = depth === 0 ? getMyId() : null; // only fetch from LS at top level

        // Direct opponent_* fields
        for (const f of OPP_ID_FIELDS) {
            if (obj[f] && typeof obj[f] === 'string' && obj[f].length > 2) {
                if (MATCH.opponentId !== obj[f]) { MATCH.opponentId = obj[f]; log('[MATCH] opp ID ('+f+'): '+obj[f]); updateOpponentUI(); }
                break;
            }
        }
        for (const f of OPP_NAME_FIELDS) {
            if (obj[f] && typeof obj[f] === 'string' && obj[f].length > 0) {
                if (MATCH.opponentUsername !== obj[f]) { MATCH.opponentUsername = obj[f]; log('[MATCH] opp name ('+f+'): '+obj[f]); updateOpponentUI(); }
                break;
            }
        }

        // player1_id / player2_id pattern — figure out which one is us
        const p1id  = obj.player1_id  || obj.p1_id   || obj.p1Id;
        const p2id  = obj.player2_id  || obj.p2_id   || obj.p2Id;
        const p1name= obj.player1_username || obj.player1_name || obj.p1_username || obj.p1Username;
        const p2name= obj.player2_username || obj.player2_name || obj.p2_username || obj.p2Username;
        if (p1id || p2id || p1name || p2name) {
            const _myId = myId || getMyId();
            const iAm1 = (me && (p1name===me||p1id===me)) || (_myId && (p1id===_myId));
            const iAm2 = (me && (p2name===me||p2id===me)) || (_myId && (p2id===_myId));
            if (iAm1) {
                // I am player1 → opponent is player2
                if (p2id  && !MATCH.opponentId)       { MATCH.opponentId = String(p2id);   log('[MATCH] opp ID (player2_id): '+p2id);   updateOpponentUI(); }
                if (p2name&& !MATCH.opponentUsername)  { MATCH.opponentUsername=String(p2name); log('[MATCH] opp name (player2): '+p2name); updateOpponentUI(); }
            } else if (iAm2) {
                // I am player2 → opponent is player1
                if (p1id  && !MATCH.opponentId)       { MATCH.opponentId = String(p1id);   log('[MATCH] opp ID (player1_id): '+p1id);   updateOpponentUI(); }
                if (p1name&& !MATCH.opponentUsername)  { MATCH.opponentUsername=String(p1name); log('[MATCH] opp name (player1): '+p1name); updateOpponentUI(); }
            } else {
                // Can't tell which is us — store player2 as a best-guess (challenger is usually p2)
                if (!MATCH.opponentId) {
                    const guess = p2id || p1id;
                    if (guess) { MATCH.opponentId=String(guess); log('[MATCH] opp ID (guess p2): '+guess); updateOpponentUI(); }
                }
                if (!MATCH.opponentUsername) {
                    const guess = p2name || p1name;
                    if (guess) { MATCH.opponentUsername=String(guess); log('[MATCH] opp name (guess p2): '+guess); updateOpponentUI(); }
                }
            }
        }

        // players[] array — find entry that isn't us
        if (Array.isArray(obj.players)) {
            const _myId = myId || getMyId();
            for (const p of obj.players) {
                if (!p||typeof p!=='object') continue;
                const pName=p.username||p.name||p.display_name||p.displayName;
                const pId  =p.id||p.user_id||p.userId;
                if (me   && (pName===me||pId===me)) continue;
                if (_myId&& pId===_myId) continue;
                if (pId   &&!MATCH.opponentId)       { MATCH.opponentId=String(pId);   log('[MATCH] opp ID (players[]): '+pId);   updateOpponentUI(); }
                if (pName &&!MATCH.opponentUsername)  { MATCH.opponentUsername=String(pName); log('[MATCH] opp name (players[]): '+pName); updateOpponentUI(); }
            }
        }

        for (const f of MATCH_ID_FIELDS) { if (obj[f]) MATCH.matchId=String(obj[f]); }

        // Recurse — include Supabase Realtime nesting: record / new_record / response
        const recurse=['payload','data','match','room','game','battle','session','meta','record','new_record','response','body'];
        for (const k of recurse) { if (obj[k]&&typeof obj[k]==='object') captureOpponent(obj[k],depth+1); }
    }

    function scanDOMForOpponent() {
        const me = CFG.myUsername;
        // Try CSS class hints first
        const hints = ['[class*="opponent"]','[class*="enemy"]','[class*="player2"]','[class*="p2"]',
                       '[class*="challenger"]','[data-player="2"]','[data-role="opponent"]','[class*="other-player"]'];
        for (const sel of hints) {
            try {
                document.querySelectorAll(sel).forEach(function(el) {
                    const txt = (el.textContent || '').trim();
                    if (txt && txt.length > 1 && txt.length < 50 && txt !== me && !/^\d/.test(txt)) {
                        if (!MATCH.opponentUsername) { MATCH.opponentUsername = txt; log('[DOM] opponent from "'+sel+'": '+txt); updateOpponentUI(); }
                    }
                });
            } catch(_) {}
        }
        // Walk every text node — find anything that looks like a username that isn't ours
        try {
            const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
            while (walker.nextNode()) {
                const txt = (walker.currentNode.textContent || '').trim();
                if (!txt || txt.length < 2 || txt.length > 40) continue;
                const parent = walker.currentNode.parentElement;
                if (!parent) continue;
                const tag = parent.tagName.toLowerCase();
                if (['script','style','button','input','meta'].includes(tag)) continue;
                const cls = (parent.className || '').toLowerCase();
                if ((cls.includes('name')||cls.includes('user')||cls.includes('player')||cls.includes('profile'))
                    && txt !== me && !txt.includes(' ') === false && !/^\d+\.?\d*$/.test(txt)) {
                    if (!MATCH.opponentUsername && txt !== me) {
                        MATCH.opponentUsername = txt;
                        log('[DOM] opponent from text scan: ' + txt);
                        updateOpponentUI();
                        break;
                    }
                }
            }
        } catch(_) {}
        log('[DOM] scan done — opp: '+(MATCH.opponentUsername||'not found')+' id: '+(MATCH.opponentId||'not found'));
    }

    /* Layer 1 — fetch hook (fires before site JS reads the response) */
    const _origFetch = window.fetch;
    window.fetch = async function (...args) {
        const url = String(args[0] instanceof Request ? args[0].url : (args[0] || ''));
        let reqInit = (args.length > 1 && args[1]) ? args[1] : {};
        const reqMethod = ((args[0] instanceof Request ? args[0].method : reqInit.method) || 'GET').toUpperCase();

        // ── Score: patch outgoing POST/PUT bodies before sending ──────────────
        if (CFG.scoreSpoof && (reqMethod==='POST'||reqMethod==='PUT') && reqInit.body && typeof reqInit.body==='string') {
            try {
                const j = JSON.parse(reqInit.body);
                if (j && typeof j==='object' && injectScoreFields(j)) {
                    reqInit = Object.assign({}, reqInit, { body: JSON.stringify(j) });
                    if (args.length > 1) args = [args[0], reqInit];
                    else args = [args[0], reqInit];
                    log('[->FETCH] score in body → '+dynGet()+'  '+url.replace(/^https?:\/\/[^/]+/,'').slice(0,45));
                }
            } catch(_) {}
        }

        // Capture report requests so mass-report can replay them exactly
        if (reqMethod === 'POST' && /report/i.test(url)) {
            try {
                const b = reqInit.body ? String(reqInit.body) : null;
                RPT.lastUrl = url; RPT.lastBody = b;
                RPT.lastHeaders = (reqInit.headers && typeof reqInit.headers === 'object')
                    ? Object.assign({}, reqInit.headers) : {};
                log('[RPT] captured: ' + url.replace(/^https?:\/\/[^/]+/,'').slice(0,55));
                const el = document.getElementById('rsw-rpt-captured');
                if (el) { el.textContent = url.replace(/^https?:\/\/[^/]+/,'').slice(0,42); el.style.color='#0f0'; }
                const be = document.getElementById('rsw-rpt-body');
                if (be && b) { be.textContent = b.slice(0,60); be.style.color='#888'; }
            } catch(_) {}
        }

        const resp = await _origFetch(...args);
        if (!SPF.pro && !SPF.ban) return resp;

        // ── BAN: intercept ban responses (any non-200 with ban text in body) ──
        if (SPF.ban && resp.status >= 400) {
            try {
                const body = await resp.clone().text();
                if (isBanMsg(body)) {
                    log('[BAN] ' + resp.status + ' ban response blocked → faking 200: ' + url.replace(/^https?:\/\/[^/]+/,'').slice(0,50));
                    // For Supabase auth refresh (/auth/v1/token), return a fake success
                    // that keeps the existing token so the SDK doesn't sign us out
                    const fakeBody = url.includes('/auth/') || url.includes('token')
                        ? '{"access_token":"","token_type":"bearer","expires_in":3600,"expires_at":' + (Math.floor(Date.now()/1000)+3600) + ',"refresh_token":""}'
                        : '{"success":true,"data":{}}';
                    return new Response(fakeBody, {
                        status: 200, statusText: 'OK', headers: resp.headers
                    });
                }
            } catch(_) {}
        }

        const ct  = resp.headers.get('content-type') || '';
        if (!ct.includes('json')) return resp;
        // patch ALL json from supabase AND omoggle (broad)
        // Always scan every JSON response for opponent data, even on unknown URLs
        const isKnown = url.includes('supabase') || url.includes('omoggle.com') || url.includes('/api/');
        try {
            const text = await resp.clone().text();
            const j    = JSON.parse(text);
            captureOpponent(j, 0);
            if (isKnown && (SPF.pro || SPF.ban)) {
                if (SPF.pro) deepPatchPro(j, 0);
                if (SPF.ban) deepPatchBan(j, 0);
                log('[PATCH] fetch: ' + url.replace(/^https?:\/\/[^/]+/, '').slice(0, 50));
                return new Response(JSON.stringify(j), {
                    status: resp.status, statusText: resp.statusText, headers: resp.headers
                });
            }
        } catch(_) {}
        return resp;
    };

    /* Layer 2 — XHR hook */
    (function() {
        const _open = XMLHttpRequest.prototype.open;
        const _send = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.open = function(m, url) {
            this._rsw_url = String(url || '');
            return _open.apply(this, arguments);
        };
        XMLHttpRequest.prototype.send = function(body) {
            // Patch outgoing score fields in XHR request body
            if (CFG.scoreSpoof && body && typeof body==='string') {
                try {
                    const j = JSON.parse(body);
                    if (j && typeof j==='object' && injectScoreFields(j)) {
                        body = JSON.stringify(j);
                        log('[->XHR] score in body → '+dynGet()+'  '+(this._rsw_url||'').slice(-45));
                    }
                } catch(_) {}
            }
            if ((SPF.pro || SPF.ban) && this._rsw_url) {
                const xhr = this;
                xhr.addEventListener('load', function() {
                    if (!SPF.pro && !SPF.ban) return;
                    try {
                        const ct = xhr.getResponseHeader('content-type') || '';
                        if (!ct.includes('json')) return;
                        const j = JSON.parse(xhr.responseText);
                        if (SPF.pro) deepPatchPro(j, 0);
                        if (SPF.ban) deepPatchBan(j, 0);
                        Object.defineProperty(xhr, 'responseText', { configurable:true, get: function(){ return JSON.stringify(j); } });
                        Object.defineProperty(xhr, 'response',     { configurable:true, get: function(){ return JSON.stringify(j); } });
                        log('[PATCH] XHR: ' + xhr._rsw_url.slice(-50));
                    } catch(_) {}
                });
            }
            // Pass the (possibly modified) body
            return body !== arguments[0] ? _send.call(this, body) : _send.apply(this, arguments);
        };
    })();

    /* Layer 3 — localStorage hook: patch Supabase cached session on every READ and WRITE */
    (function() {
        const _origGet = Storage.prototype.getItem;
        const _origSet = Storage.prototype.setItem;

        function isSbKey(key) {
            return key && (key === SB_LS_KEY || key.includes('auth-token') || key.includes('supabase') || key.startsWith('sb-'));
        }
        function patchSbObj(obj) {
            if (SPF.pro) {
                if (obj.user && typeof obj.user === 'object') deepPatchPro(obj.user, 0);
                if (obj.user_metadata || obj.app_metadata || obj.email) deepPatchPro(obj, 0);
            }
            if (SPF.ban) {
                if (obj.user && typeof obj.user === 'object') deepPatchBan(obj.user, 0);
                deepPatchBan(obj, 0);
            }
        }

        // Patch on READ
        Storage.prototype.getItem = function(key) {
            const val = _origGet.call(this, key);
            if (!SPF.pro && !SPF.ban) return val;
            if (!val || typeof val !== 'string') return val;
            if (isSbKey(key)) {
                try {
                    const obj = JSON.parse(val);
                    if (obj && typeof obj === 'object') { patchSbObj(obj); return JSON.stringify(obj); }
                } catch(_) {}
            }
            return val;
        };

        // Patch on WRITE — Supabase SDK stores fresh token after every refresh
        // This is the key fix: strip banned_until BEFORE it gets written back
        Storage.prototype.setItem = function(key, val) {
            if ((SPF.pro || SPF.ban) && isSbKey(key) && val && typeof val === 'string') {
                try {
                    const obj = JSON.parse(val);
                    if (obj && typeof obj === 'object') {
                        patchSbObj(obj);
                        val = JSON.stringify(obj);
                        if (SPF.ban) log('[BAN] setItem patched ban fields before write: ' + key.slice(-30));
                    }
                } catch(_) {}
            }
            return _origSet.call(this, key, val);
        };
    })();

    /* Ban DOM scrubber — removes ban overlay elements as they appear */
    function setupBanDOMScrubber() {
        const BAN_TEXT_PATTERNS = [/you.{0,8}(banned|suspended|restricted)/i, /account.{0,12}(ban|suspend)/i,
                                   /temporarily banned/i, /permanently banned/i, /ban expires/i];
        const BAN_CSS_PATTERNS  = ['ban','suspend','restrict','blocked','shadowban'];

        function scrubNode(node) {
            if (!node || node.nodeType !== 1) return;
            // Check text content
            const txt = node.textContent || '';
            if (BAN_TEXT_PATTERNS.some(r => r.test(txt)) && txt.length < 600) {
                const tag = node.tagName.toLowerCase();
                // Don't remove body/html/main — only overlay-like divs/sections
                if (['div','section','aside','article','p','span','h1','h2','h3'].includes(tag)) {
                    node.style.display = 'none';
                    log('[BAN] DOM: hid ban node <' + tag + '> "' + txt.slice(0,40) + '"');
                }
            }
            // Check CSS class/id for ban words
            const cls = (node.className || '') + ' ' + (node.id || '');
            if (BAN_CSS_PATTERNS.some(w => cls.toLowerCase().includes(w))) {
                node.style.display = 'none';
                node.style.visibility = 'hidden';
                log('[BAN] DOM: hid ban-class element .' + (node.className||'').split(' ')[0]);
            }
        }

        const obs = new MutationObserver(function(muts) {
            if (!SPF.ban) return;
            muts.forEach(function(m) {
                m.addedNodes.forEach(function(n) { scrubNode(n); });
            });
        });
        obs.observe(document.documentElement, { childList: true, subtree: true });
    }

    /* Layer 4 — CSS: remove common paywall/lock/upgrade overlays */
    function injectProCSS() {
        if (document.getElementById('rsw-pro-css')) return;
        const s = document.createElement('style');
        s.id = 'rsw-pro-css';
        s.textContent = [
            /* nuke upgrade banners / pro-lock overlays */
            '[data-pro],[data-locked],[data-upgrade],[class*="ProBadge"],',
            '[class*="pro-badge"],[class*="upgrade-banner"],[class*="paywall"],',
            '[class*="ProLock"],[class*="pro-lock"],[class*="PremiumBadge"],',
            '[class*="premium-badge"]{display:none!important}',
            /* remove blur/grayscale from locked content */
            '[class*="blurred"],[class*="locked"],[class*="restricted"],',
            '[class*="blur-"],[class*="ProGate"],[class*="pro-gate"]',
            '{filter:none!important;pointer-events:auto!important;',
            'user-select:auto!important;opacity:1!important}',
            /* ensure disabled buttons from free plan are clickable */
            '[data-pro-required],[disabled][class*="pro"]{',
            'pointer-events:auto!important;opacity:1!important;cursor:pointer!important}',
        ].join('');
        (document.head || document.documentElement).appendChild(s);
        log('[PRO] CSS overlay removal injected');
    }

    /* ══════════════════════════════════════════════════════════════════
       IDEAL FACE — v8.0 unchanged
    ══════════════════════════════════════════════════════════════════ */
    const IDEAL_FACE = {
         10:[0.500,0.150], 152:[0.500,0.850], 234:[0.228,0.520], 454:[0.772,0.520],
         33:[0.343,0.406], 133:[0.428,0.400], 159:[0.386,0.392], 145:[0.386,0.414],
        362:[0.572,0.406], 263:[0.657,0.394], 386:[0.614,0.389], 374:[0.614,0.411],
          0:[0.500,0.619],
        172:[0.262,0.760], 397:[0.738,0.760], 150:[0.268,0.740], 379:[0.732,0.740],
        171:[0.270,0.750], 396:[0.730,0.750],  70:[0.320,0.360], 300:[0.680,0.360],
         63:[0.335,0.355], 293:[0.665,0.355], 105:[0.380,0.350], 334:[0.620,0.350],
         46:[0.300,0.370], 276:[0.700,0.370], 116:[0.295,0.430], 345:[0.705,0.430],
        123:[0.340,0.430], 352:[0.660,0.430],  50:[0.305,0.380], 280:[0.695,0.380],
        187:[0.260,0.570], 411:[0.740,0.570], 132:[0.345,0.460], 361:[0.655,0.460],
        174:[0.275,0.680], 399:[0.725,0.680], 136:[0.285,0.720], 365:[0.715,0.720],
        148:[0.265,0.730], 377:[0.735,0.730], 176:[0.275,0.710], 401:[0.725,0.710],
         58:[0.290,0.680], 288:[0.710,0.680],
    };

    /* Layer 1 */
    const _JS = JSON.stringify;
    JSON.stringify = function (v, r, s) {
        if (CFG.scoreSpoof && v && typeof v==='object' && v.type==='SCAN_STATE' && v.payload) {
            v = { type:'SCAN_STATE', payload:{ ...v.payload, overall:dynGet(),
                isFaceStraight:true, faceStatus:'perfect', scoringConfidence:1.0, scoringWarnings:[] }};
            pktCount++; log('[P2P] SCAN_STATE → ' + dynGet());
        }
        return _JS(v, r, s);
    };

    /* Layer 4 — binary frame injection
       Server formula:  overall = round(10 × clamp(Z × X, 1.1, 10)) / 10
         Z  = scoring result from landmarks  (IDEAL_FACE gives Z ≈ 9.976)
         X  = quality byte / 255

       So to hit target T:  quality = round( T / 9.976 × 255 )
       This lets us produce ANY score 1.1–10.0 just by tuning the quality byte
       while keeping IDEAL_FACE landmarks (all metric sub-scores stay 10). */
    const IDEAL_Z = 9.976;   // Z produced by IDEAL_FACE — verified mathematically

    function scoreToQuality(target) {
        // clamp to server's valid range [1.1, 10]
        const t = Math.max(1.1, Math.min(10, target));
        return Math.max(1, Math.min(255, Math.round((t / IDEAL_Z) * 255)));
    }

    const _sign = crypto.subtle.sign.bind(crypto.subtle);
    Object.defineProperty(crypto.subtle, 'sign', { configurable:true, writable:true,
        value: async function sign(alg, key, data) {
            const nm = typeof alg==='string' ? alg : alg && alg.name;
            if (CFG.scoreSpoof && nm==='HMAC' && data instanceof ArrayBuffer && data.byteLength>=17) {
                const v = new DataView(data);
                if (v.getUint8(0)===4) {
                    const target = dynGet();                         // use current score (static or dynamic)
                    const qByte  = scoreToQuality(target);          // ← quality controls the final score
                    v.setUint8(15, qByte);                          // quality byte
                    v.setUint8(16, 2);                              // faceStatus = "perfect"
                    v.setFloat32(9, 1.0, true);                     // aspectRatio = 1.0
                    const n = v.getUint16(13, true);
                    if (data.byteLength >= 17+n*8) {
                        // Inject IDEAL_FACE landmarks (maximises Z so quality byte has full control)
                        for (const [idx,[x,y]] of Object.entries(IDEAL_FACE)) {
                            const i=parseInt(idx);
                            if (i<n) { v.setFloat32(17+i*8,x,true); v.setFloat32(17+i*8+4,y,true); }
                        }
                        pktCount++;
                        log('[!] frame  target=' + target.toFixed(1) + '  q=' + qByte + '  lm=' + n
                            + '  → ~' + (Math.round(IDEAL_Z*(qByte/255)*10)/10).toFixed(1));
                    }
                }
            }
            return _sign(alg, key, data);
        }
    });

    /* Flood */
    const _sendMap = new WeakMap();
    function startFlood() {
        if (floodTimer) return; log('[FLOOD] started 1ms');
        floodTimer = setInterval(function(){
            if (!CFG.scoreSpoof||!hookedWsRef||hookedWsRef.readyState!==WebSocket.OPEN) return;
            const fn=_sendMap.get(hookedWsRef); if(fn) fn(_JS({type:'score_update',payload:{score:dynGet()}}));
        },1);
    }
    function stopFlood() { if(floodTimer){clearInterval(floodTimer);floodTimer=null;log('[FLOOD] stopped');} }

    function readPkt(raw) {
        if (typeof raw!=='string') return null;
        try { const o=JSON.parse(raw); if(o&&typeof o.type==='string') return o; } catch(_){}
        return null;
    }
    const SCORE_FIELDS = ['score','self_score','final_score','user_score','my_score','overall','rating','playerScore','p1_score','p2_score'];

    function injectScoreFields(obj) {
        if (!obj || typeof obj !== 'object') return false;
        let ch = false;
        SCORE_FIELDS.forEach(function(f) {
            if (f in obj && typeof obj[f] === 'number' && obj[f] >= 0) { obj[f] = dynGet(); ch = true; }
        });
        return ch;
    }

    function patchOut(raw) {
        if (!CFG.scoreSpoof) return raw;
        // Try any JSON message, not just type-keyed ones
        let p; try { p = JSON.parse(raw); if (!p||typeof p!=='object') p=null; } catch(_){ p=null; }
        if (!p) return raw;

        // Known types — explicit handling
        if (p.type==='score_update'  && p.payload) { p.payload.score=dynGet(); pktCount++; return _JS(p); }
        if (p.type==='score_submit'  && p.payload) {
            p.payload.self_score=dynGet(); p.payload.score=dynGet(); pktCount++;
            log('[->WS] score_submit → '+dynGet()); return _JS(p);
        }
        if ((p.type==='SCAN_STATE'||p.type==='scan_state') && p.payload) {
            p.payload.overall=dynGet(); p.payload.score=dynGet();
            p.payload.isFaceStraight=true; p.payload.faceStatus='perfect';
            p.payload.scoringConfidence=1.0; p.payload.scoringWarnings=[];
            pktCount++; return _JS(p);
        }
        if (p.type==='final_score' && p.payload) {
            injectScoreFields(p.payload); pktCount++;
            log('[->WS] final_score → '+dynGet()); return _JS(p);
        }
        if (p.type==='round_end' && p.payload) {
            injectScoreFields(p.payload); pktCount++;
            log('[->WS] round_end patched → '+dynGet()); return _JS(p);
        }

        // Generic: scan payload and root for any numeric score field
        let ch = false;
        if (p.payload && typeof p.payload==='object') ch = injectScoreFields(p.payload) || ch;
        ch = injectScoreFields(p) || ch;
        if (ch) { pktCount++; log('[->WS] score fields patched type='+(p.type||'?')+' → '+dynGet()); return _JS(p); }
        return raw;
    }
    // Sentinel — returned by patchIn to tell the WS handler to DROP the message
    const WS_DROP = '__rsw_drop__';

    function patchIn(raw) {
        if (typeof raw!=='string') return raw;

        // Parse BEFORE readPkt so we capture from Supabase Realtime messages
        // (which use "event" not "type" and would otherwise be skipped by readPkt)
        try { const j=JSON.parse(raw); if(j&&typeof j==='object') captureOpponent(j,0); } catch(_){}

        const p=readPkt(raw); if(!p) return raw;

        // ── BAN BYPASS: drop ban/kick/suspend messages from server ──────────
        if (SPF.ban) {
            const banTypes = ['ban','banned','kick','kicked','suspend','suspended','restrict','restricted','shadow_ban'];
            if (banTypes.includes(p.type) || (p.payload && isBanMsg(_JS(p.payload)))) {
                log('[BAN] WS ban msg dropped: type=' + p.type);
                return WS_DROP;
            }
            // Also strip ban fields from any incoming payload
            if (p.payload && typeof p.payload === 'object') deepPatchBan(p.payload, 0);
        }

        if (!p.payload) return raw;

        if (p.type==='live_score') {
            log('[<-WS] live_score you='+(p.payload.your_score!=null?p.payload.your_score.toFixed(2):'?')
               +' opp='+(p.payload.opponent_score!=null?p.payload.opponent_score.toFixed(2):'?'));
            let ch=false;
            if (CFG.liveSpoof)  { p.payload.your_score=dynGet(); ch=true; }
            if (CFG.enemySpoof) { p.payload.opponent_score=CFG.enemyVal; ch=true; }
            return ch?_JS(p):raw;
        }
        if (p.type==='match_result') {
            log('[<-WS] match_result result='+p.payload.result
               +' you='+(p.payload.your_score!=null?p.payload.your_score.toFixed(1):'?')
               +' opp='+(p.payload.opponent_score!=null?p.payload.opponent_score.toFixed(1):'?'));
            let ch=false;
            if (CFG.displayWin) { p.payload.result='win'; p.payload.your_score=p.payload.p1_score=dynGet(); ch=true; }
            if (CFG.enemySpoof) { p.payload.opponent_score=p.payload.p2_score=CFG.enemyVal; ch=true; }
            if (ch){pktCount++;return _JS(p);} return raw;
        }
        return raw;
    }

    /* WebSocket hook */
    const _WS=window.WebSocket;
    window.WebSocket=function(url,proto){
        const ws=proto?new _WS(url,proto):new _WS(url);
        if(url&&url.includes('omoggle.com')){
            hookedWsRef=ws;
            // Store URL + proto for ban-reconnect
            BAN_STATE.lastUrl=url; BAN_STATE.lastProto=proto||undefined;
            log('[WS] '+url.split('?')[0]);
            if(CFG.scoreSpoof)startFlood();
        }
        const os=ws.send.bind(ws); _sendMap.set(ws,os);
        ws.send=function(d){ if(typeof d==='string')d=patchOut(d); os(d); };
        let _om=null;
        Object.defineProperty(ws,'onmessage',{configurable:true,get:()=>_om,set:fn=>_om=fn});
        ws.addEventListener('message',function(e){
            if(!_om)return;
            if(typeof e.data==='string'){
                const p=patchIn(e.data);
                if(p===WS_DROP)return;                 // drop ban message
                if(p!==e.data){_om.call(ws,new MessageEvent('message',{data:p,origin:e.origin}));return;}
            }
            _om.call(ws,e);
        });
        const oa=ws.addEventListener.bind(ws);
        ws.addEventListener=function(type,fn,opts){
            if(type==='message'){oa('message',function(e){
                if(typeof e.data!=='string'){fn.call(ws,e);return;}
                const p=patchIn(e.data);
                if(p===WS_DROP)return;                 // drop ban message
                fn.call(ws,p!==e.data?new MessageEvent('message',{data:p,origin:e.origin}):e);
            },opts);}else{oa(type,fn,opts);}
        };
        ws.addEventListener('close',function(e){
            if(hookedWsRef===ws){hookedWsRef=null;stopFlood();}
            // BAN auto-reconnect: if close reason mentions a ban, reconnect
            if(SPF.ban && BAN_STATE.lastUrl){
                const reason = (e.reason||'').toLowerCase();
                const code   = e.code;
                if(isBanMsg(reason) || code===4003 || code===4429){
                    log('[BAN] WS closed — ban detected (code='+code+' reason="'+e.reason+'") — reconnecting in 1.5s');
                    clearTimeout(BAN_STATE.reconnectTimer);
                    BAN_STATE.reconnectTimer=setTimeout(function(){
                        log('[BAN] reconnecting → '+BAN_STATE.lastUrl.split('?')[0]);
                        window.WebSocket(BAN_STATE.lastUrl, BAN_STATE.lastProto);
                    },1500);
                }
            }
        });
        return ws;
    };
    Object.assign(window.WebSocket,_WS); window.WebSocket.prototype=_WS.prototype;

    /* ══════════════════════════════════════════════════════════════════
       UI  —  RSWARE  900×900  black/grey/white
    ══════════════════════════════════════════════════════════════════ */
    const CSS = `
#rsw{position:fixed;top:30px;left:30px;width:900px;height:900px;
     background:#0c0c0c;border:1px solid #2a2a2a;border-radius:6px;
     box-shadow:0 24px 80px rgba(0,0,0,.95);z-index:2147483647;
     font-family:'Courier New',monospace;color:#d0d0d0;user-select:none;
     display:flex;flex-direction:column;overflow:hidden}
#rsw.h{display:none}

/* ── title bar ── */
#rsw-bar{background:#111;border-bottom:1px solid #222;height:52px;
         display:flex;align-items:center;padding:0 18px;gap:14px;cursor:move;flex-shrink:0}
#rsw-logo{font-family:'Arial Black',Arial,sans-serif;font-size:22px;font-weight:900;
          color:#fff;letter-spacing:5px}
#rsw-ver{font-size:9px;color:#444;letter-spacing:1px;margin-top:2px}
#rsw-spacer{flex:1}
#rsw-status{font-size:9px;color:#444;letter-spacing:2px}
.rsw-pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;
          background:#161616;border:1px solid #2a2a2a;border-radius:3px;
          font-size:9px;letter-spacing:1px;color:#555}
.rsw-pill.on{border-color:#3a3a3a;color:#0f0}
.rsw-pill.on::before{content:'';width:6px;height:6px;border-radius:50%;background:#0f0;display:inline-block}
#rsw-close{width:24px;height:24px;background:#1a1a1a;border:1px solid #333;
           color:#666;font-size:14px;cursor:pointer;border-radius:3px;
           display:flex;align-items:center;justify-content:center;flex-shrink:0}
#rsw-close:hover{background:#200;border-color:#f44;color:#f44}

/* ── tab bar ── */
#rsw-tabs{display:flex;background:#0f0f0f;border-bottom:1px solid #1e1e1e;
          flex-shrink:0;height:40px}
.rsw-tab{flex:1;display:flex;align-items:center;justify-content:center;
         font-size:9px;font-weight:900;letter-spacing:2px;color:#444;
         cursor:pointer;border-right:1px solid #1a1a1a;transition:all .12s;
         text-transform:uppercase}
.rsw-tab:last-child{border-right:none}
.rsw-tab:hover{color:#888;background:#141414}
.rsw-tab.act{color:#fff;background:#161616;border-bottom:2px solid #fff}

/* ── content ── */
#rsw-content{flex:1;overflow-y:auto;overflow-x:hidden;background:#0c0c0c}
#rsw-content::-webkit-scrollbar{width:3px}
#rsw-content::-webkit-scrollbar-thumb{background:#222}

/* ── panels ── */
.rsw-pnl{display:none;padding:18px;height:100%;box-sizing:border-box}
.rsw-pnl.act{display:block}

/* ── sections ── */
.rsw-sec{background:#111;border:1px solid #1e1e1e;border-radius:4px;
         padding:14px 16px;margin-bottom:12px}
.rsw-sec-hdr{font-size:8px;font-weight:900;letter-spacing:3px;color:#444;
             text-transform:uppercase;margin-bottom:12px;display:flex;
             align-items:center;justify-content:space-between}
.rsw-sec-hdr span{font-size:8px;color:#2a2a2a;font-weight:normal;letter-spacing:1px}

/* ── grid ── */
.rsw-grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}

/* ── rows ── */
.rsw-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.rsw-row:last-child{margin-bottom:0}
.rsw-lbl{font-size:10px;color:#666;width:130px;flex-shrink:0;letter-spacing:.5px}
.rsw-inp{flex:1;background:#161616;border:1px solid #2a2a2a;color:#fff;
         padding:8px 10px;font-size:12px;font-family:'Courier New',monospace;
         outline:none;border-radius:2px;min-width:0}
.rsw-inp:focus{border-color:#444}
.rsw-sel{flex:1;background:#161616;border:1px solid #2a2a2a;color:#fff;
         padding:8px 10px;font-size:11px;font-family:'Courier New',monospace;
         outline:none;cursor:pointer;border-radius:2px}

/* ── buttons ── */
.rsw-btn{background:#161616;border:1px solid #333;color:#aaa;
         padding:8px 16px;font-family:'Courier New',monospace;font-size:10px;
         letter-spacing:1px;cursor:pointer;border-radius:2px;
         transition:all .12s;white-space:nowrap}
.rsw-btn:hover{border-color:#555;color:#fff;background:#1e1e1e}
.rsw-btn.pri{background:#1a1a1a;border-color:#888;color:#fff}
.rsw-btn.pri:hover{background:#fff;color:#000;border-color:#fff}
.rsw-btn.grn{border-color:#2a4a2a;color:#4f4}
.rsw-btn.grn:hover{background:#0a1a0a;border-color:#4f4}
.rsw-btn.red{border-color:#4a1a1a;color:#f44}
.rsw-btn.red:hover{background:#1a0a0a;border-color:#f44}
.rsw-btn.act{background:#0a200a;border-color:#0f0;color:#0f0}
.rsw-btn.sm{padding:5px 10px;font-size:9px}

/* ── big attack buttons ── */
#rsw-atk-btns{display:grid;grid-template-columns:1fr 1fr;gap:10px}
#rsw-atk-btns .rsw-btn{padding:16px;text-align:center;font-size:12px;
                        font-weight:900;letter-spacing:3px;font-family:'Arial Black',Arial,sans-serif}

/* ── toggle rows ── */
.rsw-tr{display:flex;align-items:center;justify-content:space-between;
        padding:9px 0;border-bottom:1px solid #161616}
.rsw-tr:last-child{border-bottom:none}
.rsw-tr-lbl{font-size:10px;color:#aaa;letter-spacing:.5px}
.rsw-tr-sub{font-size:8px;color:#333;display:block;margin-top:2px}
.rsw-tog{width:38px;height:18px;background:#1a1a1a;border:1px solid #2a2a2a;
         border-radius:9px;position:relative;cursor:pointer;flex-shrink:0;
         transition:all .15s}
.rsw-tog.on{background:#0d200d;border-color:#3a5a3a}
.rsw-tog::after{content:'';position:absolute;top:3px;left:3px;width:10px;height:10px;
                background:#3a3a3a;border-radius:50%;transition:all .15s}
.rsw-tog.on::after{left:23px;background:#0f0}

/* ── camera preview ── */
#rsw-cam-wrap{border:1px solid #1e1e1e;background:#000;margin-bottom:10px;
              display:flex;align-items:center;justify-content:center;height:240px}
#rsw-cam-prev{width:320px;height:240px;display:block}

/* ── log ── */
#rsw-log{background:#060606;border:1px solid #161616;border-radius:2px;
         padding:12px;font-size:10px;color:#0f0;height:750px;
         overflow-y:auto;white-space:pre;line-height:1.7;
         font-family:'Courier New',monospace}
#rsw-log::-webkit-scrollbar{width:3px}
#rsw-log::-webkit-scrollbar-thumb{background:#1a1a1a}

/* ── show button ── */
#rsw-show{position:fixed;top:30px;left:30px;z-index:2147483646;
          background:#111;border:1px solid #333;color:#fff;padding:7px 18px;
          font-family:'Arial Black',Arial,sans-serif;font-size:12px;
          font-weight:900;letter-spacing:4px;cursor:pointer;display:none;
          border-radius:3px;box-shadow:0 4px 16px rgba(0,0,0,.8)}

/* ── dyn row ── */
#rsw-dyn-row{display:flex;align-items:center;gap:8px;flex-wrap:nowrap}
.dnum{width:58px;background:#161616;border:1px solid #2a2a2a;color:#fff;
      padding:7px 8px;font-size:11px;font-family:'Courier New',monospace;
      outline:none;border-radius:2px}
#rsw-dyn-live-row{margin-top:8px;font-size:10px;color:#555;letter-spacing:.5px}
#rsw-dyn-live{color:#fff;font-weight:900}

/* score display */
#rsw-cur-score{font-size:28px;font-weight:900;color:#fff;text-align:center;
               letter-spacing:2px;padding:8px 0;font-family:'Arial Black',Arial,sans-serif}
#rsw-cur-score span{font-size:12px;color:#444;font-weight:normal;display:block;
                    letter-spacing:3px;margin-top:2px}

/* country flag pill */
.rsw-flag{font-size:11px;margin-right:5px}
`;

    function buildUI() {
        if (document.getElementById('rsw')) return;
        const st = document.createElement('style'); st.textContent = CSS;
        document.head.appendChild(st);

        const showBtn = document.createElement('button');
        showBtn.id = 'rsw-show'; showBtn.textContent = 'RSWARE';
        document.body.appendChild(showBtn);

        /* ── build panel ── */
        const root = document.createElement('div'); root.id = 'rsw';

        /* title bar */
        root.innerHTML = `
<div id="rsw-bar">
  <div id="rsw-logo">RSWARE</div>
  <div id="rsw-ver">v9.3</div>
  <div id="rsw-spacer"></div>
  <div class="rsw-pill" id="rsw-atk-pill">OFFLINE</div>
  <div id="rsw-conf-status" style="font-size:8px;color:#333;letter-spacing:1px;margin-left:8px">AUTO</div>
  <button id="rsw-close" style="margin-left:10px">✕</button>
</div>

<div id="rsw-tabs">
  <div class="rsw-tab act" data-tab="score">SCORE</div>
  <div class="rsw-tab" data-tab="camera">CAMERA</div>
  <div class="rsw-tab" data-tab="audio">AUDIO</div>
  <div class="rsw-tab" data-tab="spoof">SPOOF</div>
  <div class="rsw-tab" data-tab="report">REPORT</div>
  <div class="rsw-tab" data-tab="config">CONFIG</div>
  <div class="rsw-tab" data-tab="log">LOG</div>
</div>

<div id="rsw-content">

  <!-- ═══ SCORE TAB ═══ -->
  <div class="rsw-pnl act" id="pnl-score">
    <div class="rsw-grid2">

      <!-- LEFT COLUMN -->
      <div>

        <!-- mode switch -->
        <div class="rsw-sec">
          <div class="rsw-sec-hdr">SCORE MODE</div>
          <div style="display:flex;gap:8px;margin-bottom:14px">
            <button id="btn-static" class="rsw-btn pri" style="flex:1;padding:12px;font-size:11px;letter-spacing:2px;font-weight:900">STATIC</button>
            <button id="btn-dynamic" class="rsw-btn" style="flex:1;padding:12px;font-size:11px;letter-spacing:2px;font-weight:900">DYNAMIC</button>
          </div>

          <!-- STATIC view -->
          <div id="rsw-static-view">
            <div class="rsw-row">
              <span class="rsw-lbl">FIXED SCORE</span>
              <input id="rsw-val" class="rsw-inp" type="number" value="10" min="0" step="0.1"
                     style="font-size:22px;font-weight:900;color:#fff;text-align:center"/>
            </div>
            <div id="rsw-val-hint" style="font-size:9px;color:#444;text-align:right;margin-top:4px;letter-spacing:.5px">
              server caps at 10 from frame scoring — WS layers send whatever you set
            </div>
          </div>

          <!-- DYNAMIC view (hidden by default) -->
          <div id="rsw-dyn-view" style="display:none">
            <div style="display:flex;gap:8px;margin-bottom:10px">
              <div style="flex:1">
                <div style="font-size:8px;color:#444;letter-spacing:2px;margin-bottom:5px">MIN</div>
                <input class="rsw-inp" id="dyn-min" type="number" value="7.0" min="0" step="0.1"
                       style="text-align:center;font-size:16px;font-weight:900;color:#fff;width:100%;box-sizing:border-box"/>
              </div>
              <div style="flex:1">
                <div style="font-size:8px;color:#444;letter-spacing:2px;margin-bottom:5px">MAX</div>
                <input class="rsw-inp" id="dyn-max" type="number" value="10.0" min="0" step="0.1"
                       style="text-align:center;font-size:16px;font-weight:900;color:#fff;width:100%;box-sizing:border-box"/>
              </div>
              <div style="width:80px">
                <div style="font-size:8px;color:#444;letter-spacing:2px;margin-bottom:5px">EVERY (SEC)</div>
                <input class="rsw-inp" id="dyn-int" type="number" value="3" min="1" step="1"
                       style="text-align:center;font-size:16px;font-weight:900;color:#fff;width:100%;box-sizing:border-box"/>
              </div>
            </div>
            <div style="background:#0d0d0d;border:1px solid #1a1a1a;padding:10px 14px;display:flex;align-items:center;justify-content:space-between">
              <span style="font-size:9px;color:#444;letter-spacing:1px">CURRENT VALUE</span>
              <span id="rsw-dyn-live" style="font-size:24px;font-weight:900;color:#fff;font-family:'Arial Black',sans-serif">--</span>
            </div>
          </div>
        </div>

        <!-- big send value display -->
        <div class="rsw-sec" style="background:#0d0d0d;text-align:center;padding:18px">
          <div style="font-size:9px;color:#333;letter-spacing:3px;margin-bottom:6px">SENDING</div>
          <div id="rsw-cur-score" style="font-size:42px;font-weight:900;color:#fff;font-family:'Arial Black',sans-serif;line-height:1">10.0</div>
          <div id="rsw-cur-mode" style="font-size:8px;color:#333;letter-spacing:2px;margin-top:6px">STATIC</div>
        </div>

        <div id="rsw-atk-btns">
          <button id="rsw-send" class="rsw-btn pri">SEND ATTACK</button>
          <button id="rsw-stop" class="rsw-btn red">STOP ATTACK</button>
        </div>
      </div>

      <!-- RIGHT COLUMN -->
      <div>
        <div class="rsw-sec">
          <div class="rsw-sec-hdr">METHOD</div>
          <select id="rsw-method" class="rsw-sel" style="width:100%;margin-bottom:0">
            <option value="all">All Layers (recommended)</option>
            <option value="spoof">Frame Inject only</option>
            <option value="display">Display Win only</option>
          </select>
        </div>

        <div class="rsw-sec">
          <div class="rsw-sec-hdr">ENEMY SCORE</div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">OVERRIDE ENEMY<span class="rsw-tr-sub">Set opponent score in live/result packets</span></span>
            <div class="rsw-tog" id="t-enemy"></div>
          </div>
          <div class="rsw-row" style="margin-top:10px">
            <span class="rsw-lbl">ENEMY VALUE</span>
            <input id="rsw-enemy-val" class="rsw-inp" type="number" value="1.0" min="0" step="0.1"/>
          </div>
        </div>

        <div class="rsw-sec">
          <div class="rsw-sec-hdr">DISPLAY PATCHES</div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">DISPLAY WIN<span class="rsw-tr-sub">Patches match_result to show win</span></span>
            <div class="rsw-tog" id="t-win"></div>
          </div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">LIVE SCORE BAR<span class="rsw-tr-sub">Patches incoming live_score bar</span></span>
            <div class="rsw-tog" id="t-live"></div>
          </div>
        </div>

        <div class="rsw-sec" style="background:#0d0d0d">
          <div class="rsw-sec-hdr">STATS</div>
          <div class="rsw-row">
            <span class="rsw-lbl">FRAMES PATCHED</span>
            <span id="rsw-cnt" style="color:#fff;font-size:18px;font-weight:900">0</span>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">WS STATUS</span>
            <span id="rsw-ws-status" style="color:#444;font-size:10px">not connected</span>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">ATTACK</span>
            <div class="rsw-pill" id="rsw-atk-pill">OFFLINE</div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- ═══ CAMERA TAB ═══ -->
  <div class="rsw-pnl" id="pnl-camera">
    <div class="rsw-grid2">
      <div>
        <div class="rsw-sec">
          <div class="rsw-sec-hdr">CAMERA PREVIEW</div>
          <div id="rsw-cam-wrap">
            <canvas id="rsw-cam-prev" width="320" height="240"></canvas>
          </div>
          <div style="font-size:9px;color:#333;text-align:center;letter-spacing:1px">
            what opponent sees
          </div>
        </div>
      </div>
      <div>
        <div class="rsw-sec">
          <div class="rsw-sec-hdr">CAMERA SPOOF</div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">ENABLE<span class="rsw-tr-sub">Replace webcam with spoofed feed</span></span>
            <div class="rsw-tog" id="t-cam"></div>
          </div>
          <div class="rsw-row" style="margin-top:12px">
            <span class="rsw-lbl">MODE</span>
            <select id="rsw-cam-mode" class="rsw-sel">
              <option value="black">Solid Black</option>
              <option value="color">Custom Color</option>
              <option value="image">Upload Image</option>
            </select>
          </div>
          <div class="rsw-row" id="rsw-cam-color-row" style="display:none">
            <span class="rsw-lbl">COLOR</span>
            <input id="rsw-cam-color" class="rsw-inp" type="color" value="#111111" style="height:36px;padding:2px 4px;cursor:pointer"/>
          </div>
          <div class="rsw-row" id="rsw-cam-img-row" style="display:none">
            <span class="rsw-lbl">IMAGE</span>
            <input id="rsw-cam-file" type="file" accept="image/*" style="display:none"/>
            <button class="rsw-btn" id="rsw-cam-upload">UPLOAD IMAGE</button>
            <span id="rsw-cam-fname" style="font-size:9px;color:#444;margin-left:6px"></span>
          </div>
        </div>

        <div class="rsw-sec" style="background:#0d0d0d">
          <div class="rsw-sec-hdr">NOTE</div>
          <div style="font-size:9px;color:#444;line-height:1.8;letter-spacing:.3px">
            Camera spoof replaces the video stream opponents see.<br>
            For best score results upload a face image so MediaPipe<br>
            can detect landmarks — our hook still injects IDEAL_FACE<br>
            before signing regardless of actual camera content.
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- ═══ AUDIO TAB ═══ -->
  <div class="rsw-pnl" id="pnl-audio">
    <div class="rsw-grid2">
      <div>
        <div class="rsw-sec">
          <div class="rsw-sec-hdr">MIC SPOOF</div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">ENABLE<span class="rsw-tr-sub">Replace mic with silent/custom audio</span></span>
            <div class="rsw-tog" id="t-mic"></div>
          </div>
          <div class="rsw-row" style="margin-top:12px">
            <span class="rsw-lbl">MODE</span>
            <select id="rsw-mic-mode" class="rsw-sel">
              <option value="silent">Silent (mute yourself)</option>
              <option value="tone">Sine Tone</option>
              <option value="noise">White Noise</option>
            </select>
          </div>
          <div class="rsw-row" id="rsw-mic-freq-row">
            <span class="rsw-lbl">FREQUENCY</span>
            <input id="rsw-mic-freq" class="rsw-inp" type="number" value="440" min="20" max="20000" step="10"/>
            <span style="font-size:10px;color:#444">Hz</span>
          </div>
        </div>
      </div>
      <div>
        <div class="rsw-sec" style="background:#0d0d0d">
          <div class="rsw-sec-hdr">INFO</div>
          <div style="font-size:9px;color:#444;line-height:2;letter-spacing:.3px">
            SILENT — opponent hears nothing from you<br>
            TONE   — sends a pure sine wave at chosen Hz<br>
            NOISE  — sends white noise<br><br>
            Takes effect on the NEXT getUserMedia call.<br>
            Refresh or reconnect to apply immediately.
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- ═══ SPOOF TAB ═══ -->
  <div class="rsw-pnl" id="pnl-spoof">
    <div class="rsw-grid2">
      <div>
        <div class="rsw-sec">
          <div class="rsw-sec-hdr">COUNTRY SPOOF</div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">ENABLE<span class="rsw-tr-sub">Spoof navigator.language + geolocation</span></span>
            <div class="rsw-tog" id="t-country"></div>
          </div>
          <div class="rsw-row" style="margin-top:12px">
            <span class="rsw-lbl">COUNTRY</span>
            <select id="rsw-country" class="rsw-sel"></select>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">LANGUAGE TAG</span>
            <span id="rsw-country-lang" style="font-size:11px;color:#fff;letter-spacing:1px">en-US</span>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">TIMEZONE</span>
            <span id="rsw-country-tz" style="font-size:10px;color:#888">America/New_York</span>
          </div>
          <button class="rsw-btn grn" id="rsw-apply-country" style="margin-top:6px">APPLY NOW</button>
        </div>
      </div>
      <div>
        <div class="rsw-sec">
          <div class="rsw-sec-hdr">PRO SPOOF</div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">ENABLE<span class="rsw-tr-sub">4-layer PRO injection (fetch + XHR + localStorage + CSS)</span></span>
            <div class="rsw-tog" id="t-pro"></div>
          </div>

          <div style="margin-top:12px;background:#0a0a0a;border:1px solid #1a1a1a;padding:10px 12px">
            <div style="font-size:8px;color:#444;letter-spacing:2px;margin-bottom:8px">HOW IT WORKS</div>
            <div style="font-size:9px;line-height:2;color:#333">
              <span style="color:#555">LAYER 1</span> — fetch() hook patches all JSON from Supabase<br>
              <span style="color:#555">LAYER 2</span> — XHR hook patches XMLHttpRequest responses<br>
              <span style="color:#555">LAYER 3</span> — localStorage hook injects PRO into cached session<br>
              <span style="color:#555">LAYER 4</span> — CSS removes paywall overlays / unlock blurred UI
            </div>
          </div>

          <div style="margin-top:10px;background:#0a0a0a;border:1px solid #1a1a1a;padding:8px 12px;font-size:9px;color:#444;line-height:1.8">
            Fields injected: <span style="color:#888">is_pro=true  plan="pro"  tier="pro"<br>
            isPro=true  hasPro=true  subscription.active=true</span>
          </div>

          <div style="display:flex;gap:8px;margin-top:10px">
            <button class="rsw-btn grn" id="rsw-pro-reload" style="flex:1;font-weight:900;letter-spacing:1px">ENABLE + RELOAD PAGE</button>
          </div>
          <div style="font-size:8px;color:#333;margin-top:6px;text-align:center;letter-spacing:.5px">
            reload applies the session patch before site JS reads it
          </div>
        </div>

        <div class="rsw-sec" style="background:#0d0d0d">
          <div class="rsw-sec-hdr">BROWSER FINGERPRINT</div>
          <div class="rsw-row">
            <span class="rsw-lbl">LANGUAGE</span>
            <span id="fp-lang" style="font-size:10px;color:#888">--</span>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">TIMEZONE</span>
            <span id="fp-tz" style="font-size:10px;color:#888">--</span>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">PLATFORM</span>
            <span id="fp-plat" style="font-size:10px;color:#888">--</span>
          </div>
        </div>
      </div>

      <!-- BAN BYPASS section — full-width below the 2-col grid -->
      <div style="grid-column:1/-1">
        <div class="rsw-sec" style="border-color:#2a1a1a">
          <div class="rsw-sec-hdr" style="color:#f44">BAN BYPASS
            <span style="color:#2a1a1a;font-size:8px;letter-spacing:1px">INTERCEPT + DROP ALL BAN SIGNALS</span>
          </div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">ENABLE<span class="rsw-tr-sub">Strip ban flags from API + WS + localStorage; spoof 403s; auto-reconnect</span></span>
            <div class="rsw-tog" id="t-ban"></div>
          </div>

          <div style="margin-top:12px;background:#0a0a0a;border:1px solid #1e1010;padding:10px 12px">
            <div style="font-size:8px;color:#444;letter-spacing:2px;margin-bottom:8px">LAYERS</div>
            <div style="font-size:9px;line-height:2;color:#333">
              <span style="color:#555">LAYER 1</span> — fetch() 403 intercept: if ban msg in body → fake 200 OK<br>
              <span style="color:#555">LAYER 2</span> — fetch + XHR JSON: strips banned/is_banned/suspended/ban_expires…<br>
              <span style="color:#555">LAYER 3</span> — localStorage Supabase session: wipes all ban fields on every read<br>
              <span style="color:#555">LAYER 4</span> — WebSocket: drops ban/kick/suspend messages before JS sees them<br>
              <span style="color:#555">LAYER 5</span> — WebSocket auto-reconnect if server closes with ban reason/code
            </div>
          </div>

          <div style="display:flex;gap:8px;margin-top:10px">
            <button class="rsw-btn red" id="rsw-ban-reconnect" style="flex:1;font-size:9px;letter-spacing:1px">FORCE RECONNECT NOW</button>
            <button class="rsw-btn grn" id="rsw-ban-reload" style="flex:1;font-size:9px;letter-spacing:1px">ENABLE + RELOAD PAGE</button>
          </div>
          <div style="font-size:8px;color:#333;margin-top:8px;line-height:1.8">
            If you see a ban screen: enable above → click RELOAD. The ban flag is wiped from your cached<br>
            Supabase session before the site reads it, so the ban wall never renders.
          </div>
        </div>
      </div>

    </div>
  </div>

  <!-- ═══ REPORT TAB ═══ -->
  <div class="rsw-pnl" id="pnl-report">
    <div class="rsw-grid2">
      <div>

        <!-- CURRENT OPPONENT -->
        <div class="rsw-sec" style="border-color:#2a3a2a">
          <div class="rsw-sec-hdr" style="color:#4f4">CURRENT OPPONENT
            <span>auto-detected from match data</span>
          </div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">NAME<span class="rsw-tr-sub">Captured from WS / API match messages</span></span>
            <span id="rsw-rpt-opp-val" style="font-size:12px;font-weight:900;color:#444;letter-spacing:1px">--</span>
          </div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">USER ID<span class="rsw-tr-sub">Raw opponent_id / player2_id field</span></span>
            <span id="rsw-rpt-opp-id" style="font-size:9px;color:#555;letter-spacing:.5px">--</span>
          </div>
          <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px">
            <button class="rsw-btn grn" id="rsw-rpt-use-opp" style="font-weight:900;letter-spacing:1px">USE AS TARGET ↓</button>
            <button class="rsw-btn sm" id="rsw-rpt-scan-dom">SCAN PAGE DOM</button>
          </div>
          <div style="font-size:8px;color:#333;margin-top:8px;line-height:1.8">
            Opponent is captured automatically from every WS/API message<br>
            the moment you enter a match. Click USE AS TARGET to fill below.<br>
            SCAN PAGE DOM tries to find the name in visible page elements.
          </div>
        </div>

        <!-- ENDPOINT CAPTURE -->
        <div class="rsw-sec">
          <div class="rsw-sec-hdr">ENDPOINT CAPTURE
            <span>click native report button once to grab URL</span>
          </div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">ENDPOINT<span class="rsw-tr-sub">Recorded from your last report click</span></span>
            <span id="rsw-rpt-captured" style="font-size:9px;color:#444;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">waiting…</span>
          </div>
          <div class="rsw-tr">
            <span class="rsw-tr-lbl">PAYLOAD<span class="rsw-tr-sub">Body JSON from last report</span></span>
            <span id="rsw-rpt-body" style="font-size:9px;color:#333;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">--</span>
          </div>
          <button class="rsw-btn sm" id="rsw-rpt-clear-cap" style="margin-top:8px">CLEAR CAPTURE</button>
        </div>

      </div>
      <div>

        <!-- TARGET + CONFIG -->
        <div class="rsw-sec">
          <div class="rsw-sec-hdr">TARGET</div>
          <div class="rsw-row">
            <span class="rsw-lbl">USER ID / NAME</span>
            <input id="rsw-rpt-target" class="rsw-inp" type="text" placeholder="auto-filled from opponent"/>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">REASON</span>
            <select id="rsw-rpt-reason" class="rsw-sel">
              <option value="spam">Spam</option>
              <option value="harassment">Harassment</option>
              <option value="inappropriate">Inappropriate Content</option>
              <option value="cheating">Cheating / Hacking</option>
              <option value="hate_speech">Hate Speech</option>
              <option value="impersonation">Impersonation</option>
              <option value="other">Other</option>
            </select>
          </div>
        </div>

        <div class="rsw-sec">
          <div class="rsw-sec-hdr">FLOOD CONFIG</div>
          <div class="rsw-row">
            <span class="rsw-lbl">REPORT COUNT</span>
            <input id="rsw-rpt-count" class="rsw-inp" type="number" value="50" min="1" max="9999" style="font-size:18px;font-weight:900;text-align:center"/>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">DELAY (MS)</span>
            <input id="rsw-rpt-delay" class="rsw-inp" type="number" value="400" min="50" max="60000"/>
            <span style="font-size:9px;color:#444">between each</span>
          </div>
          <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px">
            <button class="rsw-btn pri" id="rsw-rpt-start" style="padding:14px;font-size:11px;letter-spacing:2px;font-weight:900">MASS REPORT</button>
            <button class="rsw-btn red" id="rsw-rpt-stop" style="padding:14px;font-size:11px;letter-spacing:2px;font-weight:900">STOP</button>
          </div>
        </div>

        <div class="rsw-sec" style="background:#0d0d0d">
          <div class="rsw-sec-hdr">STATUS</div>
          <div class="rsw-row">
            <span class="rsw-lbl">STATE</span>
            <div class="rsw-pill" id="rsw-rpt-pill">IDLE</div>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">REPORTS SENT</span>
            <span id="rsw-rpt-sent" style="color:#fff;font-size:22px;font-weight:900;font-family:'Arial Black',sans-serif">0</span>
            <span id="rsw-rpt-total" style="color:#333;font-size:11px;margin-left:6px">/ 0</span>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">LAST HTTP</span>
            <span id="rsw-rpt-resp" style="color:#888;font-size:11px">--</span>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">MODE</span>
            <span id="rsw-rpt-mode" style="color:#555;font-size:9px;letter-spacing:1px">--</span>
          </div>
        </div>

      </div>
    </div>
  </div>

  <!-- ═══ CONFIG TAB ═══ -->
  <div class="rsw-pnl" id="pnl-config">
    <div class="rsw-grid2">
      <div>
        <div class="rsw-sec">
          <div class="rsw-sec-hdr">SAVED SETTINGS</div>
          <div style="font-size:9px;color:#444;line-height:1.9;margin-bottom:12px">
            All toggles and values auto-save 0.7s after any change.<br>
            Settings survive page refreshes — restored before page JS runs.<br>
            Storage key: <span style="color:#666;letter-spacing:0">rsware_omoggle_v93</span>
          </div>
          <div class="rsw-row">
            <span class="rsw-lbl">STATUS</span>
            <span id="rsw-conf-status2" style="font-size:10px;color:#444;letter-spacing:1px">--</span>
          </div>
          <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px">
            <button class="rsw-btn grn" id="rsw-conf-save">SAVE NOW</button>
            <button class="rsw-btn red" id="rsw-conf-clear">CLEAR SAVED</button>
          </div>
        </div>
        <div class="rsw-sec" style="background:#0d0d0d">
          <div class="rsw-sec-hdr">CURRENT CONFIG SNAPSHOT</div>
          <pre id="rsw-conf-preview" style="font-size:9px;color:#555;line-height:1.7;margin:0;overflow:auto;max-height:340px;white-space:pre-wrap"></pre>
        </div>
      </div>
      <div>
        <div class="rsw-sec" style="background:#0a0a0a">
          <div class="rsw-sec-hdr">WHAT IS SAVED</div>
          <div style="font-size:9px;color:#444;line-height:2.1">
            <span style="color:#666">SCORE MODE</span> static/dynamic, value, min/max<br>
            <span style="color:#666">DISPLAY WIN</span> on/off<br>
            <span style="color:#666">LIVE SCORE BAR</span> on/off<br>
            <span style="color:#666">ENEMY SCORE</span> on/off + override value<br>
            <span style="color:#666">CAMERA</span> enabled, mode, color<br>
            <span style="color:#666">MIC</span> enabled, mode, frequency<br>
            <span style="color:#666">COUNTRY SPOOF</span> on/off + country code<br>
            <span style="color:#666">PRO SPOOF</span> on/off<br>
            <span style="color:#666">BAN BYPASS</span> on/off<br><br>
            Camera images cannot be saved (binary data).<br>
            Re-upload after each page reload.
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- ═══ LOG TAB ═══ -->
  <div class="rsw-pnl" id="pnl-log">
    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
      <span style="font-size:9px;color:#444;letter-spacing:2px">ACTIVITY LOG — frames patched: <span id="rsw-cnt2" style="color:#fff">0</span></span>
      <button class="rsw-btn sm" id="rsw-log-clear">CLEAR</button>
    </div>
    <div id="rsw-log">-- RSWARE v9.0 ready --</div>
  </div>

</div>
`;
        document.body.appendChild(root);

        /* ── populate country select ── */
        const csel = document.getElementById('rsw-country');
        Object.entries(COUNTRIES).forEach(function([code, c]) {
            const o = document.createElement('option');
            o.value = code; o.textContent = c.name;
            if (code === 'US') o.selected = true;
            csel.appendChild(o);
        });

        /* update fingerprint display */
        document.getElementById('fp-lang').textContent  = navigator.language || '--';
        document.getElementById('fp-tz').textContent    = Intl.DateTimeFormat().resolvedOptions().timeZone || '--';
        document.getElementById('fp-plat').textContent  = navigator.platform || '--';

        /* ════════ event wiring ════════ */

        /* close / show */
        document.getElementById('rsw-close').onclick = function() {
            root.classList.add('h'); showBtn.style.display = 'block';
        };
        showBtn.onclick = function() { root.classList.remove('h'); showBtn.style.display = 'none'; };

        /* tabs */
        root.querySelectorAll('.rsw-tab').forEach(function(tab) {
            tab.onclick = function() {
                root.querySelectorAll('.rsw-tab').forEach(t => t.classList.remove('act'));
                root.querySelectorAll('.rsw-pnl').forEach(p => p.classList.remove('act'));
                tab.classList.add('act');
                document.getElementById('pnl-' + tab.dataset.tab).classList.add('act');
            };
        });

        /* ── STATIC / DYNAMIC mode switch ── */
        function setScoreMode(mode) {
            DYN.enabled = (mode === 'dynamic');
            document.getElementById('btn-static').className  = 'rsw-btn ' + (DYN.enabled ? '' : 'pri');
            document.getElementById('btn-dynamic').className = 'rsw-btn ' + (DYN.enabled ? 'pri' : '');
            document.getElementById('rsw-static-view').style.display  = DYN.enabled ? 'none'  : 'block';
            document.getElementById('rsw-dyn-view').style.display     = DYN.enabled ? 'block' : 'none';
            document.getElementById('rsw-cur-mode').textContent       = DYN.enabled ? 'DYNAMIC' : 'STATIC';

            if (DYN.enabled) {
                DYN.min = Math.max(0, parseFloat(document.getElementById('dyn-min').value) || 7);
                DYN.max = Math.max(0, parseFloat(document.getElementById('dyn-max').value) || 10);
                if (DYN.min > DYN.max) { var tmp = DYN.min; DYN.min = DYN.max; DYN.max = tmp; }
                var secs = Math.max(0.5, parseFloat(document.getElementById('dyn-int').value) || 3);
                dynStart(secs * 1000);
                log('[DYN] ON  ' + DYN.min.toFixed(1) + '-' + DYN.max.toFixed(1) + ' every ' + secs + 's');
            } else {
                dynStop();
                document.getElementById('rsw-dyn-live').textContent = '--';
                document.getElementById('rsw-cur-score').textContent = CFG.scoreVal.toFixed(1);
                log('[DYN] OFF  fixed=' + CFG.scoreVal);
            }
        }

        document.getElementById('btn-static').onclick  = function() { setScoreMode('static'); };
        document.getElementById('btn-dynamic').onclick = function() { setScoreMode('dynamic'); };

        /* update DYN inputs live while in dynamic mode */
        ['dyn-min','dyn-max','dyn-int'].forEach(function(id) {
            document.getElementById(id).addEventListener('input', function() {
                if (!DYN.enabled) return;
                DYN.min = Math.max(0, parseFloat(document.getElementById('dyn-min').value) || 7);
                DYN.max = Math.max(0, parseFloat(document.getElementById('dyn-max').value) || 10);
                if (DYN.min > DYN.max) { var tmp = DYN.min; DYN.min = DYN.max; DYN.max = tmp; }
                var secs = Math.max(0.5, parseFloat(document.getElementById('dyn-int').value) || 3);
                dynStart(secs * 1000); // restart with new interval
            });
        });

        /* static score input */
        document.getElementById('rsw-val').addEventListener('input', function() {
            const v = parseFloat(this.value);
            if (!isNaN(v) && v >= 0) {
                CFG.scoreVal = v;
                document.getElementById('rsw-cur-score').textContent = v.toFixed(1);
                if (v > 10) document.getElementById('rsw-cur-mode').textContent = 'STATIC — server caps at 10';
                else        document.getElementById('rsw-cur-mode').textContent = 'STATIC';
                log('[UI] score → ' + v); autoSave();
            }
        });

        /* enemy val input */
        document.getElementById('rsw-enemy-val').addEventListener('input', function() {
            const v = parseFloat(this.value);
            if (!isNaN(v) && v >= 0) { CFG.enemyVal = v; autoSave(); }
        });

        /* attack buttons */
        document.getElementById('rsw-send').onclick = function() {
            const m = document.getElementById('rsw-method').value;
            if (m === 'spoof' || m === 'all') { CFG.scoreSpoof = true; startFlood(); }
            if (m === 'display' || m === 'all') {
                CFG.displayWin = true; CFG.liveSpoof = true;
                document.getElementById('t-win').classList.add('on');
                document.getElementById('t-live').classList.add('on');
            }
            this.classList.add('act');
            document.getElementById('rsw-stop').classList.remove('act');
            document.getElementById('rsw-atk-pill').textContent = 'ATTACKING';
            document.getElementById('rsw-atk-pill').classList.add('on');
            log('[ATTACK] START  method=' + m + '  score=' + dynGet());
        };

        document.getElementById('rsw-stop').onclick = function() {
            CFG.scoreSpoof = false; CFG.displayWin = false; CFG.liveSpoof = false;
            stopFlood();
            document.getElementById('t-win').classList.remove('on');
            document.getElementById('t-live').classList.remove('on');
            document.getElementById('rsw-send').classList.remove('act');
            document.getElementById('rsw-atk-pill').textContent = 'OFFLINE';
            document.getElementById('rsw-atk-pill').classList.remove('on');
            log('[ATTACK] STOP');
        };

        /* extra toggles */
        function mkTog(id, onFn, offFn) {
            document.getElementById(id).onclick = function() {
                this.classList.toggle('on');
                const on = this.classList.contains('on');
                if (on && onFn) onFn(); if (!on && offFn) offFn();
            };
        }

        mkTog('t-win',  function(){ CFG.displayWin=true;  log('[UI] DISPLAY_WIN ON');  autoSave(); },
                        function(){ CFG.displayWin=false; log('[UI] DISPLAY_WIN OFF'); autoSave(); });
        mkTog('t-live', function(){ CFG.liveSpoof=true;   log('[UI] LIVE_BAR ON');     autoSave(); },
                        function(){ CFG.liveSpoof=false;  log('[UI] LIVE_BAR OFF');    autoSave(); });
        mkTog('t-enemy',function(){ CFG.enemySpoof=true;  log('[UI] ENEMY_SCORE ON val='+CFG.enemyVal); autoSave(); },
                        function(){ CFG.enemySpoof=false; log('[UI] ENEMY_SCORE OFF'); autoSave(); });

        /* camera */
        document.getElementById('rsw-cam-mode').onchange = function() {
            CAM.mode = this.value;
            document.getElementById('rsw-cam-color-row').style.display = this.value==='color' ? 'flex' : 'none';
            document.getElementById('rsw-cam-img-row').style.display   = this.value==='image' ? 'flex' : 'none';
        };
        document.getElementById('rsw-cam-color').oninput = function() { CAM.color = this.value; };
        document.getElementById('rsw-cam-upload').onclick = function() {
            document.getElementById('rsw-cam-file').click();
        };
        document.getElementById('rsw-cam-file').onchange = function() {
            if (!this.files || !this.files[0]) return;
            const file = this.files[0];
            document.getElementById('rsw-cam-fname').textContent = file.name;
            const reader = new FileReader();
            reader.onload = function(e) {
                const img = new Image();
                img.onload = function() { CAM.img = img; CAM.mode = 'image'; log('[CAM] image loaded: ' + file.name); };
                img.src = e.target.result;
            };
            reader.readAsDataURL(file);
        };
        mkTog('t-cam', function(){ CAM.enabled=true;  log('[CAM] enabled  mode='+CAM.mode); autoSave(); },
                       function(){ CAM.enabled=false; log('[CAM] disabled'); autoSave(); });

        /* mic */
        document.getElementById('rsw-mic-mode').onchange = function() {
            MIC.mode = this.value;
            const freqRow = document.getElementById('rsw-mic-freq-row');
            if (freqRow) freqRow.style.display = this.value === 'tone' ? 'flex' : 'none';
            log('[MIC] mode → ' + this.value);
        };
        document.getElementById('rsw-mic-freq').addEventListener('input', function() {
            const v = parseFloat(this.value);
            if (!isNaN(v) && v > 0) { MIC.freq = v; log('[MIC] freq → ' + v + 'Hz'); }
        });
        mkTog('t-mic',
            function() {
                MIC.mode = document.getElementById('rsw-mic-mode').value || 'silent';
                MIC.freq = parseFloat(document.getElementById('rsw-mic-freq').value) || 440;
                MIC.enabled = true;
                log('[MIC] enabled  mode=' + MIC.mode + (MIC.mode==='tone'?' freq='+MIC.freq+'Hz':''));
            },
            function() {
                MIC.enabled = false;
                // stop any running nodes
                _micNodes.forEach(function(n) { try { if (n.stop) n.stop(); if (n.disconnect) n.disconnect(); } catch(_) {} });
                _micNodes.length = 0;
                log('[MIC] disabled');
            });

        /* country */
        document.getElementById('rsw-country').onchange = function() {
            SPF.code = this.value;
            const c = COUNTRIES[SPF.code];
            document.getElementById('rsw-country-lang').textContent = c.lang;
            document.getElementById('rsw-country-tz').textContent   = c.tz;
        };
        document.getElementById('rsw-apply-country').onclick = function() {
            SPF.country = true;
            document.getElementById('t-country').classList.add('on');
            applyCountrySpoof();
        };
        mkTog('t-country', function(){ SPF.country=true;  applyCountrySpoof(); autoSave(); },
                           function(){ SPF.country=false; log('[SPF] country OFF'); autoSave(); });

        /* BAN bypass */
        mkTog('t-ban',
            function() {
                SPF.ban = true; autoSave();
                log('[BAN] ON — all 5 ban layers active');
            },
            function() {
                SPF.ban = false; autoSave();
                clearTimeout(BAN_STATE.reconnectTimer);
                log('[BAN] OFF');
            }
        );
        document.getElementById('rsw-ban-reconnect').onclick = function() {
            if (!BAN_STATE.lastUrl) { log('[BAN] no WS URL stored yet — join a match first'); return; }
            log('[BAN] forcing reconnect → ' + BAN_STATE.lastUrl.split('?')[0]);
            window.WebSocket(BAN_STATE.lastUrl, BAN_STATE.lastProto);
        };
        document.getElementById('rsw-ban-reload').onclick = function() {
            SPF.ban = true;
            document.getElementById('t-ban').classList.add('on');
            // Wipe ban fields from stored session immediately before reload
            try {
                const raw = localStorage.getItem(SB_LS_KEY);
                if (raw) {
                    const obj = JSON.parse(raw);
                    if (obj) { deepPatchBan(obj, 0); if(obj.user) deepPatchBan(obj.user, 0); localStorage.setItem(SB_LS_KEY, JSON.stringify(obj)); }
                }
                // Also scan all other LS keys for ban data
                for (let i = 0; i < localStorage.length; i++) {
                    const k = localStorage.key(i); if(!k) continue;
                    if (k.includes('auth') || k.includes('session') || k.includes('supabase') || k.startsWith('sb-')) {
                        try {
                            const v = localStorage.getItem(k);
                            if (!v) continue;
                            const o = JSON.parse(v);
                            if (o && typeof o === 'object') { deepPatchBan(o, 0); localStorage.setItem(k, JSON.stringify(o)); }
                        } catch(_) {}
                    }
                }
            } catch(_) {}
            log('[BAN] session ban fields wiped — reloading in 600ms...');
            setTimeout(function() { location.reload(); }, 600);
        };

        /* PRO bypass */
        mkTog('t-pro',
            function() {
                SPF.pro = true; autoSave();
                injectProCSS();
                // Also force-patch the stored Supabase session right now
                try {
                    const raw = localStorage.getItem(SB_LS_KEY);
                    if (raw) {
                        const obj = JSON.parse(raw);
                        if (obj && obj.user) { deepPatchPro(obj.user, 0); localStorage.setItem(SB_LS_KEY, JSON.stringify(obj)); }
                    }
                } catch(_) {}
                log('[PRO] ON — fetch+XHR+localStorage+CSS all active');
                log('[PRO] Reload the page for full effect (stored session patched)');
            },
            function() { SPF.pro = false; autoSave(); log('[PRO] OFF'); }
        );
        document.getElementById('rsw-pro-reload').onclick = function() {
            SPF.pro = true;
            document.getElementById('t-pro').classList.add('on');
            injectProCSS();
            // patch stored session then reload
            try {
                const raw = localStorage.getItem(SB_LS_KEY);
                if (raw) {
                    const obj = JSON.parse(raw);
                    if (obj && obj.user) { deepPatchPro(obj.user, 0); localStorage.setItem(SB_LS_KEY, JSON.stringify(obj)); }
                }
            } catch(e) {}
            log('[PRO] session patched — reloading in 800ms...');
            setTimeout(function() { location.reload(); }, 800);
        };

        /* config tab */
        function refreshConfPreview() {
            const el = document.getElementById('rsw-conf-preview');
            if (el) el.textContent = JSON.stringify({
                score:{spoof:CFG.scoreSpoof,val:CFG.scoreVal,dyn:DYN.enabled,min:DYN.min,max:DYN.max,displayWin:CFG.displayWin,liveSpoof:CFG.liveSpoof},
                enemy:{spoof:CFG.enemySpoof,val:CFG.enemyVal},
                cam:{enabled:CAM.enabled,mode:CAM.mode,color:CAM.color},
                mic:{enabled:MIC.enabled,mode:MIC.mode,freq:MIC.freq},
                spf:{country:SPF.country,code:SPF.code,pro:SPF.pro,ban:SPF.ban},
            }, null, 2);
            const s2 = document.getElementById('rsw-conf-status2');
            if (s2) s2.textContent = localStorage.getItem(CONF_KEY) ? 'SAVED ✓' : 'NOT SAVED';
        }
        document.getElementById('rsw-conf-save').onclick = function() { saveConf(); refreshConfPreview(); log('[CONF] saved manually'); };
        document.getElementById('rsw-conf-clear').onclick = function() { localStorage.removeItem(CONF_KEY); refreshConfPreview(); log('[CONF] cleared'); };
        setInterval(refreshConfPreview, 2000);

        /* log tab */
        document.getElementById('rsw-log-clear').onclick = function() {
            logs.length = 0;
            document.getElementById('rsw-log').textContent = '';
        };

        /* ══ SYNC SAVED STATE TO UI ══ */
        function syncUI() {
            // SCORE
            if (CFG.scoreVal != null) { document.getElementById('rsw-val').value = CFG.scoreVal; document.getElementById('rsw-cur-score').textContent = CFG.scoreVal.toFixed(1); }
            if (DYN.enabled) { setScoreMode('dynamic'); document.getElementById('dyn-min').value=DYN.min; document.getElementById('dyn-max').value=DYN.max; }
            if (CFG.displayWin){ document.getElementById('t-win').classList.add('on'); }
            if (CFG.liveSpoof) { document.getElementById('t-live').classList.add('on'); }
            if (CFG.enemySpoof){ document.getElementById('t-enemy').classList.add('on'); }
            if (CFG.enemyVal != null) document.getElementById('rsw-enemy-val').value = CFG.enemyVal;
            // CAM
            document.getElementById('rsw-cam-mode').value = CAM.mode;
            document.getElementById('rsw-cam-color').value = CAM.color;
            if (CAM.mode==='color') document.getElementById('rsw-cam-color-row').style.display='flex';
            if (CAM.mode==='image') document.getElementById('rsw-cam-img-row').style.display='flex';
            if (CAM.enabled) document.getElementById('t-cam').classList.add('on');
            // MIC
            document.getElementById('rsw-mic-mode').value = MIC.mode;
            document.getElementById('rsw-mic-freq').value = MIC.freq;
            if (MIC.mode!=='tone') document.getElementById('rsw-mic-freq-row').style.display='none';
            if (MIC.enabled) document.getElementById('t-mic').classList.add('on');
            // COUNTRY
            document.getElementById('rsw-country').value = SPF.code;
            const c = COUNTRIES[SPF.code]||COUNTRIES.US;
            document.getElementById('rsw-country-lang').textContent = c.lang;
            document.getElementById('rsw-country-tz').textContent   = c.tz;
            if (SPF.country){ document.getElementById('t-country').classList.add('on'); applyCountrySpoof(); }
            // PRO / BAN
            if (SPF.pro){ document.getElementById('t-pro').classList.add('on'); injectProCSS(); }
            if (SPF.ban){ document.getElementById('t-ban').classList.add('on'); }
            // conf indicator
            const cs = document.getElementById('rsw-conf-status');
            if (cs) cs.textContent = localStorage.getItem(CONF_KEY) ? 'LOADED' : 'AUTO';
        }
        syncUI();

        /* live status ticker — 250ms */
        setInterval(function() {
            /* log tab counter */
            const c2 = document.getElementById('rsw-cnt2');
            if (c2) c2.textContent = pktCount;
            /* WS status */
            const ws = document.getElementById('rsw-ws-status');
            if (ws) ws.textContent = hookedWsRef
                ? (hookedWsRef.readyState === 1 ? 'connected ✓' : 'disconnected')
                : 'not connected';
            /* big score display */
            const sc = document.getElementById('rsw-cur-score');
            if (sc) sc.textContent = dynGet().toFixed(1);
            /* dynamic live value */
            if (DYN.enabled) {
                const dl = document.getElementById('rsw-dyn-live');
                if (dl) dl.textContent = DYN.cur.toFixed(1);
            }
        }, 250);

        /* drag */
        var drag=false, ox=0, oy=0;
        document.getElementById('rsw-bar').addEventListener('mousedown', function(e){
            drag=true; var r=root.getBoundingClientRect(); ox=e.clientX-r.left; oy=e.clientY-r.top; e.preventDefault();
        });
        document.addEventListener('mousemove', function(e){ if(!drag)return; root.style.left=(e.clientX-ox)+'px'; root.style.top=(e.clientY-oy)+'px'; });
        document.addEventListener('mouseup', function(){ drag=false; });

        /* INSERT */
        document.addEventListener('keydown', function(e){
            if (e.key==='Insert'){ var h=root.classList.toggle('h'); showBtn.style.display=h?'block':'none'; }
        });

        /* ════════ MASS REPORT ════════ */

        function getAuthHeaders() {
            const h = { 'Content-Type': 'application/json' };
            try {
                for (let i = 0; i < localStorage.length; i++) {
                    const k = localStorage.key(i); if (!k) continue;
                    if (k.includes('auth-token') || k.includes('supabase') || k.startsWith('sb-')) {
                        try {
                            const obj = JSON.parse(localStorage.getItem(k) || '');
                            const tok = obj && (obj.access_token || (obj.session && obj.session.access_token));
                            if (tok) { h['Authorization'] = 'Bearer ' + tok; break; }
                        } catch(_) {}
                    }
                }
            } catch(_) {}
            return h;
        }

        async function sendOneReport(target, reason) {
            // ── Mode A: replay captured request ──
            if (RPT.lastUrl && RPT.lastBody) {
                try {
                    let body = RPT.lastBody;
                    if (target || reason) {
                        try {
                            const p = JSON.parse(body);
                            if (p && typeof p === 'object') {
                                if (target) ['userId','reportedId','reported_user_id','targetId','user_id','reported_id','reportedUserId'].forEach(function(fk){ if (fk in p) p[fk] = target; });
                                if (reason) ['reason','reportReason','report_reason','type','category'].forEach(function(fk){ if (fk in p) p[fk] = reason; });
                                body = JSON.stringify(p);
                            }
                        } catch(_) {}
                    }
                    const hdrs = Object.assign(getAuthHeaders(), RPT.lastHeaders || {});
                    // Always ensure Content-Type is json
                    hdrs['Content-Type'] = 'application/json';
                    const resp = await _origFetch(RPT.lastUrl, { method: 'POST', headers: hdrs, body });
                    return resp.status;
                } catch(_) {}
            }

            // ── Mode B: try common REST endpoints ──
            if (!target) return null;
            const authH = getAuthHeaders();
            const body = JSON.stringify({ userId: target, reportedId: target, reported_user_id: target, reason: reason || 'spam', report_reason: reason || 'spam', category: reason || 'spam' });
            const endpoints = [
                '/api/report', '/api/reports', '/api/user/report', '/api/users/report',
                '/api/v1/report', '/api/v1/reports', '/report', '/reports',
            ];
            for (const ep of endpoints) {
                try {
                    const r = await _origFetch(ep, { method:'POST', headers:authH, body });
                    if (r.status < 500) return r.status;
                } catch(_) {}
            }
            return null;
        }

        async function runReportLoop() {
            const target = document.getElementById('rsw-rpt-target').value.trim();
            const reason = document.getElementById('rsw-rpt-reason').value;
            const delay  = Math.max(50, parseInt(document.getElementById('rsw-rpt-delay').value) || 400);

            if (!target && !RPT.lastBody) {
                log('[RPT] ERROR: no target and no captured request — enter a user ID or capture first');
                return;
            }

            const modeEl = document.getElementById('rsw-rpt-mode');
            if (modeEl) modeEl.textContent = RPT.lastUrl ? 'AUTO-CAPTURE' : 'MANUAL FALLBACK';

            while (RPT.running && RPT.sent < RPT.total) {
                try {
                    const status = await sendOneReport(target, reason);
                    RPT.sent++;
                    const se = document.getElementById('rsw-rpt-sent');
                    if (se) se.textContent = RPT.sent;
                    const re = document.getElementById('rsw-rpt-resp');
                    if (re) re.textContent = status !== null ? String(status) : 'err';
                    if (RPT.sent % 10 === 0 || RPT.sent === 1)
                        log('[RPT] sent ' + RPT.sent + '/' + RPT.total + '  status=' + (status !== null ? status : 'err'));
                } catch(e) {
                    log('[RPT] send error: ' + e.message);
                }
                if (RPT.sent < RPT.total) await new Promise(function(r){ setTimeout(r, delay); });
            }
            stopMassReport(true);
        }

        function startMassReport() {
            if (RPT.running) return;
            const count = Math.max(1, parseInt(document.getElementById('rsw-rpt-count').value) || 50);
            RPT.running = true; RPT.sent = 0; RPT.total = count;
            const pill = document.getElementById('rsw-rpt-pill');
            if (pill) { pill.textContent = 'RUNNING'; pill.classList.add('on'); }
            const tot = document.getElementById('rsw-rpt-total');
            if (tot) tot.textContent = '/ ' + count;
            const se = document.getElementById('rsw-rpt-sent');
            if (se) se.textContent = '0';
            const target = document.getElementById('rsw-rpt-target').value.trim();
            const reason = document.getElementById('rsw-rpt-reason').value;
            log('[RPT] START  target=' + (target || '(auto)') + '  count=' + count
                + '  delay=' + document.getElementById('rsw-rpt-delay').value + 'ms'
                + '  mode=' + (RPT.lastUrl ? 'capture' : 'manual'));
            runReportLoop();
        }

        function stopMassReport(auto) {
            RPT.running = false;
            const pill = document.getElementById('rsw-rpt-pill');
            if (pill) { pill.textContent = 'IDLE'; pill.classList.remove('on'); }
            if (!auto) log('[RPT] STOP  total sent=' + RPT.sent);
            else       log('[RPT] DONE  total sent=' + RPT.sent);
        }

        document.getElementById('rsw-rpt-use-opp').onclick = function() {
            const val = MATCH.opponentId || MATCH.opponentUsername;
            if (!val) { log('[RPT] no opponent detected yet — enter a match first'); return; }
            document.getElementById('rsw-rpt-target').value = val;
            log('[RPT] target set to opponent: ' + val);
        };
        document.getElementById('rsw-rpt-scan-dom').onclick = function() {
            scanDOMForOpponent();
            updateOpponentUI();
        };
        document.getElementById('rsw-rpt-start').onclick = startMassReport;
        document.getElementById('rsw-rpt-stop').onclick  = function(){ stopMassReport(false); };
        document.getElementById('rsw-rpt-clear-cap').onclick = function() {
            RPT.lastUrl = null; RPT.lastBody = null; RPT.lastHeaders = {};
            const el = document.getElementById('rsw-rpt-captured'); if (el) { el.textContent = 'waiting…'; el.style.color='#444'; }
            const be = document.getElementById('rsw-rpt-body');     if (be) { be.textContent = '--'; be.style.color='#333'; }
            log('[RPT] capture cleared');
        };

        log('[RSWARE] v9.3 loaded — enable SCORE SPOOF before match');
        log('[RSWARE] INSERT key = show/hide panel');
        log('[RSWARE] REPORT tab: click native report once to capture, then MASS REPORT');
        if (localStorage.getItem(CONF_KEY)) log('[CONF] settings restored from last session');
    }

    setupBanDOMScrubber();
    if (document.readyState==='loading') document.addEventListener('DOMContentLoaded', buildUI);
    else buildUI();

})();