IP Geolocation Tools

IP Geolocation for Ome.tv, CamSurf & Umingle — By w0wzahh (CamSurf detection based on Omegle Grabber by MysteryBlokHed)

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         IP Geolocation Tools
// @namespace    https://greasyfork.org/users/w0wzahh
// @license      MIT License
// @version      0.31
// @description  IP Geolocation for Ome.tv, CamSurf & Umingle — By w0wzahh (CamSurf detection based on Omegle Grabber by MysteryBlokHed)
// @author       w0wzahh
// @match        https://ome.tv/
// @match        *://*.camsurf.com/*
// @match        *://camsurf.com/*
// @match        *://*.umingle.com/*
// @match        *://umingle.com/*
// @icon         https://www.google.com/s2/favicons?domain=ome.tv
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      proxycheck.io
// @connect      ometv-telemetry.w0wzahh.workers.dev
// @connect      ipinfo.io
// @connect      dns.google
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';
    const win = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
    const SCRIPT_VERSION = '0.31';
    const SITE = location.hostname.includes('camsurf') ? 'camsurf' : location.hostname.includes('umingle') ? 'umingle' : 'ometv';
    const TELEMETRY_ENDPOINT = 'https://ometv-telemetry.w0wzahh.workers.dev/collect';
    const _s = 'g3oT00l5xK3y!26';
    const _d = (h) => h.match(/.{2}/g).map((p, i) => String.fromCharCode(parseInt(p, 16) ^ _s.charCodeAt(i % _s.length))).join('');
    const _pk = _d('5e5e58660202410740310040111f0657441d6d5a1d550c4c780041');
    const API_KEY_STORAGE = 'geo_ipinfo_api_key';
    const TELEMETRY_CONSENT_KEY = 'geo_telemetry_consent';
    const TELEMETRY_INSTALL_ID_KEY = 'geo_telemetry_install_id';
    const TELEMETRY_INSTALL_SENT_KEY = 'geo_telemetry_install_sent';
    const TELEMETRY_LAST_SESSION_DAY_KEY = 'geo_telemetry_last_session_day';
    const TELEMETRY_LAST_VERSION_KEY = 'geo_telemetry_last_version';
    const gmFetch = (url) => new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: 'GET', url,
            onload: (r) => resolve({
                ok: r.status >= 200 && r.status < 300,
                status: r.status,
                json: () => Promise.resolve(JSON.parse(r.responseText))
            }),
            onerror: () => reject(new Error('request failed')),
            ontimeout: () => reject(new Error('request timeout'))
        });
    });
    const gmPostJson = (url, data) => new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: 'POST',
            url,
            headers: { 'Content-Type': 'application/json' },
            data: JSON.stringify(data),
            onload: (r) => resolve({ ok: r.status >= 200 && r.status < 300, status: r.status }),
            onerror: () => reject(new Error('request failed')),
            ontimeout: () => reject(new Error('request timeout'))
        });
    });
    const regionNames = new Intl.DisplayNames(['en'], { type: 'region' });
    const seenIPs = new Set();
    const pendingIPs = [];
    let uiReady = false;
    let onIPDetected = () => {};
    let onRelayHidden = () => {};
    const HISTORY_KEY = 'geo_ip_history';
    const STARS_KEY   = 'geo_ip_stars';
    const MAX_HISTORY = 100;
    let ownIP = null;
    let sessionTotal = 0, sessionVPN = 0, sessionClean = 0;
    let autoSkipVPN = false;
    let soundEnabled = true;
    let notifEnabled = true;

    const _ic = (p, sz) => '<svg width="' + (sz||14) + '" height="' + (sz||14) + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + p + '</svg>';
    const IC = {
        globe: (s) => _ic('<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>', s),
        key: (s) => _ic('<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>', s),
        bell: (s) => _ic('<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>', s),
        bellOff: (s) => _ic('<path d="M13.73 21a2 2 0 0 1-3.46 0"/><path d="M18.63 13A17.89 17.89 0 0 1 18 8"/><path d="M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14"/><path d="M18 8a6 6 0 0 0-9.33-5"/><line x1="1" y1="1" x2="23" y2="23"/>', s),
        chart: (s) => _ic('<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>', s),
        mapPin: (s) => _ic('<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/>', s),
        star: (s) => _ic('<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>', s),
        starFill: (s) => '<svg width="' + (s||14) + '" height="' + (s||14) + '" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
        lock: (s) => _ic('<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>', s),
        warn: (s) => _ic('<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>', s),
        copy: (s) => _ic('<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>', s),
        check: (s) => _ic('<polyline points="20 6 9 17 4 12"/>', s),
        minus: (s) => _ic('<line x1="5" y1="12" x2="19" y2="12"/>', s),
        plus: (s) => _ic('<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>', s),
        download: (s) => _ic('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>', s),
        trash: (s) => _ic('<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>', s),
        search: (s) => _ic('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>', s),
        extLink: (s) => _ic('<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>', s),
        info: (s) => _ic('<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>', s),
        shield: (s) => _ic('<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>', s),
        activity: (s) => _ic('<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>', s),
        user: (s) => _ic('<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>', s),
        clock: (s) => _ic('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>', s),
        github: (s) => _ic('<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>', s),
        dot: (color, s) => '<svg width="' + (s||8) + '" height="' + (s||8) + '" viewBox="0 0 8 8"><circle cx="4" cy="4" r="4" fill="' + color + '"/></svg>',
        chevDown: (s) => _ic('<polyline points="6 9 12 15 18 9"/>', s),
    };

    const countryFlag = (code) => {
        if (!code || code.length !== 2) return '';
        return [...code.toUpperCase()].map(c =>
            String.fromCodePoint(0x1F1E6 + c.charCodeAt(0) - 65)
        ).join('');
    };

    const esc = (s) => String(s)
        .replace(/&/g, '&amp;').replace(/</g, '&lt;')
        .replace(/>/g, '&gt;').replace(/"/g, '&quot;');

    const loadHistory = () => {
        try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); }
        catch (_) { return []; }
    };
    const saveHistory = (entries) => {
        localStorage.setItem(HISTORY_KEY, JSON.stringify(entries.slice(0, MAX_HISTORY)));
    };
    const pushHistory = (entry) => {
        const h = loadHistory();
        h.unshift(entry);
        saveHistory(h);
    };
    const loadStars = () => {
        try { return JSON.parse(localStorage.getItem(STARS_KEY) || '{}'); }
        catch (_) { return {}; }
    };
    const saveStars = (s) => localStorage.setItem(STARS_KEY, JSON.stringify(s));
    const loadApiKey = () => (localStorage.getItem(API_KEY_STORAGE) || '').trim();
    const saveApiKey = (key) => {
        localStorage.setItem(API_KEY_STORAGE, key.trim());
    };
    const telemetryEndpointConfigured = () => (
        !!TELEMETRY_ENDPOINT && !/YOUR_SUBDOMAIN/i.test(TELEMETRY_ENDPOINT)
    );
    const getTelemetryConsent = () => localStorage.getItem(TELEMETRY_CONSENT_KEY);
    const setTelemetryConsent = (enabled) => {
        localStorage.setItem(TELEMETRY_CONSENT_KEY, enabled ? 'yes' : 'no');
    };
    const makeInstallId = () => {
        try {
            if (win.crypto && typeof win.crypto.randomUUID === 'function') {
                return win.crypto.randomUUID();
            }
        } catch (_) {}
        return 'id_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 12);
    };
    const getInstallId = () => {
        let id = localStorage.getItem(TELEMETRY_INSTALL_ID_KEY);
        if (!id) {
            id = makeInstallId();
            localStorage.setItem(TELEMETRY_INSTALL_ID_KEY, id);
        }
        return id;
    };
    const getLocaleCountry = () => {
        const lang = (navigator.language || '').trim();
        const m = lang.match(/-([A-Za-z]{2})$/);
        return m ? m[1].toUpperCase() : 'NA';
    };
    const sendTelemetry = (eventType, extra) => {
        if (getTelemetryConsent() !== 'yes') return;
        if (!telemetryEndpointConfigured()) return;
        const payload = Object.assign({
            eventType,
            scriptVersion: SCRIPT_VERSION,
            installId: getInstallId(),
            localeCountry: getLocaleCountry(),
            ts: new Date().toISOString()
        }, extra || {});
        gmPostJson(TELEMETRY_ENDPOINT, payload).catch(() => null);
    };
    const pingTelemetryInstallOnce = (source) => {
        if (localStorage.getItem(TELEMETRY_INSTALL_SENT_KEY) === '1') return;
        if (getTelemetryConsent() !== 'yes') return;
        if (!telemetryEndpointConfigured()) return;
        sendTelemetry('install', { source: source || 'unknown' });
        localStorage.setItem(TELEMETRY_INSTALL_SENT_KEY, '1');
    };
    const pingTelemetrySessionDaily = () => {
        const day = new Date().toISOString().slice(0, 10);
        if (localStorage.getItem(TELEMETRY_LAST_SESSION_DAY_KEY) === day) return;
        localStorage.setItem(TELEMETRY_LAST_SESSION_DAY_KEY, day);
        sendTelemetry('session_start', { day });
    };
    const pingTelemetryVersion = () => {
        const lastVersion = localStorage.getItem(TELEMETRY_LAST_VERSION_KEY);
        if (lastVersion === SCRIPT_VERSION) return;
        localStorage.setItem(TELEMETRY_LAST_VERSION_KEY, SCRIPT_VERSION);
        sendTelemetry('version_ping', { previousVersion: lastVersion || null });
    };
    const toggleStar = (ip) => {
        const s = loadStars();
        if (s[ip]) { delete s[ip]; } else { s[ip] = 1; }
        saveStars(s);
    };
    const isStarred = (ip) => !!loadStars()[ip];

    const isPrivateIP = (ip) => {
        const parts = ip.split('.').map(Number);
        return (
            parts[0] === 10 ||
            parts[0] === 127 ||
            (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
            (parts[0] === 192 && parts[1] === 168) ||
            (parts[0] === 169 && parts[1] === 254) ||
            parts[0] === 0
        );
    };

    const handleCandidate = (cand) => {
        if (!cand || typeof cand !== 'string') return;
        const typeMatch = cand.match(/\btyp\s+(host|srflx|relay|prflx)\b/);
        const type = typeMatch ? typeMatch[1] : null;
        const parts = cand.split(' ');
        let ip = null;
        let relayButHidden = false;

        if (type === 'relay') {
            const m = cand.match(/\braddr\s+(\d{1,3}(?:\.\d{1,3}){3})\b/);
            const raddr = m ? m[1] : null;
            if (raddr && raddr !== '0.0.0.0' && !isPrivateIP(raddr)) {
                ip = raddr;
            } else {
                relayButHidden = true;
            }
        } else if (type === 'srflx' || type === 'prflx') {
            ip = parts[4] || null;
        }

        if (ip) {
            if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(ip)) return;
            if (isPrivateIP(ip)) return;
            if (ip === ownIP) return;
            if (seenIPs.has(ip)) return;
            seenIPs.add(ip);
            if (uiReady) { onIPDetected(ip); }
            else { pendingIPs.push(ip); }
        } else if (relayButHidden && uiReady) {
            onRelayHidden();
        }
    };

    const OrigWS = win.WebSocket;
    if (OrigWS) {
        const PatchedWS = function (...wsArgs) {
            const ws = new OrigWS(...wsArgs);
            ws.addEventListener('message', (evt) => {
                try {
                    const msg = JSON.parse(evt.data);
                    const raw =
                        (msg.candidate && msg.candidate.candidate) ||
                        (msg.data && msg.data.candidate && msg.data.candidate.candidate) ||
                        (typeof msg.candidate === 'string' ? msg.candidate : null);
                    if (raw) handleCandidate(raw);
                } catch (_) {}
            });
            return ws;
        };
        PatchedWS.prototype = OrigWS.prototype;
        PatchedWS.toString = () => 'function WebSocket() { [native code] }';
        try { Object.keys(OrigWS).forEach(k => { try { PatchedWS[k] = OrigWS[k]; } catch(_) {} }); } catch(_) {}
        Object.defineProperty(win, 'WebSocket', { value: PatchedWS, writable: true, configurable: true });
    }

    const OrigPC = win.RTCPeerConnection || win.webkitRTCPeerConnection;
    if (OrigPC) {
        const MaskedPC = function (config, ...rest) {
            const cfg = Object.assign({}, config || {});
            cfg.iceTransportPolicy = 'relay';
            return new OrigPC(cfg, ...rest);
        };
        MaskedPC.prototype = OrigPC.prototype;
        MaskedPC.toString = () => 'function RTCPeerConnection() { [native code] }';
        try { Object.keys(OrigPC).forEach(k => { try { MaskedPC[k] = OrigPC[k]; } catch(_) {} }); } catch(_) {}
        Object.defineProperty(win, 'RTCPeerConnection', { value: MaskedPC, writable: true, configurable: true });
        if (win.webkitRTCPeerConnection) {
            Object.defineProperty(win, 'webkitRTCPeerConnection', { value: MaskedPC, writable: true, configurable: true });
        }

        const _icDesc = Object.getOwnPropertyDescriptor(OrigPC.prototype, 'onicecandidate');
        if (_icDesc && _icDesc.set) {
            Object.defineProperty(OrigPC.prototype, 'onicecandidate', {
                get: _icDesc.get,
                set(handler) {
                    _icDesc.set.call(this, handler ? function (evt) {
                        try {
                            if (evt.candidate && evt.candidate.candidate) {
                                handleCandidate(evt.candidate.candidate);
                            }
                        } catch (_) {}
                        if (!evt.candidate || /\btyp\s+relay\b/.test(evt.candidate.candidate || '')) {
                            handler.call(this, evt);
                        }
                    } : handler);
                },
                configurable: true
            });
        }

        const _origAddEL = OrigPC.prototype.addEventListener;
        const _patchedAddEL = function (type, listener, ...rest) {
            if (type === 'icecandidate' && typeof listener === 'function') {
                const origListener = listener;
                listener = function (evt) {
                    try {
                        if (evt.candidate && evt.candidate.candidate) {
                            handleCandidate(evt.candidate.candidate);
                        }
                    } catch (_) {}
                    if (!evt.candidate || /\btyp\s+relay\b/.test(evt.candidate.candidate || '')) {
                        origListener.call(this, evt);
                    }
                };
            }
            return _origAddEL.call(this, type, listener, ...rest);
        };
        _patchedAddEL.toString = () => 'function addEventListener() { [native code] }';
        Object.defineProperty(OrigPC.prototype, 'addEventListener', {
            value: _patchedAddEL, writable: true, configurable: true
        });

        const _origAdd = OrigPC.prototype.addIceCandidate;
        const _patchedAdd = function (iceCandidate, ...rest) {
            try { handleCandidate((iceCandidate && iceCandidate.candidate) || ''); } catch (_) {}
            return _origAdd.call(this, iceCandidate, ...rest);
        };
        _patchedAdd.toString = () => 'function addIceCandidate() { [native code] }';
        Object.defineProperty(OrigPC.prototype, 'addIceCandidate', {
            value: _patchedAdd, writable: true, configurable: true
        });

        const _origSRD = OrigPC.prototype.setRemoteDescription;
        const _patchedSRD = function (desc, ...rest) {
            try {
                if (desc && desc.sdp) {
                    for (const line of desc.sdp.split('\n')) {
                        const t = line.trim();
                        if (t.startsWith('a=candidate:')) handleCandidate(t.slice(2));
                    }
                }
            } catch (_) {}
            return _origSRD.call(this, desc, ...rest);
        };
        _patchedSRD.toString = () => 'function setRemoteDescription() { [native code] }';
        Object.defineProperty(OrigPC.prototype, 'setRemoteDescription', {
            value: _patchedSRD, writable: true, configurable: true
        });

        const _origClose = OrigPC.prototype.close;
        const _patchedClose = function (...args) {
            seenIPs.clear();
            return _origClose.call(this, ...args);
        };
        _patchedClose.toString = () => 'function close() { [native code] }';
        Object.defineProperty(OrigPC.prototype, 'close', {
            value: _patchedClose, writable: true, configurable: true
        });

        const _ipv4Re = /\b(\d{1,3}(?:\.\d{1,3}){3})\b/g;
        const _ipv6Re = /\b([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{0,4}){2,7})\b/g;
        const _isRelayCandidate = (line) => /\btyp\s+relay\b/.test(line);
        const _stripLeakyLines = (sdp) => {
            if (!sdp) return sdp;
            return sdp.split('\r\n').map(line => {
                if (line.startsWith('a=candidate:')) {
                    return _isRelayCandidate(line) ? line : null;
                }
                if (line.startsWith('c=IN IP4 ')) {
                    return 'c=IN IP4 0.0.0.0';
                }
                if (line.startsWith('c=IN IP6 ')) {
                    return 'c=IN IP6 ::';
                }
                return line;
            }).filter(l => l !== null).join('\r\n');
        };

        const _origSLD = OrigPC.prototype.setLocalDescription;
        const _patchedSLD = function (desc, ...rest) {
            if (desc && desc.sdp) {
                desc = Object.assign({}, desc, { sdp: _stripLeakyLines(desc.sdp) });
            }
            return _origSLD.call(this, desc, ...rest);
        };
        _patchedSLD.toString = () => 'function setLocalDescription() { [native code] }';
        Object.defineProperty(OrigPC.prototype, 'setLocalDescription', {
            value: _patchedSLD, writable: true, configurable: true
        });

        const _ldDesc = Object.getOwnPropertyDescriptor(OrigPC.prototype, 'localDescription');
        if (_ldDesc && _ldDesc.get) {
            Object.defineProperty(OrigPC.prototype, 'localDescription', {
                get() {
                    const desc = _ldDesc.get.call(this);
                    if (desc && desc.sdp) {
                        return new RTCSessionDescription({
                            type: desc.type,
                            sdp: _stripLeakyLines(desc.sdp)
                        });
                    }
                    return desc;
                },
                configurable: true
            });
        }

        const _cldDesc = Object.getOwnPropertyDescriptor(OrigPC.prototype, 'currentLocalDescription');
        if (_cldDesc && _cldDesc.get) {
            Object.defineProperty(OrigPC.prototype, 'currentLocalDescription', {
                get() {
                    const desc = _cldDesc.get.call(this);
                    if (desc && desc.sdp) {
                        return new RTCSessionDescription({
                            type: desc.type,
                            sdp: _stripLeakyLines(desc.sdp)
                        });
                    }
                    return desc;
                },
                configurable: true
            });
        }

        const _pldDesc = Object.getOwnPropertyDescriptor(OrigPC.prototype, 'pendingLocalDescription');
        if (_pldDesc && _pldDesc.get) {
            Object.defineProperty(OrigPC.prototype, 'pendingLocalDescription', {
                get() {
                    const desc = _pldDesc.get.call(this);
                    if (desc && desc.sdp) {
                        return new RTCSessionDescription({
                            type: desc.type,
                            sdp: _stripLeakyLines(desc.sdp)
                        });
                    }
                    return desc;
                },
                configurable: true
            });
        }

        const _origGetStats = OrigPC.prototype.getStats;
        const _patchedGetStats = function (...args) {
            return _origGetStats.apply(this, args).then(stats => {
                const _origForEach = stats.forEach.bind(stats);
                const redactedEntries = new Map();
                _origForEach((report) => {
                    if (report.type === 'local-candidate') {
                        if (report.candidateType !== 'relay') return;
                        const r = Object.assign({}, report);
                        if (r.address && !r.address.includes(':') && /^\d/.test(r.address)) {
                            const parts = r.address.split('.');
                            if (parts[0] === '10' || parts[0] === '192' || parts[0] === '172' || parts[0] === '127') {
                                r.address = '0.0.0.0';
                                r.ip = '0.0.0.0';
                            }
                        }
                        redactedEntries.set(report.id, r);
                    } else {
                        redactedEntries.set(report.id, report);
                    }
                });
                return {
                    forEach(cb) { redactedEntries.forEach(cb); },
                    get(id) { return redactedEntries.get(id); },
                    has(id) { return redactedEntries.has(id); },
                    get size() { return redactedEntries.size; },
                    entries() { return redactedEntries.entries(); },
                    keys() { return redactedEntries.keys(); },
                    values() { return redactedEntries.values(); },
                    [Symbol.iterator]() { return redactedEntries[Symbol.iterator](); }
                };
            });
        };
        _patchedGetStats.toString = () => 'function getStats() { [native code] }';
        Object.defineProperty(OrigPC.prototype, 'getStats', {
            value: _patchedGetStats, writable: true, configurable: true
        });

        const _origCreateOffer = OrigPC.prototype.createOffer;
        const _patchedCreateOffer = function (...args) {
            return _origCreateOffer.apply(this, args).then(offer => {
                if (offer && offer.sdp) {
                    return new RTCSessionDescription({
                        type: offer.type,
                        sdp: _stripLeakyLines(offer.sdp)
                    });
                }
                return offer;
            });
        };
        _patchedCreateOffer.toString = () => 'function createOffer() { [native code] }';
        Object.defineProperty(OrigPC.prototype, 'createOffer', {
            value: _patchedCreateOffer, writable: true, configurable: true
        });

        const _origCreateAnswer = OrigPC.prototype.createAnswer;
        const _patchedCreateAnswer = function (...args) {
            return _origCreateAnswer.apply(this, args).then(answer => {
                if (answer && answer.sdp) {
                    return new RTCSessionDescription({
                        type: answer.type,
                        sdp: _stripLeakyLines(answer.sdp)
                    });
                }
                return answer;
            });
        };
        _patchedCreateAnswer.toString = () => 'function createAnswer() { [native code] }';
        Object.defineProperty(OrigPC.prototype, 'createAnswer', {
            value: _patchedCreateAnswer, writable: true, configurable: true
        });
    }

    if (win.navigator.connection) {
        const connProps = ['type', 'effectiveType', 'downlink', 'downlinkMax', 'rtt', 'saveData'];
        const connSpoofs = { type: 'unknown', effectiveType: '4g', downlink: 10, downlinkMax: Infinity, rtt: 50, saveData: false };
        connProps.forEach(prop => {
            try {
                Object.defineProperty(win.navigator.connection, prop, {
                    get: () => connSpoofs[prop],
                    configurable: true
                });
            } catch (_) {}
        });
    }

    if (SITE === 'ometv') {
        const hideRestart = () => {
            document.querySelectorAll('button, a, div, span').forEach(el => {
                const txt = (el.innerText || '').trim().toLowerCase();
                if (txt === 'restart' || txt === 'neustart') {
                    el.style.setProperty('display', 'none', 'important');
                }
            });
        };
        new MutationObserver(hideRestart).observe(document.documentElement, {
            childList: true, subtree: true
        });
    }

    const _origGetUserMedia = win.navigator.mediaDevices.getUserMedia.bind(win.navigator.mediaDevices);
    win.navigator.mediaDevices.getUserMedia = async function (constraints) {
        const stream = await _origGetUserMedia(constraints);
        const videoTrack = stream.getVideoTracks()[0];
        if (!videoTrack) return stream;
        const settings = videoTrack.getSettings();
        const w = settings.width || 640;
        const h = settings.height || 480;
        const canvas = document.createElement('canvas');
        canvas.width = w;
        canvas.height = h;
        const ctx = canvas.getContext('2d');
        const hiddenVideo = document.createElement('video');
        hiddenVideo.srcObject = new MediaStream([videoTrack]);
        hiddenVideo.muted = true;
        hiddenVideo.playsInline = true;
        hiddenVideo.play();
        let running = true;
        const draw = () => {
            if (!running) return;
            if (hiddenVideo.readyState >= hiddenVideo.HAVE_CURRENT_DATA) {
                if (canvas.width !== hiddenVideo.videoWidth || canvas.height !== hiddenVideo.videoHeight) {
                    canvas.width = hiddenVideo.videoWidth;
                    canvas.height = hiddenVideo.videoHeight;
                }
                ctx.save();
                ctx.translate(canvas.width, 0);
                ctx.scale(-1, 1);
                ctx.drawImage(hiddenVideo, 0, 0, canvas.width, canvas.height);
                ctx.restore();
            }
            requestAnimationFrame(draw);
        };
        draw();
        const flippedStream = canvas.captureStream(30);
        videoTrack.addEventListener('ended', () => { running = false; });
        stream.getAudioTracks().forEach(t => flippedStream.addTrack(t));
        return flippedStream;
    };

    function initUI() {
        if (document.getElementById('geo-live')) return;
        const style = document.createElement('style');
        style.textContent = `
            .geo-panel {
                position: fixed;
                width: 290px;
                min-width: 220px;
                min-height: 80px;
                background: rgba(12,12,16,0.95);
                border: 1px solid rgba(255,255,255,0.06);
                border-radius: 14px;
                box-shadow: 0 12px 48px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.03) inset;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
                font-size: 12px;
                color: #d4d4d8;
                z-index: 99999;
                backdrop-filter: blur(16px);
                -webkit-backdrop-filter: blur(16px);
                user-select: none;
                display: flex;
                flex-direction: column;
                overflow: hidden;
                transition: box-shadow 0.3s;
            }
            .geo-panel:hover { box-shadow: 0 16px 56px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05) inset; }
            #geo-live { top: 18px; right: 18px; height: 480px; }
            .geo-panel.minimized { height: auto !important; }
            .geo-panel.minimized .geo-tab-bar,
            .geo-panel.minimized .geo-panel-body,
            .geo-panel.minimized #geo-session-bar,
            .geo-panel.minimized #geo-autoskip-row,
            .geo-panel.minimized #geo-own-bar,
            .geo-panel.minimized #geo-dup-banner,
            .geo-panel.minimized .geo-credits,
            .geo-panel.minimized .geo-resize-handle { display: none !important; }
            .geo-panel-header {
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 11px 14px 9px;
                background: rgba(255,255,255,0.02);
                border-bottom: 1px solid rgba(255,255,255,0.06);
                cursor: grab;
                flex-shrink: 0;
            }
            .geo-panel-header:active { cursor: grabbing; }
            .geo-panel-title {
                font-size: 12px;
                font-weight: 600;
                letter-spacing: 0.03em;
                color: #f4f4f5;
                display: flex;
                align-items: center;
                gap: 8px;
            }
            #geo-status-dot {
                width: 7px; height: 7px;
                border-radius: 50%;
                background: #3f3f46;
                flex-shrink: 0;
                transition: background 0.3s, box-shadow 0.3s;
            }
            #geo-status-dot.loading { background: #f59e0b; box-shadow: 0 0 6px rgba(245,158,11,0.5); animation: geo-pulse 1.2s ease-in-out infinite; }
            #geo-status-dot.ok      { background: #22c55e; box-shadow: 0 0 6px rgba(34,197,94,0.4); }
            #geo-status-dot.error   { background: #ef4444; box-shadow: 0 0 6px rgba(239,68,68,0.4); }
            @keyframes geo-pulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
            .geo-hdr-actions {
                display: flex;
                align-items: center;
                gap: 2px;
            }
            .geo-icon-btn {
                display: flex; align-items: center; justify-content: center;
                width: 28px; height: 28px;
                background: none; border: none; border-radius: 6px;
                color: #52525b; cursor: pointer;
                transition: color 0.2s, background 0.2s;
            }
            .geo-icon-btn:hover { color: #d4d4d8; background: rgba(255,255,255,0.06); }
            .geo-icon-btn.active { color: #22c55e; }
            .geo-icon-btn svg { display: block; }
            .geo-panel-body {
                padding: 14px 14px 16px;
                overflow-y: auto;
                flex: 1;
                min-height: 0;
            }
            .geo-panel-body::-webkit-scrollbar { width: 4px; }
            .geo-panel-body::-webkit-scrollbar-track { background: transparent; }
            .geo-panel-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 2px; }
            .geo-panel-body::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
            .geo-resize-handle {
                position: absolute; bottom: 0; right: 0;
                width: 18px; height: 18px; cursor: se-resize; z-index: 2;
                background: linear-gradient(135deg, transparent 55%, rgba(255,255,255,0.1) 55%);
                border-bottom-right-radius: 14px;
                transition: background 0.2s;
            }
            .geo-resize-handle:hover {
                background: linear-gradient(135deg, transparent 55%, rgba(255,255,255,0.2) 55%);
            }
            #geo-own-bar {
                display: flex; align-items: center; gap: 8px;
                padding: 7px 14px;
                background: rgba(255,255,255,0.02);
                border-bottom: 1px solid rgba(255,255,255,0.05);
                flex-shrink: 0;
            }
            #geo-own-flag { font-size: 14px; line-height: 1; display: flex; align-items: center; color: #52525b; }
            #geo-own-text { font-size: 10px; color: #52525b; letter-spacing: 0.02em; }
            #geo-flag { display: none; }
            #geo-waiting { text-align: center; color: #52525b; font-size: 11px; padding: 28px 0; line-height: 1.5; }
            .geo-row {
                display: flex; justify-content: space-between;
                align-items: baseline; padding: 5px 0;
                border-bottom: 1px solid rgba(255,255,255,0.04); gap: 10px;
            }
            .geo-row:last-child { border-bottom: none; }
            .geo-label { color: #71717a; font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; flex-shrink: 0; font-weight: 500; }
            .geo-value { color: #d4d4d8; text-align: right; word-break: break-all; font-size: 11.5px; }
            .geo-value.highlight { color: #60a5fa; font-weight: 600; }
            .geo-btn {
                display: flex; align-items: center; justify-content: center; gap: 6px;
                width: 100%; padding: 7px 0;
                border-radius: 8px; font-size: 11px;
                cursor: pointer; letter-spacing: 0.03em;
                transition: background 0.2s, border-color 0.2s;
                font-family: inherit;
            }
            .geo-btn svg { flex-shrink: 0; }
            #geo-copy-btn {
                margin-top: 10px;
                background: rgba(96,165,250,0.08);
                border: 1px solid rgba(96,165,250,0.2);
                color: #60a5fa;
            }
            #geo-copy-btn:hover { background: rgba(96,165,250,0.16); border-color: rgba(96,165,250,0.3); }
            #geo-copy-btn.copied { color: #22c55e; border-color: rgba(34,197,94,0.25); background: rgba(34,197,94,0.08); }
            #geo-hist-header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
            #geo-hist-count { color: #52525b; font-size: 10px; font-weight: 500; }
            #geo-hist-clear {
                display: flex; align-items: center; gap: 4px;
                background: none; border: none; color: #52525b; font-size: 10px;
                cursor: pointer; padding: 2px 4px; border-radius: 4px;
                transition: color 0.2s, background 0.2s;
            }
            #geo-hist-clear:hover { color: #ef4444; background: rgba(239,68,68,0.08); }
            #geo-hist-clear svg { display: block; }
            #geo-history-list { display: flex; flex-direction: column; gap: 8px; }
            #geo-hist-empty { text-align: center; color: #52525b; font-size: 11px; padding: 24px 0; }
            .geo-hist-entry {
                background: rgba(255,255,255,0.02);
                border: 1px solid rgba(255,255,255,0.05);
                border-radius: 12px; overflow: hidden;
                cursor: pointer;
                transition: background 0.2s, border-color 0.2s, box-shadow 0.3s;
                animation: geo-hist-in 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
            }
            .geo-hist-entry:nth-child(1) { animation-delay: 0s; }
            .geo-hist-entry:nth-child(2) { animation-delay: 0.03s; }
            .geo-hist-entry:nth-child(3) { animation-delay: 0.06s; }
            .geo-hist-entry:nth-child(4) { animation-delay: 0.09s; }
            .geo-hist-entry:nth-child(5) { animation-delay: 0.12s; }
            .geo-hist-entry:nth-child(n+6) { animation-delay: 0.15s; }
            @keyframes geo-hist-in {
                0% { opacity: 0; transform: translateY(8px); }
                100% { opacity: 1; transform: translateY(0); }
            }
            .geo-hist-entry:hover { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.08); }
            .geo-hist-entry.open { box-shadow: 0 4px 20px rgba(0,0,0,0.25); border-color: rgba(255,255,255,0.08); }
            .geo-hist-hero {
                display: flex; align-items: center; gap: 10px;
                padding: 10px 12px 8px;
            }
            .geo-hist-flag { font-size: 28px; flex-shrink: 0; line-height: 1; }
            .geo-hist-info { flex: 1; min-width: 0; }
            .geo-hist-ip { color: #f4f4f5; font-weight: 700; font-size: 13px; letter-spacing: -0.01em; }
            .geo-hist-loc { color: #71717a; font-size: 10px; margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
            .geo-hist-badges { display: flex; gap: 4px; margin-top: 4px; flex-wrap: wrap; }
            .geo-hist-badge {
                font-size: 8px; padding: 1px 6px; border-radius: 8px;
                font-weight: 600; letter-spacing: 0.02em;
            }
            .geo-hist-badge.vpn { background: rgba(239,68,68,0.12); color: #ef4444; }
            .geo-hist-badge.clean { background: rgba(34,197,94,0.12); color: #22c55e; }
            .geo-hist-badge.risk-low { background: rgba(34,197,94,0.1); color: #22c55e; }
            .geo-hist-badge.risk-med { background: rgba(245,158,11,0.12); color: #f59e0b; }
            .geo-hist-badge.risk-high { background: rgba(239,68,68,0.12); color: #ef4444; }
            .geo-hist-meta {
                display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex-shrink: 0;
            }
            .geo-hist-time { color: #52525b; font-size: 9px; }
            .geo-hist-star {
                display: flex; align-items: center; justify-content: center;
                background: none; border: none; cursor: pointer;
                padding: 2px; line-height: 1;
                color: #3f3f46; transition: color 0.2s, transform 0.2s;
            }
            .geo-hist-star:hover { color: #facc15; transform: scale(1.15); }
            .geo-hist-star.active { color: #facc15; }
            .geo-hist-star svg { display: block; }
            .geo-hist-detail { display: none; padding: 0 10px 10px; }
            .geo-hist-entry.open .geo-hist-detail { display: flex; flex-direction: column; gap: 6px; }
            .geo-hist-dcard {
                background: rgba(255,255,255,0.025);
                border: 1px solid rgba(255,255,255,0.04);
                border-radius: 8px; padding: 8px 10px;
            }
            .geo-hist-dcard-hdr {
                display: flex; align-items: center; gap: 5px;
                margin-bottom: 5px; color: #52525b; font-size: 8.5px;
                text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600;
            }
            .geo-hist-dcard-hdr svg { flex-shrink: 0; color: #3f3f46; }
            .geo-hist-detail-row {
                display: flex; justify-content: space-between;
                padding: 3px 4px; gap: 6px; border-radius: 4px;
                margin: 0 -4px; cursor: pointer; position: relative;
                transition: background 0.15s;
            }
            .geo-hist-detail-row:hover { background: rgba(255,255,255,0.04); }
            .geo-hist-detail-row span:first-child { color: #52525b; text-transform: uppercase; font-size: 9px; flex-shrink: 0; font-weight: 500; }
            .geo-hist-detail-row span:last-child { color: #a1a1aa; text-align: right; word-break: break-all; font-size: 10.5px; }
            .geo-hist-copy {
                display: flex; align-items: center; justify-content: center; gap: 4px;
                width: 100%; padding: 5px 0;
                background: rgba(96,165,250,0.06);
                border: 1px solid rgba(96,165,250,0.15);
                border-radius: 7px; color: #60a5fa; font-size: 10px;
                cursor: pointer; transition: background 0.2s;
                font-family: inherit;
            }
            .geo-hist-copy:hover { background: rgba(96,165,250,0.14); }
            .geo-hist-expand-hint {
                text-align: center; font-size: 9px; color: #3f3f46;
                padding: 2px 0 6px; letter-spacing: 0.04em;
                transition: color 0.2s;
            }
            .geo-hist-entry:hover .geo-hist-expand-hint { color: #52525b; }
            .geo-hist-entry.open .geo-hist-expand-hint { display: none; }
            #geo-map-btn {
                display: flex; align-items: center; justify-content: center; gap: 6px;
                margin-top: 8px; width: 100%; padding: 7px 0;
                background: rgba(34,197,94,0.06);
                border: 1px solid rgba(34,197,94,0.2);
                border-radius: 8px; color: #22c55e; font-size: 11px;
                text-decoration: none;
                letter-spacing: 0.03em; transition: background 0.2s, border-color 0.2s;
                box-sizing: border-box;
            }
            #geo-map-btn:hover { background: rgba(34,197,94,0.14); border-color: rgba(34,197,94,0.3); }
            #geo-map-btn svg { flex-shrink: 0; }
            #geo-export-bar { display: flex; gap: 6px; margin-bottom: 10px; }
            .geo-export-btn {
                display: flex; align-items: center; justify-content: center; gap: 4px;
                flex: 1; padding: 6px 0;
                background: rgba(255,255,255,0.03);
                border: 1px solid rgba(255,255,255,0.07);
                border-radius: 7px; color: #71717a; font-size: 10px;
                cursor: pointer; transition: all 0.2s;
                font-family: inherit;
            }
            .geo-export-btn:hover { background: rgba(255,255,255,0.08); color: #d4d4d8; border-color: rgba(255,255,255,0.12); }
            .geo-export-btn svg { display: block; flex-shrink: 0; }
            .geo-credits {
                padding: 7px 14px 9px;
                border-top: 1px solid rgba(255,255,255,0.04);
                text-align: center; flex-shrink: 0;
                display: flex; align-items: center; justify-content: center; gap: 5px;
            }
            .geo-credits a {
                display: inline-flex; align-items: center; gap: 4px;
                color: #3f3f46; font-size: 9px; text-decoration: none;
                letter-spacing: 0.03em; transition: color 0.2s;
            }
            .geo-credits a:hover { color: #71717a; }
            .geo-credits svg { display: block; }
            #geo-session-bar {
                display: flex; align-items: center; gap: 5px;
                padding: 7px 14px;
                background: rgba(255,255,255,0.015);
                border-bottom: 1px solid rgba(255,255,255,0.05);
                flex-shrink: 0; flex-wrap: wrap;
            }
            .geo-sess-badge {
                font-size: 9px; padding: 2px 7px; border-radius: 10px;
                letter-spacing: 0.03em; font-weight: 600;
            }
            .geo-sess-total { background: rgba(96,165,250,0.12); color: #60a5fa; }
            .geo-sess-vpn   { background: rgba(239,68,68,0.12); color: #ef4444; }
            .geo-sess-clean { background: rgba(34,197,94,0.12); color: #22c55e; }
            #geo-dup-banner {
                display: none; padding: 7px 14px;
                background: rgba(251,191,36,0.08);
                border-bottom: 1px solid rgba(251,191,36,0.15);
                font-size: 10px; color: #fbbf24; flex-shrink: 0;
                align-items: center; gap: 6px;
            }
            #geo-risk-bar {
                height: 3px; border-radius: 2px; margin-top: 6px;
                background: rgba(255,255,255,0.06); overflow: hidden;
            }
            #geo-risk-fill {
                height: 100%; width: 0; border-radius: 2px;
                transition: width 0.5s ease, background 0.4s;
            }
            #geo-autoskip-row {
                display: flex; align-items: center; justify-content: space-between;
                padding: 7px 14px;
                background: rgba(255,255,255,0.015);
                border-bottom: 1px solid rgba(255,255,255,0.05);
                flex-shrink: 0;
            }
            #geo-autoskip-label { font-size: 10px; color: #71717a; font-weight: 500; }
            #geo-autoskip-toggle {
                width: 34px; height: 18px; border-radius: 9px;
                background: #27272a; border: 1px solid rgba(255,255,255,0.08); cursor: pointer;
                position: relative; transition: background 0.25s, border-color 0.25s; flex-shrink: 0;
            }
            #geo-autoskip-toggle::after {
                content: ''; position: absolute;
                top: 2px; left: 2px;
                width: 12px; height: 12px; border-radius: 50%;
                background: #52525b; transition: left 0.25s, background 0.25s;
            }
            #geo-autoskip-toggle.on { background: rgba(239,68,68,0.25); border-color: rgba(239,68,68,0.3); }
            #geo-autoskip-toggle.on::after { left: 18px; background: #ef4444; }
            #geo-hist-search {
                width: 100%; box-sizing: border-box;
                background: rgba(255,255,255,0.04);
                border: 1px solid rgba(255,255,255,0.08);
                border-radius: 8px; color: #d4d4d8; font-size: 11px;
                padding: 7px 10px 7px 30px; margin-bottom: 10px; outline: none;
                font-family: inherit;
                transition: border-color 0.2s, background 0.2s;
            }
            #geo-hist-search::placeholder { color: #52525b; }
            #geo-hist-search:focus { border-color: rgba(96,165,250,0.35); background: rgba(255,255,255,0.06); }
            .geo-search-wrap {
                position: relative; margin-bottom: 0;
            }
            .geo-search-wrap svg {
                position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
                color: #52525b; pointer-events: none;
            }
            .geo-search-wrap #geo-hist-search { margin-bottom: 10px; }
            #geo-stats-btn { }
            #geo-stats-panel { display: none; margin-bottom: 10px; padding: 8px; background: rgba(255,255,255,0.02); border-radius: 8px; border: 1px solid rgba(255,255,255,0.04); }
            #geo-stats-panel.open { display: block; }
            .geo-stat-row {
                display: flex; align-items: center; gap: 6px;
                margin-bottom: 5px;
            }
            .geo-stat-row:last-child { margin-bottom: 0; }
            .geo-stat-label { font-size: 10px; color: #71717a; min-width: 80px; flex-shrink: 0; }
            .geo-stat-barwrap {
                flex: 1; height: 6px; background: rgba(255,255,255,0.05);
                border-radius: 3px; overflow: hidden;
            }
            .geo-stat-bar { height: 100%; border-radius: 3px; background: linear-gradient(90deg, #3b82f6, #60a5fa); min-width: 4px; transition: width 0.3s; }
            .geo-stat-count { font-size: 9px; color: #52525b; min-width: 20px; text-align: right; font-weight: 500; }

            .geo-risk-indicator {
                display: inline-flex; align-items: center; gap: 4px;
            }
            .geo-risk-dot {
                width: 6px; height: 6px; border-radius: 50%;
                display: inline-block; flex-shrink: 0;
            }
            .geo-info-section {
                display: flex; flex-direction: column; gap: 8px;
            }
            .geo-card {
                background: rgba(255,255,255,0.025);
                border: 1px solid rgba(255,255,255,0.05);
                border-radius: 10px; padding: 10px 12px;
                animation: geo-card-in 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
            }
            .geo-card:nth-child(1) { animation-delay: 0s; }
            .geo-card:nth-child(2) { animation-delay: 0.04s; }
            .geo-card:nth-child(3) { animation-delay: 0.08s; }
            .geo-card:nth-child(4) { animation-delay: 0.12s; }
            .geo-card:nth-child(5) { animation-delay: 0.16s; }
            @keyframes geo-card-in {
                0% { opacity: 0; transform: translateY(8px); }
                100% { opacity: 1; transform: translateY(0); }
            }
            .geo-card-hdr {
                display: flex; align-items: center; gap: 6px;
                margin-bottom: 6px; color: #71717a; font-size: 9px;
                text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600;
            }
            .geo-card-hdr svg { flex-shrink: 0; color: #52525b; }
            .geo-card .geo-row { border-bottom-color: rgba(255,255,255,0.03); padding: 4px 0; }
            .geo-card .geo-row:last-child { border-bottom: none; }
            .geo-card .geo-label { font-size: 9.5px; color: #52525b; }
            .geo-card .geo-value { font-size: 11px; }
            .geo-row { cursor: pointer; position: relative; border-radius: 4px; padding: 5px 4px; margin: 0 -4px; transition: background 0.15s; }
            .geo-row:hover { background: rgba(255,255,255,0.04); }
            .geo-row-copied {
                position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
                font-size: 9px; color: #22c55e; font-weight: 600;
                pointer-events: none; letter-spacing: 0.02em;
                animation: geo-copied-in 0.25s ease;
            }
            @keyframes geo-copied-in {
                0% { opacity: 0; transform: translateY(-50%) translateX(4px); }
                100% { opacity: 1; transform: translateY(-50%) translateX(0); }
            }
            .geo-tab-bar {
                display: flex; border-bottom: 1px solid rgba(255,255,255,0.06);
                background: rgba(255,255,255,0.015); flex-shrink: 0;
            }
            .geo-tab {
                flex: 1; padding: 8px 0; text-align: center;
                font-size: 11px; font-weight: 600; color: #52525b;
                cursor: pointer; border: none; background: none;
                border-bottom: 2px solid transparent;
                transition: color 0.2s, border-color 0.2s, background 0.2s;
                font-family: inherit; letter-spacing: 0.03em;
                display: flex; align-items: center; justify-content: center; gap: 5px;
            }
            .geo-tab:hover { color: #a1a1aa; background: rgba(255,255,255,0.02); }
            .geo-tab.active { color: #60a5fa; border-bottom-color: #3b82f6; }
            .geo-tab svg { display: block; }
            .geo-tab-content { display: none; }
            .geo-tab-content.active { display: block; }
            .geo-hero {
                display: flex; align-items: center; gap: 12px;
                padding: 8px 0 4px; margin-bottom: 4px;
                animation: geo-hero-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
            }
            @keyframes geo-hero-in {
                0% { opacity: 0; transform: scale(0.9); }
                100% { opacity: 1; transform: scale(1); }
            }
            .geo-hero-flag { font-size: 36px; line-height: 1; flex-shrink: 0; }
            .geo-hero-info { flex: 1; min-width: 0; }
            .geo-hero-ip { font-size: 15px; font-weight: 700; color: #f4f4f5; letter-spacing: -0.01em; }
            .geo-hero-loc { font-size: 11px; color: #71717a; margin-top: 2px; }
            .geo-hero-badges { display: flex; gap: 4px; margin-top: 5px; flex-wrap: wrap; }
            .geo-hero-badge {
                font-size: 9px; padding: 2px 7px; border-radius: 10px;
                font-weight: 600; letter-spacing: 0.02em;
                animation: geo-badge-in 0.3s ease both;
            }
            .geo-hero-badge.vpn { background: rgba(239,68,68,0.12); color: #ef4444; animation-delay: 0.15s; }
            .geo-hero-badge.clean { background: rgba(34,197,94,0.12); color: #22c55e; animation-delay: 0.15s; }
            .geo-hero-badge.risk-low { background: rgba(34,197,94,0.1); color: #22c55e; animation-delay: 0.2s; }
            .geo-hero-badge.risk-med { background: rgba(245,158,11,0.12); color: #f59e0b; animation-delay: 0.2s; }
            .geo-hero-badge.risk-high { background: rgba(239,68,68,0.12); color: #ef4444; animation-delay: 0.2s; }
            @keyframes geo-badge-in {
                0% { opacity: 0; transform: scale(0.8); }
                100% { opacity: 1; transform: scale(1); }
            }
            #geo-risk-bar {
                height: 3px; border-radius: 2px; margin-top: 2px;
                background: rgba(255,255,255,0.06); overflow: hidden;
            }
            .geo-modal-overlay {
                position: fixed; inset: 0;
                background: rgba(0,0,0,0.65);
                backdrop-filter: blur(6px);
                -webkit-backdrop-filter: blur(6px);
                z-index: 999999;
                display: flex; align-items: center; justify-content: center;
                animation: geo-overlay-in 0.25s ease;
            }
            @keyframes geo-overlay-in {
                0% { opacity: 0; }
                100% { opacity: 1; }
            }
            .geo-modal {
                width: 380px; max-width: 92vw; max-height: 85vh;
                background: rgba(18,18,22,0.98);
                border: 1px solid rgba(255,255,255,0.08);
                border-radius: 16px;
                box-shadow: 0 24px 80px rgba(0,0,0,0.6);
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
                color: #d4d4d8;
                overflow: hidden;
                animation: geo-modal-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
            }
            @keyframes geo-modal-in {
                0% { opacity: 0; transform: scale(0.95) translateY(10px); }
                100% { opacity: 1; transform: scale(1) translateY(0); }
            }
            .geo-modal-hdr {
                display: flex; align-items: center; gap: 10px;
                padding: 20px 24px 14px;
                border-bottom: 1px solid rgba(255,255,255,0.06);
            }
            .geo-modal-hdr svg { color: #60a5fa; flex-shrink: 0; }
            .geo-modal-hdr-text { flex: 1; }
            .geo-modal-hdr h3 {
                margin: 0; font-size: 15px; font-weight: 700; color: #f4f4f5;
                letter-spacing: -0.01em;
            }
            .geo-modal-hdr p {
                margin: 3px 0 0; font-size: 11px; color: #71717a;
            }
            .geo-modal-body {
                padding: 16px 24px;
                overflow-y: auto; max-height: 50vh;
                font-size: 12px; line-height: 1.65; color: #a1a1aa;
            }
            .geo-modal-body h4 {
                font-size: 11px; font-weight: 600; color: #d4d4d8;
                margin: 14px 0 6px; text-transform: uppercase; letter-spacing: 0.05em;
            }
            .geo-modal-body h4:first-child { margin-top: 0; }
            .geo-modal-body ul {
                margin: 4px 0 0; padding-left: 16px;
            }
            .geo-modal-body ul li {
                margin-bottom: 3px;
            }
            .geo-modal-body .geo-tos-highlight {
                background: rgba(96,165,250,0.06);
                border: 1px solid rgba(96,165,250,0.12);
                border-radius: 8px; padding: 8px 10px;
                margin: 8px 0; font-size: 11px; color: #93c5fd;
            }
            .geo-modal-footer {
                display: flex; align-items: center; gap: 8px;
                padding: 14px 24px 18px;
                border-top: 1px solid rgba(255,255,255,0.06);
            }
            .geo-modal-footer .geo-m-btn {
                flex: 1; padding: 9px 0;
                border-radius: 8px; border: none;
                font-size: 12px; font-weight: 600;
                cursor: pointer; font-family: inherit;
                transition: background 0.2s, color 0.2s;
                letter-spacing: 0.01em;
            }
            .geo-m-btn-secondary {
                background: rgba(255,255,255,0.05); color: #a1a1aa;
            }
            .geo-m-btn-secondary:hover { background: rgba(255,255,255,0.1); color: #d4d4d8; }
            .geo-m-btn-primary {
                background: #3b82f6; color: #fff;
            }
            .geo-m-btn-primary:hover { background: #2563eb; }
            .geo-m-btn-danger {
                background: rgba(239,68,68,0.12); color: #ef4444;
            }
            .geo-m-btn-danger:hover { background: rgba(239,68,68,0.2); }
        `;
        document.head.appendChild(style);

        const livePanel = document.createElement('div');
        livePanel.id = 'geo-live';
        livePanel.className = 'geo-panel';
        livePanel.innerHTML = `
            <div class="geo-panel-header">
                <div class="geo-panel-title">
                    <span id="geo-status-dot"></span>
                    IP Geolocation
                </div>
                <div class="geo-hdr-actions">
                    <button class="geo-icon-btn" id="geo-telemetry-btn" title="Anonymous usage analytics">${IC.activity()}</button>
                    <button class="geo-icon-btn" id="geo-api-btn" title="Set IPInfo API key">${IC.key()}</button>
                    <button class="geo-icon-btn" id="geo-notif-btn" title="Notifications">${IC.bell()}</button>
                    <button class="geo-icon-btn" id="geo-stats-btn" title="Country stats">${IC.chart()}</button>
                    <button class="geo-icon-btn geo-minimize-btn" title="Minimize">${IC.minus()}</button>
                </div>
            </div>
            <div id="geo-session-bar">
                <span class="geo-sess-badge geo-sess-total" id="geo-sess-total">0 total</span>
                <span class="geo-sess-badge geo-sess-vpn" id="geo-sess-vpn">0 VPN</span>
                <span class="geo-sess-badge geo-sess-clean" id="geo-sess-clean">0 clean</span>
            </div>
            <div id="geo-autoskip-row">
                <span id="geo-autoskip-label">Auto-skip VPN/Proxy</span>
                <button id="geo-autoskip-toggle" title="Toggle auto-skip"></button>
            </div>
            <div id="geo-own-bar">
                <span id="geo-own-flag">${IC.globe()}</span>
                <span id="geo-own-text">You: detecting&#8230;</span>
            </div>
            <div id="geo-dup-banner"></div>
            <div class="geo-tab-bar">
                <button class="geo-tab active" data-tab="info">${IC.globe(12)} Info</button>
                <button class="geo-tab" data-tab="history">${IC.clock(12)} History</button>
            </div>
            <div class="geo-panel-body">
                <div class="geo-tab-content active" id="geo-tab-info">
                    <div id="geo-flag"></div>
                    <div id="geo-waiting">Waiting for connection&#8230;</div>
                    <div id="geo-rows"></div>
                    <button id="geo-copy-btn" class="geo-btn" style="display:none">${IC.copy()} Copy Info</button>
                    <a id="geo-map-btn" target="_blank" rel="noopener" style="display:none">${IC.mapPin()} Open in Maps</a>
                </div>
                <div class="geo-tab-content" id="geo-tab-history">
                    <div id="geo-hist-header-row">
                        <span id="geo-hist-count">0 entries</span>
                        <button id="geo-hist-clear">${IC.trash(12)} Clear All</button>
                    </div>
                    <div id="geo-export-bar">
                        <button class="geo-export-btn" id="geo-export-json">${IC.download(11)} JSON</button>
                        <button class="geo-export-btn" id="geo-export-csv">${IC.download(11)} CSV</button>
                    </div>
                    <div id="geo-stats-panel"></div>
                    <div class="geo-search-wrap">
                        ${IC.search(12)}
                        <input id="geo-hist-search" type="text" placeholder="Search IP, city, country..." />
                    </div>
                    <div id="geo-history-list"></div>
                    <div id="geo-hist-empty">No history yet</div>
                </div>
            </div>
            <div class="geo-credits"><a href="https://github.com/w0wzahh" target="_blank" rel="noopener">${IC.github(11)} w0wzahh</a> <span style="color:#27272a;font-size:9px">&middot;</span> <a href="https://gitlab.com/MysteryBlokHed" target="_blank" rel="noopener" style="color:#3f3f46;font-size:9px;text-decoration:none">CamSurf detection based on MysteryBlokHed</a></div>
            <div class="geo-resize-handle"></div>
        `;
        document.body.appendChild(livePanel);

        const makeDraggable = (panel) => {
            const header = panel.querySelector('.geo-panel-header');
            let dragging = false, ox = 0, oy = 0;
            header.addEventListener('mousedown', (e) => {
                if (e.target.tagName === 'BUTTON') return;
                e.preventDefault();
                const rect = panel.getBoundingClientRect();
                panel.style.left = rect.left + 'px';
                panel.style.top = rect.top + 'px';
                panel.style.right = 'unset';
                dragging = true;
                ox = e.clientX - rect.left;
                oy = e.clientY - rect.top;
            });
            document.addEventListener('mousemove', (e) => {
                if (dragging) {
                    panel.style.left = (e.clientX - ox) + 'px';
                    panel.style.top = (e.clientY - oy) + 'px';
                }
            });
            document.addEventListener('mouseup', () => { dragging = false; });
        };

        const makeResizable = (panel) => {
            const handle = panel.querySelector('.geo-resize-handle');
            let resizing = false, rsx = 0, rsy = 0, rsw = 0, rsh = 0;
            handle.addEventListener('mousedown', (e) => {
                e.preventDefault();
                e.stopPropagation();
                resizing = true;
                rsx = e.clientX; rsy = e.clientY;
                rsw = panel.offsetWidth; rsh = panel.offsetHeight;
            });
            document.addEventListener('mousemove', (e) => {
                if (resizing) {
                    panel.style.width = Math.max(200, rsw + e.clientX - rsx) + 'px';
                    panel.style.height = Math.max(80, rsh + e.clientY - rsy) + 'px';
                }
            });
            document.addEventListener('mouseup', () => { resizing = false; });
        };

        const makeMinimizable = (panel) => {
            const btn = panel.querySelector('.geo-minimize-btn');
            btn.addEventListener('click', () => {
                panel.classList.toggle('minimized');
                btn.innerHTML = panel.classList.contains('minimized') ? IC.plus() : IC.minus();
            });
        };

        makeDraggable(livePanel);
        makeResizable(livePanel);
        makeMinimizable(livePanel);

        livePanel.querySelectorAll('.geo-tab').forEach(tab => {
            tab.addEventListener('click', () => {
                livePanel.querySelectorAll('.geo-tab').forEach(t => t.classList.remove('active'));
                livePanel.querySelectorAll('.geo-tab-content').forEach(c => c.classList.remove('active'));
                tab.classList.add('active');
                document.getElementById('geo-tab-' + tab.dataset.tab).classList.add('active');
            });
        });

        const dot = document.getElementById('geo-status-dot');
        const flagEl = document.getElementById('geo-flag');
        const waitingEl = document.getElementById('geo-waiting');
        const rowsEl = document.getElementById('geo-rows');
        const copyBtn = document.getElementById('geo-copy-btn');
        const dupBanner = document.getElementById('geo-dup-banner');
        let currentEntry = null;
        let relayHiddenShown = false;

        const setStatus = (state) => { dot.className = state; };

        const updateSessionBar = () => {
            document.getElementById('geo-sess-total').textContent = sessionTotal + ' total';
            document.getElementById('geo-sess-vpn').textContent = sessionVPN + ' VPN';
            document.getElementById('geo-sess-clean').textContent = sessionClean + ' clean';
        };

        const buildRows = (fields, extra) => {
            const groups = [
                { title: 'Identity', icon: IC.user(11), keys: ['Name (PTR)', 'IP', 'Hostname'] },
                { title: 'Security', icon: IC.shield(11), keys: ['Proxy / VPN', 'Risk Score', 'Connection Type', 'Last Seen'] },
                { title: 'Location', icon: IC.mapPin(11), keys: ['Address', 'Country', 'Region', 'City', 'Postal', 'Continent', 'Latitude', 'Longitude'] },
                { title: 'Network', icon: IC.activity(11), keys: ['Timezone', 'ISP / Provider', 'Organisation', 'ASN'] },
            ];
            const fieldMap = {};
            fields.forEach(([label, value, hi]) => { fieldMap[label] = { value, hi }; });
            let html = '';
            if (extra && extra.hero) html += extra.hero;
            html += '<div class="geo-info-section">';
            groups.forEach(g => {
                const rows = g.keys.filter(k => fieldMap[k]).map(k => {
                    const f = fieldMap[k];
                    return '<div class="geo-row" data-copy-value="' + esc(f.value) + '">' +
                        '<span class="geo-label">' + esc(k) + '</span>' +
                        '<span class="geo-value' + (f.hi ? ' highlight' : '') + '">' + esc(f.value) + '</span>' +
                        '</div>';
                });
                if (!rows.length) return;
                html += '<div class="geo-card">' +
                    '<div class="geo-card-hdr">' + g.icon + ' ' + g.title + '</div>' +
                    rows.join('') +
                    '</div>';
            });
            html += '</div>';
            rowsEl.innerHTML = html;
            rowsEl.querySelectorAll('.geo-row[data-copy-value]').forEach(row => {
                row.addEventListener('click', () => {
                    const val = row.getAttribute('data-copy-value');
                    if (!val || val === 'N/A') return;
                    navigator.clipboard.writeText(val).then(() => {
                        const existing = row.querySelector('.geo-row-copied');
                        if (existing) existing.remove();
                        const badge = document.createElement('span');
                        badge.className = 'geo-row-copied';
                        badge.textContent = 'Copied!';
                        row.appendChild(badge);
                        setTimeout(() => badge.remove(), 1500);
                    });
                });
            });
        };

        const clearPanelFn = () => {
            currentEntry = null;
            relayHiddenShown = false;
            flagEl.innerHTML = '';
            rowsEl.innerHTML = '';
            copyBtn.style.display = 'none';
            dupBanner.style.display = 'none';
            dupBanner.innerHTML = '';
            waitingEl.textContent = 'Waiting for connection\u2026';
            waitingEl.style.display = '';
            setStatus('');
        };

        const playPing = () => {
            if (!soundEnabled) return;
            try {
                const ctx = new (win.AudioContext || win.webkitAudioContext)();
                const osc = ctx.createOscillator();
                const gain = ctx.createGain();
                osc.connect(gain);
                gain.connect(ctx.destination);
                osc.type = 'sine';
                osc.frequency.setValueAtTime(880, ctx.currentTime);
                osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.2);
                gain.gain.setValueAtTime(0.12, ctx.currentTime);
                gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
                osc.start(ctx.currentTime);
                osc.stop(ctx.currentTime + 0.4);
                osc.onended = () => ctx.close();
            } catch (_) {}
        };

        const extractNameFromHostname = (hostname) => {
            if (!hostname || hostname === 'N/A') return null;
            const SKIP = new Set([
                'dynamic','static','broadband','cable','dsl','fiber','fibre','pool','ip',
                'ptr','rdns','rev','reverse','customer','residential','home','host',
                'node','dhcp','dhcppc','lease','assigned','allocated','client','user',
                'subscriber','access','internet','service','provider','network','net',
                'com','org','edu','gov','info','biz','int','arpa','local','in','addr',
                'cpe','cust','bredband','adsl','vdsl','fttp','ftth','ppp','dial',
                'mobile','cellular','lte','nas','gpon','olt','bbnet',
            ]);
            const parts = hostname.toLowerCase().split(/[\.\-_]/);
            const candidates = parts.filter(p =>
                /^[a-z]{3,16}$/.test(p) && !SKIP.has(p) && !/^(\d)/.test(p)
            );
            if (candidates.length >= 2) {
                return candidates.slice(0, 2)
                    .map(s => s.charAt(0).toUpperCase() + s.slice(1))
                    .join(' ');
            } else if (candidates.length === 1) {
                const c = candidates[0];
                return c.charAt(0).toUpperCase() + c.slice(1);
            }
            return null;
        };

        const entryToText = (e) => [
            'Name      : ' + (e.name || 'N/A') + (e.name ? ' (from PTR)' : ''),
            'IP        : ' + e.ip,
            'Proxy/VPN : ' + (e.proxy || 'N/A'),
            'Risk Score: ' + (e.risk != null ? e.risk + '/100' : 'N/A'),
            'Conn Type : ' + (e.connType || 'N/A'),
            'Last Seen : ' + (e.lastSeen || 'N/A'),
            'Address   : ' + (e.address || 'N/A'),
            'Hostname  : ' + e.hostname,
            'Country   : ' + e.country,
            'Region    : ' + e.region,
            'City      : ' + e.city,
            'Postal    : ' + e.postal,
            'Continent : ' + (e.continent || 'N/A'),
            'Latitude  : ' + (e.lat != null ? Math.abs(e.lat).toFixed(4) + '\u00b0 ' + (e.lat >= 0 ? 'N' : 'S') : 'N/A'),
            'Longitude : ' + (e.lon != null ? Math.abs(e.lon).toFixed(4) + '\u00b0 ' + (e.lon >= 0 ? 'E' : 'W') : 'N/A'),
            'Timezone  : ' + e.timezone,
            'ISP       : ' + e.isp,
            'Organisatn: ' + (e.organisation || 'N/A'),
            'ASN       : ' + e.asn,
            'Time      : ' + e.timestamp,
        ].join('\n');

        copyBtn.addEventListener('click', () => {
            if (!currentEntry) return;
            navigator.clipboard.writeText(entryToText(currentEntry)).then(() => {
                copyBtn.innerHTML = IC.check() + ' Copied!';
                copyBtn.classList.add('copied');
                setTimeout(() => {
                    copyBtn.innerHTML = IC.copy() + ' Copy Info';
                    copyBtn.classList.remove('copied');
                }, 2000);
            });
        });

        const renderStats = () => {
            const entries = loadHistory();
            const statsPanel = document.getElementById('geo-stats-panel');
            if (!statsPanel.classList.contains('open')) return;
            const map = {};
            entries.forEach(e => {
                const k = (e.flag || '') + ' ' + e.country;
                map[k] = (map[k] || 0) + 1;
            });
            const sorted = Object.entries(map).sort((a, b) => b[1] - a[1]).slice(0, 8);
            const max = sorted.length ? sorted[0][1] : 1;
            statsPanel.innerHTML = sorted.length ? sorted.map(([country, count]) =>
                '<div class="geo-stat-row">' +
                '<span class="geo-stat-label">' + esc(country.slice(0, 18)) + '</span>' +
                '<div class="geo-stat-barwrap"><div class="geo-stat-bar" style="width:' + Math.round(count / max * 100) + '%"></div></div>' +
                '<span class="geo-stat-count">' + count + '</span>' +
                '</div>'
            ).join('') : '<div style="color:#444;font-size:10px;padding:4px 0">No data yet</div>';
        };

        const renderHistory = (filter) => {
            const list = document.getElementById('geo-history-list');
            const empty = document.getElementById('geo-hist-empty');
            const countEl = document.getElementById('geo-hist-count');
            let entries = loadHistory();
            countEl.textContent = entries.length + ' entr' + (entries.length === 1 ? 'y' : 'ies');
            const stars = loadStars();
            const q = (filter || '').toLowerCase().trim();
            if (q) {
                entries = entries.filter(e =>
                    (e.ip || '').includes(q) ||
                    (e.city || '').toLowerCase().includes(q) ||
                    (e.country || '').toLowerCase().includes(q) ||
                    (e.isp || '').toLowerCase().includes(q) ||
                    (e.hostname || '').toLowerCase().includes(q)
                );
            }
            if (!entries.length) {
                list.innerHTML = '';
                empty.style.display = '';
                return;
            }
            empty.style.display = 'none';
            list.innerHTML = entries.map((e, i) => {
                const starred = !!stars[e.ip];
                const loc = (e.city !== 'N/A' ? e.city + ', ' : '') + e.country;

                let badges = '';
                if (e.proxy && e.proxy.toLowerCase() === 'yes') {
                    badges += '<span class="geo-hist-badge vpn">VPN</span>';
                } else {
                    badges += '<span class="geo-hist-badge clean">Clean</span>';
                }
                if (e.risk != null) {
                    const r = +e.risk;
                    const rc = r >= 66 ? 'risk-high' : r >= 33 ? 'risk-med' : 'risk-low';
                    badges += '<span class="geo-hist-badge ' + rc + '">Risk ' + r + '</span>';
                }

                const detailGroups = [
                    { title: 'Identity', icon: IC.user(10), rows: [
                        ['Name (PTR)', e.name || 'N/A'], ['IP', e.ip], ['Hostname', e.hostname]
                    ]},
                    { title: 'Security', icon: IC.shield(10), rows: [
                        ['Proxy / VPN', e.proxy || 'N/A'],
                        ['Risk Score', e.risk != null ? e.risk + '/100' : 'N/A'],
                        ['Connection Type', e.connType || 'N/A'],
                        ['Last Seen', e.lastSeen || 'N/A']
                    ]},
                    { title: 'Location', icon: IC.mapPin(10), rows: [
                        ['Address', e.address || 'N/A'], ['Region', e.region], ['Postal', e.postal],
                        ['Continent', e.continent || 'N/A'],
                        ['Latitude', e.lat != null ? Math.abs(e.lat).toFixed(4) + '\u00b0 ' + (e.lat >= 0 ? 'N' : 'S') : 'N/A'],
                        ['Longitude', e.lon != null ? Math.abs(e.lon).toFixed(4) + '\u00b0 ' + (e.lon >= 0 ? 'E' : 'W') : 'N/A']
                    ]},
                    { title: 'Network', icon: IC.activity(10), rows: [
                        ['Timezone', e.timezone], ['ISP / Provider', e.isp],
                        ['Organisation', e.organisation || 'N/A'], ['ASN', e.asn]
                    ]}
                ];
                const detailHtml = detailGroups.map(g => {
                    const rows = g.rows.map(([l, v]) =>
                        '<div class="geo-hist-detail-row" data-copy-value="' + esc(v) + '"><span>' + esc(l) + '</span><span>' + esc(v) + '</span></div>'
                    ).join('');
                    return '<div class="geo-hist-dcard"><div class="geo-hist-dcard-hdr">' + g.icon + ' ' + g.title + '</div>' + rows + '</div>';
                }).join('');

                return '<div class="geo-hist-entry" data-idx="' + i + '" data-ip="' + esc(e.ip) + '">' +
                    '<div class="geo-hist-hero">' +
                    '<span class="geo-hist-flag">' + esc(e.flag || '') + '</span>' +
                    '<div class="geo-hist-info">' +
                    '<div class="geo-hist-ip">' + esc(e.ip) + '</div>' +
                    '<div class="geo-hist-loc">' + esc(loc) + '</div>' +
                    '<div class="geo-hist-badges">' + badges + '</div>' +
                    '</div>' +
                    '<div class="geo-hist-meta">' +
                    '<span class="geo-hist-time">' + esc(e.timestamp) + '</span>' +
                    '<button class="geo-hist-star' + (starred ? ' active' : '') + '" data-ip="' + esc(e.ip) + '" title="Star">' + (starred ? IC.starFill() : IC.star()) + '</button>' +
                    '</div>' +
                    '</div>' +
                    '<div class="geo-hist-expand-hint">' + IC.chevDown(10) + '</div>' +
                    '<div class="geo-hist-detail">' + detailHtml +
                    '<button class="geo-hist-copy" data-idx="' + i + '">' + IC.copy(11) + ' Copy All</button>' +
                    '</div>' +
                    '</div>';
            }).join('');

            list.querySelectorAll('.geo-hist-entry').forEach(el => {
                el.addEventListener('click', (e) => {
                    if (e.target.closest('.geo-hist-copy') || e.target.closest('.geo-hist-star') || e.target.closest('.geo-hist-detail-row')) return;
                    el.classList.toggle('open');
                });
            });
            list.querySelectorAll('.geo-hist-detail-row[data-copy-value]').forEach(row => {
                row.addEventListener('click', (ev) => {
                    ev.stopPropagation();
                    const val = row.getAttribute('data-copy-value');
                    if (!val || val === 'N/A') return;
                    navigator.clipboard.writeText(val).then(() => {
                        const existing = row.querySelector('.geo-row-copied');
                        if (existing) existing.remove();
                        const badge = document.createElement('span');
                        badge.className = 'geo-row-copied';
                        badge.textContent = 'Copied!';
                        row.appendChild(badge);
                        setTimeout(() => badge.remove(), 1500);
                    });
                });
            });
            list.querySelectorAll('.geo-hist-copy').forEach(btn => {
                btn.addEventListener('click', (ev) => {
                    ev.stopPropagation();
                    const allEntries = loadHistory();
                    const entry = allEntries[+btn.dataset.idx];
                    if (!entry) return;
                    navigator.clipboard.writeText(entryToText(entry)).then(() => {
                        btn.innerHTML = IC.check(11) + ' Copied!';
                        setTimeout(() => { btn.innerHTML = IC.copy(11) + ' Copy All'; }, 2000);
                    });
                });
            });
            list.querySelectorAll('.geo-hist-star').forEach(btn => {
                btn.addEventListener('click', (ev) => {
                    ev.stopPropagation();
                    const ip = btn.dataset.ip;
                    toggleStar(ip);
                    const nowStarred = isStarred(ip);
                    btn.innerHTML = nowStarred ? IC.starFill() : IC.star();
                    btn.classList.toggle('active', nowStarred);
                });
            });
        };

        document.getElementById('geo-hist-clear').addEventListener('click', () => {
            localStorage.removeItem(HISTORY_KEY);
            renderHistory();
            renderStats();
        });

        document.getElementById('geo-stats-btn').addEventListener('click', () => {
            const p = document.getElementById('geo-stats-panel');
            p.classList.toggle('open');
            renderStats();
        });

        document.getElementById('geo-hist-search').addEventListener('input', (e) => {
            renderHistory(e.target.value);
        });

        const exportEntries = (format) => {
            const entries = loadHistory();
            if (!entries.length) return;
            let content, mime, filename;
            if (format === 'json') {
                content = JSON.stringify(entries, null, 2);
                mime = 'application/json';
                filename = 'geo_history.json';
            } else {
                const headers = ['IP','Country','City','Region','Postal','Address','Proxy','Risk','Connection Type','Latitude','Longitude','Continent','Timezone','ISP','Organisation','ASN','Hostname','Name','Last Seen','Timestamp'];
                const rows = entries.map(e => [
                    e.ip, e.country, e.city, e.region, e.postal,
                    e.address || 'N/A', e.proxy || 'N/A',
                    e.risk != null ? e.risk : 'N/A',
                    e.connType || 'N/A',
                    e.lat != null ? e.lat.toFixed(6) : 'N/A',
                    e.lon != null ? e.lon.toFixed(6) : 'N/A',
                    e.continent || 'N/A',
                    e.timezone, e.isp, e.organisation || 'N/A', e.asn, e.hostname,
                    e.name || 'N/A', e.lastSeen || 'N/A', e.timestamp
                ].map(v => '"' + String(v || '').replace(/"/g, '""') + '"').join(','));
                content = [headers.join(','), ...rows].join('\n');
                mime = 'text/csv';
                filename = 'geo_history.csv';
            }
            const blob = new Blob([content], { type: mime });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url; a.download = filename; a.click();
            setTimeout(() => URL.revokeObjectURL(url), 1000);
        };

        document.getElementById('geo-export-json').addEventListener('click', () => exportEntries('json'));
        document.getElementById('geo-export-csv').addEventListener('click', () => exportEntries('csv'));

        document.getElementById('geo-autoskip-toggle').addEventListener('click', () => {
            autoSkipVPN = !autoSkipVPN;
            document.getElementById('geo-autoskip-toggle').classList.toggle('on', autoSkipVPN);
        });

        document.getElementById('geo-notif-btn').addEventListener('click', () => {
            if (typeof Notification === 'undefined') return;
            if (Notification.permission === 'default') {
                Notification.requestPermission();
            } else if (Notification.permission === 'granted') {
                notifEnabled = !notifEnabled;
                const nb = document.getElementById('geo-notif-btn');
                nb.innerHTML = notifEnabled ? IC.bell() : IC.bellOff();
                nb.classList.toggle('active', notifEnabled);
            }
        });

        const telemetryBtn = document.getElementById('geo-telemetry-btn');
        const updateTelemetryButton = () => {
            const consent = getTelemetryConsent() === 'yes';
            telemetryBtn.innerHTML = consent ? IC.chart() : IC.activity();
            telemetryBtn.classList.toggle('active', consent);
            telemetryBtn.title = consent
                ? 'Anonymous analytics: ON (click to disable)'
                : 'Anonymous analytics: OFF (click to enable)';
        };

        const showModal = (opts) => {
            return new Promise((resolve) => {
                const overlay = document.createElement('div');
                overlay.className = 'geo-modal-overlay';
                const modal = document.createElement('div');
                modal.className = 'geo-modal';
                let bodyHtml = '';
                if (opts.body) bodyHtml = opts.body;
                let footerHtml = '';
                (opts.buttons || []).forEach((b, i) => {
                    footerHtml += '<button class="geo-m-btn ' + (b.cls || 'geo-m-btn-secondary') + '" data-idx="' + i + '">' + b.label + '</button>';
                });
                modal.innerHTML =
                    '<div class="geo-modal-hdr">' +
                        (opts.icon || IC.shield(22)) +
                        '<div class="geo-modal-hdr-text">' +
                            '<h3>' + (opts.title || '') + '</h3>' +
                            (opts.subtitle ? '<p>' + opts.subtitle + '</p>' : '') +
                        '</div>' +
                    '</div>' +
                    '<div class="geo-modal-body">' + bodyHtml + '</div>' +
                    '<div class="geo-modal-footer">' + footerHtml + '</div>';
                overlay.appendChild(modal);
                document.body.appendChild(overlay);
                const close = (val) => {
                    overlay.style.animation = 'none';
                    overlay.style.opacity = '0';
                    overlay.style.transition = 'opacity 0.15s';
                    setTimeout(() => { overlay.remove(); resolve(val); }, 160);
                };
                modal.querySelectorAll('.geo-m-btn').forEach(btn => {
                    btn.addEventListener('click', () => {
                        const b = opts.buttons[+btn.dataset.idx];
                        close(b.value);
                    });
                });
                overlay.addEventListener('click', (e) => {
                    if (e.target === overlay) close(null);
                });
            });
        };

        const TOS_BODY =
            '<h4>Terms of Service</h4>' +
            '<p>By using Ome.tv IP Geolocation ("the Script"), you agree to the following terms:</p>' +
            '<ul>' +
            '<li><strong>Personal use only.</strong> The Script is provided for informational and educational purposes. Do not use it to harass, stalk, or harm others.</li>' +
            '<li><strong>No warranty.</strong> The Script is provided "as is" without warranty of any kind. Geolocation data is approximate and may be inaccurate.</li>' +
            '<li><strong>Third-party APIs.</strong> The Script queries ipinfo.io and proxycheck.io. Your use is subject to their respective terms of service and rate limits.</li>' +
            '<li><strong>API keys.</strong> You are responsible for your own API keys. Do not share them publicly.</li>' +
            '<li><strong>Privacy.</strong> The Script does not collect personal data by default. If you opt in to anonymous analytics, only a random install ID, script version, date, and coarse locale country are sent to our telemetry endpoint.</li>' +
            '<li><strong>Liability.</strong> The author is not liable for any misuse, data inaccuracy, or damages arising from use of the Script.</li>' +
            '</ul>' +
            '<h4>Anonymous Usage Analytics</h4>' +
            '<p>To help improve the Script, you may opt in to anonymous analytics.</p>' +
            '<div class="geo-tos-highlight">' +
                '<strong>Data sent:</strong> Random install ID, script version, date, and coarse locale country.<br>' +
                '<strong>Not sent:</strong> IP addresses, chat content, geolocation lookups, or any personal information.' +
            '</div>' +
            '<p>You can change your analytics preference at any time by clicking the analytics icon in the panel header.</p>';

        const askTelemetryConsentIfNeeded = async () => {
            const existing = getTelemetryConsent();
            if (existing === 'yes' || existing === 'no') {
                updateTelemetryButton();
                return;
            }
            const result = await showModal({
                icon: IC.shield(22),
                title: 'Terms of Service & Privacy',
                subtitle: 'Please review before continuing',
                body: TOS_BODY,
                buttons: [
                    { label: 'Decline Analytics', cls: 'geo-m-btn-secondary', value: 'decline' },
                    { label: 'Accept & Enable Analytics', cls: 'geo-m-btn-primary', value: 'accept' }
                ]
            });
            const accepted = result === 'accept';
            setTelemetryConsent(accepted);
            updateTelemetryButton();
            if (accepted) {
                pingTelemetryInstallOnce('first_run_prompt');
                pingTelemetrySessionDaily();
                pingTelemetryVersion();
            }
        };

        telemetryBtn.addEventListener('click', async () => {
            const enabled = getTelemetryConsent() === 'yes';
            if (enabled) {
                const result = await showModal({
                    icon: IC.activity(22),
                    title: 'Disable Analytics',
                    subtitle: 'Anonymous usage analytics',
                    body: '<p>Are you sure you want to disable anonymous usage analytics? No data will be sent after disabling.</p>',
                    buttons: [
                        { label: 'Keep Enabled', cls: 'geo-m-btn-secondary', value: 'keep' },
                        { label: 'Disable', cls: 'geo-m-btn-danger', value: 'disable' }
                    ]
                });
                if (result !== 'disable') return;
                setTelemetryConsent(false);
                updateTelemetryButton();
                return;
            }
            if (!telemetryEndpointConfigured()) {
                await showModal({
                    icon: IC.warn(22),
                    title: 'Not Configured',
                    body: '<p>Analytics endpoint is not configured in the script source yet.</p>',
                    buttons: [{ label: 'OK', cls: 'geo-m-btn-secondary', value: 'ok' }]
                });
                updateTelemetryButton();
                return;
            }
            const result = await showModal({
                icon: IC.shield(22),
                title: 'Enable Analytics',
                subtitle: 'Anonymous usage analytics',
                body: '<p>Enable anonymous usage analytics to help improve the Script?</p>' +
                    '<div class="geo-tos-highlight">' +
                        '<strong>Data sent:</strong> Random install ID, script version, date, and coarse locale country only.<br>' +
                        '<strong>Not sent:</strong> IP addresses, chat content, or geolocation lookups.' +
                    '</div>',
                buttons: [
                    { label: 'Not Now', cls: 'geo-m-btn-secondary', value: 'no' },
                    { label: 'Enable Analytics', cls: 'geo-m-btn-primary', value: 'yes' }
                ]
            });
            if (result !== 'yes') return;
            setTelemetryConsent(true);
            updateTelemetryButton();
            pingTelemetryInstallOnce('settings_toggle');
            pingTelemetrySessionDaily();
            pingTelemetryVersion();
        });

        const fetchOwnInfo = () => {
            const apiKey = loadApiKey();
            if (!apiKey) {
                ownIP = null;
                document.getElementById('geo-own-flag').innerHTML = IC.globe();
                document.getElementById('geo-own-text').textContent = 'You: add IPInfo API key (key icon)';
                return;
            }
            gmFetch('https://ipinfo.io/json?token=' + encodeURIComponent(apiKey))
                .then(r => r.json())
                .then(d => {
                    ownIP = d.ip;
                    const flag = countryFlag(d.country);
                    const country = d.country ? regionNames.of(d.country) : 'Unknown';
                    document.getElementById('geo-own-flag').innerHTML = flag || IC.globe();
                    document.getElementById('geo-own-text').textContent =
                        'You: ' + d.ip + ' \u2014 ' + country;
                })
                .catch(() => {
                    ownIP = null;
                    document.getElementById('geo-own-text').textContent = 'You: invalid API key';
                });
        };

        const configureApiKey = () => {
            const current = loadApiKey();
            const entered = window.prompt('Enter your IPInfo API token (saved locally in your browser):', current);
            if (entered == null) return;
            const clean = entered.trim();
            if (!clean) {
                localStorage.removeItem(API_KEY_STORAGE);
                ownIP = null;
                setStatus('error');
                waitingEl.textContent = 'IPInfo API key removed. Click the key icon to add your key.';
                waitingEl.style.display = '';
                document.getElementById('geo-own-flag').innerHTML = IC.globe();
                document.getElementById('geo-own-text').textContent = 'You: add IPInfo API key (key icon)';
                return;
            }
            saveApiKey(clean);
            setStatus('loading');
            waitingEl.textContent = 'API key saved. Waiting for connection\u2026';
            waitingEl.style.display = '';
            fetchOwnInfo();
        };

        document.getElementById('geo-api-btn').addEventListener('click', configureApiKey);

        document.addEventListener('keydown', (e) => {
            if (e.key === 'c' || e.key === 'C') {
                const active = document.activeElement;
                if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) return;
                if (!currentEntry) return;
                navigator.clipboard.writeText(entryToText(currentEntry)).then(() => {
                    copyBtn.innerHTML = IC.check() + ' Copied!';
                    copyBtn.classList.add('copied');
                    setTimeout(() => {
                        copyBtn.innerHTML = IC.copy() + ' Copy Info';
                        copyBtn.classList.remove('copied');
                    }, 2000);
                });
            }
        });

        const getLocationFn = async (ip) => {
            try {
                const apiKey = loadApiKey();
                if (!apiKey) {
                    setStatus('error');
                    flagEl.innerHTML = IC.key(28);
                    waitingEl.textContent = 'No IPInfo API key set. Click the key icon in the header.';
                    waitingEl.style.display = '';
                    return;
                }

                const [ipResp, proxyResp] = await Promise.all([
                    gmFetch('https://ipinfo.io/' + ip + '?token=' + encodeURIComponent(apiKey)),
                    gmFetch('https://proxycheck.io/v2/' + ip + '?key=' + _pk + '&vpn=1&asn=1&seen=1').catch(() => null)
                ]);
                if (!ipResp.ok) throw new Error('HTTP ' + ipResp.status);
                const d = await ipResp.json();

                if (d.bogon) {
                    setStatus('error');
                    flagEl.innerHTML = IC.lock(28);
                    waitingEl.textContent = 'Private / bogon address';
                    waitingEl.style.display = '';
                    return;
                }

                const TYPE_LABELS = { DCH: 'Datacenter', VPN: 'VPN', TOR: 'Tor Exit', PUB: 'Public Proxy', WEB: 'Web Proxy', SES: 'Search Engine' };
                let proxyType = 'N/A';
                let riskScore = null;
                let pcLat = null, pcLon = null, pcCity = null, pcRegion = null, pcCountry = null;
                let pcProvider = null, pcOrg = null, pcContinent = null, pcConnType = null, pcLastSeen = null;
                try {
                    if (proxyResp && proxyResp.ok) {
                        const pd = await proxyResp.json();
                        const ipData = pd[ip];
                        if (ipData) {
                            proxyType = ipData.proxy === 'yes'
                                ? (TYPE_LABELS[ipData.type] || ipData.type || 'Proxy')
                                : 'Clean';
                            if (ipData.risk != null) riskScore = parseInt(ipData.risk, 10);
                            if (ipData.latitude != null) pcLat = parseFloat(ipData.latitude);
                            if (ipData.longitude != null) pcLon = parseFloat(ipData.longitude);
                            if (ipData.city) pcCity = ipData.city;
                            if (ipData.region) pcRegion = ipData.region;
                            if (ipData.country) pcCountry = ipData.country;
                            if (ipData.provider) pcProvider = ipData.provider;
                            if (ipData.organisation) pcOrg = ipData.organisation;
                            if (ipData.continent) pcContinent = ipData.continent;
                            if (ipData.type) pcConnType = TYPE_LABELS[ipData.type] || ipData.type;
                            if (ipData['last seen human']) pcLastSeen = ipData['last seen human'];
                            else if (ipData.lastseen) pcLastSeen = ipData.lastseen;
                        }
                    }
                } catch (_) {}

                const isVPN = proxyType !== 'Clean' && proxyType !== 'N/A';

                if (autoSkipVPN && isVPN) {
                    const skipBtn = document.querySelector(
                        SITE === 'camsurf'
                            ? '.rv_buttons .btn-next, .next-btn, button[class*="next"], [data-action="next"]'
                            : SITE === 'umingle'
                            ? 'button[class*="next"], button[class*="skip"], [data-action="next"], [data-action="skip"], button.next-btn'
                            : '.controls__btn-decline, [data-action="decline"], button[class*="skip"], button[class*="next"], button[class*="decline"]'
                    );
                    if (skipBtn) skipBtn.click();
                }

                const flag = countryFlag(d.country);
                const countryLabel = d.country
                    ? regionNames.of(d.country) + ' (' + d.country + ')'
                    : 'Unknown';
                const org = d.org || '';
                const orgParts = org.match(/^(AS\d+)\s(.+)$/);
                const asn = orgParts ? orgParts[1] : 'N/A';
                const isp = orgParts ? orgParts[2] : (org || 'N/A');
                const provider = pcProvider || isp;
                const organisation = pcOrg || (orgParts ? orgParts[2] : '') || 'N/A';
                const continent = pcContinent || 'N/A';
                const connType = pcConnType || 'N/A';
                const lastSeen = pcLastSeen || null;
                const hostname = d.hostname || 'N/A';
                let rdnsHostname = hostname;

                if (hostname === 'N/A') {
                    try {
                        const octets = ip.split('.').reverse().join('.');
                        const dnsResp = await gmFetch('https://dns.google/resolve?name=' + octets + '.in-addr.arpa&type=PTR');
                        if (dnsResp.ok) {
                            const dnsData = await dnsResp.json();
                            if (dnsData.Answer && dnsData.Answer.length > 0) {
                                let ptr = dnsData.Answer[dnsData.Answer.length - 1].data;
                                if (ptr && ptr.endsWith('.')) ptr = ptr.slice(0, -1);
                                if (ptr) rdnsHostname = ptr;
                            }
                        }
                    } catch (_) {}
                }
                const name = extractNameFromHostname(rdnsHostname);

                let lat = null, lon = null;
                if (d.loc && d.loc.includes(',')) {
                    const parts = d.loc.split(',');
                    lat = parseFloat(parts[0]);
                    lon = parseFloat(parts[1]);
                }
                if (pcLat != null && pcLon != null) {
                    if (lat == null || lon == null) {
                        lat = pcLat; lon = pcLon;
                    } else {
                        const dLat = Math.abs(lat - pcLat), dLon = Math.abs(lon - pcLon);
                        if (dLat > 0.5 || dLon > 0.5) {
                            lat = pcLat; lon = pcLon;
                        }
                    }
                }
                const fmtCoord = (v, pos, neg) => {
                    if (v == null || isNaN(v)) return 'N/A';
                    const dir = v >= 0 ? pos : neg;
                    return Math.abs(v).toFixed(4) + '° ' + dir;
                };
                const latStr = fmtCoord(lat, 'N', 'S');
                const lonStr = fmtCoord(lon, 'E', 'W');
                const locRaw = (lat != null && lon != null) ? lat.toFixed(6) + ',' + lon.toFixed(6) : 'N/A';

                const city = pcCity || d.city || 'N/A';
                const region = pcRegion || d.region || 'N/A';

                const addrLine = [city !== 'N/A' ? city : d.city, region !== 'N/A' ? region : d.region].filter(Boolean).join(', ');
                const address = [addrLine, d.postal, d.country ? regionNames.of(d.country) : ''].filter(Boolean).join(' ') || 'N/A';
                const now = new Date();
                const timestamp = now.toLocaleDateString('en-GB') + ' '
                    + now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });

                const prevHistory = loadHistory();
                const prevEntry = prevHistory.find(e => e.ip === ip);

                sessionTotal++;
                if (isVPN) sessionVPN++;
                else if (proxyType === 'Clean') sessionClean++;
                updateSessionBar();

                currentEntry = {
                    ip: d.ip, flag, country: countryLabel,
                    name: name || null,
                    hostname: rdnsHostname, region: region,
                    city: city, postal: d.postal || 'N/A',
                    lat: lat, lon: lon, loc: locRaw,
                    timezone: d.timezone || 'N/A',
                    isp: provider, asn, timestamp, address, proxy: proxyType,
                    risk: riskScore,
                    organisation, continent, connType,
                    lastSeen
                };

                if (prevEntry) {
                    dupBanner.innerHTML = IC.warn() + ' Seen before \u2014 last seen ' + esc(prevEntry.timestamp);
                    dupBanner.style.display = 'flex';
                }

                playPing();
                pushHistory(currentEntry);
                renderHistory();
                renderStats();
                flagEl.innerHTML = '';
                waitingEl.style.display = 'none';
                setStatus('ok');
                copyBtn.style.display = '';

                const mapBtn = document.getElementById('geo-map-btn');
                if (lat != null && lon != null) {
                    mapBtn.href = 'https://maps.google.com/?q=' + lat.toFixed(6) + ',' + lon.toFixed(6);
                    mapBtn.style.display = 'flex';
                } else {
                    mapBtn.style.display = 'none';
                }

                const riskColor = riskScore == null ? '#3f3f46' : riskScore >= 76 ? '#ef4444' : riskScore >= 51 ? '#f97316' : riskScore >= 26 ? '#f59e0b' : '#22c55e';
                const riskLabel = riskScore != null ? riskScore + '/100' : 'N/A';
                const riskClass = riskScore == null ? '' : riskScore >= 51 ? 'risk-high' : riskScore >= 26 ? 'risk-med' : 'risk-low';
                const locStr = [city !== 'N/A' ? city : null, region !== 'N/A' ? region : null, d.country ? regionNames.of(d.country) : ''].filter(Boolean).join(', ');

                let heroHtml = '<div class="geo-hero">';
                heroHtml += '<div class="geo-hero-flag">' + (flag || IC.globe(32)) + '</div>';
                heroHtml += '<div class="geo-hero-info">';
                heroHtml += '<div class="geo-hero-ip">' + esc(d.ip) + '</div>';
                heroHtml += '<div class="geo-hero-loc">' + esc(locStr || 'Unknown location') + '</div>';
                heroHtml += '<div class="geo-hero-badges">';
                if (isVPN) heroHtml += '<span class="geo-hero-badge vpn">' + esc(proxyType) + '</span>';
                else if (proxyType === 'Clean') heroHtml += '<span class="geo-hero-badge clean">Clean</span>';
                if (riskScore != null) heroHtml += '<span class="geo-hero-badge ' + riskClass + '">Risk ' + riskScore + '/100</span>';
                heroHtml += '</div></div></div>';
                heroHtml += '<div id="geo-risk-bar"><div id="geo-risk-fill" style="width:' + (riskScore != null ? riskScore : 0) + '%;background:' + riskColor + '"></div></div>';

                buildRows([
                    ['Name (PTR)', name || 'N/A', !!name],
                    ['IP', d.ip, true],
                    ['Proxy / VPN', proxyType, isVPN],
                    ['Risk Score', riskLabel, riskScore != null && riskScore > 50],
                    ['Connection Type', connType, false],
                    ['Last Seen', lastSeen || 'N/A', false],
                    ['Address', address, false],
                    ['Hostname', rdnsHostname, rdnsHostname !== 'N/A'],
                    ['Country', countryLabel, false],
                    ['Region', region, false],
                    ['City', city, false],
                    ['Postal', d.postal || 'N/A', false],
                    ['Continent', continent, false],
                    ['Latitude', latStr, false],
                    ['Longitude', lonStr, false],
                    ['Timezone', d.timezone || 'N/A', false],
                    ['ISP / Provider', provider, false],
                    ['Organisation', organisation, false],
                    ['ASN', asn, false],
                ], { hero: heroHtml });

                if (notifEnabled && typeof Notification !== 'undefined' && Notification.permission === 'granted') {
                    try {
                        const _siteLabel = SITE === 'camsurf' ? 'CamSurf' : SITE === 'umingle' ? 'Umingle' : 'OmeTV';
                        new Notification(_siteLabel + ' - New Connection', {
                            body: (d.ip || ip) + ' - ' + (city !== 'N/A' ? city : '') + ', ' + (d.country || '') + (isVPN ? ' [' + proxyType + ']' : ''),
                            icon: 'https://www.google.com/s2/favicons?domain=' + location.hostname
                        });
                    } catch (_) {}
                }
            } catch (err) {
                setStatus('error');
                flagEl.innerHTML = IC.warn(28);
                waitingEl.textContent = 'Error: ' + err.message;
                waitingEl.style.display = '';
            }
        };

        fetchOwnInfo();

        updateTelemetryButton();
        askTelemetryConsentIfNeeded();
        pingTelemetryInstallOnce('startup');
        pingTelemetrySessionDaily();
        pingTelemetryVersion();

        if (!loadApiKey()) {
            setStatus('error');
            flagEl.innerHTML = IC.key(28);
            waitingEl.textContent = 'Set your IPInfo API key with the key button to enable lookups.';
            waitingEl.style.display = '';
        }

        onRelayHidden = () => {
            if (relayHiddenShown) return;
            relayHiddenShown = true;
            setStatus('error');
            flagEl.innerHTML = IC.lock(28);
            waitingEl.textContent = 'Connected - IP hidden by relay';
            waitingEl.style.display = '';
        };

        onIPDetected = (ip) => {
            if (ip === ownIP) return;
            clearPanelFn();
            setStatus('loading');
            flagEl.innerHTML = IC.globe(28);
            waitingEl.textContent = 'Looking up ' + ip + '...';
            waitingEl.style.display = '';
            getLocationFn(ip);
        };

        uiReady = true;
        updateSessionBar();
        renderHistory();
        renderStats();
        if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
            Notification.requestPermission();
        }
        pendingIPs.forEach(ip => onIPDetected(ip));
        pendingIPs.length = 0;
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initUI);
    } else {
        initUI();
    }
})();