☰

Recu.me Titan Suite

Multi-tag search engine with background tag enrichment, search history, resume-on-block, live performer browser, favorites, and auto-saved thumbnails for recu.me.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         Recu.me Titan Suite
// @namespace    https://github.com/recu-titan
// @version      19.0
// @description  Multi-tag search engine with background tag enrichment, search history, resume-on-block, live performer browser, favorites, and auto-saved thumbnails for recu.me.
// @author       Titan
// @match        https://recu.me/*
// @match        https://*.recu.me/*
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @connect      recu.me
// @connect      highmedia-stream.b-cdn.net
// @run-at       document-end
// @license MIT
// @noframes
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        defaultDepth: 5,
        storagePrefix: 'recu_v18_',
        cacheExpiry: 1000 * 60 * 60 * 24 * 7, // 7 days
        animationSpeed: 200,
        cardMinWidth: '320px',
        hoverScale: 1.05,
        spriteColumns: 4,
        spriteRows: 4,
        spriteFrames: 16,
        liveFrames: [0, 200, 400, 600, 800, 1000],
        liveAnimationSpeed: 1500,
        // Thumbnail settings
        thumbnailPath: 'thumbnail/', // Subfolder under the browser's Downloads folder (trailing slash required)
        autoSaveThumbnails: true,
        // Optional: absolute path to a local folder to scan for previously-saved thumbnails so broken/expired remote thumbs can be restored.
        // Leave empty to disable local restore. Example: 'C:\\Users\\YourName\\Downloads\\thumbnail' or '/home/you/Downloads/thumbnail'.
        // Note: this only works if your browser allows file:// access from web pages (rare). Safe to leave blank.
        localThumbnailFolder: '',
        maxSearchHistory: 20,
        // Concurrency settings
        concurrentSearches: 3, // Number of tags to search concurrently
        concurrentPages: 1 // Number of pages to fetch concurrently per tag (1 = sequential, fast)
    };

    const NO_THUMB = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"%3E%3Crect width="320" height="180" fill="%231e293b"/%3E%3Ctext x="160" y="90" font-size="20" fill="%23475569" text-anchor="middle"%3ENo Preview%3C/text%3E%3C/svg%3E';

    const FILTER_OPTIONS = {
        section: [{ v: 'videos', l: 'Videos' }, { v: 'clips', l: 'Clips' }, { v: 'live', l: 'Live' }],
        type: [{ v: 'recent', l: 'Recent' }, { v: 'bookmarked', l: 'Bookmarked' }, { v: 'viewed', l: 'Viewed' }, { v: 'liked', l: 'Liked' }],
        period: [{ v: 'total', l: 'All Time' }, { v: 'month', l: 'Month' }, { v: 'week', l: 'Week' }, { v: 'today', l: 'Today' }],
        gender: [{ v: 'all', l: 'All Genders' }, { v: 'female', l: 'Female' }, { v: 'male', l: 'Male' }, { v: 'couple', l: 'Couple' }, { v: 'trans', l: 'Trans' }],
        ethnicity: [{ v: 'all', l: 'All Ethnicities' }, { v: 'white', l: 'White' }, { v: 'black', l: 'Black' }, { v: 'asian', l: 'Asian' }, { v: 'latino', l: 'Latino' }],
        age: [{ v: 'all', l: 'All Ages' }, { v: 'young', l: 'Young' }, { v: 'mature', l: 'Mature' }],
        body: [{ v: 'all', l: 'All Body Types' }, { v: 'slim', l: 'Slim' }, { v: 'chubby', l: 'Chubby' }, { v: 'muscular', l: 'Muscular' }]
    };

    const DEFAULT_FILTERS = {
        gender: 'female'
    };

    const getDefaultFilterValue = key => DEFAULT_FILTERS[key] || FILTER_OPTIONS[key][0].v;

    const wait = ms => new Promise(r => setTimeout(r, ms));

    const suggestionCache = new Map();

    function escapeHtml(value) {
        return String(value ?? '').replace(/[&<>"']/g, ch => ({
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#39;'
        }[ch]));
    }

    function isEmptyResultsPage(html) {
        return html.includes('No recordings meet the criteria');
    }

    function isChallengePage(html) {
        return /Just a moment|cdn-cgi\/challenge-platform|cf-browser-verification|cf-chl|Verify you are human/i.test(html || '');
    }

    // ============================================================
    // 1. STATE & PERSISTENCE
    // ============================================================
    const State = {
        tagGroups: [],
        allMatches: [],
        livePerformers: [],
        favorites: [],
        thumbCache: {},
        filters: {},
        expandedGroups: new Set(),
        showFavoritesOnly: false,
        brokenThumbs: new Set(),
        // NEW: Search history
        searchHistory: [],
        currentHistoryIndex: -1,
        // NEW: Downloaded thumbnails tracking
        downloadedThumbs: new Set(),
        // NEW: Video tags cache (permanent)
        videoTagsCache: {},
        // NEW: Tag fetch progress tracking
        tagFetchProgress: {
            isRunning: false,
            current: 0,
            total: 0,
            queue: []
        },
        // NEW: Expanded cards (for tag expansion)
        expandedCards: new Set(),
        // NEW: Tag popularity index (across current results)
        tagPopularity: {},
        // NEW: Active search state (for resume functionality)
        activeSearch: null,
        isResumingSearch: false,
        searchAbortReason: null,
        // NEW: Verified thumbnails (to avoid re-checking)
        verifiedThumbs: new Set(),
        // NEW: Videos being verified (to prevent duplicate checks)
        verifyingThumbs: new Set()
    };

    // ============================================================
    // 2. VIDEO TAG MANAGER (Fetch, Cache, Index)
    // ============================================================
    const VideoTagManager = {
        CACHE_KEY: 'video_tags_cache_v1',
        PROGRESS_KEY: 'tag_fetch_progress_v1',
        CONCURRENT_FETCHES: 4,
        FETCH_DELAY: 200,
        saveTimer: null,

        init() {
            try {
                const cached = GM_getValue(this.CACHE_KEY);
                if (cached) {
                    State.videoTagsCache = JSON.parse(cached);
                }
            } catch (e) {
                State.videoTagsCache = {};
            }
        },

        saveCache() {
            GM_setValue(this.CACHE_KEY, JSON.stringify(State.videoTagsCache));
        },

        scheduleCacheSave() {
            if (this.saveTimer) {
                clearTimeout(this.saveTimer);
            }

            this.saveTimer = setTimeout(() => {
                this.saveTimer = null;
                this.saveCache();
            }, 250);
        },

        clearCache() {
            State.videoTagsCache = {};
            if (this.saveTimer) {
                clearTimeout(this.saveTimer);
                this.saveTimer = null;
            }
            GM_deleteValue(this.CACHE_KEY);
            alert('Video tag cache cleared!');
        },

        getCacheKey(video) {
            return `${video.user}_${video.id}`;
        },

        getCachedTags(video) {
            return State.videoTagsCache[this.getCacheKey(video)] || null;
        },

        async fetchVideoTags(video) {
            const cacheKey = this.getCacheKey(video);

            // Return cached if exists
            if (State.videoTagsCache[cacheKey]) {
                return State.videoTagsCache[cacheKey];
            }

            try {
                const url = `https://recu.me/${video.user}/video/${video.id}/play`;
                const res = await fetch(url);
                const html = await res.text();
                const doc = new DOMParser().parseFromString(html, 'text/html');

                const tagsDiv = doc.querySelector('.tags');
                const tags = [];

                if (tagsDiv) {
                    tagsDiv.querySelectorAll('a').forEach(a => {
                        const tagText = a.innerText.trim().replace('#', '');
                        if (tagText) tags.push(tagText);
                    });
                }

                // Cache permanently
                State.videoTagsCache[cacheKey] = tags;
                this.scheduleCacheSave();

                return tags;
            } catch (e) {
                console.error(`[Titan] Failed to fetch tags for ${video.user}/${video.id}:`, e);
                return [];
            }
        },

        // Calculate tag popularity across all current results
        calculatePopularity(videos) {
            State.tagPopularity = {};
            videos.forEach(v => {
                const tags = this.getCachedTags(v) || v.foundTags || [];
                tags.forEach(tag => {
                    State.tagPopularity[tag] = (State.tagPopularity[tag] || 0) + 1;
                });
            });
        },

        // Sort tags by popularity
        sortByPopularity(tags) {
            return [...tags].sort((a, b) => {
                const popA = State.tagPopularity[a] || 0;
                const popB = State.tagPopularity[b] || 0;
                return popB - popA;
            });
        },

        // Background fetch for all videos with progress tracking
        async fetchAllInBackground(videos, onProgress, onVideoUpdate) {
            // Filter out already cached
            const uncached = videos.filter(v => !this.getCachedTags(v));

            if (uncached.length === 0) {
                this.calculatePopularity(videos);
                return;
            }

            State.tagFetchProgress = {
                isRunning: true,
                current: videos.length - uncached.length,
                total: videos.length,
                queue: [...uncached]
            };

            onProgress(State.tagFetchProgress.current, State.tagFetchProgress.total);

            // Process in batches of CONCURRENT_FETCHES
            const processBatch = async () => {
                const batch = [];

                for (let i = 0; i < this.CONCURRENT_FETCHES && State.tagFetchProgress.queue.length > 0; i++) {
                    const video = State.tagFetchProgress.queue.shift();
                    batch.push(
                        this.fetchVideoTags(video).then(tags => {
                            video.allTags = tags;
                            State.tagFetchProgress.current++;
                            onProgress(State.tagFetchProgress.current, State.tagFetchProgress.total);
                            onVideoUpdate(video);
                            return video;
                        })
                    );

                    // Stagger the requests
                    if (i < this.CONCURRENT_FETCHES - 1) {
                        await wait(this.FETCH_DELAY);
                    }
                }

                await Promise.all(batch);

                // Continue if more in queue
                if (State.tagFetchProgress.queue.length > 0 && State.tagFetchProgress.isRunning) {
                    await processBatch();
                }
            };

            await processBatch();

            State.tagFetchProgress.isRunning = false;
            this.saveCache();
            this.calculatePopularity(videos);
        },

        stopFetching() {
            State.tagFetchProgress.isRunning = false;
            State.tagFetchProgress.queue = [];
        }
    };


    // ============================================================
    // 4. THUMBNAIL CACHE & AUTO-SAVE
    // ============================================================
    const ThumbnailManager = {
        CACHE_KEY: 'thumb_download_cache_v2',
        BROKEN_THUMB_KEY: 'broken_thumb_cache_v1',

        init() {
            // Load downloaded thumbs tracking
            try {
                const cached = GM_getValue(this.CACHE_KEY);
                if (cached) {
                    State.downloadedThumbs = new Set(JSON.parse(cached));
                }
                const brokenCached = GM_getValue(this.BROKEN_THUMB_KEY);
                if (brokenCached) {
                    State.brokenThumbs = new Set(JSON.parse(brokenCached));
                }
            } catch (e) {
                State.downloadedThumbs = new Set();
                State.brokenThumbs = new Set();
            }
        },

        _saveTimer: null,

        _flushSave() {
            GM_setValue(this.CACHE_KEY, JSON.stringify([...State.downloadedThumbs]));
            GM_setValue(this.BROKEN_THUMB_KEY, JSON.stringify([...State.brokenThumbs]));
        },

        saveCache({ immediate = false } = {}) {
            if (immediate) {
                if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
                this._flushSave();
                return;
            }
            if (this._saveTimer) return;
            this._saveTimer = setTimeout(() => {
                this._saveTimer = null;
                this._flushSave();
            }, 500);
        },

        getCacheKey(video) {
            return `${video.user}_${video.id}`;
        },

        getCleanTitle(video) {
            const rawTitle = video && video.title ? video.title : `${video?.user || 'video'}_video`;
            return rawTitle
                .replace(/[^a-z0-9]/gi, '_')
                .replace(/_+/g, '_')
                .replace(/^_+|_+$/g, '')
                .substring(0, 50) || `${video?.user || 'video'}_video`;
        },

        getPrimaryThumbnailBaseName(video) {
            return `${video.user}_${video.id}_${this.getCleanTitle(video)}`;
        },

        getRestoreBaseNames(video) {
            return [
                this.getPrimaryThumbnailBaseName(video),
                `${video.user}_${video.id}_${video.user}_video`,
                `${video.user}_${video.id}_${video.user}`
            ];
        },

        getDownloadFilename(video, thumbUrl) {
            const ext = thumbUrl && thumbUrl.includes('.png') ? 'png' : 'jpg';
            return `${CONFIG.thumbnailPath}${this.getPrimaryThumbnailBaseName(video)}.${ext}`;
        },

        isAlreadyDownloaded(video) {
            return State.downloadedThumbs.has(this.getCacheKey(video));
        },

        // Fetch thumbnail as blob using XHR (emulates website's blob creation)
        async fetchThumbnailAsBlob(thumbUrl) {
            if (!thumbUrl || thumbUrl === NO_THUMB || thumbUrl.startsWith('data:') || thumbUrl.startsWith('blob:')) {
                return null;
            }

            // Add delay to avoid overwhelming the server
            await wait(200);

            return new Promise((resolve) => {
                const xhr = new XMLHttpRequest();
                xhr.open("GET", thumbUrl);
                xhr.responseType = 'blob';

                xhr.onabort = function () {
                    resolve(null);
                };

                xhr.onerror = function () {
                    resolve(null);
                };

                xhr.onload = function () {
                    if (this.status !== 200) {
                        resolve(null);
                        return;
                    }

                    const blob = this.response;
                    if (blob && blob.size > 0) {
                        // Create blob URL
                        const blobURL = URL.createObjectURL(blob);
                        resolve(blobURL);
                    } else {
                        resolve(null);
                    }
                };

                xhr.send();

                // Timeout after 10 seconds
                setTimeout(() => {
                    if (xhr.readyState !== XMLHttpRequest.DONE) {
                        xhr.abort();
                        resolve(null);
                    }
                }, 10000);
            });
        },

        // Check if thumbnail URL is valid using HTTP status code
        async checkThumbnailValid(thumbUrl) {
            if (!thumbUrl || thumbUrl === NO_THUMB || thumbUrl.startsWith('data:')) {
                return false;
            }

            // Blob URLs are always considered valid (they're already loaded)
            if (thumbUrl.startsWith('blob:')) {
                return true;
            }

            // Check if already marked as broken
            if (State.brokenThumbs.has(thumbUrl)) {
                return false;
            }

            // No delay for faster auto-save - validation happens in parallel

            // Use image load test to detect 4xx errors (410, 404, etc.)
            // This is more reliable than HEAD requests which can't read status with no-cors
            return new Promise((resolve) => {
                const img = new Image();
                let resolved = false;

                const markBroken = () => {
                    if (!resolved) {
                        resolved = true;
                        State.brokenThumbs.add(thumbUrl);
                        this.saveCache();
                        resolve(false);
                    }
                };

                const markValid = () => {
                    if (!resolved) {
                        resolved = true;
                        resolve(true);
                    }
                };

                img.onload = () => {
                    // Image loaded successfully - check if it has valid dimensions
                    if (img.naturalWidth > 0 && img.naturalHeight > 0) {
                        markValid();
                    } else {
                        // Image loaded but has no dimensions - likely broken
                        markBroken();
                    }
                };

                img.onerror = () => {
                    // Image failed to load - likely 4xx (410, 404, etc.) or network error
                    markBroken();
                };

                img.src = thumbUrl;

                // Timeout after 5 seconds
                setTimeout(() => {
                    if (!resolved) {
                        if (img.complete && img.naturalWidth > 0 && img.naturalHeight > 0) {
                            markValid();
                        } else {
                            // Timeout - assume broken
                            markBroken();
                        }
                    }
                }, 5000);
            });
        },

        // Restore thumbnail from local Downloads folder
        // Pattern: {performer_username}_{video_ID}_{performer_username}_video.jpg
        // Also supports: {performer_username}_{video_ID}_{performer_username}.jpg (without _video)
        async restoreLocalThumbnail(video) {
            if (!video || !video.user || !video.id) return null;
            if (!CONFIG.localThumbnailFolder) return null;

            const thumbnailFolder = CONFIG.localThumbnailFolder.replace(/[\\/]+$/, '');
            const sep = thumbnailFolder.includes('\\') ? '\\' : '/';
            const extensions = ['jpg', 'jpeg', 'png'];

            // Try two patterns:
            // 1. username_id_username_video.ext (with _video suffix)
            // 2. username_id_username.ext (without _video suffix)
            const patterns = this.getRestoreBaseNames(video);

            for (const baseFilename of patterns) {
                for (const ext of extensions) {
                    const filename = `${baseFilename}.${ext}`;
                    const filePath = `${thumbnailFolder}${sep}${filename}`;
                    // Convert path to file:// URL format
                    const fileUrl = `file:///${filePath.replace(/\\/g, '/').replace(/ /g, '%20').replace(/^\/+/, '')}`;

                    // Try to load the image to verify it exists
                    try {
                        const exists = await new Promise((resolve) => {
                            const img = new Image();
                            let resolved = false;

                            img.onload = () => {
                                if (!resolved) {
                                    resolved = true;
                                    resolve(true);
                                }
                            };

                            img.onerror = () => {
                                if (!resolved) {
                                    resolved = true;
                                    resolve(false);
                                }
                            };

                            img.src = fileUrl;

                            // Timeout after 2 seconds
                            setTimeout(() => {
                                if (!resolved) {
                                    resolved = true;
                                    resolve(false);
                                }
                            }, 2000);
                        });

                        if (exists) {
                            return fileUrl;
                        }
                    } catch (e) {
                        // Continue to next extension/pattern
                        continue;
                    }
                }
            }

            return null;
        },

        // Re-acquire thumbnail from video page
        async reacquireThumbnail(video) {
            if (!video || !video.url) return null;

            // First try to restore from local files
            const localThumb = await this.restoreLocalThumbnail(video);
            if (localThumb) {
                return localThumb;
            }

            // Add delay to avoid overwhelming the server
            await wait(500);

            // Try to fetch from video page
            try {
                // Construct video page URL if needed
                let videoUrl = video.url;
                if (!videoUrl.startsWith('http')) {
                    videoUrl = videoUrl.startsWith('/') ? `https://recu.me${videoUrl}` : `https://recu.me/${videoUrl}`;
                }

                const res = await fetch(videoUrl);
                if (!res.ok) return null;

                const html = await res.text();
                const doc = new DOMParser().parseFromString(html, 'text/html');

                // Try multiple selectors for thumbnail
                const splash = doc.querySelector('.video-splash[data-src]');
                if (splash && splash.dataset.src) {
                    const newThumb = splash.dataset.src;
                    if (newThumb && newThumb !== NO_THUMB && !State.brokenThumbs.has(newThumb)) {
                        // Verify new thumbnail is valid
                        const isValid = await this.checkThumbnailValid(newThumb);
                        if (isValid) {
                            return newThumb;
                        }
                    }
                }

                // Try alternative selector
                const img = doc.querySelector('.video-thumb img[src], .video-splash img[src]');
                if (img && img.src && img.src !== NO_THUMB) {
                    const newThumb = img.src;
                    if (!State.brokenThumbs.has(newThumb)) {
                        const isValid = await this.checkThumbnailValid(newThumb);
                        if (isValid) {
                            return newThumb;
                        }
                    }
                }

                // Try to get "Playing now" thumbnail from slider-body as fallback
                const sliderBody = doc.querySelector('.slider-body');
                if (sliderBody) {
                    // Look for video with "Playing now" message
                    const playingNowMessages = sliderBody.querySelectorAll('.message');
                    for (const message of playingNowMessages) {
                        if (message.textContent.trim().toLowerCase().includes('playing now')) {
                            // Find the video-thumb container
                            const videoThumb = message.closest('.video-thumb');
                            if (videoThumb) {
                                const playingNowSplash = videoThumb.querySelector('.video-splash[data-src]');
                                if (playingNowSplash && playingNowSplash.dataset.src) {
                                    const playingThumb = playingNowSplash.dataset.src;
                                    if (playingThumb && playingThumb !== NO_THUMB && !State.brokenThumbs.has(playingThumb)) {
                                        const isValid = await this.checkThumbnailValid(playingThumb);
                                        if (isValid) {
                                            return playingThumb;
                                        }
                                    }
                                }
                            }
                            break; // Found one, no need to continue
                        }
                    }
                }

                return null;
            } catch (e) {
                console.error(`[Titan] Failed to re-acquire thumbnail for ${video.user}/${video.id}:`, e);
                return null;
            }
        },

        async autoSaveThumb(video) {
            if (!video || !video.thumb || video.thumb === NO_THUMB) return false;
            if (!CONFIG.autoSaveThumbnails) return false;

            const cacheKey = this.getCacheKey(video);

            // Already downloaded
            if (State.downloadedThumbs.has(cacheKey)) {
                return true;
            }

            const thumbUrl = video.thumb;

            // Skip validation for blob URLs and file:// URLs (already valid)
            // Only validate HTTP/HTTPS URLs, and do it quickly
            let isValid = true;
            if (thumbUrl.startsWith('http://') || thumbUrl.startsWith('https://')) {
                // Quick check - skip if already marked broken, otherwise assume valid for speed
                if (State.brokenThumbs.has(thumbUrl)) {
                    return false;
                }
                // Skip validation during auto-save for speed - download will fail gracefully if broken
                isValid = true;
            }

            const filename = this.getDownloadFilename(video, thumbUrl);

            return new Promise((resolve) => {
                GM_download({
                    url: thumbUrl,
                    name: filename,
                    saveAs: false,
                    onload: () => {
                        State.downloadedThumbs.add(cacheKey);
                        this.saveCache();
                        resolve(true);
                    },
                    onerror: (err) => {
                        console.error(`[Titan] Download failed: ${filename}`, err);
                        resolve(false);
                    }
                });
            });
        },

        async batchSave(videos, progressCallback) {
            let saved = 0;
            let skipped = 0;
            const CONCURRENT_DOWNLOADS = 5; // Process multiple thumbnails concurrently

            // Filter out already downloaded videos first
            const videosToDownload = videos.filter(v => !this.isAlreadyDownloaded(v));
            skipped = videos.length - videosToDownload.length;

            // Process in concurrent batches
            for (let i = 0; i < videosToDownload.length; i += CONCURRENT_DOWNLOADS) {
                const batch = videosToDownload.slice(i, i + CONCURRENT_DOWNLOADS);
                const batchPromises = batch.map(video => this.autoSaveThumb(video));

                const results = await Promise.all(batchPromises);
                saved += results.filter(r => r === true).length;

                if (progressCallback) {
                    const current = Math.min(i + CONCURRENT_DOWNLOADS, videosToDownload.length);
                    progressCallback(current + skipped, videos.length, saved, skipped, 0, 0);
                }

                // Small delay between batches only
                if (i + CONCURRENT_DOWNLOADS < videosToDownload.length) {
                    await wait(10);
                }
            }

            this.saveCache({ immediate: true });
            return { saved, skipped, invalid: 0, reacquired: 0 };
        },

        clearCache() {
            if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
            State.downloadedThumbs.clear();
            State.brokenThumbs.clear();
            GM_deleteValue(this.CACHE_KEY);
            GM_deleteValue(this.BROKEN_THUMB_KEY);
            alert('Thumbnail cache cleared!');
        }
    };


    // ============================================================
    // 5. SEARCH HISTORY MANAGEMENT
    // ============================================================
    const SearchHistory = {
        KEY: 'search_history_v2',

        init() {
            try {
                const cached = GM_getValue(this.KEY);
                if (cached) {
                    State.searchHistory = JSON.parse(cached);
                }
            } catch (e) {
                State.searchHistory = [];
            }
        },

        save() {
            GM_setValue(this.KEY, JSON.stringify(State.searchHistory));
        },

        add(searchEntry) {
            // searchEntry = { timestamp, tagGroups, filters, depth, results, resultsCount }
            const entry = {
                id: Date.now(),
                timestamp: new Date().toISOString(),
                tagGroups: JSON.parse(JSON.stringify(searchEntry.tagGroups)),
                filters: JSON.parse(JSON.stringify(searchEntry.filters)),
                depth: searchEntry.depth,
                results: searchEntry.results,
                resultsCount: searchEntry.results.length,
                label: this.generateLabel(searchEntry.tagGroups)
            };

            State.searchHistory.unshift(entry);

            // Limit history size
            if (State.searchHistory.length > CONFIG.maxSearchHistory) {
                State.searchHistory = State.searchHistory.slice(0, CONFIG.maxSearchHistory);
            }

            State.currentHistoryIndex = 0;
            this.save();
            return entry;
        },

        generateLabel(tagGroups) {
            return tagGroups.map(g => {
                if (g.tags.length > 2) {
                    return `${g.tags.slice(0, 2).join('/')}+${g.tags.length - 2}`;
                }
                return g.tags.join('/');
            }).join(' & ');
        },

        get(index) {
            return State.searchHistory[index] || null;
        },

        remove(index) {
            State.searchHistory.splice(index, 1);
            this.save();
        },

        clear() {
            State.searchHistory = [];
            State.currentHistoryIndex = -1;
            this.save();
        },

        mergeResults(indices) {
            // Merge results from multiple history entries
            const mergedResults = [];
            indices.forEach(idx => {
                const entry = this.get(idx);
                if (entry && entry.results) {
                    mergedResults.push(...entry.results);
                }
            });
            return mergeVideoLists(mergedResults);
        }
    };

    // ============================================================
    // 6. BACKUP & PERSISTENCE
    // ============================================================
    const Backup = {
        KEY: 'autosave_master',
        FAV_KEY: 'favorites',
        THUMB_CACHE_KEY: 'thumb_cache_v5',

        saveLocal: () => {
            const data = {
                ts: Date.now(),
                groups: State.tagGroups,
                filters: getCurrentFilters()
            };
            GM_setValue(Backup.KEY, JSON.stringify(data));
        },

        saveFavorites: () => {
            GM_setValue(Backup.FAV_KEY, JSON.stringify(State.favorites));
            const thumbCacheToSave = {};
            State.favorites.forEach(username => {
                if (State.thumbCache[username]) {
                    thumbCacheToSave[username] = State.thumbCache[username];
                }
            });
            GM_setValue(Backup.THUMB_CACHE_KEY, JSON.stringify(thumbCacheToSave));
        },

        loadFavorites: () => {
            try {
                const favRaw = GM_getValue(Backup.FAV_KEY);
                if (favRaw) State.favorites = JSON.parse(favRaw) || [];
                const thumbRaw = GM_getValue(Backup.THUMB_CACHE_KEY);
                if (thumbRaw) State.thumbCache = JSON.parse(thumbRaw) || {};
            } catch (e) {
                State.favorites = [];
                State.thumbCache = {};
            }
        },

        restoreLocal: () => {
            const raw = GM_getValue(Backup.KEY);
            if (raw) {
                try {
                    const data = JSON.parse(raw);
                    if (Date.now() - data.ts < CONFIG.cacheExpiry) {
                        if (data.groups) State.tagGroups = data.groups;
                        if (data.filters) applyFiltersToUI(applyStartupFilterDefaults(data.filters));
                    }
                } catch (e) { console.error("Restore failed", e); }
            }
            Backup.loadFavorites();
        },

        exportToFile: () => {
            const data = {
                version: '19.0 Titan',
                date: new Date().toISOString(),
                filters: getCurrentFilters(),
                groups: State.tagGroups,
                favorites: State.favorites,
                thumbCache: State.thumbCache,
                searchHistory: State.searchHistory
            };
            const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `titan_backup_${new Date().toISOString().slice(0, 10)}.json`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
        },

        importFromFile: (file) => {
            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const data = JSON.parse(e.target.result);
                    if (data.groups) State.tagGroups = data.groups;
                    if (data.filters) applyFiltersToUI(data.filters);
                    if (data.favorites) State.favorites = data.favorites;
                    if (data.thumbCache) State.thumbCache = data.thumbCache;
                    if (data.searchHistory) State.searchHistory = data.searchHistory;
                    if (window.renderChips) window.renderChips();
                    if (window.renderHistoryPanel) window.renderHistoryPanel();
                    Backup.saveLocal();
                    Backup.saveFavorites();
                    SearchHistory.save();
                    alert(`Imported ${State.tagGroups.length} groups, ${State.favorites.length} favorites, and ${State.searchHistory.length} history entries.`);
                } catch (err) { alert("Invalid Backup File"); }
            };
            reader.readAsText(file);
        }
    };

    // ============================================================
    // 6.5. SEARCH INDEXER & RESUME
    // ============================================================
    const SearchIndexer = {
        KEY: 'active_search_index_v1',
        _saveTimer: null,
        _pendingState: null,

        _flush() {
            const searchState = this._pendingState;
            if (!searchState) return;
            this._pendingState = null;
            try {
                const toSave = {
                    ...searchState,
                    completedTags: searchState.completedTags instanceof Set
                        ? Array.from(searchState.completedTags)
                        : (Array.isArray(searchState.completedTags) ? searchState.completedTags : []),
                    timestamp: Date.now()
                };
                GM_setValue(this.KEY, JSON.stringify(toSave));
            } catch (e) {
                console.error('[Titan] Failed to save search index:', e);
            }
        },

        save(searchState, { immediate = false } = {}) {
            this._pendingState = searchState;
            if (immediate) {
                if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
                this._flush();
                return;
            }
            if (this._saveTimer) return;
            this._saveTimer = setTimeout(() => {
                this._saveTimer = null;
                this._flush();
            }, 400);
        },

        load() {
            try {
                const raw = GM_getValue(this.KEY);
                if (!raw) return null;
                const data = JSON.parse(raw);
                // Only return if search is less than 24 hours old
                if (Date.now() - data.timestamp < 1000 * 60 * 60 * 24) {
                    return data;
                }
                // Clear old search
                this.clear();
                return null;
            } catch (e) {
                return null;
            }
        },

        clear() {
            if (this._saveTimer) { clearTimeout(this._saveTimer); this._saveTimer = null; }
            this._pendingState = null;
            GM_deleteValue(this.KEY);
            State.activeSearch = null;
        },

        updateProgress(groupIndex, tagIndex, completedPages, totalPages) {
            if (!State.activeSearch) return;

            const tagKey = `${groupIndex}_${tagIndex}`;
            State.activeSearch.progress[tagKey] = {
                completedPages,
                totalPages,
                lastUpdate: Date.now()
            };
            this.save(State.activeSearch);
        },

        markTagComplete(groupIndex, tagIndex) {
            if (!State.activeSearch) return;
            const tagKey = `${groupIndex}_${tagIndex}`;
            if (!State.activeSearch.completedTags) {
                State.activeSearch.completedTags = new Set();
            }
            State.activeSearch.completedTags.add(tagKey);
            this.save(State.activeSearch, { immediate: true });
        },

        isTagComplete(groupIndex, tagIndex) {
            if (!State.activeSearch || !State.activeSearch.completedTags) return false;
            return State.activeSearch.completedTags.has(`${groupIndex}_${tagIndex}`);
        },

        getTagProgress(groupIndex, tagIndex) {
            if (!State.activeSearch) return null;
            const tagKey = `${groupIndex}_${tagIndex}`;
            return State.activeSearch.progress[tagKey] || null;
        }
    };

    const Memory = {
        save: (key, data) => {
            try { localStorage.setItem(CONFIG.storagePrefix + 'cache_' + key, JSON.stringify({ ts: Date.now(), data })); }
            catch (e) {
                Object.keys(localStorage).forEach(storageKey => {
                    if (storageKey.startsWith(CONFIG.storagePrefix)) {
                        localStorage.removeItem(storageKey);
                    }
                });
            }
        },
        load: (key) => {
            const raw = localStorage.getItem(CONFIG.storagePrefix + 'cache_' + key);
            if (!raw) return null;
            try {
                const parsed = JSON.parse(raw);
                return (Date.now() - parsed.ts) < CONFIG.cacheExpiry ? parsed.data : null;
            } catch (e) {
                localStorage.removeItem(CONFIG.storagePrefix + 'cache_' + key);
                return null;
            }
        },
        clear: () => {
            Object.keys(localStorage).forEach(k => { if (k.startsWith(CONFIG.storagePrefix)) localStorage.removeItem(k); });
            alert('Cache cleared.');
        }
    };

    // ============================================================
    // 7. ENGINE & HELPERS
    // ============================================================
    function parseDuration(str) {
        if (!str) return 0;
        const parts = str.split(':').map(Number);
        if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
        if (parts.length === 2) return parts[0] * 60 + parts[1];
        return 0;
    }

    function normalizeList(values) {
        return [...new Set((values || []).filter(Boolean))];
    }

    function sortVideos(videos) {
        return [...videos].sort((a, b) => {
            if ((a.foundTimestamp || 0) !== (b.foundTimestamp || 0)) {
                return (b.foundTimestamp || 0) - (a.foundTimestamp || 0);
            }
            return (a.foundPage || 999) - (b.foundPage || 999);
        });
    }

    function mergeVideoRecords(existing, incoming) {
        const merged = {
            ...existing,
            ...incoming,
            title: existing.title || incoming.title,
            url: incoming.url || existing.url,
            thumb: incoming.thumb && incoming.thumb !== NO_THUMB ? incoming.thumb : existing.thumb,
            duration: incoming.duration || existing.duration,
            durationSec: incoming.durationSec || existing.durationSec || 0,
            foundTags: normalizeList([...(existing.foundTags || []), ...(incoming.foundTags || [])]),
            searchedTags: normalizeList([...(existing.searchedTags || []), ...(incoming.searchedTags || [])]),
            allTags: normalizeList([...(existing.allTags || []), ...(incoming.allTags || [])])
        };

        merged.foundPage = Math.min(existing.foundPage || 999, incoming.foundPage || 999);
        merged.foundTimestamp = Math.max(existing.foundTimestamp || 0, incoming.foundTimestamp || 0);

        return merged;
    }

    function mergeVideoLists(...lists) {
        const merged = new Map();

        lists.flat().forEach(video => {
            if (!video) return;
            const key = video.user || video.id;
            if (!key) return;

            if (merged.has(key)) {
                merged.set(key, mergeVideoRecords(merged.get(key), video));
                return;
            }

            merged.set(key, {
                ...video,
                foundTags: normalizeList(video.foundTags),
                searchedTags: normalizeList(video.searchedTags),
                allTags: normalizeList(video.allTags)
            });
        });

        return sortVideos(Array.from(merged.values()));
    }

    function getCurrentFilters() {
        const filters = {};
        Object.keys(FILTER_OPTIONS).forEach(key => {
            const el = document.getElementById(`v16-opt-${key}`);
            filters[key] = el ? el.value : getDefaultFilterValue(key);
        });
        return filters;
    }

    function isTagSearchSectionSupported(filters) {
        return filters.section === 'videos' || filters.section === 'clips';
    }

    function applyStartupFilterDefaults(filters) {
        return {
            ...filters,
            gender: getDefaultFilterValue('gender')
        };
    }

    function applyFiltersToUI(filters) {
        Object.keys(filters).forEach(key => {
            const el = document.getElementById(`v16-opt-${key}`);
            if (el) el.value = filters[key];
        });
    }

    function isLiveStreamUrl(url) {
        return url && (url.includes('highmedia-stream') || /[?&]c=/.test(url));
    }

    function extractTValue(url) {
        const match = url.match(/[?&]t=(\d+)/);
        return match ? match[1] : null;
    }

    function buildLiveThumbUrl(url, newCValue) {
        const tValue = extractTValue(url);
        const room = url.match(/room=([^&]+)/)?.[1];
        if (room && tValue) {
            return `https://highmedia-stream.b-cdn.net/stream?room=${room}&t=${tValue}&c=${newCValue}`;
        }
        if (url.includes('&c=')) {
            return url.replace(/&c=\d+/, `&c=${newCValue}`);
        } else if (url.includes('?c=')) {
            return url.replace(/\?c=\d+/, `?c=${newCValue}`);
        }
        return url;
    }

    function getSpritePosition(frame) {
        const col = frame % CONFIG.spriteColumns;
        const row = Math.floor(frame / CONFIG.spriteColumns);
        const xPercent = (col / (CONFIG.spriteColumns - 1)) * 100;
        const yPercent = (row / (CONFIG.spriteRows - 1)) * 100;
        return `${xPercent}% ${yPercent}%`;
    }

    async function fetchStaticThumb(username) {
        try {
            const res = await fetch(`/performer/${username}/popular`);
            const html = await res.text();
            const doc = new DOMParser().parseFromString(html, 'text/html');
            const splash = doc.querySelector('.video-splash[data-src]');
            if (splash) {
                const thumbUrl = splash.dataset.src;
                if (thumbUrl && !thumbUrl.startsWith('blob:')) {
                    return thumbUrl;
                }
            }
            const img = doc.querySelector('.video-thumb img[src]');
            if (img && img.src && !img.src.startsWith('blob:')) {
                return img.src;
            }
            return null;
        } catch (e) {
            console.error(`[Titan] Failed to fetch static thumb for ${username}:`, e);
            return null;
        }
    }

    async function fetchSuggestions(query) {
        const normalizedQuery = (query || '').trim().toLowerCase();
        if (normalizedQuery.length < 2) return [];
        if (suggestionCache.has(normalizedQuery)) return suggestionCache.get(normalizedQuery);

        try {
            const res = await fetch(`/performers/search-suggest/${encodeURIComponent(normalizedQuery)}`, { method: 'POST' });
            const html = await res.text();
            const doc = new DOMParser().parseFromString(html, 'text/html');
            const items = [];
            doc.querySelectorAll('a[href*="/tag:"]').forEach(a => {
                const val = a.innerText.trim().replace('#', '');
                if (!items.find(i => i.val === val)) items.push({ type: 'tag', val, label: '#' + val });
            });
            suggestionCache.set(normalizedQuery, items);
            return items;
        } catch (e) {
            suggestionCache.set(normalizedQuery, []);
            return [];
        }
    }

    // ============================================================
    // 8. SCANNERS
    // ============================================================
    async function scanLivePerformers(filters, depth, logCallback) {
        const performers = new Map();
        let baseUrl = `/live/gender:${filters.gender}/ethnicity:${filters.ethnicity}/age:${filters.age}/body:${filters.body}`;

        for (let page = 1; page <= depth; page++) {
            logCallback(`🔴 Scanning live performers (Page ${page}/${depth})...`);
            const url = page > 1 ? `${baseUrl}/page/${page}` : baseUrl;

            try {
                const res = await fetch(url);
                const html = await res.text();
                const doc = new DOMParser().parseFromString(html, 'text/html');
                let found = 0;

                doc.querySelectorAll('a[href*="/performer/"]').forEach(link => {
                    const href = link.getAttribute('href');
                    const match = href.match(/\/performer\/([^\/]+)/);

                    if (match) {
                        const username = match[1];
                        if (!performers.has(username)) {
                            const splashDiv = link.querySelector('.video-splash');
                            let thumb = null;
                            if (splashDiv) {
                                thumb = splashDiv.dataset.src || splashDiv.dataset.bg || null;
                                if (!thumb && splashDiv.style.backgroundImage) {
                                    const bgMatch = splashDiv.style.backgroundImage.match(/url\(['"]?([^'"]+)['"]?\)/);
                                    if (bgMatch) thumb = bgMatch[1];
                                }
                            }

                            if (thumb) {
                                const isLive = isLiveStreamUrl(thumb);
                                const type = isLive ? 'live' : 'sprite';
                                State.thumbCache[username] = { url: thumb, timestamp: Date.now(), type };
                                performers.set(username, {
                                    username,
                                    url: href,
                                    thumb,
                                    thumbType: type,
                                    isLive: true,
                                    isFavorite: State.favorites.includes(username)
                                });
                                found++;
                            }
                        }
                    }
                });
                logCallback(`🔴 Page ${page}: Found ${found} performers (Total: ${performers.size})`);
                if (found === 0 && page > 1) break;
                await wait(300);
            } catch (e) { break; }
        }
        Backup.saveFavorites();
        return Array.from(performers.values());
    }

    // Find the maximum page number that exists for a tag using binary search
    async function findMaxPage(tag, baseUrl, logCallback) {
        logCallback(`🔍 Detecting max pages for <b>#${tag}</b>...`);

        // Helper to check if a page exists
        const checkPage = async (page) => {
            const url = `${baseUrl}/page/${page}`;
            try {
                const res = await fetch(url, { method: 'GET', redirect: 'manual' });
                // 200 = page exists, 302/404 = page doesn't exist
                if (res.status === 200) {
                    // Verify it actually has content by checking for video thumbs
                    const html = await res.text();
                    // Empty-state marker: site explicitly says no results match
                    if (isEmptyResultsPage(html) || isChallengePage(html)) return false;
                    const doc = new DOMParser().parseFromString(html, 'text/html');
                    // Exclude trending-performers-videos widget (appears on every page)
                    const realThumbs = Array.from(doc.querySelectorAll('.video-thumb[data-performer]'))
                        .filter(t => !t.closest('.trending-performers-videos'));
                    return realThumbs.length > 0;
                }
                // 302 redirect means page doesn't exist
                return false;
            } catch (e) {
                return false;
            }
        };

        // Start at page 500
        let testPage = 500;
        let exists = await checkPage(testPage);
        await wait(100); // Small delay between checks

        if (!exists) {
            // Page 500 doesn't exist, search down
            logCallback(`📉 Page 500 not found, searching down...`);
            const firstPageExists = await checkPage(1);
            await wait(100);
            if (!firstPageExists) {
                logCallback(`⚠️ No matching pages detected for <b>#${tag}</b>`);
                return 0;
            }

            let low = 1;
            let high = 500;
            let maxFound = 1;

            while (low <= high) {
                const mid = Math.floor((low + high) / 2);
                const midExists = await checkPage(mid);
                await wait(100);

                if (midExists) {
                    maxFound = mid;
                    low = mid + 1; // Search higher
                } else {
                    high = mid - 1; // Search lower
                }
            }

            logCallback(`✅ Found max page: ${maxFound}`);
            return maxFound;
        } else {
            // Page 500 exists, search up to find the maximum
            logCallback(`📈 Page 500 exists, searching up for maximum...`);
            let low = 500;
            let high = 5000; // Start with reasonable upper bound
            let maxFound = 500;

            // First, find an upper bound by doubling
            while (high < 10000) {
                const testHigh = high;
                const highExists = await checkPage(testHigh);
                await wait(100);

                if (highExists) {
                    maxFound = testHigh;
                    high = testHigh * 2; // Double and try again
                } else {
                    break; // Found upper bound
                }
            }

            // Now binary search between maxFound and high
            low = maxFound;
            while (low <= high) {
                const mid = Math.floor((low + high) / 2);
                const midExists = await checkPage(mid);
                await wait(100);

                if (midExists) {
                    maxFound = mid;
                    low = mid + 1; // Search higher
                } else {
                    high = mid - 1; // Search lower
                }
            }

            logCallback(`✅ Found max page: ${maxFound}`);
            return maxFound;
        }
    }

    // Analyze max pages for all tags and display recommendations
    async function analyzeTagPages(tagGroups, filters, updateCallback) {
        const results = [];
        const allTags = normalizeList(tagGroups.flatMap(g => g.tags.map(tag => tag.replace('#', '').trim()).filter(Boolean)));

        const sectionPrefix = filters.section === 'clips' ? '/clips' : '';
        let baseUrl = `${sectionPrefix}/type:${filters.type}`;
        baseUrl += `/period:${filters.period}`;
        baseUrl += `/gender:${filters.gender}/ethnicity:${filters.ethnicity}/age:${filters.age}/body:${filters.body}`;

        for (let i = 0; i < allTags.length; i++) {
            const tag = allTags[i];
            const tagBaseUrl = `${baseUrl}/tag:${encodeURIComponent(tag)}`;

            updateCallback(`Analyzing #${tag} (${i + 1}/${allTags.length})...`);

            try {
                const maxPage = await findMaxPage(tag, tagBaseUrl, (msg) => {
                    // Silent during analysis
                });

                results.push({ tag, maxPage: maxPage || 0 });
            } catch (e) {
                console.error(`[Titan] Error analyzing #${tag}:`, e);
                results.push({ tag, maxPage: 0, error: true });
            }

            // Small delay between tags
            await wait(200);
        }

        return results;
    }

    async function scanVideos(tag, depth, logCallback, resumeFromPage = null, tagMeta = null) {
        const filters = getCurrentFilters();
        tag = tag.replace('#', '').trim();
        const cacheKey = `${tag}_${filters.section}_${filters.type}_${filters.period}_${filters.gender}_${filters.ethnicity}_${filters.age}_${filters.body}`;
        const pageProgressKey = `page_${cacheKey}`;
        const maxPageKey = `maxpage_${cacheKey}`;
        const progressMeta = tagMeta && Number.isInteger(tagMeta.groupIndex) && Number.isInteger(tagMeta.tagIndex) ? tagMeta : null;

        // Check for cached results
        const cached = Memory.load(cacheKey);
        const lastPage = Memory.load(pageProgressKey);
        const lastPageNum = lastPage ? parseInt(lastPage, 10) : 0;

        // Check for cached max page
        let cachedMaxPage = Memory.load(maxPageKey);
        let actualMaxPage = cachedMaxPage ? parseInt(cachedMaxPage, 10) : null;

        // Determine start page:
        // 1. If resumeFromPage is explicitly provided (from SearchIndexer), use it
        // 2. If depth increased and we have a lastPage, continue from lastPage + 1
        // 3. Otherwise start from page 1
        let startPage = 1;
        if (resumeFromPage !== null && resumeFromPage !== undefined) {
            startPage = parseInt(resumeFromPage, 10);
        } else if (depth > lastPageNum && lastPageNum > 0) {
            startPage = lastPageNum + 1;
        } else {
            startPage = 1;
        }

        // Ensure startPage is at least 1
        startPage = Math.max(1, startPage);

        // If we have cached results and depth hasn't increased, return cached
        if (cached && depth <= lastPageNum) {
            logCallback(`<span style="color:#4ade80">⚡ Loaded #${tag} from memory (${cached.length} performers)</span>`);
            return cached.map(v => ({
                ...v,
                foundTags: v.foundTags ? [...new Set([...v.foundTags, tag])] : [tag]
            }));
        }

        // Load existing performers from cache if resuming
        const performers = new Map(); // Key by performer username
        if (cached) {
            cached.forEach(v => {
                performers.set(v.user, { ...v });
            });
        }

        const sectionPrefix = filters.section === 'clips' ? '/clips' : '';
        let baseUrl = `${sectionPrefix}/type:${filters.type}`;
        baseUrl += `/period:${filters.period}`;
        baseUrl += `/gender:${filters.gender}/ethnicity:${filters.ethnicity}/age:${filters.age}/body:${filters.body}`;
        baseUrl += `/tag:${encodeURIComponent(tag)}`;

        // Detect actual max page if not cached or if depth is high
        if (!actualMaxPage && depth >= 100) {
            actualMaxPage = await findMaxPage(tag, baseUrl, logCallback);
            if (actualMaxPage) {
                Memory.save(maxPageKey, actualMaxPage);
                // Adjust depth to not exceed actual max
                depth = Math.min(depth, actualMaxPage);
                logCallback(`📊 Using detected max page: ${actualMaxPage} (adjusted depth: ${depth})`);
            }
        } else if (actualMaxPage) {
            // Use cached max page to limit depth
            depth = Math.min(depth, actualMaxPage);
        }

        let lastSuccessfulPage = startPage - 1;
        let emptyPageStreak = 0;
        const concurrentPages = CONFIG.concurrentPages || 1;
        let shouldStop = false;
        let interruptedReason = null;

        const pauseSearch = (reason) => {
            interruptedReason = reason;
            State.searchAbortReason = reason;
            if (progressMeta) {
                SearchIndexer.updateProgress(progressMeta.groupIndex, progressMeta.tagIndex, lastSuccessfulPage, depth);
            }
            if (State.activeSearch) {
                State.activeSearch.pauseReason = reason;
                SearchIndexer.save(State.activeSearch);
            }
            shouldStop = true;
        };

        // Helper function to fetch and process a single page
        const fetchPage = async (page) => {
            if (shouldStop) return null;

            logCallback(`📡 Scanning <b>#${tag}</b> (Page ${page}/${depth})...`);
            const url = `${baseUrl}/page/${page}`;

            try {
                const res = await fetch(url);
                if (!res.ok) {
                    // Check for 403 Forbidden - refresh and resume
                    if (res.status === 403) {
                        logCallback(`🔄 403 Forbidden detected - search paused for resume.`);
                        pauseSearch('HTTP 403 / Cloudflare block');
                        return null; // Return null to stop current execution
                    }
                    logCallback(`⚠️ Page ${page} returned ${res.status} - continuing to next tag`);
                    shouldStop = true;
                    return null;
                }

                const html = await res.text();
                if (isChallengePage(html)) {
                    logCallback(`🔄 Cloudflare challenge detected - search paused for resume.`);
                    pauseSearch('Cloudflare challenge page');
                    return null;
                }
                // Empty-state marker: stop scanning this tag immediately
                if (isEmptyResultsPage(html)) {
                    if (page === startPage) {
                        logCallback(`<span style="color:#fbbf24">⚠️ <b>#${tag}</b> has no results matching current filters</span>`);
                    }
                    shouldStop = true;
                    return { page, results: [], found: 0, emptyState: true };
                }
                const doc = new DOMParser().parseFromString(html, 'text/html');
                // Exclude trending-performers-videos widget (appears on every page even with no results)
                const videoThumbs = Array.from(doc.querySelectorAll('.video-thumb[data-performer]'))
                    .filter(t => !t.closest('.trending-performers-videos'));
                const pageResults = [];

                videoThumbs.forEach(thumbEl => {
                    const performer = thumbEl.getAttribute('data-performer');
                    if (!performer) return;

                    // Only keep first video per performer
                    if (!performers.has(performer)) {
                        const link = thumbEl.querySelector('a[href*="/video/"]');
                        if (!link) return;

                        const href = link.getAttribute('href');
                        const match = href.match(/(?:\/([^\/]+))?\/video\/(\d+)/);
                        if (!match) return;

                        const id = match[2];
                        const user = match[1] || performer;

                        const splashEl = link.querySelector('.video-splash');
                        let thumb = splashEl ? (splashEl.dataset.src || NO_THUMB) : NO_THUMB;
                        if (thumb.includes('url(')) thumb = thumb.slice(5, -2);

                        // Try to get title from link title attribute, or use performer name as fallback
                        let title = link.getAttribute('title') || link.getAttribute('data-title') || `${performer} video`;
                        if (!title || title.trim() === '') {
                            title = `${performer} video`;
                        }

                        const timeEl = thumbEl.querySelector('.video-time');
                        const durationStr = timeEl ? timeEl.innerText.replace('HD', '').replace(/<[^>]*>/g, '').trim() : '00:00';

                        const videoObj = {
                            id,
                            url: href,
                            title,
                            thumb,
                            user: performer,
                            duration: durationStr,
                            durationSec: parseDuration(durationStr),
                            foundTags: [tag], // Track which tag found this performer
                            foundPage: page, // Track which page this was found on
                            foundTimestamp: Date.now() // Track when it was found
                        };
                        pageResults.push({ performer, videoObj });
                    } else {
                        // Performer already exists, add this tag to its foundTags
                        const existing = performers.get(performer);
                        if (!existing.foundTags.includes(tag)) {
                            existing.foundTags.push(tag);
                        }
                        // Update page and timestamp if found on an earlier page (newer content)
                        if (page < (existing.foundPage || Infinity)) {
                            existing.foundPage = page;
                            existing.foundTimestamp = Date.now();
                        } else if (page === existing.foundPage) {
                            // Same page, update timestamp to most recent
                            existing.foundTimestamp = Date.now();
                        }
                    }
                });

                return { page, results: pageResults, found: pageResults.length };
            } catch (e) {
                // Log error but continue to next tag instead of breaking entire search
                console.error(`[Titan] Error scanning #${tag} page ${page}:`, e);
                logCallback(`⚠️ Error on page ${page} for <b>#${tag}</b> - continuing to next tag`);
                shouldStop = true;
                return null;
            }
        };

        // Process pages with concurrency
        if (concurrentPages > 1) {
            // Concurrent page fetching
            for (let page = startPage; page <= depth && !shouldStop; page += concurrentPages) {
                const pageBatch = [];
                for (let i = 0; i < concurrentPages && (page + i) <= depth; i++) {
                    pageBatch.push(fetchPage(page + i));
                }

                const results = await Promise.all(pageBatch);

                // Process results in order
                for (const result of results) {
                    if (!result || shouldStop) {
                        shouldStop = true;
                        break;
                    }

                    // Add new performers to the map
                    result.results.forEach(({ performer, videoObj }) => {
                        if (!performers.has(performer)) {
                            performers.set(performer, videoObj);
                        }
                    });

                    // If no results found and we're past page 1, check if we've reached the max
                    // Only stop if we're at or past the detected max page, or if we're significantly past depth
                    if (result.found === 0 && result.page > 1) {
                        emptyPageStreak++;
                        const shouldStopEarly = actualMaxPage
                            ? (result.page >= actualMaxPage)
                            : (emptyPageStreak >= 2);

                        if (shouldStopEarly) {
                            logCallback(`ℹ️ <b>#${tag}</b> has no more pages (stopped at page ${result.page - 1}${actualMaxPage ? `, max detected: ${actualMaxPage}` : ''}) - continuing with other tags...`);
                            shouldStop = true;
                            break;
                        } else {
                            // Empty page but not at max yet - might be a gap, continue
                            logCallback(`⚠️ <b>#${tag}</b> page ${result.page} is empty, but continuing (max: ${actualMaxPage || 'unknown'})...`);
                        }
                    } else {
                        emptyPageStreak = 0;
                    }

                    lastSuccessfulPage = Math.max(lastSuccessfulPage, result.page);
                    if (progressMeta) {
                        SearchIndexer.updateProgress(progressMeta.groupIndex, progressMeta.tagIndex, lastSuccessfulPage, depth);
                    }
                }

                // Small delay between batches to avoid overwhelming the server
                if (!shouldStop && (page + concurrentPages) <= depth) {
                    await wait(50);
                }
            }
        } else {
            // Sequential page fetching (original behavior)
            for (let page = startPage; page <= depth && !shouldStop; page++) {
                const result = await fetchPage(page);

                if (!result || shouldStop) {
                    break;
                }

                // Add new performers to the map
                result.results.forEach(({ performer, videoObj }) => {
                    if (!performers.has(performer)) {
                        performers.set(performer, videoObj);
                    }
                });

                // If no results found and we're past page 1, check if we've reached the max
                // Only stop if we're at or past the detected max page, or if we're significantly past depth
                if (result.found === 0 && result.page > 1) {
                    emptyPageStreak++;
                    const shouldStopEarly = actualMaxPage
                        ? (result.page >= actualMaxPage)
                        : (emptyPageStreak >= 2);

                    if (shouldStopEarly) {
                        logCallback(`ℹ️ <b>#${tag}</b> has no more pages (stopped at page ${result.page - 1}${actualMaxPage ? `, max detected: ${actualMaxPage}` : ''}) - continuing with other tags...`);
                        break;
                    } else {
                        // Empty page but not at max yet - might be a gap, continue
                        logCallback(`⚠️ <b>#${tag}</b> page ${result.page} is empty, but continuing (max: ${actualMaxPage || 'unknown'})...`);
                    }
                } else {
                    emptyPageStreak = 0;
                }

                lastSuccessfulPage = result.page;
                if (progressMeta) {
                    SearchIndexer.updateProgress(progressMeta.groupIndex, progressMeta.tagIndex, lastSuccessfulPage, depth);
                }
                // Reduced delay for faster sequential fetching
                await wait(50);
            }
        }

        // Save results and page progress
        const result = Array.from(performers.values());
        Memory.save(cacheKey, result);
        Memory.save(pageProgressKey, lastSuccessfulPage);

        if (interruptedReason) {
            throw new Error(interruptedReason);
        }

        return result;
    }

    // ============================================================
    // 9. UI & RENDERERS
    // ============================================================
    function createUI() {
        const old = document.getElementById('v16-titan');
        if (old) old.remove();

        // Initialize managers
        ThumbnailManager.init();
        SearchHistory.init();
        VideoTagManager.init();

        const ui = document.createElement('div');
        ui.id = 'v16-titan';
        ui.style.cssText = `
            background: #0f172a; border: 2px solid #a855f7; border-radius: 12px;
            padding: 25px; margin: 20px 0; color: white;
            font-family: 'Segoe UI', sans-serif; box-shadow: 0 10px 50px rgba(0,0,0,0.8);
            width: 95vw; position: relative; left: 50%; transform: translateX(-50%); z-index: 1000;
        `;

        let filterHTML = '';
        Object.keys(FILTER_OPTIONS).forEach(key => {
            const defaultValue = getDefaultFilterValue(key);
            const opts = FILTER_OPTIONS[key].map(o => `<option value="${o.v}"${o.v === defaultValue ? ' selected' : ''}>${o.l}</option>`).join('');
            filterHTML += `
                <div style="flex:1; min-width:120px;">
                    <label style="font-size:11px; color:#94a3b8; display:block; margin-bottom:4px; text-transform:uppercase;">${key}</label>
                    <select id="v16-opt-${key}" style="width:100%; background:#1e293b; border:1px solid #334155; color:white; padding:6px; border-radius:4px;">
                        ${opts}
                    </select>
                </div>
            `;
        });

        ui.innerHTML = `
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
                <h3 style="margin:0; color:#a855f7; font-size:24px; font-weight:800;">🔱 TITAN V19.0</h3>
                <div style="display:flex; gap:10px; align-items:center;">
                    <label style="font-size:13px; color:#94a3b8;">Depth: <input id="v16-depth" type="number" value="${CONFIG.defaultDepth}" style="width:40px; background:#1e293b; border:1px solid #334155; color:white; text-align:center;"></label>
                    <label style="font-size:12px; color:#94a3b8; display:flex; align-items:center; gap:5px;">
                        <input type="checkbox" id="v16-auto-save" ${CONFIG.autoSaveThumbnails ? 'checked' : ''}>
                        Auto-Save
                    </label>
                    <button id="v16-toggle-settings" style="background:none; border:1px solid #334155; color:#cbd5e1; cursor:pointer; padding:2px 8px; border-radius:4px;">⚙️</button>
                    <button id="v16-download" style="background:#0ea5e9; border:none; color:white; cursor:pointer; padding:2px 10px; border-radius:4px; font-weight:bold;">📥 Save All</button>
                </div>
            </div>

            <div id="v16-settings-panel" style="display:none; background:#020617; padding:15px; border-radius:8px; border:1px solid #334155; margin-bottom:20px;">
                <div style="display:flex; flex-wrap:wrap; gap:10px; margin-bottom:15px;">
                    ${filterHTML}
                </div>
                <div style="display:flex; justify-content:space-between; border-top:1px solid #1e293b; padding-top:10px;">
                    <div style="display:flex; gap:10px;">
                        <button id="v16-clear-cache" style="color:#f87171; background:none; border:none; cursor:pointer;">⚠️ Clear Search Cache</button>
                        <button id="v16-clear-thumb-cache" style="color:#f97316; background:none; border:none; cursor:pointer;">🗑️ Clear Thumb Cache</button>
                        <button id="v16-clear-tag-cache" style="color:#8b5cf6; background:none; border:none; cursor:pointer;">🏷️ Clear Tag Cache</button>
                        <button id="v16-clear-history" style="color:#ef4444; background:none; border:none; cursor:pointer;">🧹 Clear History</button>
                        <button id="v16-clear-all-cache" style="color:#dc2626; background:none; border:none; cursor:pointer; font-weight:bold;">🔥 Clear ALL Cache</button>
                    </div>
                    <div style="display:flex; gap:10px;">
                        <button id="v16-export" style="background:#334155; border:none; color:white; padding:5px 10px; cursor:pointer; border-radius:4px;">Export</button>
                        <input type="file" id="v16-import" style="display:none;">
                        <button onclick="document.getElementById('v16-import').click()" style="background:#334155; border:none; color:white; padding:5px 10px; cursor:pointer; border-radius:4px;">Import</button>
                    </div>
                </div>
            </div>

            <div id="v16-tabs" style="display:flex; gap:5px; margin-bottom:20px; border-bottom:1px solid #334155; padding-bottom:10px;">
                <button class="v16-tab active" data-tab="search" style="padding:10px 20px; background:#a855f7; border:none; color:white; border-radius:6px 6px 0 0; cursor:pointer; font-weight:bold;">🔍 Tag Search</button>
                <button class="v16-tab" data-tab="history" style="padding:10px 20px; background:#1e293b; border:1px solid #334155; border-bottom:none; color:#94a3b8; border-radius:6px 6px 0 0; cursor:pointer;">📜 History (<span id="v16-history-count">0</span>)</button>
                <button class="v16-tab" data-tab="live" style="padding:10px 20px; background:#1e293b; border:1px solid #334155; border-bottom:none; color:#94a3b8; border-radius:6px 6px 0 0; cursor:pointer;">🔴 Live</button>
            </div>

            <div id="v16-tab-search" class="v16-tab-content">
                <div style="display:flex; gap:15px; margin-bottom:20px; position:relative;">
                    <div style="flex:1; position:relative;">
                        <input id="v16-input" type="text" placeholder="Type tags (space separated)... Enter=OR, Shift+Enter=AND" autocomplete="off"
                            style="width:100%; padding:15px; background:#020617; border:1px solid #334155; border-radius:8px; color:white; outline:none; font-size:16px;">
                        <div id="v16-suggest" style="position:absolute; top:100%; left:0; right:0; background:#1e293b; border:1px solid #a855f7; z-index:100; max-height:250px; overflow-y:auto; display:none;"></div>
                    </div>
                    <button id="v16-smart" style="padding:0 30px; background:linear-gradient(45deg, #a855f7, #d946ef); border:none; color:white; border-radius:8px; font-weight:bold; cursor:pointer;">✨ Smart</button>
                    <button id="v16-go" style="padding:0 35px; background:#334155; border:1px solid #a855f7; color:white; border-radius:8px; font-weight:bold; cursor:pointer;">Search</button>
                </div>
                <div id="v16-chips" style="display:flex; flex-wrap:wrap; gap:10px; margin-bottom:20px; min-height:40px;"></div>
                <div id="v16-page-analysis" style="display:none; padding:15px; background:#1e293b; border:1px solid #475569; border-radius:8px; margin-bottom:15px;">
                    <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
                        <span style="color:#94a3b8; font-size:14px; font-weight:bold;">📊 Page Analysis & Recommendations</span>
                        <button id="v16-analyze-pages" style="background:#0ea5e9; border:none; color:white; padding:6px 15px; border-radius:6px; font-size:12px; cursor:pointer; font-weight:bold;">🔍 Analyze Pages</button>
                    </div>
                    <div id="v16-page-recommendations" style="font-size:13px; color:#cbd5e1;"></div>
                </div>
            </div>

            <div id="v16-tab-history" class="v16-tab-content" style="display:none;">
                <div style="margin-bottom:15px; display:flex; gap:10px; align-items:center;">
                    <span style="color:#94a3b8; font-size:14px;">📜 Search History - Click to reload results, or merge multiple searches</span>
                    <button id="v16-merge-selected" style="background:#6366f1; border:none; color:white; padding:5px 15px; border-radius:4px; cursor:pointer; display:none;">🔗 Merge Selected</button>
                </div>
                <div id="v16-history-list" style="display:flex; flex-direction:column; gap:10px; max-height:400px; overflow-y:auto;"></div>
            </div>

            <div id="v16-tab-live" class="v16-tab-content" style="display:none;">
                <div style="display:flex; gap:15px; margin-bottom:20px; align-items:center;">
                    <input id="v16-live-search" type="text" placeholder="Filter performers by name..."
                        style="flex:1; padding:15px; background:#020617; border:1px solid #334155; border-radius:8px; color:white; outline:none; font-size:16px;">
                    <button id="v16-scan-live" style="padding:15px 30px; background:linear-gradient(45deg, #ef4444, #f97316); border:none; color:white; border-radius:8px; font-weight:bold; cursor:pointer;">🔴 Scan Live</button>
                    <button id="v16-show-favorites" style="padding:15px 20px; background:#1e293b; border:2px solid #eab308; color:#eab308; border-radius:8px; font-weight:bold; cursor:pointer;">⭐ (<span id="v16-fav-count">0</span>)</button>
                </div>
                <div id="v16-live-status" style="font-size:14px; color:#cbd5e1; margin-bottom:15px;"></div>
                <div id="v16-live-grid" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:15px;"></div>
            </div>

            <div id="v16-status" style="font-size:14px; color:#cbd5e1; margin-bottom:20px; display:none; padding:15px; background:#1e293b; border-radius:8px;"></div>
            <div id="v16-resume-notification" style="display:none; padding:15px; background:linear-gradient(135deg, #f59e0b, #d97706); border:2px solid #fbbf24; border-radius:8px; margin-bottom:20px; color:#fff;">
                <div style="display:flex; justify-content:space-between; align-items:center;">
                    <div>
                        <div style="font-weight:bold; font-size:16px; margin-bottom:5px;">⏸️ Interrupted Search Detected</div>
                        <div style="font-size:13px; opacity:0.9;" id="v16-resume-info">A previous search was interrupted. Click Resume to continue from where it left off.</div>
                    </div>
                    <div style="display:flex; gap:10px;">
                        <button id="v16-resume-btn" style="background:#fff; color:#d97706; border:none; padding:8px 20px; border-radius:6px; font-weight:bold; cursor:pointer;">▶️ Resume</button>
                        <button id="v16-dismiss-resume" style="background:rgba(255,255,255,0.2); color:#fff; border:1px solid rgba(255,255,255,0.3); padding:8px 15px; border-radius:6px; cursor:pointer;">✕ Dismiss</button>
                    </div>
                </div>
            </div>
            <div id="v16-tag-fetch-progress" style="display:none; padding:10px 15px; background:#1e1b4b; border:1px solid #6366f1; border-radius:8px; margin-bottom:15px;">
                <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
                    <span style="color:#a5b4fc; font-size:13px;">🏷️ <span id="v16-tag-fetch-text">Fetching video tags...</span></span>
                    <span id="v16-tag-fetch-stats" style="color:#6366f1; font-size:12px;">0/0</span>
                </div>
                <div style="background:#312e81; border-radius:4px; height:6px; overflow:hidden;">
                    <div id="v16-tag-fetch-bar" style="background:linear-gradient(90deg, #6366f1, #8b5cf6); height:100%; width:0%; transition:width 0.3s;"></div>
                </div>
            </div>
            <div id="v16-download-progress" style="display:none; padding:15px; background:#1e293b; border-radius:8px; margin-bottom:20px;">
                <div style="display:flex; justify-content:space-between; margin-bottom:10px;">
                    <span id="v16-dl-text">Downloading...</span>
                    <span id="v16-dl-stats"></span>
                </div>
                <div style="background:#334155; border-radius:4px; height:8px; overflow:hidden;">
                    <div id="v16-dl-bar" style="background:linear-gradient(90deg, #a855f7, #d946ef); height:100%; width:0%; transition:width 0.3s;"></div>
                </div>
            </div>
            <div id="v16-grid" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(${CONFIG.cardMinWidth}, 1fr)); gap:20px;"></div>
        `;

        const target = document.querySelector('.container .row .col-12');
        if (target) target.insertBefore(ui, target.firstChild);

        const els = {
            input: ui.querySelector('#v16-input'),
            suggest: ui.querySelector('#v16-suggest'),
            chips: ui.querySelector('#v16-chips'),
            grid: ui.querySelector('#v16-grid'),
            settingsBtn: ui.querySelector('#v16-toggle-settings'),
            settingsPanel: ui.querySelector('#v16-settings-panel'),
            importInput: ui.querySelector('#v16-import'),
            exportBtn: ui.querySelector('#v16-export'),
            clearBtn: ui.querySelector('#v16-clear-cache'),
            clearThumbCacheBtn: ui.querySelector('#v16-clear-thumb-cache'),
            clearTagCacheBtn: ui.querySelector('#v16-clear-tag-cache'),
            clearHistoryBtn: ui.querySelector('#v16-clear-history'),
            clearAllCacheBtn: ui.querySelector('#v16-clear-all-cache'),
            goBtn: ui.querySelector('#v16-go'),
            smartBtn: ui.querySelector('#v16-smart'),
            downloadBtn: ui.querySelector('#v16-download'),
            status: ui.querySelector('#v16-status'),
            resumeNotification: ui.querySelector('#v16-resume-notification'),
            resumeBtn: ui.querySelector('#v16-resume-btn'),
            dismissResumeBtn: ui.querySelector('#v16-dismiss-resume'),
            resumeInfo: ui.querySelector('#v16-resume-info'),
            depth: ui.querySelector('#v16-depth'),
            autoSaveCheckbox: ui.querySelector('#v16-auto-save'),
            liveSearch: ui.querySelector('#v16-live-search'),
            scanLiveBtn: ui.querySelector('#v16-scan-live'),
            liveGrid: ui.querySelector('#v16-live-grid'),
            liveStatus: ui.querySelector('#v16-live-status'),
            showFavoritesBtn: ui.querySelector('#v16-show-favorites'),
            favCount: ui.querySelector('#v16-fav-count'),
            tabs: ui.querySelectorAll('.v16-tab'),
            historyList: ui.querySelector('#v16-history-list'),
            historyCount: ui.querySelector('#v16-history-count'),
            pageAnalysis: ui.querySelector('#v16-page-analysis'),
            analyzePagesBtn: ui.querySelector('#v16-analyze-pages'),
            pageRecommendations: ui.querySelector('#v16-page-recommendations'),
            mergeBtn: ui.querySelector('#v16-merge-selected'),
            dlProgress: ui.querySelector('#v16-download-progress'),
            dlText: ui.querySelector('#v16-dl-text'),
            dlStats: ui.querySelector('#v16-dl-stats'),
            dlBar: ui.querySelector('#v16-dl-bar'),
            tagFetchProgress: ui.querySelector('#v16-tag-fetch-progress'),
            tagFetchText: ui.querySelector('#v16-tag-fetch-text'),
            tagFetchStats: ui.querySelector('#v16-tag-fetch-stats'),
            tagFetchBar: ui.querySelector('#v16-tag-fetch-bar')
        };

        const liveAnimationTimers = new Map();
        const videoAnimationTimers = new Map();
        let selectedHistoryIndices = new Set();

        // Auto-save toggle
        els.autoSaveCheckbox.onchange = () => {
            CONFIG.autoSaveThumbnails = els.autoSaveCheckbox.checked;
        };

        function updateFavCount() {
            els.favCount.textContent = State.favorites.length;
        }

        function updateHistoryCount() {
            els.historyCount.textContent = State.searchHistory.length;
        }

        function toggleFavorite(username, currentThumb, thumbType, btnElement) {
            const idx = State.favorites.indexOf(username);
            if (idx === -1) {
                State.favorites.push(username);
                if (currentThumb) {
                    State.thumbCache[username] = {
                        url: currentThumb,
                        timestamp: Date.now(),
                        type: thumbType || (isLiveStreamUrl(currentThumb) ? 'live' : 'sprite')
                    };
                }
            } else {
                State.favorites.splice(idx, 1);
            }
            Backup.saveFavorites();
            updateFavCount();

            if (els.liveGrid.offsetParent) {
                if (State.showFavoritesOnly) renderLiveGrid(buildFavoritePerformers());
                else {
                    const query = els.liveSearch.value.toLowerCase();
                    renderLiveGrid(State.livePerformers.filter(p => p.username.toLowerCase().includes(query)));
                }
            }

            if (btnElement) {
                const isFav = State.favorites.includes(username);
                btnElement.innerHTML = isFav ? '★' : '☆';
                btnElement.style.color = isFav ? '#eab308' : 'white';
            }
        }

        function buildFavoritePerformers() {
            return State.favorites.map(username => {
                const existing = State.livePerformers.find(p => p.username === username);
                if (existing) return existing;
                const cached = State.thumbCache[username];
                return {
                    username,
                    url: `/performer/${username}`,
                    thumb: cached ? cached.url : NO_THUMB,
                    thumbType: cached ? cached.type : 'sprite',
                    isLive: false,
                    isFavorite: true
                };
            });
        }

        // ============================================================
        // HISTORY PANEL RENDERER
        // ============================================================
        function renderHistoryPanel() {
            updateHistoryCount();
            selectedHistoryIndices.clear();
            els.mergeBtn.style.display = 'none';

            if (State.searchHistory.length === 0) {
                els.historyList.innerHTML = `<div style="color:#64748b; text-align:center; padding:40px;">No search history yet. Run some searches!</div>`;
                return;
            }

            els.historyList.innerHTML = State.searchHistory.map((entry, idx) => {
                const date = new Date(entry.timestamp);
                const timeStr = date.toLocaleString();
                const tagsPreview = escapeHtml(entry.label || 'Unknown tags');

                return `
                <div class="v16-history-item" data-idx="${idx}" style="background:#1e293b; border:1px solid #334155; border-radius:8px; padding:15px; display:flex; justify-content:space-between; align-items:center; cursor:pointer; transition:0.2s;">
                    <div style="display:flex; align-items:center; gap:15px;">
                        <input type="checkbox" class="v16-history-check" data-idx="${idx}" style="width:18px; height:18px; cursor:pointer;">
                        <div>
                            <div style="font-weight:bold; color:#fff; margin-bottom:5px;">${tagsPreview}</div>
                            <div style="font-size:12px; color:#64748b;">${escapeHtml(timeStr)} • Depth: ${entry.depth} • <span style="color:#4ade80;">${entry.resultsCount} results</span></div>
                        </div>
                    </div>
                    <div style="display:flex; gap:10px;">
                        <button class="v16-history-load" data-idx="${idx}" style="background:#a855f7; border:none; color:white; padding:8px 15px; border-radius:4px; cursor:pointer; font-size:12px;">📂 Load</button>
                        <button class="v16-history-rerun" data-idx="${idx}" style="background:#0ea5e9; border:none; color:white; padding:8px 15px; border-radius:4px; cursor:pointer; font-size:12px;">🔄 Re-run</button>
                        <button class="v16-history-delete" data-idx="${idx}" style="background:#7f1d1d; border:none; color:#fca5a5; padding:8px 15px; border-radius:4px; cursor:pointer; font-size:12px;">🗑️</button>
                    </div>
                </div>`;
            }).join('');

            // Attach listeners
            els.historyList.querySelectorAll('.v16-history-check').forEach(cb => {
                cb.onclick = (e) => {
                    e.stopPropagation();
                    const idx = parseInt(cb.dataset.idx);
                    if (cb.checked) {
                        selectedHistoryIndices.add(idx);
                    } else {
                        selectedHistoryIndices.delete(idx);
                    }
                    els.mergeBtn.style.display = selectedHistoryIndices.size >= 2 ? 'block' : 'none';
                };
            });

            els.historyList.querySelectorAll('.v16-history-load').forEach(btn => {
                btn.onclick = async (e) => {
                    e.stopPropagation();
                    const idx = parseInt(btn.dataset.idx);
                    const entry = SearchHistory.get(idx);
                    if (entry && entry.results) {
                        State.allMatches = entry.results;
                        State.currentHistoryIndex = idx;
                        State.expandedCards.clear();
                        els.status.style.display = 'block';
                        els.status.innerHTML = `📜 <b>Loaded from history:</b> ${entry.label}<br>👉 <b>${entry.resultsCount} results</b>`;
                        renderGrid(entry.results);
                        // Switch to search tab to show results
                        switchToTab('search');

                        // Check if we need to fetch tags for any videos
                        const needsFetch = entry.results.some(v => !VideoTagManager.getCachedTags(v));
                        if (needsFetch) {
                            els.tagFetchProgress.style.display = 'block';
                            await VideoTagManager.fetchAllInBackground(
                                entry.results,
                                (current, total) => {
                                    const percent = Math.round((current / total) * 100);
                                    els.tagFetchBar.style.width = `${percent}%`;
                                    els.tagFetchStats.textContent = `${current}/${total}`;
                                    els.tagFetchText.textContent = current === total
                                        ? 'Tag fetch complete!'
                                        : 'Fetching video tags...';
                                },
                                (video) => {
                                    updateVideoCard(video);
                                }
                            );

                            // Re-render with popularity-sorted tags
                            renderGrid(State.allMatches);

                            setTimeout(() => {
                                els.tagFetchProgress.style.display = 'none';
                            }, 2000);
                        }
                    }
                };
            });

            els.historyList.querySelectorAll('.v16-history-rerun').forEach(btn => {
                btn.onclick = async (e) => {
                    e.stopPropagation();
                    const idx = parseInt(btn.dataset.idx);
                    const entry = SearchHistory.get(idx);
                    if (entry && entry.results && entry.results.length > 0) {
                        // Gather videos from history entry
                        const videos = entry.results;

                        // Show progress indicator
                        const originalText = btn.textContent;
                        btn.disabled = true;
                        btn.textContent = '⏳ Fetching...';

                        try {
                            // Calculate concurrency based on number of tags
                            // Count all tags across all tag groups
                            const totalTags = entry.tagGroups ?
                                entry.tagGroups.reduce((sum, group) => sum + (group.tags ? group.tags.length : 0), 0) : 0;

                            // Concurrency logic:
                            // - If tags <= 10: use number of tags (match the number of tags)
                            // - If tags > 10: split by half (tags / 2, rounded up)
                            // - Maximum is 10
                            let CONCURRENCY;
                            if (totalTags === 0) {
                                CONCURRENCY = 1; // Default to 1 if no tags
                            } else if (totalTags <= 10) {
                                CONCURRENCY = totalTags; // Match number of tags
                            } else {
                                CONCURRENCY = Math.min(Math.ceil(totalTags / 2), 10); // Split by half, max 10
                            }

                            const results = { success: 0, failed: 0 };
                            let processed = 0;
                            const THUMBNAIL_CONCURRENCY = 10; // Process 10 thumbnails concurrently for speed

                            for (let i = 0; i < videos.length; i += THUMBNAIL_CONCURRENCY) {
                                const batch = videos.slice(i, i + THUMBNAIL_CONCURRENCY);
                                const promises = batch.map(async (video) => {
                                    const thumbUrl = await fetchStaticThumb(video.user);
                                    if (thumbUrl) {
                                        video.thumb = thumbUrl;
                                        results.success++;
                                    } else {
                                        results.failed++;
                                    }

                                    processed++;
                                    const percent = Math.round((processed / videos.length) * 100);
                                    btn.textContent = `⏳ ${percent}% (${results.success}✓ ${results.failed}✗)`;
                                });

                                await Promise.all(promises);

                                // Small delay between batches only
                                if (i + THUMBNAIL_CONCURRENCY < videos.length) {
                                    await wait(50);
                                }
                            }

                            // Re-render grid with updated thumbnails
                            renderGrid(videos);

                            btn.textContent = '✅ Done';
                            setTimeout(() => {
                                btn.textContent = originalText;
                                btn.disabled = false;
                            }, 2000);
                        } catch (error) {
                            console.error('[Titan] Error fetching thumbnails:', error);
                            btn.textContent = '❌ Error';
                            setTimeout(() => {
                                btn.textContent = originalText;
                                btn.disabled = false;
                            }, 2000);
                        }
                    } else {
                        // If no results, restore tag groups and filters, then run search
                        State.tagGroups = JSON.parse(JSON.stringify(entry.tagGroups));
                        applyFiltersToUI(entry.filters);
                        els.depth.value = entry.depth;
                        renderChips();
                        switchToTab('search');
                        // Trigger search
                        els.goBtn.click();
                    }
                };
            });

            els.historyList.querySelectorAll('.v16-history-delete').forEach(btn => {
                btn.onclick = (e) => {
                    e.stopPropagation();
                    const idx = parseInt(btn.dataset.idx);
                    if (confirm('Delete this search from history?')) {
                        SearchHistory.remove(idx);
                        renderHistoryPanel();
                    }
                };
            });

            els.historyList.querySelectorAll('.v16-history-item').forEach(item => {
                item.onmouseenter = () => item.style.borderColor = '#a855f7';
                item.onmouseleave = () => item.style.borderColor = '#334155';
            });
        }

        window.renderHistoryPanel = renderHistoryPanel;

        function switchToTab(tabName) {
            els.tabs.forEach(t => {
                t.classList.remove('active');
                t.style.background = '#1e293b';
                t.style.color = '#94a3b8';
            });
            const activeTab = ui.querySelector(`.v16-tab[data-tab="${tabName}"]`);
            if (activeTab) {
                activeTab.classList.add('active');
                activeTab.style.background = '#a855f7';
                activeTab.style.color = 'white';
            }
            ui.querySelectorAll('.v16-tab-content').forEach(c => c.style.display = 'none');
            ui.querySelector(`#v16-tab-${tabName}`).style.display = 'block';
        }

        // Tab switching
        els.tabs.forEach(tab => {
            tab.onclick = () => {
                switchToTab(tab.dataset.tab);
                if (tab.dataset.tab === 'history') {
                    renderHistoryPanel();
                }
            };
        });

        els.showFavoritesBtn.onclick = () => {
            State.showFavoritesOnly = !State.showFavoritesOnly;
            els.showFavoritesBtn.style.background = State.showFavoritesOnly ? '#eab308' : '#1e293b';
            els.showFavoritesBtn.style.color = State.showFavoritesOnly ? '#000' : '#eab308';
            if (State.showFavoritesOnly) {
                els.liveStatus.innerHTML = `⭐ Showing <b>${State.favorites.length}</b> favorites`;
                renderLiveGrid(buildFavoritePerformers());
            } else {
                els.liveStatus.innerHTML = State.livePerformers.length ? `🔴 <b>${State.livePerformers.length}</b> live performers` : '';
                renderLiveGrid(State.livePerformers);
            }
        };

        els.scanLiveBtn.onclick = async () => {
            els.scanLiveBtn.disabled = true;
            els.scanLiveBtn.innerText = '⏳ Scanning...';
            State.showFavoritesOnly = false;
            State.brokenThumbs.clear();
            els.showFavoritesBtn.style.background = '#1e293b';
            els.showFavoritesBtn.style.color = '#eab308';

            const filters = getCurrentFilters();
            const depth = parseInt(els.depth.value) || CONFIG.defaultDepth;
            State.livePerformers = await scanLivePerformers(filters, depth, msg => els.liveStatus.innerHTML = msg);
            State.livePerformers.forEach(p => p.isFavorite = State.favorites.includes(p.username));
            els.liveStatus.innerHTML = `🔴 <b>${State.livePerformers.length}</b> live performers found`;
            renderLiveGrid(State.livePerformers);
            els.scanLiveBtn.disabled = false;
            els.scanLiveBtn.innerText = '🔴 Scan Live';
        };

        els.liveSearch.oninput = () => {
            if (State.showFavoritesOnly) return;
            const query = els.liveSearch.value.toLowerCase();
            renderLiveGrid(State.livePerformers.filter(p => p.username.toLowerCase().includes(query)));
        };

        function renderLiveGrid(performers) {
            liveAnimationTimers.forEach(timer => clearInterval(timer));
            liveAnimationTimers.clear();
            els.liveGrid.innerHTML = performers.map(p => {
                const isFav = State.favorites.includes(p.username);
                const isLive = isLiveStreamUrl(p.thumb) || p.thumbType === 'live';
                const isHighMedia = p.thumb && p.thumb.includes('highmedia-stream');
                const bgSize = isHighMedia ? 'cover' : `${CONFIG.spriteColumns * 100}% ${CONFIG.spriteRows * 100}%`;
                const safeUser = escapeHtml(p.username);
                const safeThumb = escapeHtml(p.thumb || '');
                const safeUrl = escapeHtml(p.url || '#');

                return `
                <div class="v16-live-card-wrapper" data-username="${safeUser}" style="position:relative;">
                    <a href="${safeUrl}" target="_blank" style="text-decoration:none; display:block;">
                        <div class="v16-live-card" style="background:#1e293b; border-radius:8px; overflow:hidden; border:1px solid ${isFav ? '#eab308' : '#334155'}; transition:0.3s; position:relative; aspect-ratio:16/9;">
                            <div class="v16-thumb-container" style="width:100%; height:100%; background-image:url('${safeThumb}'); background-size:${bgSize}; background-position:center; transition: background-position 0s;"></div>
                            <div style="position:absolute; bottom:0; left:0; right:0; background:rgba(0,0,0,0.7); padding:8px; font-size:12px; color:white; display:flex; justify-content:space-between; align-items:center;">
                                <span style="font-weight:bold; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${safeUser}</span>
                                ${p.isLive ? '<span style="background:#ef4444; padding:2px 5px; border-radius:3px; font-size:10px;">LIVE</span>' : ''}
                            </div>
                        </div>
                    </a>
                    <button class="v16-fav-btn" data-user="${safeUser}" style="position:absolute; top:5px; right:5px; background:rgba(0,0,0,0.5); border:none; border-radius:50%; width:28px; height:28px; cursor:pointer; color:${isFav ? '#eab308' : 'white'}; font-size:16px; display:flex; align-items:center; justify-content:center; z-index:10;">${isFav ? '★' : '☆'}</button>
                </div>`;
            }).join('');

            els.liveGrid.querySelectorAll('.v16-live-card-wrapper').forEach(card => {
                const username = card.dataset.username;
                const performer = performers.find(p => p.username === username);
                const thumbContainer = card.querySelector('.v16-thumb-container');
                const currentThumbUrl = performer.thumb;

                const tester = new Image();
                tester.src = currentThumbUrl;
                tester.onerror = async () => {
                    if (!State.brokenThumbs.has(currentThumbUrl)) {
                        State.brokenThumbs.add(currentThumbUrl);
                        ThumbnailManager.saveCache();
                        const newThumb = await fetchStaticThumb(username);
                        if (newThumb) {
                            State.thumbCache[username] = { url: newThumb, type: 'sprite', timestamp: Date.now() };
                            performer.thumb = newThumb;
                            performer.thumbType = 'sprite';
                            thumbContainer.style.backgroundImage = `url('${newThumb}')`;
                            thumbContainer.style.backgroundSize = `${CONFIG.spriteColumns * 100}% ${CONFIG.spriteRows * 100}%`;
                        } else {
                            thumbContainer.style.backgroundImage = 'none';
                            thumbContainer.innerText = 'NO PREVIEW';
                            thumbContainer.style.display = 'flex'; thumbContainer.style.alignItems = 'center'; thumbContainer.style.justifyContent = 'center';
                        }
                    }
                };

                card.onmouseenter = () => {
                    let frame = 0;
                    const initialType = performer.thumbType || (isLiveStreamUrl(performer.thumb) ? 'live' : 'sprite');
                    const intervalMs = initialType === 'live' ? CONFIG.liveAnimationSpeed : CONFIG.animationSpeed;
                    const timer = setInterval(() => {
                        const currentType = performer.thumbType || (isLiveStreamUrl(performer.thumb) ? 'live' : 'sprite');
                        if (currentType === 'sprite') {
                            frame = (frame + 1) % CONFIG.spriteFrames;
                            thumbContainer.style.backgroundPosition = getSpritePosition(frame);
                        } else if (currentType === 'live') {
                            frame = (frame + 1) % CONFIG.liveFrames.length;
                            thumbContainer.style.backgroundImage = `url('${buildLiveThumbUrl(performer.thumb, CONFIG.liveFrames[frame])}')`;
                        }
                    }, intervalMs);
                    liveAnimationTimers.set(username, timer);
                };
                card.onmouseleave = () => {
                    clearInterval(liveAnimationTimers.get(username));
                    liveAnimationTimers.delete(username);
                    if ((performer.thumbType || 'sprite') === 'sprite') thumbContainer.style.backgroundPosition = 'center';
                };
            });

            els.liveGrid.querySelectorAll('.v16-fav-btn').forEach(btn => {
                btn.onclick = (e) => {
                    e.preventDefault();
                    const user = btn.dataset.user;
                    const perf = performers.find(p => p.username === user);
                    toggleFavorite(user, perf?.thumb, perf?.thumbType, btn);
                };
            });
        }

        let timer;
        let suggestionRequestId = 0;
        els.settingsBtn.onclick = () => { els.settingsPanel.style.display = els.settingsPanel.style.display === 'none' ? 'block' : 'none'; };
        els.exportBtn.onclick = Backup.exportToFile;
        els.importInput.onchange = (e) => Backup.importFromFile(e.target.files[0]);
        els.clearBtn.onclick = Memory.clear;
        els.clearThumbCacheBtn.onclick = () => ThumbnailManager.clearCache();
        els.clearTagCacheBtn.onclick = () => VideoTagManager.clearCache();
        els.clearHistoryBtn.onclick = () => {
            if (confirm('Clear all search history?')) {
                SearchHistory.clear();
                renderHistoryPanel();
            }
        };
        els.clearAllCacheBtn.onclick = () => {
            if (confirm('Clear ALL cache? This will remove:\n- Search cache\n- Thumbnail cache\n- Tag cache\n- Search history\n\nThis cannot be undone!')) {
                Memory.clear();
                ThumbnailManager.clearCache();
                VideoTagManager.clearCache();
                SearchHistory.clear();
                renderHistoryPanel();
                alert('All cache cleared!');
            }
        };

        els.mergeBtn.onclick = async () => {
            if (selectedHistoryIndices.size < 2) return;

            const indices = Array.from(selectedHistoryIndices).sort((a, b) => a - b);
            State.allMatches = SearchHistory.mergeResults(indices);
            State.currentHistoryIndex = -1;
            State.expandedCards.clear();

            els.status.style.display = 'block';
            els.status.innerHTML = `🔗 <b>Merged ${indices.length} history entries</b><br>👉 <b>${State.allMatches.length} merged results</b>`;
            renderGrid(State.allMatches);
            switchToTab('search');

            const needsFetch = State.allMatches.some(v => !VideoTagManager.getCachedTags(v));
            if (needsFetch) {
                els.tagFetchProgress.style.display = 'block';
                await VideoTagManager.fetchAllInBackground(
                    State.allMatches,
                    (current, total) => {
                        const percent = Math.round((current / total) * 100);
                        els.tagFetchBar.style.width = `${percent}%`;
                        els.tagFetchStats.textContent = `${current}/${total}`;
                        els.tagFetchText.textContent = current === total
                            ? 'Tag fetch complete!'
                            : 'Fetching video tags...';
                    },
                    (video) => {
                        updateVideoCard(video);
                    }
                );

                renderGrid(State.allMatches);
                setTimeout(() => {
                    els.tagFetchProgress.style.display = 'none';
                }, 2000);
            }
        };


        // Download all with progress
        els.downloadBtn.onclick = async () => {
            if (State.allMatches.length === 0) return alert("No videos found to download!");
            if (!confirm(`Download ${State.allMatches.length} thumbnails?\n\nBlack/empty images will be automatically skipped.`)) return;

            els.dlProgress.style.display = 'block';
            els.downloadBtn.disabled = true;
            els.downloadBtn.style.opacity = '0.5';

            const result = await ThumbnailManager.batchSave(State.allMatches, (current, total, saved, skipped, invalid, reacquired) => {
                const percent = Math.round((current / total) * 100);
                els.dlBar.style.width = `${percent}%`;
                els.dlText.textContent = `Processing ${current}/${total}...`;
                els.dlStats.innerHTML = `<span style="color:#4ade80;">✓ ${saved}</span> | <span style="color:#94a3b8;">⏭ ${skipped}</span> | <span style="color:#f87171;">❌ ${invalid}</span>${reacquired > 0 ? ` | <span style="color:#fbbf24;">🔄 ${reacquired}</span>` : ''}`;
            });

            els.dlText.textContent = 'Complete!';
            els.dlStats.innerHTML = `<span style="color:#4ade80;">Saved: ${result.saved}</span> | <span style="color:#94a3b8;">Cached: ${result.skipped}</span> | <span style="color:#f87171;">Invalid: ${result.invalid}</span>${result.reacquired > 0 ? ` | <span style="color:#fbbf24;">Re-acquired: ${result.reacquired}</span>` : ''}`;

            setTimeout(() => {
                els.dlProgress.style.display = 'none';
                els.downloadBtn.disabled = false;
                els.downloadBtn.style.opacity = '1';
            }, 3000);
        };

        els.input.addEventListener('input', () => {
            clearTimeout(timer);
            const val = els.input.value.trim();
            suggestionRequestId++;
            const requestId = suggestionRequestId;
            if (val.length < 2) { els.suggest.innerHTML = ''; els.suggest.style.display = 'none'; return; }
            timer = setTimeout(async () => {
                const items = await fetchSuggestions(val);
                if (requestId !== suggestionRequestId || els.input.value.trim() !== val) return;
                if (items.length) {
                    els.suggest.innerHTML = items.map(i => `<div class="v16-item" data-val="${escapeHtml(i.val)}" style="padding:10px; cursor:pointer; border-bottom:1px solid #333;">${escapeHtml(i.label)}</div>`).join('');
                    els.suggest.style.display = 'block';
                    els.suggest.querySelectorAll('.v16-item').forEach(div => {
                        div.onclick = () => {
                            const tag = div.dataset.val;
                            els.input.value = tag;
                            els.suggest.style.display = 'none';
                            // Auto-add as OR group when clicked
                            addGroup('OR', [tag]);
                        };
                    });
                } else {
                    els.suggest.innerHTML = '';
                    els.suggest.style.display = 'none';
                }
            }, 300);
        });

        const addGroup = (type, tags) => {
            if (!tags.length) return;
            State.tagGroups.push({ type, tags });
            els.input.value = '';
            renderChips();
            Backup.saveLocal();
        };

        els.smartBtn.onclick = async () => {
            const val = els.input.value.trim();
            if (!val) return;
            els.smartBtn.innerText = "⏳ ...";
            const suggestions = await fetchSuggestions(val);
            const related = new Set([val.toLowerCase()]);
            suggestions.forEach(s => { if (s.val.includes(val.toLowerCase())) related.add(s.val); });
            addGroup('OR', Array.from(related));
            els.smartBtn.innerText = "✨ Smart";
            els.suggest.style.display = 'none';
        };

        els.input.onkeydown = (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                const tags = els.input.value.split(/[\s,]+/).map(t => t.replace('#', '').trim()).filter(t => t);
                if (tags.length === 0) return;
                if (e.shiftKey) {
                    tags.forEach(t => addGroup('AND', [t]));
                } else {
                    addGroup('OR', tags);
                }
                els.suggest.style.display = 'none';
            }
        };

        function renderChips() {
            // Show page analysis section if there are tags
            if (State.tagGroups.length > 0) {
                els.pageAnalysis.style.display = 'block';
            } else {
                els.pageAnalysis.style.display = 'none';
            }

            els.chips.innerHTML = State.tagGroups.map((g, i) => {
                const isOr = g.type === 'OR';
                const isExpanded = State.expandedGroups.has(i);
                const color = isOr ? '#a855f7' : '#64748b';

                if (isOr && g.tags.length > 1) {
                    if (isExpanded) {
                        return `
                            <div class="v16-group-container" data-idx="${i}" style="display:flex; flex-wrap:wrap; gap:5px; padding:10px; background:${color}15; border:2px dashed ${color}; border-radius:8px;">
                                <div style="width:100%; display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
                                    <span style="font-size:11px; color:${color}; font-weight:bold;">[OR GROUP] Click tag to remove</span>
                                    <span class="v16-collapse" data-idx="${i}" style="cursor:pointer; color:#94a3b8; font-size:12px; padding:2px 8px; background:#1e293b; border-radius:4px;">▲ Collapse</span>
                                </div>
                                ${g.tags.map((tag, ti) => `<div class="v16-tag-chip" data-group="${i}" data-tag="${ti}" style="background:${color}33; border:1px solid ${color}; padding:6px 12px; border-radius:20px; font-size:12px; color:#fff; cursor:pointer; display:flex; align-items:center; gap:6px;"><span>#${escapeHtml(tag)}</span><span style="color:#f87171; font-weight:bold;">×</span></div>`).join('')}
                                <div class="v16-delete-group" data-idx="${i}" style="background:#7f1d1d; border:1px solid #ef4444; padding:6px 12px; border-radius:20px; font-size:12px; color:#fca5a5; cursor:pointer;">🗑 Delete All</div>
                            </div>`;
                    } else {
                        const label = g.tags.length > 2 ? `${g.tags.slice(0, 2).join(' / ')} +${g.tags.length - 2}` : g.tags.join(' / ');
                        return `<div class="v16-group-chip" data-idx="${i}" style="background:${color}22; border:1px solid ${color}; padding:8px 16px; border-radius:25px; font-size:13px; color:#fff; display:flex; align-items:center; gap:10px; cursor:pointer;"><span style="color:${color}; font-weight:bold;">[OR]</span><span>${escapeHtml(label)}</span><span style="color:#94a3b8; font-size:11px;">▼</span><span class="del" data-idx="${i}" style="cursor:pointer; font-weight:bold; color:#f87171;">×</span></div>`;
                    }
                } else {
                    return `<div style="background:${color}22; border:1px solid ${color}; padding:8px 16px; border-radius:25px; font-size:13px; color:#fff; display:flex; align-items:center; gap:10px;"><span>${escapeHtml(g.tags.join(' & '))}</span><span class="del" data-idx="${i}" style="cursor:pointer; font-weight:bold; color:#f87171;">×</span></div>`;
                }
            }).join('');

            els.chips.querySelectorAll('.del').forEach(s => s.onclick = (e) => { e.stopPropagation(); State.tagGroups.splice(s.dataset.idx, 1); State.expandedGroups.delete(parseInt(s.dataset.idx)); renderChips(); Backup.saveLocal(); });
            els.chips.querySelectorAll('.v16-group-chip').forEach(c => c.onclick = (e) => { if (e.target.classList.contains('del')) return; State.expandedGroups.add(parseInt(c.dataset.idx)); renderChips(); });
            els.chips.querySelectorAll('.v16-collapse').forEach(s => s.onclick = (e) => { e.stopPropagation(); State.expandedGroups.delete(parseInt(s.dataset.idx)); renderChips(); });
            els.chips.querySelectorAll('.v16-tag-chip').forEach(c => c.onclick = () => { const g = parseInt(c.dataset.group), t = parseInt(c.dataset.tag); State.tagGroups[g].tags.splice(t, 1); if (!State.tagGroups[g].tags.length) { State.tagGroups.splice(g, 1); State.expandedGroups.delete(g); } renderChips(); Backup.saveLocal(); });
            els.chips.querySelectorAll('.v16-delete-group').forEach(b => b.onclick = () => { const i = parseInt(b.dataset.idx); State.tagGroups.splice(i, 1); State.expandedGroups.delete(i); renderChips(); Backup.saveLocal(); });
        }

        window.renderChips = renderChips;

        // Page analysis button handler
        els.analyzePagesBtn.onclick = async () => {
            if (!State.tagGroups.length) return alert("Add tags first!");

            els.pageAnalysis.style.display = 'block';
            els.analyzePagesBtn.disabled = true;
            els.analyzePagesBtn.textContent = '⏳ Analyzing...';
            els.pageRecommendations.innerHTML = '<div style="color:#94a3b8;">Starting analysis...</div>';

            const filters = getCurrentFilters();
            if (!isTagSearchSectionSupported(filters)) {
                els.pageRecommendations.innerHTML = '<div style="color:#fbbf24;">⚠️ Live mode is not supported in Tag Search. Use the Live tab for live performer scans.</div>';
                els.analyzePagesBtn.disabled = false;
                els.analyzePagesBtn.textContent = '🔍 Analyze Pages';
                return;
            }
            const allTags = State.tagGroups.flatMap(g => g.tags);

            try {
                const results = await analyzeTagPages(State.tagGroups, filters, (msg) => {
                    els.pageRecommendations.innerHTML = `<div style="color:#0ea5e9;">${msg}</div>`;
                });

                // Display recommendations
                let html = '<div style="margin-bottom:10px; padding:10px; background:#0f172a; border-radius:6px;">';
                html += '<div style="font-weight:bold; margin-bottom:8px; color:#a5b4fc;">📊 Analysis Results:</div>';

                const maxPages = results.map(r => r.maxPage).filter(p => p > 0);
                const minMax = maxPages.length > 0 ? Math.min(...maxPages) : 0;
                const maxMax = maxPages.length > 0 ? Math.max(...maxPages) : 0;
                const avgMax = maxPages.length > 0 ? Math.round(maxPages.reduce((a, b) => a + b, 0) / maxPages.length) : 0;

                results.forEach(({ tag, maxPage, error }) => {
                    const color = error ? '#f87171' : (maxPage > 0 ? '#4ade80' : '#fbbf24');
                    const icon = error ? '❌' : (maxPage > 0 ? '✓' : '⚠️');
                    html += `<div style="display:flex; justify-content:space-between; padding:5px 0; border-bottom:1px solid #334155;">
                        <span style="color:#cbd5e1;">${icon} <b>#${escapeHtml(tag)}</b></span>
                        <span style="color:${color}; font-weight:bold;">${error ? 'Error' : (maxPage > 0 ? `${maxPage} pages` : 'Not detected')}</span>
                    </div>`;
                });

                html += '</div>';

                if (maxPages.length > 0) {
                    html += `<div style="padding:10px; background:#1e293b; border-radius:6px; margin-top:10px;">
                        <div style="color:#94a3b8; font-size:12px; margin-bottom:5px;">📈 Statistics:</div>
                        <div style="display:flex; gap:15px; flex-wrap:wrap;">
                            <span style="color:#4ade80;">Min: <strong>${minMax}</strong></span>
                            <span style="color:#0ea5e9;">Avg: <strong>${avgMax}</strong></span>
                            <span style="color:#fbbf24;">Max: <strong>${maxMax}</strong></span>
                        </div>
                        <div style="margin-top:10px; padding-top:10px; border-top:1px solid #334155;">
                            <div style="color:#a5b4fc; font-size:12px; margin-bottom:5px;">💡 Recommended Depth:</div>
                            <div style="display:flex; gap:10px; align-items:center;">
                                <input type="number" id="v16-recommended-depth" value="${avgMax}"
                                    style="width:80px; padding:5px; background:#0f172a; border:1px solid #475569; color:white; border-radius:4px; text-align:center;">
                                <button id="v16-apply-recommended"
                                    style="background:#0ea5e9; border:none; color:white; padding:5px 15px; border-radius:4px; cursor:pointer; font-size:12px;">Apply to Depth</button>
                            </div>
                        </div>
                    </div>`;
                }

                els.pageRecommendations.innerHTML = html;
                const applyBtn = els.pageRecommendations.querySelector('#v16-apply-recommended');
                if (applyBtn) {
                    applyBtn.onclick = () => {
                        const recInput = els.pageRecommendations.querySelector('#v16-recommended-depth');
                        if (recInput) els.depth.value = recInput.value;
                        applyBtn.textContent = '✓ Applied';
                        setTimeout(() => { applyBtn.textContent = 'Apply to Depth'; }, 2000);
                    };
                }
                els.analyzePagesBtn.disabled = false;
                els.analyzePagesBtn.textContent = '🔍 Re-Analyze';
            } catch (e) {
                console.error('[Titan] Error analyzing pages:', e);
                els.pageRecommendations.innerHTML = `<div style="color:#f87171;">❌ Error during analysis: ${e.message}</div>`;
                els.analyzePagesBtn.disabled = false;
                els.analyzePagesBtn.textContent = '🔍 Analyze Pages';
            }
        };

        els.goBtn.onclick = async () => {
            if (!State.tagGroups.length) return alert("Add tags first!");
            els.status.style.display = 'block';
            els.grid.innerHTML = '';
            els.goBtn.disabled = true;
            els.goBtn.style.opacity = '0.5';
            State.expandedCards.clear();
            State.searchAbortReason = null;

            // Stop any existing tag fetch
            VideoTagManager.stopFetching();
            els.tagFetchProgress.style.display = 'none';

            const depth = parseInt(els.depth.value) || CONFIG.defaultDepth;
            const filters = getCurrentFilters();
            if (!isTagSearchSectionSupported(filters)) {
                els.status.innerHTML = '⚠️ Live mode is not supported in Tag Search. Use the Live tab for live performer scans.';
                els.goBtn.disabled = false;
                els.goBtn.style.opacity = '1';
                return;
            }
            const logs = [];
            let intersection = null;

            // Initialize search state for indexing/resume
            const previousActiveSearch = State.isResumingSearch ? State.activeSearch : null;
            State.isResumingSearch = false;
            State.activeSearch = previousActiveSearch ? {
                ...previousActiveSearch,
                tagGroups: JSON.parse(JSON.stringify(State.tagGroups)),
                filters: filters,
                depth: depth,
                progress: previousActiveSearch.progress || {},
                completedTags: previousActiveSearch.completedTags instanceof Set
                    ? previousActiveSearch.completedTags
                    : new Set(previousActiveSearch.completedTags || []),
                startTime: previousActiveSearch.startTime || Date.now()
            } : {
                tagGroups: JSON.parse(JSON.stringify(State.tagGroups)),
                filters: filters,
                depth: depth,
                progress: {},
                completedTags: new Set(),
                startTime: Date.now()
            };
            SearchIndexer.save(State.activeSearch);

            // Collect all searched tags for reference
            const allSearchedTags = State.tagGroups.flatMap(g => g.tags);

            // Collect all tags that need to be searched with their metadata
            const tagsToSearch = [];
            for (let groupIndex = 0; groupIndex < State.tagGroups.length; groupIndex++) {
                const group = State.tagGroups[groupIndex];
                for (let tagIndex = 0; tagIndex < group.tags.length; tagIndex++) {
                    const tag = group.tags[tagIndex];

                    // Check if we're resuming and this tag was already completed
                    if (SearchIndexer.isTagComplete(groupIndex, tagIndex)) {
                        logs.push(`<span style="color:#94a3b8">⏭️ Skipping #${tag} (already completed)</span>`);
                        continue;
                    }

                    tagsToSearch.push({
                        tag,
                        groupIndex,
                        tagIndex,
                        groupType: group.type
                    });
                }
            }

            // Process tags concurrently in batches
            const concurrentSearches = CONFIG.concurrentSearches || 3;
            const tagResults = new Map(); // Map: "groupIndex_tagIndex" -> results array

            // Throttle status DOM writes — many concurrent page fetches can spam this 100s of times/sec.
            // We coalesce updates to one paint per animation frame.
            let pendingStatusHtml = null;
            let statusRafScheduled = false;
            const setStatusThrottled = (html) => {
                pendingStatusHtml = html;
                if (statusRafScheduled) return;
                statusRafScheduled = true;
                requestAnimationFrame(() => {
                    statusRafScheduled = false;
                    if (pendingStatusHtml !== null) {
                        els.status.innerHTML = pendingStatusHtml;
                        pendingStatusHtml = null;
                    }
                });
            };

            // Helper function to search a single tag
            const searchTag = async (tagInfo) => {
                const { tag, groupIndex, tagIndex } = tagInfo;

                // Get resume page if available
                const tagProgress = SearchIndexer.getTagProgress(groupIndex, tagIndex);
                const resumeFromPage = tagProgress ? tagProgress.completedPages + 1 : null;

                try {
                    const list = await scanVideos(tag, depth, (msg) => {
                        const statusMsg = logs.join('<br>') + (logs.length ? '<br>' : '') + msg;
                        setStatusThrottled(statusMsg);
                    }, resumeFromPage, { groupIndex, tagIndex });

                    // Update progress - mark tag as complete
                    SearchIndexer.markTagComplete(groupIndex, tagIndex);

                    // Log completion - search continues with other tags
                    logs.push(`<span style="color:#4ade80">✓ Completed #${tag} (${list.length} results)</span>`);

                    return { groupIndex, tagIndex, results: list, success: true };
                } catch (e) {
                    console.error(`[Titan] Error scanning tag ${tag}:`, e);
                    if (State.searchAbortReason) {
                        logs.push(`<span style="color:#fbbf24">⏸ Paused while scanning #${tag}: ${escapeHtml(State.searchAbortReason)}</span>`);
                    } else {
                        logs.push(`<span style="color:#f87171">❌ Error scanning #${tag} - continuing...</span>`);
                    }
                    return { groupIndex, tagIndex, results: [], success: false };
                }
            };

            // Process tags in concurrent batches
            for (let i = 0; i < tagsToSearch.length; i += concurrentSearches) {
                if (State.searchAbortReason) break;
                const batch = tagsToSearch.slice(i, i + concurrentSearches);
                const batchTagNames = batch.map(t => `#${t.tag}`).join(', ');
                logs.push(`<span style="color:#0ea5e9">🔄 Processing batch: ${batchTagNames} (${i + 1}-${Math.min(i + concurrentSearches, tagsToSearch.length)} of ${tagsToSearch.length})</span>`);
                els.status.innerHTML = logs.join('<br>');

                const batchPromises = batch.map(tagInfo => searchTag(tagInfo));

                const batchResults = await Promise.all(batchPromises);

                // Store results
                batchResults.forEach(({ groupIndex, tagIndex, results }) => {
                    const key = `${groupIndex}_${tagIndex}`;
                    tagResults.set(key, results);
                });

                if (State.searchAbortReason) break;

                // Reduced delay between batches for faster processing
                if (i + concurrentSearches < tagsToSearch.length) {
                    await wait(50);
                }
            }

            if (State.searchAbortReason) {
                const reason = escapeHtml(State.searchAbortReason);
                els.status.innerHTML = logs.join('<br>') + `<br><hr style="border-color:#333; margin:15px 0;">` +
                    `<span style="color:#fbbf24; font-weight:bold;">⏸ Search paused: ${reason}</span><br>` +
                    `<span style="color:#94a3b8; font-size:12px;">Solve the challenge or refresh the page, then use Resume to continue from saved progress.</span>`;
                els.resumeInfo.innerHTML = `<strong>Search paused</strong><br><span style="opacity:0.85;">${reason}</span>`;
                els.resumeNotification.style.display = 'block';
                els.goBtn.disabled = false;
                els.goBtn.style.opacity = '1';
                return;
            }

            // Now process results by group for intersection logic
            for (let groupIndex = 0; groupIndex < State.tagGroups.length; groupIndex++) {
                const group = State.tagGroups[groupIndex];
                const groupMap = new Map(); // Key by performer (user)

                // Collect all results for this group
                for (let tagIndex = 0; tagIndex < group.tags.length; tagIndex++) {
                    const key = `${groupIndex}_${tagIndex}`;
                    const results = tagResults.get(key) || [];

                    results.forEach(v => {
                        const performerKey = v.user; // Use performer as key
                        if (groupMap.has(performerKey)) {
                            // Merge foundTags
                            const existing = groupMap.get(performerKey);
                            existing.foundTags = [...new Set([...existing.foundTags, ...v.foundTags])];
                        } else {
                            groupMap.set(performerKey, { ...v });
                        }
                    });
                }

                const videos = Array.from(groupMap.values());
                const name = group.tags.length > 1 ? `[${group.tags[0]} +${group.tags.length - 1}]` : `#${group.tags[0]}`;
                logs.push(`<span style="color:${group.type === 'OR' ? '#a855f7' : '#94a3b8'}">${name}</span>: ${videos.length} performers`);

                if (intersection === null) {
                    intersection = videos;
                } else {
                    // Merge foundTags when intersecting (key by performer)
                    const currentMap = new Map(videos.map(v => [v.user, v]));
                    intersection = intersection.filter(v => currentMap.has(v.user)).map(v => {
                        const other = currentMap.get(v.user);
                        return {
                            ...v,
                            foundTags: [...new Set([...v.foundTags, ...other.foundTags])]
                        };
                    });
                }
            }

            // Prepare new results
            const newResults = (intersection || []).map(v => ({
                ...v,
                searchedTags: allSearchedTags,
                // Ensure foundPage and foundTimestamp exist (defaults for cached results)
                foundPage: v.foundPage || 999,
                foundTimestamp: v.foundTimestamp || 0
            }));

            State.allMatches = mergeVideoLists(newResults);
            renderGrid(State.allMatches);

            // Save to history
            const historyEntry = SearchHistory.add({
                tagGroups: State.tagGroups,
                filters: filters,
                depth: depth,
                results: State.allMatches
            });

            els.status.innerHTML = logs.join('<br>') + `<br><hr style="border-color:#333; margin:15px 0;">` +
                `👉 <b>Total: <span style="color:#4ade80; font-size:16px;">${State.allMatches.length} Performers</span></b>` +
                `<br><span style="color:#64748b; font-size:12px;">💾 Saved to history</span>`;

            updateHistoryCount();

            // Clear search index - search is complete
            SearchIndexer.clear();

            // Start background tag fetching from /play pages (polish step after search)
            if (State.allMatches.length > 0) {
                // Small delay before starting tag fetch to let search complete
                await wait(500);

                els.tagFetchProgress.style.display = 'block';
                els.tagFetchText.textContent = 'Polishing: Fetching tags from /play pages...';

                await VideoTagManager.fetchAllInBackground(
                    State.allMatches,
                    // Progress callback
                    (current, total) => {
                        const percent = Math.round((current / total) * 100);
                        els.tagFetchBar.style.width = `${percent}%`;
                        els.tagFetchStats.textContent = `${current}/${total}`;
                        els.tagFetchText.textContent = current === total
                            ? 'Tag fetch complete!'
                            : 'Fetching video tags...';
                    },
                    // Video update callback
                    (video) => {
                        updateVideoCard(video);
                    }
                );

                // Update history with enriched tag data
                if (State.searchHistory.length > 0) {
                    State.searchHistory[0].results = State.allMatches;
                    SearchHistory.save();
                }

                // Re-render all cards with popularity-sorted tags
                await renderGrid(State.allMatches);

                setTimeout(() => {
                    els.tagFetchProgress.style.display = 'none';
                }, 2000);
            }

            // Auto-save thumbnails if enabled
            if (CONFIG.autoSaveThumbnails && State.allMatches.length > 0) {
                els.dlProgress.style.display = 'block';
                els.dlText.textContent = 'Auto-saving thumbnails...';

                await ThumbnailManager.batchSave(State.allMatches, (current, total, saved, skipped, invalid, reacquired) => {
                    const percent = Math.round((current / total) * 100);
                    els.dlBar.style.width = `${percent}%`;
                    els.dlStats.innerHTML = `<span style="color:#4ade80;">✓ ${saved}</span> | <span style="color:#94a3b8;">⏭ ${skipped}</span> | <span style="color:#f87171;">❌ ${invalid}</span>${reacquired > 0 ? ` | <span style="color:#fbbf24;">🔄 ${reacquired}</span>` : ''}`;
                });

                setTimeout(() => {
                    els.dlProgress.style.display = 'none';
                }, 2000);
            }

            els.goBtn.disabled = false;
            els.goBtn.style.opacity = '1';
        };


        function renderGrid(videos) {
            videoAnimationTimers.forEach(timer => clearInterval(timer));
            videoAnimationTimers.clear();

            els.grid.innerHTML = videos.map(v => renderVideoCard(v)).join('');
            attachCardListeners();
        }

        // Check and restore local thumbnail for a video
        async function checkAndRestoreThumbnail(video) {
            // If thumbnail is missing or broken, try to restore from local files
            if (!video.thumb || video.thumb === NO_THUMB || State.brokenThumbs.has(video.thumb)) {
                const localThumb = await ThumbnailManager.restoreLocalThumbnail(video);
                if (localThumb) {
                    video.thumb = localThumb;
                    return true;
                }
            }
            return false;
        }

        function renderVideoCard(v) {
            const isFav = State.favorites.includes(v.user);
            const isCached = ThumbnailManager.isAlreadyDownloaded(v);
            const isExpanded = State.expandedCards.has(v.id);

            // Check for local thumbnail asynchronously (won't block rendering)
            if (CONFIG.localThumbnailFolder && (!v.thumb || v.thumb === NO_THUMB || State.brokenThumbs.has(v.thumb))) {
                checkAndRestoreThumbnail(v).then(restored => {
                    if (restored) {
                        // Re-render this card with the restored thumbnail
                        const cardEl = document.querySelector(`.v16-card-wrapper[data-id="${v.id}"]`);
                        if (cardEl) {
                            const thumbContainer = cardEl.querySelector('.v16-thumb-container');
                            if (thumbContainer) {
                                thumbContainer.style.backgroundImage = `url('${v.thumb}')`;
                                thumbContainer.style.backgroundSize = `${CONFIG.spriteColumns * 100}% ${CONFIG.spriteRows * 100}%`;
                                const noThumbDiv = thumbContainer.querySelector('div[style*="No thumbnail"]');
                                if (noThumbDiv) noThumbDiv.remove();
                            }
                        }
                    }
                });
            }

            // Simple thumbnail display - just show the URL
            const safeThumb = escapeHtml(v.thumb || '');
            const safeDuration = escapeHtml(v.duration || '');
            const safeUser = escapeHtml(v.user || '');
            const safeUrl = escapeHtml(v.url || '#');
            const safeId = escapeHtml(v.id || '');
            const thumbStyle = v.thumb && v.thumb !== NO_THUMB
                ? `background-image:url('${safeThumb}'); background-size:${CONFIG.spriteColumns * 100}% ${CONFIG.spriteRows * 100}%;`
                : 'background-image:none;';
            const thumbDisplay = v.thumb && v.thumb !== NO_THUMB ? '' : '<div style="display:flex; align-items:center; justify-content:center; height:100%; color:#64748b; font-size:12px;">No thumbnail</div>';

            // Get all tags: from cache, or foundTags as fallback
            const cachedTags = VideoTagManager.getCachedTags(v);
            const allTags = cachedTags || v.allTags || [];
            const foundTags = v.foundTags || [];
            const searchedTags = v.searchedTags || [];

            // Separate matched vs other tags (Set lookup is O(1) vs Array.includes O(n))
            const matchedSet = new Set();
            for (const t of searchedTags) matchedSet.add(t);
            for (const t of foundTags) matchedSet.add(t);
            const matchedTags = [];
            const otherTags = [];
            for (const t of allTags) {
                if (matchedSet.has(t)) matchedTags.push(t);
                else otherTags.push(t);
            }

            // Sort by popularity
            const sortedMatched = VideoTagManager.sortByPopularity(matchedTags);
            const sortedOther = VideoTagManager.sortByPopularity(otherTags);

            // Combine: matched first, then others
            const allSortedTags = [...sortedMatched, ...sortedOther];

            // Dynamic collapse: show ~3-5 tags based on typical card width
            const collapseThreshold = 4;
            const visibleTags = isExpanded ? allSortedTags : allSortedTags.slice(0, collapseThreshold);
            const hiddenCount = allSortedTags.length - collapseThreshold;

            // Build tag HTML
            const tagsHtml = visibleTags.map(tag => {
                const safeTag = escapeHtml(tag);
                const isMatched = matchedTags.includes(tag);
                if (isMatched) {
                    return `<span class="v16-tag v16-tag-matched" style="background:#4ade80; color:#000; padding:2px 6px; border-radius:3px; font-size:10px; font-weight:bold;">#${safeTag}</span>`;
                } else {
                    return `<span class="v16-tag v16-tag-other" style="background:#334155; color:#94a3b8; padding:2px 6px; border-radius:3px; font-size:10px;">#${safeTag}</span>`;
                }
            }).join(' ');

            // Expand/collapse button
            let expandBtnHtml = '';
            if (!isExpanded && hiddenCount > 0) {
                expandBtnHtml = `<span class="v16-tag-expand" data-id="${safeId}" style="background:#1e293b; color:#6366f1; padding:2px 6px; border-radius:3px; font-size:10px; cursor:pointer; border:1px solid #6366f1;">+${hiddenCount} more</span>`;
            } else if (isExpanded && allSortedTags.length > collapseThreshold) {
                expandBtnHtml = `<span class="v16-tag-collapse" data-id="${safeId}" style="background:#1e293b; color:#6366f1; padding:2px 6px; border-radius:3px; font-size:10px; cursor:pointer; border:1px solid #6366f1;">− less</span>`;
            }

            // Loading indicator if tags not yet fetched
            const loadingHtml = (!cachedTags && allTags.length === 0 && foundTags.length > 0)
                ? `<span style="color:#64748b; font-size:10px; font-style:italic;">Loading tags...</span>`
                : '';

            return `
            <div class="v16-card-wrapper" data-id="${safeId}" style="position:relative;">
                <a href="${safeUrl}" target="_blank" style="text-decoration:none; display:block;">
                    <div class="v16-card" style="background:#1e293b; border-radius:8px; overflow:hidden; border:1px solid #334155; transition:0.3s; position:relative; z-index:1;">
                        <div class="v16-thumb-container" style="width:100%; aspect-ratio:16/9; overflow:hidden; background:#000; position:relative; ${thumbStyle} background-position:0% 0%;">
                            ${thumbDisplay}
                            <div style="position:absolute; bottom:8px; right:8px; background:rgba(0,0,0,0.85); color:white; font-size:11px; padding:3px 6px; border-radius:4px; font-weight:bold;">${safeDuration}</div>
                            ${isCached ? '<div style="position:absolute; top:8px; left:8px; background:#4ade80; color:#000; font-size:10px; padding:2px 5px; border-radius:3px;">💾</div>' : ''}
                        </div>
                        <div style="padding:12px;">
                            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
                                <div style="color:#a855f7; font-size:14px; font-weight:bold;">@${safeUser}</div>
                                <div style="color:#64748b; font-size:11px;">${safeDuration}</div>
                            </div>
                            <div class="v16-tags-container" style="display:flex; flex-wrap:wrap; gap:4px; min-height:20px;">
                                ${tagsHtml}
                                ${expandBtnHtml}
                                ${loadingHtml}
                            </div>
                        </div>
                    </div>
                </a>
                <button class="v16-fav-btn" data-user="${safeUser}" data-thumb="${safeThumb}" style="position:absolute; top:5px; right:5px; background:rgba(0,0,0,0.5); border:none; border-radius:50%; width:28px; height:28px; cursor:pointer; color:${isFav ? '#eab308' : 'white'}; font-size:16px; display:flex; align-items:center; justify-content:center; z-index:10;">${isFav ? '★' : '☆'}</button>
            </div>`;
        }

        // Coalesce per-video card updates into a single rAF flush.
        // The background tag fetcher fires updateVideoCard once per video; without
        // batching we'd do N querySelectors + N innerHTML parses + N listener rebinds.
        const _pendingCardUpdates = new Map();
        let _cardUpdateRaf = null;
        function _flushCardUpdates() {
            _cardUpdateRaf = null;
            const videos = Array.from(_pendingCardUpdates.values());
            _pendingCardUpdates.clear();
            for (const video of videos) {
                const card = els.grid.querySelector(`.v16-card-wrapper[data-id="${video.id}"]`);
                if (!card) continue;
                const newCard = document.createElement('div');
                newCard.innerHTML = renderVideoCard(video);
                card.replaceWith(newCard.firstElementChild);
                attachCardListeners(video.id);
            }
        }
        function updateVideoCard(video) {
            if (!video || !video.id) return;
            _pendingCardUpdates.set(video.id, video);
            if (_cardUpdateRaf !== null) return;
            _cardUpdateRaf = requestAnimationFrame(_flushCardUpdates);
        }

        function prependVideoCards(videos) {
            const fragment = document.createDocumentFragment();
            videos.forEach((v, i) => {
                const wrapper = document.createElement('div');
                wrapper.innerHTML = renderVideoCard(v);
                const card = wrapper.firstElementChild;
                // Add fade-in animation with stagger
                card.style.opacity = '0';
                card.style.transform = 'translateY(-20px)';
                card.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
                setTimeout(() => {
                    card.style.opacity = '1';
                    card.style.transform = 'translateY(0)';
                }, i * 100);
                fragment.appendChild(card);
            });
            els.grid.insertBefore(fragment, els.grid.firstChild);
            attachCardListeners();
        }

        function attachCardListeners(specificId = null) {
            const selector = specificId
                ? `.v16-card-wrapper[data-id="${specificId}"]`
                : '.v16-card-wrapper';

            els.grid.querySelectorAll(selector).forEach(card => {
                const vidId = card.dataset.id;
                const thumbContainer = card.querySelector('.v16-thumb-container');

                // Hover animation
                card.onmouseenter = () => {
                    let frame = 0;
                    const timer = setInterval(() => {
                        frame = (frame + 1) % CONFIG.spriteFrames;
                        thumbContainer.style.backgroundPosition = getSpritePosition(frame);
                    }, CONFIG.animationSpeed);
                    videoAnimationTimers.set(vidId, timer);
                };

                card.onmouseleave = () => {
                    clearInterval(videoAnimationTimers.get(vidId));
                    videoAnimationTimers.delete(vidId);
                    thumbContainer.style.backgroundPosition = '0% 0%';
                };

                // Favorite button
                const favBtn = card.querySelector('.v16-fav-btn');
                if (favBtn) {
                    favBtn.onclick = (e) => {
                        e.preventDefault();
                        toggleFavorite(favBtn.dataset.user, favBtn.dataset.thumb, 'sprite', favBtn);
                    };
                }

                // Tag expand/collapse
                const expandBtn = card.querySelector('.v16-tag-expand');
                if (expandBtn) {
                    expandBtn.onclick = (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        State.expandedCards.add(expandBtn.dataset.id);
                        const video = State.allMatches.find(v => v.id === expandBtn.dataset.id);
                        if (video) updateVideoCard(video);
                    };
                }

                const collapseBtn = card.querySelector('.v16-tag-collapse');
                if (collapseBtn) {
                    collapseBtn.onclick = (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        State.expandedCards.delete(collapseBtn.dataset.id);
                        const video = State.allMatches.find(v => v.id === collapseBtn.dataset.id);
                        if (video) updateVideoCard(video);
                    };
                }
            });
        }

        const style = document.createElement('style');
        style.textContent = `
            .v16-card:hover { transform: scale(${CONFIG.hoverScale}) translateY(-10px); border-color: #a855f7 !important; z-index: 10; box-shadow: 0 15px 40px rgba(0,0,0,0.9); }
            .v16-live-card:hover { transform: scale(1.03); border-color: #ef4444 !important; box-shadow: 0 10px 30px rgba(239,68,68,0.3); }
            .v16-live-card-wrapper { position: relative; }
            .v16-fav-btn:hover { transform: scale(1.2); }
            .v16-item:hover { background: #334155; }
            .v16-tag-chip:hover { background: #7f1d1d !important; border-color: #ef4444 !important; }
            .v16-group-chip:hover { transform: scale(1.02); }
            .v16-history-item:hover { background: #334155 !important; }
            .v16-tag { transition: transform 0.2s, box-shadow 0.2s; cursor: default; }
            .v16-tag:hover { transform: scale(1.1); box-shadow: 0 2px 8px rgba(0,0,0,0.3); }
            .v16-tag-matched:hover { background: #22c55e !important; }
            .v16-tag-other:hover { background: #475569 !important; color: #fff !important; }
            .v16-tag-expand:hover, .v16-tag-collapse:hover { background: #6366f1 !important; color: #fff !important; }
            .v16-card-wrapper { animation: none; }
            @keyframes fadeSlideIn {
                from { opacity: 0; transform: translateY(-20px); }
                to { opacity: 1; transform: translateY(0); }
            }
        `;
        document.head.appendChild(style);

        // Check for interrupted search on page load
        function checkForInterruptedSearch() {
            const interrupted = SearchIndexer.load();
            if (interrupted && interrupted.tagGroups) {
                State.activeSearch = interrupted;
                // Convert completedTags Set from array (JSON doesn't support Set)
                if (Array.isArray(interrupted.completedTags)) {
                    State.activeSearch.completedTags = new Set(interrupted.completedTags);
                } else {
                    State.activeSearch.completedTags = new Set();
                }

                // If this was a 403 refresh, auto-resume immediately
                if (interrupted.refresh403) {
                    // Clear the flag
                    State.activeSearch.refresh403 = false;
                    SearchIndexer.save(State.activeSearch);
                    // Auto-resume after a short delay to let page load
                    setTimeout(() => {
                        resumeInterruptedSearch();
                    }, 500);
                    return;
                }

                // Show resume notification for manual interruptions
                const elapsed = Math.floor((Date.now() - interrupted.timestamp) / 1000 / 60);
                const tagCount = interrupted.tagGroups.reduce((sum, g) => sum + g.tags.length, 0);
                const completedCount = State.activeSearch.completedTags.size;
                const remainingCount = tagCount - completedCount;

                els.resumeInfo.innerHTML = `
                    <strong>${tagCount} tags</strong> at depth <strong>${interrupted.depth}</strong><br>
                    <span style="opacity:0.8;">${completedCount} completed, ${remainingCount} remaining</span> • Interrupted ${elapsed} min ago
                `;
                els.resumeNotification.style.display = 'block';
            }
        }

        // Resume interrupted search
        function resumeInterruptedSearch() {
            if (!State.activeSearch) return;

            // Restore tag groups and filters
            State.tagGroups = JSON.parse(JSON.stringify(State.activeSearch.tagGroups));
            applyFiltersToUI(State.activeSearch.filters);
            els.depth.value = State.activeSearch.depth;
            renderChips();

            // Hide resume notification
            els.resumeNotification.style.display = 'none';

            // Switch to search tab and trigger search
            switchToTab('search');
            State.isResumingSearch = true;
            setTimeout(() => {
                els.goBtn.click();
            }, 100);
        }

        // Load initial data
        Backup.restoreLocal();
        if (State.tagGroups.length) renderChips();
        updateFavCount();
        updateHistoryCount();

        // Check for interrupted search
        checkForInterruptedSearch();

        // Resume button handler
        els.resumeBtn.onclick = () => {
            resumeInterruptedSearch();
        };

        // Dismiss resume notification
        els.dismissResumeBtn.onclick = () => {
            SearchIndexer.clear();
            els.resumeNotification.style.display = 'none';
        };

        // Expose global function for clearing all cache from console
        window.clearAllTitanCache = function () {
            Memory.clear();
            ThumbnailManager.clearCache();
            VideoTagManager.clearCache();
            SearchHistory.clear();
            SearchIndexer.clear();
            alert('All Titan cache cleared!');
        };
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', createUI);
    } else {
        createUI();
    }
})();