Recon+

Unlocks Paywalls in recon

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Recon+
// @namespace    https://update.greasyfork.org/scripts/582087/Recon%2B.user.js
// @version      2.1
// @match        https://www.recon.com/*
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @connect      basemaps.cartocdn.com
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require      https://unpkg.com/[email protected]/dist/leaflet.js
// @resource     LEAFLET_CSS https://unpkg.com/[email protected]/dist/leaflet.css
// @run-at       document-start
// @description Unlocks Paywalls in recon
// ==/UserScript==

(function(){
// src/state.ts
var win = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
var origXHR = win.XMLHttpRequest;
var profileDetailsCache = new Map;
var profileVersionsMap = new Map;
var profileMediaMap = new Map;
var state = {
  lastBlockedRequest: null,
  lastProfileDetailPageData: null,
  authHeaders: {}
};
var interestMap = {
  1: "Recon Men",
  2: "Skinheads",
  4: "Leather",
  5: "Rubber",
  6: "Sports Gear",
  7: "Military",
  8: "Hoods & Masks",
  9: "Muscle",
  10: "Punks",
  13: "Bikers",
  15: "Bondage",
  18: "Suits",
  19: "Fisting",
  20: "Masters & Slaves",
  27: "Boots",
  30: "Tattoos & Piercings",
  31: "Bears",
  34: "Fighting",
  36: "Feet",
  37: "Pups & Handlers",
  38: "Smokers",
  39: "Gunge",
  40: "Trackies",
  41: "Underwear",
  42: "Chastity",
  43: "Watersports",
  44: "Impact Play",
  45: "Electro",
  46: "Sneakers & Socks",
  47: "ADBL"
};

// src/toast.ts
function showToast(message, type = "success") {
  let container = document.getElementById("custom-toast-container");
  if (!container) {
    container = document.createElement("div");
    container.id = "custom-toast-container";
    container.style.cssText = `position: fixed; top: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none;`;
    document.body.appendChild(container);
  }
  const toast = document.createElement("div");
  toast.textContent = message;
  toast.style.cssText = `background: ${type === "error" ? "#cf2a2a" : "#2acf65"}; color: #fff; padding: 12px 24px; border-radius: 4px; font-family: sans-serif; font-weight: bold; font-size: 14px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); opacity: 0; transform: translateY(-20px); transition: all 0.3s ease;`;
  container.appendChild(toast);
  setTimeout(() => {
    toast.style.opacity = "1";
    toast.style.transform = "translateY(0)";
  }, 10);
  setTimeout(() => {
    toast.style.opacity = "0";
    toast.style.transform = "translateY(-20px)";
    setTimeout(() => toast.remove(), 300);
  }, 3500);
}
function showProgressIndicator(title) {
  let container = document.getElementById("custom-toast-container");
  if (!container) {
    container = document.createElement("div");
    container.id = "custom-toast-container";
    container.style.cssText = `position: fixed; top: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none;`;
    document.body.appendChild(container);
  }
  const card = document.createElement("div");
  card.style.cssText = `
        background: rgba(20, 20, 22, 0.95);
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
        color: #fff;
        padding: 16px;
        border-radius: 8px;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        font-size: 14px;
        box-shadow: 0 10px 25px rgba(0,0,0,0.5);
        border: 1px solid rgba(255,255,255,0.1);
        width: 300px;
        pointer-events: auto;
        opacity: 0;
        transform: translateY(-20px);
        transition: all 0.3s ease;
        display: flex;
        flex-direction: column;
        gap: 10px;
    `;
  const header = document.createElement("div");
  header.style.cssText = `display: flex; justify-content: space-between; align-items: center; font-weight: bold;`;
  const titleEl = document.createElement("span");
  titleEl.textContent = title;
  const pctEl = document.createElement("span");
  pctEl.style.cssText = `color: #a855f7; font-variant-numeric: tabular-nums; font-weight: bold;`;
  pctEl.textContent = "0%";
  header.appendChild(titleEl);
  header.appendChild(pctEl);
  const msgEl = document.createElement("div");
  msgEl.style.cssText = `font-size: 12px; color: rgba(255,255,255,0.7); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`;
  msgEl.textContent = "Initializing...";
  const barTrack = document.createElement("div");
  barTrack.style.cssText = `background: rgba(255,255,255,0.1); height: 6px; border-radius: 3px; overflow: hidden;`;
  const barFill = document.createElement("div");
  barFill.style.cssText = `width: 0%; height: 100%; background: linear-gradient(90deg, #6366f1, #a855f7); transition: width 0.2s ease; border-radius: 3px;`;
  barTrack.appendChild(barFill);
  card.appendChild(header);
  card.appendChild(msgEl);
  card.appendChild(barTrack);
  container.appendChild(card);
  setTimeout(() => {
    card.style.opacity = "1";
    card.style.transform = "translateY(0)";
  }, 10);
  return {
    update(percent, message) {
      const safePercent = Math.min(100, Math.max(0, percent));
      pctEl.textContent = `${Math.round(safePercent)}%`;
      msgEl.textContent = message;
      barFill.style.width = `${safePercent}%`;
      if (safePercent >= 100) {
        barFill.style.background = "linear-gradient(90deg, #2ec4b6, #2acf65)";
        pctEl.style.color = "#2acf65";
      }
    },
    close(delay = 1000) {
      setTimeout(() => {
        card.style.opacity = "0";
        card.style.transform = "translateY(-20px)";
        setTimeout(() => card.remove(), 300);
      }, delay);
    }
  };
}

// src/utils.ts
async function fetchImageAsBlob(url) {
  try {
    const res = await win.fetch(url);
    if (res.ok)
      return await res.blob();
  } catch (e) {
    console.warn(`[Script] Standard fetch failed for ${url}, trying GM_xmlhttpRequest:`, e);
  }
  if (typeof GM_xmlhttpRequest !== "undefined") {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url,
        responseType: "blob",
        onload: function(response) {
          if (response.status >= 200 && response.status < 300) {
            resolve(response.response);
          } else {
            reject(new Error(`GM_xmlhttpRequest failed with status ${response.status}`));
          }
        },
        onerror: function(err) {
          reject(err);
        }
      });
    });
  }
  throw new Error(`Cannot fetch ${url} without CORS or GM_xmlhttpRequest`);
}
function getJSZip() {
  if (typeof JSZip !== "undefined")
    return Promise.resolve(JSZip);
  if (win.JSZip)
    return Promise.resolve(win.JSZip);
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js";
    script.onload = () => resolve(window.JSZip || win.JSZip);
    script.onerror = () => reject(new Error("Failed to load JSZip"));
    document.head.appendChild(script);
  });
}
async function exportProfileData() {
  const progress = showProgressIndicator("Exporting Profile");
  let JSZipLib;
  try {
    progress.update(5, "Loading JSZip library...");
    JSZipLib = await getJSZip();
  } catch (e) {
    console.error(e);
    showToast("Failed to load JSZip library.", "error");
    progress.close(0);
    return;
  }
  progress.update(10, "Analyzing profile data...");
  const data = state.lastProfileDetailPageData;
  const profileId = data ? data.id : (win.location.pathname.match(/\/(?:members|profiles)\/([a-f0-9-]{36}|[a-zA-Z0-9_-]+)/i) || [])[1] || "unknown";
  let displayName = "";
  if (data) {
    displayName = data.displayName || data.username || data.name || "";
  }
  if (!displayName) {
    const h1 = document.querySelector("h1, .profile-header h1, t101-profile-header h1, .member-info h1, .profile-info-container h1");
    if (h1 && h1.innerText.trim()) {
      displayName = h1.innerText.trim();
    } else if (document.title) {
      displayName = document.title.replace(/\s*-\s*Recon\s*$/i, "").trim();
    }
  }
  if (!displayName) {
    displayName = "Profile";
  }
  const photoUrls = new Set;
  document.querySelectorAll(".photo-tile").forEach((t) => {
    const htmlEl = t;
    const bgImg = htmlEl.style.backgroundImage;
    if (bgImg) {
      const url = bgImg.replace(/^url\(["']?/, "").replace(/["']?\)$/, "").split("?")[0];
      if (url)
        photoUrls.add(url);
    }
  });
  document.querySelectorAll(".profile-avatar").forEach((t) => {
    const htmlEl = t;
    const bgImg = htmlEl.style.backgroundImage;
    if (bgImg) {
      const url = bgImg.replace(/^url\(["']?/, "").replace(/["']?\)$/, "").split("?")[0];
      if (url)
        photoUrls.add(url);
    }
  });
  if (data) {
    const findUrls = (obj) => {
      if (typeof obj === "string") {
        if (obj.startsWith("http") && (obj.includes("/photos/") || obj.includes("/images/") || obj.match(/\.(jpg|jpeg|png|webp|gif)/i))) {
          photoUrls.add(obj.split("?")[0]);
        }
      } else if (typeof obj === "object" && obj !== null) {
        for (const key in obj) {
          if (Object.prototype.hasOwnProperty.call(obj, key)) {
            findUrls(obj[key]);
          }
        }
      }
    };
    findUrls(data);
  }
  const urlsArray = Array.from(photoUrls);
  if (urlsArray.length === 0) {
    showToast("No photos found to export.", "error");
    progress.close(0);
    return;
  }
  progress.update(15, `Found ${urlsArray.length} photos. Starting download...`);
  const zip = new JSZipLib;
  let infoText = "";
  infoText += `=== RECON PROFILE EXPORT ===
`;
  infoText += `Export Date: ${new Date().toLocaleString()}
`;
  infoText += `Profile URL: ${win.location.href}

`;
  const interestMap2 = {
    1: "Recon Men",
    2: "Skinheads",
    4: "Leather",
    5: "Rubber",
    6: "Sports Gear",
    7: "Military",
    8: "Hoods & Masks",
    9: "Muscle",
    10: "Punks",
    13: "Bikers",
    15: "Bondage",
    18: "Suits",
    19: "Fisting",
    20: "Masters & Slaves",
    27: "Boots",
    30: "Tattoos & Piercings",
    31: "Bears",
    34: "Fighting",
    36: "Feet",
    37: "Pups & Handlers",
    38: "Smokers",
    39: "Gunge",
    40: "Trackies",
    41: "Underwear",
    42: "Chastity",
    43: "Watersports",
    44: "Impact Play",
    45: "Electro",
    46: "Sneakers & Socks",
    47: "ADBL"
  };
  if (data) {
    infoText += `Display Name: ${data.displayName || "N/A"}
`;
    infoText += `Username: ${data.username || "N/A"}
`;
    infoText += `Profile ID: ${data.id || "N/A"}
`;
    if (data.location) {
      infoText += `Location: ID ${data.location.locationId || "N/A"}
`;
    }
    if (data.lastUpdatedDate) {
      infoText += `Last Updated: ${data.lastUpdatedDate}
`;
    }
    if (data.interests && data.interests.length > 0) {
      infoText += `Interests: ${data.interests.map((id) => interestMap2[id] || `Tag ${id}`).join(", ")}
`;
    }
    infoText += `
--- PROFILE INFO (RAW KEY-VALUES) ---
`;
    for (const [key, val] of Object.entries(data)) {
      if (typeof val !== "object" && val !== null) {
        infoText += `${key}: ${val}
`;
      }
    }
  }
  const bioSection = document.querySelector(".text-container, .bio-container, .member-bio");
  if (bioSection) {
    infoText += `
--- PROFILE BIO ---
`;
    infoText += bioSection.innerText.trim() + `
`;
  }
  const infoSections = document.querySelectorAll(".details-container, .stats-container, .profile-details");
  if (infoSections.length > 0) {
    infoText += `
--- PROFILE DETAILS ---
`;
    infoSections.forEach((section) => {
      const htmlEl = section;
      infoText += htmlEl.innerText.trim() + `

`;
    });
  }
  zip.file("profile_info.txt", infoText);
  if (data) {
    zip.file("profile_data.json", JSON.stringify(data, null, 4));
  }
  let successCount = 0;
  let completedCount = 0;
  const photosFolder = zip.folder("photos");
  const downloadPromises = urlsArray.map(async (url, idx) => {
    try {
      const blob = await fetchImageAsBlob(url);
      const ext = url.split(".").pop()?.split(/[?#]/)[0] || "jpg";
      photosFolder.file(`photo_${idx + 1}.${ext}`, blob);
      successCount++;
    } catch (err) {
      console.error(`[Script] Failed to download photo from ${url}:`, err);
    } finally {
      completedCount++;
      const percent = 15 + Math.round(completedCount / urlsArray.length * 65);
      progress.update(percent, `Downloading photos (${completedCount}/${urlsArray.length})...`);
    }
  });
  await Promise.all(downloadPromises);
  if (successCount === 0) {
    showToast("Failed to download any profile photos.", "error");
    progress.close(0);
    return;
  }
  progress.update(80, `Zipping ${successCount} photos...`);
  try {
    const content = await zip.generateAsync({ type: "blob" }, (metadata) => {
      const percent = 80 + Math.round(metadata.percent / 100 * 15);
      progress.update(percent, `Zipping files (${Math.round(metadata.percent)}%)...`);
    });
    progress.update(95, "Saving export file...");
    const blobUrl = URL.createObjectURL(content);
    const a = document.createElement("a");
    const safeName = displayName?.replace(/[^a-z0-9_-]/gi, "_").toLowerCase();
    a.href = blobUrl;
    a.download = `${safeName}_${profileId}_export.zip`;
    a.click();
    URL.revokeObjectURL(blobUrl);
    progress.update(100, `Complete! Saved ${successCount}/${urlsArray.length} photos.`);
    progress.close(2000);
  } catch (err) {
    console.error("[Script] Error creating zip file:", err);
    showToast("Error generating zip archive.", "error");
    progress.close(0);
  }
}
function extractPhotoUrls(obj) {
  const urlsMap = new Map;
  const findUrls = (val) => {
    if (!val)
      return;
    if (typeof val === "object") {
      if (typeof val.url === "string" && val.url) {
        let url = val.url;
        if (url.startsWith("//")) {
          url = "https:" + url;
        }
        const baseUrl = url.split("?")[0];
        if (!urlsMap.has(baseUrl) || url.includes("size=")) {
          urlsMap.set(baseUrl, url);
        }
      }
      for (const key in val) {
        if (Object.prototype.hasOwnProperty.call(val, key)) {
          findUrls(val[key]);
        }
      }
    } else if (typeof val === "string") {
      if (val.startsWith("http") && (val.includes("/profiles/") || val.includes("/photos/") || val.includes("/images/") || val.includes("/files/") || val.includes("t101api.com") || val.match(/\.(jpg|jpeg|png|webp|gif)/i))) {
        const baseUrl = val.split("?")[0];
        if (!urlsMap.has(baseUrl) || val.includes("size=")) {
          urlsMap.set(baseUrl, val);
        }
      }
    }
  };
  findUrls(obj);
  return Array.from(urlsMap.values());
}

// src/locate.ts
function degToRad(d) {
  return d * Math.PI / 180;
}
function radToDeg(r) {
  return r * 180 / Math.PI;
}
function offsetLatLon(lat, lon, northMetres, eastMetres) {
  const R = 6371000;
  const dLat = northMetres / R;
  const dLon = eastMetres / (R * Math.cos(degToRad(lat)));
  return { lat: lat + radToDeg(dLat), lon: lon + radToDeg(dLon) };
}
function haversine(lat1, lon1, lat2, lon2) {
  const R = 6371000;
  const dLat = degToRad(lat2 - lat1);
  const dLon = degToRad(lon2 - lon1);
  const a = Math.sin(dLat / 2) ** 2 + Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) * Math.sin(dLon / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function bearingTo(lat1, lon1, lat2, lon2) {
  const φ1 = degToRad(lat1), φ2 = degToRad(lat2);
  const Δ_ = degToRad(lon2 - lon1);
  const y = Math.sin(Δ_) * Math.cos(φ2);
  const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δ_);
  return Math.atan2(y, x);
}
function destinationPoint(lat, lon, bearingRad, distMetres) {
  const R = 6371000;
  const d = distMetres / R;
  const φ1 = degToRad(lat), λ1 = degToRad(lon);
  const φ2 = Math.asin(Math.sin(φ1) * Math.cos(d) + Math.cos(φ1) * Math.sin(d) * Math.cos(bearingRad));
  const λ2 = λ1 + Math.atan2(Math.sin(bearingRad) * Math.sin(d) * Math.cos(φ1), Math.cos(d) - Math.sin(φ1) * Math.sin(φ2));
  return { lat: radToDeg(φ2), lon: radToDeg(λ2) };
}
async function searchFromPoint(lat, lon, targetProfileId, myProfileId, age) {
  const ageMin = age ? String(age) : "18";
  const ageMax = age ? String(age) : "99";
  const params = new URLSearchParams({
    activeWithinMinutes: "525600",
    ageMax,
    ageMin,
    culture: "en",
    isExplore: "true",
    latitude: lat.toFixed(6),
    longitude: lon.toFixed(6),
    myProfileId,
    radiusMetres: "40225",
    sortProperty: "distance"
  });
  const url = `https://www.recon.com/api/profileSearch/profiles?${params.toString()}`;
  try {
    const res = await win.fetch(url, { headers: state.authHeaders });
    if (!res.ok) {
      console.warn(`[Recon+ Locate] fetch failed with status ${res.status}`);
      return null;
    }
    const json = await res.json();
    if (!json?.data)
      return null;
    const match = json.data.find((p) => p.profileId === targetProfileId);
    if (!match)
      return null;
    return { profileId: match.profileId, distanceMetres: match.distanceMetres };
  } catch (e) {
    console.warn("[Recon+ Locate] fetch error", e);
    return null;
  }
}
function getMyProfileId() {
  const navLinks = Array.from(document.querySelectorAll("a[href]"));
  for (const a of navLinks) {
    const m = a.href.match(/\/profiles\/([a-z0-9-]{36})\/?/i);
    if (m && !a.href.includes("cruise") && !a.href.includes("bookmarks") && !a.href.includes("connections")) {
      if (a.querySelector(".profile-avatar") || a.classList.contains("nav-item-right-container") || a.classList.contains("mobile-nav-cell")) {
        if (a.closest("t101-nav-bar, t101-mobile-nav-bar")) {
          return m[1];
        }
      }
    }
  }
  for (const a of navLinks) {
    const m = a.href.match(/\/profiles\/([a-z0-9-]{36})\/?$/i);
    if (m && a.closest("t101-nav-bar, t101-mobile-nav-bar"))
      return m[1];
  }
  return null;
}
function getTargetProfileId() {
  const urlMatch = win.location.pathname.match(/\/profiles\/([a-f0-9-]{36})/i);
  if (urlMatch)
    return urlMatch[1];
  if (state.lastProfileDetailPageData?.id) {
    return state.lastProfileDetailPageData.id;
  }
  for (const [id, data] of profileDetailsCache) {
    if (data.isDetailed)
      return id;
  }
  return null;
}
function getTargetAge() {
  const data = state.lastProfileDetailPageData;
  if (data && typeof data.age === "number" && data.age > 0) {
    return data.age;
  }
  const ageEl = document.querySelector('.age-value, .age, [class*="age-value"], [class*="age_value"]');
  if (ageEl && ageEl.textContent) {
    const m = ageEl.textContent.match(/(\d+)/);
    if (m)
      return parseInt(m[1], 10);
  }
  const text = document.body.innerText;
  const match = text.match(/Age\s*:\s*(\d+)/i) || text.match(/(\d+)\s*years?\s*old/i);
  if (match)
    return parseInt(match[1], 10);
  return null;
}
function getUserPosition() {
  return new Promise((resolve, reject) => {
    if (!navigator.geolocation) {
      reject(new Error("Geolocation not supported in this browser"));
      return;
    }
    navigator.geolocation.getCurrentPosition((pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }), (err) => reject(new Error(`Geolocation denied: ${err.message}`)), { enableHighAccuracy: false, timeout: 12000, maximumAge: 120000 });
  });
}
function getProfileDistanceMetres() {
  const d = state.lastProfileDetailPageData?.distanceMetres;
  if (typeof d === "number" && d > 0)
    return d;
  const distEl = document.querySelector(".distance-value");
  const text = distEl ? distEl.textContent : document.body.innerText;
  const kmMatch = text.match(/(\d[\d,]*\.?\d*)\s*km\b/i);
  if (kmMatch) {
    const v = parseFloat(kmMatch[1].replace(/,/g, "")) * 1000;
    if (v > 0)
      return v;
  }
  const miMatch = text.match(/(\d[\d,]*\.?\d*)\s*mi(?:le)?s?\b/i);
  if (miMatch) {
    const v = parseFloat(miMatch[1].replace(/,/g, "")) * 1609.34;
    if (v > 0)
      return v;
  }
  const ftMatch = text.match(/(\d[\d,]*\.?\d*)\s*ft\b/i);
  if (ftMatch) {
    const v = parseFloat(ftMatch[1].replace(/,/g, "")) * 0.3048;
    if (v > 0)
      return v;
  }
  const mMatch = text.match(/(\d[\d,]+)\s*m\b(?!\s*[il])/i);
  if (mMatch) {
    const v = parseFloat(mMatch[1].replace(/,/g, ""));
    if (v > 0)
      return v;
  }
  return null;
}
var PROBE_OFFSET_METRES = 5000;
async function triangulate(targetProfileId, myProfileId, centerLat, centerLon, onProgress) {
  const probeOffsets = [
    { name: "origin", north: 0, east: 0 },
    { name: "North", north: PROBE_OFFSET_METRES, east: 0 },
    { name: "South", north: -PROBE_OFFSET_METRES, east: 0 },
    { name: "East", north: 0, east: PROBE_OFFSET_METRES },
    { name: "West", north: 0, east: -PROBE_OFFSET_METRES }
  ];
  const targetAge = getTargetAge();
  let probes = [];
  const runScan = async (ageFilter) => {
    probes = [];
    for (let i = 0;i < probeOffsets.length; i++) {
      const off = probeOffsets[i];
      const { lat, lon } = offsetLatLon(centerLat, centerLon, off.north, off.east);
      const filterLabel = ageFilter ? `age ${ageFilter}` : "broad search";
      onProgress(`Scanning ${i + 1}/${probeOffsets.length} (${off.name}, ${filterLabel})…`);
      const hit = await searchFromPoint(lat, lon, targetProfileId, myProfileId, ageFilter);
      if (hit) {
        probes.push({ lat, lon, isHit: true, distanceMetres: hit.distanceMetres });
      } else {
        probes.push({ lat, lon, isHit: false, distanceMetres: 0 });
      }
      await new Promise((r) => setTimeout(r, 600));
    }
  };
  await runScan(targetAge);
  let hits = probes.filter((p) => p.isHit);
  if (hits.length === 0 && targetAge !== null) {
    onProgress("No hits with age filter. Retrying with broad age search…");
    await runScan(null);
    hits = probes.filter((p) => p.isHit);
  }
  if (hits.length === 0) {
    onProgress("Profile not found in any probe search.");
    return null;
  }
  let targetDist = getProfileDistanceMetres();
  if (!targetDist || targetDist <= 0) {
    const firstHit = hits[0];
    const distFromCenterToProbe = haversine(centerLat, centerLon, firstHit.lat, firstHit.lon);
    targetDist = firstHit.distanceMetres + distFromCenterToProbe;
  }
  onProgress("Resolving location using positive and negative constraints…");
  let bestLat = centerLat;
  let bestLon = centerLon;
  let minPenalty = Infinity;
  for (let deg = 0;deg < 360; deg++) {
    const bearingRad = degToRad(deg);
    const candidate = destinationPoint(centerLat, centerLon, bearingRad, targetDist);
    let penalty = 0;
    for (const probe of probes) {
      const distToCandidate = haversine(probe.lat, probe.lon, candidate.lat, candidate.lon);
      if (probe.isHit) {
        penalty += Math.abs(distToCandidate - probe.distanceMetres);
      } else {
        const maxRadius = 40225;
        if (distToCandidate < maxRadius - 1000) {
          penalty += (maxRadius - 1000 - distToCandidate) * 5;
        }
      }
    }
    if (penalty < minPenalty) {
      minPenalty = penalty;
      bestLat = candidate.lat;
      bestLon = candidate.lon;
    }
  }
  const radiusMetres = Math.max(200, Math.min(1e4, minPenalty));
  return { lat: bestLat, lon: bestLon, radiusMetres };
}
function ensureLeafletCss() {
  if (document.querySelector("#recon-leaflet-css"))
    return;
  try {
    const gmGet = typeof GM_getResourceText !== "undefined" ? GM_getResourceText : win.GM_getResourceText;
    const gmAdd = typeof GM_addStyle !== "undefined" ? GM_addStyle : win.GM_addStyle;
    if (gmGet && gmAdd) {
      const css = gmGet("LEAFLET_CSS");
      if (css) {
        const style = document.createElement("style");
        style.id = "recon-leaflet-css";
        style.textContent = css;
        document.head.appendChild(style);
        return;
      }
    }
  } catch (_) {}
  const link = document.createElement("link");
  link.id = "recon-leaflet-css";
  link.rel = "stylesheet";
  link.href = "https://unpkg.com/[email protected]/dist/leaflet.css";
  document.head.appendChild(link);
}
function waitForLeaflet(timeoutMs = 8000) {
  const check = () => typeof L !== "undefined" && L.map ? L : win.L && win.L.map ? win.L : null;
  const immediate = check();
  if (immediate)
    return Promise.resolve(immediate);
  return new Promise((resolve, reject) => {
    const start = Date.now();
    const poll = setInterval(() => {
      const leaflet = check();
      if (leaflet) {
        clearInterval(poll);
        resolve(leaflet);
      } else if (Date.now() - start > timeoutMs) {
        clearInterval(poll);
        reject(new Error("Leaflet not available — please reinstall the script to refresh @require cache."));
      }
    }, 150);
  });
}
function renderDualMap(container, L2, primary, secondary) {
  ensureLeafletCss();
  if (container._leafletMap) {
    try {
      container._leafletMap.remove();
    } catch (_) {}
  }
  container.innerHTML = "";
  container.style.cssText = `
        width: 100%;
        height: 360px;
        border-radius: 12px;
        overflow: hidden;
        margin-top: 16px;
        position: relative;
        box-shadow: 0 4px 24px rgba(0,0,0,0.5);
        border: 1px solid rgba(255,255,255,0.08);
    `;
  const MAP_HEIGHT = 360;
  const mapDiv = document.createElement("div");
  mapDiv.style.cssText = `width:100%;height:${MAP_HEIGHT}px;`;
  container.appendChild(mapDiv);
  const zoomForRadius = (r) => {
    if (r < 300)
      return 15;
    if (r < 800)
      return 14;
    if (r < 2000)
      return 13;
    if (r < 5000)
      return 12;
    if (r < 15000)
      return 11;
    if (r < 40000)
      return 10;
    return 8;
  };
  const map = L2.map(mapDiv, {
    zoomControl: true,
    attributionControl: true,
    preferCanvas: false
  });
  container._leafletMap = map;
  const CspTileLayer = L2.TileLayer.extend({
    createTile: function(coords, done) {
      const tile = document.createElement("img");
      tile.alt = "";
      tile.setAttribute("role", "presentation");
      const url = this.getTileUrl(coords);
      const gmXhr = typeof GM_xmlhttpRequest !== "undefined" ? GM_xmlhttpRequest : win.GM_xmlhttpRequest;
      if (gmXhr) {
        gmXhr({
          method: "GET",
          url,
          responseType: "blob",
          onload: function(response) {
            if (response.status === 200 && response.response) {
              const blobUrl = URL.createObjectURL(response.response);
              tile.onload = () => {
                done(null, tile);
                URL.revokeObjectURL(blobUrl);
              };
              tile.onerror = () => {
                done(new Error("Tile failed"), tile);
                URL.revokeObjectURL(blobUrl);
              };
              tile.src = blobUrl;
            } else {
              tile.src = url;
              L2.DomEvent.on(tile, "load", L2.Util.bind(this._tileOnLoad, this, done, tile));
              L2.DomEvent.on(tile, "error", L2.Util.bind(this._tileOnError, this, done, tile));
            }
          }.bind(this),
          onerror: function() {
            tile.src = url;
            L2.DomEvent.on(tile, "load", L2.Util.bind(this._tileOnLoad, this, done, tile));
            L2.DomEvent.on(tile, "error", L2.Util.bind(this._tileOnError, this, done, tile));
          }.bind(this)
        });
      } else {
        tile.src = url;
        L2.DomEvent.on(tile, "load", L2.Util.bind(this._tileOnLoad, this, done, tile));
        L2.DomEvent.on(tile, "error", L2.Util.bind(this._tileOnError, this, done, tile));
      }
      return tile;
    }
  });
  new CspTileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", {
    attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/">CARTO</a>',
    subdomains: "abcd",
    maxZoom: 19
  }).addTo(map);
  const addLayer = (layer) => {
    if (!layer.pinOnly) {
      L2.circle([layer.lat, layer.lon], {
        radius: layer.radiusMetres,
        color: layer.color,
        fillColor: layer.color,
        fillOpacity: layer.ringOnly ? 0.06 : 0.18,
        weight: 2,
        opacity: layer.ringOnly ? 0.5 : 0.8,
        dashArray: layer.ringOnly ? "6 4" : undefined
      }).addTo(map);
    }
    const size = layer.ringOnly && !layer.pinOnly ? 10 : 14;
    const icon = L2.divIcon({
      className: "",
      html: `<div style="width:${size}px;height:${size}px;background:${layer.color};border-radius:50%;border:2px solid #fff;box-shadow:0 0 8px ${layer.color}bb;"></div>`,
      iconSize: [size, size],
      iconAnchor: [size / 2, size / 2]
    });
    L2.marker([layer.lat, layer.lon], { icon }).addTo(map);
  };
  if (secondary)
    addLayer(secondary);
  addLayer(primary);
  let lineDistanceMetres = null;
  if (secondary) {
    lineDistanceMetres = haversine(primary.lat, primary.lon, secondary.lat, secondary.lon);
    L2.polyline([[primary.lat, primary.lon], [secondary.lat, secondary.lon]], { color: "#ffffff", weight: 1.5, opacity: 0.6, dashArray: "5 6" }).addTo(map);
    const midLat = (primary.lat + secondary.lat) / 2;
    const midLon = (primary.lon + secondary.lon) / 2;
    const distLabel = lineDistanceMetres >= 1000 ? `${(lineDistanceMetres / 1000).toFixed(2)} km` : `${Math.round(lineDistanceMetres)} m`;
    const labelIcon = L2.divIcon({
      className: "",
      html: `<div style="background:rgba(14,14,20,0.88);color:#fff;font-size:10px;font-family:system-ui,sans-serif;font-weight:600;padding:2px 7px;border-radius:6px;border:1px solid rgba(255,255,255,0.2);white-space:nowrap;pointer-events:none;">${distLabel}</div>`,
      iconAnchor: [30, 10],
      iconSize: [60, 20]
    });
    L2.marker([midLat, midLon], { icon: labelIcon, interactive: false }).addTo(map);
  }
  const boundsFromLayer = (layer) => {
    const R = 6371000;
    const r = Math.max(layer.radiusMetres, 200);
    const dLat = r / R * (180 / Math.PI);
    const dLon = r / (R * Math.cos(layer.lat * Math.PI / 180)) * (180 / Math.PI);
    return L2.latLngBounds([layer.lat - dLat, layer.lon - dLon], [layer.lat + dLat, layer.lon + dLon]);
  };
  let bounds = boundsFromLayer(primary);
  if (secondary)
    bounds = bounds.extend(boundsFromLayer(secondary));
  const legendRows = [
    `<div style="display:flex;align-items:center;gap:6px;">
            <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${primary.color};"></span>
            <span style="color:${primary.color};font-weight:700;">${primary.label}</span>
         </div>
         <div style="color:#aaa;font-size:11px;margin-left:16px;">${primary.subtitle}</div>`
  ];
  if (secondary) {
    legendRows.push(`<div style="display:flex;align-items:center;gap:6px;margin-top:6px;">
                <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${secondary.color};border:2px solid rgba(255,255,255,0.4);"></span>
                <span style="color:${secondary.color};font-weight:700;">${secondary.label}</span>
             </div>
             <div style="color:#aaa;font-size:11px;margin-left:16px;">${secondary.subtitle}</div>`);
  }
  if (lineDistanceMetres !== null) {
    const distLabel = lineDistanceMetres >= 1000 ? `${(lineDistanceMetres / 1000).toFixed(2)} km` : `${Math.round(lineDistanceMetres)} m`;
    legendRows.push(`<div style="margin-top:8px;padding-top:7px;border-top:1px solid rgba(255,255,255,0.12);display:flex;align-items:center;gap:6px;">
                <span style="color:#fff;font-size:11px;">\uD83D\uDCCF Distance between pins:</span>
                <span style="color:#fff;font-weight:700;font-size:12px;">${distLabel}</span>
             </div>`);
  }
  const overlay = document.createElement("div");
  overlay.style.cssText = `
        position: absolute;
        top: 10px; left: 10px;
        z-index: 1000;
        background: rgba(14,14,20,0.93);
        backdrop-filter: blur(10px);
        color: #fff;
        font-family: system-ui, sans-serif;
        font-size: 12px;
        padding: 10px 14px;
        border-radius: 10px;
        border: 1px solid rgba(255,255,255,0.12);
        pointer-events: none;
        line-height: 1.6;
        min-width: 180px;
    `;
  overlay.innerHTML = legendRows.join("");
  container.appendChild(overlay);
  const doSetView = () => {
    map.invalidateSize({ animate: false });
    map.fitBounds(bounds, { padding: [24, 24], animate: false, maxZoom: zoomForRadius(primary.radiusMetres) });
  };
  setTimeout(doSetView, 50);
  setTimeout(doSetView, 300);
}
function renderLocateMap(container, L2, lat, lon, radiusMetres, label = "\uD83D\uDCCD Estimated Location", subtitle = `±${Math.round(radiusMetres)}m accuracy`, color = "#e32222") {
  renderDualMap(container, L2, { lat, lon, radiusMetres, color, label, subtitle });
}
function injectDistanceMap() {
  if (!win.location.pathname.match(/\/profiles\/[^/]+/i))
    return;
  if (document.getElementById("recon-locate-map"))
    return;
  const anchor = document.querySelector("t101-profile-extra-info") || document.querySelector(".right-wrapper > t101-profile-extra-info") || document.querySelector(".laptop-background") || document.querySelector(".right-wrapper");
  if (!anchor)
    return;
  const distMetres = getProfileDistanceMetres();
  if (!distMetres || distMetres <= 0)
    return;
  const mapContainer = document.createElement("div");
  mapContainer.id = "recon-locate-map";
  mapContainer.style.cssText = `
        width: 100%;
        margin-top: 20px;
        border-radius: 12px;
        overflow: hidden;
        position: relative;
        box-shadow: 0 4px 24px rgba(0,0,0,0.5);
        border: 1px solid rgba(255,255,255,0.08);
    `;
  anchor.after(mapContainer);
  Promise.all([waitForLeaflet(), getUserPosition()]).then(([L2, pos]) => {
    renderLocateMap(mapContainer, L2, pos.lat, pos.lon, distMetres, "\uD83D\uDCCD Approximate Area", `Profile is within ${Math.round(distMetres)}m of your position`, "#3b82f6");
  }).catch((err) => {
    console.warn("[Recon+ Map] Could not render distance map:", err?.message || err);
    mapContainer.remove();
  });
}
function injectLocateButton() {
  injectDistanceMap();
  if (!win.location.pathname.match(/\/profiles\/[^/]+/i))
    return;
  if (document.getElementById("recon-locate-btn"))
    return;
  const exportBtn = document.getElementById("custom-export-profile-btn");
  if (!exportBtn)
    return;
  const btn = document.createElement("button");
  btn.id = "recon-locate-btn";
  btn.textContent = "\uD83D\uDD3A Triangulate";
  btn.title = "Probe from multiple points to narrow down the actual location";
  btn.style.cssText = `background-color: #1a2a1a; color: #4ade80; border: 1px solid #4ade8055; padding: 8px 16px; margin-left: 8px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s;`;
  btn.onmouseover = () => btn.style.backgroundColor = "#243324";
  btn.onmouseout = () => btn.style.backgroundColor = "#1a2a1a";
  exportBtn.parentNode.insertBefore(btn, exportBtn.nextSibling);
  btn.addEventListener("click", async () => {
    btn.disabled = true;
    btn.textContent = "⏳ Triangulating…";
    const setStatus = (msg) => {
      btn.textContent = `⏳ ${msg}`;
    };
    try {
      const [L2, pos] = await Promise.all([waitForLeaflet(), getUserPosition()]);
      const targetId = getTargetProfileId();
      const myId = getMyProfileId();
      if (!targetId) {
        showToast("Could not determine target profile ID.", "error");
        btn.disabled = false;
        btn.textContent = "\uD83D\uDD3A Triangulate";
        return;
      }
      if (!myId) {
        showToast("Could not determine your profile ID. Make sure you are logged in.", "error");
        btn.disabled = false;
        btn.textContent = "\uD83D\uDD3A Triangulate";
        return;
      }
      const result = await triangulate(targetId, myId, pos.lat, pos.lon, setStatus);
      const dist = getProfileDistanceMetres();
      document.getElementById("recon-locate-map")?.remove();
      const mapContainer = document.createElement("div");
      mapContainer.id = "recon-locate-map";
      mapContainer.style.cssText = `
                width: 100%;
                margin-top: 20px;
                border-radius: 12px;
                overflow: hidden;
                position: relative;
                box-shadow: 0 4px 24px rgba(0,0,0,0.5);
                border: 1px solid rgba(255,255,255,0.08);
            `;
      const anchor = document.querySelector("t101-profile-extra-info") || document.querySelector(".right-wrapper > t101-profile-extra-info") || document.querySelector(".laptop-background") || document.querySelector(".right-wrapper");
      if (anchor)
        anchor.after(mapContainer);
      if (!dist) {
        showToast("Distance not found on page — cannot plot location.", "error");
        mapContainer.remove();
      } else if (result) {
        const bearing = bearingTo(pos.lat, pos.lon, result.lat, result.lon);
        const targetPoint = destinationPoint(pos.lat, pos.lon, bearing, dist);
        const userLayer = {
          lat: pos.lat,
          lon: pos.lon,
          radiusMetres: dist,
          color: "#3b82f6",
          label: "\uD83D\uDCCD Your Position",
          subtitle: `~${Math.round(dist)}m reported distance`,
          ringOnly: true
        };
        const targetLayer = {
          lat: targetPoint.lat,
          lon: targetPoint.lon,
          radiusMetres: 0,
          color: "#f97316",
          label: "\uD83D\uDCCC Estimated Location",
          subtitle: `On ring edge · bearing ${Math.round(radToDeg(bearing + 2 * Math.PI) % 360)}°`,
          pinOnly: true
        };
        renderDualMap(mapContainer, L2, targetLayer, userLayer);
        showToast(`Location estimated on ring edge — ${Math.round(dist)}m from you`, "success");
      } else {
        renderDualMap(mapContainer, L2, {
          lat: pos.lat,
          lon: pos.lon,
          radiusMetres: dist,
          color: "#f59e0b",
          label: "\uD83D\uDCCD Distance Ring",
          subtitle: `Profile within ~${Math.round(dist)}m`,
          ringOnly: false
        });
        showToast("Triangulation inconclusive — showing distance ring only.", "warning");
      }
      mapContainer.scrollIntoView({ behavior: "smooth", block: "nearest" });
    } catch (e) {
      showToast(`Error: ${e?.message}`, "error");
    }
    btn.disabled = false;
    btn.textContent = "\uD83D\uDD3A Triangulate";
  });
}

// src/lightbox.ts
function setupPhotoViewer() {
  const tiles = document.querySelectorAll(".photo-tile:not([data-lightbox-ready])");
  tiles.forEach((tile) => {
    const htmlTile = tile;
    htmlTile.setAttribute("data-lightbox-ready", "true");
    htmlTile.style.pointerEvents = "auto";
    htmlTile.style.cursor = "pointer";
    htmlTile.addEventListener("click", (e) => {
      e.preventDefault();
      e.stopPropagation();
      const container = htmlTile.closest(".member-photos, t101-profile-photos, .gallery-grid, .photo-grid") || document.body;
      const allTiles = Array.from(container.querySelectorAll(".photo-tile"));
      const urls = allTiles.map((t) => {
        const htmlEl = t;
        const bgImg2 = htmlEl.style.backgroundImage;
        return bgImg2 ? bgImg2.replace(/^url\(["']?/, "").replace(/["']?\)$/, "").split("?")[0] : null;
      }).filter((b) => Boolean(b));
      const bgImg = htmlTile.style.backgroundImage;
      if (!bgImg)
        return;
      let currentUrl = bgImg.replace(/^url\(["']?/, "").replace(/["']?\)$/, "").split("?")[0];
      let currentIndex = urls.indexOf(currentUrl);
      if (currentIndex === -1) {
        currentIndex = 0;
        urls.unshift(currentUrl);
      }
      createFullscreenLightbox(urls, currentIndex);
    });
  });
}
function createFullscreenLightbox(urls, startIndex) {
  if (document.getElementById("custom-photo-lightbox"))
    return;
  document.documentElement.style.setProperty("overflow", "hidden", "important");
  document.body.style.setProperty("overflow", "hidden", "important");
  let currentIndex = startIndex;
  const overlay = document.createElement("div");
  overlay.id = "custom-photo-lightbox";
  overlay.style.cssText = `position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.95); z-index: 100000; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.25s ease;`;
  const img = document.createElement("img");
  img.src = urls[currentIndex];
  img.style.cssText = `max-width: 90%; max-height: 90%; object-fit: contain; box-shadow: 0 0 20px rgba(0,0,0,0.8); border-radius: 4px; user-select: none; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);`;
  const closeBtn = document.createElement("div");
  closeBtn.innerHTML = "&#x2715;";
  closeBtn.style.cssText = `position: absolute; top: 20px; right: 25px; color: #ffffff; font-size: 35px; cursor: pointer; user-select: none; z-index: 100002; transition: transform 0.1s;`;
  function updateImage(index) {
    currentIndex = (index + urls.length) % urls.length;
    img.style.transform = "scale(0.97)";
    img.style.opacity = "0";
    setTimeout(() => {
      img.src = urls[currentIndex];
      img.onload = () => {
        img.style.transform = "scale(1)";
        img.style.opacity = "1";
      };
    }, 150);
  }
  if (urls.length > 1) {
    const leftBtn = document.createElement("div");
    leftBtn.innerHTML = "&#x276E;";
    leftBtn.style.cssText = `position: absolute; left: 30px; top: 50%; transform: translateY(-50%); width: 55px; height: 55px; background: rgba(255,255,255,0.08); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #fff; font-size: 24px; cursor: pointer; user-select: none; transition: all 0.25s ease; border: 1px solid rgba(255,255,255,0.15); z-index: 100001;`;
    leftBtn.onmouseover = () => {
      leftBtn.style.background = "rgba(255,255,255,0.2)";
      leftBtn.style.transform = "translateY(-50%) scale(1.08)";
    };
    leftBtn.onmouseout = () => {
      leftBtn.style.background = "rgba(255,255,255,0.08)";
      leftBtn.style.transform = "translateY(-50%) scale(1)";
    };
    leftBtn.addEventListener("click", (e) => {
      e.stopPropagation();
      updateImage(currentIndex - 1);
    });
    const rightBtn = document.createElement("div");
    rightBtn.innerHTML = "&#x276F;";
    rightBtn.style.cssText = `position: absolute; right: 30px; top: 50%; transform: translateY(-50%); width: 55px; height: 55px; background: rgba(255,255,255,0.08); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #fff; font-size: 24px; cursor: pointer; user-select: none; transition: all 0.25s ease; border: 1px solid rgba(255,255,255,0.15); z-index: 100001;`;
    rightBtn.onmouseover = () => {
      rightBtn.style.background = "rgba(255,255,255,0.2)";
      rightBtn.style.transform = "translateY(-50%) scale(1.08)";
    };
    rightBtn.onmouseout = () => {
      rightBtn.style.background = "rgba(255,255,255,0.08)";
      rightBtn.style.transform = "translateY(-50%) scale(1)";
    };
    rightBtn.addEventListener("click", (e) => {
      e.stopPropagation();
      updateImage(currentIndex + 1);
    });
    overlay.appendChild(leftBtn);
    overlay.appendChild(rightBtn);
  }
  const handleKeyDown = (e) => {
    if (e.key === "ArrowLeft" && urls.length > 1) {
      updateImage(currentIndex - 1);
    } else if (e.key === "ArrowRight" && urls.length > 1) {
      updateImage(currentIndex + 1);
    } else if (e.key === "Escape") {
      closeLightbox();
    }
  };
  const closeLightbox = () => {
    win.removeEventListener("keydown", handleKeyDown);
    overlay.style.opacity = "0";
    setTimeout(() => {
      overlay.remove();
      document.documentElement.style.removeProperty("overflow");
      document.body.style.removeProperty("overflow");
    }, 250);
  };
  win.addEventListener("keydown", handleKeyDown);
  closeBtn.addEventListener("click", closeLightbox);
  overlay.addEventListener("click", (e) => {
    if (e.target === overlay)
      closeLightbox();
  });
  overlay.appendChild(img);
  overlay.appendChild(closeBtn);
  document.body.appendChild(overlay);
  setTimeout(() => overlay.style.opacity = "1", 10);
}

// src/dom.ts
function paintProfileData(profileId) {
  const card = document.getElementById(profileId);
  if (!card || card.dataset.reconInjected)
    return;
  const data = profileDetailsCache.get(profileId);
  const textContainer = card.querySelector(".text-container");
  if (!data || !textContainer)
    return;
  card.dataset.reconInjected = "true";
  if (data.location && data.location.locationId) {
    let locDiv = document.createElement("div");
    locDiv.className = "custom-injected-location";
    locDiv.style.cssText = `font-family: 'Raleway', sans-serif; font-size: 13px; font-weight: 700; color: #e32222; margin: 4px 0; text-align: left;`;
    locDiv.innerHTML = `\uD83D\uDCCD Loading...`;
    const nameNode = textContainer.querySelector(".name");
    if (nameNode)
      nameNode.after(locDiv);
    else
      textContainer.prepend(locDiv);
    const rawLocId = data.location.locationId;
    if (isNaN(Number(rawLocId))) {
      locDiv.innerHTML = `\uD83D\uDCCD ${rawLocId}`;
    } else {
      getLocationFromIndexedDB(rawLocId).then((cachedLoc) => {
        if (cachedLoc) {
          const locName = cachedLoc.name || cachedLoc.longName || rawLocId;
          locDiv.innerHTML = `\uD83D\uDCCD ${locName}`;
        } else {
          let fetchUrl = data.location.locationUrl || `https://www.recon.com/api/location/locations/${rawLocId}`;
          if (!fetchUrl.includes("culture=")) {
            fetchUrl += (fetchUrl.includes("?") ? "&" : "?") + "culture=en";
          }
          win.fetch(fetchUrl, {
            headers: {
              ...state.authHeaders
            }
          }).then((res) => res.json()).then((locData) => {
            const name = locData.shortLocationName || locData.name || locData.longLocationName;
            if (name) {
              const longName = locData.longLocationName || locData.longName || name;
              locDiv.innerHTML = `\uD83D\uDCCD ${name}`;
              saveLocationToIndexedDB(rawLocId, fetchUrl, name, longName);
            } else {
              locDiv.innerHTML = `\uD83D\uDCCD Loc ID: ${rawLocId}`;
            }
          }).catch(() => {
            locDiv.innerHTML = `\uD83D\uDCCD Loc ID: ${rawLocId}`;
          });
        }
      });
    }
  }
  if (data.interests && data.interests.length > 0) {
    let intDiv = document.createElement("div");
    intDiv.className = "custom-injected-interests";
    intDiv.style.cssText = `display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; justify-content: flex-start;`;
    data.interests.forEach((item) => {
      let id = item;
      let text = "";
      if (item && typeof item === "object") {
        id = item.interestId || item.id || item.interest;
        text = item.name || item.text || item.label;
      }
      let span = document.createElement("span");
      span.style.cssText = `background-color: rgba(255, 255, 255, 0.2); color: #fff; padding: 3px 8px; border-radius: 10px; font-size: 11px; font-weight: bold;`;
      span.textContent = text || interestMap[id] || `Tag: ${id}`;
      intDiv.appendChild(span);
    });
    textContainer.appendChild(intDiv);
  }
}
function paintProfileDetailPage(data) {
  if (!data || !data.lastUpdatedDate)
    return;
  let container = document.querySelector(".profile-header, t101-profile-header, .member-info, .profile-info-container");
  if (!container) {
    container = document.querySelector("h1");
  }
  if (!container) {
    const cruiseBtn = document.querySelector('a[href*="/cruise"], button:has(.fa-ship), [class*="cruise"], button:has(t101-icon-cruise), button:has(t101-icon-cruise-icon), t101-icon-cruise, t101-icon-cruise-icon, button:has([class*="cruise"]), button:has([id*="cruise"])');
    if (cruiseBtn) {
      container = cruiseBtn.parentElement;
    }
  }
  if (!container)
    return;
  if (document.getElementById("custom-profile-last-updated"))
    return;
  const dateDiv = document.createElement("div");
  dateDiv.id = "custom-profile-last-updated";
  dateDiv.style.cssText = `
        font-family: 'Raleway', sans-serif;
        font-size: 14px;
        font-weight: 700;
        color: #adadad;
        margin: 10px 0;
        display: inline-flex;
        align-items: center;
        gap: 6px;
    `;
  try {
    const dateObj = new Date(data.lastUpdatedDate);
    const formattedDate = dateObj.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
    dateDiv.innerHTML = `\uD83D\uDCC5 Profile Updated: <span style="color: #fff; font-weight: 800;">${formattedDate}</span>`;
  } catch (e) {
    dateDiv.innerHTML = `\uD83D\uDCC5 Profile Updated: <span style="color: #fff; font-weight: 800;">${data.lastUpdatedDate}</span>`;
  }
  if (container.tagName === "H1") {
    container.after(dateDiv);
  } else {
    container.appendChild(dateDiv);
  }
}
function injectViewedButton() {
  const cruiseBtn = document.querySelector('a[href*="/cruise"], button:has(.fa-ship), [class*="cruise"], button:has(t101-icon-cruise), button:has(t101-icon-cruise-icon), t101-icon-cruise, t101-icon-cruise-icon, button:has([class*="cruise"]), button:has([id*="cruise"])');
  if (cruiseBtn) {
    const targetEl = cruiseBtn.closest("button") || cruiseBtn.closest("a") || cruiseBtn;
    let viewBtn = document.getElementById("manual-view-btn");
    if (!viewBtn) {
      viewBtn = document.createElement("button");
      viewBtn.id = "manual-view-btn";
      viewBtn.textContent = "Show As Viewed";
      viewBtn.style.cssText = `background-color: #2b2b2b; color: #ffffff; border: 1px solid #444; padding: 8px 16px; margin-left: 8px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s;`;
      viewBtn.onmouseover = () => viewBtn.style.backgroundColor = "#3d3d3d";
      viewBtn.onmouseout = () => viewBtn.style.backgroundColor = "#2b2b2b";
      viewBtn.addEventListener("click", () => {
        if (!state.lastBlockedRequest)
          return showToast("No profile visit tracker captured yet.", "error");
        const xhr = new origXHR;
        xhr._isManualTrigger = true;
        xhr.open(state.lastBlockedRequest.method, state.lastBlockedRequest.url);
        if (state.lastBlockedRequest.headers) {
          for (const [key, value] of Object.entries(state.lastBlockedRequest.headers)) {
            xhr.setRequestHeader(key, value);
          }
        }
        xhr.onload = function() {
          if (this.status >= 200 && this.status < 300) {
            showToast("Profile marked as viewed successfully!");
          } else {
            let errorMsg = `Failed to contact server: Status ${this.status}`;
            try {
              const errData = JSON.parse(this.responseText);
              if (errData && errData.title) {
                errorMsg = errData.title;
              }
            } catch (e) {}
            showToast(errorMsg, "error");
          }
        };
        xhr.onerror = () => showToast("Failed to contact server.", "error");
        xhr.send(state.lastBlockedRequest.body);
      });
      targetEl.parentNode.insertBefore(viewBtn, targetEl.nextSibling);
    }
    if (!document.getElementById("custom-export-profile-btn")) {
      const exportBtn = document.createElement("button");
      exportBtn.id = "custom-export-profile-btn";
      exportBtn.textContent = "Export Profile";
      exportBtn.style.cssText = `background-color: #2b2b2b; color: #ffffff; border: 1px solid #444; padding: 8px 16px; margin-left: 8px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s;`;
      exportBtn.onmouseover = () => exportBtn.style.backgroundColor = "#3d3d3d";
      exportBtn.onmouseout = () => exportBtn.style.backgroundColor = "#2b2b2b";
      exportBtn.addEventListener("click", exportProfileData);
      viewBtn.parentNode.insertBefore(exportBtn, viewBtn.nextSibling);
    }
    injectLocateButton();
  }
}
function paintPremiumGallery(profileId) {
  const photos = profileMediaMap.get(profileId);
  if (!photos || photos.length === 0)
    return;
  if (document.getElementById("custom-premium-gallery"))
    return;
  let feed = document.querySelector("t101-profile-member-feed, .newsfeed");
  let photoGrid = document.querySelector("t101-profile-latest-photos-panel, .latest-photos-block, .photo-tile-container, t101-profile-photos, .member-photos, .gallery-grid, .photo-grid");
  let parent = feed ? feed.parentElement : photoGrid ? photoGrid.parentElement : null;
  if (!parent) {
    parent = document.querySelector(".profile-header, t101-profile-header, .member-info, .profile-info-container, t101-profile-main, .profile-main-container");
  }
  if (!parent)
    return;
  const galleryDiv = document.createElement("div");
  galleryDiv.id = "custom-premium-gallery";
  galleryDiv.style.cssText = `
        margin: 24px 0;
        padding: 20px;
        background: rgba(255, 255, 255, 0.03);
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
        border: 1px solid rgba(255, 255, 255, 0.08);
        border-radius: 12px;
        font-family: 'Raleway', sans-serif;
        clear: both;
    `;
  const titleDiv = document.createElement("div");
  titleDiv.style.cssText = `
        display: flex;
        align-items: center;
        gap: 8px;
        margin-bottom: 16px;
    `;
  titleDiv.innerHTML = `
        <span style="font-size: 20px;">\uD83D\uDC51</span>
        <h3 style="margin: 0; font-family: 'Barlow', sans-serif; font-size: 20px; font-weight: 800; background: linear-gradient(90deg, #ff3333, #ff7b00); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-transform: uppercase; letter-spacing: 0.5px;">Recon+ Premium Photos</h3>
        <span style="background: rgba(255, 51, 51, 0.15); border: 1px solid rgba(255, 51, 51, 0.3); color: #ff3333; font-size: 10px; font-weight: 800; padding: 2px 6px; border-radius: 4px; text-transform: uppercase; margin-left: auto;">Unlocked</span>
    `;
  galleryDiv.appendChild(titleDiv);
  const grid = document.createElement("div");
  grid.style.cssText = `
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
        gap: 12px;
        margin-top: 16px;
    `;
  photos.forEach((photoUrl, index) => {
    let previewUrl = photoUrl;
    if (previewUrl.includes("size=")) {
      previewUrl = previewUrl.replace(/size=\d+/, "size=102");
    } else {
      previewUrl += (previewUrl.includes("?") ? "&" : "?") + "size=102";
    }
    if (!previewUrl.includes("deviceTypeId=")) {
      previewUrl += "&deviceTypeId=1";
    }
    const tile = document.createElement("div");
    tile.className = "photo-tile";
    tile.style.cssText = `
            aspect-ratio: 1;
            background-image: url('${previewUrl}');
            background-size: cover;
            background-position: center;
            border-radius: 8px;
            cursor: pointer;
            border: 1px solid rgba(255, 255, 255, 0.05);
            transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s, border-color 0.25s;
        `;
    tile.onmouseover = () => {
      tile.style.transform = "scale(1.05)";
      tile.style.boxShadow = "0 8px 16px rgba(0, 0, 0, 0.4)";
      tile.style.borderColor = "rgba(255, 255, 255, 0.2)";
    };
    tile.onmouseout = () => {
      tile.style.transform = "scale(1)";
      tile.style.boxShadow = "none";
      tile.style.borderColor = "rgba(255, 255, 255, 0.05)";
    };
    tile.addEventListener("click", (e) => {
      e.preventDefault();
      e.stopPropagation();
      const highResUrls = photos.map((u) => u.split("?")[0]);
      createFullscreenLightbox(highResUrls, index);
    });
    grid.appendChild(tile);
  });
  galleryDiv.appendChild(grid);
  if (feed) {
    feed.parentNode.insertBefore(galleryDiv, feed);
  } else if (photoGrid) {
    photoGrid.after(galleryDiv);
  } else {
    parent.appendChild(galleryDiv);
  }
}

// src/db.ts
function getLocationFromIndexedDB(locationId) {
  return new Promise((resolve) => {
    try {
      const request = win.indexedDB.open("RECON_DB");
      request.onsuccess = function(event) {
        const db = event.target.result;
        if (!db.objectStoreNames.contains("App_Locations")) {
          db.close();
          resolve(null);
          return;
        }
        const transaction = db.transaction("App_Locations", "readonly");
        const store = transaction.objectStore("App_Locations");
        function checkVal(val) {
          if (val && val.data) {
            if (val.expiry) {
              const expiryDate = new Date(val.expiry);
              if (expiryDate > new Date) {
                return val.data;
              }
            } else {
              return val.data;
            }
          }
          return null;
        }
        const getRequest = store.get(Number(locationId));
        getRequest.onsuccess = function() {
          let result = checkVal(getRequest.result);
          if (result) {
            db.close();
            resolve(result);
          } else {
            const getRequestStr = store.get(String(locationId));
            getRequestStr.onsuccess = function() {
              db.close();
              resolve(checkVal(getRequestStr.result));
            };
            getRequestStr.onerror = function() {
              db.close();
              resolve(null);
            };
          }
        };
        getRequest.onerror = function() {
          db.close();
          resolve(null);
        };
      };
      request.onerror = function() {
        resolve(null);
      };
    } catch (e) {
      resolve(null);
    }
  });
}
function saveLocationToIndexedDB(locationId, locationUrl, name, longName) {
  try {
    const request = win.indexedDB.open("RECON_DB");
    request.onsuccess = function(event) {
      const db = event.target.result;
      if (!db.objectStoreNames.contains("App_Locations")) {
        db.close();
        return;
      }
      const transaction = db.transaction("App_Locations", "readwrite");
      const store = transaction.objectStore("App_Locations");
      const expiryDate = new Date;
      expiryDate.setFullYear(expiryDate.getFullYear() + 1);
      const value = {
        data: {
          locationId: Number(locationId),
          locationUrl,
          longName,
          name
        },
        expiry: expiryDate.toString()
      };
      store.put(value, Number(locationId));
      transaction.oncomplete = function() {
        db.close();
      };
    };
  } catch (e) {
    console.error("[Script] Error saving location to IndexedDB", e);
  }
}
function getFromStore(storeName, profileId) {
  return new Promise((resolve) => {
    try {
      const request = win.indexedDB.open("RECON_DB");
      request.onsuccess = function(event) {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(storeName)) {
          db.close();
          resolve(null);
          return;
        }
        const transaction = db.transaction(storeName, "readonly");
        const store = transaction.objectStore(storeName);
        function checkVal(val) {
          if (val) {
            if (val.data) {
              if (val.expiry) {
                const expiryDate = new Date(val.expiry);
                if (expiryDate > new Date) {
                  return val.data;
                }
              } else {
                return val.data;
              }
            }
            return val;
          }
          return null;
        }
        const dbKey = /^\d+$/.test(String(profileId)) ? Number(profileId) : String(profileId);
        const getRequest = store.get(dbKey);
        getRequest.onsuccess = function() {
          let result = checkVal(getRequest.result);
          if (result) {
            db.close();
            resolve(result);
          } else {
            const fallbackKey = typeof dbKey === "number" ? String(dbKey) : isNaN(Number(dbKey)) ? null : Number(dbKey);
            if (fallbackKey !== null) {
              const getRequestFallback = store.get(fallbackKey);
              getRequestFallback.onsuccess = function() {
                db.close();
                resolve(checkVal(getRequestFallback.result));
              };
              getRequestFallback.onerror = function() {
                db.close();
                resolve(null);
              };
            } else {
              db.close();
              resolve(null);
            }
          }
        };
        getRequest.onerror = function() {
          db.close();
          resolve(null);
        };
      };
      request.onerror = function() {
        resolve(null);
      };
    } catch (e) {
      resolve(null);
    }
  });
}
async function getProfileFromIndexedDB(profileId) {
  try {
    const [profileCache, profileDetail] = await Promise.all([
      getFromStore("Profile_Cache", profileId),
      getFromStore("Profile_Detail_Cache", profileId)
    ]);
    if (!profileCache && !profileDetail) {
      return null;
    }
    return {
      ...profileCache || {},
      ...profileDetail || {}
    };
  } catch (e) {
    console.error("[Script] Error getting profile from IndexedDB", e);
    return null;
  }
}
function saveProfileToIndexedDB(profileId, data) {
  try {
    const request = win.indexedDB.open("RECON_DB");
    request.onsuccess = function(event) {
      const db = event.target.result;
      if (!db.objectStoreNames.contains("Profile_Cache")) {
        db.close();
        return;
      }
      const transaction = db.transaction("Profile_Cache", "readwrite");
      const store = transaction.objectStore("Profile_Cache");
      const dbKey = /^\d+$/.test(String(profileId)) ? Number(profileId) : String(profileId);
      const getRequest = store.get(dbKey);
      getRequest.onsuccess = function() {
        let existingData = null;
        const val = getRequest.result;
        if (val) {
          existingData = val.data ? val.data : val;
        }
        const mergedData = existingData ? { ...existingData, ...data } : data;
        const expiryDate = new Date;
        expiryDate.setFullYear(expiryDate.getFullYear() + 1);
        const value = {
          data: mergedData,
          expiry: expiryDate.toString()
        };
        store.put(value, dbKey);
      };
      transaction.oncomplete = function() {
        db.close();
      };
    };
  } catch (e) {
    console.error("[Script] Error saving profile to IndexedDB", e);
  }
}
function checkAndPaintProfileFromIndexedDB() {
  const match = win.location.pathname.match(/\/(?:members|profiles)\/([a-f0-9-]{36}|[a-zA-Z0-9_-]+)/i);
  if (match) {
    const profileId = match[1];
    const uuidOrNumRegex = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\d+)$/i;
    if (profileId && uuidOrNumRegex.test(profileId)) {
      getProfileFromIndexedDB(profileId).then((cachedProfile) => {
        if (cachedProfile) {
          state.lastProfileDetailPageData = cachedProfile;
          paintProfileDetailPage(cachedProfile);
        }
      });
    }
  }
}

// src/xhr.ts
function setupXHRInterceptor() {
  win.XMLHttpRequest = function() {
    const xhr = new origXHR;
    const origOpen = xhr.open;
    const origSend = xhr.send;
    const origSetHeader = xhr.setRequestHeader;
    xhr._headers = {};
    xhr.setRequestHeader = function(header, value) {
      if (this._headers) {
        this._headers[header] = value;
      }
      return origSetHeader.apply(this, arguments);
    };
    xhr.open = function(method, url) {
      this._customUrl = url;
      if (typeof url === "string" && url.includes("/visitedProfiles/")) {
        if (!this._isManualTrigger) {
          this.send = function(body) {
            state.lastBlockedRequest = {
              url,
              method,
              headers: this._headers || {},
              body
            };
            Object.defineProperty(this, "readyState", { value: 4 });
            Object.defineProperty(this, "status", { value: 200 });
            Object.defineProperty(this, "responseText", { value: '{"success":true}' });
            if (this.onreadystatechange)
              this.onreadystatechange();
            if (this.onload)
              this.onload();
          };
          return origOpen.apply(this, arguments);
        }
      }
      return origOpen.apply(this, arguments);
    };
    xhr.send = function(body) {
      if (this._headers) {
        const authKeys = ["authorization", "x-xsrf-token", "client-id", "x-client-id"];
        for (const key of Object.keys(this._headers)) {
          if (authKeys.includes(key.toLowerCase())) {
            state.authHeaders[key] = this._headers[key];
          }
        }
      }
      this.addEventListener("load", function() {
        if (this._customUrl) {
          if (this._customUrl.includes("/api/profileSearch/profiles")) {
            console.log(`[Recon+] Intercepted search URL: ${this._customUrl}`);
            try {
              const data = JSON.parse(this.responseText);
              console.log(`[Recon+] Search returned profiles:`, data?.data?.length || 0);
              if (data && Array.isArray(data.data)) {
                data.data.forEach((p) => {
                  if (p.profileId && p.profileUrl) {
                    const match = p.profileUrl.match(/\/profiles\/([^/]+)\/(\d+)/);
                    if (match) {
                      const uuid = match[1];
                      const ver = parseInt(match[2], 10);
                      profileVersionsMap.set(uuid, ver);
                      console.log(`[Recon+] Mapped version: ${uuid} -> ${ver}`);
                    } else {
                      console.log(`[Recon+] Could not parse version from profileUrl: ${p.profileUrl}`);
                    }
                  }
                });
              }
            } catch (e) {
              console.error("[Script] Error parsing profileSearch data", e);
            }
          } else if (this._customUrl.includes("/api/media/profiles/") && this._customUrl.includes("/fileProperties")) {
            console.log(`[Recon+] Intercepted fileProperties URL: ${this._customUrl}`);
            try {
              const data = JSON.parse(this.responseText);
              const match = this._customUrl.match(/\/api\/media\/profiles\/([^/]+)\/fileProperties/);
              if (match && match[1]) {
                const profileId = match[1];
                const urls = extractPhotoUrls(data);
                console.log(`[Recon+] Captured ${urls.length} media files for profile: ${profileId}`);
                profileMediaMap.set(profileId, urls);
                setTimeout(() => paintPremiumGallery(profileId), 50);
              }
            } catch (e) {
              console.error("[Script] Error parsing fileProperties data", e);
            }
          } else if (this._customUrl.includes("/api/profile/profiles/")) {
            console.log(`[Recon+] Intercepted profile URL: ${this._customUrl}`);
            try {
              const data = JSON.parse(this.responseText);
              if (data && data.id) {
                if (this._customUrl.includes("/detail/")) {
                  console.log(`[Recon+] Intercepted profile detail: ${data.id}, version: ${data.version}`);
                  state.lastProfileDetailPageData = data;
                  const existing = document.getElementById("custom-profile-last-updated");
                  if (existing)
                    existing.remove();
                  setTimeout(() => paintProfileDetailPage(data), 50);
                  data.isDetailed = true;
                  const cachedSummary = profileDetailsCache.get(data.id);
                  const merged = cachedSummary ? { ...cachedSummary, ...data } : data;
                  profileDetailsCache.set(data.id, merged);
                  saveProfileToIndexedDB(data.id, merged);
                } else {
                  console.log(`[Recon+] Intercepted profile summary: ${data.id}`);
                  profileDetailsCache.set(data.id, data);
                  setTimeout(() => paintProfileData(data.id), 50);
                }
              }
            } catch (e) {
              console.error("[Script] Error parsing profile data", e);
            }
          }
        }
      });
      return origSend.apply(this, arguments);
    };
    return xhr;
  };
}

// src/styles.ts
function injectStyles() {
  const style = document.createElement("style");
  style.innerHTML = `
        .cdk-overlay-container:has(t101-paywall), t101-paywall { display: none !important; opacity: 0 !important; pointer-events: none !important; }
        html.cdk-global-scrollblock, body { position: static !important; overflow-y: auto !important; overflow: auto !important; width: auto !important; }
        .photo-tile { pointer-events: auto !important; cursor: pointer !important; }
        .text-container .interests-container { display: none !important; }
        .text-container { height: auto !important; max-height: none !important; padding-bottom: 15px !important; }
    `;
  if (document.head) {
    document.head.appendChild(style);
  } else {
    const docObserver = new MutationObserver(() => {
      if (document.head) {
        document.head.appendChild(style);
        docObserver.disconnect();
      }
    });
    docObserver.observe(document.documentElement, { childList: true });
  }
}

// src/filter.ts
var filterState = {
  currentSearchKeyword: "",
  fetchQueue: [],
  activeFetches: 0,
  queueTotal: 0,
  queueCompleted: 0
};
var maxConcurrentFetches = 2;
var fetchDelayMs = 250;
function getActiveCards() {
  const uuidOrNumRegex = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\d+)$/i;
  return Array.from(document.querySelectorAll("[id]")).filter((el) => uuidOrNumRegex.test(el.id) && (el.querySelector(".text-container") !== null || el.querySelector(".photo-tile") !== null || el.classList.contains("photo-tile")));
}
function containsKeyword(obj, keyword) {
  if (!obj)
    return false;
  if (typeof obj === "string") {
    const val = obj.toLowerCase();
    if (val.startsWith("http") || val.includes("/photos/") || val.includes("/images/")) {
      return false;
    }
    const matched = val.includes(keyword);
    if (matched) {
      console.log(`[Recon+] containsKeyword: Found match in string "${val}" for keyword "${keyword}"`);
    }
    return matched;
  }
  if (typeof obj === "object") {
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        const lowerKey = key.toLowerCase();
        if (lowerKey.endsWith("id") || lowerKey.includes("url") || lowerKey.includes("image") || lowerKey.includes("photo") || lowerKey.includes("date") || lowerKey.includes("expiry") || lowerKey === "lat" || lowerKey === "lon" || lowerKey === "latitude" || lowerKey === "longitude" || lowerKey.includes("distance") || lowerKey.includes("status") || lowerKey.includes("culture")) {
          continue;
        }
        if (containsKeyword(obj[key], keyword)) {
          return true;
        }
      }
    }
  }
  return false;
}
async function applyFilterToCard(cardEl, keyword) {
  const profileId = cardEl.id;
  console.log(`[Recon+] applyFilterToCard: card=${profileId}, keyword="${keyword}"`);
  if (!keyword) {
    cardEl.style.removeProperty("display");
    cardEl.style.removeProperty("flex-grow");
    cardEl.style.removeProperty("flex-shrink");
    cardEl.style.removeProperty("max-width");
    return;
  }
  let data = profileDetailsCache.get(profileId);
  console.log(`[Recon+] Memory cache check for ${profileId}:`, data ? "Found" : "Not Found");
  if (!data) {
    data = await getProfileFromIndexedDB(profileId) || undefined;
    console.log(`[Recon+] IndexedDB cache check for ${profileId}:`, data ? "Found" : "Not Found");
    if (data) {
      profileDetailsCache.set(profileId, data);
    }
  }
  const isDetailed = data && (data.isDetailed || typeof data.longText === "string");
  console.log(`[Recon+] ${profileId} isDetailed = ${isDetailed}`);
  if (isDetailed && data) {
    const matches = containsKeyword(data, keyword);
    console.log(`[Recon+] Profile ${profileId} matches keyword "${keyword}"? ${matches}`);
    if (matches) {
      cardEl.style.removeProperty("display");
      cardEl.style.setProperty("flex-grow", "0", "important");
      cardEl.style.setProperty("flex-shrink", "0", "important");
      cardEl.style.setProperty("max-width", "220px", "important");
    } else {
      cardEl.style.setProperty("display", "none", "important");
      cardEl.style.removeProperty("flex-grow");
      cardEl.style.removeProperty("flex-shrink");
      cardEl.style.removeProperty("max-width");
    }
    updateFilterStatusUI();
  } else {
    console.log(`[Recon+] Profile ${profileId} details not available yet. Enqueueing fetch...`);
    cardEl.style.removeProperty("display");
    cardEl.style.setProperty("flex-grow", "0", "important");
    cardEl.style.setProperty("flex-shrink", "0", "important");
    cardEl.style.setProperty("max-width", "220px", "important");
    enqueueProfileFetch(profileId);
  }
}
function enqueueProfileFetch(profileId) {
  if (filterState.fetchQueue.includes(profileId))
    return;
  filterState.fetchQueue.push(profileId);
  filterState.queueTotal = filterState.fetchQueue.length + filterState.queueCompleted;
  updateFilterStatusUI();
  processFetchQueue();
}
function getProfilePathFromCard(cardEl) {
  const profileId = cardEl.id;
  const links = Array.from(cardEl.querySelectorAll("a"));
  for (const link of links) {
    const href = link.getAttribute("href") || "";
    const match = href.match(/\/members?\/([^?#]+)/);
    if (match) {
      return match[1].replace(/\/$/, "");
    }
  }
  return profileId;
}
function processFetchQueue() {
  if (filterState.fetchQueue.length === 0 || filterState.activeFetches >= maxConcurrentFetches) {
    return;
  }
  if (Object.keys(state.authHeaders).length === 0) {
    setTimeout(processFetchQueue, 1000);
    return;
  }
  const profileId = filterState.fetchQueue.shift();
  if (!profileId)
    return;
  filterState.activeFetches++;
  updateFilterStatusUI();
  const card = document.getElementById(profileId);
  fetchProfileDetail(profileId, card).then((data) => {
    filterState.activeFetches--;
    filterState.queueCompleted++;
    updateFilterStatusUI();
    if (data) {
      data.isDetailed = true;
      const cachedSummary = profileDetailsCache.get(profileId);
      const merged = cachedSummary ? { ...cachedSummary, ...data } : data;
      profileDetailsCache.set(profileId, merged);
      saveProfileToIndexedDB(profileId, merged);
      if (card && filterState.currentSearchKeyword) {
        const matches = containsKeyword(merged, filterState.currentSearchKeyword);
        if (matches) {
          card.style.removeProperty("display");
          card.style.setProperty("flex-grow", "0", "important");
          card.style.setProperty("flex-shrink", "0", "important");
          card.style.setProperty("max-width", "220px", "important");
        } else {
          card.style.setProperty("display", "none", "important");
          card.style.removeProperty("flex-grow");
          card.style.removeProperty("flex-shrink");
          card.style.removeProperty("max-width");
        }
        updateFilterStatusUI();
      }
    }
    setTimeout(processFetchQueue, fetchDelayMs);
  }).catch((err) => {
    console.error(`[Recon+] Error fetching profile ${profileId}:`, err);
    filterState.activeFetches--;
    filterState.queueCompleted++;
    updateFilterStatusUI();
    setTimeout(processFetchQueue, fetchDelayMs);
  });
  if (filterState.activeFetches < maxConcurrentFetches && filterState.fetchQueue.length > 0) {
    processFetchQueue();
  }
}
async function fetchProfileDetail(profileId, cardEl) {
  console.log(`[Recon+] fetchProfileDetail starting for ${profileId}`);
  let data = profileDetailsCache.get(profileId);
  if (data && (data.isDetailed || typeof data.longText === "string")) {
    console.log(`[Recon+] Found detailed profile in memory for ${profileId}`);
    return data;
  }
  data = await getProfileFromIndexedDB(profileId) || undefined;
  if (data && (data.isDetailed || typeof data.longText === "string")) {
    console.log(`[Recon+] Found detailed profile in IndexedDB for ${profileId}`);
    profileDetailsCache.set(profileId, data);
    return data;
  }
  const path = cardEl ? getProfilePathFromCard(cardEl) : profileId;
  console.log(`[Recon+] Path for ${profileId} parsed as: "${path}"`);
  const parts = path.split("/");
  const uuid = parts[0];
  let version = parts[1] || null;
  if (!version) {
    version = profileVersionsMap.get(uuid) || null;
    if (version)
      console.log(`[Recon+] Version for ${uuid} found in profileVersionsMap: ${version}`);
  }
  if (!version) {
    const summary = profileDetailsCache.get(uuid);
    if (summary) {
      if (summary.version) {
        version = summary.version;
        console.log(`[Recon+] Version for ${uuid} found in summary.version: ${version}`);
      } else if (summary.profileUrl) {
        const match = summary.profileUrl.match(/\/profiles\/([^/]+)\/(\d+)/);
        if (match) {
          version = match[2];
          console.log(`[Recon+] Version for ${uuid} found in summary.profileUrl: ${version}`);
        }
      }
    }
  }
  if (!version) {
    console.warn(`[Recon+] Version not found for profile ${uuid}, defaulting to 1`);
    version = 1;
  }
  const targetUrl = `/api/profile/profiles/${uuid}/detail/${version}?culture=en`;
  console.log(`[Recon+] Fetching detail for ${uuid} from: ${targetUrl}`);
  const res = await win.fetch(targetUrl, {
    headers: {
      ...state.authHeaders
    }
  });
  if (!res.ok) {
    console.error(`[Recon+] Fetch failed: Status ${res.status} for ${targetUrl}`);
    throw new Error(`Fetch status: ${res.status}`);
  }
  const json = await res.json();
  console.log(`[Recon+] Fetch success for ${uuid}:`, json);
  return json;
}
function triggerFilterChange(val) {
  filterState.currentSearchKeyword = val.trim().toLowerCase();
  filterState.fetchQueue = [];
  filterState.queueTotal = 0;
  filterState.queueCompleted = 0;
  const cards = getActiveCards();
  if (!filterState.currentSearchKeyword) {
    cards.forEach((card) => {
      card.style.removeProperty("display");
      card.style.removeProperty("flex-grow");
      card.style.removeProperty("flex-shrink");
      card.style.removeProperty("max-width");
      delete card.dataset.reconLastFilterKeyword;
    });
    updateFilterStatusUI();
    return;
  }
  cards.forEach((card) => {
    card.dataset.reconLastFilterKeyword = filterState.currentSearchKeyword;
    applyFilterToCard(card, filterState.currentSearchKeyword);
  });
  processFetchQueue();
}
function updateFilterStatusUI() {
  const statusEl = document.getElementById("recon-filter-status");
  const badgeEl = document.getElementById("recon-filter-count");
  const loadMoreEl = document.getElementById("recon-filter-load-more");
  if (!statusEl)
    return;
  const cards = getActiveCards();
  const total = cards.length;
  const visible = cards.filter((c) => c.style.display !== "none").length;
  if (loadMoreEl) {
    loadMoreEl.style.display = filterState.currentSearchKeyword ? "block" : "none";
  }
  if (Object.keys(state.authHeaders).length === 0) {
    statusEl.innerHTML = `<span>Waiting for authentication...</span>`;
    if (badgeEl)
      badgeEl.style.display = "none";
    return;
  }
  if (!filterState.currentSearchKeyword) {
    statusEl.innerHTML = `<span>Status: Filter inactive</span>`;
    if (badgeEl)
      badgeEl.style.display = "none";
    return;
  }
  if (badgeEl) {
    badgeEl.textContent = `${visible}/${total}`;
    badgeEl.style.display = "block";
  }
  if (filterState.fetchQueue.length > 0 || filterState.activeFetches > 0) {
    const pct = filterState.queueTotal > 0 ? Math.round(filterState.queueCompleted / filterState.queueTotal * 100) : 0;
    statusEl.innerHTML = `<span>Scanning profiles...</span><span style="color: #ff3333; font-weight: bold;">${pct}% (${filterState.queueCompleted}/${filterState.queueTotal})</span>`;
  } else {
    statusEl.innerHTML = `<span>Scan complete</span><span style="color: #2acf65; font-weight: bold;">Matched: ${visible}</span>`;
  }
}
function updatePanelVisibility() {
  const panel = document.getElementById("recon-filter-panel");
  if (!panel)
    return;
  const hasCards = getActiveCards().length > 0;
  if (hasCards) {
    panel.style.display = "flex";
  } else {
    panel.style.display = "none";
  }
}
function triggerLoadMore() {
  const loadMoreBtn = document.getElementById("recon-filter-load-more");
  if (loadMoreBtn) {
    loadMoreBtn.disabled = true;
    loadMoreBtn.textContent = "Loading...";
  }
  const cards = getActiveCards();
  cards.forEach((card) => {
    card.style.removeProperty("display");
    card.style.removeProperty("flex-grow");
    card.style.removeProperty("flex-shrink");
    card.style.removeProperty("max-width");
  });
  win.scrollTo({ top: document.body.scrollHeight, behavior: "instant" });
  setTimeout(() => {
    if (filterState.currentSearchKeyword) {
      triggerFilterChange(filterState.currentSearchKeyword);
    }
    if (loadMoreBtn) {
      loadMoreBtn.disabled = false;
      loadMoreBtn.textContent = "Load More";
    }
    win.scrollTo({ top: 0, behavior: "smooth" });
  }, 1200);
}
function injectFilterPanel() {
  const myTypeFilter = document.querySelector("t101-profile-my-type-filter");
  if (!myTypeFilter)
    return;
  let panel = document.getElementById("recon-filter-panel");
  if (panel) {
    if (panel.parentNode !== myTypeFilter.parentNode) {
      myTypeFilter.parentNode.insertBefore(panel, myTypeFilter.nextSibling);
    }
    return;
  }
  panel = document.createElement("div");
  panel.id = "recon-filter-panel";
  panel.style.cssText = `
        position: relative;
        background-color: #ffffff26;
        padding: 25px 15px 15px;
        width: 100%;
        box-sizing: border-box;
        color: #fff;
        display: none;
        flex-direction: column;
        gap: 15px;
        font-family: 'Barlow', sans-serif;
        border-bottom: 1px solid rgba(255, 255, 255, 0.25);
    `;
  const titleDiv = document.createElement("div");
  titleDiv.style.cssText = `
        display: flex;
        justify-content: space-between;
        align-items: center;
        user-select: none;
    `;
  const titleH3 = document.createElement("h3");
  titleH3.textContent = "Recon+ Filter";
  titleH3.style.cssText = `
        font: 20px/25px Barlow, sans-serif;
        font-weight: 700;
        color: #fff;
        margin: 0;
        letter-spacing: 0;
    `;
  const badgeSpan = document.createElement("span");
  badgeSpan.id = "recon-filter-count";
  badgeSpan.style.cssText = `
        display: none;
        background: #ff3333;
        color: #fff;
        padding: 2px 8px;
        border-radius: 12px;
        font-size: 11px;
        font-weight: 800;
        font-family: 'Barlow', sans-serif;
    `;
  titleDiv.appendChild(titleH3);
  titleDiv.appendChild(badgeSpan);
  panel.appendChild(titleDiv);
  const inputWrapper = document.createElement("div");
  inputWrapper.style.cssText = `
        position: relative;
        display: flex;
        align-items: center;
        width: 100%;
    `;
  const input = document.createElement("input");
  input.type = "text";
  input.placeholder = "Search bio keywords...";
  input.style.cssText = `
        background: rgba(0, 0, 0, 0.25);
        border: 1px solid rgba(255, 255, 255, 0.15);
        border-radius: 6px;
        color: #fff;
        padding: 10px 32px 10px 12px;
        font-size: 14px;
        outline: none;
        width: 100%;
        box-sizing: border-box;
        font-family: 'Raleway', sans-serif;
        transition: border-color 0.2s, background 0.2s;
    `;
  input.onfocus = () => {
    input.style.borderColor = "#ff3333";
    input.style.background = "rgba(0, 0, 0, 0.35)";
  };
  input.onblur = () => {
    input.style.borderColor = "rgba(255, 255, 255, 0.15)";
    input.style.background = "rgba(0, 0, 0, 0.25)";
  };
  const clearBtn = document.createElement("span");
  clearBtn.innerHTML = "&#x2715;";
  clearBtn.style.cssText = `
        position: absolute;
        right: 12px;
        cursor: pointer;
        color: rgba(255, 255, 255, 0.4);
        font-size: 14px;
        display: none;
        user-select: none;
        transition: color 0.2s;
    `;
  clearBtn.onmouseover = () => clearBtn.style.color = "#fff";
  clearBtn.onmouseout = () => clearBtn.style.color = "rgba(255, 255, 255, 0.4)";
  clearBtn.onclick = () => {
    input.value = "";
    clearBtn.style.display = "none";
    triggerFilterChange("");
  };
  input.addEventListener("input", () => {
    clearBtn.style.display = input.value ? "block" : "none";
    triggerFilterChange(input.value);
  });
  inputWrapper.appendChild(input);
  inputWrapper.appendChild(clearBtn);
  panel.appendChild(inputWrapper);
  const statusDiv = document.createElement("div");
  statusDiv.id = "recon-filter-status";
  statusDiv.style.cssText = `
        font-size: 12px;
        color: #adadad;
        font-family: 'Raleway', sans-serif;
        display: flex;
        justify-content: space-between;
        align-items: center;
        width: 100%;
    `;
  statusDiv.innerHTML = `<span>Status: Ready</span>`;
  const loadMoreBtn = document.createElement("button");
  loadMoreBtn.id = "recon-filter-load-more";
  loadMoreBtn.textContent = "Load More";
  loadMoreBtn.style.cssText = `
        background: rgba(255, 51, 51, 0.15);
        color: #ff3333;
        border: 1px solid rgba(255, 51, 51, 0.3);
        border-radius: 4px;
        padding: 4px 10px;
        font-size: 11px;
        font-weight: 800;
        cursor: pointer;
        text-transform: uppercase;
        font-family: 'Barlow', sans-serif;
        display: none;
        transition: all 0.2s;
    `;
  loadMoreBtn.onmouseover = () => {
    loadMoreBtn.style.background = "#ff3333";
    loadMoreBtn.style.color = "#fff";
  };
  loadMoreBtn.onmouseout = () => {
    loadMoreBtn.style.background = "rgba(255, 51, 51, 0.15)";
    loadMoreBtn.style.color = "#ff3333";
  };
  loadMoreBtn.onclick = triggerLoadMore;
  statusDiv.appendChild(loadMoreBtn);
  panel.appendChild(statusDiv);
  myTypeFilter.parentNode.insertBefore(panel, myTypeFilter.nextSibling);
}

// src/index.ts
(function() {
  injectStyles();
  setupXHRInterceptor();
  checkAndPaintProfileFromIndexedDB();
  const observer = new MutationObserver(() => {
    injectViewedButton();
    setupPhotoViewer();
    if (state.lastProfileDetailPageData) {
      paintProfileDetailPage(state.lastProfileDetailPageData);
    }
    const match = win.location.pathname.match(/\/(?:members|profiles)\/([a-f0-9-]{36}|[a-zA-Z0-9_-]+)/i);
    if (match && match[1]) {
      paintPremiumGallery(match[1]);
    }
    profileDetailsCache.forEach((data, profileId) => {
      const card = document.getElementById(profileId);
      if (card && !card.dataset.reconInjected)
        paintProfileData(profileId);
    });
    injectFilterPanel();
    updatePanelVisibility();
    injectDistanceMap();
    if (filterState.currentSearchKeyword) {
      const cards = getActiveCards();
      cards.forEach((card) => {
        if (card.dataset.reconLastFilterKeyword !== filterState.currentSearchKeyword) {
          card.dataset.reconLastFilterKeyword = filterState.currentSearchKeyword;
          applyFilterToCard(card, filterState.currentSearchKeyword);
        }
      });
    }
  });
  observer.observe(document.documentElement, { childList: true, subtree: true });
  if (state.lastProfileDetailPageData) {
    paintProfileDetailPage(state.lastProfileDetailPageData);
  }
  const initMatch = win.location.pathname.match(/\/(?:members|profiles)\/([a-f0-9-]{36}|[a-zA-Z0-9_-]+)/i);
  if (initMatch && initMatch[1]) {
    paintPremiumGallery(initMatch[1]);
  }
})();

})();