Gander

Add preview popups for torrent/group links on redacted.sh and orpheus.network

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Gander
// @namespace    waiter7@red
// @version      1.3.7
// @description  Add preview popups for torrent/group links on redacted.sh and orpheus.network
// @author       waiter7
// @match        https://redacted.sh/*
// @match        https://orpheus.network/*
// @license      MIT
// @grant        GM_xmlhttpRequest
// @connect      redacted.sh
// @connect      orpheus.network
// ==/UserScript==

(function() {
    'use strict';

    // Get base URL for current site
    function getBaseUrl() {
        const hostname = window.location.hostname;
        if (hostname.includes('orpheus.network')) {
            return 'https://orpheus.network';
        } else {
            return 'https://redacted.sh';
        }
    }

    const BASE_URL = getBaseUrl();

    // Cache for API responses to avoid duplicate requests
    const cache = new Map();
    // Cache for artist popup DOM elements (keyed by artistId)
    const artistPopupCache = new Map();
    let currentPopup = null;

    // Helper Functions
    
    // Add hover effect to element
    function addHoverEffect(element, hoverOpacity = '1', defaultOpacity = '0.6') {
        element.addEventListener('mouseenter', () => {
            element.style.opacity = hoverOpacity;
        });
        element.addEventListener('mouseleave', () => {
            element.style.opacity = defaultOpacity;
        });
    }

    // Create preview icon
    function createPreviewIcon(title, onClick) {
        const icon = document.createElement('span');
        icon.innerHTML = ' 🔍';
        icon.style.cursor = 'pointer';
        icon.style.fontSize = '0.9em';
        icon.style.opacity = '0.6';
        icon.style.transition = 'opacity 0.2s';
        icon.title = title;
        
        addHoverEffect(icon);
        
        icon.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            onClick(e);
        });
        
        return icon;
    }

    // Parse torrent URL to extract groupId and torrentId
    function parseTorrentUrl(href) {
        try {
            const url = new URL(href, window.location.origin);
            return {
                groupId: url.searchParams.get('id'),
                torrentId: url.searchParams.get('torrentid')
            };
        } catch (e) {
            return { groupId: null, torrentId: null };
        }
    }

    // Parse artist URL to extract artistId
    function parseArtistUrl(href) {
        try {
            const url = new URL(href, window.location.origin);
            return url.searchParams.get('id');
        } catch (e) {
            return null;
        }
    }

    // Get cache key for API responses
    function getCacheKey(type, id) {
        return `${type}-${id}`;
    }

    // Ensure scrollbar styles are added (only once)
    function ensureScrollbarStyles() {
        if (!document.querySelector('style[data-torrent-preview-scrollbar]')) {
            const scrollbarStyle = document.createElement('style');
            scrollbarStyle.setAttribute('data-torrent-preview-scrollbar', 'true');
            scrollbarStyle.textContent = `
                [data-torrent-preview-popup]::-webkit-scrollbar {
                    width: 8px;
                }
                [data-torrent-preview-popup]::-webkit-scrollbar-track {
                    background: rgba(0, 0, 0, 0.2);
                }
                [data-torrent-preview-popup]::-webkit-scrollbar-thumb {
                    background: rgba(255, 255, 255, 0.2);
                    border-radius: 4px;
                }
                [data-torrent-preview-popup]::-webkit-scrollbar-thumb:hover {
                    background: rgba(255, 255, 255, 0.3);
                }
                
                @keyframes popupFadeIn {
                    0% {
                        opacity: 0;
                        transform: scale(0.9) translateY(-10px);
                    }
                    100% {
                        opacity: 1;
                        transform: scale(1) translateY(0);
                    }
                }
                
                [data-torrent-preview-popup] {
                    animation: popupFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
                }
                
                .loading-ellipses {
                    display: inline-block;
                    width: 1.2em;
                    text-align: left;
                }
                
                .loading-ellipses::after {
                    content: '...';
                    animation: loadingEllipses 1.4s steps(4, end) infinite;
                }
                
                @keyframes loadingEllipses {
                    0% {
                        content: '';
                    }
                    25% {
                        content: '.';
                    }
                    50% {
                        content: '..';
                    }
                    75%, 100% {
                        content: '...';
                    }
                }
            `;
            document.head.appendChild(scrollbarStyle);
        }
    }

    // Setup base popup styles
    function setupPopupStyles(popup, width = '380px', maxHeight = '600px') {
        popup.setAttribute('data-torrent-preview-popup', 'true');
        popup.style.position = 'absolute';
        popup.style.zIndex = '10000';
        popup.style.width = width;
        popup.style.maxHeight = maxHeight;
        popup.style.overflowY = 'auto';
        popup.style.overflowX = 'hidden';
        popup.style.scrollbarWidth = 'thin';
        popup.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.5)';
        popup.style.backgroundColor = '#1a1a1a';
        popup.style.borderRadius = '8px';
        popup.style.contain = 'layout style paint';
    }

    // Add preview icons to all torrent links
    function addPreviewIcons() {
        // Skip top10.php page due to layout issues
        if (window.location.pathname === '/top10.php') {
            return;
        }

        // Don't add icons when viewing a specific torrent, group, or artist page
        const currentUrl = new URL(window.location.href);
        // const currentGroupId = currentUrl.searchParams.get('id');
        // const currentTorrentId = currentUrl.searchParams.get('torrentid');
        // const currentArtistId = window.location.pathname === '/artist.php' ? currentUrl.searchParams.get('id') : null;
        // if (currentGroupId || currentTorrentId || currentArtistId) {
            // return;
        // }

        // Find all torrent links that don't already have preview icons
        const links = document.querySelectorAll('a[href*="torrents.php"]');

        links.forEach(link => {
            // Skip if already processed
            if (link.dataset.previewAdded) return;

            // Skip if inside a popup
            if (link.closest('[data-torrent-preview-popup]')) return;

            // Skip button classes
            if (link.classList.contains('button_fl') || link.classList.contains('button_dl')) return;

            // Skip links with hash fragments (e.g., #postid, #info, etc.)
            if (link.href.includes('#')) return;

            // Skip if link contains an image
            if (link.querySelector('img')) return;

            const href = link.getAttribute('href');
            if (!href) return;

            // Parse URL to determine if it's a group or torrent link
            const { groupId, torrentId } = parseTorrentUrl(href);

            // Only add icon if it's a valid group or torrent link
            if (!groupId && !torrentId) return;

            // Create preview icon
            const icon = createPreviewIcon('Preview torrent', (e) => {
                showPreview(groupId, torrentId, e);
            });

            // Smart icon placement: check link context for better placement
            const parent = link.parentElement;
            const isInTable = parent && (parent.tagName === 'TD' || parent.tagName === 'TH');
            const isListingPage = window.location.pathname === '/torrents.php' && 
                                 !window.location.search.includes('id=') && 
                                 !window.location.search.includes('torrentid=');
            
            // On listing pages or in tables, use insertAdjacentElement for precise placement
            if (isListingPage || isInTable) {
                link.insertAdjacentElement('afterend', icon);
            } else {
                // Default: place after link
                link.after(icon);
            }
            link.dataset.previewAdded = 'true';
        });

        // Find all artist links
        const artistLinks = document.querySelectorAll('a[href*="artist.php"]');

        artistLinks.forEach(link => {
            // Skip if already processed
            if (link.dataset.artistPreviewAdded) return;

            // Skip if inside a popup
            if (link.closest('[data-torrent-preview-popup]')) return;

            // Skip button classes
            if (link.classList.contains('button_fl') || link.classList.contains('button_dl')) return;

            // Skip links with hash fragments (e.g., #artistcomments, #info, etc.)
            if (link.href.includes('#')) return;

            // Skip if link contains an image
            if (link.querySelector('img')) return;

            // Skip if link is inside torrent_action_buttons (DL/FL buttons area)
            if (link.closest('.torrent_action_buttons')) return;

            const href = link.getAttribute('href');
            if (!href) return;

            // Parse URL to get artist ID
            const artistId = parseArtistUrl(href);

            // Only add icon if it's a valid artist link
            if (!artistId) return;

            // Skip if we're on artist.php and the link is to the same artist
            if (window.location.pathname === '/artist.php') {
                const currentArtistId = currentUrl.searchParams.get('id');
                if (currentArtistId && artistId === currentArtistId) {
                    return;
                }
            }

            // Create preview icon
            const icon = createPreviewIcon('Preview artist', (e) => {
                showArtistPreview(artistId, e);
            });

            // Smart icon placement: artist links are often in <strong> tags with other content
            // Check if link is inside a <strong> tag - if so, ensure icon goes right after link
            const parent = link.parentElement;
            if (parent && parent.tagName === 'STRONG') {
                // If next sibling exists (like " - " text), insert before it
                // Otherwise, insert after the link
                if (link.nextSibling) {
                    parent.insertBefore(icon, link.nextSibling);
                } else {
                    link.insertAdjacentElement('afterend', icon);
                }
            } else {
                // Default: place immediately after link
                link.insertAdjacentElement('afterend', icon);
            }
            link.dataset.artistPreviewAdded = 'true';
        });
    }

    // Fetch data from API
    async function fetchData(groupId, torrentId) {
        const cacheKey = getCacheKey(torrentId ? 'torrent' : 'group', torrentId || groupId);

        // Check cache first
        if (cache.has(cacheKey)) {
            return cache.get(cacheKey);
        }

        return new Promise((resolve, reject) => {
            const action = torrentId ? 'torrent' : 'torrentgroup';
            const id = torrentId || groupId;
            const url = `${BASE_URL}/ajax.php?action=${action}&id=${id}`;

            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.status === 'success') {
                            cache.set(cacheKey, data.response);
                            resolve(data.response);
                        } else {
                            reject(new Error('API returned failure status'));
                        }
                    } catch (e) {
                        reject(e);
                    }
                },
                onerror: function(error) {
                    reject(error);
                }
            });
        });
    }

    // Fetch artist data from API
    async function fetchArtistData(artistId) {
        const cacheKey = getCacheKey('artist', artistId);

        // Check cache first
        if (cache.has(cacheKey)) {
            return cache.get(cacheKey);
        }

        return new Promise((resolve, reject) => {
            const url = `${BASE_URL}/ajax.php?action=artist&id=${artistId}`;

            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.status === 'success') {
                            cache.set(cacheKey, data.response);
                            resolve(data.response);
                        } else {
                            reject(new Error('API returned failure status'));
                        }
                    } catch (e) {
                        reject(e);
                    }
                },
                onerror: function(error) {
                    reject(error);
                }
            });
        });
    }

    // Format bytes to human readable
    function formatBytes(bytes) {
        if (bytes === 0) return '0 B';
        const k = 1024;
        const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }

    // Format date to relative time
    function formatRelativeTime(dateString) {
        const date = new Date(dateString);
        const now = new Date();
        const diffMs = now - date;
        const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
        const diffMonths = Math.floor(diffDays / 30);
        const diffYears = Math.floor(diffDays / 365);

        if (diffYears > 0) return `${diffYears} year${diffYears > 1 ? 's' : ''} ago`;
        if (diffMonths > 0) return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`;
        if (diffDays > 0) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
        return 'Today';
    }

    // Get unique contributors from torrents
    function getContributors(torrents) {
        const contributors = new Set();
        torrents.forEach(torrent => {
            if (torrent.username) {
                contributors.add(torrent.username);
            }
        });
        return Array.from(contributors);
    }

    // Get oldest torrent date
    function getOldestDate(torrents) {
        if (!torrents || torrents.length === 0) return null;
        return torrents.reduce((oldest, torrent) => 
            torrent.time < oldest ? torrent.time : oldest, 
            torrents[0].time
        );
    }

    // Get most recent upload date
    function getMostRecentDate(torrents) {
        if (!torrents || torrents.length === 0) return null;
        return torrents.reduce((newest, torrent) => 
            torrent.time > newest ? torrent.time : newest, 
            torrents[0].time
        );
    }

    // HTML escape helper
    function escapeHtml(text) {
        if (text == null) return '';
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }
    
    // Decode HTML entities (e.g., &iacute; -> í, &#54756; -> 塔) before escaping
    function decodeHtml(html) {
        if (html == null) return '';
        const str = String(html);
        
        // Use a more robust method: create a temporary element and decode
        // This handles both named entities (&amp;) and numeric entities (&#54756; or &#xD547;)
        const txt = document.createElement('textarea');
        txt.innerHTML = str;
        let decoded = txt.value;
        
        // If textarea method didn't fully decode (some browsers have issues with numeric entities),
        // use a more explicit approach for numeric entities
        // Handle decimal numeric entities: &#54756;
        decoded = decoded.replace(/&#(\d+);/g, (match, dec) => {
            return String.fromCharCode(parseInt(dec, 10));
        });
        
        // Handle hexadecimal numeric entities: &#xD547; or &#xD547;
        decoded = decoded.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => {
            return String.fromCharCode(parseInt(hex, 16));
        });
        
        return decoded;
    }
    
    // Safely decode and escape HTML - handles special characters properly
    function safeHtml(text) {
        if (text == null) return '';
        // First decode any HTML entities, then escape for safe display
        return escapeHtml(decodeHtml(String(text)));
    }

    // Format artist info - returns HTML with links
    function formatArtists(musicInfo) {
        if (!musicInfo) return 'Unknown Artist';

        const mainParts = [];
        const otherParts = [];

        // Main artists
        if (musicInfo.artists && musicInfo.artists.length > 0) {
            mainParts.push(...musicInfo.artists.map(a => ({
                html: `<a href="${BASE_URL}/artist.php?id=${a.id}">${safeHtml(a.name)}</a>`,
                type: 'main'
            })));
        }

        // Group all "with" artists together - they'll go on a new line
        const withArtists = [];
        if (musicInfo.with && musicInfo.with.length > 0) {
            withArtists.push(...musicInfo.with);
        }

        // Add other artist types to main list
        if (musicInfo.composers && musicInfo.composers.length > 0) {
            otherParts.push(...musicInfo.composers.map(a => ({
                html: `<a href="${BASE_URL}/artist.php?id=${a.id}">${safeHtml(a.name)}</a>`,
                type: 'composed'
            })));
        }
        if (musicInfo.conductor && musicInfo.conductor.length > 0) {
            otherParts.push(...musicInfo.conductor.map(a => ({
                html: `<a href="${BASE_URL}/artist.php?id=${a.id}">${safeHtml(a.name)}</a>`,
                type: 'conducted'
            })));
        }
        if (musicInfo.dj && musicInfo.dj.length > 0) {
            otherParts.push(...musicInfo.dj.map(a => ({
                html: `<a href="${BASE_URL}/artist.php?id=${a.id}">${safeHtml(a.name)}</a>`,
                type: 'dj'
            })));
        }

        if (mainParts.length === 0 && otherParts.length === 0 && withArtists.length === 0) {
            return 'Unknown Artist';
        }

        // Combine main and other parts
        const allMainParts = [...mainParts, ...otherParts];

        // Build main artist line
        const mainArtistLines = [];
        allMainParts.forEach(p => {
            if (p.type === 'main') {
                mainArtistLines.push(p.html);
            } else if (p.type === 'composed') {
                mainArtistLines.push(`composed by ${p.html}`);
            } else if (p.type === 'conducted') {
                mainArtistLines.push(`conducted by ${p.html}`);
            } else if (p.type === 'dj') {
                mainArtistLines.push(`DJ ${p.html}`);
            }
        });

        // Handle "show more" for main artists if needed
        let mainLine;
        if (mainArtistLines.length <= 6) {
            mainLine = mainArtistLines.join(', ');
        } else {
            const visible = mainArtistLines.slice(0, 6).join(', ');
            const hidden = mainArtistLines.slice(6).join(', ');
            const hiddenCount = mainArtistLines.length - 6;
            mainLine = `<span class="artists-visible">${visible}</span><span class="artists-more-toggle" style="cursor: pointer; opacity: 0.7; font-weight: normal;"> [show ${hiddenCount} more]</span><span class="artists-hidden" style="display: none;">, ${hidden}</span>`;
        }

        // Add "with" artists on a new line if present
        if (withArtists.length > 0) {
            const withLinks = withArtists.map(a =>
                `<a href="${BASE_URL}/artist.php?id=${a.id}">${safeHtml(a.name)}</a>`
            ).join(', ');
            return `${mainLine}<br><span style="display: inline-block; padding-top: 2px;"><i>with ${withLinks}</i></span>`;
        }

        return mainLine;
    }

    // Format artists with expansion support
    function formatArtistsWithExpansion(artists, maxVisible = 6) {
        if (!artists || artists.length === 0) return '';
        
        const artistLinks = artists.map(a =>
            `<a href="${BASE_URL}/artist.php?id=${a.id}">${safeHtml(a.name)}</a>`
        );
        
        if (artistLinks.length <= maxVisible) {
            return artistLinks.join(', ');
        } else {
            const visible = artistLinks.slice(0, maxVisible).join(', ');
            const hidden = artistLinks.slice(maxVisible).join(', ');
            const hiddenCount = artistLinks.length - maxVisible;
            return `<span class="artists-visible">${visible}</span><span class="artists-more-toggle" style="cursor: pointer; opacity: 0.7; font-weight: normal;"> [show ${hiddenCount} more]</span><span class="artists-hidden" style="display: none;">, ${hidden}</span>`;
        }
    }

    // Format artists with "Various Artists" logic (2 or fewer show each, 3+ show "Various Artists" with expand)
    function formatArtistsWithVarious(artists, uniqueId) {
        if (!artists || artists.length === 0) return { html: '', hasExpand: false, allArtists: '' };
        
        const artistLinks = artists.map(a =>
            `<a href="${BASE_URL}/artist.php?id=${a.id}">${safeHtml(a.name)}</a>`
        );
        
        if (artists.length <= 2) {
            return { html: artistLinks.join(', '), hasExpand: false, allArtists: '' };
        } else {
            const allArtists = artistLinks.join(', ');
            return {
                html: `<span id="various-artists-toggle-${uniqueId}" style="cursor: pointer; opacity: 0.9; font-weight: 500; display: inline-flex; align-items: center; gap: 4px; white-space: nowrap;"><span>Various Artists</span><span id="various-artists-indicator-${uniqueId}" style="opacity: 0.6; font-size: 0.85em;">▼</span></span>`,
                hasExpand: true,
                uniqueId: uniqueId,
                allArtists: allArtists
            };
        }
    }

    // Format number compactly (e.g., 12769 -> 12.7k, 5442 -> 5.4k)
    function formatCompactNumber(num) {
        if (num >= 1000000) {
            return (num / 1000000).toFixed(1) + 'M';
        } else if (num >= 1000) {
            return (num / 1000).toFixed(1) + 'k';
        }
        return num.toString();
    }

    // Format relative time compactly (e.g., "9 years ago" -> "9y", "5 days ago" -> "5d")
    function formatCompactTime(timeStr) {
        if (!timeStr || timeStr === 'Unknown') return timeStr;
        // Handle both singular and plural forms
        const match = timeStr.match(/(\d+)\s*(year|month|day|hour|minute|second|week)s?\s*ago/i);
        if (match) {
            const num = match[1];
            const unit = match[2].toLowerCase();
            const unitMap = {
                'year': 'y',
                'month': 'mo',
                'week': 'w',
                'day': 'd',
                'hour': 'h',
                'minute': 'm',
                'second': 's'
            };
            return num + (unitMap[unit] || unit[0]);
        }
        // Fallback: return as-is if pattern doesn't match
        return timeStr;
    }

    // Create grayscale SVG icons
    function createIcon(iconType) {
        const iconSize = '14';
        const strokeColor = '#B0B0B0';
        const strokeWidth = '1.5';
        const fillColor = 'none';
        
        const icons = {
            calendar: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <rect x="3" y="4" width="10" height="9" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}" rx="1"/>
                <line x1="5" y1="2" x2="5" y2="4" stroke="${strokeColor}" stroke-width="${strokeWidth}"/>
                <line x1="11" y1="2" x2="11" y2="4" stroke="${strokeColor}" stroke-width="${strokeWidth}"/>
                <line x1="3" y1="7" x2="13" y2="7" stroke="${strokeColor}" stroke-width="${strokeWidth}"/>
                <circle cx="6" cy="9.5" r="0.8" fill="${strokeColor}"/>
                <circle cx="10" cy="9.5" r="0.8" fill="${strokeColor}"/>
            </svg>`,
            
            clock: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <circle cx="8" cy="8" r="6" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}"/>
                <line x1="8" y1="8" x2="8" y2="5" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-linecap="round"/>
                <line x1="8" y1="8" x2="11" y2="8" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-linecap="round"/>
            </svg>`,
            
            user: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <circle cx="8" cy="5" r="2.5" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}"/>
                <path d="M3 14 Q3 10 8 10 Q13 10 13 14" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}"/>
            </svg>`,
            
            users: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <circle cx="6" cy="5" r="2" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}"/>
                <path d="M2 14 Q2 11 6 11 Q10 11 10 14" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}"/>
                <circle cx="11" cy="5.5" r="1.8" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}"/>
                <path d="M8 14 Q8 11.5 11 11.5 Q14 11.5 14 14" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}"/>
            </svg>`,
            
            cycle: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <path d="M8 3 L8 10 M8 10 L5 7 M8 10 L11 7" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}" stroke-linecap="round" stroke-linejoin="round"/>
                <line x1="3" y1="13" x2="13" y2="13" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-linecap="round"/>
            </svg>`,
            
            arrowUp: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <path d="M8 3 L8 13 M8 3 L4 7 M8 3 L12 7" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>`,
            
            arrowDown: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <path d="M8 13 L8 3 M8 13 L4 9 M8 13 L12 9" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>`,
            
            disk: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <rect x="3" y="5" width="10" height="8" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}" rx="1"/>
                <line x1="3" y1="7" x2="13" y2="7" stroke="${strokeColor}" stroke-width="${strokeWidth}"/>
                <circle cx="8" cy="9.5" r="1" fill="${strokeColor}"/>
            </svg>`,
            
            folder: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <path d="M3 4 L6 4 L7 5 L13 5 L13 12 L3 12 Z" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}"/>
            </svg>`,
            
            microphone: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <path d="M8 2 L8 9" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-linecap="round"/>
                <path d="M5 9 Q5 7 8 7 Q11 7 11 9" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}"/>
                <line x1="6" y1="11" x2="10" y2="11" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-linecap="round"/>
                <line x1="7" y1="13" x2="9" y2="13" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-linecap="round"/>
                <path d="M4 9 L4 11 M12 9 L12 11" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-linecap="round"/>
            </svg>`,
            
            musicNote: `<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; opacity: 0.7;">
                <ellipse cx="7" cy="10" rx="2" ry="2.5" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}"/>
                <line x1="7" y1="2" x2="7" y2="7.5" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-linecap="round"/>
                <path d="M7 2 Q9 2 10 4 Q11 6 11 8" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="${fillColor}" stroke-linecap="round"/>
            </svg>`
        };
        
        return icons[iconType] || '';
    }

    // Show preview popup
    async function showPreview(groupId, torrentId, event) {
        // Close existing popup if any
        closePopup();

        // Create popup container
        const popup = document.createElement('div');
        popup.className = 'box';
        popup.style.width = '380px';
        popup.style.maxHeight = '600px';
        setupPopupStyles(popup);
        ensureScrollbarStyles();

        // Position near cursor but ensure it's visible
        // For absolute positioning, we need to account for scroll position
        const x = Math.min(event.pageX + 10, window.innerWidth + window.pageXOffset - 400);
        const y = event.pageY + 10;
        popup.style.left = `${x}px`;
        popup.style.top = `${y}px`;

        // Add loading message with animated ellipses
        popup.innerHTML = `
            <div style="padding: 15px;">
                <div style="text-align: center; font-size: 0.95em; opacity: 0.9;">
                    Loading<span class="loading-ellipses"></span>
                </div>
            </div>
        `;

        document.body.appendChild(popup);
        currentPopup = popup;

        try {
            const data = await fetchData(groupId, torrentId);

            // Check if popup was closed while loading
            if (currentPopup !== popup) return;

            const group = data.group;
            const torrents = torrentId ? [data.torrent] : data.torrents;

            // Get data for display
            const artists = formatArtists(group.musicInfo);
            const title = group.name;
            const year = group.year || 'Unknown';
            const image = group.wikiImage || '';
            const tags = group.tags || [];
            const contributors = getContributors(torrents);
            const oldestDate = torrentId ? data.torrent.time : getOldestDate(torrents);
            const newestDate = !torrentId ? getMostRecentDate(torrents) : null;
            const age = oldestDate ? formatRelativeTime(oldestDate) : 'Unknown';
            const fullOldestDate = oldestDate || 'Unknown';

            // Build torrent link
            const torrentLink = torrentId
                ? `${BASE_URL}/torrents.php?torrentid=${torrentId}`
                : `${BASE_URL}/torrents.php?id=${groupId}`;

            // Build popup content
            let content = `
                <div style="position: relative;">
                    <button id="closePreview" style="position: absolute; top: 8px; right: 8px; background: none; border: none; font-size: 22px; cursor: pointer; opacity: 0.6; padding: 0; line-height: 1; transition: opacity 0.2s, background 0.2s; z-index: 10; box-shadow: none; text-shadow: none;">×</button>
            `;

            // Get release type name
            const releaseTypeMap = {
                1: 'Album', 3: 'Soundtrack', 5: 'EP', 6: 'Anthology', 7: 'Compilation',
                9: 'Single', 11: 'Live album', 13: 'Remix', 14: 'Bootleg', 15: 'Interview',
                16: 'Mixtape', 17: 'Demo', 18: 'Concert Recording', 19: 'DJ Mix', 21: 'Unknown'
            };
            const releaseType = group.releaseType ? releaseTypeMap[group.releaseType] || 'Unknown' : null;

            // Build group link for cover image
            const groupLink = `${BASE_URL}/torrents.php?id=${groupId}`;

            // Add cover art if available - positioned relative to .box, not inner div
            if (image) {
                content += `
                    <div id="blurredBg" style="position: absolute; top: 0; left: 0; right: 0; height: 200px; background-image: url('${image}'); background-size: cover; background-position: center; filter: blur(20px) brightness(0.35); opacity: 0.8; z-index: 0; transform: scale(1.15); pointer-events: none;"></div>
                    <div id="gradientOverlay" style="position: absolute; top: 0; left: 0; right: 0; height: 100%; background: linear-gradient(to bottom, transparent 0%, transparent 120px, rgba(26, 26, 26, 0.7) 170px, #1a1a1a 210px); z-index: 0; pointer-events: none;"></div>
                `;
            }

            content += `
                    <div style="padding: 18px; position: relative; z-index: 1;">
            `;

            // Add cover art image inside padded div - make it clickable
            if (image) {
                content += `
                    <div style="text-align: center; margin-bottom: 8px; padding: 20px;">
                        <a href="${groupLink}" style="display: inline-block; cursor: pointer;">
                            <img src="${image}" alt="Cover art" style="max-width: 100%; max-height: 200px; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); display: block; margin: 0 auto; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'" />
                        </a>
                    </div>
                `;
            }

            // Artist and title with year
            if (torrentId && data.torrent) {
                const torrent = data.torrent;
                
                // Get main artists and "with" artists separately
                const musicInfo = group.musicInfo || {};
                const mainArtists = musicInfo.artists || [];
                const withArtists = musicInfo.with || [];
                
                // Format main artists with "Various Artists" logic
                const artistUniqueId = `single-${groupId || torrentId || Date.now()}`;
                const mainArtistsFormatted = formatArtistsWithVarious(mainArtists, artistUniqueId);
                const showGuestArtists = !mainArtistsFormatted.hasExpand && withArtists.length > 0;
                
                // Format "with" artists with expansion support (only if not using "Various Artists")
                const withArtistLinks = showGuestArtists ? formatArtistsWithExpansion(withArtists) : '';
                
                // Get edition year
                const editionYear = torrent.remasterYear || year;
                
                // Build edition info (without year since it's in title)
                const editionParts = [];
                if (torrent.remasterRecordLabel) editionParts.push(torrent.remasterRecordLabel);
                if (torrent.remasterCatalogueNumber) editionParts.push(torrent.remasterCatalogueNumber);
                if (torrent.remasterTitle) editionParts.push(torrent.remasterTitle);
                
                // For single torrents: new layout
                content += `
                    <div style="margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
                        <!-- Title with edition year -->
                        <div style="font-weight: bold; font-size: 1.15em; margin-bottom: 6px; line-height: 1.3; text-align: center;">
                            <a href="${torrentLink}" style="text-decoration: none;">${safeHtml(title)}</a>
                            ${editionYear && editionYear !== 'Unknown' ? `<span style="opacity: 0.7; font-weight: normal; font-size: 0.95em;"> (${editionYear})</span>` : ''}
                        </div>
                        
                        <!-- Artists -->
                        <div style="opacity: 0.85; line-height: 1.6; margin-bottom: 4px; text-align: center;">
                            <div style="display: flex; align-items: center; justify-content: center; gap: 4px; flex-wrap: nowrap;">
                                ${createIcon('musicNote')}
                                <span style="white-space: nowrap;">${mainArtistsFormatted.html}</span>
                            </div>
                            ${mainArtistsFormatted.hasExpand ? `<div id="various-artists-list-${artistUniqueId}" style="display: none; margin-top: 4px; opacity: 0.85; text-align: center; word-wrap: break-word; overflow-wrap: break-word;">${mainArtistsFormatted.allArtists}</div>` : ''}
                        </div>
                `;
                
                // "feat. Guests" line if present and not using "Various Artists"
                if (showGuestArtists && withArtistLinks) {
                    content += `
                        <div style="opacity: 0.7; font-style: italic; margin-bottom: 4px; text-align: center;">
                            feat. ${withArtistLinks}
                        </div>
                    `;
                }
                
                // If using "Various Artists", include guest artists in expanded section
                if (mainArtistsFormatted.hasExpand && withArtists.length > 0) {
                    const withArtistLinksExpanded = formatArtistsWithExpansion(withArtists);
                    content += `
                        <div id="various-artists-feat-${artistUniqueId}" style="display: none; opacity: 0.7; font-style: italic; margin-top: 4px; text-align: center;">
                            feat. ${withArtistLinksExpanded}
                        </div>
                    `;
                }
                
                // Edition info (without year)
                if (editionParts.length > 0) {
                    content += `
                        <div style="opacity: 0.75; font-size: 0.9em; margin-top: 8px; margin-bottom: 8px; text-align: center;">
                            ${safeHtml(editionParts.join(' / '))}
                        </div>
                    `;
                }
                
                // Release type and format info as pill badges
                const formatParts = [torrent.media, torrent.format, torrent.encoding].filter(Boolean);
                const formatText = formatParts.map(part => safeHtml(part)).join(' ');
                const pills = [];
                
                if (releaseType) {
                    pills.push(`<span style="display: inline-block; padding: 4px 10px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; font-size: 0.85em; opacity: 0.9;">${escapeHtml(releaseType)}</span>`);
                }
                if (formatText) {
                    pills.push(`<span style="display: inline-block; padding: 4px 10px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; font-size: 0.85em; opacity: 0.9; text-transform: uppercase; letter-spacing: 0.5px;">${formatText}</span>`);
                }
                
                if (pills.length > 0) {
                    content += `
                        <div style="display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; align-items: center; margin-top: ${editionParts.length > 0 ? '8px' : '6px'};">
                            ${pills.join('')}
                        </div>
                    `;
                }
                
                content += `
                    </div>
                `;
            } else {
                // For groups: similar layout to single torrent
                // Get main artists and "with" artists separately
                const musicInfo = group.musicInfo || {};
                const mainArtists = musicInfo.artists || [];
                const withArtists = musicInfo.with || [];
                
                // Format main artists with "Various Artists" logic
                const artistUniqueId = `group-${groupId || Date.now()}`;
                const mainArtistsFormatted = formatArtistsWithVarious(mainArtists, artistUniqueId);
                const showGuestArtists = !mainArtistsFormatted.hasExpand && withArtists.length > 0;
                
                // Format "with" artists with expansion support (only if not using "Various Artists")
                const withArtistLinks = showGuestArtists ? formatArtistsWithExpansion(withArtists) : '';
                
                content += `
                    <div style="margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
                        <!-- Title with group year -->
                        <div style="font-weight: bold; font-size: 1.15em; margin-bottom: 6px; line-height: 1.3; text-align: center;">
                            <a href="${torrentLink}" style="text-decoration: none;">${safeHtml(title)}</a>
                            ${year !== 'Unknown' ? `<span style="opacity: 0.7; font-weight: normal; font-size: 0.95em;"> (${year})</span>` : ''}
                        </div>
                        
                        <!-- Artists -->
                        <div style="opacity: 0.85; line-height: 1.6; margin-bottom: 4px; text-align: center;">
                            <div style="display: flex; align-items: center; justify-content: center; gap: 4px; flex-wrap: nowrap;">
                                ${createIcon('musicNote')}
                                <span style="white-space: nowrap;">${mainArtistsFormatted.html}</span>
                            </div>
                            ${mainArtistsFormatted.hasExpand ? `<div id="various-artists-list-${artistUniqueId}" style="display: none; margin-top: 4px; opacity: 0.85; text-align: center; word-wrap: break-word; overflow-wrap: break-word;">${mainArtistsFormatted.allArtists}</div>` : ''}
                        </div>
                `;
                
                // "feat. Guests" line if present and not using "Various Artists"
                if (showGuestArtists && withArtistLinks) {
                    content += `
                        <div style="opacity: 0.7; font-style: italic; margin-bottom: 4px; text-align: center;">
                            feat. ${withArtistLinks}
                        </div>
                    `;
                }
                
                // If using "Various Artists", include guest artists in expanded section
                if (mainArtistsFormatted.hasExpand && withArtists.length > 0) {
                    const withArtistLinksExpanded = formatArtistsWithExpansion(withArtists);
                    content += `
                        <div id="various-artists-feat-${artistUniqueId}" style="display: none; opacity: 0.7; font-style: italic; margin-top: 4px; text-align: center;">
                            feat. ${withArtistLinksExpanded}
                        </div>
                    `;
                }
                
                // Release type as pill badge (no format info for groups)
                if (releaseType) {
                    content += `
                        <div style="display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; align-items: center; margin-top: 8px;">
                            <span style="display: inline-block; padding: 4px 10px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; font-size: 0.85em; opacity: 0.9;">${safeHtml(releaseType)}</span>
                        </div>
                    `;
                }
                
                content += `
                    </div>
                `;
            }

            // Tags using site's native styling
            if (tags.length > 0) {
                const tagNames = Array.isArray(tags) ? tags : Object.values(tags);
                const tagLinks = tagNames.map(tag =>
                    `<a href="${BASE_URL}/torrents.php?action=advanced&amp;taglist=${encodeURIComponent(tag)}">${safeHtml(tag)}</a>`
                ).join(', ');
                content += `
                    <div style="margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); margin-top: -4px; text-align: center;">
                        <div class="tags">${tagLinks}</div>
                    </div>
                `;
            }
            if (!torrentId) {
                // Calculate stats
                const recentUploadTime = newestDate ? formatRelativeTime(newestDate) : null;
                
                // Get unique uploaders
                const uniqueUploaders = [];
                const uploaderMap = new Map();
                torrents.forEach(t => {
                    if (t.username && t.userId && !uploaderMap.has(t.userId)) {
                        uploaderMap.set(t.userId, t.username);
                        uniqueUploaders.push({ id: t.userId, name: t.username });
                    }
                });
                
                // Group torrents by edition
                const editionGroups = new Map();
                torrents.forEach(torrent => {
                    const editionKey = JSON.stringify({
                        remastered: torrent.remastered || false,
                        remasterYear: torrent.remasterYear || 0,
                        remasterTitle: torrent.remasterTitle || '',
                        remasterRecordLabel: torrent.remasterRecordLabel || '',
                        remasterCatalogueNumber: torrent.remasterCatalogueNumber || ''
                    });
                    
                    if (!editionGroups.has(editionKey)) {
                        editionGroups.set(editionKey, []);
                    }
                    editionGroups.get(editionKey).push(torrent);
                });
                const editionGroupsArray = Array.from(editionGroups.entries());
                const editionCount = editionGroupsArray.length;
                
                // Calculate torrent stats
                const totalSnatches = torrents.reduce((sum, t) => sum + (t.snatched || 0), 0);
                const totalSeeders = torrents.reduce((sum, t) => sum + (t.seeders || 0), 0);
                const totalLeechers = torrents.reduce((sum, t) => sum + (t.leechers || 0), 0);
                const totalSize = torrents.reduce((sum, t) => sum + (t.size || 0), 0);
                const avgSnatches = torrents.length > 0 ? Math.round(totalSnatches / torrents.length) : 0;
                
                // Find most snatched and most seeded torrents
                const mostSnatched = torrents.reduce((max, t) => 
                    (t.snatched || 0) > (max.snatched || 0) ? t : max, torrents[0]);
                const mostSeeded = torrents.reduce((max, t) => 
                    (t.seeders || 0) > (max.seeders || 0) ? t : max, torrents[0]);
                
                // Most Snatched and Most Seeded (only if multiple editions) - moved to top
                if (editionCount > 1) {
                    // Helper function to get edition info for a torrent
                    const getEditionInfo = (torrent) => {
                        const editionParts = [];
                        if (torrent.remasterYear && torrent.remasterYear > 0) {
                            editionParts.push(torrent.remasterYear);
                        }
                        if (torrent.remasterRecordLabel) {
                            editionParts.push(torrent.remasterRecordLabel);
                        }
                        if (torrent.remasterCatalogueNumber) {
                            editionParts.push(torrent.remasterCatalogueNumber);
                        }
                        if (torrent.remasterTitle) {
                            editionParts.push(torrent.remasterTitle);
                        }
                        return editionParts.length > 0 ? editionParts.join(' / ') : 'Original Release';
                    };
                    
                    const mostSnatchedFormatParts = [mostSnatched.media, mostSnatched.format, mostSnatched.encoding].filter(Boolean);
                    const mostSeededFormatParts = [mostSeeded.media, mostSeeded.format, mostSeeded.encoding].filter(Boolean);
                    const mostSnatchedEdition = getEditionInfo(mostSnatched);
                    const mostSeededEdition = getEditionInfo(mostSeeded);
                    
                    const mostSnatchedFormatText = mostSnatchedFormatParts.map(part => safeHtml(part)).join(' ');
                    const mostSeededFormatText = mostSeededFormatParts.map(part => safeHtml(part)).join(' ');
                    
                    content += `
                        <div style="margin-bottom: 12px; font-size: 0.85em; opacity: 0.8;">
                            <div style="display: flex; gap: 16px; justify-content: center; flex-wrap: nowrap; align-items: flex-start;">
                                <div style="text-align: center; flex: 1; min-width: 0;">
                                    <div style="opacity: 0.7; margin-bottom: 4px; font-weight: 500;">Most Snatched</div>
                                    <div style="opacity: 0.65; font-size: 0.9em; margin-bottom: 6px; padding-bottom: 4px; border-bottom: 1px solid rgba(255, 255, 255, 0.08);">${safeHtml(mostSnatchedEdition)}</div>
                                    <a href="${BASE_URL}/torrents.php?torrentid=${mostSnatched.id}" style="text-decoration: none; opacity: 0.9; font-size: 0.95em; text-transform: uppercase; letter-spacing: 1px; display: block; margin-bottom: 4px;">
                                        ${mostSnatchedFormatText}
                                    </a>
                                    <div style="opacity: 0.6; font-size: 0.9em; margin-top: 2px;">${(mostSnatched.snatched || 0).toLocaleString()} snatches</div>
                                </div>
                                <div style="text-align: center; flex: 1; min-width: 0;">
                                    <div style="opacity: 0.7; margin-bottom: 4px; font-weight: 500;">Most Seeded</div>
                                    <div style="opacity: 0.65; font-size: 0.9em; margin-bottom: 6px; padding-bottom: 4px; border-bottom: 1px solid rgba(255, 255, 255, 0.08);">${safeHtml(mostSeededEdition)}</div>
                                    <a href="${BASE_URL}/torrents.php?torrentid=${mostSeeded.id}" style="text-decoration: none; opacity: 0.9; font-size: 0.95em; text-transform: uppercase; letter-spacing: 1px; display: block; margin-bottom: 4px;">
                                        ${mostSeededFormatText}
                                    </a>
                                    <div style="opacity: 0.6; font-size: 0.9em; margin-top: 2px;">${(mostSeeded.seeders || 0).toLocaleString()} seeders</div>
                                </div>
                            </div>
                        </div>
                        <div style="margin-top: 12px; margin-bottom: 0px; padding-bottom: 0px; border-bottom: 1px solid rgba(255, 255, 255, 0.1);"></div>
                    `;
                }
                
                // Format dates for tooltips
                const formatDateForTooltip = (dateString) => {
                    if (!dateString || dateString === 'Unknown') return 'Unknown';
                    try {
                        const date = new Date(dateString);
                        return date.toLocaleString('en-US', { 
                            year: 'numeric', 
                            month: 'long', 
                            day: 'numeric',
                            hour: '2-digit',
                            minute: '2-digit'
                        });
                    } catch (e) {
                        return dateString;
                    }
                };
                
                const fullOldestDateFormatted = formatDateForTooltip(fullOldestDate);
                const newestDateFormatted = newestDate ? formatDateForTooltip(newestDate) : null;
                const compactAge = formatCompactTime(age);
                const compactRecentTime = newestDate ? formatCompactTime(recentUploadTime) : null;
                
                // Icon-based info bar - compact, wraps naturally (placed after Most Snatched/Most Seeded)
                content += `
                    <div style="margin-top: ${editionCount > 1 ? '12px' : '12px'}; margin-bottom: 12px; font-size: 0.85em; opacity: 0.85;">
                        <div style="display: flex; flex-wrap: wrap; gap: 8px 10px; justify-content: center; align-items: center;">
                `;
                
                // Created icon
                content += `
                    <div title="${escapeHtml('Created: ' + fullOldestDateFormatted)}" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                        ${createIcon('calendar')}
                        <span>${compactAge}</span>
                    </div>
                `;
                
                // Latest icon (pencil for last updated)
                if (newestDate) {
                    content += `
                        <div title="${escapeHtml('Last updated: ' + newestDateFormatted)}" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                            ${createIcon('clock')}
                            <span>${compactRecentTime}</span>
                        </div>
                    `;
                }
                
                // Uploader icon
                if (uniqueUploaders.length > 0) {
                    if (uniqueUploaders.length === 1) {
                        const uploader = uniqueUploaders[0];
                        content += `
                            <div title="Uploader" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                                ${createIcon('user')}
                                <a href="${BASE_URL}/user.php?id=${uploader.id}" style="text-decoration: none; opacity: 0.85; max-width: 80px; overflow: hidden; text-overflow: ellipsis;">${safeHtml(uploader.name)}</a>
                            </div>
                        `;
                    } else {
                        content += `
                            <div title="Uploaders" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                                ${createIcon('users')}
                                <span id="expandUploaders" style="cursor: pointer; opacity: 0.8; text-decoration: underline;">${uniqueUploaders.length}</span>
                            </div>
                        `;
                    }
                }
                
                // Snatches icon (cycle icon)
                if (totalSnatches > 0) {
                    content += `
                        <div title="${escapeHtml('Total snatches: ' + totalSnatches.toLocaleString())}" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                            ${createIcon('cycle')}
                            <span>${formatCompactNumber(totalSnatches)}</span>
                        </div>
                    `;
                }
                
                // Seeders icon (up arrow)
                if (totalSeeders > 0) {
                    content += `
                        <div title="${escapeHtml('Total seeders: ' + totalSeeders.toLocaleString())}" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                            ${createIcon('arrowUp')}
                            <span>${formatCompactNumber(totalSeeders)}</span>
                        </div>
                    `;
                }
                
                // Leechers icon (down arrow)
                if (totalLeechers > 0) {
                    content += `
                        <div title="${escapeHtml('Total leechers: ' + totalLeechers.toLocaleString())}" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                            ${createIcon('arrowDown')}
                            <span>${formatCompactNumber(totalLeechers)}</span>
                        </div>
                    `;
                }
                
                // Size icon
                if (totalSize > 0) {
                    const sizeStr = formatBytes(totalSize);
                    content += `
                        <div title="${escapeHtml('Total size: ' + sizeStr)}" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                            ${createIcon('disk')}
                            <span>${sizeStr}</span>
                        </div>
                    `;
                }
                
                content += `
                        </div>
                    </div>
                `;
                
                // Expandable uploader list (full width, underneath icons)
                if (uniqueUploaders.length > 1) {
                    const uploaderLinks = uniqueUploaders.map(u => 
                        `<a href="${BASE_URL}/user.php?id=${u.id}" style="text-decoration: none; opacity: 0.85;">${safeHtml(u.name)}</a>`
                    ).join(', ');
                    
                    content += `
                        <div id="uploaderList" style="display: none; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.1); font-size: 0.9em; opacity: 0.85;">
                            <div style="opacity: 0.7; margin-bottom: 6px;">Uploaders:</div>
                            <div>${uploaderLinks}</div>
                        </div>
                    `;
                }
                
                // HR under icon section
                content += `
                    <div style="margin-top: 0px; margin-bottom: 12px; padding-bottom: 0px; border-bottom: 1px solid rgba(255, 255, 255, 0.1);"></div>
                `;
                
                // Build edition/torrent count line with arrow indicator (moved to bottom)
                content += `
                    <div style="margin-bottom: 10px; font-size: 0.95em; text-align: center;">
                        <span class="expandTorrentList" style="cursor: pointer; opacity: 0.9; display: inline-flex; align-items: center; gap: 4px;">
                            <strong>${editionCount}</strong> Edition${editionCount !== 1 ? 's' : ''} with <strong>${torrents.length}</strong> Torrent${torrents.length !== 1 ? 's' : ''}
                            <span id="torrentListIndicator" style="opacity: 0.6; font-size: 0.85em;">▼</span>
                        </span>
                    </div>
                `;

                // Expandable torrent list (placed at bottom since it gets long)
                content += `
                    <div id="torrentList" style="display: none; margin-bottom: 10px; font-size: 0.9em;">
                `;

                // Build torrent list grouped by edition with card-based styling
                editionGroupsArray.forEach(([editionKey, groupTorrents], groupIndex) => {
                    const firstTorrent = groupTorrents[0];
                    const editionParts = [];
                    
                    // Build edition label
                    if (firstTorrent.remasterYear && firstTorrent.remasterYear > 0) {
                        editionParts.push(firstTorrent.remasterYear);
                    }
                    if (firstTorrent.remasterRecordLabel) {
                        editionParts.push(firstTorrent.remasterRecordLabel);
                    }
                    if (firstTorrent.remasterCatalogueNumber) {
                        editionParts.push(firstTorrent.remasterCatalogueNumber);
                    }
                    if (firstTorrent.remasterTitle) {
                        editionParts.push(firstTorrent.remasterTitle);
                    }
                    
                    const editionLabel = editionParts.length > 0 
                        ? editionParts.join(' / ')
                        : 'Original Release';
                    
                    // Show edition header if there are multiple editions or if edition info exists
                    const showEditionHeader = editionGroupsArray.length > 1 || editionLabel !== 'Original Release';
                    const isFirstEdition = groupIndex === 0;
                    
                    // Single container for each edition with all its torrents
                    content += `
                        <div style="margin-top: ${isFirstEdition ? '0' : '8px'}; padding: 8px; background: rgba(255, 255, 255, 0.03); border-radius: 4px; border-left: 3px solid rgba(255, 255, 255, 0.15); transition: background 0.2s;">
                    `;
                    
                    if (showEditionHeader) {
                        content += `
                            <div style="margin-bottom: 6px; opacity: 0.9; font-size: 0.95em; font-weight: 600;">
                                ${safeHtml(editionLabel)}
                            </div>
                        `;
                    }
                    
                    // List torrents in this edition within the same container
                    groupTorrents.forEach((torrent, torrentIndex) => {
                        const torrentUrl = `${BASE_URL}/torrents.php?torrentid=${torrent.id}`;
                        const formatParts = [torrent.media, torrent.format, torrent.encoding].filter(Boolean);
                        const formatText = formatParts.map(part => safeHtml(part)).join(' ');
                        const isLastTorrent = torrentIndex === groupTorrents.length - 1;
                        
                        content += `
                            <div style="margin-bottom: ${isLastTorrent ? '0' : '4px'}; padding: 4px 0;">
                                <div style="display: flex; align-items: center; gap: 8px;">
                                    <div style="flex: 1; min-width: 0;">
                                        <a href="${torrentUrl}" style="text-decoration: none; opacity: 0.9; font-size: 0.9em; display: inline-block;">${formatText || 'View Torrent'}</a>
                                    </div>
                                    <div style="flex-shrink: 0; text-align: right; opacity: 0.7; font-size: 0.85em; white-space: nowrap;">
                                        ${formatBytes(torrent.size)}
                                    </div>
                                </div>
                            </div>
                        `;
                    });
                    
                    content += `
                        </div>
                    `;
                });

                content += `
                    </div>
                `;
            } else {
                // Show torrent-specific info with icon section
                const torrent = data.torrent;
                
                // Format dates for tooltips
                const formatDateForTooltip = (dateString) => {
                    if (!dateString || dateString === 'Unknown') return 'Unknown';
                    try {
                        const date = new Date(dateString);
                        return date.toLocaleString('en-US', { 
                            year: 'numeric', 
                            month: 'long', 
                            day: 'numeric',
                            hour: '2-digit',
                            minute: '2-digit'
                        });
                    } catch (e) {
                        return dateString;
                    }
                };
                
                const fullOldestDateFormatted = formatDateForTooltip(fullOldestDate);
                const uploaderName = torrent.username || 'Unknown';
                const uploaderLink = torrent.userId
                    ? `<a href="${BASE_URL}/user.php?id=${torrent.userId}" style="text-decoration: none; opacity: 0.85; max-width: 80px; overflow: hidden; text-overflow: ellipsis; display: inline-block;">${safeHtml(uploaderName)}</a>`
                    : safeHtml(uploaderName);
                const compactAge = formatCompactTime(age);
                
                // Icon-based info bar - compact, wraps naturally
                content += `
                    <div style="margin-top: 14px; margin-bottom: 10px; font-size: 0.85em; opacity: 0.85;">
                        <div style="display: flex; flex-wrap: wrap; gap: 8px 10px; justify-content: center; align-items: center;">
                `;
                
                // Uploader icon
                content += `
                    <div title="Uploader" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                        ${createIcon('user')}
                        ${uploaderLink}
                    </div>
                `;
                
                // Snatches icon (cycle icon)
                if (torrent.snatched !== undefined && torrent.snatched !== null) {
                    content += `
                        <div title="${escapeHtml('Snatches: ' + (torrent.snatched || 0).toLocaleString())}" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                            ${createIcon('cycle')}
                            <span>${formatCompactNumber(torrent.snatched || 0)}</span>
                        </div>
                    `;
                }
                
                // Seeders icon (up arrow)
                if (torrent.seeders !== undefined && torrent.seeders !== null) {
                    content += `
                        <div title="${escapeHtml('Seeders: ' + (torrent.seeders || 0).toLocaleString())}" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                            ${createIcon('arrowUp')}
                            <span>${formatCompactNumber(torrent.seeders || 0)}</span>
                        </div>
                    `;
                }
                
                // Leechers icon (down arrow)
                if (torrent.leechers !== undefined && torrent.leechers !== null) {
                    content += `
                        <div title="${escapeHtml('Leechers: ' + (torrent.leechers || 0).toLocaleString())}" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                            ${createIcon('arrowDown')}
                            <span>${formatCompactNumber(torrent.leechers || 0)}</span>
                        </div>
                    `;
                }
                
                // Size icon
                if (torrent.size) {
                    const sizeStr = formatBytes(torrent.size);
                    content += `
                        <div title="${escapeHtml('Size: ' + sizeStr)}" style="display: flex; align-items: center; gap: 3px; white-space: nowrap;">
                            ${createIcon('disk')}
                            <span>${sizeStr}</span>
                        </div>
                    `;
                }
                
                content += `
                        </div>
                    </div>
                `;
            }

            content += `
                    </div>
                </div>
            `;

            popup.innerHTML = content;

            // Fix Safari gradient rendering: must set explicit height SYNCHRONOUSLY after reflow
            // Safari calculates height: 100% incorrectly on first render if done async
            const popupHeight = Math.max(popup.scrollHeight, popup.offsetHeight);
            const gradientOverlay = popup.querySelector('#gradientOverlay');
            if (gradientOverlay) {
                gradientOverlay.style.height = `${popupHeight}px`;
            }

            // Adjust position if popup would overflow bottom of viewport
            setTimeout(() => {
                const popupRect = popup.getBoundingClientRect();
                const viewportBottom = window.innerHeight + window.pageYOffset;
                const marginBottom = 15;
                const popupBottom = popupRect.top + popupHeight + window.pageYOffset + marginBottom;
                if (popupBottom > viewportBottom) {
                    const overflow = popupBottom - viewportBottom;
                    const currentTop = parseInt(popup.style.top) || event.pageY + 10;
                    const newTop = Math.max(window.pageYOffset + 10, currentTop - overflow);
                    popup.style.top = `${newTop}px`;
                }
            }, 0);

            // Add close button handler
            const closeBtn = popup.querySelector('#closePreview');
            if (closeBtn) {
                closeBtn.addEventListener('click', closePopup);
                addHoverEffect(closeBtn);
            }

            // Add expand handlers (for groups only)
            if (!torrentId) {
                // Expand torrent list handler
                const torrentList = popup.querySelector('#torrentList');
                const expandButton = popup.querySelector('.expandTorrentList');
                const indicator = popup.querySelector('#torrentListIndicator');
                
                if (torrentList && expandButton) {
                    const toggleTorrentList = () => {
                        const isHidden = torrentList.style.display === 'none' || !torrentList.style.display;
                        torrentList.style.display = isHidden ? 'block' : 'none';
                        
                        // Update arrow indicator
                        if (indicator) {
                            indicator.textContent = isHidden ? '▲' : '▼';
                        }
                    };
                    
                    expandButton.addEventListener('click', toggleTorrentList);
                    addHoverEffect(expandButton, '1', '0.9');
                }

                // Add hover effects to torrent cards (matching artist popup style)
                const torrentCards = popup.querySelectorAll('#torrentList > div');
                torrentCards.forEach(card => {
                    card.addEventListener('mouseenter', function() {
                        this.style.background = 'rgba(255, 255, 255, 0.05)';
                    });
                    card.addEventListener('mouseleave', function() {
                        this.style.background = 'rgba(255, 255, 255, 0.03)';
                    });
                });

                // Add uploader expand handler if needed
                const expandUploadersBtn = popup.querySelector('#expandUploaders');
                const uploaderList = popup.querySelector('#uploaderList');
                if (expandUploadersBtn && uploaderList) {
                    // Extract count from button text (format: "90" or "90 uploaders")
                    const match = expandUploadersBtn.textContent.match(/(\d+)/);
                    const uploaderCount = match ? parseInt(match[1]) : (uniqueUploaders ? uniqueUploaders.length : 0);
                    const originalText = `${uploaderCount}`;
                    
                    expandUploadersBtn.addEventListener('click', function(e) {
                        e.stopPropagation();
                        const isHidden = uploaderList.style.display === 'none' || !uploaderList.style.display;
                        uploaderList.style.display = isHidden ? 'block' : 'none';
                        expandUploadersBtn.textContent = isHidden ? `${uploaderCount} (hide)` : originalText;
                    });
                    addHoverEffect(expandUploadersBtn, '1', '0.8');
                }
            }

            // Add artist expand handlers for all artist expansion toggles
            const artistToggles = popup.querySelectorAll('.artists-more-toggle');
            artistToggles.forEach(toggle => {
                // Find the corresponding hidden artists span (next sibling with artists-hidden class)
                let hiddenSpan = toggle.nextElementSibling;
                while (hiddenSpan && !hiddenSpan.classList.contains('artists-hidden')) {
                    hiddenSpan = hiddenSpan.nextElementSibling;
                }
                
                if (hiddenSpan) {
                    // Extract initial count from button text
                    const initialText = toggle.textContent;
                    const match = initialText.match(/\[show (\d+) more\]/);
                    const hiddenCount = match ? parseInt(match[1]) : 0;
                    
                    toggle.addEventListener('click', function() {
                        if (hiddenSpan.style.display === 'none' || !hiddenSpan.style.display) {
                            hiddenSpan.style.display = 'inline';
                            toggle.textContent = ' [show less]';
                        } else {
                            hiddenSpan.style.display = 'none';
                            toggle.textContent = ` [show ${hiddenCount} more]`;
                        }
                    });
                    addHoverEffect(toggle, '1', '0.7');
                }
            });

            // Add handlers for "Various Artists" expand/collapse
            const variousArtistsToggles = popup.querySelectorAll('[id^="various-artists-toggle-"]');
            variousArtistsToggles.forEach(toggle => {
                const uniqueId = toggle.id.replace('various-artists-toggle-', '');
                const artistsList = popup.querySelector(`#various-artists-list-${uniqueId}`);
                const featSection = popup.querySelector(`#various-artists-feat-${uniqueId}`);
                const indicator = popup.querySelector(`#various-artists-indicator-${uniqueId}`);
                
                if (artistsList) {
                    toggle.addEventListener('click', function() {
                        const isExpanded = artistsList.style.display !== 'none' && artistsList.style.display !== '';
                        artistsList.style.display = isExpanded ? 'none' : 'block';
                        if (featSection) {
                            featSection.style.display = isExpanded ? 'none' : 'block';
                        }
                        if (indicator) {
                            indicator.textContent = isExpanded ? '▼' : '▲';
                        }
                    });
                    toggle.addEventListener('mouseenter', function() {
                        this.style.opacity = '1';
                    });
                    toggle.addEventListener('mouseleave', function() {
                        this.style.opacity = '0.9';
                    });
                }
            });

        } catch (error) {
            console.error('Error loading preview:', error);
            if (currentPopup === popup) {
                popup.innerHTML = `
                    <div style="padding: 15px;">
                        <button id="closePreview" style="float: right; background: none; border: none; font-size: 20px; cursor: pointer; opacity: 0.7; padding: 0; line-height: 1;">×</button>
                        <div style="color: #ff0000;">Error loading preview</div>
                    </div>
                `;
                const closeBtn = popup.querySelector('#closePreview');
                if (closeBtn) {
                    closeBtn.addEventListener('click', closePopup);
                }
            }
        }
    }

    // Show artist preview popup
    // Helper function to attach event handlers to artist popup
    function attachArtistPopupHandlers(popup, artistId) {
        // Add close button handler
        const closeBtn = popup.querySelector('#closePreview');
        if (closeBtn) {
            closeBtn.addEventListener('click', closePopup);
            closeBtn.addEventListener('mouseenter', function() {
                this.style.opacity = '1';
                this.style.background = 'rgba(255,255,255,0.1)';
            });
            closeBtn.addEventListener('mouseleave', function() {
                this.style.opacity = '0.6';
                this.style.background = 'none';
            });
        }

        // Add tag expansion handler
        const expandTagsBtn = popup.querySelector('#expandTags');
        const tagsHidden = popup.querySelector('.tags-hidden');
        if (expandTagsBtn && tagsHidden) {
            const initialText = expandTagsBtn.textContent;
            const match = initialText.match(/\[show (\d+) more\]/);
            const hiddenCount = match ? parseInt(match[1]) : 0;
            
            expandTagsBtn.addEventListener('click', function() {
                if (tagsHidden.style.display === 'none') {
                    tagsHidden.style.display = 'inline';
                    expandTagsBtn.textContent = '[show less]';
                } else {
                    tagsHidden.style.display = 'none';
                    expandTagsBtn.textContent = `[show ${hiddenCount} more]`;
                }
            });
            addHoverEffect(expandTagsBtn, '1', '0.7');
        }

        // Function to render releases based on sort type and show count
        const renderReleases = (releasesList, releases, releaseTypeMap, sortBy, showAll) => {
            // Clear existing releases
            releasesList.innerHTML = '';
            
            // Helper to decode HTML entities
            // Calculate total snatches for each release
            const releasesWithSnatches = releases.map(release => {
                const totalSnatches = release.torrent ? release.torrent.reduce((sum, t) => sum + (t.snatched || 0), 0) : 0;
                return { ...release, totalSnatches };
            });
            
            // Sort releases based on sort type
            let sortedReleases;
            if (sortBy === 'snatches') {
                sortedReleases = [...releasesWithSnatches]
                    .sort((a, b) => (b.totalSnatches || 0) - (a.totalSnatches || 0));
            } else {
                sortedReleases = [...releases].sort((a, b) => {
                    const getLatestDate = (release) => {
                        if (!release.torrent || release.torrent.length === 0) return 0;
                        return Math.max(...release.torrent.map(t => {
                            if (!t.time) return 0;
                            const date = new Date(t.time);
                            return isNaN(date.getTime()) ? 0 : date.getTime();
                        }));
                    };
                    return getLatestDate(b) - getLatestDate(a);
                });
            }
            
            // Limit to 3 if not showing all
            const releasesToShow = showAll ? sortedReleases : sortedReleases.slice(0, 3);
            
            // Render each release
            releasesToShow.forEach((release, index) => {
                const releaseLink = `${BASE_URL}/torrents.php?id=${release.groupId}`;
                const releaseYear = release.groupYear || '';
                const releaseType = release.releaseType ? releaseTypeMap[release.releaseType] || null : null;
                const groupName = safeHtml(release.groupName || '');
                const isFirst = index === 0;
                
                // Get snatches or date based on sort type
                let rightColumn = '';
                if (sortBy === 'snatches') {
                    const snatches = release.totalSnatches || 0;
                    rightColumn = `
                        <div style="flex-shrink: 0; text-align: right; opacity: 0.75; font-size: 0.85em; white-space: nowrap;">
                            <div style="font-weight: 500;">${snatches.toLocaleString()}</div>
                            <div style="opacity: 0.6; font-size: 0.9em;">snatches</div>
                        </div>
                    `;
                } else {
                    const latestDate = release.torrent && release.torrent.length > 0
                        ? release.torrent.reduce((latest, t) => {
                            if (!t.time) return latest;
                            const tDate = new Date(t.time);
                            return !isNaN(tDate.getTime()) && tDate.getTime() > latest ? tDate.getTime() : latest;
                        }, 0)
                        : 0;
                    const dateStr = latestDate ? new Date(latestDate).toLocaleDateString() : '';
                    if (dateStr) {
                        rightColumn = `
                            <div style="flex-shrink: 0; text-align: right; opacity: 0.65; font-size: 0.85em; white-space: nowrap;">
                                <div style="font-size: 0.9em;">${dateStr}</div>
                            </div>
                        `;
                    }
                }
                
                const releaseDiv = document.createElement('div');
                releaseDiv.style.cssText = `margin-bottom: ${isFirst ? '8px' : '10px'}; padding: 8px; background: rgba(255, 255, 255, 0.03); border-radius: 4px; border-left: 3px solid rgba(255, 255, 255, ${isFirst ? '0.3' : '0.15'}); transition: background 0.2s;`;
                releaseDiv.innerHTML = `
                    <div style="display: flex; align-items: flex-start; gap: 8px;">
                        <div style="flex: 1; min-width: 0;">
                            <div style="margin-bottom: 4px;">
                                <a href="${releaseLink}" style="text-decoration: none; opacity: 0.95; font-weight: ${isFirst ? '500' : 'normal'}; font-size: ${isFirst ? '1em' : '0.95em'}; display: inline-block;">${safeHtml(groupName)}</a>
                            </div>
                            <div style="display: flex; flex-wrap: wrap; gap: 6px; align-items: center; font-size: 0.85em; opacity: 0.7;">
                                ${releaseYear ? `<span>${releaseYear}</span>` : ''}
                                ${releaseYear && releaseType ? `<span style="opacity: 0.5;">•</span>` : ''}
                                ${releaseType ? `<span style="opacity: 0.8;">${releaseType}</span>` : ''}
                            </div>
                        </div>
                        ${rightColumn}
                    </div>
                `;
                
                // Add hover effects
                // Release div hover effects (custom background change, not opacity)
                releaseDiv.addEventListener('mouseenter', function() {
                    this.style.background = 'rgba(255, 255, 255, 0.06)';
                });
                releaseDiv.addEventListener('mouseleave', function() {
                    this.style.background = 'rgba(255, 255, 255, 0.03)';
                });
                
                releasesList.appendChild(releaseDiv);
            });
        };

        // Get data from popup or cache
        const artistData = popup._artistData || (artistPopupCache.has(artistId) ? artistPopupCache.get(artistId)._artistData : null);
        
        if (artistData && artistData.topReleases && artistData.topReleases.length > 0) {
            const releasesList = popup.querySelector('#releasesList');
            const sortBySnatchesBtn = popup.querySelector('#sortBySnatches');
            const sortByDateBtn = popup.querySelector('#sortByDate');
            const showAllBtn = popup.querySelector('#showAllReleases');
            
            // Track current state
            let currentSort = 'snatches';
            let showingAll = false;
            
            // Initial render (top 3 by snatches)
            renderReleases(releasesList, artistData.topReleases, artistData.releaseTypeMap, 'snatches', false);
            
            // Sort toggle handlers
            if (sortBySnatchesBtn && sortByDateBtn) {
                const updateSortButtons = () => {
                    if (currentSort === 'snatches') {
                        sortBySnatchesBtn.style.opacity = '0.9';
                        sortBySnatchesBtn.style.textDecoration = 'underline';
                        sortByDateBtn.style.opacity = '0.6';
                        sortByDateBtn.style.textDecoration = 'none';
                    } else {
                        sortBySnatchesBtn.style.opacity = '0.6';
                        sortBySnatchesBtn.style.textDecoration = 'none';
                        sortByDateBtn.style.opacity = '0.9';
                        sortByDateBtn.style.textDecoration = 'underline';
                    }
                };
                
                sortBySnatchesBtn.addEventListener('click', function() {
                    currentSort = 'snatches';
                    updateSortButtons();
                    renderReleases(releasesList, artistData.topReleases, artistData.releaseTypeMap, currentSort, showingAll);
                });
                
                sortByDateBtn.addEventListener('click', function() {
                    currentSort = 'date';
                    updateSortButtons();
                    renderReleases(releasesList, artistData.topReleases, artistData.releaseTypeMap, currentSort, showingAll);
                });
                
                // Add hover effects
                // Sort buttons have dynamic opacity based on active state, so use custom hover
                [sortBySnatchesBtn, sortByDateBtn].forEach(btn => {
                    btn.addEventListener('mouseenter', function() {
                        if (this.style.opacity !== '0.9') {
                            this.style.opacity = '0.8';
                        }
                    });
                    btn.addEventListener('mouseleave', function() {
                        if (this.style.opacity !== '0.9') {
                            this.style.opacity = '0.6';
                        }
                    });
                });
                
                updateSortButtons();
            }
            
            // Show all handlers (top and bottom buttons)
            const showAllTopBtn = popup.querySelector('#showAllReleasesTop');
            const showAllBottomBtn = popup.querySelector('#showAllReleasesBottom');
            
            const updateShowAllButtons = () => {
                const buttonText = showingAll ? 'show less' : 'show all';
                if (showAllTopBtn) {
                    showAllTopBtn.querySelector('span').textContent = buttonText;
                    showAllTopBtn.style.display = showingAll ? 'block' : 'none';
                }
                if (showAllBottomBtn) {
                    showAllBottomBtn.querySelector('span').textContent = buttonText;
                    showAllBottomBtn.style.display = showingAll ? 'none' : 'block';
                }
            };
            
            const toggleShowAll = () => {
                showingAll = !showingAll;
                updateShowAllButtons();
                renderReleases(releasesList, artistData.topReleases, artistData.releaseTypeMap, currentSort, showingAll);
            };
            
            if (showAllTopBtn) {
                const topSpan = showAllTopBtn.querySelector('span');
                topSpan.addEventListener('click', toggleShowAll);
                addHoverEffect(topSpan, '1', '0.8');
            }
            
            if (showAllBottomBtn) {
                const bottomSpan = showAllBottomBtn.querySelector('span');
                bottomSpan.addEventListener('click', toggleShowAll);
                addHoverEffect(bottomSpan, '1', '0.8');
            }
            
            // Initialize button visibility
            updateShowAllButtons();
        }
    }

    async function showArtistPreview(artistId, event) {
        // Close existing popup if any
        closePopup();

        // Check if we have a cached popup for this artist
        if (artistPopupCache.has(artistId)) {
            const cachedPopup = artistPopupCache.get(artistId);
            // Clone the cached popup
            const popup = cachedPopup.cloneNode(true);
            // Preserve the data from cached popup
            popup._artistData = cachedPopup._artistData;
            // Ensure styles are applied (cloneNode may not preserve all styles)
            if (!popup.style.position) {
                setupPopupStyles(popup);
            }
            
            // Position near cursor
            const x = Math.min(event.pageX + 10, window.innerWidth + window.pageXOffset - 400);
            const y = event.pageY + 10;
            popup.style.left = `${x}px`;
            popup.style.top = `${y}px`;
            
            document.body.appendChild(popup);
            currentPopup = popup;
            
            // Reattach event handlers
            attachArtistPopupHandlers(popup, artistId);
            return;
        }

        // Create popup container
        const popup = document.createElement('div');
        popup.className = 'box';
        popup.setAttribute('data-artist-id', artistId);
        setupPopupStyles(popup);
        ensureScrollbarStyles();

        // Position near cursor but ensure it's visible
        const x = Math.min(event.pageX + 10, window.innerWidth + window.pageXOffset - 400);
        const y = event.pageY + 10;
        popup.style.left = `${x}px`;
        popup.style.top = `${y}px`;

        // Add loading message with animated ellipses
        popup.innerHTML = `
            <div style="padding: 15px;">
                <div style="text-align: center; font-size: 0.95em; opacity: 0.9;">
                    Loading<span class="loading-ellipses"></span>
                </div>
            </div>
        `;

        document.body.appendChild(popup);
        currentPopup = popup;

        try {
            const data = await fetchArtistData(artistId);

            // Check if popup was closed while loading
            if (currentPopup !== popup) return;

            const artist = data;
            const artistLink = `${BASE_URL}/artist.php?id=${artistId}`;
            const image = artist.image || '';
            const tags = artist.tags || [];
            const stats = artist.statistics || {};
            // Deduplicate releases by groupId (same release can appear multiple times if artist has multiple roles)
            const releasesMap = new Map();
            (artist.torrentgroup || []).forEach(release => {
                if (release.groupId && !releasesMap.has(release.groupId)) {
                    releasesMap.set(release.groupId, release);
                }
            });
            const topReleases = Array.from(releasesMap.values());
            const similarArtists = artist.similarArtists || [];

            // Build popup content
            let content = `
                <div style="position: relative;">
                    <button id="closePreview" style="position: absolute; top: 8px; right: 8px; background: none; border: none; font-size: 22px; cursor: pointer; opacity: 0.6; padding: 0; line-height: 1; transition: opacity 0.2s, background 0.2s; z-index: 10; box-shadow: none; text-shadow: none;">×</button>
            `;

            // Add artist image background - positioned relative to .box, not inner div
            if (image) {
                content += `
                    <div id="blurredBg" style="position: absolute; top: 0; left: 0; right: 0; height: 200px; background-image: url('${image}'); background-size: cover; background-position: center; filter: blur(20px) brightness(0.35); opacity: 0.8; z-index: 0; transform: scale(1.15); pointer-events: none;"></div>
                    <div id="gradientOverlay" style="position: absolute; top: 0; left: 0; right: 0; height: 100%; background: linear-gradient(to bottom, transparent 0%, transparent 120px, rgba(26, 26, 26, 0.7) 170px, #1a1a1a 210px); z-index: 0; pointer-events: none;"></div>
                `;
            }

            content += `
                    <div style="padding: 18px; position: relative; z-index: 1;">
            `;

            // Add artist image inside padded div - make it clickable
            if (image) {
                content += `
                    <div style="text-align: center; margin-bottom: 8px; padding: 20px;">
                        <a href="${artistLink}" style="display: inline-block; cursor: pointer;">
                            <img src="${image}" alt="Artist" style="max-width: 100%; max-height: 200px; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); display: block; margin: 0 auto; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'" />
                        </a>
                    </div>
                `;
            }

            // Artist name
            content += `
                <div style="margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
                    <div style="font-weight: bold; font-size: 1.15em; margin-bottom: 6px; line-height: 1.3; text-align: center;">
                        <a href="${artistLink}" style="text-decoration: none;">${safeHtml(artist.name)}</a>
                    </div>
            `;

            // Statistics - single line with icons and compact numbers
            if (stats.numGroups || stats.numTorrents || stats.numSeeders || stats.numSnatches) {
                content += `
                    <div style="display: flex; flex-wrap: nowrap; gap: 12px 16px; margin-bottom: 0; font-size: 0.85em; opacity: 0.85; justify-content: center; align-items: center; overflow-x: auto;">
                `;
                
                if (stats.numGroups) {
                    content += `
                        <div title="Groups: ${stats.numGroups.toLocaleString()}" style="display: flex; align-items: center; gap: 4px; white-space: nowrap; flex-shrink: 0;">
                            ${createIcon('folder')}
                            <span>${stats.numGroups}</span>
                        </div>
                    `;
                }
                
                if (stats.numTorrents) {
                    content += `
                        <div title="Torrents: ${stats.numTorrents.toLocaleString()}" style="display: flex; align-items: center; gap: 4px; white-space: nowrap; flex-shrink: 0;">
                            ${createIcon('disk')}
                            <span>${stats.numTorrents}</span>
                        </div>
                    `;
                }
                
                if (stats.numSeeders) {
                    content += `
                        <div title="Seeders: ${stats.numSeeders.toLocaleString()}" style="display: flex; align-items: center; gap: 4px; white-space: nowrap; flex-shrink: 0;">
                            ${createIcon('arrowUp')}
                            <span>${formatCompactNumber(stats.numSeeders)}</span>
                        </div>
                    `;
                }
                
                if (stats.numSnatches) {
                    content += `
                        <div title="Snatches: ${stats.numSnatches.toLocaleString()}" style="display: flex; align-items: center; gap: 4px; white-space: nowrap; flex-shrink: 0;">
                            ${createIcon('cycle')}
                            <span>${formatCompactNumber(stats.numSnatches)}</span>
                        </div>
                    `;
                }
                
                content += `
                    </div>
                `;
            }

            content += `</div>`;

            // Tags with expansion if more than 8
            if (tags.length > 0) {
                const visibleTags = tags.slice(0, 8);
                const hiddenTags = tags.slice(8);
                const visibleTagLinks = visibleTags.map(tag =>
                    `<a href="${BASE_URL}/torrents.php?action=advanced&amp;taglist=${encodeURIComponent(tag.name)}">${safeHtml(tag.name)}</a>`
                ).join(', ');
                
                if (hiddenTags.length > 0) {
                    const hiddenTagLinks = hiddenTags.map(tag =>
                        `<a href="${BASE_URL}/torrents.php?action=advanced&amp;taglist=${encodeURIComponent(tag.name)}">${safeHtml(tag.name)}</a>`
                    ).join(', ');
                    content += `
                        <div style="margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); margin-top: -4px; text-align: center;">
                            <div class="tags">
                                <span class="tags-visible">${visibleTagLinks}</span>
                                <span id="expandTags" style="cursor: pointer; opacity: 0.7; margin-left: 4px; text-decoration: underline;">[show ${hiddenTags.length} more]</span>
                                <span class="tags-hidden" style="display: none;">, ${hiddenTagLinks}</span>
                            </div>
                        </div>
                    `;
                } else {
                    content += `
                        <div style="margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); margin-top: -4px; text-align: center;">
                            <div class="tags">${visibleTagLinks}</div>
                        </div>
                    `;
                }
            }

            // Release type map (defined outside if block so it's always available)
            const releaseTypeMap = {
                1: 'Album', 3: 'Soundtrack', 5: 'EP', 6: 'Anthology', 7: 'Compilation',
                9: 'Single', 11: 'Live album', 13: 'Remix', 14: 'Bootleg', 15: 'Interview',
                16: 'Mixtape', 17: 'Demo', 18: 'Concert Recording', 19: 'DJ Mix', 21: 'Unknown'
            };

            // Similar artists (moved above releases)
            if (similarArtists.length > 0) {
                const similarLinks = similarArtists.slice(0, 5).map(artist => {
                    // Handle different possible field names for artist ID
                    const artistId = artist.id || artist.artistId || artist.artist_id;
                    if (!artistId) {
                        console.warn('Similar artist missing ID:', artist);
                        return safeHtml(artist.name);
                    }
                    return `<a href="${BASE_URL}/artist.php?id=${artistId}" style="text-decoration: none; opacity: 0.85;">${safeHtml(artist.name)}</a>`;
                }).join(', ');
                content += `
                    <div style="margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); font-size: 0.9em; opacity: 0.85;">
                        <span style="opacity: 0.7;">Similar:</span> ${similarLinks}
                    </div>
                `;
            }

            // Consolidated releases section with sortable header
            if (topReleases.length > 0) {
                // Helper to decode HTML entities
                // Calculate total snatches for each release
                const releasesWithSnatches = topReleases.map(release => {
                    const totalSnatches = release.torrent ? release.torrent.reduce((sum, t) => sum + (t.snatched || 0), 0) : 0;
                    return { ...release, totalSnatches };
                });

                content += `
                    <div style="margin-bottom: 12px;">
                        <div style="margin-bottom: 8px; opacity: 0.85; font-size: 0.95em; display: flex; align-items: center; gap: 8px;">
                            <strong>Top Releases</strong>
                            <span style="opacity: 0.6; font-size: 0.9em;">|</span>
                            <span id="sortBySnatches" style="cursor: pointer; opacity: 0.9; text-decoration: underline;">By Snatches</span>
                            <span style="opacity: 0.5;">/</span>
                            <span id="sortByDate" style="cursor: pointer; opacity: 0.6;">By Date</span>
                        </div>
                        ${topReleases.length > 3 ? `
                        <div id="showAllReleasesTop" style="margin-bottom: 6px; text-align: center; display: none;">
                            <span style="cursor: pointer; opacity: 0.8; font-size: 0.9em; text-decoration: underline;">show all</span>
                        </div>
                        ` : ''}
                        <div id="releasesList" style="font-size: 0.9em;">
                        </div>
                        ${topReleases.length > 3 ? `
                        <div id="showAllReleasesBottom" style="margin-top: 8px; text-align: center;">
                            <span style="cursor: pointer; opacity: 0.8; font-size: 0.9em; text-decoration: underline;">show all</span>
                        </div>
                        ` : ''}
                    </div>
                `;
            }

            content += `
                    </div>
                </div>
            `;

            popup.innerHTML = content;

            // Fix Safari gradient rendering: must set explicit height SYNCHRONOUSLY after reflow
            // Safari calculates height: 100% incorrectly on first render if done async
            const popupHeight = Math.max(popup.scrollHeight, popup.offsetHeight);
            const gradientOverlay = popup.querySelector('#gradientOverlay');
            if (gradientOverlay) {
                gradientOverlay.style.height = `${popupHeight}px`;
            }

            // Adjust position if popup would overflow bottom of viewport
            setTimeout(() => {
                const popupRect = popup.getBoundingClientRect();
                const viewportBottom = window.innerHeight + window.pageYOffset;
                const marginBottom = 15;
                const popupBottom = popupRect.top + popupHeight + window.pageYOffset + marginBottom;
                if (popupBottom > viewportBottom) {
                    const overflow = popupBottom - viewportBottom;
                    const currentTop = parseInt(popup.style.top) || event.pageY + 10;
                    const newTop = Math.max(window.pageYOffset + 10, currentTop - overflow);
                    popup.style.top = `${newTop}px`;
                }
            }, 0);

            // Store releases data and releaseTypeMap on popup for deferred sorting
            const artistData = {
                topReleases: topReleases,
                releaseTypeMap: releaseTypeMap
            };
            popup._artistData = artistData;
            
            // Attach all event handlers
            attachArtistPopupHandlers(popup, artistId);
            
            // Cache the popup for future use (clone after attaching handlers, but preserve data)
            const cachedPopup = popup.cloneNode(true);
            cachedPopup._artistData = artistData;
            artistPopupCache.set(artistId, cachedPopup);

        } catch (error) {
            console.error('Error loading artist preview:', error);
            if (currentPopup === popup) {
                popup.innerHTML = `
                    <div style="padding: 15px;">
                        <button id="closePreview" style="float: right; background: none; border: none; font-size: 20px; cursor: pointer; opacity: 0.7; padding: 0; line-height: 1;">×</button>
                        <div style="color: #ff0000;">Error loading artist preview</div>
                    </div>
                `;
                const closeBtn = popup.querySelector('#closePreview');
                if (closeBtn) {
                    closeBtn.addEventListener('click', closePopup);
                }
            }
        }
    }

    // Close popup
    function closePopup() {
        if (currentPopup) {
            currentPopup.remove();
            currentPopup = null;
        }
    }

    // Click outside to close
    document.addEventListener('click', (e) => {
        if (currentPopup && !currentPopup.contains(e.target)) {
            closePopup();
        }
    });

    // ESC key to close
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape' && currentPopup) {
            closePopup();
        }
    });

    // Initial run
    addPreviewIcons();

    // Watch for new links added dynamically
    const observer = new MutationObserver(() => {
        addPreviewIcons();
    });

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