☰

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 のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==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();
    }
})();