IP Geolocation Tools

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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();
    }
})();