Greasy Fork

Greasy Fork is available in English.

WorldGuessr GeoAssist

Advanced WorldGuessr overlay toolkit - lat/long bands, offset radius circle, country outlines, and exact pin marker. Left Shift to open.

当前为 2026-05-04 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WorldGuessr GeoAssist
// @namespace    http://greasyfork.icu/
// @version      1.0
// @description  Advanced WorldGuessr overlay toolkit - lat/long bands, offset radius circle, country outlines, and exact pin marker. Left Shift to open.
// @author       WhosGravy
// @match        *://www.worldguessr.com/*
// @match        *://worldguessr.com/*
// @icon         data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0%25' stop-color='%2322d3ee'/><stop offset='100%25' stop-color='%23818cf8'/></linearGradient></defs><rect x='4' y='4' width='56' height='56' rx='16' fill='url(%23g)'/><path fill='%23e2e8f0' d='M32 14a14 14 0 0 0-14 14c0 8.8 11 22.4 12.2 23.9a2.5 2.5 0 0 0 3.7 0C35 50.4 46 36.8 46 28a14 14 0 0 0-14-14zm0 19.2a5.2 5.2 0 1 1 0-10.4 5.2 5.2 0 0 1 0 10.4z'/></svg>
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const _Proxy = window.Proxy;
    const _Reflect = window.Reflect;
    const SCRIPT_VERSION = '1.0';
    const PIN_GLYPH_SVG = '<svg viewBox="0 0 24 24" role="img" aria-hidden="true" focusable="false"><path fill="#e2e8f0" d="M12 2.2a7.2 7.2 0 0 0-7.2 7.2c0 4.5 5.6 11.4 6.2 12.2a1.3 1.3 0 0 0 2 0c.6-.8 6.2-7.7 6.2-12.2A7.2 7.2 0 0 0 12 2.2zm0 9.9a2.7 2.7 0 1 1 0-5.4 2.7 2.7 0 0 1 0 5.4z"/></svg>';
    const MAP_PIN_IMAGE_URL = 'https://cdn.discordapp.com/attachments/1470671042975502502/1500377533265219765/bluepin.png?ex=69f836f7&is=69f6e577&hm=02350a611af2193ec848c20fa19e69b5ea8d3f1d9bc9bf392493a3913f7e6601&';
    const MAP_PIN_SIZE = [28, 34];
    const SV_HEAT_TILE_URL = 'https://mt1.google.com/vt?lyrs=svv|cb_client:apiv3&style=40,18&x={x}&y={y}&z={z}';

    // Unlock iframes
    const _origSetAttr = Element.prototype.setAttribute;
    Element.prototype.setAttribute = new _Proxy(_origSetAttr, {
        apply(target, thisArg, args) {
            if (thisArg.tagName === 'IFRAME' && args[0].toLowerCase() === 'sandbox') return;
            return _Reflect.apply(target, thisArg, args);
        }
    });

    // Country ISO-2 pool for decoys
    const ALL_COUNTRIES = [
        'AF', 'AL', 'DZ', 'AR', 'AM', 'AU', 'AT', 'AZ', 'BD', 'BE', 'BO', 'BA', 'BR', 'BG', 'KH', 'CM',
        'CA', 'CL', 'CN', 'CO', 'CR', 'CU', 'CZ', 'DK', 'DO', 'EC', 'EG', 'SV', 'ET', 'FI', 'FR', 'GE',
        'DE', 'GH', 'GR', 'GT', 'HN', 'HU', 'IN', 'ID', 'IR', 'IQ', 'IE', 'IL', 'IT', 'JP', 'JO', 'KZ',
        'KE', 'KR', 'KW', 'KG', 'LA', 'LB', 'LY', 'MY', 'MX', 'MD', 'MN', 'MA', 'MZ', 'MM', 'NP', 'NL',
        'NZ', 'NI', 'NE', 'NG', 'NO', 'PK', 'PA', 'PY', 'PE', 'PH', 'PL', 'PT', 'RO', 'RU', 'SA', 'SN',
        'RS', 'ZA', 'ES', 'LK', 'SD', 'SE', 'CH', 'SY', 'TZ', 'TH', 'TN', 'TR', 'UA', 'AE', 'GB', 'US',
        'UY', 'UZ', 'VE', 'VN', 'YE', 'ZM', 'ZW'
    ];

    // Persistence
    const STORAGE_KEY = 'wg_geoassist_v3';

    const DEFAULTS = {
        bandsEnabled: true,
        mode: 'both',
        latHeightKm: 200,
        lngWidthKm: 300,
        latColor: '#ff5a5a',
        lngColor: '#468cff',
        circleEnabled: false,
        circleRadiusKm: 500,
        circleColor: '#22c55e',
        countryEnabled: false,
        countryDecoys: 2,
        countryColor: '#fbbf24',
        pinEnabled: false,
        svHeatEnabled: false,
        svHeatOpacity: 45,
        toggleHotkeyCode: '',
        toggleHotkeyLabel: 'None',
        mapsHotkeyCode: '',
        mapsHotkeyLabel: 'None'
    };

    function loadConfig() {
        try { return { ...DEFAULTS, ...JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') }; }
        catch { return { ...DEFAULTS }; }
    }
    function saveConfig() { localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg)); }

    let cfg = loadConfig();

    // State
    let gameMap = null;
    let latBand = null;
    let lngBand = null;
    let latRandFrac = Math.random();
    let lngRandFrac = Math.random();
    let circleLayer = null;
    let circleRandBearing = Math.random() * Math.PI * 2;
    let circleRandFrac = 0.15 + Math.sqrt(Math.random()) * 0.75;
    let exactPinMarker = null;
    let svHeatLayer = null;
    let countryLayers = [];
    let countryFeatureIndex = null;
    let currentIso = null;
    let lastCoords = null;
    let panel = null;
    let activeTab = 'bands';
    let coordPollId = null;
    let countryBusy = false;
    let countryRetryTimer = null;
    let countryRetryAttempt = 0;
    let countryRunId = 0;
    let lastActivatedCheatKeys = [];
    let awaitingHotkeyCapture = false;
    let awaitingMapsHotkeyCapture = false;

    // Coordinate extraction
    function getCoordinates() {
        try {
            const iframe =
                document.querySelector('#PanoramaIframe') ||
                document.querySelector('iframe[src*="location"]') ||
                document.querySelector('.iframeWithStreetView');
            if (iframe?.src) {
                const loc = new URL(iframe.src).searchParams.get('location');
                if (loc) {
                    const [lat, lng] = loc.split(',').map(Number);
                    if (isFinite(lat) && isFinite(lng)) return { lat, lng };
                }
            }
        } catch {}
        return null;
    }

    // Unit helpers
    const EARTH_RADIUS_KM = 6371;
    const COUNTRY_FETCH_DELAY_MS = 60;
    const COUNTRY_FETCH_RETRIES = 2;
    const COUNTRY_RETRY_DELAY_MS = 1400;
    const COUNTRY_AUTO_RETRY_DELAY_MS = 2200;
    const COUNTRY_AUTO_RETRY_MAX = 8;
    const COUNTRY_DATA_URL = 'https://cdn.jsdelivr.net/gh/datasets/geo-countries@master/data/countries.geojson';
    const kmToLatDeg = km => km / 111.32;
    const kmToLngDeg = (km, lat) => km / (111.32 * Math.max(Math.cos(lat * Math.PI / 180), 0.001));
    const sleep = ms => new Promise(r => setTimeout(r, ms));

    async function fetchJsonWithRetry(url, options = {}, retries = COUNTRY_FETCH_RETRIES) {
        let lastErr;
        for (let i = 0; i <= retries; i++) {
            try {
                const res = await fetch(url, options);
                if (!res.ok) throw new Error('HTTP ' + res.status);
                return await res.json();
            } catch (err) {
                lastErr = err;
                if (i < retries) await sleep(COUNTRY_RETRY_DELAY_MS + i * 500);
            }
        }
        throw lastErr;
    }

    function offsetCoords(lat, lng, distanceKm, bearingRad) {
        const angDist = distanceKm / EARTH_RADIUS_KM;
        const lat1 = lat * Math.PI / 180;
        const lng1 = lng * Math.PI / 180;

        const sinLat1 = Math.sin(lat1);
        const cosLat1 = Math.cos(lat1);
        const sinAng = Math.sin(angDist);
        const cosAng = Math.cos(angDist);

        const lat2 = Math.asin(sinLat1 * cosAng + cosLat1 * sinAng * Math.cos(bearingRad));
        const lng2 = lng1 + Math.atan2(
            Math.sin(bearingRad) * sinAng * cosLat1,
            cosAng - sinLat1 * Math.sin(lat2)
        );

        return {
            lat: lat2 * 180 / Math.PI,
            lng: ((lng2 * 180 / Math.PI + 540) % 360) - 180
        };
    }

    function hexToRgba(hex, alpha) {
        const raw = String(hex || '').trim().replace('#', '');
        const full = raw.length === 3 ? raw.split('').map(c => c + c).join('') : raw;
        if (!/^[0-9a-fA-F]{6}$/.test(full)) return `rgba(255,255,255,${alpha})`;
        const int = parseInt(full, 16);
        const r = (int >> 16) & 255;
        const g = (int >> 8) & 255;
        const b = int & 255;
        return `rgba(${r},${g},${b},${alpha})`;
    }

    // Bands
    function clearBands() {
        if (!gameMap) return;
        if (latBand) { gameMap.removeLayer(latBand); latBand = null; }
        if (lngBand) { gameMap.removeLayer(lngBand); lngBand = null; }
    }

    function drawBands() {
        if (!gameMap || !lastCoords) return;
        clearBands();
        const { lat, lng } = lastCoords;
        if (cfg.mode === 'lat' || cfg.mode === 'both') {
            const h = kmToLatDeg(cfg.latHeightKm);
            const mn = lat - latRandFrac * h;
            latBand = L.rectangle([[mn, -540], [mn + h, 540]], {
                color: hexToRgba(cfg.latColor, 0.85), weight: 1.5,
                fillColor: cfg.latColor, fillOpacity: 0.11,
                interactive: false, noClip: true
            }).addTo(gameMap);
        }
        if (cfg.mode === 'lng' || cfg.mode === 'both') {
            const w = kmToLngDeg(cfg.lngWidthKm, lat);
            const mn = lng - lngRandFrac * w;
            lngBand = L.rectangle([[-90, mn], [90, mn + w]], {
                color: hexToRgba(cfg.lngColor, 0.85), weight: 1.5,
                fillColor: cfg.lngColor, fillOpacity: 0.11,
                interactive: false, noClip: true
            }).addTo(gameMap);
        }
    }

    // Circle
    function clearCircle() {
        if (circleLayer && gameMap) { gameMap.removeLayer(circleLayer); circleLayer = null; }
    }

    function drawCircle() {
        if (!gameMap || !lastCoords) return;
        clearCircle();
        const offsetKm = cfg.circleRadiusKm * circleRandFrac;
        const circleCenter = offsetCoords(lastCoords.lat, lastCoords.lng, offsetKm, circleRandBearing);
        circleLayer = L.circle([circleCenter.lat, circleCenter.lng], {
            radius: cfg.circleRadiusKm * 1000,
            color: hexToRgba(cfg.circleColor, 0.95),
            weight: 2,
            fillColor: cfg.circleColor,
            fillOpacity: 0.14,
            interactive: false
        }).addTo(gameMap);
    }

    // Exact pin
    function clearExactPin() {
        if (exactPinMarker && gameMap) {
            gameMap.removeLayer(exactPinMarker);
            exactPinMarker = null;
        }
    }

    function drawExactPin() {
        if (!gameMap || !lastCoords) return;
        clearExactPin();
        const icon = L.icon({
            iconUrl: MAP_PIN_IMAGE_URL,
            iconSize: MAP_PIN_SIZE,
            iconAnchor: [Math.round(MAP_PIN_SIZE[0] / 2), MAP_PIN_SIZE[1]]
        });
        exactPinMarker = L.marker([lastCoords.lat, lastCoords.lng], { icon, interactive: false }).addTo(gameMap);
    }

    // Street View heat overlay
    function clearSvHeat() {
        if (svHeatLayer && gameMap) {
            gameMap.removeLayer(svHeatLayer);
            svHeatLayer = null;
        }
    }

    function drawSvHeat() {
        if (!gameMap) return;
        clearSvHeat();
        svHeatLayer = L.tileLayer(SV_HEAT_TILE_URL, {
            maxZoom: 20,
            opacity: Math.max(0, Math.min(1, (cfg.svHeatOpacity || 45) / 100)),
            noWrap: true,
            pane: 'overlayPane',
            className: 'cga-svheat-layer'
        }).addTo(gameMap);
    }

    function openCurrentLocationInMaps() {
        const c = lastCoords || getCoordinates();
        if (!c) return;
        const url = `https://www.google.com/maps?q=${c.lat},${c.lng}&ll=${c.lat},${c.lng}&z=18&t=k`;
        window.open(url, '_blank', 'noopener,noreferrer');
    }

    // Country outlines
    function clearCountryLayers() {
        if (!gameMap) return;
        countryLayers.forEach(l => { try { gameMap.removeLayer(l); } catch {} });
        countryLayers = [];
    }

    const getCountryStyle = () => ({
        color: cfg.countryColor,
        weight: 2.5,
        fillColor: cfg.countryColor,
        fillOpacity: 0.07,
        interactive: false
    });

    function addGeoLayer(data) {
        if (!gameMap || !data) return;
        try {
            const layer = L.geoJSON(data, { style: () => getCountryStyle() }).addTo(gameMap);
            countryLayers.push(layer);
        } catch (e) { console.warn('[GeoAssist] GeoJSON draw error', e); }
    }

    function setCountryStatus(msg, color) {
        const el = document.getElementById('cga-country-status');
        if (el) { el.textContent = msg; el.style.color = color || cfg.countryColor; }
    }

    function clearCountryRetry() {
        if (countryRetryTimer) {
            clearTimeout(countryRetryTimer);
            countryRetryTimer = null;
        }
    }

    function isCountryRunActive(runId) {
        return runId === countryRunId && cfg.countryEnabled && !!gameMap;
    }

    function scheduleCountryRetry(lat, lng) {
        if (!cfg.countryEnabled || !gameMap) return;
        if (countryRetryAttempt >= COUNTRY_AUTO_RETRY_MAX) {
            setCountryStatus('Failed (retry limit)', '#f87171');
            return;
        }
        clearCountryRetry();
        countryRetryAttempt++;
        setCountryStatus(`Retrying... ${countryRetryAttempt}/${COUNTRY_AUTO_RETRY_MAX}`, '#fbbf24');
        countryRetryTimer = setTimeout(() => {
            countryRetryTimer = null;
            drawCountryOutlines(lat, lng);
        }, COUNTRY_AUTO_RETRY_DELAY_MS);
    }

    function getFeatureIso2(props) {
        if (!props) return null;
        const raw =
            props['ISO3166-1-Alpha-2'] ||
            props.ISO_A2 ||
            props.iso_a2 ||
            props.ISO2 ||
            props.iso2;
        if (!raw || raw === '-99') return null;
        return String(raw).toUpperCase();
    }

    async function ensureCountryIndex() {
        if (countryFeatureIndex) return countryFeatureIndex;
        const data = await fetchJsonWithRetry(COUNTRY_DATA_URL);
        const index = new Map();
        for (const feature of (data?.features || [])) {
            const iso2 = getFeatureIso2(feature?.properties);
            if (!iso2) continue;
            if (!index.has(iso2)) index.set(iso2, []);
            index.get(iso2).push(feature);
        }
        countryFeatureIndex = index;
        return index;
    }

    function addCountryByIso2(iso2) {
        if (!countryFeatureIndex || !iso2) return false;
        const features = countryFeatureIndex.get(iso2);
        if (!features?.length) return false;
        addGeoLayer({ type: 'FeatureCollection', features });
        return true;
    }

    async function resolveCountryIso2(lat, lng) {
        try {
            const revData = await fetchJsonWithRetry(
                `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=jsonv2&zoom=3&addressdetails=1`,
                { headers: { 'Accept-Language': 'en' } }
            );
            const iso = revData?.address?.country_code?.toUpperCase();
            if (iso) return iso;
        } catch {}

        try {
            const bdcData = await fetchJsonWithRetry(
                `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lng}&localityLanguage=en`
            );
            const iso = bdcData?.countryCode?.toUpperCase();
            if (iso) return iso;
        } catch {}

        return null;
    }

    async function drawCountryOutlines(snapLat, snapLng) {
        const runId = ++countryRunId;
        countryBusy = true;
        clearCountryRetry();
        clearCountryLayers();
        setCountryStatus('Loading boundaries...', '#94a3b8');

        try {
            await ensureCountryIndex();

            currentIso = await resolveCountryIso2(snapLat, snapLng);
            if (!isCountryRunActive(runId)) return;
            if (currentIso) addCountryByIso2(currentIso);

            const pool = ALL_COUNTRIES.filter(c => c !== currentIso).sort(() => Math.random() - 0.5);
            let addedDecoys = 0;
            for (const code of pool) {
                if (addedDecoys >= cfg.countryDecoys) break;
                await sleep(COUNTRY_FETCH_DELAY_MS);
                if (!isCountryRunActive(runId)) break;
                if (addCountryByIso2(code)) addedDecoys++;
            }
            if (!isCountryRunActive(runId)) return;
            countryRetryAttempt = 0;
            setCountryStatus(currentIso ? 'Loaded' : 'Loaded (no exact country)', currentIso ? '#4ade80' : '#fbbf24');
        } catch (e) {
            console.error('[GeoAssist] Country fetch failed', e);
            if (isCountryRunActive(runId)) {
                setCountryStatus('Failed, retrying...', '#f87171');
                scheduleCountryRetry(snapLat, snapLng);
            }
        }
        if (runId === countryRunId) countryBusy = false;
    }

    function isCheatEnabled(key) {
        if (key === 'bands') return !!cfg.bandsEnabled;
        if (key === 'circle') return !!cfg.circleEnabled;
        if (key === 'country') return !!cfg.countryEnabled;
        if (key === 'pin') return !!cfg.pinEnabled;
        if (key === 'svheat') return !!cfg.svHeatEnabled;
        return false;
    }

    function getEnabledCheatKeys() {
        return ['bands', 'circle', 'country', 'pin', 'svheat'].filter(isCheatEnabled);
    }

    function updateLastUsedCheatSet() {
        lastActivatedCheatKeys = getEnabledCheatKeys();
    }

    function syncCheatUi(key) {
        if (!panel) return;

        if (key === 'bands') {
            const tog = panel.querySelector('#cga-bands-tog');
            const ctrl = panel.querySelector('#cga-bands-ctrl');
            if (tog) tog.checked = !!cfg.bandsEnabled;
            if (ctrl) {
                ctrl.style.opacity = cfg.bandsEnabled ? '1' : '.35';
                ctrl.style.pointerEvents = cfg.bandsEnabled ? '' : 'none';
            }
            return;
        }

        if (key === 'circle') {
            const tog = panel.querySelector('#cga-circle-tog');
            const ctrl = panel.querySelector('#cga-circle-ctrl');
            if (tog) tog.checked = !!cfg.circleEnabled;
            if (ctrl) {
                ctrl.style.opacity = cfg.circleEnabled ? '1' : '.35';
                ctrl.style.pointerEvents = cfg.circleEnabled ? '' : 'none';
            }
            return;
        }

        if (key === 'country') {
            const tog = panel.querySelector('#cga-country-tog');
            const ctrl = panel.querySelector('#cga-country-ctrl');
            if (tog) tog.checked = !!cfg.countryEnabled;
            if (ctrl) {
                ctrl.style.opacity = cfg.countryEnabled ? '1' : '.35';
                ctrl.style.pointerEvents = cfg.countryEnabled ? '' : 'none';
            }
            return;
        }

        if (key === 'pin') {
            const tog = panel.querySelector('#cga-pin-tog');
            if (tog) tog.checked = !!cfg.pinEnabled;
            return;
        }

        if (key === 'svheat') {
            const tog = panel.querySelector('#cga-svheat-tog');
            if (tog) tog.checked = !!cfg.svHeatEnabled;
            const val = panel.querySelector('#cga-svheat-op-val');
            const sl = panel.querySelector('#cga-svheat-op');
            const ctrl = panel.querySelector('#cga-svheat-ctrl');
            if (val) val.textContent = `${cfg.svHeatOpacity}%`;
            if (sl) {
                sl.value = cfg.svHeatOpacity;
                sl.style.setProperty('--pct', `${cfg.svHeatOpacity}%`);
            }
            if (ctrl) {
                ctrl.style.opacity = cfg.svHeatEnabled ? '1' : '.35';
                ctrl.style.pointerEvents = cfg.svHeatEnabled ? '' : 'none';
            }
        }
    }

    function setCheatEnabled(key, enabled, markAsActivated = true) {
        if (key === 'bands') {
            cfg.bandsEnabled = !!enabled;
            if (cfg.bandsEnabled) drawBands(); else clearBands();
        }
        if (key === 'circle') {
            cfg.circleEnabled = !!enabled;
            if (cfg.circleEnabled) drawCircle(); else clearCircle();
        }
        if (key === 'country') {
            cfg.countryEnabled = !!enabled;
            if (cfg.countryEnabled && lastCoords) {
                countryRetryAttempt = 0;
                drawCountryOutlines(lastCoords.lat, lastCoords.lng);
            } else {
                clearCountryRetry();
                countryRetryAttempt = 0;
                countryRunId++;
                countryBusy = false;
                clearCountryLayers();
                setCountryStatus('', '');
            }
        }
        if (key === 'pin') {
            cfg.pinEnabled = !!enabled;
            if (cfg.pinEnabled) drawExactPin(); else clearExactPin();
        }
        if (key === 'svheat') {
            cfg.svHeatEnabled = !!enabled;
            if (cfg.svHeatEnabled) drawSvHeat(); else clearSvHeat();
        }

        if (markAsActivated) updateLastUsedCheatSet();
        saveConfig();
        syncCheatUi(key);
    }

    function formatHotkeyFromEvent(e) {
        if (e.code.startsWith('Key')) return e.code.slice(3).toUpperCase();
        if (e.code.startsWith('Digit')) return e.code.slice(5);
        if (e.code === 'Space') return 'Space';
        if (e.code.startsWith('Arrow')) return e.code.replace('Arrow', '');
        return e.key.length === 1 ? e.key.toUpperCase() : e.key;
    }

    function syncHotkeyUi() {
        if (!panel) return;
        const valEl = panel.querySelector('#cga-hotkey-val');
        const setBtn = panel.querySelector('#cga-hotkey-set');
        const mapValEl = panel.querySelector('#cga-maps-hotkey-val');
        const mapSetBtn = panel.querySelector('#cga-maps-hotkey-set');
        if (valEl) valEl.textContent = awaitingHotkeyCapture ? 'Press key...' : (cfg.toggleHotkeyLabel || 'None');
        if (setBtn) setBtn.textContent = awaitingHotkeyCapture ? 'Cancel capture' : 'Set key';
        if (mapValEl) mapValEl.textContent = awaitingMapsHotkeyCapture ? 'Press key...' : (cfg.mapsHotkeyLabel || 'None');
        if (mapSetBtn) mapSetBtn.textContent = awaitingMapsHotkeyCapture ? 'Cancel capture' : 'Set key';
    }

    function syncOverlayColorUi() {
        if (!panel) return;
        panel.style.setProperty('--col-lat', cfg.latColor);
        panel.style.setProperty('--col-lng', cfg.lngColor);
        panel.style.setProperty('--col-circle', cfg.circleColor);
        panel.style.setProperty('--col-country', cfg.countryColor);

        const latLabel = panel.querySelector('#cga-lat-label');
        const lngLabel = panel.querySelector('#cga-lng-label');
        if (latLabel) latLabel.style.color = cfg.latColor;
        if (lngLabel) lngLabel.style.color = cfg.lngColor;

        const bandsTog = panel.querySelector('#cga-tog-bands-wrap');
        const circleTog = panel.querySelector('#cga-tog-circle-wrap');
        const countryTog = panel.querySelector('#cga-tog-country-wrap');
        if (bandsTog) bandsTog.style.setProperty('--acc', cfg.latColor);
        if (circleTog) circleTog.style.setProperty('--acc', cfg.circleColor);
        if (countryTog) countryTog.style.setProperty('--acc', cfg.countryColor);

        const latSl = panel.querySelector('#cga-lat-sl');
        const lngSl = panel.querySelector('#cga-lng-sl');
        const circleSl = panel.querySelector('#cga-radius-sl');
        if (latSl) latSl.style.setProperty('--acc', cfg.latColor);
        if (lngSl) lngSl.style.setProperty('--acc', cfg.lngColor);
        if (circleSl) circleSl.style.setProperty('--acc', cfg.circleColor);
    }

    function toggleMostRecentCheat() {
        const targets = lastActivatedCheatKeys.length ? [...lastActivatedCheatKeys] : getEnabledCheatKeys();
        if (!targets.length) return;

        const allEnabled = targets.every(isCheatEnabled);
        for (const key of targets) setCheatEnabled(key, !allEnabled, false);
    }

    // Master update on new location
    function onNewCoords(coords) {
        lastCoords = coords;
        latRandFrac = Math.random();
        lngRandFrac = Math.random();
        circleRandBearing = Math.random() * Math.PI * 2;
        circleRandFrac = 0.15 + Math.sqrt(Math.random()) * 0.75;

        if (cfg.bandsEnabled) drawBands(); else clearBands();
        if (cfg.circleEnabled) drawCircle(); else clearCircle();
        if (cfg.pinEnabled) drawExactPin(); else clearExactPin();
        if (cfg.svHeatEnabled) drawSvHeat(); else clearSvHeat();

        if (cfg.countryEnabled) {
            countryRetryAttempt = 0;
            drawCountryOutlines(coords.lat, coords.lng);
        } else {
            clearCountryRetry();
            countryRetryAttempt = 0;
            countryRunId++;
            countryBusy = false;
            clearCountryLayers();
        }
    }

    // Leaflet map interception
    const mapObserver = new MutationObserver(() => {
        try {
            if (typeof L !== 'undefined' && L.Map?.prototype.setView) {
                const origSetView = L.Map.prototype.setView;
                L.Map.prototype.setView = new _Proxy(origSetView, {
                    apply(target, thisArg, args) {
                        if (!gameMap) {
                            gameMap = thisArg;
                            if (lastCoords) {
                                if (cfg.bandsEnabled) drawBands();
                                if (cfg.circleEnabled) drawCircle();
                                if (cfg.pinEnabled) drawExactPin();
                                if (cfg.svHeatEnabled) drawSvHeat();
                            }
                        }
                        return _Reflect.apply(target, thisArg, args);
                    }
                });
                mapObserver.disconnect();
            }
        } catch {}
    });
    mapObserver.observe(document.body, { childList: true, subtree: true });

    // Coordinate polling
    const iframeObserver = new MutationObserver(() => {
        const iframe =
            document.querySelector('#PanoramaIframe') ||
            document.querySelector('iframe[src*="location"]') ||
            document.querySelector('.iframeWithStreetView');
        if (iframe) {
            iframeObserver.disconnect();
            if (coordPollId) clearInterval(coordPollId);
            coordPollId = setInterval(() => {
                const c = getCoordinates();
                if (!c) return;
                if (!lastCoords || lastCoords.lat !== c.lat || lastCoords.lng !== c.lng) onNewCoords(c);
            }, 500);
        }
    });
    iframeObserver.observe(document.body, { childList: true, subtree: true });

    // Panel styles
    const PANEL_CSS = `
#cga-panel {
    position: fixed;
    top: 50%; left: 50%;
    transform: translate(-50%,-50%);
    z-index: 2147483647;
    width: 392px;
    max-height: 82vh;
    display: flex;
    flex-direction: column;
    background: #090c10;
    border: 1px solid rgba(255,255,255,0.07);
    border-radius: 18px;
    color: #94a3b8;
    font-family: 'DM Sans', 'Segoe UI', system-ui, sans-serif;
    font-size: 14px;
    box-shadow:
        0 40px 100px rgba(0,0,0,0.85),
        0 0 0 1px rgba(255,255,255,0.035) inset,
        0 1px 0 rgba(255,255,255,0.06) inset;
    overflow: hidden;
    transition: left 0.22s ease, top 0.22s ease, opacity 0.18s ease, height 0.2s ease;
}
#cga-panel.cga-dragging { transition: none; }
#cga-panel.cga-enter { animation: cga-fade-in 0.18s ease-out; }
#cga-panel.cga-exit { animation: cga-fade-out 0.16s ease-in forwards; }
@keyframes cga-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}
@keyframes cga-fade-out {
    from { opacity: 1; }
    to { opacity: 0; }
}
#cga-panel * { box-sizing: border-box; margin: 0; padding: 0; }

#cga-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 20px 14px;
    border-bottom: 1px solid rgba(255,255,255,0.05);
    cursor: grab;
    flex-shrink: 0;
    background: linear-gradient(to bottom, rgba(255,255,255,0.025), transparent);
}
#cga-header:active { cursor: grabbing; }
#cga-logo { display: flex; align-items: center; gap: 9px; }
#cga-logo-mark {
    width: 30px; height: 30px;
    background: linear-gradient(135deg,#22d3ee 0%,#818cf8 100%);
    border-radius: 9px;
    display: flex; align-items: center; justify-content: center;
    font-size: 15px;
    box-shadow: 0 0 16px rgba(34,211,238,0.25);
}
#cga-logo-mark svg { width: 18px; height: 18px; display: block; }
.cga-map-pin-icon { background: transparent; border: 0; }
.cga-map-pin-wrap {
    width: 30px; height: 30px;
    background: linear-gradient(135deg,#22d3ee 0%,#818cf8 100%);
    border-radius: 9px;
    display: flex; align-items: center; justify-content: center;
    box-shadow: 0 0 12px rgba(34,211,238,0.45);
}
.cga-map-pin-wrap svg { width: 18px; height: 18px; display: block; }
#cga-logo-name {
    font-size: 15px; font-weight: 700;
    letter-spacing: 0.05em; text-transform: uppercase;
    color: #e2e8f0;
}
#cga-logo-ver {
    font-size: 10px; color: #334155;
    letter-spacing: 0.06em; margin-left: 5px; font-weight: 600;
}
#cga-close {
    width: 28px; height: 28px;
    border-radius: 7px;
    display: flex; align-items: center; justify-content: center;
    font-size: 15px; color: #334155;
    cursor: pointer; transition: all 0.15s;
    border: 1px solid transparent;
}
#cga-close:hover { color: #f87171; border-color: rgba(248,113,113,0.25); background: rgba(248,113,113,0.07); }

#cga-tabs {
    display: grid; grid-template-columns: repeat(4,1fr);
    border-bottom: 1px solid rgba(255,255,255,0.05);
    flex-shrink: 0;
}
.cga-tab {
    padding: 11px 0 10px;
    text-align: center;
    font-size: 11.5px; font-weight: 700;
    letter-spacing: 0.07em; text-transform: uppercase;
    color: #2d3748; cursor: pointer;
    transition: all 0.15s;
    border-bottom: 2px solid transparent;
    position: relative; top: 1px;
}
.cga-tab::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: -2px;
    height: 2px;
    opacity: 0;
}
.cga-tab:hover { color: #4a5568; }
.cga-tab[data-tab="bands"].active {
    color: transparent;
    border-color: transparent;
    background: linear-gradient(90deg, var(--col-lat,#ff7070) 0%, var(--col-lng,#60a5fa) 100%);
    -webkit-background-clip: text;
    background-clip: text;
}
.cga-tab[data-tab="bands"].active::after {
    opacity: 1;
    background: linear-gradient(90deg, var(--col-lat,#ff7070) 0%, var(--col-lng,#60a5fa) 100%);
}
.cga-tab[data-tab="circle"].active { color: var(--col-circle,#22c55e); border-color: var(--col-circle,#22c55e); }
.cga-tab[data-tab="country"].active { color: var(--col-country,#fbbf24); border-color: var(--col-country,#fbbf24); }
.cga-tab[data-tab="pin"].active { color: #60a5fa; border-color: #60a5fa; }

#cga-body {
    flex: 1;
    overflow: hidden;
}

.cga-tab-body { display: none; padding: 18px 20px 14px; }
.cga-tab-body.active {
    display: block;
    animation: cga-tab-in 0.18s cubic-bezier(0.16,1,0.3,1);
}
@keyframes cga-tab-in {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: translateY(0); }
}

.cga-feat-head {
    display: flex; align-items: center;
    justify-content: flex-end;
    min-height: 28px;
    margin: 12px 0 14px;
}
.cga-feat-label {
    display: flex; align-items: center; gap: 8px;
    font-size: 12px; font-weight: 700;
    letter-spacing: 0.08em; text-transform: uppercase;
    color: #e2e8f0;
}
.cga-feat-icon { font-size: 16px; line-height: 1; }

.cga-tog { position: relative; width: 46px; height: 25px; cursor: pointer; flex-shrink: 0; }
.cga-tog input { opacity: 0; width: 0; height: 0; position: absolute; }
.cga-tog-track {
    position: absolute; inset: 0;
    border-radius: 12px;
    background: #1a2035;
    border: 1px solid rgba(255,255,255,0.07);
    transition: all 0.22s;
}
.cga-tog input:checked ~ .cga-tog-track {
    background: var(--acc,#22d3ee);
    border-color: var(--acc,#22d3ee);
    box-shadow: 0 0 12px color-mix(in srgb,var(--acc,#22d3ee) 40%,transparent);
}
.cga-tog-knob {
    position: absolute; width: 18px; height: 18px;
    border-radius: 50%; background: #3a4560;
    top: 3px; left: 3px;
    transition: all 0.22s;
    box-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.cga-tog input:checked ~ .cga-tog-track .cga-tog-knob { left: 25px; background: #fff; }

.cga-hr { border: none; border-top: 1px solid rgba(255,255,255,0.04); margin: 14px 0; }
.cga-stack { display: flex; flex-direction: column; gap: 15px; }

.cga-srow-top {
    display: flex; justify-content: space-between; align-items: baseline;
    margin-bottom: 8px;
}
.cga-slabel { font-size: 11.5px; font-weight: 600; color: #4a5568; letter-spacing: 0.04em; }
.cga-sval {
    font-size: 12px; font-weight: 700; color: #e2e8f0;
    font-family: 'JetBrains Mono','Courier New',monospace;
}
.cga-range {
    -webkit-appearance: none; appearance: none;
    width: 100%; height: 18px; border-radius: 3px;
    background: linear-gradient(to right, var(--acc,#22d3ee) var(--pct,50%), #1a2035 var(--pct,50%));
    background-size: 100% 3px;
    background-repeat: no-repeat;
    background-position: center;
    outline: none; cursor: pointer;
}
.cga-range::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 15px; height: 15px; border-radius: 50%;
    background: var(--acc,#22d3ee);
    cursor: pointer;
    box-shadow: 0 0 0 3px color-mix(in srgb,var(--acc,#22d3ee) 20%,transparent), 0 2px 6px rgba(0,0,0,0.5);
    transition: transform 0.1s;
}
.cga-range:active::-webkit-slider-thumb { transform: scale(1.2); }
.cga-range::-moz-range-thumb {
    width: 15px; height: 15px; border: 0; border-radius: 50%;
    background: var(--acc,#22d3ee);
    cursor: pointer;
    box-shadow: 0 0 0 3px color-mix(in srgb,var(--acc,#22d3ee) 20%,transparent), 0 2px 6px rgba(0,0,0,0.5);
}
.cga-range::-moz-range-track {
    height: 3px;
    background: #1a2035;
    border: 0;
    border-radius: 3px;
}

.cga-mode-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 5px; }
.cga-mbtn {
    padding: 7px 0; border-radius: 7px;
    border: 1px solid #1a2035; background: transparent;
    color: #2d3748; font-size: 11px; font-weight: 700;
    cursor: pointer; text-align: center;
    letter-spacing: 0.05em; text-transform: uppercase;
    transition: all 0.15s; font-family: inherit;
}
.cga-mbtn[data-mode="lat"] { color: var(--col-lat,#ff7070); border-color: color-mix(in srgb, var(--col-lat,#ff7070) 35%, transparent); background: color-mix(in srgb, var(--col-lat,#ff7070) 10%, transparent); }
.cga-mbtn[data-mode="lng"] { color: var(--col-lng,#60a5fa); border-color: color-mix(in srgb, var(--col-lng,#60a5fa) 35%, transparent); background: color-mix(in srgb, var(--col-lng,#60a5fa) 10%, transparent); }
.cga-mbtn[data-mode="both"] { color: #c4b5fd; border-color: rgba(196,181,253,0.35); background: rgba(196,181,253,0.06); }
.cga-mbtn[data-mode="lat"]:hover { border-color: color-mix(in srgb, var(--col-lat,#ff7070) 60%, transparent); background: color-mix(in srgb, var(--col-lat,#ff7070) 18%, transparent); }
.cga-mbtn[data-mode="lng"]:hover { border-color: color-mix(in srgb, var(--col-lng,#60a5fa) 60%, transparent); background: color-mix(in srgb, var(--col-lng,#60a5fa) 18%, transparent); }
.cga-mbtn[data-mode="both"]:hover { border-color: rgba(196,181,253,0.6); background: rgba(196,181,253,0.12); }
.cga-mbtn[data-mode="lat"].on { background: color-mix(in srgb, var(--col-lat,#ff7070) 25%, transparent); border-color: color-mix(in srgb, var(--col-lat,#ff7070) 75%, transparent); color: var(--col-lat,#ff7070); }
.cga-mbtn[data-mode="lng"].on { background: color-mix(in srgb, var(--col-lng,#60a5fa) 25%, transparent); border-color: color-mix(in srgb, var(--col-lng,#60a5fa) 75%, transparent); color: var(--col-lng,#60a5fa); }
.cga-mbtn[data-mode="both"].on {
    color: #e2e8f0;
    border-color: rgba(167,139,250,0.75);
    background: linear-gradient(90deg, color-mix(in srgb, var(--col-lat,#ff7070) 25%, transparent), color-mix(in srgb, var(--col-lng,#60a5fa) 25%, transparent));
}

.cga-stepper {
    display: flex; align-items: center; gap: 8px;
    background: #0d1117; border: 1px solid rgba(255,255,255,0.06);
    border-radius: 10px; padding: 8px 12px;
}
.cga-sbtn {
    width: 24px; height: 24px; border-radius: 6px;
    border: 1px solid rgba(255,255,255,0.07);
    background: rgba(255,255,255,0.04); color: #6b7280;
    font-size: 17px; font-weight: 700; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: all 0.12s; line-height: 1; user-select: none;
}
.cga-sbtn:hover { background: rgba(255,255,255,0.09); color: #e2e8f0; }
.cga-snum {
    flex: 1; text-align: center; font-weight: 700; color: #e2e8f0;
    font-size: 16px; font-family: 'JetBrains Mono',monospace;
}
.cga-sunit { font-size: 11px; color: #2d3748; letter-spacing: 0.04em; }

.cga-status-row {
    display: flex; justify-content: space-between; align-items: center;
    margin-bottom: 8px;
}
#cga-country-status { font-size: 11px; font-weight: 700; letter-spacing: 0.04em; color: var(--col-country,#fbbf24); min-height: 14px; }

.cga-btn {
    width: 100%; padding: 9px 12px; border-radius: 9px;
    border: 1px solid rgba(255,255,255,0.06);
    background: rgba(255,255,255,0.025); color: #4a5568;
    font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em;
    text-transform: uppercase; cursor: pointer;
    transition: all 0.15s; font-family: inherit;
}
.cga-btn:hover { background: rgba(255,255,255,0.06); color: #94a3b8; border-color: rgba(255,255,255,0.1); }

.cga-color-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}
.cga-color-input {
    width: 34px;
    height: 24px;
    border: 1px solid rgba(255,255,255,0.16);
    border-radius: 6px;
    background: transparent;
    cursor: pointer;
    padding: 0;
}
.cga-color-input::-webkit-color-swatch-wrapper { padding: 0; }
.cga-color-input::-webkit-color-swatch { border: 0; border-radius: 5px; }

.cga-svheat-layer { filter: saturate(1.3) contrast(1.15); }

#cga-hint {
    padding: 9px 18px 13px; text-align: center;
    font-size: 9.5px; letter-spacing: 0.12em;
    color: #161e2a; text-transform: uppercase;
    border-top: 1px solid rgba(255,255,255,0.03);
    flex-shrink: 0;
}
`;

    // Panel builder
    function buildPanel() {
        if (!document.getElementById('cga-fonts')) {
            const lnk = document.createElement('link');
            lnk.id = 'cga-fonts';
            lnk.rel = 'stylesheet';
            lnk.href = 'https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap';
            document.head.appendChild(lnk);
        }

        const styleEl = document.createElement('style');
        styleEl.id = 'cga-styles';
        styleEl.textContent = PANEL_CSS;
        document.head.appendChild(styleEl);

        const el = document.createElement('div');
        el.id = 'cga-panel';

        el.innerHTML = `
<div id="cga-header">
    <div id="cga-logo">
        <div id="cga-logo-mark" aria-label="GeoAssist logo">
            ${PIN_GLYPH_SVG}
        </div>
        <div>
            <span id="cga-logo-name">GeoAssist</span>
            <span id="cga-logo-ver">v${SCRIPT_VERSION}</span>
        </div>
    </div>
    <div id="cga-close">X</div>
</div>

<div id="cga-tabs">
    <div class="cga-tab${activeTab === 'bands' ? ' active' : ''}" data-tab="bands">Lat/Long</div>
    <div class="cga-tab${activeTab === 'circle' ? ' active' : ''}" data-tab="circle">Circle</div>
    <div class="cga-tab${activeTab === 'country' ? ' active' : ''}" data-tab="country">Country</div>
    <div class="cga-tab${activeTab === 'pin' ? ' active' : ''}" data-tab="pin">Misc</div>
</div>

<div id="cga-body">
    <div class="cga-tab-body${activeTab === 'bands' ? ' active' : ''}" data-body="bands">
        <div class="cga-feat-head">
            <label class="cga-tog" id="cga-tog-bands-wrap" style="--acc:${cfg.latColor}">
                <input type="checkbox" id="cga-bands-tog"${cfg.bandsEnabled ? ' checked' : ''}>
                <div class="cga-tog-track"><div class="cga-tog-knob"></div></div>
            </label>
        </div>
        <div id="cga-bands-ctrl" class="cga-stack" style="${cfg.bandsEnabled ? '' : 'opacity:.35;pointer-events:none'}">
            <div>
                <div class="cga-slabel" style="margin-bottom:8px">Display mode</div>
                <div class="cga-mode-grid">
                    <div class="cga-mbtn${cfg.mode === 'lat' ? ' on' : ''}" data-mode="lat">Lat</div>
                    <div class="cga-mbtn${cfg.mode === 'both' ? ' on' : ''}" data-mode="both">Both</div>
                    <div class="cga-mbtn${cfg.mode === 'lng' ? ' on' : ''}" data-mode="lng">Lng</div>
                </div>
            </div>
            <hr class="cga-hr" style="margin:2px 0">
            <div>
                <div class="cga-srow-top">
                    <span class="cga-slabel" id="cga-lat-label" style="color:${cfg.latColor}">Latitude height</span>
                    <span class="cga-sval" id="cga-lat-val">${cfg.latHeightKm} km</span>
                </div>
                <input class="cga-range" type="range" id="cga-lat-sl"
                    style="--acc:${cfg.latColor};--pct:${((cfg.latHeightKm - 50) / 4950 * 100).toFixed(1)}%"
                    min="50" max="5000" step="50" value="${cfg.latHeightKm}">
            </div>
            <div>
                <div class="cga-srow-top">
                    <span class="cga-slabel" id="cga-lng-label" style="color:${cfg.lngColor}">Longitude width</span>
                    <span class="cga-sval" id="cga-lng-val">${cfg.lngWidthKm} km</span>
                </div>
                <input class="cga-range" type="range" id="cga-lng-sl"
                    style="--acc:${cfg.lngColor};--pct:${((cfg.lngWidthKm - 50) / 4950 * 100).toFixed(1)}%"
                    min="50" max="5000" step="50" value="${cfg.lngWidthKm}">
            </div>
        </div>
    </div>

    <div class="cga-tab-body${activeTab === 'circle' ? ' active' : ''}" data-body="circle">
        <div class="cga-feat-head">
            <label class="cga-tog" id="cga-tog-circle-wrap" style="--acc:${cfg.circleColor}">
                <input type="checkbox" id="cga-circle-tog"${cfg.circleEnabled ? ' checked' : ''}>
                <div class="cga-tog-track"><div class="cga-tog-knob"></div></div>
            </label>
        </div>
        <div id="cga-circle-ctrl" class="cga-stack" style="${cfg.circleEnabled ? '' : 'opacity:.35;pointer-events:none'}">
            <div>
                <div class="cga-srow-top">
                    <span class="cga-slabel">Radius</span>
                    <span class="cga-sval" id="cga-radius-val">${cfg.circleRadiusKm} km</span>
                </div>
                <input class="cga-range" type="range" id="cga-radius-sl"
                    style="--acc:${cfg.circleColor};--pct:${((cfg.circleRadiusKm - 50) / 4950 * 100).toFixed(1)}%"
                    min="50" max="5000" step="50" value="${cfg.circleRadiusKm}">
            </div>
        </div>
    </div>

    <div class="cga-tab-body${activeTab === 'country' ? ' active' : ''}" data-body="country">
        <div class="cga-feat-head">
            <label class="cga-tog" id="cga-tog-country-wrap" style="--acc:${cfg.countryColor}">
                <input type="checkbox" id="cga-country-tog"${cfg.countryEnabled ? ' checked' : ''}>
                <div class="cga-tog-track"><div class="cga-tog-knob"></div></div>
            </label>
        </div>
        <div id="cga-country-ctrl" class="cga-stack" style="${cfg.countryEnabled ? '' : 'opacity:.35;pointer-events:none'}">
            <div>
                <div class="cga-status-row">
                    <span class="cga-slabel">Decoy countries</span>
                    <span id="cga-country-status"></span>
                </div>
                <div class="cga-stepper">
                    <button class="cga-sbtn" id="cga-dec-minus">-</button>
                    <span class="cga-snum" id="cga-dec-val">${cfg.countryDecoys}</span>
                    <span class="cga-sunit">decoys</span>
                    <button class="cga-sbtn" id="cga-dec-plus">+</button>
                </div>
            </div>
            <button class="cga-btn" id="cga-country-refetch">Refetch outlines</button>
        </div>
    </div>

    <div class="cga-tab-body${activeTab === 'pin' ? ' active' : ''}" data-body="pin">
        <div class="cga-feat-head">
            <label class="cga-tog" style="--acc:#60a5fa">
                <input type="checkbox" id="cga-pin-tog"${cfg.pinEnabled ? ' checked' : ''}>
                <div class="cga-tog-track"><div class="cga-tog-knob"></div></div>
            </label>
        </div>
        <div class="cga-stack">
            <div class="cga-slabel">Pin exact location on the map</div>
            <hr class="cga-hr" style="margin:4px 0">
            <div class="cga-feat-head" style="margin:0 0 8px; min-height:24px; justify-content:space-between; width:100%;">
                <span class="cga-slabel">Street View heat overlay</span>
                <label class="cga-tog" style="--acc:#ef4444">
                    <input type="checkbox" id="cga-svheat-tog"${cfg.svHeatEnabled ? ' checked' : ''}>
                    <div class="cga-tog-track"><div class="cga-tog-knob"></div></div>
                </label>
            </div>
            <div id="cga-svheat-ctrl" style="${cfg.svHeatEnabled ? '' : 'opacity:.35;pointer-events:none'}">
                <div class="cga-srow-top">
                    <span class="cga-slabel">Opacity</span>
                    <span class="cga-sval" id="cga-svheat-op-val">${cfg.svHeatOpacity}%</span>
                </div>
                <input class="cga-range" type="range" id="cga-svheat-op" style="--acc:#ef4444;--pct:${cfg.svHeatOpacity}%" min="0" max="100" step="1" value="${cfg.svHeatOpacity}">
            </div>
            <hr class="cga-hr" style="margin:4px 0">
            <div class="cga-srow-top" style="margin-bottom:2px">
                <span class="cga-slabel">Last-cheat toggle key</span>
                <span class="cga-sval" id="cga-hotkey-val">${cfg.toggleHotkeyLabel || 'None'}</span>
            </div>
            <button class="cga-btn" id="cga-hotkey-set">Set key</button>
            <button class="cga-btn" id="cga-hotkey-clear">Clear key</button>
            <div class="cga-srow-top" style="margin-bottom:2px">
                <span class="cga-slabel">Open location in maps</span>
                <span class="cga-sval" id="cga-maps-hotkey-val">${cfg.mapsHotkeyLabel || 'None'}</span>
            </div>
            <button class="cga-btn" id="cga-maps-hotkey-set">Set key</button>
            <button class="cga-btn" id="cga-maps-hotkey-clear">Clear key</button>
            <hr class="cga-hr" style="margin:4px 0">
            <div class="cga-color-row"><span class="cga-slabel">Latitude overlay</span><input class="cga-color-input" type="color" id="cga-col-lat" value="${cfg.latColor}"></div>
            <div class="cga-color-row"><span class="cga-slabel">Longitude overlay</span><input class="cga-color-input" type="color" id="cga-col-lng" value="${cfg.lngColor}"></div>
            <div class="cga-color-row"><span class="cga-slabel">Circle overlay</span><input class="cga-color-input" type="color" id="cga-col-circle" value="${cfg.circleColor}"></div>
            <div class="cga-color-row"><span class="cga-slabel">Country overlay</span><input class="cga-color-input" type="color" id="cga-col-country" value="${cfg.countryColor}"></div>
            <button class="cga-btn" id="cga-colors-reset">Reset overlay colors</button>
        </div>
    </div>
</div>

<div id="cga-hint">Left Shift · toggle panel</div>
        `;

        document.body.appendChild(el);
        el.classList.add('cga-enter');

        el.querySelector('#cga-close').onclick = () => hidePanel(el);

        el.querySelectorAll('.cga-tab').forEach(tab => {
            tab.onclick = () => {
                switchTabWithResize(el, tab.dataset.tab);
            };
        });

        const bandsTogEl = el.querySelector('#cga-bands-tog');
        bandsTogEl.onchange = () => setCheatEnabled('bands', bandsTogEl.checked);

        el.querySelectorAll('.cga-mbtn').forEach(btn => {
            btn.onclick = () => {
                cfg.mode = btn.dataset.mode;
                saveConfig();
                el.querySelectorAll('.cga-mbtn').forEach(b => b.classList.remove('on'));
                btn.classList.add('on');
                drawBands();
            };
        });

        bindSlider(el, '#cga-lat-sl', '#cga-lat-val', 'latHeightKm', 50, 5000, drawBands);
        bindSlider(el, '#cga-lng-sl', '#cga-lng-val', 'lngWidthKm', 50, 5000, drawBands);

        const circleTogEl = el.querySelector('#cga-circle-tog');
        circleTogEl.onchange = () => setCheatEnabled('circle', circleTogEl.checked);
        bindSlider(el, '#cga-radius-sl', '#cga-radius-val', 'circleRadiusKm', 50, 5000, drawCircle);

        const countryTogEl = el.querySelector('#cga-country-tog');
        countryTogEl.onchange = () => setCheatEnabled('country', countryTogEl.checked);

        el.querySelector('#cga-dec-minus').onclick = () => {
            if (cfg.countryDecoys > 0) {
                cfg.countryDecoys--;
                el.querySelector('#cga-dec-val').textContent = cfg.countryDecoys;
                saveConfig();
            }
        };
        el.querySelector('#cga-dec-plus').onclick = () => {
            if (cfg.countryDecoys < 25) {
                cfg.countryDecoys++;
                el.querySelector('#cga-dec-val').textContent = cfg.countryDecoys;
                saveConfig();
            }
        };
        el.querySelector('#cga-country-refetch').onclick = () => {
            if (!lastCoords) return;
            countryRetryAttempt = 0;
            clearCountryRetry();
            clearCountryLayers();
            drawCountryOutlines(lastCoords.lat, lastCoords.lng);
        };

        const pinTogEl = el.querySelector('#cga-pin-tog');
        pinTogEl.onchange = () => setCheatEnabled('pin', pinTogEl.checked);

        const svHeatTogEl = el.querySelector('#cga-svheat-tog');
        const svHeatOpEl = el.querySelector('#cga-svheat-op');
        const svHeatOpValEl = el.querySelector('#cga-svheat-op-val');
        svHeatTogEl.onchange = () => setCheatEnabled('svheat', svHeatTogEl.checked);
        svHeatOpEl.oninput = () => {
            cfg.svHeatOpacity = parseInt(svHeatOpEl.value, 10);
            svHeatOpValEl.textContent = `${cfg.svHeatOpacity}%`;
            svHeatOpEl.style.setProperty('--pct', `${cfg.svHeatOpacity}%`);
            saveConfig();
            if (cfg.svHeatEnabled) drawSvHeat();
        };

        const hotkeySetBtn = el.querySelector('#cga-hotkey-set');
        const hotkeyClearBtn = el.querySelector('#cga-hotkey-clear');
        const mapsHotkeySetBtn = el.querySelector('#cga-maps-hotkey-set');
        const mapsHotkeyClearBtn = el.querySelector('#cga-maps-hotkey-clear');
        hotkeySetBtn.onclick = () => {
            awaitingMapsHotkeyCapture = false;
            awaitingHotkeyCapture = !awaitingHotkeyCapture;
            syncHotkeyUi();
        };
        hotkeyClearBtn.onclick = () => {
            awaitingHotkeyCapture = false;
            cfg.toggleHotkeyCode = '';
            cfg.toggleHotkeyLabel = 'None';
            saveConfig();
            syncHotkeyUi();
        };
        mapsHotkeySetBtn.onclick = () => {
            awaitingHotkeyCapture = false;
            awaitingMapsHotkeyCapture = !awaitingMapsHotkeyCapture;
            syncHotkeyUi();
        };
        mapsHotkeyClearBtn.onclick = () => {
            awaitingMapsHotkeyCapture = false;
            cfg.mapsHotkeyCode = '';
            cfg.mapsHotkeyLabel = 'None';
            saveConfig();
            syncHotkeyUi();
        };

        const applyOverlayColor = (key, val) => {
            cfg[key] = val;
            saveConfig();
            syncOverlayColorUi();
            if (cfg.bandsEnabled) drawBands();
            if (cfg.circleEnabled) drawCircle();
            if (cfg.countryEnabled && lastCoords) {
                clearCountryLayers();
                drawCountryOutlines(lastCoords.lat, lastCoords.lng);
            }
        };

        el.querySelector('#cga-col-lat').oninput = e => applyOverlayColor('latColor', e.target.value);
        el.querySelector('#cga-col-lng').oninput = e => applyOverlayColor('lngColor', e.target.value);
        el.querySelector('#cga-col-circle').oninput = e => applyOverlayColor('circleColor', e.target.value);
        el.querySelector('#cga-col-country').oninput = e => applyOverlayColor('countryColor', e.target.value);
        el.querySelector('#cga-colors-reset').onclick = () => {
            cfg.latColor = '#ff5a5a';
            cfg.lngColor = '#468cff';
            cfg.circleColor = '#22c55e';
            cfg.countryColor = '#fbbf24';
            saveConfig();
            syncOverlayColorUi();
            el.querySelector('#cga-col-lat').value = cfg.latColor;
            el.querySelector('#cga-col-lng').value = cfg.lngColor;
            el.querySelector('#cga-col-circle').value = cfg.circleColor;
            el.querySelector('#cga-col-country').value = cfg.countryColor;
            if (cfg.bandsEnabled) drawBands();
            if (cfg.circleEnabled) drawCircle();
            if (cfg.countryEnabled && lastCoords) {
                clearCountryLayers();
                drawCountryOutlines(lastCoords.lat, lastCoords.lng);
            }
        };
        syncHotkeyUi();
        syncOverlayColorUi();
        syncCheatUi('svheat');

        makeDraggable(el, el.querySelector('#cga-header'));

        el.addEventListener('keydown', e => e.stopImmediatePropagation(), true);
        el.addEventListener('keyup', e => e.stopImmediatePropagation(), true);

        panel = el;
        return el;
    }

    function bindSlider(panelEl, sliderId, valId, key, min, max, redrawFn) {
        const slider = panelEl.querySelector(sliderId);
        const valEl = panelEl.querySelector(valId);
        slider.addEventListener('input', () => {
            cfg[key] = parseInt(slider.value, 10);
            valEl.textContent = cfg[key] + ' km';
            slider.style.setProperty('--pct', ((cfg[key] - min) / (max - min) * 100).toFixed(1) + '%');
            saveConfig();
            redrawFn();
        });
    }

    function makeDraggable(el, handle) {
        let startX, startY, origLeft, origTop;
        handle.addEventListener('mousedown', e => {
            if (e.target.closest('#cga-close')) return;
            e.preventDefault();
            el.classList.add('cga-dragging');
            const rect = el.getBoundingClientRect();
            el.style.transform = 'none';
            el.style.left = rect.left + 'px';
            el.style.top = rect.top + 'px';
            startX = e.clientX; startY = e.clientY;
            origLeft = rect.left; origTop = rect.top;
            const onMove = e2 => {
                el.style.left = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, origLeft + e2.clientX - startX)) + 'px';
                el.style.top = Math.max(0, Math.min(window.innerHeight - el.offsetHeight, origTop + e2.clientY - startY)) + 'px';
            };
            const onUp = () => {
                document.removeEventListener('mousemove', onMove);
                document.removeEventListener('mouseup', onUp);
                el.classList.remove('cga-dragging');
            };
            document.addEventListener('mousemove', onMove);
            document.addEventListener('mouseup', onUp);
        });
    }

    function keepPanelInViewport(el) {
        if (!el || el.style.display === 'none') return;
        requestAnimationFrame(() => {
            const rect = el.getBoundingClientRect();
            let nextTop = rect.top;
            let nextLeft = rect.left;
            let changed = false;

            if (rect.bottom > window.innerHeight) {
                nextTop = Math.max(0, rect.top - (rect.bottom - window.innerHeight));
                changed = true;
            }
            if (rect.top < 0) {
                nextTop = 0;
                changed = true;
            }
            if (rect.right > window.innerWidth) {
                nextLeft = Math.max(0, rect.left - (rect.right - window.innerWidth));
                changed = true;
            }
            if (rect.left < 0) {
                nextLeft = 0;
                changed = true;
            }

            if (!changed) return;
            if (el.style.transform !== 'none') {
                el.style.transform = 'none';
                el.style.left = rect.left + 'px';
                el.style.top = rect.top + 'px';
            }
            el.style.left = nextLeft + 'px';
            el.style.top = nextTop + 'px';
        });
    }

    function showPanel(el) {
        if (!el) return;
        el.classList.remove('cga-exit');
        el.style.display = 'flex';
        keepPanelInViewport(el);
        // Restart entry animation cleanly.
        el.classList.remove('cga-enter');
        void el.offsetWidth;
        el.classList.add('cga-enter');
    }

    function switchTabWithResize(el, nextTab) {
        if (!el) return;
        const startHeight = el.offsetHeight;

        activeTab = nextTab;
        el.querySelectorAll('.cga-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === nextTab));
        el.querySelectorAll('.cga-tab-body').forEach(b => b.classList.toggle('active', b.dataset.body === nextTab));

        const endHeight = el.offsetHeight;
        if (startHeight !== endHeight) {
            const rect = el.getBoundingClientRect();
            let targetTop = rect.top;
            if (rect.top + endHeight > window.innerHeight) {
                targetTop = Math.max(0, window.innerHeight - endHeight);
            }

            el.style.height = startHeight + 'px';
            void el.offsetHeight;
            el.style.height = endHeight + 'px';

            if (targetTop !== rect.top) {
                if (el.style.transform !== 'none') {
                    el.style.transform = 'none';
                    el.style.left = rect.left + 'px';
                    el.style.top = rect.top + 'px';
                }
                el.style.top = targetTop + 'px';
            }

            const onEnd = () => {
                el.style.height = '';
                keepPanelInViewport(el);
                el.removeEventListener('transitionend', onEnd);
            };
            el.addEventListener('transitionend', onEnd);
        }

        keepPanelInViewport(el);
    }

    function hidePanel(el) {
        if (!el || el.style.display === 'none') return;
        el.classList.remove('cga-enter');
        el.classList.add('cga-exit');
        setTimeout(() => {
            if (el.classList.contains('cga-exit')) {
                el.style.display = 'none';
                el.classList.remove('cga-exit');
            }
        }, 170);
    }

    document.addEventListener('keydown', e => {
        if (awaitingHotkeyCapture || awaitingMapsHotkeyCapture) {
            e.preventDefault();
            e.stopImmediatePropagation();
            if (e.key === 'Escape') {
                awaitingHotkeyCapture = false;
                awaitingMapsHotkeyCapture = false;
                syncHotkeyUi();
                return;
            }
            if (e.key === 'Shift' && e.location === KeyboardEvent.DOM_KEY_LOCATION_LEFT) {
                return;
            }
            if (awaitingHotkeyCapture) {
                cfg.toggleHotkeyCode = e.code;
                cfg.toggleHotkeyLabel = formatHotkeyFromEvent(e);
            }
            if (awaitingMapsHotkeyCapture) {
                cfg.mapsHotkeyCode = e.code;
                cfg.mapsHotkeyLabel = formatHotkeyFromEvent(e);
            }
            awaitingHotkeyCapture = false;
            awaitingMapsHotkeyCapture = false;
            saveConfig();
            syncHotkeyUi();
            return;
        }

        if (e.key === 'Shift' && e.location === KeyboardEvent.DOM_KEY_LOCATION_LEFT) {
            e.preventDefault();
            e.stopImmediatePropagation();
            if (!panel) buildPanel();
            else {
                const shouldShow = panel.style.display === 'none';
                if (shouldShow) showPanel(panel);
                else hidePanel(panel);
            }
            return;
        }

        if (cfg.toggleHotkeyCode && e.code === cfg.toggleHotkeyCode) {
            e.preventDefault();
            e.stopImmediatePropagation();
            toggleMostRecentCheat();
            return;
        }

        if (cfg.mapsHotkeyCode && e.code === cfg.mapsHotkeyCode) {
            e.preventDefault();
            e.stopImmediatePropagation();
            openCurrentLocationInMaps();
        }
    }, true);

    window.addEventListener('resize', () => keepPanelInViewport(panel));

    let lastUrl = location.href;
    setInterval(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            setTimeout(() => {
                gameMap = null;
                lastCoords = null;
                countryBusy = false;
                countryRunId++;
                clearCountryRetry();
                countryRetryAttempt = 0;
                exactPinMarker = null;
                if (coordPollId) { clearInterval(coordPollId); coordPollId = null; }
                iframeObserver.observe(document.body, { childList: true, subtree: true });
                mapObserver.observe(document.body, { childList: true, subtree: true });
            }, 900);
        }
    }, 500);

    const domWatcher = new MutationObserver(() => {
        if (!document.querySelector('.leaflet-container') && gameMap) gameMap = null;
    });
    domWatcher.observe(document.body, { childList: true, subtree: true });

})();