Recon+

Unlocks Paywalls in recon

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();