Coomer BetterUI NS Update

Video thumbnails, modal gallery carousel, avatar placeholders, Pinterest-style layout

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

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

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

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

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

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

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

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

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

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

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

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

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

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Coomer BetterUI NS Update
// @namespace    http://tampermonkey.net/
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/mp4box.all.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/umd/index.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/dexie.min.js
// @require      https://cdn.jsdelivr.net/npm/@glidejs/[email protected]/dist/glide.min.js
// @version      3.12.4
// @description  Video thumbnails, modal gallery carousel, avatar placeholders, Pinterest-style layout
// @author       xxxchimp
// @Additional   Night-S
// @license      MIT
// @match        https://coomer.st/*/user/*
// @match        https://coomer.su/*/user/*
// @match        https://coomer.st/posts/*
// @match        https://coomer.su/posts/*
// @match        https://kemono.su/*/user/*
// @match        https://kemono.party/*/user/*
// @match        https://kemono.cr/*/user/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      coomer.st
// @connect      coomer.su
// @connect      kemono.su
// @connect      kemono.party
// @connect      kemono.cr
// @connect      n1.coomer.st
// @connect      n2.coomer.st
// @connect      n3.coomer.st
// @connect      n4.coomer.st
// @connect      n5.coomer.st
// @connect      n6.coomer.st
// @connect      c1.coomer.st
// @connect      c2.coomer.st
// @connect      c3.coomer.st
// @connect      c4.coomer.st
// @connect      c5.coomer.st
// @connect      c6.coomer.st
// @connect      n1.kemono.su
// @connect      n2.kemono.su
// @connect      n3.kemono.su
// @connect      n4.kemono.su
// @connect      api.anthropic.com
// @connect      generativelanguage.googleapis.com
// @connect      *
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // =========================================================================
    // CONFIGURATION & SETTINGS
    // =========================================================================

    const DEFAULT_SETTINGS = {
        cardColumns: 5,
        pauseGifsWhenHidden: true,
        prefixFilenamesWithTitle: false,
        maxFilenamePrefixLength: 40,
        debug: false,
        analyzeMp4Atoms: false,
        // AI thumbnail settings
        aiProvider: 'none', // 'none', 'claude', 'gemini'
        aiAutoFallback: false // Auto-try AI when standard thumbnail fails
    };

    const settings = {
        ...DEFAULT_SETTINGS,
        ...JSON.parse(GM_getValue('betterui_settings', '{}') || '{}')
    };

    const saveSettings = () => GM_setValue('betterui_settings', JSON.stringify(settings));

    // AI API keys stored separately (not in settings object)
    const getAiApiKey = (provider) => GM_getValue(`betterui_ai_key_${provider}`, '');
    const setAiApiKey = (provider, key) => GM_setValue(`betterui_ai_key_${provider}`, key);

    const CONFIG = {
        thumbnailSize: 180,
        seekTime: 2,
        maxConcurrentVideo: 3,
        maxConcurrentApi: 8,
        retryDelay: 200,
        maxVideoSizeForThumbnail: 300 * 1024 * 1024, // 300MB
        maxNonFaststartSize: 20 * 1024 * 1024, // 20MB
        thumbnailCacheMaxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
        thumbnailCacheMaxSize: 500, // Max cached thumbnails
        // AI frame selection config
        aiFrameCount: 6, // Number of frames to extract for AI analysis
        aiFramePositions: [0.1, 0.25, 0.4, 0.55, 0.7, 0.85], // Positions as fraction of video duration
        extensions: {
            video: ['.mp4', '.webm', '.mov', '.m4v', '.mkv', '.avi', '.m3u8'],
            image: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff'],
            archive: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
            audio: ['.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a'],
            document: ['.pdf', '.doc', '.docx', '.txt', '.rtf', '.psd', '.ai']
        }
    };

    // =========================================================================
    // THUMBNAIL CACHE (IndexedDB via Dexie)
    // =========================================================================

    let thumbnailDb = null;

    /**
     * Initialize IndexedDB for thumbnail caching
     */
    async function initThumbnailCache() {
        if (typeof Dexie === 'undefined') {
            debugLog('Dexie not available, caching disabled');
            return false;
        }

        try {
            thumbnailDb = new Dexie('BetterUIThumbnails');
            thumbnailDb.version(1).stores({
                thumbnails: 'url, dataUrl, duration, createdAt'
            });
            await thumbnailDb.open();
            debugLog('Thumbnail cache initialized');

            // Cleanup old entries periodically
            cleanupThumbnailCache();
            return true;
        } catch (e) {
            debugLog('Failed to init thumbnail cache:', e.message);
            thumbnailDb = null;
            return false;
        }
    }

    /**
     * Get cached thumbnail
     */
    async function getCachedThumbnail(url) {
        if (!thumbnailDb) return null;
        try {
            const cached = await thumbnailDb.thumbnails.get(url);
            if (cached) {
                // Check if expired
                if (Date.now() - cached.createdAt > CONFIG.thumbnailCacheMaxAge) {
                    await thumbnailDb.thumbnails.delete(url);
                    return null;
                }
                debugLog('Cache hit:', url.substring(url.lastIndexOf('/') + 1));
                return { dataUrl: cached.dataUrl, duration: cached.duration };
            }
        } catch (e) {
            debugLog('Cache read error:', e.message);
        }
        return null;
    }

    /**
     * Store thumbnail in cache
     */
    async function cacheThumbnail(url, dataUrl, duration) {
        if (!thumbnailDb) return;
        try {
            await thumbnailDb.thumbnails.put({
                url,
                dataUrl,
                duration: duration || 0,
                createdAt: Date.now()
            });
            debugLog('Cached:', url.substring(url.lastIndexOf('/') + 1));
        } catch (e) {
            debugLog('Cache write error:', e.message);
        }
    }

    /**
     * Cleanup old cache entries
     */
    async function cleanupThumbnailCache() {
        if (!thumbnailDb) return;
        try {
            const expiry = Date.now() - CONFIG.thumbnailCacheMaxAge;
            const deleted = await thumbnailDb.thumbnails
                .where('createdAt')
                .below(expiry)
                .delete();
            if (deleted > 0) debugLog(`Cleaned ${deleted} expired cache entries`);

            // Also limit total size
            const count = await thumbnailDb.thumbnails.count();
            if (count > CONFIG.thumbnailCacheMaxSize) {
                const excess = count - CONFIG.thumbnailCacheMaxSize;
                const oldest = await thumbnailDb.thumbnails
                    .orderBy('createdAt')
                    .limit(excess)
                    .toArray();
                await thumbnailDb.thumbnails.bulkDelete(oldest.map(t => t.url));
                debugLog(`Removed ${excess} excess cache entries`);
            }
        } catch (e) {
            debugLog('Cache cleanup error:', e.message);
        }
    }

    // =========================================================================
    // UTILITY FUNCTIONS
    // =========================================================================

    const debugLog = (msg, data = '') => settings.debug && console.log(`[BetterUI] ${msg}`, data);

    /**
     * Create Material Symbols icon HTML
     * @param {string} name - Icon name (e.g., 'close', 'download')
     * @param {string} size - Size class: 'sm', 'md', 'lg' (default: 'md')
     * @param {boolean} filled - Use filled variant
     */
    function icon(name, size = 'md', filled = false) {
        const sizeClass = size === 'md' ? '' : ` icon-${size}`;
        const fillClass = filled ? ' icon-filled' : '';
        return `<span class="material-symbols-rounded${sizeClass}${fillClass}">${name}</span>`;
    }

    /**
     * Detect file type from path
     */
    function getFileType(path) {
        if (!path) return 'other';
        const lowerPath = path.toLowerCase();
        for (const [type, exts] of Object.entries(CONFIG.extensions)) {
            if (exts.some(ext => lowerPath.includes(ext))) return type;
        }
        console.log("getFileType NO MATCH:", { path });
        return 'other';
    }

    const isVideo = path => getFileType(path) === 'video';
    const isImage = path => getFileType(path) === 'image';

    /**
     * Convert relative path to full URL
     */
    function toFullUrl(path, baseUrl, server) {
        if (!path) return null;

        // Full URL already?
        if (path.startsWith('http')) return path;

        // Popular pages: attachments include server
        if (server && path.startsWith('/')) {
            return `${server}/data${path}`;
        }

        // Normal user pages
        if (path.startsWith('/')) {
            return `${baseUrl}${path}`;
        }

        return `${baseUrl}/data/${path}`;
    }

    /**
     * Sanitise string for use in filenames
     */
    function sanitiseFilename(str, maxLength = 50) {
        if (!str) return '';
        return str
            .replace(/[<>:"/\\|?*\x00-\x1f]/g, '')
            .replace(/\s+/g, '_')
            .replace(/_+/g, '_')
            .replace(/^_|_$/g, '')
            .substring(0, maxLength);
    }

    /**
     * Extract filename from URL
     */
    function getFilenameFromUrl(url, fallback = 'download') {
        try {
            const pathname = new URL(url).pathname;
            const filename = pathname.split('/').pop();
            return filename?.includes('.') ? decodeURIComponent(filename) : fallback;
        } catch {
            return fallback;
        }
    }

    /**
     * Escape HTML entities
     */
    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    /**
     * Convert URLs in text to clickable links
     */
    function linkifyText(text) {
        if (!text) return '';
        const escaped = escapeHtml(text);
        return escaped
            .replace(/(https?:\/\/[^\s<>"{}|\\^`\[\]]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>')
            .replace(/\n/g, '<br>');
    }

    /**
     * Sanitize string for use as filename
     */
    function sanitizeFilename(name) {
        if (!name) return 'untitled';
        return name
            .replace(/[<>:"/\\|?*]/g, '_')
            .replace(/\s+/g, ' ')
            .trim()
            .slice(0, 100);
    }

    /**
     * Show toast notification
     */
    function showToast(message, type = 'info') {
        const existing = document.querySelector('.betterui-toast');
        if (existing) existing.remove();

        const toast = document.createElement('div');
        toast.className = `betterui-toast betterui-toast-${type}`;
        toast.textContent = message;
        document.body.appendChild(toast);

        // Trigger animation
        requestAnimationFrame(() => toast.classList.add('visible'));

        setTimeout(() => {
            toast.classList.remove('visible');
            setTimeout(() => toast.remove(), 300);
        }, 3000);
    }

    function formatDuration(sec) {
        sec = Math.floor(sec || 0);
        const m = Math.floor(sec / 60);
        const s = sec % 60;
        return `${m}:${s.toString().padStart(2, '0')}`;
    }

    // =========================================================================
    // XHR WRAPPER
    // =========================================================================

    /**
     * Promisified GM_xmlhttpRequest wrapper
     */
    function gmFetch(url, options = {}) {
        return new Promise((resolve, reject) => {
            // Set appropriate Accept header based on response type
            let defaultHeaders = {};
            if (options.responseType === 'json') {
                defaultHeaders['Accept'] = 'text/css';
            } else if (options.responseType === 'arraybuffer' || options.responseType === 'blob') {
                defaultHeaders['Accept'] = '*/*';
            }

            GM_xmlhttpRequest({
                method: options.method || 'GET',
                url,
                responseType: options.responseType || 'text',
                headers: options.headers || defaultHeaders,
                data: options.data || null,
                timeout: options.timeout || 30000,
                onload: response => {
                    if (response.status >= 200 && response.status < 300) {
                        resolve(response);
                    } else {
                        debugLog('HTTP error:', response.status, url);
                        reject(new Error(`HTTP ${response.status}`));
                    }
                },
                onerror: (err) => {
                    debugLog('Network error:', url);
                    reject(new Error('Network error'));
                },
                ontimeout: () => {
                    debugLog('Timeout:', url);
                    reject(new Error('Timeout'));
                }
            });
        });
    }

    /**
     * Resolve redirects to get final URL
     */
    async function resolveRedirectUrl(url) {
        try {
            const response = await gmFetch(url, { method: 'HEAD' });
            const finalUrl = response.finalUrl || url;
            if (finalUrl !== url) debugLog('Redirect:', `${url} -> ${finalUrl}`);
            return finalUrl;
        } catch {
            return url;
        }
    }

    /**
     * Get file size via HEAD request
     */
    async function getFileSize(url) {
        try {
            const finalUrl = await resolveRedirectUrl(url);
            const response = await gmFetch(finalUrl, { method: 'HEAD' });
            const contentLength = response.responseHeaders?.match(/content-length:\s*(\d+)/i);
            return contentLength ? parseInt(contentLength[1], 10) : null;
        } catch {
            return null;
        }
    }


    /**
     * Fetch JSON from API
     */
    async function fetchJson(url) {
        const response = await gmFetch(url, { responseType: 'json', timeout: 15000 });
        return response.response;
    }

    // =========================================================================
    // MP4 ANALYSIS
    // =========================================================================

    /**
     * Parse MP4 atom structure from ArrayBuffer
     */
    function parseMp4Atoms(buffer) {
        const view = new DataView(buffer);
        const atoms = [];
        let offset = 0;

        while (offset < buffer.byteLength - 8 && atoms.length < 50) {
            try {
                let size = view.getUint32(offset, false);
                const type = String.fromCharCode(
                    view.getUint8(offset + 4),
                    view.getUint8(offset + 5),
                    view.getUint8(offset + 6),
                    view.getUint8(offset + 7)
                );

                if (!/^[\x20-\x7E]{4}$/.test(type)) break;

                let actualSize = size;
                if (size === 1 && offset + 16 <= buffer.byteLength) {
                    actualSize = view.getUint32(offset + 8, false) * 0x100000000 + view.getUint32(offset + 12, false);
                }

                atoms.push({ type, size, actualSize, offset });
                if (size === 0) break;
                offset += size === 1 ? actualSize : size;
            } catch {
                break;
            }
        }
        return atoms;
    }

    /**
     * Analyze MP4 atom structure
     */
    async function analyzeMp4Structure(url, logToConsole = false) {
        try {
            const finalUrl = await resolveRedirectUrl(url);
            const response = await gmFetch(finalUrl, {
                responseType: 'arraybuffer',
                headers: { 'Range': 'bytes=0-65535' },
                timeout: 15000
            });

            const atoms = parseMp4Atoms(response.response);
            let moovOffset = null, mdatOffset = null;

            atoms.forEach(atom => {
                if (atom.type === 'moov') moovOffset = atom.offset;
                if (atom.type === 'mdat') mdatOffset = atom.offset;
            });

            const isFaststart = moovOffset !== null && mdatOffset !== null && moovOffset < mdatOffset;

            if (logToConsole && settings.analyzeMp4Atoms) {
                const filename = url.split('/').pop();
                console.group(`[MP4 Analysis] ${filename}`);
                atoms.forEach((a, i) => console.log(`${i + 1}. [${a.type}] offset: ${(a.offset / 1024).toFixed(1)}KB`));
                console.log(isFaststart ? '%c✓ FASTSTART' : '%c✗ NOT FASTSTART', `color: ${isFaststart ? 'green' : 'red'}; font-weight: bold`);
                console.groupEnd();
            }

            return { atoms, moovFound: moovOffset !== null, mdatFound: mdatOffset !== null, isFaststart };
        } catch {
            return null;
        }
    }

    // =========================================================================
    // MEDIA EXTRACTION
    // =========================================================================

    /**
     * Extract media URLs from post data
     */
    function extractMediaFromPost(postData, baseUrl, options = {}) {
        const { type = 'all', includeText = false } = options;
        const items = [];
        const post = postData.post || postData;
        const postTitle = post.title || post.substring || '';

        const shouldInclude = (path) => {
            if (type === 'all') return isVideo(path) || isImage(path);
            if (type === 'video') return isVideo(path);
            if (type === 'image') return isImage(path);
            return false;
        };

        // Add text content first if requested
        // API returns HTML content, mark it for proper rendering
        if (includeText && post.content?.trim()) {
            items.push({
                type: 'text',
                title: postTitle,
                content: post.content,
                isHtml: true, // Content from API is HTML
                isDownloadable: false
            });
        }

        // Main file
        if (post.file?.path && shouldInclude(post.file.path)) {
            items.push({
                url: toFullUrl(post.file.path, baseUrl),
                type: getFileType(post.file.path),
                name: post.file.name || post.file.path,
                postTitle,
                isDownloadable: true
            });
        }

        // Attachments
        if (Array.isArray(post.attachments)) {
            post.attachments.forEach(att => {
                if (att.path && shouldInclude(att.path)) {
                    items.push({
                        url: toFullUrl(att.path, baseUrl, att.server), // <- fixed here
                        type: getFileType(att.path),
                        name: att.name || att.path,
                        postTitle,
                        isDownloadable: true
                    });
                }
            });
        }

        // Embed (video only)
        if (type !== 'image' && post.embed?.url && isVideo(post.embed.url)) {
            items.push({
                url: post.embed.url,
                type: 'video',
                name: 'embed',
                postTitle,
                isDownloadable: true
            });
        }

        return items;
    }

    /**
     * Count file types in post
     */
    function countPostFiles(postData) {
        const post = postData.post || postData;
        const counts = { video: 0, image: 0, archive: 0, audio: 0, document: 0, other: 0 };

        if (post.file?.path) counts[getFileType(post.file.path)]++;
        if (Array.isArray(post.attachments)) {
            post.attachments.forEach(att => {
                if (att.path) counts[getFileType(att.path)]++;
            });
        }
        return counts;
    }

    // =========================================================================
    // URL PARSING
    // =========================================================================

    /**
     * Parse post URL to extract components
     */
    function parsePostUrl(url) {
        try {
            const urlObj = new URL(url);
            const match = urlObj.pathname.match(/^\/([^/]+)\/user\/([^/]+)\/post\/([^/]+)/);
            if (!match) return null;
            return {
                baseUrl: urlObj.origin,
                service: match[1],
                userId: match[2],
                postId: match[3]
            };
        } catch {
            return null;
        }
    }

    function parsePopularPostUrl(url) {
        try {
            const urlObj = new URL(url, location.origin);
            const m = urlObj.pathname.match(/^\/([^\/]+)\/user\/([^\/]+)\/post\/([^\/]+)/);
            if (!m) return null;
            return {
                baseUrl: urlObj.origin,
                service: m[1],
                userId: m[2],
                postId: m[3]
            };
        } catch {
            return null;
        }
    }

    const isUserPage = () => /\/[^/]+\/user\/[^/]+/.test(window.location.pathname);
    const isPopularPage = () => /^\/posts\/popular/.test(window.location.pathname);
    const isPostPage = () => /\/[^/]+\/user\/[^/]+\/post\/[^/]+/.test(window.location.pathname);

    /**
     * Get current page offset from URL (?o=XX)
     * Returns 0 for first page
     */
    function getPageOffset() {
        const params = new URLSearchParams(window.location.search);
        const offset = parseInt(params.get('o'), 10);
        return isNaN(offset) ? 0 : offset;
    }

    // =========================================================================
    // STATE MANAGEMENT
    // =========================================================================

    let videoThumbnailQueue = [];
    let activeVideoProcesses = 0;
    let apiQueue = [];
    let activeApiProcesses = 0;

    const userPostsCache = new Map(); // cacheKey -> Map(postId -> postData)
    const fetchedOffsets = new Map(); // cacheKey -> Set of fetched offsets
    const pendingBatchFetches = new Map();
    const postDataCache = new Map();
    const pendingRequests = new Map();

    let userAvatarUrl = null;
    let currentUserPath = null; // Track current creator for navigation detection
    let videoThumbnailObserver = null;

    // GIF control - track which elements have been set up
    const gifHoverSetup = new WeakSet();

    let galleryOverlay = null;
    let galleryMediaItems = [];
    let galleryPostUrl = null;
    let galleryGlide = null; // Glide.js instance
    let galleryDocumentHandlerBound = false;

    // Bulk selection state
    const selectedPosts = new Map(); // postUrl -> { parsed, postData }
    let bulkActionBar = null;
    let bulkDownloadInProgress = false;

    // Volume persistence across media players
    let persistedVolume = parseFloat(GM_getValue('betterui_volume', '1')) || 1;

    // Preload cache for gallery items
    const preloadedMedia = new Map();

    /**
     * Save volume level
     */
    function setPersistedVolume(volume) {
        persistedVolume = volume;
        GM_setValue('betterui_volume', volume.toString());
    }

    /**
     * Preload media items for smoother gallery experience
     */
    function preloadGalleryItems(items, startIndex = 0, count = 3) {
        const indicesToPreload = [];
        for (let i = 0; i < count; i++) {
            const idx = startIndex + i;
            if (idx < items.length && items[idx].type !== 'text') {
                indicesToPreload.push(idx);
            }
        }

        indicesToPreload.forEach(idx => {
            const item = items[idx];
            if (preloadedMedia.has(item.url)) return;

            if (item.type === 'image') {
                const img = new Image();
                img.src = item.url;
                preloadedMedia.set(item.url, img);
                debugLog('Preloading image:', item.url);
            } else if (item.type === 'video') {
                const video = document.createElement('video');
                video.preload = 'metadata';
                video.src = item.url;
                preloadedMedia.set(item.url, video);
                debugLog('Preloading video metadata:', item.url);
            }
        });
    }

    /**
     * Return popular, else Get current user path from URL
     */
    function getCurrentUserPath() {
        if (isPopularPage()) return 'popular';

        const match = window.location.pathname.match(/^\/([^/]+)\/user\/([^/]+)/);
        return match ? `${match[1]}/${match[2]}` : null;
    }

    /**
     * Handle navigation to new creator
     */
    function handleNavigationChange() {
        const newUserPath = getCurrentUserPath();

        if (newUserPath !== currentUserPath) {
            currentUserPath = newUserPath;
            postDataCache.clear();
            userPostsCache.clear();
            fetchedOffsets.clear();
            pendingBatchFetches.clear();
            selectedPosts.clear();
            updateBulkActionBar();

            // ✔ User pages fetch avatar
            if (isUserPage()) getUserAvatarUrl();

            // ✔ Always setup cards including Popular page
            setTimeout(() => setupPostCards(), 100);
        }
    }

    /**
     * Initialize navigation observer
     */
    function initNavigationObserver() {
        currentUserPath = getCurrentUserPath();

        // Listen for browser back/forward
        window.addEventListener('popstate', handleNavigationChange);

        // Intercept pushState and replaceState
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        history.pushState = function (...args) {
            originalPushState.apply(this, args);
            handleNavigationChange();
        };

        history.replaceState = function (...args) {
            originalReplaceState.apply(this, args);
            handleNavigationChange();
        };

        // Also observe link clicks that might trigger SPA navigation
        document.addEventListener('click', (e) => {
            const link = e.target.closest('a[href*="/user/"]');
            if (link && link.href.includes('/user/')) {
                // Check after navigation completes
                setTimeout(handleNavigationChange, 200);
            }
        }, true);

        debugLog('Navigation observer initialized');
    }

    /**
    * RETRY ALL FAILED / UNPROCESSED THUMBNAILS
    */
    function retryFailedThumbnails() {
        const retryIndicators = document.querySelectorAll('.video-play-indicator.retry-available');
        let count = 0;

        retryIndicators.forEach(indicator => {
            const link = indicator.closest('a.fancy-link');
            if (!link) return;

            console.log('[BetterUI] Retrying via bulk:', link.href);

            // Use the SAME pathway individual clicks use
            handleVideoThumbnailRetry(link);

            count++;
        });

        if (count > 0) {
            showToast(`Retrying ${count} thumbnails…`, 'info');
        } else {
            showToast(`No red retry thumbnails found.`, 'info');
        }
    }

    // =========================================================================
    // STYLES
    // =========================================================================

    function injectStyles() {
        if (document.getElementById('betterui-styles')) return;

        // Load Material Symbols font
        const fontLink = document.createElement('link');
        fontLink.rel = 'stylesheet';
        fontLink.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&display=swap';
        document.head.appendChild(fontLink);

        const css = `
        /* Material Icons */
        .material-symbols-rounded {
            font-family: 'Material Symbols Rounded' !important;
            font-weight: normal !important;
            font-style: normal !important;
            font-size: 24px !important;
            line-height: 1 !important;
            letter-spacing: normal !important;
            text-transform: none !important;
            display: inline-block !important;
            white-space: nowrap !important;
            word-wrap: normal !important;
            direction: ltr !important;
            -webkit-font-feature-settings: 'liga' !important;
            font-feature-settings: 'liga' !important;
            -webkit-font-smoothing: antialiased !important;
        }
        .icon-sm { font-size: 18px !important; }
        .icon-md { font-size: 24px !important; }
        .icon-lg { font-size: 32px !important; }
        .icon-filled { font-variation-settings: 'FILL' 1 !important; }

        /* Card Layout */
        .site-section--user .card-list .card-list__items {
            --card-size: 242px !important;
        }

        .card-list--legacy, .card-list__items {
            display: flex !important;
            flex-wrap: wrap !important;
            gap: var(--betterui-gap) !important;
            padding: 16px !important;
        }
        .card-list .card-list__items .post-card {
            flex: 0 0 calc((100% - (var(--betterui-gap) * (var(--betterui-cols) - 1))) / var(--betterui-cols)) !important;
            max-width: calc((100% - (var(--betterui-gap) * (var(--betterui-cols) - 1))) / var(--betterui-cols)) !important;
            height: initial;
            margin: 0 !important;
            background: #1e1e1e !important;
            text-decoration: none !important;
            display: flex !important;
            flex-direction: column !important;
            overflow: hidden !important;
            border-radius: 8px !important;
            box-sizing: border-box !important;
        }
        .card-list .card-list__items .post-card > .fancy-link:hover,
        .card-list .card-list__items .post-card > .fancy-link:focus,
        .card-list .card-list__items .post-card > .fancy-link:active {
            background-color: #000;
            border-bottom-color: var(--local-color1-primary);
        }
        .card-list__items .post-card .card-thumbnail-wrapper {
            position: relative !important;
            height: var(--card-size);
            width: 100% !important;
            overflow: hidden !important;
            background: #1a1a1a !important;
        }
        .card-list__items .post-card .card-thumbnail-wrapper img,
        .card-list__items .post-card .post__thumbnail .image-link,
        .card-list__items .post-card .post__thumbnail img {
            width: 100%;
            height: 100%;
            object-fit: cover !important;
            transition: opacity 0.15s ease !important;
        }

        /* Generated thumbnails */
        .generated-thumbnail, .thumbnail-placeholder, .thumbnail-loading, .avatar-placeholder {
            position: absolute !important;
            top: 0 !important;
            left: 0 !important;
            width: 100% !important;
            height: 100% !important;
        }
        .generated-thumbnail img, .avatar-placeholder img {
            width: 100% !important;
            height: 100% !important;
            object-fit: cover !important;
        }

        /* File count overlay */
        .file-count-overlay {
            position: absolute !important;
            bottom: 8px !important;
            left: 8px !important;
            display: flex !important;
            gap: 4px !important;
            z-index: 10 !important;
            pointer-events: none !important;
        }
        .file-count-badge {
            background: rgba(0,0,0,0.75) !important;
            color: white !important;
            padding: 2px 6px !important;
            border-radius: 4px !important;
            font-size: 11px !important;
            display: flex !important;
            align-items: center !important;
            gap: 3px !important;
        }

        /* Video play indicator */
        .video-play-indicator {
            position: absolute !important;
            top: 50% !important;
            left: 50% !important;
            transform: translate(-50%, -50%) !important;
            background: rgba(0,0,0,0.7) !important;
            color: white !important;
            width: 50px !important;
            height: 50px !important;
            border-radius: 50% !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            font-size: 20px !important;
            z-index: 5 !important;
            pointer-events: none !important;
            transition: transform 0.2s ease, background 0.2s ease !important;
        }
        .video-play-indicator.retry-available {
            background: rgba(200,50,50,0.8) !important;
            cursor: pointer !important;
            pointer-events: auto !important;
        }
        .video-play-indicator.ai-retry-available {
            background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
            cursor: pointer !important;
            pointer-events: auto !important;
        }
        .video-play-indicator.ai-retry-available:hover {
            background: linear-gradient(135deg, #818cf8 0%, #a78bfa 100%) !important;
            transform: scale(1.1) !important;
        }
        .video-duration {
            position: absolute !important;
            bottom: 8px !important;
            right: 8px !important;
            background: rgba(0,0,0,0.75) !important;
            color: white !important;
            padding: 2px 6px !important;
            border-radius: 4px !important;
            font-size: 11px !important;
            z-index: 10 !important;
        }

        .video-duration-badge {
            background: rgba(0,0,0,0.75);
            color: white;
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 11px;
            margin-left: auto;
        }

        /* GIF placeholder for hover-to-play */
        .gif-placeholder {
            position: absolute !important;
            top: 0 !important;
            left: 0 !important;
            width: 100% !important;
            height: 100% !important;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%) !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            color: #4ade80 !important;
            z-index: 3 !important;
            pointer-events: none !important;
            transition: opacity 0.15s ease !important;
        }
        .gif-placeholder .material-symbols-rounded {
            font-size: 48px !important;
            opacity: 0.8 !important;
        }

        /* Text content area */
        .card-list__items .post-card .post-card__header,
        .card-list__items .post-card .model-label,
        .card-list__items .post-card .post-card__footer {
            padding: 12px !important;
            display: flex !important;
            flex-direction: column !important;
            gap: 6px !important;
            flex-grow: 1 !important;
        }

        /* Post title */
        .card-list__items .post-card .post-card__header {
            font-size: 14px !important;
            font-weight: 500 !important;
            text-shadow: none !important;
            color: #e0e0e0 !important;
            line-height: 1.4 !important;
            display: -webkit-box !important;
            -webkit-line-clamp: 2 !important;
            -webkit-box-orient: vertical !important;
            overflow: hidden !important;
            word-break: break-word !important;
        }

        /* Model label */
        .model-label {
            font-size: 12px;
            font-weight: 600;
            color: #fff;
            background: rgba(0,0,0,0.6);
            display: inline-block;
            padding: 2px 6px;
            border-radius: 4px;
            margin-bottom: 4px;
        }

        /* Date styling */
        .card-list__items .post-card .post-card__footer time {
            font-size: 11px !important;
            color: #fff !important;
        }

        /* Attachment count text */
        .card-list__items .post-card .post-card__footer > div > div > div {
            font-size: 11px !important;
            color: #fff !important;
        }

        /* Image collage */
        .image-collage {
            top: 0 !important;
            left: 0 !important;
            width: 100% !important;
            height: 100% !important;
            display: flex !important;
            gap: 2px !important;
        }
        .image-collage.collage-2,.image-collage.collage-3 { flex-wrap: nowrap !important; flex-direction: rowcolumn !important;}
        .image-collage.collage-2 .collage-img { width: 50% !important; height: 100% !important; }
        .image-collage.collage-3 .collage-left { width: 60% !important; height: 100% !important; }
        .image-collage.collage-3 .collage-right { width: 40% !important; display: flex !important; flex-direction: column !important; gap: 2px !important; }
        .image-collage.collage-3 .collage-right .collage-img { width: 100% !important; height: 48% !important; }
        .collage-img { object-fit: cover !important; border-radius: 5px !important; }
        .collage-more-indicator {
            position: absolute !important;
            bottom: 6px !important;
            left: 6px !important;
            background: rgba(0,0,0,0.75) !important;
            color: white !important;
            padding: 2px 8px !important;
            border-radius: 4px !important;
            font-size: 11px !important;
            z-index: 10 !important;
        }

        /* Responsive breakpoints */
        @media (max-width: 1400px) { .card-list .card-list__items .post-card { flex: 0 0 calc((100% - 48px) / 4) !important; max-width: calc((100% - 48px) / 4) !important; } }
        @media (max-width: 1100px) { .card-list .card-list__items .post-card { flex: 0 0 calc((100% - 32px) / 3) !important; max-width: calc((100% - 32px) / 3) !important; } }
        @media (max-width: 768px) { .card-list .card-list__items .post-card { flex: 0 0 calc((100% - 12px) / 2) !important; max-width: calc((100% - 12px) / 2) !important; } }
        @media (max-width: 480px) { .card-list .card-list__items .post-card { flex: 0 0 100% !important; max-width: 100% !important; } }

        /* Gallery Overlay */
        .media-gallery-overlay {
            position: fixed !important;
            inset: 0 !important;
            background: rgba(0, 0, 0, 0.95) !important;
            z-index: 99999 !important;
            display: flex !important;
            flex-direction: column !important;
            align-items: center !important;
            justify-content: center !important;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.2s, visibility 0.2s !important;
        }
        .media-gallery-overlay.active { opacity: 1; visibility: visible; }

        .gallery-close, .gallery-download-all {
            position: absolute !important;
            top: 16px !important;
            height: 44px !important;
            background: rgba(255, 255, 255, 0.1) !important;
            border: none !important;
            border-radius: 22px !important;
            color: white !important;
            cursor: pointer !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            transition: background 0.2s !important;
            z-index: 10 !important;
        }
        .gallery-close { right: 16px !important; width: 44px !important; font-size: 24px !important; }
        .gallery-download-all { right: 70px !important; padding: 0 16px !important; gap: 8px !important; font-size: 14px !important; }
        .gallery-close:hover, .gallery-download-all:hover { background: rgba(255, 255, 255, 0.25) !important; }
        .gallery-download-all:disabled { opacity: 0.5 !important; cursor: not-allowed !important; }

        .gallery-nav {
            position: absolute !important;
            top: 50% !important;
            transform: translateY(-50%) !important;
            width: 50px !important;
            height: 50px !important;
            background: rgba(255, 255, 255, 0.1) !important;
            border: none !important;
            border-radius: 50% !important;
            color: white !important;
            font-size: 24px !important;
            cursor: pointer !important;
            transition: background 0.2s !important;
            z-index: 10 !important;
        }
        .gallery-nav.prev { left: 16px !important; }
        .gallery-nav.next { right: 16px !important; }
        .gallery-nav:hover { background: rgba(255, 255, 255, 0.25) !important; }
        .gallery-nav:disabled { opacity: 0.3 !important; cursor: not-allowed !important; }

        /* Glide.js Core Styles */
        .glide { position: relative; width: 90vw; max-width: 90vw; perspective: 1200px; }
        .glide__track { overflow: visible !important; }
        .glide__slides {
            display: flex;
            flex-wrap: nowrap;
            will-change: transform;
            backface-visibility: hidden;
            transform-style: preserve-3d;
            touch-action: pan-Y;
            padding: 0;
            margin: 0;
            list-style: none;
            white-space: nowrap;
        }
        .glide__slide {
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            height: 70vh !important;
            flex-shrink: 0 !important;
            white-space: normal !important;
            user-select: none !important;
            -webkit-touch-callout: none !important;
            -webkit-tap-highlight-color: transparent !important;
            transition: transform 0.4s ease, opacity 0.4s ease !important;
            transform-style: preserve-3d !important;
        }
        .glide__slide .slide-inner {
            position: relative !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            width: 100% !important;
            height: 100% !important;
            transition: transform 0.4s ease, opacity 0.4s ease !important;
            transform-style: preserve-3d !important;
            border-radius: 8px !important;
            overflow: hidden !important;
            background: rgba(0,0,0,0.3) !important;
        }
        .glide__slide.is-active .slide-inner {
            transform: perspective(1200px) rotateY(0deg) scale(1) !important;
            opacity: 1 !important;
            z-index: 10 !important;
        }
        .glide__slide.is-prev .slide-inner {
            transform-origin: 100% 50% !important;
            transform: perspective(1200px) rotateY(35deg) scale(0.85) translateX(10%) !important;
            opacity: 0.7 !important;
            z-index: 5 !important;
        }
        .glide__slide.is-next .slide-inner {
            transform-origin: 0% 50% !important;
            transform: perspective(1200px) rotateY(-35deg) scale(0.85) translateX(-10%) !important;
            opacity: 0.7 !important;
            z-index: 5 !important;
        }
        .glide__slide.is-far-prev .slide-inner {
            transform-origin: 100% 50% !important;
            transform: perspective(1200px) rotateY(50deg) scale(0.7) translateX(20%) !important;
            opacity: 0.4 !important;
            z-index: 1 !important;
        }
        .glide__slide.is-far-next .slide-inner {
            transform-origin: 0% 50% !important;
            transform: perspective(1200px) rotateY(-50deg) scale(0.7) translateX(-20%) !important;
            opacity: 0.4 !important;
            z-index: 1 !important;
        }
        .glide__slide img,
        .glide__slide video {
            max-width: 100% !important;
            max-height: 70vh !important;
            object-fit: contain !important;
        }
        .glide__slide video {
            outline: none !important;
        }
        .glide__slide audio {
            min-width: 300px !important;
        }
        .glide--dragging { cursor: grabbing !important; }

        .gallery-thumbnails {
            display: flex !important;
            gap: 8px !important;
            padding: 16px !important;
            max-width: 90vw !important;
            overflow-x: auto !important;
            margin-top: 16px !important;
        }
        .gallery-thumb {
            width: 60px !important;
            height: 60px !important;
            border-radius: 4px !important;
            cursor: pointer !important;
            opacity: 0.5 !important;
            border: 2px solid transparent !important;
            flex-shrink: 0 !important;
            background: #333 !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            overflow: hidden !important;
        }
        .gallery-thumb.active { opacity: 1 !important; border-color: white !important; }
        .gallery-thumb img { width: 100% !important; height: 100% !important; object-fit: cover !important; }

        .gallery-counter {
            position: absolute !important;
            bottom: 16px !important;
            left: 50% !important;
            transform: translateX(-50%) !important;
            color: white !important;
            font-size: 14px !important;
            background: rgba(0,0,0,0.5) !important;
            padding: 4px 12px !important;
            border-radius: 12px !important;
        }
        .gallery-info {
            position: absolute !important;
            top: 16px !important;
            left: 16px !important;
            color: white !important;
            font-size: 14px !important;
            max-width: 50% !important;
            overflow: hidden !important;
            text-overflow: ellipsis !important;
            white-space: nowrap !important;
        }
        .gallery-item-download {
            position: absolute !important;
            top: 16px !important;
            right: 16px !important;
            width: 44px !important;
            height: 44px !important;
            background: rgba(0, 0, 0, 0.7) !important;
            border: none !important;
            border-radius: 50% !important;
            color: white !important;
            font-size: 20px !important;
            cursor: pointer !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            transition: background 0.2s, transform 0.2s !important;
            z-index: 10 !important;
        }
        .gallery-item-download:hover { background: rgba(0, 0, 0, 0.9) !important; transform: scale(1.1) !important; }
        .gallery-item-download.downloading { animation: pulse 1s infinite !important; }
        @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }

        .gallery-loading {
            display: flex !important;
            flex-direction: column !important;
            align-items: center !important;
            gap: 12px !important;
            color: white !important;
        }
        .loading-spinner {
            width: 48px !important;
            height: 48px !important;
            border: 4px solid rgba(255,255,255,0.2) !important;
            border-top-color: rgba(255,255,255,0.8) !important;
            border-radius: 50% !important;
            animation: spin 1s linear infinite !important;
        }
        @keyframes spin { to { transform: rotate(360deg); } }

        /* Gallery Text Content */
        .gallery-text-content {
            max-width: 800px !important;
            max-height: 70vh !important;
            overflow-y: auto !important;
            padding: 24px !important;
            background: #1a1a1a !important;
            border-radius: 8px !important;
            color: #ddd !important;
            font-size: 15px !important;
            line-height: 1.6 !important;
        }
        .gallery-text-content h3 {
            color: #fff !important;
            font-size: 20px !important;
            margin: 0 0 16px 0 !important;
            padding-bottom: 12px !important;
            border-bottom: 1px solid #333 !important;
        }
        .gallery-text-content a { color: #4ade80 !important; text-decoration: none !important; }
        .gallery-text-content a:hover { text-decoration: underline !important; }
        .gallery-text-body { word-wrap: break-word !important; }
        .gallery-text-body p { margin: 0 0 12px 0 !important; }
        .gallery-text-body img { max-width: 100% !important; height: auto !important; border-radius: 4px !important; margin: 8px 0 !important; }
        .gallery-text-body ul, .gallery-text-body ol { margin: 0 0 12px 20px !important; padding: 0 !important; }
        .gallery-text-body li { margin-bottom: 4px !important; }
        .gallery-text-body blockquote { border-left: 3px solid #444 !important; margin: 12px 0 !important; padding-left: 12px !important; color: #aaa !important; }
        .gallery-text-body pre, .gallery-text-body code { background: #2a2a2a !important; border-radius: 4px !important; padding: 2px 6px !important; font-family: monospace !important; }
        .gallery-text-body pre { padding: 12px !important; overflow-x: auto !important; }
        .gallery-text-badge {
            display: inline-block !important;
            background: #333 !important;
            color: #aaa !important;
            font-size: 10px !important;
            padding: 2px 6px !important;
            border-radius: 3px !important;
            margin-bottom: 12px !important;
        }

        /* Download Toast */
        .download-toast {
            position: fixed !important;
            bottom: 20px !important;
            right: 20px !important;
            width: 320px !important;
            background: #1a1a1a !important;
            border: 1px solid #333 !important;
            border-radius: 12px !important;
            box-shadow: 0 8px 32px rgba(0,0,0,0.4) !important;
            z-index: 999999 !important;
            padding: 16px !important;
            opacity: 0;
            visibility: hidden;
            transform: translateY(20px);
            transition: all 0.3s !important;
        }
        .download-toast.active { opacity: 1; visibility: visible; transform: translateY(0); }
        .download-toast-header { display: flex !important; justify-content: space-between !important; align-items: center !important; margin-bottom: 12px !important; }
        .download-toast-title { color: #fff !important; font-size: 14px !important; font-weight: 600 !important; }
        .download-toast-close { background: transparent !important; border: none !important; color: #888 !important; font-size: 16px !important; cursor: pointer !important; }
        .download-toast-progress { height: 6px !important; background: #333 !important; border-radius: 3px !important; overflow: hidden !important; margin-bottom: 10px !important; }
        .download-toast-progress-bar { height: 100% !important; background: linear-gradient(90deg, #4ade80, #22c55e) !important; width: 0%; transition: width 0.3s !important; }
        .download-toast-status { color: #aaa !important; font-size: 12px !important; margin-bottom: 4px !important; }
        .download-toast-filename { color: #888 !important; font-size: 11px !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; }

        /* Settings Button */
        .betterui-settings-btn {
            position: fixed !important;
            top: 16px !important;
            left: 16px !important;
            height: 44px !important;
            background: rgba(30, 30, 30, 0.95) !important;
            border: 1px solid #333 !important;
            border-radius: 22px !important;
            color: #aaa !important;
            font-size: 20px !important;
            cursor: pointer !important;
            display: flex !important;
            align-items: center !important;
            z-index: 99990 !important;
            transition: all 0.3s !important;
            overflow: hidden !important;
            padding: 0 14px !important;
        }
        .betterui-settings-btn:hover { background: rgba(40, 40, 40, 0.98) !important; color: #fff !important; }
        .betterui-settings-btn .settings-label {
            max-width: 0 !important;
            opacity: 0 !important;
            overflow: hidden !important;
            margin-left: 0 !important;
            transition: all 0.3s !important;
            font-size: 13px !important;
            white-space: nowrap !important;
        }
        .betterui-settings-btn:hover .settings-label { max-width: 200px !important; opacity: 1 !important; margin-left: 8px !important; }

        /* Retry Failed Thumbnails Button */
        .betterui-retry-thumb-btn {
            position: fixed !important;
            top: 16px !important;
            left: 180px !important;
            height: 44px !important;
            background: rgba(30, 30, 30, 0.95) !important;
            border: 1px solid #333 !important;
            border-radius: 22px !important;
            color: #aaa !important;
            font-size: 20px !important;
            cursor: pointer !important;
            display: flex !important;
            align-items: center !important;
            z-index: 99990 !important;
            transition: all 0.3s !important;
            overflow: hidden !important;
            padding: 0 14px !important;
        }

        .betterui-retry-thumb-btn:hover {
            background: rgba(40, 40, 40, 0.98) !important;
            color: #fff !important;
        }

        .betterui-retry-thumb-btn .retry-label {
            max-width: 0 !important;
            opacity: 0 !important;
            overflow: hidden !important;
            margin-left: 0 !important;
            transition: all 0.3s !important;
            font-size: 13px !important;
            white-space: nowrap !important;
        }

        .betterui-retry-thumb-btn:hover .retry-label {
            max-width: 200px !important;
            opacity: 1 !important;
            margin-left: 8px !important;
        }

        /* Settings Modal */
        .betterui-settings-modal {
            position: fixed !important;
            inset: 0 !important;
            background: rgba(0, 0, 0, 0.85) !important;
            z-index: 999999 !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.2s, visibility 0.2s !important;
        }
        .betterui-settings-modal.active { opacity: 1; visibility: visible; }
        .betterui-settings-content {
            background: #1a1a1a !important;
            border: 1px solid #333 !important;
            border-radius: 12px !important;
            width: 90% !important;
            max-width: 480px !important;
            max-height: 80vh !important;
            overflow-y: auto !important;
        }
        .betterui-settings-header {
            display: flex !important;
            justify-content: space-between !important;
            align-items: center !important;
            padding: 16px 20px !important;
            border-bottom: 1px solid #333 !important;
        }
        .betterui-settings-header h2 { color: #fff !important; font-size: 18px !important; margin: 0 !important; }
        .betterui-settings-close { background: transparent !important; border: none !important; color: #888 !important; font-size: 24px !important; cursor: pointer !important; }
        .betterui-settings-body { padding: 20px !important; }
        .betterui-setting-group { margin-bottom: 20px !important; }
        .betterui-setting-group h3 { color: #888 !important; font-size: 11px !important; text-transform: uppercase !important; margin: 0 0 12px 0 !important; }
        .betterui-setting-item {
            display: flex !important;
            justify-content: space-between !important;
            align-items: center !important;
            padding: 12px 0 !important;
            border-bottom: 1px solid #2a2a2a !important;
        }
        .betterui-setting-item:last-child { border-bottom: none !important; }
        .betterui-setting-info { flex: 1 !important; margin-right: 16px !important; }
        .betterui-setting-label { color: #fff !important; font-size: 14px !important; margin-bottom: 4px !important; }
        .betterui-setting-desc { color: #666 !important; font-size: 12px !important; }

        /* Toggle Switch */
        .betterui-toggle { position: relative !important; width: 44px !important; height: 24px !important; flex-shrink: 0 !important; display: inline-block !important; cursor: pointer !important; }
        .betterui-toggle input {
            position: absolute !important;
            top: 0 !important;
            left: 0 !important;
            width: 100% !important;
            height: 100% !important;
            opacity: 0 !important;
            cursor: pointer !important;
            z-index: 2 !important;
            margin: 0 !important;
            padding: 0 !important;
        }
        .betterui-toggle-slider {
            position: absolute !important;
            inset: 0 !important;
            background: #333 !important;
            border-radius: 12px !important;
            pointer-events: none !important;
            transition: 0.3s !important;
        }
        .betterui-toggle-slider:before {
            content: "" !important;
            position: absolute !important;
            height: 18px !important;
            width: 18px !important;
            left: 3px !important;
            bottom: 3px !important;
            background: #888 !important;
            border-radius: 50% !important;
            transition: 0.3s !important;
        }
        .betterui-toggle input:checked + .betterui-toggle-slider { background: #22c55e !important; }
        .betterui-toggle input:checked + .betterui-toggle-slider:before { transform: translateX(20px) !important; background: #fff !important; }

        /* Select dropdown */
        .betterui-select {
            background: #2a2a2a !important;
            border: 1px solid #444 !important;
            border-radius: 6px !important;
            color: #fff !important;
            padding: 8px 12px !important;
            font-size: 13px !important;
            cursor: pointer !important;
            min-width: 150px !important;
        }
        .betterui-select:focus { outline: none !important; border-color: #6366f1 !important; }

        /* Text input */
        .betterui-input {
            background: #2a2a2a !important;
            border: 1px solid #444 !important;
            border-radius: 6px !important;
            color: #fff !important;
            padding: 8px 12px !important;
            font-size: 13px !important;
            min-width: 180px !important;
        }
        .betterui-input:focus { outline: none !important; border-color: #6366f1 !important; }
        .betterui-input::placeholder { color: #666 !important; }

        /* AI key items - hidden by default */
        .ai-key-item { display: none !important; }
        .ai-key-item.visible { display: flex !important; }

        /* Bulk Selection */
        .post-select-checkbox {
            position: absolute !important;
            top: 8px !important;
            left: 8px !important;
            width: 24px !important;
            height: 24px !important;
            background: rgba(0,0,0,0.7) !important;
            border: 2px solid #666 !important;
            border-radius: 4px !important;
            cursor: pointer !important;
            z-index: 15 !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            transition: all 0.2s ease !important;
            opacity: 0.6 !important;
        }
        .post-select-checkbox:hover {
            opacity: 1 !important;
            border-color: #22c55e !important;
        }
        .post-select-checkbox.selected {
            background: #22c55e !important;
            border-color: #22c55e !important;
            opacity: 1 !important;
        }
        .post-select-checkbox .material-symbols-rounded {
            color: #fff !important;
            font-size: 18px !important;
            display: none !important;
        }
        .post-select-checkbox.selected .material-symbols-rounded {
            display: block !important;
        }
        .card-thumbnail-wrapper:hover .post-select-checkbox {
            opacity: 1 !important;
        }

        /* Bulk Action Bar */
        .bulk-action-bar {
            position: fixed !important;
            bottom: 0 !important;
            left: 0 !important;
            right: 0 !important;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%) !important;
            border-top: 1px solid #333 !important;
            padding: 12px 20px !important;
            display: flex !important;
            align-items: center !important;
            justify-content: space-between !important;
            z-index: 9999 !important;
            transform: translateY(100%) !important;
            transition: transform 0.3s ease !important;
            box-shadow: 0 -4px 20px rgba(0,0,0,0.5) !important;
        }
        .bulk-action-bar.visible {
            transform: translateY(0) !important;
        }
        .bulk-action-bar .selection-info {
            display: flex !important;
            align-items: center !important;
            gap: 16px !important;
            color: #fff !important;
            font-size: 14px !important;
        }
        .bulk-action-bar .selection-count {
            font-weight: 600 !important;
            color: #22c55e !important;
        }
        .bulk-action-bar .bulk-actions {
            display: flex !important;
            gap: 10px !important;
        }
        .bulk-action-bar button {
            display: flex !important;
            align-items: center !important;
            gap: 6px !important;
            padding: 8px 16px !important;
            border: none !important;
            border-radius: 6px !important;
            font-size: 13px !important;
            font-weight: 500 !important;
            cursor: pointer !important;
            transition: all 0.2s ease !important;
        }
        .bulk-action-bar .btn-download {
            background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%) !important;
            color: #fff !important;
        }
        .bulk-action-bar .btn-download:hover {
            background: linear-gradient(135deg, #16a34a 0%, #15803d 100%) !important;
        }
        .bulk-action-bar .btn-download:disabled {
            background: #444 !important;
            cursor: not-allowed !important;
        }
        .bulk-action-bar .btn-clear {
            background: #333 !important;
            color: #fff !important;
        }
        .bulk-action-bar .btn-clear:hover {
            background: #444 !important;
        }
        .bulk-action-bar .btn-select-all {
            background: transparent !important;
            color: #888 !important;
            border: 1px solid #444 !important;
        }
        .bulk-action-bar .btn-select-all:hover {
            background: #333 !important;
            color: #fff !important;
        }
        .bulk-progress {
            display: flex !important;
            align-items: center !important;
            gap: 10px !important;
            color: #fff !important;
        }
        .bulk-progress-bar {
            width: 200px !important;
            height: 6px !important;
            background: #333 !important;
            border-radius: 3px !important;
            overflow: hidden !important;
        }
        .bulk-progress-fill {
            height: 100% !important;
            background: #22c55e !important;
            transition: width 0.2s ease !important;
        }

        /* Toast notifications */
        .betterui-toast {
            position: fixed !important;
            bottom: 80px !important;
            left: 50% !important;
            transform: translateX(-50%) translateY(20px) !important;
            background: #1a1a2e !important;
            color: #fff !important;
            padding: 12px 24px !important;
            border-radius: 8px !important;
            font-size: 14px !important;
            z-index: 10001 !important;
            opacity: 0 !important;
            transition: all 0.3s ease !important;
            box-shadow: 0 4px 20px rgba(0,0,0,0.4) !important;
        }
        .betterui-toast.visible {
            opacity: 1 !important;
            transform: translateX(-50%) translateY(0) !important;
        }
        .betterui-toast-success {
            border-left: 4px solid #22c55e !important;
        }
        .betterui-toast-error {
            border-left: 4px solid #ef4444 !important;
        }
        .betterui-toast-info {
            border-left: 4px solid #3b82f6 !important;
        }
        `;

        const style = document.createElement('style');
        style.id = 'betterui-styles';
        style.textContent = css;
        document.head.appendChild(style);
    }

    function applyCardColumns() {
        const cols = settings.cardColumns || 6;
        document.documentElement.style.setProperty('--betterui-cols', cols);

        // dynamically recompute gap to preserve look
        const perGap = 80 / (cols - 1);  // 80px total gap from original layout
        document.documentElement.style.setProperty('--betterui-gap', perGap + 'px');
    }

    // =========================================================================
    // SETTINGS UI
    // =========================================================================

    function createSettingsUI() {
        const btn = document.createElement('button');
        btn.className = 'betterui-settings-btn';
        btn.innerHTML = `${icon('settings', 'sm')}<span class="settings-label">BetterUI Settings</span>`;
        btn.addEventListener('click', () => {
            document.getElementById('betterui-settings-modal')?.classList.add('active');
        });
        document.body.appendChild(btn);

        const retryRedBtn = document.createElement('button');
        retryRedBtn.className = 'betterui-retry-thumb-btn';
        retryRedBtn.innerHTML = `${icon('refresh', 'sm')}<span class="retry-label">Retry Failed</span>`;
        retryRedBtn.addEventListener('click', retryFailedThumbnails);
        document.body.appendChild(retryRedBtn);

        const modal = document.createElement('div');
        modal.className = 'betterui-settings-modal';
        modal.id = 'betterui-settings-modal';
        modal.innerHTML = `
            <div class="betterui-settings-content">
                <div class="betterui-settings-header">
                    <h2>BetterUI Settings</h2>
                    <button class="betterui-settings-close">${icon('close', 'md')}</button>
                </div>
                <div class="betterui-settings-body">
                    <div class="betterui-setting-group">
                        <h3>Layout</h3>
                            <div class="betterui-setting-item">
                                <div class="betterui-setting-info">
                                    <div class="betterui-setting-label">Card Columns</div>
                                    <div class="betterui-setting-desc">Number of cards displayed per row (4–8)</div>
                                </div>
                                <select data-setting-select="cardColumns">
                                    <option value="4">4</option>
                                    <option value="5">5</option>
                                    <option value="6">6</option>
                                    <option value="7">7</option>
                                    <option value="8">8</option>
                                </select>
                            </div>
                        <h3>Performance</h3>
                        <div class="betterui-setting-item">
                            <div class="betterui-setting-info">
                                <div class="betterui-setting-label">GIF hover-to-play</div>
                                <div class="betterui-setting-desc">GIFs pause when not hovered (thumbnails) or inactive (gallery)</div>
                            </div>
                            <label class="betterui-toggle">
                                <input type="checkbox" data-setting="pauseGifsWhenHidden">
                                <span class="betterui-toggle-slider"></span>
                            </label>
                        </div>
                    </div>
                    <div class="betterui-setting-group">
                        <h3>Downloads</h3>
                        <div class="betterui-setting-item">
                            <div class="betterui-setting-info">
                                <div class="betterui-setting-label">Prefix filenames with post title</div>
                                <div class="betterui-setting-desc">Add post title to downloaded files</div>
                            </div>
                            <label class="betterui-toggle">
                                <input type="checkbox" data-setting="prefixFilenamesWithTitle">
                                <span class="betterui-toggle-slider"></span>
                            </label>
                        </div>
                    </div>
                    <div class="betterui-setting-group">
                        <h3>AI Thumbnails</h3>
                        <div class="betterui-setting-item">
                            <div class="betterui-setting-info">
                                <div class="betterui-setting-label">AI Provider</div>
                                <div class="betterui-setting-desc">Select AI for smart frame selection</div>
                            </div>
                            <select data-setting-select="aiProvider" class="betterui-select">
                                <option value="none">Disabled</option>
                                <option value="claude">Claude (Anthropic)</option>
                                <option value="gemini">Gemini (Google)</option>
                            </select>
                        </div>
                        <div class="betterui-setting-item ai-key-item" data-provider="claude">
                            <div class="betterui-setting-info">
                                <div class="betterui-setting-label">Claude API Key</div>
                                <div class="betterui-setting-desc">Get from console.anthropic.com</div>
                            </div>
                            <input type="password" class="betterui-input" data-api-key="claude" placeholder="sk-ant-...">
                        </div>
                        <div class="betterui-setting-item ai-key-item" data-provider="gemini">
                            <div class="betterui-setting-info">
                                <div class="betterui-setting-label">Gemini API Key</div>
                                <div class="betterui-setting-desc">Get from aistudio.google.com</div>
                            </div>
                            <input type="password" class="betterui-input" data-api-key="gemini" placeholder="AIza...">
                        </div>
                        <div class="betterui-setting-item">
                            <div class="betterui-setting-info">
                                <div class="betterui-setting-label">Auto-fallback to AI</div>
                                <div class="betterui-setting-desc">Automatically try AI when standard thumbnail fails</div>
                            </div>
                            <label class="betterui-toggle">
                                <input type="checkbox" data-setting="aiAutoFallback">
                                <span class="betterui-toggle-slider"></span>
                            </label>
                        </div>
                    </div>
                    <div class="betterui-setting-group">
                        <h3>Developer</h3>
                        <div class="betterui-setting-item">
                            <div class="betterui-setting-info">
                                <div class="betterui-setting-label">Debug mode</div>
                                <div class="betterui-setting-desc">Log debug messages to console</div>
                            </div>
                            <label class="betterui-toggle">
                                <input type="checkbox" data-setting="debug">
                                <span class="betterui-toggle-slider"></span>
                            </label>
                        </div>
                        <div class="betterui-setting-item">
                            <div class="betterui-setting-info">
                                <div class="betterui-setting-label">Analyze MP4 atoms</div>
                                <div class="betterui-setting-desc">Log MP4 structure to console</div>
                            </div>
                            <label class="betterui-toggle">
                                <input type="checkbox" data-setting="analyzeMp4Atoms">
                                <span class="betterui-toggle-slider"></span>
                            </label>
                        </div>
                    </div>
                </div>
            </div>
        `;

        // Append to DOM first
        document.body.appendChild(modal);

        // Close handlers
        const closeModal = () => modal.classList.remove('active');
        modal.querySelector('.betterui-settings-close').addEventListener('click', closeModal);
        modal.addEventListener('click', e => {
            if (e.target === modal) closeModal();
        });

        // Set initial checkbox states and bind events
        modal.querySelectorAll('input[data-setting]').forEach(input => {
            const settingName = input.dataset.setting;

            // Set initial state
            input.checked = settings[settingName];

            // Use addEventListener for change event
            input.addEventListener('change', function (e) {
                e.stopPropagation();
                settings[settingName] = this.checked;
                saveSettings();
                console.log(`[BetterUI] Setting "${settingName}" changed to:`, this.checked);
            });

            // Also handle click on the label/toggle wrapper
            const label = input.closest('.betterui-toggle');
            if (label) {
                label.addEventListener('click', function (e) {
                    // Only handle if click wasn't on the input itself
                    if (e.target !== input) {
                        e.preventDefault();
                        e.stopPropagation();
                        input.checked = !input.checked;
                        settings[settingName] = input.checked;
                        saveSettings();
                        console.log(`[BetterUI] Setting "${settingName}" toggled to:`, input.checked);
                    }
                });
            }
        });

        // Card columns select handler
        const cardColumnsSelect = modal.querySelector('select[data-setting-select="cardColumns"]');
        if (cardColumnsSelect) {
            cardColumnsSelect.value = settings.cardColumns;
            cardColumnsSelect.addEventListener('change', function (e) {
                e.stopPropagation();
                const v = parseInt(this.value, 10);
                if (v >= 4 && v <= 8) {
                    settings.cardColumns = v;
                    saveSettings();
                    applyCardColumns();
                }
            });
        }

        // AI Provider select handler
        const aiProviderSelect = modal.querySelector('select[data-setting-select="aiProvider"]');
        if (aiProviderSelect) {
            aiProviderSelect.value = settings.aiProvider;
            updateAiKeyVisibility(modal, settings.aiProvider);

            aiProviderSelect.addEventListener('change', function (e) {
                e.stopPropagation();
                settings.aiProvider = this.value;
                saveSettings();
                updateAiKeyVisibility(modal, this.value);
                console.log(`[BetterUI] AI Provider changed to:`, this.value);
            });
        }

        // AI API key input handlers
        modal.querySelectorAll('input[data-api-key]').forEach(input => {
            const provider = input.dataset.apiKey;
            input.value = getAiApiKey(provider);

            input.addEventListener('change', function (e) {
                e.stopPropagation();
                setAiApiKey(provider, this.value.trim());
                console.log(`[BetterUI] ${provider} API key updated`);
            });

            input.addEventListener('blur', function (e) {
                e.stopPropagation();
                setAiApiKey(provider, this.value.trim());
            });
        });
    }

    /**
     * Show/hide API key inputs based on selected provider
     */
    function updateAiKeyVisibility(modal, provider) {
        modal.querySelectorAll('.ai-key-item').forEach(item => {
            item.classList.remove('visible');
        });
        if (provider !== 'none') {
            const keyItem = modal.querySelector(`.ai-key-item[data-provider="${provider}"]`);
            if (keyItem) keyItem.classList.add('visible');
        }
    }

    // =========================================================================
    // AVATAR & THUMBNAIL HELPERS
    // =========================================================================

    /*function getUserAvatarUrl() {
        const avatarImg = document.querySelector('.user-header__avatar .fancy-image__image');
        if (avatarImg?.src) {
            userAvatarUrl = avatarImg.src;
            debugLog('Found avatar:', userAvatarUrl);
        }
    }*/

    function getUserAvatarUrl() {
        if (userAvatarUrl) return userAvatarUrl;

        // Primary selector path
        const avatarImg = document.querySelector('#main > section.site-section--user > header.user-header > a.user-header__avatar > picture.fancy-image__picture > img.fancy-image__image');
        if (avatarImg?.src) {
            userAvatarUrl = avatarImg.src;
            return userAvatarUrl;
        }

        // Fallback selectors
        const fallbackSelectors = [
            '.user-header__avatar img',
            '.user-header .fancy-image__image',
            '.user-header img'
        ];

        for (const selector of fallbackSelectors) {
            const img = document.querySelector(selector);
            if (img?.src) {
                userAvatarUrl = img.src;
                return userAvatarUrl;
            }
        }

        return null;
    }

    function getOrCreateThumbnailWrapper(link) {
        let wrapper = link.querySelector('.card-thumbnail-wrapper');
        if (!wrapper) {
            wrapper = document.createElement('div');
            wrapper.className = 'card-thumbnail-wrapper';

            const existingThumb = link.querySelector('.post-card__image');
            if (existingThumb) {
                wrapper.appendChild(existingThumb);
            }

            link.insertBefore(wrapper, link.firstChild);
        }
        return wrapper;
    }

    function insertAvatarPlaceholder(link) {
        const wrapper = link.querySelector('.card-thumbnail-wrapper');
        if (!wrapper) return;

        wrapper.innerHTML = '';

        const container = document.createElement('div');
        container.className = 'generated-thumbnail avatar-fallback';

        const avatar = document.createElement('div');
        avatar.className = 'avatar-placeholder';
        container.appendChild(avatar);

        const playIndicator = document.createElement('div');
        playIndicator.className = 'video-play-indicator';
        container.appendChild(playIndicator);

        wrapper.appendChild(container);
    }

    function removeAvatarPlaceholder(link) {
        link.querySelector('.avatar-placeholder')?.remove();
    }

    function formatDuration(seconds) {
        if (!seconds || !isFinite(seconds)) return '';
        const mins = Math.floor(seconds / 60);
        const secs = Math.floor(seconds % 60);
        return `${mins}:${secs.toString().padStart(2, '0')}`;
    }

    // =========================================================================
    // THUMBNAIL GENERATION
    // =========================================================================

    /**
     * Check if video is suitable for thumbnail generation
     */
    async function checkVideoSuitability(videoUrl) {
        const fileSize = await getFileSize(videoUrl);

        if (fileSize && fileSize > CONFIG.maxVideoSizeForThumbnail) {
            return { suitable: false, reason: 'File too large' };
        }

        if (videoUrl.toLowerCase().includes('.mp4')) {
            const atomInfo = await analyzeMp4Structure(videoUrl, true);
            if (atomInfo && !atomInfo.isFaststart && fileSize && fileSize > CONFIG.maxNonFaststartSize) {
                return { suitable: false, reason: 'Non-faststart MP4 too large' };
            }
        }

        return { suitable: true };
    }

    /**
     * Fetch video as blob with range request
     */
    async function fetchVideoBlob(videoUrl, mode = 'start') {
        const finalUrl = await resolveRedirectUrl(videoUrl);
        const headers = { 'Accept': '*/*' };

        if (mode === 'start') headers['Range'] = 'bytes=0-5242880';
        else if (mode === 'end') headers['Range'] = 'bytes=-10485760';

        const response = await gmFetch(finalUrl, { responseType: 'blob', headers, timeout: 120000 });
        return URL.createObjectURL(response.response);
    }

    /**
     * Generate thumbnail from video
     */
    function generateThumbnail(videoUrl) {
        return new Promise(async (resolve, reject) => {
            const modes = ['start', 'combined', 'full'];
            let currentMode = 0;

            const tryMode = async () => {
                if (currentMode >= modes.length) {
                    reject(new Error('All modes failed'));
                    return;
                }

                const mode = modes[currentMode];
                debugLog(`Thumbnail attempt: ${mode}`);

                // Size check for full mode
                if (mode === 'full') {
                    const size = await getFileSize(videoUrl);
                    if (size && size > CONFIG.maxVideoSizeForThumbnail) {
                        reject(new Error('File too large'));
                        return;
                    }
                }

                let blobUrl;
                try {
                    if (mode === 'combined' && typeof MP4Box !== 'undefined') {
                        blobUrl = await fetchMp4BoxBlob(videoUrl);
                    } else {
                        blobUrl = await fetchVideoBlob(videoUrl, mode);
                    }
                } catch (e) {
                    currentMode++;
                    tryMode();
                    return;
                }

                const video = document.createElement('video');
                video.muted = true;
                video.preload = 'metadata';
                video.src = blobUrl;

                const cleanup = () => {
                    video.src = '';
                    URL.revokeObjectURL(blobUrl);
                };

                const timeout = setTimeout(() => {
                    cleanup();
                    currentMode++;
                    tryMode();
                }, 15000);

                video.onloadedmetadata = () => {
                    const seekTo = Math.min(CONFIG.seekTime, video.duration * 0.1);
                    video.currentTime = seekTo;
                };

                video.onseeked = () => {
                    clearTimeout(timeout);
                    try {
                        const canvas = document.createElement('canvas');
                        canvas.width = CONFIG.thumbnailSize;
                        canvas.height = CONFIG.thumbnailSize;
                        const ctx = canvas.getContext('2d');

                        const aspect = video.videoWidth / video.videoHeight;
                        let dw, dh, dx, dy;
                        if (aspect > 1) {
                            dh = canvas.height;
                            dw = dh * aspect;
                            dx = (canvas.width - dw) / 2;
                            dy = 0;
                        } else {
                            dw = canvas.width;
                            dh = dw / aspect;
                            dx = 0;
                            dy = (canvas.height - dh) / 2;
                        }

                        ctx.drawImage(video, dx, dy, dw, dh);
                        const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
                        cleanup();
                        resolve({ dataUrl, duration: video.duration });
                    } catch (e) {
                        cleanup();
                        reject(e);
                    }
                };

                video.onerror = () => {
                    clearTimeout(timeout);
                    cleanup();
                    currentMode++;
                    tryMode();
                };
            };

            tryMode();
        });
    }

    /**
     * Fetch MP4 using MP4Box for non-faststart files
     */
    function fetchMp4BoxBlob(videoUrl) {
        return new Promise(async (resolve, reject) => {
            if (typeof MP4Box === 'undefined') {
                reject(new Error('MP4Box not available'));
                return;
            }

            const finalUrl = await resolveRedirectUrl(videoUrl);
            const fileSize = await getFileSize(videoUrl);
            if (!fileSize) {
                reject(new Error('Cannot determine file size'));
                return;
            }

            const mp4box = MP4Box.createFile();
            let resolved = false;

            mp4box.onError = () => !resolved && reject(new Error('MP4Box error'));
            mp4box.onReady = info => {
                if (resolved) return;
                resolved = true;

                const track = info.tracks.find(t => t.type === 'video');
                if (!track) {
                    reject(new Error('No video track'));
                    return;
                }

                mp4box.setSegmentOptions(track.id, null, { nbSamples: 100 });
                const initSegs = mp4box.initializeSegmentation();
                mp4box.start();

                const chunks = [initSegs[0].buffer];
                mp4box.onSegment = (id, user, buffer) => chunks.push(buffer);

                setTimeout(() => {
                    mp4box.stop();
                    const blob = new Blob(chunks, { type: 'video/mp4' });
                    resolve(URL.createObjectURL(blob));
                }, 500);
            };

            // Fetch start and end of file
            const fetchRange = async (start, end) => {
                const response = await gmFetch(finalUrl, {
                    responseType: 'arraybuffer',
                    headers: { 'Range': `bytes=${start}-${end}` },
                    timeout: 30000
                });
                const buffer = response.response;
                buffer.fileStart = start;
                return buffer;
            };

            try {
                const startBuffer = await fetchRange(0, Math.min(5 * 1024 * 1024, fileSize - 1));
                mp4box.appendBuffer(startBuffer);

                if (fileSize > 10 * 1024 * 1024) {
                    const endStart = Math.max(0, fileSize - 10 * 1024 * 1024);
                    const endBuffer = await fetchRange(endStart, fileSize - 1);
                    mp4box.appendBuffer(endBuffer);
                }

                mp4box.flush();
            } catch (e) {
                reject(e);
            }
        });
    }

    // =========================================================================
    // AI SMART FRAME SELECTION
    // =========================================================================

    /**
     * Extract multiple frames from video at different positions
     * Returns array of { position, dataUrl } objects
     */
    async function extractVideoFrames(videoUrl, positions = CONFIG.aiFramePositions) {
        return new Promise(async (resolve, reject) => {
            let blobUrl;
            try {
                blobUrl = await fetchVideoBlob(videoUrl, 'full');
            } catch (e) {
                reject(new Error('Failed to fetch video for frame extraction'));
                return;
            }

            const video = document.createElement('video');
            video.muted = true;
            video.preload = 'metadata';
            video.src = blobUrl;

            const cleanup = () => {
                video.src = '';
                URL.revokeObjectURL(blobUrl);
            };

            const timeout = setTimeout(() => {
                cleanup();
                reject(new Error('Frame extraction timeout'));
            }, 60000);

            video.onloadedmetadata = async () => {
                const frames = [];
                const duration = video.duration;

                for (const pos of positions) {
                    const seekTo = duration * pos;
                    try {
                        const dataUrl = await extractFrameAt(video, seekTo);
                        frames.push({ position: pos, time: seekTo, dataUrl });
                    } catch (e) {
                        debugLog(`Frame extraction failed at ${pos}:`, e.message);
                    }
                }

                clearTimeout(timeout);
                cleanup();

                if (frames.length === 0) {
                    reject(new Error('No frames extracted'));
                } else {
                    resolve({ frames, duration });
                }
            };

            video.onerror = () => {
                clearTimeout(timeout);
                cleanup();
                reject(new Error('Video load error'));
            };
        });
    }

    /**
     * Extract a single frame at specified time
     */
    function extractFrameAt(video, time) {
        return new Promise((resolve, reject) => {
            video.currentTime = time;

            const onSeeked = () => {
                video.removeEventListener('seeked', onSeeked);
                try {
                    const canvas = document.createElement('canvas');
                    // Use smaller size for AI to reduce token usage
                    canvas.width = 256;
                    canvas.height = 256;
                    const ctx = canvas.getContext('2d');

                    const aspect = video.videoWidth / video.videoHeight;
                    let dw, dh, dx, dy;
                    if (aspect > 1) {
                        dh = canvas.height;
                        dw = dh * aspect;
                        dx = (canvas.width - dw) / 2;
                        dy = 0;
                    } else {
                        dw = canvas.width;
                        dh = dw / aspect;
                        dx = 0;
                        dy = (canvas.height - dh) / 2;
                    }

                    ctx.fillStyle = '#000';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    ctx.drawImage(video, dx, dy, dw, dh);

                    resolve(canvas.toDataURL('image/jpeg', 0.7));
                } catch (e) {
                    reject(e);
                }
            };

            video.addEventListener('seeked', onSeeked);
        });
    }

    /**
     * Call Claude API for frame selection
     */
    async function selectBestFrameClaude(frames) {
        const apiKey = getAiApiKey('claude');
        if (!apiKey) throw new Error('Claude API key not configured');

        const imageContent = frames.map((frame, idx) => ([
            {
                type: 'text',
                text: `Frame ${idx + 1} (at ${Math.round(frame.time)}s):`
            },
            {
                type: 'image',
                source: {
                    type: 'base64',
                    media_type: 'image/jpeg',
                    data: frame.dataUrl.split(',')[1]
                }
            }
        ])).flat();

        const response = await gmFetch('https://api.anthropic.com/v1/messages', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'x-api-key': apiKey,
                'anthropic-version': '2023-06-01'
            },
            data: JSON.stringify({
                model: 'claude-3-5-haiku-latest',
                max_tokens: 50,
                messages: [{
                    role: 'user',
                    content: [
                        ...imageContent,
                        {
                            type: 'text',
                            text: 'Select the best frame for a video thumbnail. Consider: visual clarity, interesting content, good composition, not a black/blank frame. Reply with ONLY the frame number (1-' + frames.length + ').'
                        }
                    ]
                }]
            }),
            responseType: 'json',
            timeout: 30000
        });

        const result = JSON.parse(response.responseText);
        if (result.error) throw new Error(result.error.message);

        const text = result.content?.[0]?.text || '';
        const match = text.match(/(\d+)/);
        if (!match) throw new Error('Could not parse frame selection');

        const frameNum = parseInt(match[1], 10);
        if (frameNum < 1 || frameNum > frames.length) throw new Error('Invalid frame number');

        return frameNum - 1; // Return 0-indexed
    }

    /**
     * Call Gemini API for frame selection
     */
    async function selectBestFrameGemini(frames) {
        const apiKey = getAiApiKey('gemini');
        if (!apiKey) throw new Error('Gemini API key not configured');

        const parts = [];
        frames.forEach((frame, idx) => {
            parts.push({ text: `Frame ${idx + 1} (at ${Math.round(frame.time)}s):` });
            parts.push({
                inline_data: {
                    mime_type: 'image/jpeg',
                    data: frame.dataUrl.split(',')[1]
                }
            });
        });
        parts.push({
            text: 'Select the best frame for a video thumbnail. Consider: visual clarity, interesting content, good composition, not a black/blank frame. Reply with ONLY the frame number (1-' + frames.length + ').'
        });

        const response = await gmFetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            data: JSON.stringify({
                contents: [{ parts }],
                generationConfig: { maxOutputTokens: 50 }
            }),
            responseType: 'json',
            timeout: 30000
        });

        const result = JSON.parse(response.responseText);
        if (result.error) throw new Error(result.error.message);

        const text = result.candidates?.[0]?.content?.parts?.[0]?.text || '';
        const match = text.match(/(\d+)/);
        if (!match) throw new Error('Could not parse frame selection');

        const frameNum = parseInt(match[1], 10);
        if (frameNum < 1 || frameNum > frames.length) throw new Error('Invalid frame number');

        return frameNum - 1; // Return 0-indexed
    }

    /**
     * Generate thumbnail using AI frame selection
     */
    async function generateAiThumbnail(videoUrl) {
        const provider = settings.aiProvider;
        if (provider === 'none') throw new Error('AI provider not configured');

        debugLog(`AI thumbnail generation using ${provider}`);

        // Extract frames
        const { frames, duration } = await extractVideoFrames(videoUrl);
        debugLog(`Extracted ${frames.length} frames for AI analysis`);

        // Select best frame via AI
        let bestFrameIndex;
        if (provider === 'claude') {
            bestFrameIndex = await selectBestFrameClaude(frames);
        } else if (provider === 'gemini') {
            bestFrameIndex = await selectBestFrameGemini(frames);
        } else {
            throw new Error('Unknown AI provider');
        }

        debugLog(`AI selected frame ${bestFrameIndex + 1}`);

        // Generate full-size thumbnail from selected frame position
        const selectedFrame = frames[bestFrameIndex];
        const fullSizeDataUrl = await generateThumbnailAtTime(videoUrl, selectedFrame.time);

        return { dataUrl: fullSizeDataUrl, duration };
    }

    /**
     * Generate thumbnail at specific time (for AI-selected frame)
     */
    function generateThumbnailAtTime(videoUrl, time) {
        return new Promise(async (resolve, reject) => {
            let blobUrl;
            try {
                blobUrl = await fetchVideoBlob(videoUrl, 'full');
            } catch (e) {
                reject(e);
                return;
            }

            const video = document.createElement('video');
            video.muted = true;
            video.preload = 'metadata';
            video.src = blobUrl;

            const cleanup = () => {
                video.src = '';
                URL.revokeObjectURL(blobUrl);
            };

            const timeout = setTimeout(() => {
                cleanup();
                reject(new Error('Thumbnail generation timeout'));
            }, 30000);

            video.onloadedmetadata = () => {
                video.currentTime = time;
            };

            video.onseeked = () => {
                clearTimeout(timeout);
                try {
                    const canvas = document.createElement('canvas');
                    canvas.width = CONFIG.thumbnailSize;
                    canvas.height = CONFIG.thumbnailSize;
                    const ctx = canvas.getContext('2d');

                    const aspect = video.videoWidth / video.videoHeight;
                    let dw, dh, dx, dy;
                    if (aspect > 1) {
                        dh = canvas.height;
                        dw = dh * aspect;
                        dx = (canvas.width - dw) / 2;
                        dy = 0;
                    } else {
                        dw = canvas.width;
                        dh = dw / aspect;
                        dx = 0;
                        dy = (canvas.height - dh) / 2;
                    }

                    ctx.fillStyle = '#000';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    ctx.drawImage(video, dx, dy, dw, dh);

                    cleanup();
                    resolve(canvas.toDataURL('image/jpeg', 0.85));
                } catch (e) {
                    cleanup();
                    reject(e);
                }
            };

            video.onerror = () => {
                clearTimeout(timeout);
                cleanup();
                reject(new Error('Video load error'));
            };
        });
    }

    /**
     * Check if AI is available for thumbnail generation
     */
    function isAiAvailable() {
        if (settings.aiProvider === 'none') return false;
        const apiKey = getAiApiKey(settings.aiProvider);
        return !!apiKey;
    }

    /**
     * Handle AI thumbnail retry click
     */
    async function handleAiThumbnailRetry(link) {
        if (!isAiAvailable()) {
            debugLog('AI not available for retry');
            return;
        }

        const playIndicator = link.querySelector('.video-play-indicator');
        if (playIndicator) {
            playIndicator.innerHTML = icon('hourglass_empty', 'lg');
            playIndicator.classList.remove('retry-available', 'ai-retry-available');
            playIndicator.title = 'AI processing...';
        }

        const parsed = parsePostUrl(link.href);
        if (!parsed) {
            console.warn("Popular page detected: Post URL did not parse, attempting fallback fetch:", link.href);
            parsed = parsePopularPostUrl(link.href);
            if (!parsed) return; // final fallback
        }

        let postData = postDataCache.get(link.href);
        if (!postData) {
            const apiUrl = `${parsed.baseUrl}/api/v1/${parsed.service}/user/${parsed.userId}/post/${parsed.postId}`;
            try {
                postData = await fetchJson(apiUrl);
            } catch (e) {
                markThumbnailFailed(link, 'API fetch failed');
                return;
            }
        }

        if (!postData) {
            markThumbnailFailed(link, 'No post data');
            return;
        }

        const videoItems = extractMediaFromPost(postData, parsed.baseUrl, { type: 'video' });
        if (videoItems.length === 0) {
            markThumbnailFailed(link, 'No video');
            return;
        }

        for (const item of videoItems) {
            try {
                const result = await generateAiThumbnail(item.url);
                insertVideoThumbnail(link, result.dataUrl, result.duration);
                updateFooterDuration(link);
                addModelLabelToCard(link, postData);
                await cacheThumbnail(item.url, result.dataUrl, result.duration);
                debugLog('AI thumbnail generated successfully');
                return;
            } catch (e) {
                debugLog('AI thumbnail failed:', e.message);
            }
        }

        markThumbnailFailed(link, 'AI failed');
    }

    /**
     * Handle None-AI thumbnail retry click
     */

    async function handleVideoThumbnailRetry(link) {
        const playIndicator = link.querySelector('.video-play-indicator');
        if (playIndicator) {
            playIndicator.innerHTML = icon('hourglass_empty', 'lg');
            playIndicator.classList.remove('retry-available');
            playIndicator.title = 'Retrying thumbnail...';
        }

        const parsed = parsePostUrl(link.href);
        if (!parsed) {
            markThumbnailFailed(link, 'Invalid post');
            return;
        }

        let postData = postDataCache.get(link.href);
        if (!postData) {
            const cacheKey = `${parsed.baseUrl}/${parsed.service}/user/${parsed.userId}`;
            postData = userPostsCache.get(cacheKey)?.get(parsed.postId);
        }

        if (!postData) {
            try {
                postData = await fetchJson(`${parsed.baseUrl}/api/v1/${parsed.service}/user/${parsed.userId}/post/${parsed.postId}`);
                postDataCache.set(link.href, postData);
                link.dataset.videoThumbnailFailed = 'false';
            } catch (e) {
                markThumbnailFailed(link, 'No post data');
                return;
            }
        }

        const videoItems = extractMediaFromPost(postData, parsed.baseUrl, { type: 'video' });
        if (videoItems.length === 0) {
            markThumbnailFailed(link, 'No video');
            return;
        }

        for (const item of videoItems) {
            try {
                // Try very early frame first, fallback slightly later
                const dataUrl =
                    await generateThumbnailAtTime(item.url, 0.5)
                    || await generateThumbnailAtTime(item.url, 1);

                insertVideoThumbnail(link, dataUrl, null);
                addModelLabelToCard(link, postData);
                await cacheThumbnail(item.url, dataUrl, null);
                return;
            } catch (e) {
                console.error('[Retry Thumbnail] Error for', item.url, e);
            }
        }

        markThumbnailFailed(link, 'Retry failed');
    }

    function insertVideoThumbnail(link, dataUrl, duration) {
        const wrapper = link.querySelector('.card-thumbnail-wrapper');
        if (!wrapper) return;

        removeAvatarPlaceholder(link);
        wrapper.querySelector('.generated-thumbnail')?.remove();

        const container = document.createElement('div');
        container.className = 'generated-thumbnail';

        const img = document.createElement('img');
        img.src = dataUrl;
        container.appendChild(img);

        const playIcon = document.createElement('div');
        playIcon.className = 'video-play-indicator';
        playIcon.innerHTML = icon('play_arrow', 'lg', true);
        container.appendChild(playIcon);

        if (duration) {
            const durationEl = document.createElement('div');
            durationEl.className = 'video-duration';
            durationEl.textContent = formatDuration(duration);
            container.appendChild(durationEl);
        }

        wrapper.appendChild(container);
        link.dataset.videoThumbnailSuccess = 'true';
    }

    function addModelLabelToCard(link, postData) {
        const username = postData?.user?.name || postData?.user?.id;
        if (!username) return;

        const existing = link.querySelector('.model-label');
        if (existing) return;

        const div = document.createElement('div');
        div.className = 'model-label';
        div.textContent = username;

        link.insertAdjacentElement('beforebegin', div);
    }

    function insertImageFallbackThumbnail(link, imageUrl) {
        const wrapper = link.querySelector('.card-thumbnail-wrapper');
        if (!wrapper) return;

        removeAvatarPlaceholder(link);

        const container = document.createElement('div');
        container.className = 'generated-thumbnail video-fallback';

        const img = document.createElement('img');
        img.src = imageUrl;
        img.loading = 'eager';
        container.appendChild(img);

        const playIcon = document.createElement('div');
        playIcon.className = 'video-play-indicator';
        playIcon.innerHTML = icon('play_arrow', 'lg', true);
        container.appendChild(playIcon);

        wrapper.appendChild(container);
        link.dataset.videoThumbnailSuccess = 'true';
    }

    function markThumbnailFailed(link, message = 'Failed') {
        link.dataset.videoThumbnailFailed = 'true';

        // Always insert placeholder first, so DOM is stable
        insertAvatarPlaceholder(link);

        const playIndicator = link.querySelector('.video-play-indicator');
        if (playIndicator) {
            // Check if AI is available for retry
            if (isAiAvailable() && message !== 'AI failed') {
                playIndicator.classList.add('ai-retry-available');
                playIndicator.innerHTML = icon('auto_awesome', 'lg');
                playIndicator.title = `${message} - Click for AI thumbnail`;
            } else {
                playIndicator.classList.add('retry-available');
                playIndicator.innerHTML = '↻';
                playIndicator.title = `${message} - Click to retry`;
            }
        }
    }

    /**
     * Mark thumbnail for AI retry without marking as failed
     * Used when fallback to image/avatar succeeded but AI could improve it
     */
    function markThumbnailForAiRetry(link, message = 'Skipped') {
        const playIndicator = link.querySelector('.video-play-indicator');
        if (playIndicator) {
            playIndicator.classList.add('ai-retry-available');
            playIndicator.innerHTML = icon('auto_awesome', 'lg');
            playIndicator.title = `${message} - Click for AI thumbnail`;
        }
    }

    // =========================================================================
    // IMAGE COLLAGE
    // =========================================================================

    function insertImageCollage(link, imageUrls, totalCount) {
        const wrapper = link.querySelector('.card-thumbnail-wrapper');
        if (!wrapper) return;
        const thumbnail = wrapper.querySelector('.post-card__image');
        if (thumbnail) thumbnail.classList.add('hidden');

        removeAvatarPlaceholder(link);
        wrapper.querySelector('.image-collage')?.remove();

        const count = Math.min(imageUrls.length, 3);
        const collage = document.createElement('div');
        collage.className = `image-collage collage-${count}`;

        if (count === 1) {
            const img = document.createElement('img');
            img.src = imageUrls[0];
            img.className = 'collage-img';
            img.loading = 'eager';
            collage.appendChild(img);
        } else if (count === 2) {
            imageUrls.slice(0, 2).forEach(url => {
                const img = document.createElement('img');
                img.src = url;
                img.className = 'collage-img';
                img.loading = 'eager';
                collage.appendChild(img);
            });
        } else {
            const left = document.createElement('div');
            left.className = 'collage-left';
            const leftImg = document.createElement('img');
            leftImg.src = imageUrls[0];
            leftImg.className = 'collage-img';
            leftImg.loading = 'eager';
            left.appendChild(leftImg);

            const right = document.createElement('div');
            right.className = 'collage-right';
            imageUrls.slice(1, 3).forEach(url => {
                const img = document.createElement('img');
                img.src = url;
                img.className = 'collage-img';
                img.loading = 'eager';
                right.appendChild(img);
            });

            collage.appendChild(left);
            collage.appendChild(right);
        }

        if (totalCount > 3) {
            const indicator = document.createElement('div');
            indicator.className = 'collage-more-indicator';
            indicator.textContent = `+${totalCount - 3} more`;
            collage.appendChild(indicator);
        }

        wrapper.appendChild(collage);
    }

    // =========================================================================
    // FILE COUNT OVERLAY
    // =========================================================================

    function insertFileCountOverlay(link, counts) {
        const wrapper = link.querySelector('.card-thumbnail-wrapper');
        if (!wrapper || wrapper.querySelector('.file-count-overlay')) return;

        const icons = { video: icon('videocam', 'sm', true), image: icon('image', 'sm', true), archive: icon('folder_zip', 'sm', true), audio: icon('headphones', 'sm', true), document: icon('description', 'sm', true) };
        const badges = [];

        for (const [type, count] of Object.entries(counts)) {
            if (count > 0 && icons[type]) {
                badges.push(`<div class="file-count-badge">${icons[type]} ${count}</div>`);
            }
        }

        if (badges.length > 0) {
            const overlay = document.createElement('div');
            overlay.className = 'file-count-overlay';
            overlay.innerHTML = badges.join('');
            wrapper.appendChild(overlay);
        }
    }

    // =========================================================================
    // VIDEO THUMBNAIL QUEUE
    // =========================================================================

    function processVideoQueue() {
        while (activeVideoProcesses < CONFIG.maxConcurrentVideo && videoThumbnailQueue.length > 0) {
            const link = videoThumbnailQueue.shift();
            if (link && !link.dataset.videoThumbnailProcessed) {
                activeVideoProcesses++;
                processVideoThumbnail(link).finally(() => {
                    activeVideoProcesses--;
                    processVideoQueue();
                });
            }
        }
    }

    function ensurePlaceholder(link) {
        const wrapper = link.querySelector('.card-thumbnail-wrapper');
        if (!wrapper) return;

        // Only add once
        if (!wrapper.querySelector('.avatar-placeholder')) {
            const placeholder = document.createElement('div');
            placeholder.className = 'avatar-placeholder';
            wrapper.appendChild(placeholder);
        }
        if (!wrapper.querySelector('.video-play-indicator')) {
            const indicator = document.createElement('div');
            indicator.className = 'video-play-indicator retry-available';
            indicator.textContent = '↻';
            wrapper.appendChild(indicator);
        }
    }

    async function processVideoThumbnail(link) {
        if (link.dataset.videoThumbnailSuccess === 'true') return;
        link.dataset.videoThumbnailProcessed = 'true';

        const parsed = parsePostUrl(link.href);
        if (!parsed) return;

        // ---- LOAD postData FIRST ----
        let postData = postDataCache.get(link.href);
        if (!postData) {
            const cacheKey = `${parsed.baseUrl}/${parsed.service}/user/${parsed.userId}`;
            if (userPostsCache.has(cacheKey)) {
                postData = userPostsCache.get(cacheKey).get(parsed.postId);
            }
            if (!postData) {
                try {
                    postData = await fetchJson(`${parsed.baseUrl}/api/v1/${parsed.service}/user/${parsed.userId}/post/${parsed.postId}`);
                } catch {
                    markThumbnailFailed(link, 'API error');
                    return;
                }
            }
        }

        // ---- POST DATA VALIDATION ----
        if (!postData) {
            insertAvatarPlaceholder(link);
            markThumbnailFailed(link, 'No metadata');
            return;
        }

        const videoItems = extractMediaFromPost(postData, parsed.baseUrl, { type: 'video' });
        // If no video, try image fallback or mark failed
        if (videoItems.length === 0) {
            // If image attachments exist, use the first as fallback thumbnail
            if (imageItems.length > 0) {
                insertImageFallbackThumbnail(link, imageItems[0].url);
            }
            // Otherwise show avatar + failure indicator
            else {
                insertAvatarPlaceholder(link);
                markThumbnailFailed(link, 'No video');
            }

            return;
        }

        const imageItems = extractMediaFromPost(postData, parsed.baseUrl, { type: 'image' });

        let anyAttempted = false;
        let skipReason = '';

        for (const item of videoItems) {
            const cached = await getCachedThumbnail(item.url);
            if (cached) {
                insertVideoThumbnail(link, cached.dataUrl, cached.duration);
                addModelLabelToCard(link, postData);
                return;
            }
            const suitability = await checkVideoSuitability(item.url);
            if (!suitability.suitable) {
                debugLog(`Skipping video: ${suitability.reason}`);
                skipReason = suitability.reason;
                continue;
            }
            anyAttempted = true;
            try {
                const result = await generateThumbnail(item.url);
                insertVideoThumbnail(link, result.dataUrl, result.duration);
                addModelLabelToCard(link, postData);

                await cacheThumbnail(item.url, result.dataUrl, result.duration);
                return;
            } catch (e) {
                debugLog('Thumbnail failed:', e.message);
                markThumbnailFailed(link, 'Preview failed');
                return;
            }
        }

        const allSkipped = !anyAttempted && videoItems.length > 0;

        // ---- SUITABILITY SKIP OR VIDEO FAIL FALLBACK ----
        if (allSkipped) {
            // If an image fallback exists, show it
            if (imageItems.length > 0) {
                insertImageFallbackThumbnail(link, imageItems[0].url);

                // If AI can help, mark AI retry
                if (isAiAvailable()) {
                    markThumbnailForAiRetry(link, skipReason || 'Skipped');
                }
            } else {
                // No images either, show avatar + failed indicator
                insertAvatarPlaceholder(link);
                markThumbnailFailed(link, skipReason || 'Skipped');
            }

            return;
        }

        // ---- IMAGE-ONLY POSTS (no video attempted but images exist) ----
        if (imageItems.length > 0) {
            insertImageFallbackThumbnail(link, imageItems[0].url);
            return;
        }

        const userAvatarUrl = postData?.user?.avatar_url;
        if (userAvatarUrl) {
            insertAvatarPlaceholder(link);
            markThumbnailFailed(link, 'No preview');
            return;
        }

        if (settings.aiAutoFallback && isAiAvailable()) {
            debugLog('Attempting automatic AI fallback');
            for (const item of videoItems) {
                try {
                    const result = await generateAiThumbnail(item.url);
                    insertVideoThumbnail(link, result.dataUrl, result.duration);
                    await cacheThumbnail(item.url, result.dataUrl, result.duration);
                    debugLog('AI auto-fallback thumbnail generated');
                    return;
                } catch (e) {
                    debugLog('AI auto-fallback failed:', e.message);
                }
            }
        }

        markThumbnailFailed(link, allSkipped ? (skipReason || 'Skipped') : 'Preview failed');
    }

    // =========================================================================
    // BATCH API FETCHING
    // =========================================================================

    /**
     * Batch fetch posts for a user starting from a specific offset
     * Fetches current page and next page (100 posts total if available)
     * Skips offsets that have already been fetched
     */
    async function batchFetchUserPosts(baseUrl, service, userId, startOffset = 0) {
        const cacheKey = `${baseUrl}/${service}/user/${userId}`;
        const limit = 50;

        // Initialize cache structures if needed
        if (!userPostsCache.has(cacheKey)) {
            userPostsCache.set(cacheKey, new Map());
        }
        if (!fetchedOffsets.has(cacheKey)) {
            fetchedOffsets.set(cacheKey, new Set());
        }

        const postsMap = userPostsCache.get(cacheKey);
        const fetched = fetchedOffsets.get(cacheKey);

        // Determine which offsets need to be fetched (current page + next page)
        const offsetsToFetch = [];
        const currentPageOffset = Math.floor(startOffset / limit) * limit; // Normalize to page boundary

        // Add current page and next page if not already fetched
        if (!fetched.has(currentPageOffset)) {
            offsetsToFetch.push(currentPageOffset);
        }
        const nextPageOffset = currentPageOffset + limit;
        if (!fetched.has(nextPageOffset)) {
            offsetsToFetch.push(nextPageOffset);
        }

        if (offsetsToFetch.length === 0) {
            debugLog(`All offsets already fetched for ${service}/${userId}`);
            return postsMap;
        }

        // Check for pending fetch for same offsets
        const pendingKey = `${cacheKey}:${offsetsToFetch.join(',')}`;
        if (pendingBatchFetches.has(pendingKey)) {
            return pendingBatchFetches.get(pendingKey);
        }

        const fetchPromise = (async () => {
            try {
                for (const offset of offsetsToFetch) {
                    const url = `${baseUrl}/api/v1/${service}/user/${userId}/posts?o=${offset}&limit=${limit}`;
                    debugLog(`Fetching posts offset=${offset} for ${service}/${userId}`);

                    const posts = await fetchJson(url);

                    if (Array.isArray(posts) && posts.length > 0) {
                        posts.forEach(post => postsMap.set(post.id, post));
                        fetched.add(offset);
                        debugLog(`Fetched ${posts.length} posts at offset ${offset}`);
                    } else {
                        // Mark as fetched even if empty (end of posts)
                        fetched.add(offset);
                        debugLog(`No more posts at offset ${offset}`);
                    }
                }

                debugLog(`Cache now has ${postsMap.size} posts for ${service}/${userId}`);
            } catch (e) {
                debugLog('Batch fetch failed:', e.message);
            }

            pendingBatchFetches.delete(pendingKey);
            return postsMap;
        })();

        pendingBatchFetches.set(pendingKey, fetchPromise);
        return fetchPromise;
    }

    // =========================================================================
    // INTERSECTION OBSERVERS
    // =========================================================================

    function initVideoThumbnailObserver() {
        if (videoThumbnailObserver) return;

        videoThumbnailObserver = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const link = entry.target;
                    if (!link.dataset.videoThumbnailQueued && !link.dataset.videoThumbnailProcessed) {
                        link.dataset.videoThumbnailQueued = 'true';
                        videoThumbnailQueue.push(link);
                        processVideoQueue();
                    }
                }
            });
        }, { rootMargin: '200px' });

        // expose globally so setupPostCards can attach
        window._videoObserver = videoThumbnailObserver;
    }

    // =========================================================================
    // GIF CONTROL (hover-to-load)
    // =========================================================================

    /**
     * Setup hover-to-play for GIF in thumbnail wrapper
     * JS controls visibility to avoid CSS hover issues with transparent images
     */
    function setupThumbnailGifHover(wrapper) {
        if (gifHoverSetup.has(wrapper)) return;

        const img = wrapper.querySelector('img[src*=".gif"], img[src*=".GIF"]');
        if (!img) return;

        gifHoverSetup.add(wrapper);

        const gifUrl = img.src;
        const transparentPixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

        // Create placeholder with GIF icon
        const placeholder = document.createElement('div');
        placeholder.className = 'gif-placeholder';
        placeholder.innerHTML = `${icon('gif_box', 'lg', true)}`;

        // Store URL and initially hide image
        img.dataset.gifSrc = gifUrl;
        img.style.opacity = '0';
        img.src = transparentPixel;

        wrapper.appendChild(placeholder);

        // Use mouseenter/mouseleave with explicit visibility control
        wrapper.addEventListener('mouseenter', () => {
            // Load GIF if needed
            if (img.dataset.gifSrc && img.src !== img.dataset.gifSrc) {
                img.src = img.dataset.gifSrc;
            }
            img.style.opacity = '1';
            placeholder.style.opacity = '0';
        });

        wrapper.addEventListener('mouseleave', () => {
            img.style.opacity = '0';
            placeholder.style.opacity = '1';
        });
    }

    /**
     * Process GIFs in thumbnail wrappers on the page
     */
    function processPageGifs() {
        if (!settings.pauseGifsWhenHidden) return;

        document.querySelectorAll('.card-thumbnail-wrapper').forEach(wrapper => {
            setupThumbnailGifHover(wrapper);
        });
    }

    /**
     * Control GIF playback in gallery slides
     * Only load GIF src for active slide, clear for inactive
     */
    function updateGalleryGifPlayback(activeIndex) {
        galleryOverlay?.querySelectorAll('.glide__slide').forEach((slide, index) => {
            const img = slide.querySelector('img[data-gif-src]');
            if (!img) return;

            if (index === activeIndex) {
                // Load GIF for active slide
                if (img.dataset.gifSrc && img.src !== img.dataset.gifSrc) {
                    img.src = img.dataset.gifSrc;
                }
            } else {
                // Unload GIF for inactive slides - use tiny transparent pixel
                if (img.src && img.src !== 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7') {
                    img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
                }
            }
        });
    }

    // =========================================================================
    // GALLERY
    // =========================================================================

    function createGalleryOverlay() {
        if (galleryOverlay) return;

        galleryOverlay = document.createElement('div');
        galleryOverlay.className = 'media-gallery-overlay';
        galleryOverlay.innerHTML = `
            <button class="gallery-close" aria-label="Close">${icon('close', 'md')}</button>
            <button class="gallery-download-all">${icon('download', 'sm')}<span class="download-label">Download All</span><span class="download-progress" style="display:none"></span></button>
            <div class="gallery-info"></div>
            <button class="gallery-nav prev" aria-label="Previous">${icon('chevron_left', 'lg')}</button>
            <button class="gallery-nav next" aria-label="Next">${icon('chevron_right', 'lg')}</button>
            <div class="glide">
                <div class="glide__track" data-glide-el="track">
                    <ul class="glide__slides"></ul>
                </div>
            </div>
            <div class="gallery-thumbnails"></div>
            <div class="gallery-counter"></div>
        `;

        galleryOverlay.querySelector('.gallery-close').onclick = closeGallery;
        galleryOverlay.querySelector('.gallery-nav.prev').onclick = () => navigateGallery(-1);
        galleryOverlay.querySelector('.gallery-nav.next').onclick = () => navigateGallery(1);
        galleryOverlay.querySelector('.gallery-download-all').onclick = downloadAllMedia;
        galleryOverlay.onclick = e => e.target === galleryOverlay && closeGallery();

        document.body.appendChild(galleryOverlay);

        // Single keyboard handler for gallery
        document.addEventListener('keydown', handleGalleryKeyboard, true);
    }

    async function openGallery(link) {
        const parsed = parsePostUrl(link.href);
        if (!parsed) return;

        createGalleryOverlay();

        // Clear preload cache for new gallery
        preloadedMedia.clear();

        // Destroy previous Glide instance
        if (galleryGlide) {
            galleryGlide.destroy();
            galleryGlide = null;
        }

        const slidesContainer = galleryOverlay.querySelector('.glide__slides');
        slidesContainer.innerHTML = '<li class="glide__slide"><div class="slide-inner"><div class="gallery-loading"><div class="loading-spinner"></div><span>Loading media...</span></div></div></li>';

        galleryOverlay.classList.add('active');
        document.body.style.overflow = 'hidden';

        // Always fetch from API to get full post content (HTML)
        let postData;
        try {
            const apiUrl = `${parsed.baseUrl}/api/v1/${parsed.service}/user/${parsed.userId}/post/${parsed.postId}`;
            postData = await fetchJson(apiUrl);
        } catch {
            // Fallback to cached data if API fails
            postData = postDataCache.get(link.href);
            if (!postData) {
                const cacheKey = `${parsed.baseUrl}/${parsed.service}/user/${parsed.userId}`;
                if (userPostsCache.has(cacheKey)) {
                    postData = userPostsCache.get(cacheKey).get(parsed.postId);
                }
            }
            if (!postData) {
                slidesContainer.innerHTML = '<li class="glide__slide"><div class="slide-inner"><div class="gallery-loading"><span>Failed to load media</span></div></div></li>';
                return;
            }
        }

        galleryMediaItems = extractMediaFromPost(postData, parsed.baseUrl, { type: 'all', includeText: true });
        galleryPostUrl = link.href;

        if (galleryMediaItems.length === 0) {
            slidesContainer.innerHTML = '<li class="glide__slide"><div class="slide-inner"><div class="gallery-loading"><span>No media found</span></div></div></li>';
            return;
        }

        const post = postData.post || postData;
        galleryOverlay.querySelector('.gallery-info').textContent = post.title || post.content?.substring(0, 50)?.replace(/<[^>]*>/g, '') || post.substring || 'post';

        // Preload first 3 media items
        preloadGalleryItems(galleryMediaItems, 0, 3);

        // Build slides
        buildGallerySlides();
        buildGalleryThumbnails();

        // Initialize Glide
        initGlide();

        galleryOverlay.setAttribute('tabindex', '-1');
        galleryOverlay.focus();
    }

    /**
     * Build Glide slides from media items
     */
    /**
     * Setup video frame capture for gallery videos
     * Captures frame when video plays/seeks for caching
     */
    function setupGalleryVideoCapture(video, videoUrl) {
        if (!videoUrl) return;

        let captured = false;

        const doCapture = async () => {
            if (captured) return;
            if (capturedVideoUrls.has(videoUrl)) {
                captured = true;
                return;
            }

            // Wait a bit for video to stabilize
            await new Promise(r => setTimeout(r, 500));

            video.addEventListener("loadedmetadata", async function () {
                console.log(video.videoWidth);
                // Only capture if we have video dimensions
                if (video.videoWidth === 0 || video.videoHeight === 0) return;

                try {
                    await captureVideoFrame(video, videoUrl);
                    captured = true;
                    debugLog('Gallery video frame captured:', videoUrl);
                } catch (e) {
                    debugLog('Gallery video capture failed:', e.message);
                }
            });
        };

        // Capture when video plays
        video.addEventListener('play', doCapture, { once: true });

        // Also try on timeupdate (in case play event was missed)
        const onTimeUpdate = () => {
            if (video.currentTime > 0.5 && !captured) {
                doCapture();
                video.removeEventListener('timeupdate', onTimeUpdate);
            }
        };
        video.addEventListener('timeupdate', onTimeUpdate);
    }

    function buildGallerySlides() {
        const slidesContainer = galleryOverlay.querySelector('.glide__slides');
        slidesContainer.innerHTML = '';

        galleryMediaItems.forEach((item, index) => {
            const slide = document.createElement('li');
            slide.className = 'glide__slide';
            slide.dataset.index = index;

            // Inner wrapper for coverflow transforms
            const inner = document.createElement('div');
            inner.className = 'slide-inner';

            if (item.type === 'text') {
                const container = document.createElement('div');
                container.className = 'gallery-text-content';
                const contentHtml = item.isHtml ? item.content : linkifyText(item.content);
                container.innerHTML = `
                    <span class="gallery-text-badge">POST CONTENT</span>
                    ${item.title ? `<h3>${escapeHtml(item.title)}</h3>` : ''}
                    <div class="gallery-text-body">${contentHtml}</div>
                `;
                inner.appendChild(container);
            } else if (item.type === 'video') {
                const video = document.createElement('video');
                video.src = item.url;
                video.controls = true;
                video.preload = 'metadata';
                video.volume = persistedVolume;
                video.addEventListener('volumechange', () => setPersistedVolume(video.volume));

                // Set up frame capture for caching
                video.dataset.videoUrl = item.url;
                setupGalleryVideoCapture(video, item.url);

                inner.appendChild(video);
                addDownloadButton(inner, item);
            } else if (item.type === 'audio') {
                const audio = document.createElement('audio');
                audio.src = item.url;
                audio.controls = true;
                audio.volume = persistedVolume;
                audio.addEventListener('volumechange', () => setPersistedVolume(audio.volume));
                inner.appendChild(audio);
                addDownloadButton(inner, item);
            } else {
                const img = document.createElement('img');
                const isGif = item.url.toLowerCase().includes('.gif');

                if (isGif && settings.pauseGifsWhenHidden) {
                    // For GIFs: store src, only load for first slide
                    img.dataset.gifSrc = item.url;
                    if (index === 0) {
                        img.src = item.url;
                    } else {
                        // Use transparent placeholder for inactive slides
                        img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
                    }
                } else {
                    img.src = item.url;
                }

                img.loading = index < 3 ? 'eager' : 'lazy';
                inner.appendChild(img);
                addDownloadButton(inner, item);
            }

            slide.appendChild(inner);
            slidesContainer.appendChild(slide);
        });
    }

    /**
     * Initialize Glide.js carousel with coverflow effect
     */
    function initGlide() {
        const glideElement = galleryOverlay.querySelector('.glide');
        const slideCount = galleryMediaItems.length;

        // Determine perView based on slide count
        const perView = slideCount >= 3 ? 3 : slideCount;

        galleryGlide = new Glide(glideElement, {
            type: 'slider',
            startAt: 0,
            focusAt: 'center',
            perView: perView,
            gap: 20,
            peek: { before: 50, after: 50 },
            keyboard: false, // We handle keyboard ourselves
            rewind: false,
            animationDuration: 400,
            animationTimingFunc: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
            swipeThreshold: 80,
            dragThreshold: 120,
            breakpoints: {
                768: { perView: 1, peek: 0, gap: 0 }
            }
        });

        // Update UI and coverflow on slide change
        galleryGlide.on(['mount.after', 'run'], () => {
            updateCoverflowClasses();
            updateGalleryUI();
            handleSlideChange();
        });

        galleryGlide.mount();
    }

    /**
     * Apply coverflow CSS classes based on slide position
     */
    function updateCoverflowClasses() {
        if (!galleryGlide) return;

        const slides = galleryOverlay.querySelectorAll('.glide__slide');
        const currentIndex = galleryGlide.index;

        slides.forEach((slide, i) => {
            // Remove all position classes
            slide.classList.remove('is-active', 'is-prev', 'is-next', 'is-far-prev', 'is-far-next');

            const diff = i - currentIndex;

            if (diff === 0) {
                slide.classList.add('is-active');
            } else if (diff === -1) {
                slide.classList.add('is-prev');
            } else if (diff === 1) {
                slide.classList.add('is-next');
            } else if (diff < -1) {
                slide.classList.add('is-far-prev');
            } else if (diff > 1) {
                slide.classList.add('is-far-next');
            }
        });
    }

    /**
     * Handle slide change - autoplay video, pause others
     */
    function handleSlideChange() {
        if (!galleryGlide) return;
        const index = galleryGlide.index;

        // Pause all videos except current ---improve logic by fetching all slides, then using ifs to check for class and index for more accurate iteration
        galleryOverlay.querySelectorAll('.glide__slide video').forEach((video, i) => {
            if (i + 1 === index) {
                video.play().catch(() => { });
            } else {
                video.pause();
            }
        });

        // Control GIF playback - only active slide plays
        updateGalleryGifPlayback(index);

        // Preload upcoming slides
        preloadGalleryItems(galleryMediaItems, index + 1, 3);
    }

    /**
     * Update gallery UI elements
     */
    function updateGalleryUI() {
        if (!galleryGlide) return;

        const index = galleryGlide.index;
        const total = galleryMediaItems.length;

        // Update counter
        galleryOverlay.querySelector('.gallery-counter').textContent = `${index + 1} / ${total}`;

        // Update nav buttons
        galleryOverlay.querySelector('.gallery-nav.prev').disabled = index === 0;
        galleryOverlay.querySelector('.gallery-nav.next').disabled = index === total - 1;

        // Update thumbnails
        galleryOverlay.querySelectorAll('.gallery-thumb').forEach((t, i) => {
            t.classList.toggle('active', i === index);
        });

        galleryOverlay.querySelectorAll('.gallery-thumb')[index]?.scrollIntoView({ behavior: 'smooth', inline: 'center' });
    }

    function buildGalleryThumbnails() {
        const strip = galleryOverlay.querySelector('.gallery-thumbnails');
        strip.innerHTML = '';

        galleryMediaItems.forEach((item, index) => {
            const thumb = document.createElement('div');
            thumb.className = 'gallery-thumb';
            if (index === 0) thumb.classList.add('active');

            if (item.type === 'image') {
                const img = document.createElement('img');
                img.src = item.url;
                img.loading = 'lazy';
                thumb.appendChild(img);
            } else if (item.type === 'video') {
                thumb.innerHTML = icon('play_arrow', 'md', true);
            } else if (item.type === 'audio') {
                thumb.innerHTML = icon('headphones', 'sm', true);
            } else if (item.type === 'text') {
                thumb.innerHTML = icon('article', 'sm', true);
            } else {
                thumb.innerHTML = icon('attach_file', 'sm', true);
            }

            thumb.onclick = () => {
                if (galleryGlide) {
                    galleryGlide.go('=' + index);
                }
            };
            strip.appendChild(thumb);
        });
    }

    function addDownloadButton(container, item) {
        const btn = document.createElement('button');
        btn.className = 'gallery-item-download';
        btn.innerHTML = icon('download', 'md');
        btn.title = `Download ${item.name || item.type}`;
        btn.onclick = (e) => {
            e.stopPropagation();
            downloadSingleMedia(item, btn);
        };
        container.appendChild(btn);
    }

    function navigateGallery(dir) {
        if (!galleryGlide) return;
        if (dir < 0) {
            galleryGlide.go('<');
        } else {
            galleryGlide.go('>');
        }
    }

    function closeGallery() {
        if (!galleryOverlay) return;

        // Pause all videos
        galleryOverlay.querySelectorAll('video').forEach(video => {
            video.pause();
            video.src = '';
        });

        // Destroy Glide instance
        if (galleryGlide) {
            galleryGlide.destroy();
            galleryGlide = null;
        }

        galleryOverlay.classList.remove('active');
        document.body.style.overflow = '';
        galleryMediaItems = [];
        galleryPostUrl = null;
    }

    function handleGalleryKeyboard(e) {
        if (!galleryOverlay?.classList.contains('active')) return;

        // Keys to intercept when gallery is open
        const interceptKeys = ['Escape', 'ArrowLeft', 'ArrowRight', ' ', 'ArrowUp', 'ArrowDown'];

        if (interceptKeys.includes(e.key)) {
            // Block ALL propagation to prevent native page navigation
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();

            switch (e.key) {
                case 'Escape':
                    closeGallery();
                    break;
                case 'ArrowLeft':
                    navigateGallery(-1);
                    break;
                case 'ArrowRight':
                    navigateGallery(1);
                    break;
                case ' ':
                    const video = galleryOverlay.querySelector('.glide__slide.is-active video');
                    if (video) video.paused ? video.play() : video.pause();
                    break;
                // ArrowUp/Down - just block, don't do anything
            }
        }
    }

    // =========================================================================
    // DOWNLOADS
    // =========================================================================

    function getProgressToast() {
        let toast = document.getElementById('download-toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.id = 'download-toast';
            toast.className = 'download-toast';
            toast.innerHTML = `
                <div class="download-toast-header">
                    <span class="download-toast-title">Downloading...</span>
                    <button class="download-toast-close">${icon('close', 'sm')}</button>
                </div>
                <div class="download-toast-progress"><div class="download-toast-progress-bar"></div></div>
                <div class="download-toast-status">Preparing...</div>
                <div class="download-toast-filename"></div>
            `;
            toast.querySelector('.download-toast-close').onclick = () => toast.classList.remove('active');
            document.body.appendChild(toast);
        }
        return toast;
    }

    function updateToast(toast, { title, progress, status, filename, show = true }) {
        if (title !== undefined) toast.querySelector('.download-toast-title').textContent = title;
        if (progress !== undefined) toast.querySelector('.download-toast-progress-bar').style.width = `${Math.min(100, progress)}%`;
        if (status !== undefined) toast.querySelector('.download-toast-status').textContent = status;
        if (filename !== undefined) toast.querySelector('.download-toast-filename').textContent = filename;
        if (show) toast.classList.add('active');
    }

    function hideToast(toast, delay = 3000) {
        setTimeout(() => toast.classList.remove('active'), delay);
    }

    function getDownloadFilename(item) {
        let filename = getFilenameFromUrl(item.url, item.name);
        if (settings.prefixFilenamesWithTitle && item.postTitle) {
            const prefix = sanitiseFilename(item.postTitle, settings.maxFilenamePrefixLength);
            if (prefix) filename = `${prefix}_${filename}`;
        }
        return filename;
    }

    async function downloadSingleMedia(item, btn) {
        if (!item?.url) return;

        const filename = getDownloadFilename(item);

        if (btn) {
            btn.disabled = true;
            btn.classList.add('downloading');
            btn.innerHTML = '⏳';
        }

        try {
            const finalUrl = await resolveRedirectUrl(item.url);
            const response = await gmFetch(finalUrl, { responseType: 'blob', timeout: 300000 });

            const blobUrl = URL.createObjectURL(response.response);
            const a = document.createElement('a');
            a.href = blobUrl;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            a.remove();
            setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);

            if (btn) btn.innerHTML = icon('check', 'md');
        } catch {
            if (btn) btn.innerHTML = icon('close', 'md');
        }

        if (btn) {
            setTimeout(() => {
                btn.innerHTML = icon('download', 'md');
                btn.disabled = false;
                btn.classList.remove('downloading');
            }, 1500);
        }
    }

    async function downloadAllMedia() {
        const downloadable = galleryMediaItems.filter(i => i.isDownloadable !== false);
        if (downloadable.length === 0) return;

        const btn = galleryOverlay.querySelector('.gallery-download-all');
        const label = btn.querySelector('.download-label');
        const progress = btn.querySelector('.download-progress');
        const toast = getProgressToast();

        btn.disabled = true;
        label.style.display = 'none';
        progress.style.display = 'inline';

        const postTitle = sanitiseFilename(galleryOverlay.querySelector('.gallery-info')?.textContent || 'media', 50);
        const zipFilename = `${postTitle}_${Date.now()}.zip`;

        updateToast(toast, { title: 'Preparing Download', progress: 0, status: `0 / ${downloadable.length} files`, filename: zipFilename });

        const usedNames = new Set();
        const getUniqueName = (name) => {
            if (!usedNames.has(name)) { usedNames.add(name); return name; }
            const dot = name.lastIndexOf('.');
            const base = dot > 0 ? name.slice(0, dot) : name;
            const ext = dot > 0 ? name.slice(dot) : '';
            let i = 1;
            while (usedNames.has(`${base}_${i}${ext}`)) i++;
            const unique = `${base}_${i}${ext}`;
            usedNames.add(unique);
            return unique;
        };

        // Collect files for fflate
        const files = {};
        let completed = 0, failed = 0;

        console.log('[BetterUI] Starting download of', downloadable.length, 'files');

        for (const item of downloadable) {
            const filename = getUniqueName(getDownloadFilename(item));
            updateToast(toast, { status: `Downloading ${completed + 1} / ${downloadable.length}`, filename, progress: (completed / downloadable.length) * 85 });
            progress.textContent = `${completed + 1}/${downloadable.length}`;

            try {
                console.log('[BetterUI] Fetching:', filename);
                const finalUrl = await resolveRedirectUrl(item.url);
                const response = await gmFetch(finalUrl, { responseType: 'arraybuffer', timeout: 300000 });

                // Convert ArrayBuffer to Uint8Array for fflate
                const uint8Array = new Uint8Array(response.response);
                files[filename] = uint8Array;
                console.log('[BetterUI] Downloaded:', filename, uint8Array.byteLength, 'bytes');
            } catch (e) {
                console.error('[BetterUI] Download failed:', filename, e);
                failed++;
            }
            completed++;
        }

        const fileCount = Object.keys(files).length;
        console.log('[BetterUI] Creating zip with', fileCount, 'files');

        if (fileCount === 0) {
            updateToast(toast, { title: 'Download Failed', status: 'No files could be downloaded', progress: 0 });
            hideToast(toast, 5000);
            label.style.display = 'inline';
            progress.style.display = 'none';
            label.innerHTML = `${icon('error', 'sm')} Error`;
            setTimeout(() => { label.textContent = 'Download All'; btn.disabled = false; }, 2000);
            return;
        }

        updateToast(toast, { status: 'Creating zip...', progress: 88 });

        try {
            // Use fflate to create zip - wrapped in Promise for async/await
            const zipData = await new Promise((resolve, reject) => {
                console.log('[BetterUI] Starting fflate.zip()');

                // fflate.zip(files, options, callback)
                fflate.zip(files, { level: 6 }, (err, data) => {
                    if (err) {
                        console.error('[BetterUI] fflate error:', err);
                        reject(err);
                    } else {
                        console.log('[BetterUI] fflate complete, size:', data.byteLength);
                        resolve(data);
                    }
                });
            });

            updateToast(toast, { status: 'Preparing download...', progress: 95 });
            console.log('[BetterUI] Zip created:', zipData.byteLength, 'bytes');

            const blob = new Blob([zipData], { type: 'application/zip' });
            const blobUrl = URL.createObjectURL(blob);

            const a = document.createElement('a');
            a.href = blobUrl;
            a.download = zipFilename;
            document.body.appendChild(a);
            a.click();
            a.remove();

            setTimeout(() => {
                URL.revokeObjectURL(blobUrl);
                console.log('[BetterUI] Blob URL revoked');
            }, 60000);

            updateToast(toast, { title: 'Download Complete', status: `Downloaded ${completed - failed} of ${downloadable.length} files`, progress: 100 });
            label.innerHTML = `${icon('check_circle', 'sm')} Done`;
            hideToast(toast, 3000);
            console.log('[BetterUI] Download complete');
        } catch (e) {
            console.error('[BetterUI] Zip creation failed:', e);
            updateToast(toast, { title: 'Download Failed', status: e.message || 'Zip creation failed' });
            label.innerHTML = `${icon('error', 'sm')} Error`;
            hideToast(toast, 5000);
        }

        label.style.display = 'inline';
        progress.style.display = 'none';
        setTimeout(() => { label.textContent = 'Download All'; btn.disabled = false; }, 2000);
    }

    // =========================================================================
    // GALLERY CLICK HANDLING
    // =========================================================================

    function handleGalleryClick(e) {
        // Handle regular retry click
        const retryIndicator = e.target.closest('.video-play-indicator.retry-available');
        if (retryIndicator) {
            const link = retryIndicator.closest('.fancy-link');
            if (link) {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
                handleVideoThumbnailRetry(link);
            }
            return;
        }

        // Handle AI retry click
        const aiRetryIndicator = e.target.closest('.video-play-indicator.ai-retry-available');
        if (aiRetryIndicator) {
            const link = aiRetryIndicator.closest('.fancy-link');
            if (link) {
                e.preventDefault();
                e.stopPropagation();
                handleAiThumbnailRetry(link);
            }
            return;
        }

        // Skip regular retry clicks
        if (e.target.closest('.video-play-indicator.retry-available')) return;

        const wrapper = e.target.closest('.card-thumbnail-wrapper') || e.target.closest('.post-card__image-container');
        if (!wrapper) return;

        const link = wrapper.closest('.fancy-link[href*="/post/"]');
        if (!link) return;

        debugLog('Gallery click intercepted');
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        openGallery(link);
    }

    function bindGalleryClickHandler() {
        if (galleryDocumentHandlerBound) return;
        galleryDocumentHandlerBound = true;

        // Checkbox handler - must be registered first to run first in capture phase
        document.addEventListener('click', (e) => {
            const checkbox = e.target.closest('.post-select-checkbox');
            if (checkbox) {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
                const link = checkbox.closest('.fancy-link');
                if (link) togglePostSelection(link);
            }
        }, true);

        document.addEventListener('click', handleGalleryClick, true);
    }

    function addGalleryClickHandler(link) {
        if (link.dataset.galleryClick) return;
        link.dataset.galleryClick = 'true';
        bindGalleryClickHandler();

        const wrapper = link.querySelector('.card-thumbnail-wrapper, .post-card__image-container');
        if (wrapper) wrapper.style.cursor = 'pointer';
    }

    // =========================================================================
    // BULK SELECTION & DOWNLOAD
    // =========================================================================

    /**
     * Create the bulk action bar UI
     */
    function createBulkActionBar() {
        if (bulkActionBar) return;

        bulkActionBar = document.createElement('div');
        bulkActionBar.className = 'bulk-action-bar';
        bulkActionBar.innerHTML = `
            <div class="selection-info">
                <span>${icon('check_box', 'sm')}</span>
                <span><span class="selection-count">0</span> posts selected</span>
            </div>
            <div class="bulk-actions">
                <button class="btn-select-all">${icon('select_all', 'sm')}Select All</button>
                <button class="btn-clear">${icon('close', 'sm')}Clear</button>
                <button class="btn-download">${icon('download', 'sm')}Download All</button>
            </div>
        `;

        bulkActionBar.querySelector('.btn-select-all').addEventListener('click', selectAllPosts);
        bulkActionBar.querySelector('.btn-clear').addEventListener('click', clearSelection);
        bulkActionBar.querySelector('.btn-download').addEventListener('click', startBulkDownload);

        document.body.appendChild(bulkActionBar);
    }

    /**
     * Update the bulk action bar visibility and count
     */
    function updateBulkActionBar() {
        if (!bulkActionBar) return;

        const count = selectedPosts.size;
        bulkActionBar.querySelector('.selection-count').textContent = count;

        if (count > 0) {
            bulkActionBar.classList.add('visible');
        } else {
            bulkActionBar.classList.remove('visible');
        }
    }

    /**
     * Add selection checkbox to a post card
     */
    function addSelectionCheckbox(link) {
        const wrapper = link.querySelector('.card-thumbnail-wrapper');
        if (!wrapper || wrapper.querySelector('.post-select-checkbox')) return;

        const checkbox = document.createElement('div');
        checkbox.className = 'post-select-checkbox';
        checkbox.innerHTML = icon('check', 'sm');
        checkbox.title = 'Select for bulk download';

        // Use capture phase to intercept before gallery handler
        checkbox.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            togglePostSelection(link);
        }, true);

        wrapper.appendChild(checkbox);
    }

    /**
     * Toggle selection state for a post
     */
    async function togglePostSelection(link) {
        const postUrl = link.href;
        const checkbox = link.querySelector('.post-select-checkbox');

        if (selectedPosts.has(postUrl)) {
            selectedPosts.delete(postUrl);
            checkbox?.classList.remove('selected');
        } else {
            const parsed = parsePostUrl(postUrl);
            if (!parsed) return;

            // Get post data
            let postData = postDataCache.get(postUrl);
            if (!postData) {
                const cacheKey = `${parsed.baseUrl}/${parsed.service}/user/${parsed.userId}`;
                postData = userPostsCache.get(cacheKey)?.get(parsed.postId);
            }

            selectedPosts.set(postUrl, { parsed, postData });
            checkbox?.classList.add('selected');
        }

        updateBulkActionBar();
    }

    /**
     * Select all visible posts
     */
    function selectAllPosts() {
        document.querySelectorAll('.fancy-link[data-card-setup]').forEach(link => {
            if (!selectedPosts.has(link.href)) {
                togglePostSelection(link);
            }
        });
    }

    /**
     * Clear all selections
     */
    function clearSelection() {
        selectedPosts.forEach((_, postUrl) => {
            const link = document.querySelector(`.fancy-link[href="${postUrl}"]`);
            const checkbox = link?.querySelector('.post-select-checkbox');
            checkbox?.classList.remove('selected');
        });
        selectedPosts.clear();
        updateBulkActionBar();
    }

    /**
     * Start bulk download of selected posts
     */
    async function startBulkDownload() {
        if (bulkDownloadInProgress || selectedPosts.size === 0) return;

        bulkDownloadInProgress = true;
        const downloadBtn = bulkActionBar.querySelector('.btn-download');
        const actionsDiv = bulkActionBar.querySelector('.bulk-actions');

        // Show progress UI
        const originalContent = actionsDiv.innerHTML;
        actionsDiv.innerHTML = `
            <div class="bulk-progress">
                <span class="progress-text">Preparing...</span>
                <div class="bulk-progress-bar">
                    <div class="bulk-progress-fill" style="width: 0%"></div>
                </div>
            </div>
        `;

        const progressText = actionsDiv.querySelector('.progress-text');
        const progressFill = actionsDiv.querySelector('.bulk-progress-fill');

        try {
            const allFiles = [];
            const posts = Array.from(selectedPosts.entries());
            let processedPosts = 0;

            // Collect all files from selected posts
            for (const [postUrl, { parsed, postData }] of posts) {
                progressText.textContent = `Scanning ${processedPosts + 1}/${posts.length}...`;
                progressFill.style.width = `${(processedPosts / posts.length) * 30}%`;

                let data = postData;
                if (!data && parsed) {
                    try {
                        data = await fetchJson(`${parsed.baseUrl}/api/v1/${parsed.service}/user/${parsed.userId}/post/${parsed.postId}`);
                    } catch (e) {
                        debugLog('Failed to fetch post data:', postUrl);
                    }
                }

                if (data) {
                    const mediaItems = extractMediaFromPost(data, parsed.baseUrl, {});
                    const postTitle = sanitizeFilename(data.title || parsed.postId);

                    mediaItems.forEach((item, idx) => {
                        if (item.type === 'text') return;
                        allFiles.push({
                            url: item.url,
                            filename: item.filename,
                            postTitle,
                            postId: parsed.postId
                        });
                    });
                }

                processedPosts++;
            }

            if (allFiles.length === 0) {
                showToast('No files found in selected posts', 'error');
                return;
            }

            progressText.textContent = `Downloading ${allFiles.length} files...`;

            // Download files and create ZIP
            const zipFiles = {};
            let downloadedCount = 0;

            for (const file of allFiles) {
                try {
                    const response = await gmFetch(file.url, {
                        responseType: 'arraybuffer',
                        timeout: 120000
                    });
                    if (typeof file.filename === 'undefined') {
                        file.filename = getFilenameFromUrl(file.url);
                    }

                    // Create folder structure: postTitle_postId/filename
                    const folderName = `${file.postTitle}_${file.postId}`;
                    const filePath = `${folderName}/${file.filename}`;

                    zipFiles[filePath] = new Uint8Array(response.response);
                    downloadedCount++;

                    const progress = 30 + (downloadedCount / allFiles.length) * 60;
                    progressFill.style.width = `${progress}%`;
                    progressText.textContent = `Downloading ${downloadedCount}/${allFiles.length}...`;
                } catch (e) {
                    debugLog('Failed to download:', file.url, e.message);
                }
            }

            if (downloadedCount === 0) {
                showToast('Failed to download any files', 'error');
                return;
            }

            progressText.textContent = 'Creating ZIP...';
            progressFill.style.width = '95%';

            // Create ZIP using fflate
            const zipped = fflate.zipSync(zipFiles, { level: 0 });
            const blob = new Blob([zipped], { type: 'application/zip' });

            // Generate filename with timestamp
            const timestamp = new Date().toISOString().slice(0, 10);
            const creatorNameLookupByElement = document.querySelector('.user-header__profile span[itemprop="name"]').textContent;
            const creatorMatch = window.location.pathname.match(/\/([^/]+)\/user\/([^/]+)/);
            const creatorName = creatorNameLookupByElement ? creatorNameLookupByElement : (creatorMatch ? creatorMatch[2] : 'posts');
            const zipFilename = `${creatorName}_${selectedPosts.size}_posts_${timestamp}.zip`;

            // Trigger download
            const downloadUrl = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = downloadUrl;
            a.download = zipFilename;
            a.click();
            URL.revokeObjectURL(downloadUrl);

            progressFill.style.width = '100%';
            progressText.textContent = `Downloaded ${downloadedCount} files!`;

            showToast(`Downloaded ${downloadedCount} files from ${selectedPosts.size} posts`, 'success');

            // Clear selection after successful download
            setTimeout(() => {
                clearSelection();
            }, 1500);
        } catch (e) {
            debugLog('Bulk download error:', e);
            showToast('Download failed: ' + e.message, 'error');
        } finally {
            bulkDownloadInProgress = false;
            setTimeout(() => {
                actionsDiv.innerHTML = originalContent;
                actionsDiv.querySelector('.btn-select-all').addEventListener('click', selectAllPosts);
                actionsDiv.querySelector('.btn-clear').addEventListener('click', clearSelection);
                actionsDiv.querySelector('.btn-download').addEventListener('click', startBulkDownload);
            }, 2000);
        }
    }

    // =========================================================================
    // POST CARD SETUP
    // =========================================================================

    async function setupPostCards() {
        let links;
        if (isPopularPage()) {
            // Popular page uses a different DOM structure
            links = document.querySelectorAll('article.post-card a.fancy-link');
        } else {
            // User pages & others use the regular structure
            links = document.querySelectorAll('.card-list .card-list__items .post-card .fancy-link');
        }

        // Get current page offset from URL
        const currentOffset = getPageOffset();

        // Collect unique users for batch fetching
        const users = new Map();
        links.forEach(link => {
            if (!link.href?.includes('/post/') || link.dataset.cardSetup) return;
            const parsed = parsePostUrl(link.href);
            if (!parsed) return;
            const key = `${parsed.baseUrl}/${parsed.service}/user/${parsed.userId}`;
            if (!users.has(key)) users.set(key, parsed);
        });

        // Batch fetch all users with current page offset
        await Promise.all([...users.values()].map(p => batchFetchUserPosts(p.baseUrl, p.service, p.userId, currentOffset)));

        // Process each card
        links.forEach(link => {
            if (!link.href?.includes('/post/') || link.dataset.cardSetup) return;
            link.dataset.cardSetup = 'true';

            // FORCE attach observer for video queue
            if (window._videoObserver) {
                window._videoObserver.observe(link);
            }

            getOrCreateThumbnailWrapper(link);

            const parsed = parsePostUrl(link.href);
            if (!parsed) return;

            const cacheKey = `${parsed.baseUrl}/${parsed.service}/user/${parsed.userId}`;
            const postsMap = userPostsCache.get(cacheKey);
            const postData = postsMap?.get(parsed.postId);

            if (!postData) return;

            const counts = countPostFiles(postData);
            const videoItems = extractMediaFromPost(postData, parsed.baseUrl, { type: 'video' });
            const imageItems = extractMediaFromPost(postData, parsed.baseUrl, { type: 'image' });
            if (isPopularPage()) {
                document.querySelectorAll('article.post-card[data-user]').forEach(article => {
                    const username = article.dataset.user;
                    if (!username) return;

                    if (article.querySelector('.model-label')) return;

                    const div = document.createElement('div');
                    div.className = 'model-label';
                    div.textContent = username;

                    const link = article.querySelector('a.fancy-link');
                    if (link) article.insertBefore(div, link);
                });
            }

            addGalleryClickHandler(link);
            addSelectionCheckbox(link);
            insertFileCountOverlay(link, counts);
            if (videoItems.length > 0) {
                const videoUrl = videoItems[0].url;

                getCachedThumbnail(videoUrl).then(cache => {
                    if (!cache || !cache.duration) return;

                    const formatted = formatDuration(cache.duration);
                    if (!formatted) return;

                    const footer = link.querySelector('.post-card__footer');
                    if (!footer) return;

                    // Prevent duplicate insertions
                    if (footer.querySelector('.video-duration-badge')) return;

                    const badge = document.createElement('span');
                    badge.className = 'video-duration-badge';
                    badge.textContent = formatted;
                    footer.appendChild(badge);
                });
            }

            if (videoItems.length > 0) {
                insertAvatarPlaceholder(link);

                const playIcon = document.createElement('div');
                playIcon.className = 'video-play-indicator';
                playIcon.innerHTML = icon('play_arrow', 'lg', true);
                link.querySelector('.card-thumbnail-wrapper')?.appendChild(playIcon);

                if (videoItems.length > 0) {
                    insertAvatarPlaceholder(link);

                    const playIcon = document.createElement('div');
                    playIcon.className = 'video-play-indicator';
                    playIcon.innerHTML = icon('play_arrow', 'lg', true);
                    link.querySelector('.card-thumbnail-wrapper')?.appendChild(playIcon);

                    // Always let observer handle queueing
                    window._videoObserver.observe(link);
                }
            }
        });
    }

    function updateFooterDuration(link) {
        const dur = link.dataset.videoDuration;
        if (!dur) return;

        const footer = link.querySelector('.post-card__footer div div');
        if (!footer) return;

        if (!footer.querySelector('.video-duration')) {
            const span = document.createElement('span');
            span.className = 'video-duration';
            span.style.marginLeft = '6px';
            span.textContent = formatDuration(dur);
            footer.appendChild(span);
        }
    }

    // =========================================================================
    // POST PAGE VIDEO CAPTURE
    // =========================================================================

    const capturedVideoUrls = new Set(); // Track captured videos to avoid duplicates

    /**
     * Capture first frame from video element and cache it
     */
    async function captureVideoFrame(video, videoUrl) {
        if (capturedVideoUrls.has(videoUrl)) return;

        // Check if already cached
        const cached = await getCachedThumbnail(videoUrl);
        if (cached) {
            debugLog('Video already cached:', videoUrl);
            capturedVideoUrls.add(videoUrl);
            return;
        }

        try {
            // Wait for video to have enough data
            if (video.readyState < 2) {
                await new Promise((resolve, reject) => {
                    const timeout = setTimeout(() => reject(new Error('Timeout')), 10000);
                    video.addEventListener('loadeddata', () => {
                        clearTimeout(timeout);
                        resolve();
                    }, { once: true });
                });
            }

            // Only seek if video is paused and near the start
            // Don't disrupt active playback
            if (video.paused && video.currentTime < 1) {
                const seekTo = Math.min(2, video.duration * 0.1);
                video.currentTime = seekTo;
                await new Promise(resolve => {
                    video.addEventListener('seeked', resolve, { once: true });
                });
            }

            // Capture frame at current position
            const canvas = document.createElement('canvas');
            canvas.width = CONFIG.thumbnailSize;
            canvas.height = CONFIG.thumbnailSize;
            const ctx = canvas.getContext('2d');

            const aspect = video.videoWidth / video.videoHeight;
            let dw, dh, dx, dy;
            if (aspect > 1) {
                dh = canvas.height;
                dw = dh * aspect;
                dx = (canvas.width - dw) / 2;
                dy = 0;
            } else {
                dw = canvas.width;
                dh = dw / aspect;
                dx = 0;
                dy = (canvas.height - dh) / 2;
            }

            ctx.fillStyle = '#000';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            ctx.drawImage(video, dx, dy, dw, dh);

            const dataUrl = canvas.toDataURL('image/jpeg', 0.85);

            // Cache it
            await cacheThumbnail(videoUrl, dataUrl, video.duration);
            capturedVideoUrls.add(videoUrl);

            debugLog('Captured and cached video frame:', videoUrl);
        } catch (e) {
            debugLog('Failed to capture video frame:', e.message);
        }
    }

    /**
     * Setup video frame capture on post pages
     * Monitors video elements and captures frames when they load/play
     */
    function initPostPageCapture() {
        if (!isPostPage()) return;

        debugLog('Setting up post page video capture');

        // Find all video elements on the page
        const setupVideoCapture = () => {
            document.querySelectorAll('video').forEach(video => {
                if (video.dataset.captureSetup) return;
                video.dataset.captureSetup = 'true';

                // Get video URL from src or source element
                let videoUrl = video.src || video.querySelector('source')?.src;
                if (!videoUrl) return;

                // Normalize URL
                if (videoUrl.startsWith('/')) {
                    videoUrl = window.location.origin + videoUrl;
                }

                debugLog('Found video on post page:', videoUrl);

                // Capture on various events
                const doCapture = () => captureVideoFrame(video, videoUrl);

                // Try to capture when video has data
                if (video.readyState >= 2) {
                    doCapture();
                } else {
                    video.addEventListener('loadeddata', doCapture, { once: true });
                }

                // Also capture on play (in case loadeddata already fired)
                video.addEventListener('play', doCapture, { once: true });
            });
        };

        // Initial setup
        setupVideoCapture();

        // Watch for dynamically added videos
        const observer = new MutationObserver(mutations => {
            if (mutations.some(m => m.addedNodes.length > 0)) {
                setupVideoCapture();
            }
        });

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

    // =========================================================================
    // CLEANUP
    // =========================================================================

    function cleanup() {
        closeGallery();
        galleryOverlay?.remove();
        galleryOverlay = null;

        if (galleryDocumentHandlerBound) {
            document.removeEventListener('click', handleGalleryClick, true);
            galleryDocumentHandlerBound = false;
        }

        // Remove keyboard listener
        document.removeEventListener('keydown', handleGalleryKeyboard, true);

        videoThumbnailObserver?.disconnect();
        videoThumbnailObserver = null;

        videoThumbnailQueue = [];
        apiQueue = [];
        postDataCache.clear();
        pendingRequests.clear();
        userPostsCache.clear();
        fetchedOffsets.clear();
        pendingBatchFetches.clear();
        preloadedMedia.clear();
        selectedPosts.clear();
        bulkActionBar?.remove();
        bulkActionBar = null;
    }

    // =========================================================================
    // INITIALIZATION
    // =========================================================================

    async function init() {
        // Initialize thumbnail cache for both user pages and post pages
        await initThumbnailCache();

        // On post pages, just set up video capture
        if (isPostPage()) {
            debugLog('Initializing v3.10.4 (post page capture mode)');
            initPostPageCapture();
            return;
        }

        // On user list pages, run full initialization
        if (!isUserPage() && !isPopularPage()) {
            debugLog('Not a user page or popular, skipping');
            return;
        }

        debugLog('Initializing v3.10.4');

        // Initialize navigation observer for SPA navigation
        initNavigationObserver();

        if (isPopularPage()) {
            initVideoThumbnailObserver();
            const popObserver = new MutationObserver(() => setupPostCards());
            popObserver.observe(document.body, { childList: true, subtree: true });
        }

        getUserAvatarUrl();
        injectStyles();
        createSettingsUI();
        applyCardColumns();
        createBulkActionBar();
        initVideoThumbnailObserver();
        setupPostCards();


        // Process GIFs for hover-to-play
        if (settings.pauseGifsWhenHidden) {
            processPageGifs();
        }

        const mutationObserver = new MutationObserver(mutations => {
            if (mutations.some(m => m.addedNodes.length > 0)) {
                initVideoThumbnailObserver();
                setupPostCards();
                if (settings.pauseGifsWhenHidden) processPageGifs();
            }
        });

        mutationObserver.observe(document.body, { childList: true, subtree: true });
        window.addEventListener('beforeunload', cleanup);

        debugLog('Initialized');
    }

    if (document.readyState === 'complete') init();
    else window.addEventListener('load', init);
})();