WG Cheat Panel

Clean cheat overlay for WorldGuessr — reads coords from Street View iframe, pins the location on the guess map

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         WG Cheat Panel
// @namespace    wg-cheat-panel
// @version      1.0.0
// @description  Clean cheat overlay for WorldGuessr — reads coords from Street View iframe, pins the location on the guess map
// @author       RandomAccount
// @match        https://www.worldguessr.com/*
// @license      MIT
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(() => {
  "use strict";

  // ─────────────────────────────────────────────
  // State
  // ─────────────────────────────────────────────
  let lat = null;
  let lng = null;
  let panelOpen = false;
  let isDragging = false;
  let dragOffX = 0;
  let dragOffY = 0;
  let lastSrc = "";
  let leafletMarker = null;
  let pinActive = false;

  // ─────────────────────────────────────────────
  // Coord extraction — parses Google Maps iframe src
  // ─────────────────────────────────────────────
  function parseCoords(src) {
    if (!src) return null;
    let m;
    m = src.match(/[?&]location=(-?\d+\.\d+),(-?\d+\.\d+)/);
    if (m) return { lat: parseFloat(m[1]), lng: parseFloat(m[2]) };
    m = src.match(/[?&]q=(-?\d+\.\d+),(-?\d+\.\d+)/);
    if (m) return { lat: parseFloat(m[1]), lng: parseFloat(m[2]) };
    m = src.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
    if (m) return { lat: parseFloat(m[1]), lng: parseFloat(m[2]) };
    m = src.match(/!2d(-?\d+\.\d+)!3d(-?\d+\.\d+)/);
    if (m) return { lat: parseFloat(m[2]), lng: parseFloat(m[1]) };
    m = src.match(/cbll=(-?\d+\.\d+),(-?\d+\.\d+)/);
    if (m) return { lat: parseFloat(m[1]), lng: parseFloat(m[2]) };
    return null;
  }

  function findCoords() {
    for (const iframe of document.querySelectorAll("iframe")) {
      const src = iframe.src || iframe.getAttribute("src") || "";
      if (!src.includes("google.com/maps")) continue;
      const c = parseCoords(src);
      if (c) return c;
    }
    return null;
  }

  // ─────────────────────────────────────────────
  // Leaflet map finder
  // Uses the same deep recursive scan as the
  // original working script: walks object properties
  // up to depth 4 looking for a Leaflet map instance
  // whose _container matches the leaflet-container el.
  // ─────────────────────────────────────────────
  function isLeafletCandidate(obj, container) {
    return Boolean(
      obj &&
      typeof obj === "object" &&
      typeof obj.latLngToContainerPoint === "function" &&
      typeof obj.containerPointToLatLng === "function" &&
      typeof obj.getZoom === "function" &&
      obj._container === container
    );
  }

  function findLeafletMapForContainer(container) {
    if (!container) return null;
    const seen = new WeakSet();

    function scan(obj, depth) {
      if (!obj || typeof obj !== "object" || seen.has(obj) || depth < 0) return null;
      seen.add(obj);
      if (isLeafletCandidate(obj, container)) return obj;
      let keys = [];
      try { keys = Object.getOwnPropertyNames(obj); } catch { return null; }
      for (const key of keys) {
        if (key === "parentNode" || key === "children" || key === "childNodes") continue;
        let val;
        try { val = obj[key]; } catch { continue; }
        if (!val || typeof val !== "object") continue;
        if (isLeafletCandidate(val, container)) return val;
        const found = scan(val, depth - 1);
        if (found) return found;
      }
      return null;
    }

    // Search roots: the container itself, its parent, any #miniMapArea ancestor, then window
    const roots = [
      container,
      container.parentElement,
      container.closest("#miniMapArea"),
      window,
    ];
    for (const root of roots) {
      if (!root) continue;
      const found = scan(root, root === window ? 2 : 4);
      if (found) return found;
    }
    return null;
  }

  function getLeafletMap() {
    const containers = document.querySelectorAll(".leaflet-container");
    for (const el of containers) {
      const map = findLeafletMapForContainer(el);
      if (map) return map;
    }
    return null;
  }

  // ─────────────────────────────────────────────
  // Pin management
  // ─────────────────────────────────────────────
  // Extract the Leaflet constructor (L) from the map instance itself.
  // WorldGuessr bundles Leaflet as a module so window.L is often undefined.
  // But the map object's prototype chain leads back to L.Map, and L.Map
  // has .addInitHook on it — we can walk up to find the L namespace by
  // looking for divIcon / marker on the map's constructor exports,
  // or we can use the map's own addLayer to place a raw marker directly.
  function getLFromMap(map) {
    if (window.L) return window.L;
    // Try to get L from the map's options or internal refs
    try {
      // Leaflet attaches _leaflet_id to instances; the constructor is L.Map
      // Walk the prototype to find the namespace
      let proto = Object.getPrototypeOf(map);
      while (proto) {
        const ctor = proto.constructor;
        if (ctor && ctor.version && typeof ctor.marker === "function") return ctor;
        proto = Object.getPrototypeOf(proto);
      }
    } catch {}
    return null;
  }

  function placePin(coordLat, coordLng) {
    const map = getLeafletMap();
    if (!map) return false;

    // Remove existing marker
    if (leafletMarker) {
      try { map.removeLayer(leafletMarker); } catch {}
      leafletMarker = null;
    }

    // Build the pin as a raw positioned div injected into the map's pane,
    // positioned via Leaflet's own latLngToLayerPoint — no L namespace needed.
    try {
      const L = getLFromMap(map);

      if (L) {
        // Full Leaflet path — proper marker with custom icon
        const icon = L.divIcon({
          className: "",
          html: `<div style="width:28px;height:36px;filter:drop-shadow(0 3px 8px rgba(99,102,241,0.5))">
            <svg viewBox="0 0 28 36" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%">
              <path d="M14 0C6.27 0 0 6.27 0 14c0 9.63 14 22 14 22S28 23.63 28 14C28 6.27 21.73 0 14 0z" fill="#6366f1"/>
              <circle cx="14" cy="14" r="5.5" fill="white"/>
              <circle cx="14" cy="14" r="3" fill="#6366f1"/>
            </svg>
          </div>`,
          iconSize: [28, 36],
          iconAnchor: [14, 36],
        });
        leafletMarker = L.marker([coordLat, coordLng], { icon, zIndexOffset: 9999 }).addTo(map);
      } else {
        // Fallback: inject a DOM pin directly into the marker pane and
        // reposition it whenever the map moves, using the map's own projection.
        const markerPane = map.getPane ? map.getPane("markerPane") : map._panes && map._panes.markerPane;
        if (!markerPane) return false;

        const pin = document.createElement("div");
        pin.id = "wgcp-leaflet-pin";
        pin.style.cssText = `
          position: absolute;
          width: 28px;
          height: 36px;
          pointer-events: none;
          z-index: 9999;
          filter: drop-shadow(0 3px 8px rgba(99,102,241,0.5));
          transform: translate(-14px, -36px);
        `;
        pin.innerHTML = `
          <svg viewBox="0 0 28 36" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%">
            <path d="M14 0C6.27 0 0 6.27 0 14c0 9.63 14 22 14 22S28 23.63 28 14C28 6.27 21.73 0 14 0z" fill="#6366f1"/>
            <circle cx="14" cy="14" r="5.5" fill="white"/>
            <circle cx="14" cy="14" r="3" fill="#6366f1"/>
          </svg>
        `;
        markerPane.appendChild(pin);

        function positionPin() {
          try {
            const pt = map.latLngToLayerPoint([coordLat, coordLng]);
            pin.style.left = pt.x + "px";
            pin.style.top  = pt.y + "px";
          } catch {}
        }

        positionPin();
        map.on("move zoom viewreset zoomend moveend", positionPin);

        // Store as a fake marker object so removePin can clean it up
        leafletMarker = {
          _wgcpDom: pin,
          _wgcpMap: map,
          _wgcpHandler: positionPin,
        };
      }

      pinActive = true;
      return true;
    } catch {
      return false;
    }
  }

  function removePin() {
    if (leafletMarker) {
      // Real Leaflet marker
      if (typeof leafletMarker.remove === "function" || typeof leafletMarker.removeFrom === "function") {
        const map = getLeafletMap();
        try { if (map) map.removeLayer(leafletMarker); } catch {}
        try { leafletMarker.remove(); } catch {}
      }
      // DOM fallback marker
      if (leafletMarker._wgcpDom) {
        try {
          leafletMarker._wgcpMap.off("move zoom viewreset zoomend moveend", leafletMarker._wgcpHandler);
          leafletMarker._wgcpDom.remove();
        } catch {}
      }
      leafletMarker = null;
    }
    // Also clean up any orphaned pin divs
    document.getElementById("wgcp-leaflet-pin")?.remove();
    pinActive = false;
  }

  // ─────────────────────────────────────────────
  // Styles
  // ─────────────────────────────────────────────
  const style = document.createElement("style");
  style.textContent = `
    @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=DM+Mono:wght@400;500&display=swap');

    #wgcp-root {
      position: fixed;
      bottom: 24px;
      left: 24px;
      z-index: 999999;
      font-family: 'DM Sans', system-ui, sans-serif;
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 10px;
    }

    #wgcp-panel {
      width: 300px;
      background: #ffffff;
      border-radius: 20px;
      box-shadow:
        0 0 0 1px rgba(0,0,0,0.06),
        0 4px 6px -1px rgba(0,0,0,0.06),
        0 24px 48px -8px rgba(0,0,0,0.18);
      overflow: hidden;
      display: none;
      flex-direction: column;
      transform-origin: bottom left;
    }

    #wgcp-panel.open {
      display: flex;
      animation: wgcp-pop 0.2s cubic-bezier(0.34,1.56,0.64,1);
    }

    @keyframes wgcp-pop {
      from { opacity:0; transform:scale(0.88) translateY(12px); }
      to   { opacity:1; transform:scale(1) translateY(0); }
    }

    /* Header */
    #wgcp-header {
      padding: 14px 14px 0;
      display: flex;
      align-items: center;
      justify-content: space-between;
      cursor: move;
      user-select: none;
      gap: 10px;
    }

    #wgcp-logo {
      width: 32px;
      height: 32px;
      border-radius: 10px;
      background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
    }

    #wgcp-logo svg {
      width: 16px;
      height: 16px;
      fill: none;
      stroke: white;
      stroke-width: 2;
      stroke-linecap: round;
    }

    #wgcp-title-group { flex: 1; min-width: 0; }

    #wgcp-title {
      font-size: 14px;
      font-weight: 700;
      color: #0f172a;
      line-height: 1.2;
    }

    #wgcp-subtitle {
      font-size: 11px;
      color: #94a3b8;
      font-weight: 500;
    }

    #wgcp-close {
      width: 26px;
      height: 26px;
      border-radius: 8px;
      border: none;
      background: #f1f5f9;
      color: #94a3b8;
      cursor: pointer;
      font-size: 13px;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.15s;
      flex-shrink: 0;
    }
    #wgcp-close:hover { background: #fee2e2; color: #ef4444; }

    /* Status */
    #wgcp-status-wrap { padding: 10px 14px 0; }

    #wgcp-status {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 4px 10px;
      border-radius: 999px;
      background: #f8fafc;
      border: 1px solid #e2e8f0;
      font-size: 11px;
      font-weight: 600;
      color: #94a3b8;
      transition: all 0.3s;
    }

    #wgcp-status.found {
      background: #f0fdf4;
      border-color: #86efac;
      color: #16a34a;
    }

    #wgcp-dot {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: #cbd5e1;
      flex-shrink: 0;
      transition: background 0.3s, box-shadow 0.3s;
    }

    #wgcp-status.found #wgcp-dot {
      background: #22c55e;
      box-shadow: 0 0 0 3px rgba(34,197,94,0.2);
      animation: wgcp-blink 2s infinite;
    }

    @keyframes wgcp-blink {
      0%,100% { opacity: 1; }
      50% { opacity: 0.5; }
    }

    /* Coord cards */
    #wgcp-coords {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 8px;
      padding: 10px 14px 0;
    }

    .wgcp-card {
      background: #f8fafc;
      border: 1px solid #e2e8f0;
      border-radius: 12px;
      padding: 10px 11px;
      transition: all 0.25s;
    }

    .wgcp-card.lit {
      background: #eef2ff;
      border-color: #c7d2fe;
    }

    .wgcp-card-label {
      font-size: 10px;
      font-weight: 700;
      letter-spacing: 0.07em;
      text-transform: uppercase;
      color: #cbd5e1;
      margin-bottom: 2px;
      transition: color 0.25s;
    }

    .wgcp-card.lit .wgcp-card-label { color: #a5b4fc; }

    .wgcp-card-value {
      font-family: 'DM Mono', monospace;
      font-size: 14px;
      font-weight: 500;
      color: #dde0ff;
      line-height: 1.3;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      transition: color 0.25s;
    }

    .wgcp-card.lit .wgcp-card-value { color: #4338ca; }

    /* Map */
    #wgcp-map-wrap {
      margin: 10px 14px 0;
      border-radius: 12px;
      overflow: hidden;
      height: 152px;
      background: #f8fafc;
      border: 1px solid #e2e8f0;
      position: relative;
      flex-shrink: 0;
    }

    #wgcp-map-empty {
      position: absolute;
      inset: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 6px;
    }

    #wgcp-map-empty svg {
      width: 26px;
      height: 26px;
      stroke: #e2e8f0;
      fill: none;
      stroke-width: 1.5;
    }

    #wgcp-map-empty span {
      font-size: 12px;
      color: #cbd5e1;
      font-weight: 500;
    }

    #wgcp-map-iframe {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      border: none;
      display: none;
    }

    /* Actions */
    #wgcp-actions {
      display: grid;
      grid-template-columns: 1fr 1fr 1fr;
      gap: 6px;
      padding: 10px 14px 14px;
    }

    .wgcp-btn {
      border: 1px solid #e2e8f0;
      border-radius: 10px;
      padding: 9px 6px 8px;
      font-family: 'DM Sans', system-ui, sans-serif;
      font-size: 11px;
      font-weight: 600;
      cursor: pointer;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 4px;
      transition: all 0.15s;
      background: #f8fafc;
      color: #475569;
      line-height: 1.1;
      text-align: center;
    }

    .wgcp-btn svg {
      width: 15px;
      height: 15px;
      fill: none;
      stroke: #6366f1;
      stroke-width: 2;
      stroke-linecap: round;
      stroke-linejoin: round;
      flex-shrink: 0;
    }

    .wgcp-btn:hover {
      background: #f1f5f9;
      border-color: #c7d2fe;
      color: #4338ca;
      transform: translateY(-1px);
    }

    .wgcp-btn:active { transform: translateY(0); }

    .wgcp-btn.primary {
      background: linear-gradient(135deg, #6366f1, #8b5cf6);
      border-color: transparent;
      color: white;
      box-shadow: 0 2px 8px rgba(99,102,241,0.3);
    }

    .wgcp-btn.primary svg { stroke: white; }
    .wgcp-btn.primary:hover { opacity: 0.9; color: white; border-color: transparent; }

    .wgcp-btn.danger {
      background: #fef2f2;
      border-color: #fecaca;
      color: #dc2626;
    }

    .wgcp-btn.danger svg { stroke: #dc2626; }
    .wgcp-btn.danger:hover { background: #fee2e2; color: #dc2626; border-color: #fca5a5; }

    .wgcp-btn.success {
      background: #f0fdf4;
      border-color: #86efac;
      color: #16a34a;
    }

    .wgcp-btn.success svg { stroke: #16a34a; }

    /* FAB */
    #wgcp-fab-wrap { position: relative; }

    #wgcp-fab {
      width: 46px;
      height: 46px;
      border-radius: 14px;
      border: none;
      background: linear-gradient(135deg, #6366f1, #8b5cf6);
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      box-shadow: 0 4px 14px rgba(99,102,241,0.45);
      transition: all 0.2s cubic-bezier(0.34,1.56,0.64,1);
    }

    #wgcp-fab:hover {
      transform: scale(1.08);
      box-shadow: 0 6px 20px rgba(99,102,241,0.55);
    }

    #wgcp-fab:active { transform: scale(0.96); }

    #wgcp-fab svg {
      width: 20px;
      height: 20px;
      fill: none;
      stroke: white;
      stroke-width: 2.5;
      stroke-linecap: round;
      transition: transform 0.25s;
    }

    #wgcp-fab.open svg { transform: rotate(45deg); }

    #wgcp-pin-badge {
      position: absolute;
      top: -3px;
      right: -3px;
      width: 11px;
      height: 11px;
      border-radius: 50%;
      background: #22c55e;
      border: 2px solid white;
      display: none;
    }
  `;
  document.head.appendChild(style);

  // ─────────────────────────────────────────────
  // Build DOM
  // ─────────────────────────────────────────────
  const root = document.createElement("div");
  root.id = "wgcp-root";
  document.body.appendChild(root);

  const panel = document.createElement("div");
  panel.id = "wgcp-panel";
  root.appendChild(panel);

  panel.innerHTML = `
    <div id="wgcp-header">
      <div id="wgcp-logo">
        <svg viewBox="0 0 24 24">
          <circle cx="12" cy="12" r="3"/>
          <circle cx="12" cy="12" r="7"/>
          <line x1="12" y1="2" x2="12" y2="5"/>
          <line x1="12" y1="19" x2="12" y2="22"/>
          <line x1="2" y1="12" x2="5" y2="12"/>
          <line x1="19" y1="12" x2="22" y2="12"/>
        </svg>
      </div>
      <div id="wgcp-title-group">
        <div id="wgcp-title">WG Cheat Panel</div>
        <div id="wgcp-subtitle">Location assistant</div>
      </div>
      <button id="wgcp-close">✕</button>
    </div>

    <div id="wgcp-status-wrap">
      <div id="wgcp-status">
        <div id="wgcp-dot"></div>
        <span id="wgcp-status-text">Waiting for location…</span>
      </div>
    </div>

    <div id="wgcp-coords">
      <div class="wgcp-card" id="wgcp-lat-card">
        <div class="wgcp-card-label">Latitude</div>
        <div class="wgcp-card-value" id="wgcp-lat">—</div>
      </div>
      <div class="wgcp-card" id="wgcp-lng-card">
        <div class="wgcp-card-label">Longitude</div>
        <div class="wgcp-card-value" id="wgcp-lng">—</div>
      </div>
    </div>

    <div id="wgcp-map-wrap">
      <div id="wgcp-map-empty">
        <svg viewBox="0 0 24 24"><path d="M3 7l6-3 6 3 6-3v13l-6 3-6-3-6 3V7z"/><line x1="9" y1="4" x2="9" y2="17"/><line x1="15" y1="7" x2="15" y2="20"/></svg>
        <span>No location yet</span>
      </div>
      <iframe id="wgcp-map-iframe" loading="lazy" referrerpolicy="no-referrer-when-downgrade" allowfullscreen></iframe>
    </div>

    <div id="wgcp-actions">
      <button class="wgcp-btn primary" id="wgcp-pin-btn">
        <svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg>
        Pin map
      </button>
      <button class="wgcp-btn" id="wgcp-copy-btn">
        <svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
        Copy
      </button>
      <button class="wgcp-btn" id="wgcp-maps-btn">
        <svg viewBox="0 0 24 24"><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"/></svg>
        Maps
      </button>
    </div>
  `;

  // FAB
  const fabWrap = document.createElement("div");
  fabWrap.id = "wgcp-fab-wrap";
  const fab = document.createElement("button");
  fab.id = "wgcp-fab";
  fab.innerHTML = `<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`;
  const pinBadge = document.createElement("div");
  pinBadge.id = "wgcp-pin-badge";
  fabWrap.appendChild(fab);
  fabWrap.appendChild(pinBadge);
  root.appendChild(fabWrap);

  // ─────────────────────────────────────────────
  // Refs
  // ─────────────────────────────────────────────
  const statusEl   = panel.querySelector("#wgcp-status");
  const statusText = panel.querySelector("#wgcp-status-text");
  const latEl      = panel.querySelector("#wgcp-lat");
  const lngEl      = panel.querySelector("#wgcp-lng");
  const latCard    = panel.querySelector("#wgcp-lat-card");
  const lngCard    = panel.querySelector("#wgcp-lng-card");
  const mapIframe  = panel.querySelector("#wgcp-map-iframe");
  const mapEmpty   = panel.querySelector("#wgcp-map-empty");
  const pinBtn     = panel.querySelector("#wgcp-pin-btn");
  const copyBtn    = panel.querySelector("#wgcp-copy-btn");
  const mapsBtn    = panel.querySelector("#wgcp-maps-btn");
  const closeBtn   = panel.querySelector("#wgcp-close");
  const header     = panel.querySelector("#wgcp-header");

  // ─────────────────────────────────────────────
  // Drag
  // ─────────────────────────────────────────────
  header.addEventListener("mousedown", (e) => {
    if (e.target === closeBtn) return;
    isDragging = true;
    const rect = root.getBoundingClientRect();
    dragOffX = e.clientX - rect.left;
    dragOffY = e.clientY - rect.bottom;
    e.preventDefault();
  });

  document.addEventListener("mousemove", (e) => {
    if (!isDragging) return;
    root.style.left   = Math.max(8, e.clientX - dragOffX) + "px";
    root.style.bottom = Math.max(8, window.innerHeight - (e.clientY - dragOffY)) + "px";
  });

  document.addEventListener("mouseup", () => { isDragging = false; });

  // ─────────────────────────────────────────────
  // FAB toggle
  // ─────────────────────────────────────────────
  fab.addEventListener("click", () => {
    panelOpen = !panelOpen;
    panel.classList.toggle("open", panelOpen);
    fab.classList.toggle("open", panelOpen);
  });

  closeBtn.addEventListener("click", () => {
    panelOpen = false;
    panel.classList.remove("open");
    fab.classList.remove("open");
  });

  // ─────────────────────────────────────────────
  // Pin button
  // ─────────────────────────────────────────────
  function setPinBtnState(active) {
    if (active) {
      pinBtn.className = "wgcp-btn danger";
      pinBtn.innerHTML = `
        <svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
        Remove
      `;
      pinBadge.style.display = "block";
    } else {
      pinBtn.className = "wgcp-btn primary";
      pinBtn.innerHTML = `
        <svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg>
        Pin map
      `;
      pinBadge.style.display = "none";
    }
  }

  pinBtn.addEventListener("click", () => {
    if (lat === null) return;

    if (pinActive) {
      removePin();
      setPinBtnState(false);
    } else {
      const ok = placePin(lat, lng);
      if (ok) {
        setPinBtnState(true);
      } else {
        const prev = pinBtn.innerHTML;
        pinBtn.className = "wgcp-btn";
        pinBtn.textContent = "Not ready";
        setTimeout(() => {
          pinBtn.className = "wgcp-btn primary";
          pinBtn.innerHTML = prev;
        }, 1500);
      }
    }
  });

  // ─────────────────────────────────────────────
  // Copy
  // ─────────────────────────────────────────────
  copyBtn.addEventListener("click", () => {
    if (lat === null) return;
    navigator.clipboard.writeText(`${lat.toFixed(6)}, ${lng.toFixed(6)}`).then(() => {
      copyBtn.classList.add("success");
      copyBtn.innerHTML = `<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>Copied!`;
      setTimeout(() => {
        copyBtn.classList.remove("success");
        copyBtn.innerHTML = `<svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>Copy`;
      }, 1800);
    });
  });

  // ─────────────────────────────────────────────
  // Open Google Maps
  // ─────────────────────────────────────────────
  mapsBtn.addEventListener("click", () => {
    if (lat === null) return;
    window.open(`https://www.google.com/maps?q=${lat},${lng}&z=10`, "_blank");
  });

  // ─────────────────────────────────────────────
  // UI update
  // ─────────────────────────────────────────────
  function updateUI(coords) {
    if (!coords) return;
    if (coords.lat === lat && coords.lng === lng) return;

    lat = coords.lat;
    lng = coords.lng;

    statusEl.classList.add("found");
    statusText.textContent = "Location acquired";

    latEl.textContent = lat.toFixed(5);
    lngEl.textContent = lng.toFixed(5);
    latCard.classList.add("lit");
    lngCard.classList.add("lit");

    const url = `https://www.google.com/maps?q=${lat},${lng}&z=7&output=embed`;
    if (mapIframe.src !== url) mapIframe.src = url;
    mapIframe.style.display = "block";
    mapEmpty.style.display = "none";

    // Move existing pin to new coords on round change
    if (pinActive) placePin(lat, lng);
  }

  // ─────────────────────────────────────────────
  // Polling
  // ─────────────────────────────────────────────
  function poll() {
    const iframe = document.querySelector("iframe[src*='google.com/maps']");
    const src = iframe ? (iframe.src || "") : "";
    if (src === lastSrc) return;
    lastSrc = src;
    updateUI(findCoords());
  }

  new MutationObserver(() => {
    updateUI(findCoords());
  }).observe(document.body, {
    subtree: true,
    childList: true,
    attributes: true,
    attributeFilter: ["src"],
  });

  setInterval(poll, 800);
  poll();
})();