RSWARE — Omoggle 1v1

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

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

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

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

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

이 스크립트를 설치하려면 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();

})();