WG Cheat Panel

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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