Recon+

Unlocks Paywalls in recon

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Recon+
// @namespace    https://update.greasyfork.org/scripts/582087/Recon%2B.user.js
// @version      2.0
// @match        https://www.recon.com/*
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @run-at       document-start
// @description Unlocks Paywalls in recon
// ==/UserScript==

(function () {
    'use strict';

    const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
    const profileDetailsCache = new Map();
    const profileVersionsMap = new Map();
    let lastBlockedRequest = null;
    let lastProfileDetailPageData = null;
    const authHeaders = {};

    // Complete dictionary for interest IDs
    const 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"
    };

    // -------------------------------------------------------------------------
    // 1. CSS INJECTION (Hiding Angular's original elements)
    // -------------------------------------------------------------------------
    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 });
    }

    // -------------------------------------------------------------------------
    // 2. TOAST SYSTEM
    // -------------------------------------------------------------------------
    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);
    }

    // -------------------------------------------------------------------------
    // 3. XHR INTERCEPTOR
    // -------------------------------------------------------------------------
    const origXHR = win.XMLHttpRequest;
    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) {
            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) {
                        lastBlockedRequest = {
                            url: url,
                            method: method,
                            headers: this._headers,
                            body: 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 () {
            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())) {
                        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/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}`);
                                    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;
    };

    // Helper to query location from IndexedDB (RECON_DB -> App_Locations)
    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;
                    }

                    // Try querying as number first
                    const getRequest = store.get(Number(locationId));
                    getRequest.onsuccess = function () {
                        let result = checkVal(getRequest.result);
                        if (result) {
                            db.close();
                            resolve(result);
                        } else {
                            // Try querying as string
                            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);
            }
        });
    }

    // Helper to save location to IndexedDB (RECON_DB -> App_Locations)
    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); // 1 year expiry

                const value = {
                    data: {
                        locationId: Number(locationId),
                        locationUrl: locationUrl,
                        longName: longName,
                        name: 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);
        }
    }

    // Helper to query profile from IndexedDB (RECON_DB -> Profile_Cache)
    function getProfileFromIndexedDB(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("Profile_Cache")) {
                        db.close();
                        resolve(null);
                        return;
                    }
                    const transaction = db.transaction("Profile_Cache", "readonly");
                    const store = transaction.objectStore("Profile_Cache");

                    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 {
                            // Fallback to get as opposite type if keys are mixed
                            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);
            }
        });
    }

    // Helper to save profile to IndexedDB (RECON_DB -> Profile_Cache)
    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;
                    }

                    // Merge details
                    const mergedData = existingData ? { ...existingData, ...data } : data;

                    const expiryDate = new Date();
                    expiryDate.setFullYear(expiryDate.getFullYear() + 1); // 1 year expiry

                    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\/([^/]+)/);
        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) {
                        lastProfileDetailPageData = cachedProfile;
                        paintProfileDetailPage(cachedProfile);
                    }
                });
            }
        }
    }


    // -------------------------------------------------------------------------
    // 4. DOM INJECTION ENGINE
    // -------------------------------------------------------------------------
    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";

        // A. Inject Location (Using IndexedDB with Fetch fallback)
        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 = `📍 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 = `📍 ${rawLocId}`;
            } else {
                getLocationFromIndexedDB(rawLocId).then(cachedLoc => {
                    if (cachedLoc) {
                        const locName = cachedLoc.name || cachedLoc.longName || rawLocId;
                        locDiv.innerHTML = `📍 ${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: {
                                ...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 = `📍 ${name}`;
                                    saveLocationToIndexedDB(rawLocId, fetchUrl, name, longName);
                                } else {
                                    locDiv.innerHTML = `📍 Loc ID: ${rawLocId}`;
                                }
                            }).catch(() => { locDiv.innerHTML = `📍 Loc ID: ${rawLocId}`; });
                    }
                });
            }
        }

        // B. Inject Full Interests Array
        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 = `📅 Profile Updated: <span style="color: #fff; font-weight: 800;">${formattedDate}</span>`;
        } catch (e) {
            dateDiv.innerHTML = `📅 Profile Updated: <span style="color: #fff; font-weight: 800;">${data.lastUpdatedDate}</span>`;
        }

        if (container.tagName === 'H1') {
            container.after(dateDiv);
        } else {
            container.appendChild(dateDiv);
        }
    }

    // -------------------------------------------------------------------------
    // 5. RESTORED FEATURES (Buttons & Lightbox)
    // -------------------------------------------------------------------------
    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: 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() {
        showToast('Preparing profile export...', 'success');

        let JSZipLib;
        try {
            JSZipLib = await getJSZip();
        } catch (e) {
            console.error(e);
            showToast('Failed to load JSZip library.', 'error');
            return;
        }

        const data = lastProfileDetailPageData;
        const profileId = data ? data.id : (win.location.pathname.match(/\/members\/([^/]+)/) || [])[1] || 'unknown';
        const displayName = data ? (data.displayName || data.username) : (document.title || 'Profile');

        // 1. Gather all unique photo URLs
        const photoUrls = new Set();

        // From DOM photo-tiles
        document.querySelectorAll('.photo-tile').forEach(t => {
            const bgImg = t.style.backgroundImage;
            if (bgImg) {
                const url = bgImg.replace(/^url\(["']?/, '').replace(/["']?\)$/, '').split('?')[0];
                if (url) photoUrls.add(url);
            }
        });

        // From DOM profile avatars
        document.querySelectorAll('.profile-avatar').forEach(t => {
            const bgImg = t.style.backgroundImage;
            if (bgImg) {
                const url = bgImg.replace(/^url\(["']?/, '').replace(/["']?\)$/, '').split('?')[0];
                if (url) photoUrls.add(url);
            }
        });

        // From lastProfileDetailPageData recursively
        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');
            return;
        }

        showToast(`Found ${urlsArray.length} photos. Starting download...`, 'success');

        const zip = new JSZipLib();

        // 2. Add profile_info.txt
        let infoText = '';
        infoText += `=== RECON PROFILE EXPORT ===\n`;
        infoText += `Export Date: ${new Date().toLocaleString()}\n`;
        infoText += `Profile URL: ${win.location.href}\n\n`;

        if (data) {
            infoText += `Display Name: ${data.displayName || 'N/A'}\n`;
            infoText += `Username: ${data.username || 'N/A'}\n`;
            infoText += `Profile ID: ${data.id || 'N/A'}\n`;
            if (data.location) {
                infoText += `Location: ID ${data.location.locationId || 'N/A'}\n`;
            }
            if (data.lastUpdatedDate) {
                infoText += `Last Updated: ${data.lastUpdatedDate}\n`;
            }
            if (data.interests && data.interests.length > 0) {
                infoText += `Interests: ${data.interests.map(id => interestMap[id] || `Tag ${id}`).join(', ')}\n`;
            }

            infoText += `\n--- PROFILE INFO (RAW KEY-VALUES) ---\n`;
            for (const [key, val] of Object.entries(data)) {
                if (typeof val !== 'object' && val !== null) {
                    infoText += `${key}: ${val}\n`;
                }
            }
        }

        // Gather bio and details text from DOM
        const bioSection = document.querySelector('.text-container, .bio-container, .member-bio');
        if (bioSection) {
            infoText += `\n--- PROFILE BIO ---\n`;
            infoText += bioSection.innerText.trim() + `\n`;
        }

        const infoSections = document.querySelectorAll('.details-container, .stats-container, .profile-details');
        if (infoSections.length > 0) {
            infoText += `\n--- PROFILE DETAILS ---\n`;
            infoSections.forEach(section => {
                infoText += section.innerText.trim() + `\n\n`;
            });
        }

        zip.file("profile_info.txt", infoText);

        // 3. Add raw JSON if present
        if (data) {
            zip.file("profile_data.json", JSON.stringify(data, null, 4));
        }

        // 4. Download and add photos
        let successCount = 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);
            }
        });

        await Promise.all(downloadPromises);

        if (successCount === 0) {
            showToast('Failed to download any profile photos.', 'error');
            return;
        }

        showToast(`Zipping ${successCount} photos...`, 'success');

        try {
            const content = await zip.generateAsync({ type: 'blob' });
            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);
            showToast(`Export complete! Saved ${successCount}/${urlsArray.length} photos.`, 'success');
        } catch (err) {
            console.error('[Script] Error creating zip file:', err);
            showToast('Error generating zip archive.', 'error');
        }
    }

    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;

            // Inject Viewed Button
            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 (!lastBlockedRequest) return showToast('No profile visit tracker captured yet.', 'error');
                    const xhr = new origXHR();
                    xhr._isManualTrigger = true;
                    xhr.open(lastBlockedRequest.method, lastBlockedRequest.url);
                    if (lastBlockedRequest.headers) {
                        for (const [key, value] of Object.entries(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(lastBlockedRequest.body);
                });
                targetEl.parentNode.insertBefore(viewBtn, targetEl.nextSibling);
            }

            // Inject Export Button
            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);
            }
        }
    }

    function setupPhotoViewer() {
        const tiles = document.querySelectorAll('.photo-tile:not([data-lightbox-ready])');
        tiles.forEach(tile => {
            tile.setAttribute('data-lightbox-ready', 'true');
            tile.style.pointerEvents = 'auto'; tile.style.cursor = 'pointer';
            tile.addEventListener('click', (e) => {
                e.preventDefault(); e.stopPropagation();

                // Get all photo tiles in the same gallery container, or globally on the page if not grouped
                const container = tile.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 bgImg = t.style.backgroundImage;
                    return bgImg ? bgImg.replace(/^url\(["']?/, '').replace(/["']?\)$/, '').split('?')[0] : null;
                }).filter(Boolean);

                const bgImg = tile.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);
    }

    // -------------------------------------------------------------------------
    // 5.5. DESCRIPTION KEYWORD FILTER
    // -------------------------------------------------------------------------
    let currentSearchKeyword = '';
    let fetchQueue = [];
    let activeFetches = 0;
    let queueTotal = 0;
    let queueCompleted = 0;
    const maxConcurrentFetches = 2;
    const 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') || el.querySelector('.photo-tile') || el.classList.contains('photo-tile')));
    }

    function containsKeyword(obj, keyword) {
        if (!obj) return false;
        if (typeof obj === 'string') {
            const val = obj.toLowerCase();
            // Skip URLs and image paths to avoid matching on file names/extensions
            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();
                    // Skip technical keys
                    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;
        }

        // Check memory cache
        let data = profileDetailsCache.get(profileId);
        console.log(`[Recon+] Memory cache check for ${profileId}:`, data ? "Found" : "Not Found");

        // Check IndexedDB cache
        if (!data) {
            data = await getProfileFromIndexedDB(profileId);
            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) {
            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...`);
            // Keep visible until fetched
            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 (fetchQueue.includes(profileId)) return;
        fetchQueue.push(profileId);
        queueTotal = fetchQueue.length + 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(/\/$/, ''); // Remove trailing slash
            }
        }
        return profileId;
    }

    function processFetchQueue() {
        if (fetchQueue.length === 0 || activeFetches >= maxConcurrentFetches) {
            return;
        }

        if (Object.keys(authHeaders).length === 0) {
            setTimeout(processFetchQueue, 1000);
            return;
        }

        const profileId = fetchQueue.shift();
        activeFetches++;
        updateFilterStatusUI();

        const card = document.getElementById(profileId);
        fetchProfileDetail(profileId, card).then(data => {
            activeFetches--;
            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 && currentSearchKeyword) {
                    const matches = containsKeyword(merged, 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);
            activeFetches--;
            queueCompleted++;
            updateFilterStatusUI();
            setTimeout(processFetchQueue, fetchDelayMs);
        });

        if (activeFetches < maxConcurrentFetches && 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);
        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;

        // Try profileVersionsMap
        if (!version) {
            version = profileVersionsMap.get(uuid);
            if (version) console.log(`[Recon+] Version for ${uuid} found in profileVersionsMap: ${version}`);
        }

        // Try summary object
        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}`);
                    }
                }
            }
        }

        // Default version if not found
        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: {
                ...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) {
        currentSearchKeyword = val.trim().toLowerCase();
        fetchQueue = [];
        queueTotal = 0;
        queueCompleted = 0;

        const cards = getActiveCards();

        if (!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 = currentSearchKeyword;
            applyFilterToCard(card, 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 = currentSearchKeyword ? 'block' : 'none';
        }

        if (Object.keys(authHeaders).length === 0) {
            statusEl.innerHTML = `<span>Waiting for authentication...</span>`;
            if (badgeEl) badgeEl.style.display = 'none';
            return;
        }

        if (!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 (fetchQueue.length > 0 || activeFetches > 0) {
            const pct = queueTotal > 0 ? Math.round((queueCompleted / queueTotal) * 100) : 0;
            statusEl.innerHTML = `<span>Scanning profiles...</span><span style="color: #ff3333; font-weight: bold;">${pct}% (${queueCompleted}/${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 (currentSearchKeyword) {
                triggerFilterChange(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);
    }

    // -------------------------------------------------------------------------
    // 6. OBSERVER LIFECYCLE
    // -------------------------------------------------------------------------
    const observer = new MutationObserver(() => {
        injectViewedButton();
        setupPhotoViewer();
        if (lastProfileDetailPageData) {
            paintProfileDetailPage(lastProfileDetailPageData);
        }

        profileDetailsCache.forEach((data, profileId) => {
            const card = document.getElementById(profileId);
            if (card && !card.dataset.reconInjected) paintProfileData(profileId);
        });

        // Recon+ Keyword Filter hooks
        injectFilterPanel();
        updatePanelVisibility();

        if (currentSearchKeyword) {
            const cards = getActiveCards();
            cards.forEach(card => {
                if (card.dataset.reconLastFilterKeyword !== currentSearchKeyword) {
                    card.dataset.reconLastFilterKeyword = currentSearchKeyword;
                    applyFilterToCard(card, currentSearchKeyword);
                }
            });
        }
    });

    observer.observe(document.documentElement, { childList: true, subtree: true });

    // Handle initial state if page is loaded
    if (lastProfileDetailPageData) {
        paintProfileDetailPage(lastProfileDetailPageData);
    }

})();