better_drunkenslug_xxx_previews

Better Thumbnail preview for drunkenslug xxx with infinite scrolling, Hover zoom, gallery view, Ctrl-hover lightbox, click-to-add-to-cart, configurable keyword filters, and session-based new post highlighting

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

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

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

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

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

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

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

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

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

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

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

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

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

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         better_drunkenslug_xxx_previews
// @namespace    http://tampermonkey.net/
// @version      2.10.11
// @description  Better Thumbnail preview for drunkenslug xxx with infinite scrolling, Hover zoom, gallery view, Ctrl-hover lightbox, click-to-add-to-cart, configurable keyword filters, and session-based new post highlighting
// @author       takuto
// @match        https://drunkenslug.com/*
// @run-at       document-idle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      *
// @license      MIT
// ==/UserScript==

const EBDS_IS_SEARCH_PAGE = location.pathname.startsWith('/search');

const EBDS_ENABLED = (() => {
    try {
        const t = parseInt(new URLSearchParams(location.search).get('t'), 10);
        return (Number.isInteger(t) && t >= 6000 && t <= 6999) || EBDS_IS_SEARCH_PAGE;
    } catch (e) { return false; }
})();

const EBDS_DEFAULT_BLACKLIST_TERMS = [
    'a.b.multimedia.erotica.male'
];
const EBDS_BLACKLIST_STORAGE_KEY = 'ebdsBlacklistTerms';
const EBDS_HIDE_WITHOUT_PICS_STORAGE_KEY = 'ebdsHideWithoutPictures';
const EBDS_MIN_FILE_SIZE_MB_KEY = 'ebdsMinFileSizeMb';
const EBDS_SCALE_HOVER_PREVIEW_KEY = 'ebdsScaleHoverPreview';
const EBDS_FILTER_LINKS_POSITION_KEY = 'ebdsFilterLinksPosition';
const EBDS_INFINITE_SCROLL_KEY = 'ebdsInfiniteScroll';
// Storage key for session expiration minutes setting
const EBDS_SESSION_EXPIRATION_KEY = 'ebdsSessionExpiration';
const EBDS_SAB_URL_KEY = 'ebdsSabnzbdUrl';
const EBDS_SAB_API_KEY = 'ebdsSabnzbdApiKey';
const EBDS_HIDE_SAB_BUTTON_KEY = 'ebdsHideSabReadFeedsButton';

let EBDS_HIDE_WITHOUT_PICTURES = true;
try {
    const stored = localStorage.getItem(EBDS_HIDE_WITHOUT_PICS_STORAGE_KEY);
    if (stored !== null) EBDS_HIDE_WITHOUT_PICTURES = (stored === '1');
} catch (e) { }

let EBDS_MIN_FILE_SIZE_MB = 0;
try {
    const stored = Number(localStorage.getItem(EBDS_MIN_FILE_SIZE_MB_KEY));
    if (Number.isFinite(stored) && stored >= 0) EBDS_MIN_FILE_SIZE_MB = stored;
} catch (e) { }

let EBDS_SCALE_HOVER_PREVIEW = false;
try {
    EBDS_SCALE_HOVER_PREVIEW = localStorage.getItem(EBDS_SCALE_HOVER_PREVIEW_KEY) === '1';
} catch (e) { }

let EBDS_FILTER_LINKS_POSITION = 'footer';
try {
    const storedPosition = localStorage.getItem(EBDS_FILTER_LINKS_POSITION_KEY);
    if (['footer', 'header', 'hidden'].includes(storedPosition)) EBDS_FILTER_LINKS_POSITION = storedPosition;
} catch (e) { }

let EBDS_INFINITE_SCROLL = true;
try {
    const stored = localStorage.getItem(EBDS_INFINITE_SCROLL_KEY);
    if (stored !== null) EBDS_INFINITE_SCROLL = stored === '1';
} catch (e) { }

let EBDS_HIDE_SAB_BUTTON = false;
try {
    EBDS_HIDE_SAB_BUTTON = localStorage.getItem(EBDS_HIDE_SAB_BUTTON_KEY) === '1';
} catch (e) { }

const EBDS_TEMPORARY_FILTER_BYPASS = {
    withoutPictures: false,
    keywords: false,
    minimumSize: false
};

function parseFileSizeMb(value) {
    const match = String(value || '').match(/\b(\d+(?:[.,]\d+)?)\s*(KB|MB|GB|TB)\b/i);
    if (!match) return null;
    const amount = Number(match[1].replace(',', '.'));
    if (!Number.isFinite(amount)) return null;
    const factors = { KB: 1 / 1024, MB: 1, GB: 1024, TB: 1024 * 1024 };
    return amount * factors[match[2].toUpperCase()];
}

function applyCtrlHoverPreviewSizing(image) {
    image.style.objectFit = 'contain';
    if (!EBDS_SCALE_HOVER_PREVIEW) {
        image.style.width = 'auto';
        image.style.height = 'auto';
        image.style.maxWidth = '95vw';
        image.style.maxHeight = '95vh';
        return;
    }

    image.style.maxWidth = '100vw';
    image.style.maxHeight = '100vh';
    const scaleToViewport = () => {
        if (!image.naturalWidth || !image.naturalHeight) return;
        const imageRatio = image.naturalWidth / image.naturalHeight;
        const viewportRatio = window.innerWidth / window.innerHeight;
        if (imageRatio >= viewportRatio) {
            image.style.width = '100vw';
            image.style.height = 'auto';
        } else {
            image.style.width = 'auto';
            image.style.height = '100vh';
        }
    };
    if (image.complete) scaleToViewport();
    else image.addEventListener('load', scaleToViewport, { once: true });
}

function normalizeBlacklistTerm(term) {
    return (term || '').trim().toLowerCase();
}

function getReleaseTitleText(row) {
    const titleLink = row ? row.querySelector('a.title') : null;
    return titleLink ? titleLink.textContent.trim().toLowerCase() : '';
}

function loadBlacklistTerms() {
    try {
        const raw = localStorage.getItem(EBDS_BLACKLIST_STORAGE_KEY);
        if (raw !== null) {
            const stored = JSON.parse(raw);
            if (!Array.isArray(stored)) throw new Error('Invalid blacklist storage');
            return Array.from(new Set(stored.map(normalizeBlacklistTerm).filter(Boolean)));
        }
    } catch (e) { }
    return EBDS_DEFAULT_BLACKLIST_TERMS.slice();
}

function persistBlacklistTerms(terms) {
    try { localStorage.setItem(EBDS_BLACKLIST_STORAGE_KEY, JSON.stringify(terms)); } catch (e) { }
}

let EBDS_BLACKLIST_TERMS = loadBlacklistTerms();

let ctrlPressed = false;

// Hidden prefixes (session-persistent; cleared on new session)
const EBDS_HIDDEN_PREFIXES_KEY = 'ebdsHiddenPrefixes';
const EBDS_CLICKED_STORAGE_KEY = 'ebdsClickedGuids';
const EBDS_UI_SESSION_KEY = 'ebdsUiSession';
let EBDS_HIDDEN_PREFIXES = new Set();
let EBDS_CLICKED_GUIDS = new Set();
const EBDS_PENDING_CART_REQUESTS = new Map();
function loadHiddenPrefixes() {
    try {
        const raw = localStorage.getItem(EBDS_HIDDEN_PREFIXES_KEY) || '[]';
        const arr = JSON.parse(raw);
        if (Array.isArray(arr)) return new Set(arr.filter(Boolean).map(s => String(s).toLowerCase()));
    } catch (e) { }
    return new Set();
}
function persistHiddenPrefixes() {
    try { localStorage.setItem(EBDS_HIDDEN_PREFIXES_KEY, JSON.stringify(Array.from(EBDS_HIDDEN_PREFIXES))); } catch (e) { }
}
function loadClickedGuids() {
    try {
        const raw = localStorage.getItem(EBDS_CLICKED_STORAGE_KEY) || '[]';
        const arr = JSON.parse(raw);
        if (Array.isArray(arr)) return new Set(arr.filter(Boolean).map(String));
    } catch (e) { }
    return new Set();
}
function persistClickedGuids() {
    try { localStorage.setItem(EBDS_CLICKED_STORAGE_KEY, JSON.stringify(Array.from(EBDS_CLICKED_GUIDS))); } catch (e) { }
}
function addClickedGuid(guid) {
    if (!guid) return;
    EBDS_CLICKED_GUIDS.add(String(guid));
    persistClickedGuids();
}
function clearClickedGuids() {
    EBDS_CLICKED_GUIDS = new Set();
    try { localStorage.removeItem(EBDS_CLICKED_STORAGE_KEY); } catch (e) { }
}

EBDS_HIDDEN_PREFIXES = loadHiddenPrefixes();
EBDS_CLICKED_GUIDS = loadClickedGuids();
function derivePrefixFromSrc(src) {
    try {
        if (!src) return null;
        const parts = src.split('/');
        const filename = parts[parts.length - 1] || src;
        const idx = Math.min(...['_', '&', '.'].map(c => {
            const i = filename.indexOf(c);
            return i === -1 ? Infinity : i;
        }));
        if (!Number.isFinite(idx) || idx <= 0) return filename.toLowerCase();
        return filename.substring(0, idx).toLowerCase();
    } catch (e) { return null; }
}

// Derive prefix from an element (prefer title/alt/dataset.name, fallback to src/href filename)
function derivePrefixFromElement(el) {
    try {
        if (!el) return null;
        let candidate = null;
        // Prefer title or alt or dataset.name
        try { candidate = (el.getAttribute && el.getAttribute('title')) || el.alt || (el.dataset && el.dataset.name) || null; } catch (e) { }
        // If candidate looks like the "posted • name • size" format, try to pick the token that contains underscores or looks like a filename
        if (candidate) {
            // split by '•' and pick token that contains '_' or '.' or '&', else pick the longest
            const parts = candidate.split('•').map(s => s.trim()).filter(Boolean);
            let pick = parts.find(p => /[_&.]/.test(p));
            if (!pick) pick = parts.sort((a,b) => b.length - a.length)[0];
            candidate = pick || candidate;
        }
        // If still no candidate or candidate seems not useful, fall back to src or dataset.href
        if (!candidate) {
            try { candidate = (el.dataset && el.dataset.href) || el.src || ''; } catch (e) { candidate = ''; }
        }
        if (!candidate) return null;
        // If it's a URL, take last pathname segment
        try { if (candidate.indexOf('/') !== -1) candidate = candidate.split('/').pop(); } catch (e) { }
        candidate = candidate.split('?')[0].split('#')[0].trim();
        // Derive prefix up to first delimiter
        const idx = Math.min(...['_', '&', '.'].map(c => {
            const i = candidate.indexOf(c);
            return i === -1 ? Infinity : i;
        }));
        if (!Number.isFinite(idx) || idx <= 0) return candidate.toLowerCase();
        return candidate.substring(0, idx).toLowerCase();
    } catch (e) { return null; }
}
function matchesHiddenPrefixForSrc(src) {
    try {
        const p = derivePrefixFromSrc(src);
        return p && EBDS_HIDDEN_PREFIXES && EBDS_HIDDEN_PREFIXES.has(p);
    } catch (e) { return false; }
}
function matchesHiddenPrefixForRow(row) {
    try {
        if (!row) return false;
        // Prefer checking an actual image element inside the row (title/alt/dataset may be present)
        const img = row.querySelector('img');
        if (img) {
            const p = derivePrefixFromElement(img);
            return p && EBDS_HIDDEN_PREFIXES && EBDS_HIDDEN_PREFIXES.has(p);
        }
        const anchor = row.querySelector('a[href]');
        const href = anchor ? anchor.href : null;
        if (!href) return false;
        const p = derivePrefixFromSrc(href);
        return p && EBDS_HIDDEN_PREFIXES && EBDS_HIDDEN_PREFIXES.has(p);
    } catch (e) { return false; }
}
function addHiddenPrefix(pref) {
    try {
        if (!pref) return;
        const p = String(pref).toLowerCase();
        if (EBDS_HIDDEN_PREFIXES.has(p)) return;
        EBDS_HIDDEN_PREFIXES.add(p);
        persistHiddenPrefixes();
        applyRowFiltering();
    } catch (e) { }
}
function removeHiddenPrefix(pref) {
    try {
        if (!pref) return;
        const p = String(pref).toLowerCase();
        if (EBDS_HIDDEN_PREFIXES.has(p)) {
            EBDS_HIDDEN_PREFIXES.delete(p);
            persistHiddenPrefixes();
            applyRowFiltering();
        }
    } catch (e) { }
}
function clearHiddenPrefixes() {
    try {
        EBDS_HIDDEN_PREFIXES = new Set();
        localStorage.removeItem(EBDS_HIDDEN_PREFIXES_KEY);
    } catch (e) { }
}
// Expose a few helpers globally
try { window.addHiddenPrefix = addHiddenPrefix; window.removeHiddenPrefix = removeHiddenPrefix; window.clearHiddenPrefixes = clearHiddenPrefixes; window.loadHiddenPrefixes = loadHiddenPrefixes; } catch (e) { }

function addToCartByGuid(guid, src) {
    if (!guid) return Promise.resolve(false);
    const guidKey = String(guid);
    if (EBDS_PENDING_CART_REQUESTS.has(guidKey)) return EBDS_PENDING_CART_REQUESTS.get(guidKey);

    const row = document.getElementById('guid' + guidKey);
    const cart = row ? row.querySelector('.icon_cart') : null;
    let request;

    // Dynamically appended cart icons do not have the site's jQuery handler, so use the API fallback.
    if (cart && !(row && row.dataset.ebdsInfiniteAdded === '1')) {
        request = new Promise(resolve => {
            let settled = false;
            const observer = new MutationObserver(() => {
                if (cart.classList.contains('icon_cart_clicked')) finish(true);
            });
            const timeout = setTimeout(() => finish(false), 10000);
            function finish(added) {
                if (settled) return;
                settled = true;
                clearTimeout(timeout);
                observer.disconnect();
                if (added) markAddedInDOM(guidKey, src);
                resolve(added);
            }
            observer.observe(cart, { attributes: true, attributeFilter: ['class'] });
            try {
                cart.click();
                if (cart.classList.contains('icon_cart_clicked')) finish(true);
            } catch (error) {
                console.log('Add to cart error', error);
                finish(false);
            }
        });
    } else {
        request = (async () => {
            const server = (window.SERVERROOT !== undefined) ? String(window.SERVERROOT) : '/';
            const url = server.replace(/\/?$/, '/') + 'cart?add=' + encodeURIComponent(guidKey);
            try {
                const resp = await fetch(url, { method: 'POST', credentials: 'same-origin' });
                if (resp.ok) {
                    markAddedInDOM(guidKey, src);
                    return true;
                } else {
                    console.log('Add to cart failed', await resp.text());
                }
            } catch (err) {
                console.log('Add to cart error', err);
            }
            return false;
        })();
    }

    request = request.finally(() => EBDS_PENDING_CART_REQUESTS.delete(guidKey));

    EBDS_PENDING_CART_REQUESTS.set(guidKey, request);
    return request;
}

function markAddedInDOM(guidToUse, src) {
    // When an item is marked added/hidden, also record its GUID for this session
    try { if (guidToUse) addClickedGuid(guidToUse); } catch (e) { }

    const orig = guidToUse ? document.querySelector('img.ebds-preview-img[data-guid="' + guidToUse + '"]') : null;
    if (orig) {
        const wrapper = orig.closest('.ebds-preview');
        try { if (wrapper) wrapper.remove(); else orig.remove(); } catch (e) { }
    }
    // Remove gallery items matching by data-guid or src
    try {
        const overlay = document.getElementById('ebds-gallery-overlay');
        if (overlay) {
            const galleryItems = Array.from(overlay.querySelectorAll('.ebds-gallery-item')).filter(item => {
                const img = item.querySelector('img');
                return (guidToUse && item.dataset.guid === guidToUse)
                    || (img && guidToUse && img.dataset.guid === guidToUse)
                    || (img && src && img.src === src);
            });
            galleryItems.forEach(item => {
                try { item.remove(); } catch (e) { }
            });

            try {
                if (typeof window.ebdsUpdateGalleryFooter === 'function') window.ebdsUpdateGalleryFooter();
            } catch (e) { }
        }
    } catch (e) { }
}

document.addEventListener('keydown', (e) => {
    if (e.key === 'Control') {
        ctrlPressed = true;
    }
});

document.addEventListener('keyup', (e) => {
    if (e.key === 'Control') {
        ctrlPressed = false;
    }
});

function applyRowFiltering() {
    if (!EBDS_ENABLED) return;
    const rows = document.querySelectorAll('tr');
    rows.forEach(row => {
        const hasOriginalImage = row.dataset && row.dataset.ebdsHasImgLink === '1';
        const isOperationsRow = (row.id === 'nzb_multi_operations') || !!row.querySelector('#nzb_multi_operations');
        const isReleaseRow = (!isOperationsRow) && (
            (row.id && row.id.indexOf('guid') === 0) ||
            !!row.querySelector('.icon_cart') ||
            !!row.querySelector('input[type="checkbox"][name^="nzbs"]')
        );
        const titleText = isReleaseRow ? getReleaseTitleText(row) : '';
        const containsBlacklistedTerm = EBDS_BLACKLIST_TERMS.some(term => term && titleText.includes(term));
        const prefixHidden = matchesHiddenPrefixForRow(row);
        const missingOriginalImage = EBDS_HIDE_WITHOUT_PICTURES && isReleaseRow && !hasOriginalImage;
        const fileSizeMb = isReleaseRow ? parseFileSizeMb(row.textContent) : null;
        const belowMinimumSize = EBDS_MIN_FILE_SIZE_MB > 0 && fileSizeMb !== null && fileSizeMb < EBDS_MIN_FILE_SIZE_MB;
        const bypassedByAnyFilter =
            (EBDS_TEMPORARY_FILTER_BYPASS.keywords && containsBlacklistedTerm) ||
            (EBDS_TEMPORARY_FILTER_BYPASS.withoutPictures && missingOriginalImage) ||
            (EBDS_TEMPORARY_FILTER_BYPASS.minimumSize && belowMinimumSize);
        const shouldHide = prefixHidden ||
            (containsBlacklistedTerm && !EBDS_TEMPORARY_FILTER_BYPASS.keywords) ||
            (missingOriginalImage && !EBDS_TEMPORARY_FILTER_BYPASS.withoutPictures) ||
            (belowMinimumSize && !EBDS_TEMPORARY_FILTER_BYPASS.minimumSize);
        row.classList.toggle('ebds-temporarily-shown', bypassedByAnyFilter && !shouldHide);

        if (shouldHide) {
            row.dataset.ebdsHidden = '1';
            row.style.display = 'none';
            // Also remove from gallery if present
            const guid = row.id && row.id.indexOf('guid') === 0 ? row.id.substring(4) : null;
            if (guid) {
                const overlay = document.getElementById('ebds-gallery-overlay');
                if (overlay && overlay.classList.contains('visible')) {
                    const galleryImgs = Array.from(overlay.querySelectorAll('img')).filter(i => i.dataset.guid == guid);
                    galleryImgs.forEach(imgElem => {
                        const itemWrapper = imgElem.closest('.ebds-gallery-item');
                        if (itemWrapper) itemWrapper.remove();
                    });

                }
            }
        } else if (row.dataset.ebdsHidden === '1') {
            row.style.display = '';
            delete row.dataset.ebdsHidden;
        }
    });
    try {
        if (typeof window.ebdsUpdateGalleryFooter === 'function') window.ebdsUpdateGalleryFooter();
    } catch (e) { }
    try {
        if (typeof window.ebdsRefreshGallery === 'function') window.ebdsRefreshGallery();
    } catch (e) { }
}


(function () {
    'use strict';
    if (!EBDS_ENABLED) { console.log('ebds: disabled - t not in 6000-6999'); return; }

    function isSupportedImageUrl(value) {
        try {
            return /\.(?:jpe?g|gif|png)$/i.test(new URL(value, location.href).pathname);
        } catch (e) {
            return false;
        }
    }

    function createImagePreview(link) {
        let div = document.createElement('div');
        div.classList.add('ebds-preview');
        div.style.display = 'inline-block';
        div.style.margin = '10px';
        div.style.position = 'relative';

        let img = document.createElement('img');
        img.classList.add('ebds-preview-img');
        // Use thumbnail src if available, else the link href
        let thumbSrc = link.querySelector('img') ? link.querySelector('img').src : link.href;
        img.src = thumbSrc;
        img.style.maxWidth = '400px';
        img.style.maxHeight = '400px';
        img.style.display = 'block';
        img.style.cursor = 'pointer';

        // Capture row/guid/cart at creation time so click handler can always find them
        let containingRow = link.closest('tr');
        let capturedGuid = null;
        if (containingRow && containingRow.id && containingRow.id.indexOf('guid') === 0) {
            capturedGuid = containingRow.id.substring(4);
        }
        console.log('Preview created:', link.href, 'row?', !!containingRow, 'guid', capturedGuid);

        // expose a few data attributes so gallery items can add to cart later
        if (capturedGuid) try { img.dataset.guid = capturedGuid; } catch (e) { }
        try { img.dataset.href = link.href; } catch (e) { }
        if (containingRow && containingRow.id) try { img.dataset.rowId = containingRow.id; } catch (e) { };

        // Shift+Left-click on inline preview => hide this prefix for the session
        img.addEventListener('click', function (e) {
            try {
                        if (e.shiftKey && e.button === 0) {
                    e.preventDefault();
                    e.stopImmediatePropagation();
                    const p = derivePrefixFromElement(img);
                    if (p) addHiddenPrefix(p);
                }
            } catch (ex) { }
        });

        // Try to derive a display name and file size from the row (best-effort)
        let displayName = null;
        let displaySize = null;
        let displayPosted = null;
        let detailsHref = null;
        try {
            if (containingRow) {
                // Prefer a non-image link's text for the release name
                const nameAnchor = Array.from(containingRow.querySelectorAll('a')).find(a => a.href !== link.href && !isSupportedImageUrl(a.href) && a.textContent.trim().length > 0);
                if (nameAnchor) displayName = nameAnchor.textContent.trim();
                // Fallback: try the third cell which commonly holds the name
                if (!displayName) {
                    const nameCell = containingRow.querySelector('td:nth-child(3)');
                    if (nameCell) displayName = nameCell.textContent.trim();
                }
                // Size: look for patterns like "700 MB", "1.2 GB", etc.
                const sizeMatch = containingRow.textContent.match(/\b\d+(?:[.,]\d+)?\s*(KB|MB|GB|TB)\b/i);
                if (sizeMatch) displaySize = sizeMatch[0].trim();
                // Posted time: look for td with class "less mid"
                const timeCell = containingRow.querySelector('td.less.mid');
                if (timeCell) displayPosted = timeCell.textContent.trim();
                // Details href: look for a.title
                const titleLink = containingRow.querySelector('a.title');
                if (titleLink) detailsHref = titleLink.href;
            }
        } catch (e) { }
        if (displayName) try { img.dataset.name = displayName; } catch (e) { }
        if (displaySize) try { img.dataset.size = displaySize; } catch (e) { }
        if (displayPosted) try { img.dataset.posted = displayPosted; } catch (e) { }
        if (detailsHref) try { img.dataset.detailsHref = detailsHref; } catch (e) { }

        // Store caption data for the gallery (do not display inline in the listing)
        try {
            const posted = displayPosted || '';
            const name = displayName || '';
            const size = displaySize || '';
            let fullText;
            if (size) {
                const prefix = posted ? posted + ' • ' : '';
                const suffix = ' • ' + size;
                fullText = prefix + name + suffix;
            } else {
                const parts = [posted, name].filter(Boolean);
                fullText = parts.join(' • ');
            }
            if (fullText.trim()) img.title = fullText.trim(); // tooltip only
        } catch (e) { }

        // Create enlarged image container (full-screen modal like before)
        let enlargedContainer = document.createElement('div');
        enlargedContainer.style.position = 'fixed';
        // Make sure this is on top of the gallery overlay and floating buttons
        enlargedContainer.style.zIndex = '2147483660';
        enlargedContainer.style.display = 'none';
        enlargedContainer.style.top = '0';
        enlargedContainer.style.left = '0';
        enlargedContainer.style.width = '100vw';
        enlargedContainer.style.height = '100vh';
        enlargedContainer.style.backgroundColor = 'rgba(0,0,0,0.8)';
        enlargedContainer.style.alignItems = 'center';
        enlargedContainer.style.justifyContent = 'center';

        let enlargedImg = document.createElement('img');
        enlargedImg.style.maxWidth = '90vw';
        enlargedImg.style.maxHeight = '90vh';
        enlargedImg.style.objectFit = 'contain';

        enlargedContainer.appendChild(enlargedImg);
        // mark for global close handling and allow clicking the overlay to close the enlarged preview
        enlargedContainer.classList.add('ebds-enlarged');
        enlargedContainer.addEventListener('click', function (ev) {
            if (ev.target === enlargedContainer) {
                enlargedContainer.style.display = 'none';
            }
        });
        // Append to body so it's not constrained by parent stacking contexts and will appear above overlays
        document.body.appendChild(enlargedContainer);

        // Clicking the thumbnail opens the enlarged preview (click enlarged image to add to cart)
        img.addEventListener('click', function (e) {
            e.preventDefault();
            if (!enlargedImg.getAttribute('src')) enlargedImg.src = link.href;
            enlargedContainer.style.display = 'flex';
            enlargedContainer.dataset.openedAt = String(Date.now());
        });

        // Add Ctrl+hover for lightbox preview
        img.addEventListener('mouseenter', (e) => {
            if (e.ctrlKey || ctrlPressed) openHoverLightbox(img, img.src);
        });
        img.addEventListener('mousemove', (e) => {
            if (e.ctrlKey || ctrlPressed) openHoverLightbox(img, img.src);
            else closeHoverLightbox(img);
        });
        img.addEventListener('mouseleave', () => {
            if (!ctrlPressed) closeHoverLightbox(img);
        });

        // Clicking the enlarged image should add the item to cart (same as cart icon)
        enlargedImg.addEventListener('click', function (e) {
            e.preventDefault();
            // If the enlarged preview was just opened via thumbnail click, ignore the first immediate click to avoid accidental add
            try {
                const openedAt = enlargedContainer.dataset.openedAt ? parseInt(enlargedContainer.dataset.openedAt, 10) : 0;
                if (openedAt && (Date.now() - openedAt) < 300) {
                    delete enlargedContainer.dataset.openedAt;
                    return;
                }
            } catch (ex) { }

            // Hide/disable the original thumbnail immediately to avoid layout/mouse events causing flicker
            try { img.style.visibility = 'hidden'; img.style.pointerEvents = 'none'; } catch (ex) { }

            // Remove the thumbnail wrapper from the listing and update gallery if present
            function restoreOnError(delay) {
                setTimeout(() => {
                    try { img.style.visibility = 'visible'; img.style.pointerEvents = 'auto'; } catch (ex) { }
                }, delay);
            }

            console.log('Preview clicked for', link.href);

            // Determine the GUID using captured values or live DOM as fallback.
            let effectiveRow = containingRow || link.closest('tr') || null;
            if (!effectiveRow) console.log('Preview: no row found via captured or live lookup');

            let guid = capturedGuid || (effectiveRow && effectiveRow.id && effectiveRow.id.indexOf('guid') === 0 ? effectiveRow.id.substring(4) : null);
            if (!guid) {
                console.log('Preview: cannot determine cart or guid for this preview');
                restoreOnError(400);
                return;
            }
            addToCartByGuid(guid, link.href).then(added => {
                if (added) enlargedContainer.remove();
                else restoreOnError(400);
            });
        });

        // Insert thumbnail inline (previous behavior)
        div.appendChild(img);
        link.parentNode.insertBefore(div, link.nextSibling);
    }

    window.hoverLightboxes = new Map();

    // Single shared listener to close all enlarged previews on scroll/resize
    function closeAllEnlarged() {
        document.querySelectorAll('.ebds-enlarged').forEach(el => {
            if (el.style.display !== 'none') el.style.display = 'none';
        });
    }
    ['scroll', 'wheel', 'touchmove'].forEach(evt => {
        window.addEventListener(evt, closeAllEnlarged, { passive: true });
    });
    window.addEventListener('resize', closeAllEnlarged);

    function openHoverLightbox(img, src) {
        if (window.hoverLightboxes.has(img)) return;
        const lb = document.createElement('div');
        lb.className = 'ebds-hover-lb';
        lb.style.position = 'fixed';
        lb.style.inset = '0';
        lb.style.background = 'rgba(0,0,0,0.95)';
        lb.style.display = 'flex';
        lb.style.alignItems = 'center';
        lb.style.justifyContent = 'center';
        lb.style.zIndex = '2147483650';
        const im = document.createElement('img');
        im.src = src;
        applyCtrlHoverPreviewSizing(im);
        lb.appendChild(im);
        im.style.cursor = 'pointer';
        im.title = 'Click to add to cart';
        im.addEventListener('click', (ev) => {
            ev.stopPropagation();
            const guid = img.dataset.guid;
            if (guid) {
                addToCartByGuid(guid, img.src);
                lb.remove();
                window.hoverLightboxes.delete(img);
            }
        });
        lb.addEventListener('click', () => {
            lb.remove();
            window.hoverLightboxes.delete(img);
        });
        document.body.appendChild(lb);
        window.hoverLightboxes.set(img, lb);
    }

    function closeHoverLightbox(img) {
        const lb = window.hoverLightboxes.get(img);
        if (lb) {
            lb.remove();
            window.hoverLightboxes.delete(img);
        }
    }

    function processRows(rows) {
        Array.from(rows).forEach(row => {
            if (row.dataset.ebdsPreviewProcessed === '1') return;
            const imageLinks = Array.from(row.querySelectorAll('a[href]')).filter(link => isSupportedImageUrl(link.href));
            if (imageLinks.length > 0) {
                try { row.dataset.ebdsHasImgLink = '1'; } catch (e) { }
                const preferredLink = imageLinks.find(link => link.querySelector('img')) || imageLinks[0];
                createImagePreview(preferredLink);
            }
            row.dataset.ebdsPreviewProcessed = '1';
            Array.from(row.querySelectorAll('a')).forEach(link => {
                const text = link.textContent.trim();
                if (text === 'Thumbnail' || text === 'Preview') link.remove();
            });
        });
    }

    window.ebdsProcessRows = processRows;
    processRows(document.querySelectorAll('tr'));
})();

(function () {
    'use strict';
    if (!EBDS_ENABLED) return;
    // Inject CSS for gallery button and overlay
    const style = document.createElement('style');
    style.textContent = `
    .ebds-gallery-button{
        position:fixed;
        right:16px;
        bottom:16px;
        transform:none;
        z-index:2147483646;
        background:#0d6efd;
        color:white;
        border:none;
        border-radius:50%;
        width:56px;
        height:56px;
        display:flex;
        align-items:center;
        justify-content:center;
        box-shadow:0 6px 18px rgba(0,0,0,0.25);
        cursor:pointer;
        font-weight:600;
        font-size:14px;
    }
    .ebds-gallery-button.active{
        background:white;
        color:#0d6efd;
    }
    .ebds-config-button{
        z-index:2147483646;
        background:#f0ad4e;
        color:#1b1f24;
        border:none;
        border-radius:10px;
        padding:8px 12px;
        min-width:72px;
        display:inline-flex;
        align-items:center;
        justify-content:center;
        box-shadow:0 6px 18px rgba(0,0,0,0.18);
        cursor:pointer;
        font-weight:700;
        font-size:12px;
        line-height:1.2;
        gap:6px;
    }
    .ebds-config-button.ebds-floating{
        position:fixed;
        top:12px;
        right:16px;
        transform:none;
    }
    .ebds-sab-button{
        position:fixed;
        top:12px;
        right:96px;
        z-index:2147483646;
        background:#6c757d;
        color:white;
        border:none;
        border-radius:10px;
        padding:7px 9px;
        min-width:82px;
        cursor:pointer;
        font-weight:700;
        font-size:11px;
        line-height:1.2;
        box-shadow:0 6px 18px rgba(0,0,0,0.18);
    }
    .ebds-sab-button:disabled{ cursor:wait; opacity:0.8; }
    .ebds-sab-button.ebds-success{ background:#198754; }
    .ebds-sab-button.ebds-error{ background:#dc3545; }
    .ebds-config-panel{
        position:fixed;
        right:16px;
        top:60px;
        bottom:auto;
        width:320px;
        max-width:90vw;
        background:#0f0f0f;
        color:#eee;
        border:1px solid rgba(255,255,255,0.08);
        border-radius:12px;
        padding:12px;
        box-shadow:0 12px 32px rgba(0,0,0,0.55);
        z-index:2147483647;
        display:none;
        max-height:80vh;
        overflow-y:auto;
    }
    .ebds-config-panel.visible{ display:block; }
    .ebds-config-title{ font-weight:700; font-size:14px; margin-bottom:4px; }
    .ebds-config-desc{ color:#b5b5b5; font-size:12px; margin-bottom:8px; }
    .ebds-config-row{ display:flex; gap:8px; }
    .ebds-config-input{ flex:1; padding:7px 8px; border-radius:8px; border:1px solid #2b2b2b; background:#1a1a1a; color:#eee; }
    .ebds-config-add{ background:#0d6efd; color:white; border:none; border-radius:8px; padding:7px 10px; font-weight:700; cursor:pointer; }
    .ebds-chip-list{ display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
    .ebds-chip{ background:#191919; border:1px solid #2d2d2d; padding:6px 8px; border-radius:999px; display:inline-flex; align-items:center; gap:6px; font-size:12px; }
    .ebds-chip button{ background:none; border:none; color:#bbb; cursor:pointer; font-weight:700; }
    .ebds-chip button:hover{ color:#fff; }

    .ebds-config-empty{ color:#888; font-size:12px; }
    .ebds-config-actions{ display:flex; justify-content:flex-end; margin-top:10px; }
    .ebds-config-close{ background:#343a40; color:white; border:none; border-radius:8px; padding:6px 10px; cursor:pointer; }
    .ebds-config-toggle{ display:flex; align-items:center; gap:8px; margin:0; font-size:12px; }
    .ebds-config-toggle input[type="checkbox"]{ width:16px; height:16px; }
    .ebds-config-toggle input[type="range"]{ -webkit-appearance: none; appearance: none; background: #ddd; height: 6px; border-radius: 3px; flex:1; }
    .ebds-config-toggle input[type="range"]::-webkit-slider-thumb{ -webkit-appearance: none; appearance: none; width: 16px; height: 16px; background: #0d6efd; border-radius: 50%; cursor: pointer; }
    .ebds-config-toggle input[type="range"]::-moz-range-thumb{ width: 16px; height: 16px; background: #0d6efd; border-radius: 50%; cursor: pointer; border: none; }
    .ebds-category-toggles{
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 8px;
    }
    #ebds-gallery-overlay{
        position:fixed;
        inset:0;
        z-index:2147483645;
        background:rgba(0,0,0,0.9);
        display:none;
        padding:24px;
        overflow:auto;
    }
    #ebds-gallery-overlay.visible{ display:block; }
    .ebds-gallery-grid{
        display:grid;
        grid-template-columns: repeat(var(--ebds-cols, 3), minmax(0, 1fr));
        gap:16px;
        align-items:start;
    }
    .ebds-gallery-item img{
        width:100%;
        height:auto;
        max-height:500px;
        display:block;
        border-radius:6px;
        box-shadow:0 4px 12px rgba(0,0,0,0.5);
        cursor:pointer;
    }
    .ebds-gallery-placeholder-visual{
        min-height:180px;
        display:flex;
        align-items:center;
        justify-content:center;
        border:1px dashed #555;
        border-radius:6px;
        background:#191919;
        color:#888;
        font-size:14px;
        text-decoration:none;
        cursor:pointer;
    }
    .ebds-gallery-placeholder-visual:hover{ color:#bbb; border-color:#777; }
    .ebds-gallery-item.ebds-temporarily-shown img{
        box-shadow:0 0 0 2px #ff8c00, 0 4px 12px rgba(0,0,0,0.5);
    }
    .ebds-gallery-item.ebds-temporarily-shown .ebds-preview-caption{ color:#ff8c00; }
    .ebds-gallery-item.ebds-temporarily-shown .ebds-gallery-placeholder-visual{
        color:#ff8c00;
        border-color:#ff8c00;
    }
    /* When the vertical limiter is disabled, allow tall images to expand */
    #ebds-gallery-overlay.ebds-gallery-no-vertical-limit .ebds-gallery-item img{
        max-height:none;
    }
    .ebds-gallery-item{
        position:relative;
        min-width:0;
        overflow:hidden;
    }
    .ebds-preview-caption{
        display:block;
        width:100%;
        margin-top:6px;
        font-size:12px;
        color:#ddd;
        max-width:100%;
        overflow:hidden;
        text-overflow:ellipsis;
        white-space:nowrap;
    }
    /* Highlight for posts newer than last visit */
    .ebds-preview-caption.ebds-new-post{
        color: #7ab800; /* green */
    }
    .ebds-gallery-category-links{
        margin-top:0px;
        display:flex;
        justify-content:center;
        gap:10px;
        flex-wrap:wrap;
    }
    .ebds-gallery-category-links a{
        color:#ddd;
        text-decoration:none;
    }
    .ebds-gallery-category-links a:hover{
        color:#fff;
    }
    .ebds-gallery-pagination{
        margin-top:0px;
        display:flex;
        justify-content:center;
        color:#ddd;
    }
    .ebds-gallery-pagination-spacer.ebds-top{ padding:4px 0 16px; }
    .ebds-gallery-pagination-spacer.ebds-bottom{ padding:16px 0 4px; }
    .ebds-gallery-pagination .pagination{ margin:0; }
    .ebds-gallery-pagination a, .ebds-gallery-pagination span { color: #ddd; }
    body.ebds-infinite-active nav.pagination,
    body.ebds-infinite-active nav[aria-label="Pagination"],
    body.ebds-infinite-active ul.pagination{ display:none !important; }
    .ebds-infinite-status{
        display:block;
        width:100%;
        padding:16px;
        border:0;
        background:transparent;
        color:#aaa;
        font-size:12px;
        text-align:center;
        cursor:default;
    }
    .ebds-infinite-status[hidden]{ display:none; }
    .ebds-infinite-status.ebds-retry{ color:#f0a040; cursor:pointer; }
    .ebds-gallery-filter-links{
        padding:16px 0 4px;
        color:#aaa;
        font-size:12px;
        text-align:center;
    }
    .ebds-gallery-filter-links.ebds-header{ padding:4px 0 16px; }
    .ebds-gallery-filter-links a{ color:#f0a040; text-decoration:none; }
    .ebds-gallery-filter-links a:hover{ color:#ffc166; text-decoration:underline; }
    .ebds-gallery-filter-links a.ebds-filter-bypassed{
        color:#ff8c00;
        font-weight:600;
        text-decoration:underline;
    }
    `;
    document.head.appendChild(style);
    document.body.classList.toggle('ebds-infinite-active', EBDS_INFINITE_SCROLL);

    let EBDS_GALLERY_COLS = 3;
    try {
        const storedCols = parseInt(localStorage.getItem('ebdsGalleryCols'), 10);
        if (Number.isFinite(storedCols)) EBDS_GALLERY_COLS = Math.min(10, Math.max(1, storedCols));
    } catch (e) { }

// Gallery vertical limiter: when true, images are constrained to max-height (default: disabled)
let EBDS_GALLERY_VERTICAL_LIMIT = false;
try {
    const v = localStorage.getItem('ebdsGalleryVerticalLimit');
    if (v !== null) EBDS_GALLERY_VERTICAL_LIMIT = (v === '1');
} catch (e) { }

// Option to only show new items in gallery (default false)
let EBDS_GALLERY_SHOW_ONLY_NEW = false;
try { EBDS_GALLERY_SHOW_ONLY_NEW = (localStorage.getItem('ebdsGalleryShowOnlyNew') === '1'); } catch (e) { }

    let EBDS_SESSION_EXPIRATION_MINUTES = 5;
    try {
        const storedMinutes = parseInt(localStorage.getItem(EBDS_SESSION_EXPIRATION_KEY), 10);
        if (Number.isFinite(storedMinutes)) EBDS_SESSION_EXPIRATION_MINUTES = Math.min(1440, Math.max(1, storedMinutes));
    } catch (e) { }

    const categoryLinks = [
        { id: 'todaysTop', href: '/browse?t=6000&top=1', title: 'Todays Top Grabs', text: 'Todays Top Grabs' },
        { id: 'allXxx', href: '/browse?t=6000', text: 'All XXX' },
        { id: 'dvd', href: '/browse?t=6010', text: 'DVD' },
        { id: 'hd', href: '/browse?t=6040', text: 'HD' },
        { id: 'other', href: '/browse?t=6999', text: 'Other' },
        { id: 'packs', href: '/browse?t=6070', text: 'Packs' },
        { id: 'sd', href: '/browse?t=6080', text: 'SD' },
        { id: 'uhd', href: '/browse?t=6045', text: 'UHD' },
        { id: 'vr', href: '/browse?t=6050', text: 'VR' },
        { id: 'wmv', href: '/browse?t=6020', text: 'WMV' },
        { id: 'xvid', href: '/browse?t=6030', text: 'XviD' }
    ];

    // Create floating button
    const btn = document.createElement('button');
    btn.className = 'ebds-gallery-button';
    btn.title = 'Open image gallery (G)';
    btn.textContent = 'Gallery';
    document.body.appendChild(btn);

    // Configuration button and panel for blacklist keywords
    const configBtn = document.createElement('button');
    configBtn.className = 'ebds-config-button ebds-floating';
    configBtn.title = 'Configure EBDS';
    configBtn.textContent = 'Config';
    document.body.appendChild(configBtn);

    const sabBtn = document.createElement('button');
    sabBtn.className = 'ebds-sab-button';
    sabBtn.hidden = true;
    sabBtn.title = 'Tell SABnzbd to read and process all RSS feeds now';
    sabBtn.textContent = 'Read feeds';
    document.body.appendChild(sabBtn);

    const configPanel = document.createElement('div');
    configPanel.className = 'ebds-config-panel';
    configPanel.innerHTML = `
        <div class="ebds-config-title">Keyboard shortcuts</div>
        <div class="ebds-config-desc">G: Gallery | ←/→: Navigate | Esc: Close | Ctrl-hover: Preview</div>
        <div class="ebds-config-toggle">
            <input class="ebds-infinite-scroll-toggle" type="checkbox" id="ebdsInfiniteScrollToggle">
            <label for="ebdsInfiniteScrollToggle">Load the next page while scrolling (infinite scrolling Beta)</label>
        </div>
        <div class="ebds-config-toggle">
            <input class="ebds-gallery-only-new-toggle" type="checkbox" id="ebdsGalleryOnlyNewToggle">
            <label for="ebdsGalleryOnlyNewToggle">Show only new items in gallery</label>
        </div>
        <div class="ebds-config-toggle">
            <input class="ebds-hide-nopics-toggle" type="checkbox" id="ebdsHideNoPicsToggle">
            <label for="ebdsHideNoPicsToggle">Hide items without pictures</label>
        </div>
        <div class="ebds-config-toggle">
            <label for="ebdsMinFileSizeInput">Minimum filesize (MB, 0 disables)</label>
            <input id="ebdsMinFileSizeInput" type="number" min="0" step="0.01" value="0" style="width:90px; padding:4px;">
            <button id="ebdsMinFileSizeSet" type="button" class="ebds-config-add" style="padding:6px 8px;">Set</button>
        </div>
        <div class="ebds-config-toggle">
            <input class="ebds-vertical-limit-toggle" type="checkbox" id="ebdsVerticalLimitToggle">
            <label for="ebdsVerticalLimitToggle">Limit gallery image height</label>
        </div>
        <div class="ebds-config-toggle">
            <input class="ebds-scale-hover-toggle" type="checkbox" id="ebdsScaleHoverToggle">
            <label for="ebdsScaleHoverToggle">Scale Ctrl-hover preview to browser size</label>
        </div>
        <div class="ebds-config-toggle">
            <label for="ebdsFilterLinksPosition">Filter count links</label>
            <select id="ebdsFilterLinksPosition">
                <option value="footer">Footer</option>
                <option value="header">Header</option>
                <option value="hidden">Not shown</option>
            </select>
        </div>
        <div class="ebds-config-toggle">
            <label for="ebdsGalleryCols">Gallery columns: <span id="ebdsGalleryColsValue">3</span></label>
            <input type="range" id="ebdsGalleryCols" min="1" max="10" value="3">
        </div>
        <div class="ebds-config-toggle">
            <label for="ebdsSessionExpirationInput">Session expiration (mins): <span id="ebdsSessionExpirationValue">5</span></label>
            <input type="number" id="ebdsSessionExpirationInput" min="1" max="1440" value="5" style="width:80px; padding:4px;">
            <button id="ebdsSessionExpirationSet" type="button" class="ebds-config-add" style="margin-left:8px; padding:6px 8px;">Set</button>
        </div>
        <div class="ebds-config-title" style="margin-top:10px;">SABnzbd</div>
        <div class="ebds-config-desc">Credentials are stored by the userscript manager.</div>
        <div class="ebds-config-toggle" style="margin-bottom:8px;">
            <input class="ebds-hide-sab-button-toggle" type="checkbox" id="ebdsHideSabButtonToggle">
            <label for="ebdsHideSabButtonToggle">Hide Read feeds button</label>
        </div>
        <div class="ebds-config-row" style="margin-bottom:8px;">
            <input id="ebdsSabUrl" class="ebds-config-input" type="url" inputmode="url" autocomplete="off" placeholder="https://sabnzbd.example.com" aria-label="SABnzbd URL">
        </div>
        <div class="ebds-config-row">
            <input id="ebdsSabApiKey" class="ebds-config-input" type="password" autocomplete="new-password" data-bwignore="true" placeholder="SABnzbd API key" aria-label="SABnzbd API key">
            <button id="ebdsSabSave" class="ebds-config-add" type="button">Save</button>
        </div>
        <div id="ebdsSabStatus" class="ebds-config-desc" style="margin-top:6px; margin-bottom:0;"></div>
        <div class="ebds-config-title">Exclude keywords</div>
        <div class="ebds-config-desc">Release titles containing these terms will be hidden.</div>
        <div class="ebds-config-row">
            <input class="ebds-config-input ebds-blacklist-input" type="text" placeholder="Add keyword..." aria-label="Add keyword to exclude">
            <button class="ebds-config-add ebds-blacklist-add" type="button">Add</button>
        </div>
        <div class="ebds-chip-list"></div>


        <div class="ebds-config-actions"><button class="ebds-config-close" type="button">Close</button></div>
    `;
    document.body.appendChild(configPanel);

    const categoryLinksDiv = document.createElement('div');
    categoryLinksDiv.innerHTML = '<div class="ebds-config-title">Category links visibility</div><div class="ebds-config-desc">Choose which category links to show in the gallery.</div><div class="ebds-category-toggles"></div>';
    configPanel.appendChild(categoryLinksDiv);

    const actions = configPanel.querySelector('.ebds-config-actions');
    if (actions) {
        configPanel.insertBefore(categoryLinksDiv, actions);
    }

    const EBDS_LINK_VISIBILITY_KEY = 'ebdsCategoryLinksVisibility';

    function loadLinkVisibility() {
        try {
            const stored = JSON.parse(localStorage.getItem(EBDS_LINK_VISIBILITY_KEY) || '{}');
            return stored;
        } catch (e) {
            return {};
        }
    }

    function persistLinkVisibility(vis) {
        try {
            localStorage.setItem(EBDS_LINK_VISIBILITY_KEY, JSON.stringify(vis));
        } catch (e) {}
    }

    let EBDS_LINK_VISIBILITY = loadLinkVisibility();

    categoryLinks.forEach(link => {
        const toggleDiv = document.createElement('div');
        toggleDiv.className = 'ebds-config-toggle';
        const input = document.createElement('input');
        input.type = 'checkbox';
        input.id = 'ebdsLink' + link.id;
        input.checked = EBDS_LINK_VISIBILITY[link.id] !== false;
        input.addEventListener('change', () => {
            EBDS_LINK_VISIBILITY[link.id] = input.checked;
            persistLinkVisibility(EBDS_LINK_VISIBILITY);
        });
        const label = document.createElement('label');
        label.htmlFor = input.id;
        label.textContent = link.text;
        toggleDiv.appendChild(input);
        toggleDiv.appendChild(label);
        categoryLinksDiv.querySelector('.ebds-category-toggles').appendChild(toggleDiv);
    });

    const blacklistInput = configPanel.querySelector('.ebds-blacklist-input');
    const blacklistList = configPanel.querySelector('.ebds-chip-list');
    const addBlacklistBtn = configPanel.querySelector('.ebds-blacklist-add');
    const closeConfigBtn = configPanel.querySelector('.ebds-config-close');
    const hidePicsToggle = configPanel.querySelector('.ebds-hide-nopics-toggle');
    const minFileSizeInput = configPanel.querySelector('#ebdsMinFileSizeInput');
    const minFileSizeSetBtn = configPanel.querySelector('#ebdsMinFileSizeSet');
    const verticalLimitToggle = configPanel.querySelector('.ebds-vertical-limit-toggle');
    const scaleHoverToggle = configPanel.querySelector('.ebds-scale-hover-toggle');
    const filterLinksPositionSelect = configPanel.querySelector('#ebdsFilterLinksPosition');
    const galleryOnlyNewToggle = configPanel.querySelector('.ebds-gallery-only-new-toggle');
    const infiniteScrollToggle = configPanel.querySelector('.ebds-infinite-scroll-toggle');
    const sabUrlInput = configPanel.querySelector('#ebdsSabUrl');
    const sabApiKeyInput = configPanel.querySelector('#ebdsSabApiKey');
    const sabSaveBtn = configPanel.querySelector('#ebdsSabSave');
    const sabStatus = configPanel.querySelector('#ebdsSabStatus');
    const hideSabButtonToggle = configPanel.querySelector('.ebds-hide-sab-button-toggle');

    function loadSabConfig() {
        try {
            return {
                url: String(GM_getValue(EBDS_SAB_URL_KEY, '') || ''),
                apiKey: String(GM_getValue(EBDS_SAB_API_KEY, '') || '')
            };
        } catch (e) {
            return { url: '', apiKey: '' };
        }
    }

    function normalizeSabUrl(rawUrl) {
        const url = new URL(String(rawUrl || '').trim());
        if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error('URL must use HTTP or HTTPS');
        url.username = '';
        url.password = '';
        url.search = '';
        url.hash = '';
        url.pathname = url.pathname.replace(/\/+$/, '');
        return url.toString().replace(/\/$/, '');
    }

    function setSabStatus(message, isError = false) {
        if (!sabStatus) return;
        sabStatus.textContent = message;
        sabStatus.style.color = isError ? '#ff7b7b' : '#7ab800';
    }

    function updateSabButtonVisibility(config = loadSabConfig()) {
        sabBtn.hidden = EBDS_HIDE_SAB_BUTTON || !(config.url && config.apiKey);
    }

    function buildSabApiUrl(config, params) {
        const apiUrl = new URL(config.url);
        apiUrl.pathname = apiUrl.pathname.replace(/\/+$/, '');
        if (!apiUrl.pathname.endsWith('/api')) apiUrl.pathname += '/api';
        apiUrl.search = '';
        apiUrl.searchParams.set('output', 'json');
        apiUrl.searchParams.set('apikey', config.apiKey);
        Object.entries(params).forEach(([key, value]) => apiUrl.searchParams.set(key, String(value)));
        return apiUrl;
    }

    function requestSab(config, params) {
        return new Promise((resolve, reject) => {
            let apiUrl;
            try {
                apiUrl = buildSabApiUrl(config, params);
            } catch (e) {
                reject(new Error('Invalid SABnzbd URL'));
                return;
            }

            GM_xmlhttpRequest({
                method: 'GET',
                url: apiUrl.toString(),
                timeout: 15000,
                responseType: 'json',
                onload: response => {
                    let data = response.response;
                    if (!data && response.responseText) {
                        try { data = JSON.parse(response.responseText); } catch (e) { }
                    }
                    if (response.status < 200 || response.status >= 300) {
                        reject(new Error(data && data.error ? data.error : `HTTP ${response.status || 'error'}`));
                        return;
                    }
                    if (data && data.error) {
                        reject(new Error(data.error));
                        return;
                    }
                    resolve(data);
                },
                ontimeout: () => reject(new Error('SABnzbd request timed out')),
                onerror: () => reject(new Error('Unable to connect to SABnzbd'))
            });
        });
    }

    async function saveSabConfig() {
        sabSaveBtn.disabled = true;
        sabSaveBtn.textContent = 'Testing…';
        try {
            const url = normalizeSabUrl(sabUrlInput.value);
            const existingConfig = loadSabConfig();
            const apiKey = sabApiKeyInput.value.trim() || existingConfig.apiKey;
            if (!apiKey) throw new Error('API key is required');
            const config = { url, apiKey };
            const response = await requestSab(config, { mode: 'queue', start: 0, limit: 1 });
            if (!response || !response.queue) throw new Error('Unexpected response from SABnzbd');
            await GM_setValue(EBDS_SAB_URL_KEY, url);
            await GM_setValue(EBDS_SAB_API_KEY, apiKey);
            sabUrlInput.value = url;
            sabApiKeyInput.value = '';
            sabApiKeyInput.placeholder = 'API key saved';
            updateSabButtonVisibility(config);
            setSabStatus('Connection verified and saved.');
            return true;
        } catch (e) {
            setSabStatus(e.message || 'Unable to verify SABnzbd settings.', true);
            return false;
        } finally {
            sabSaveBtn.disabled = false;
            sabSaveBtn.textContent = 'Save';
        }
    }

    function setSabButtonState(label, state = '') {
        sabBtn.textContent = label;
        sabBtn.classList.remove('ebds-success', 'ebds-error');
        if (state) sabBtn.classList.add(state);
    }

    async function triggerSabFeeds() {
        const config = loadSabConfig();
        if (!config.url || !config.apiKey) {
            updateSabButtonVisibility(config);
            return;
        }

        sabBtn.disabled = true;
        setSabButtonState('Reading…');
        try {
            const response = await requestSab(config, { mode: 'rss_now' });
            if (!response || response.status !== true) throw new Error('SABnzbd did not accept the request');
            setSabButtonState('Feeds requested', 'ebds-success');
            setTimeout(() => setSabButtonState('Read feeds'), 2500);
        } catch (e) {
            setSabButtonState('SAB error', 'ebds-error');
            setSabStatus(e.message || 'SABnzbd request failed.', true);
            setTimeout(() => setSabButtonState('Read feeds'), 3000);
        } finally {
            sabBtn.disabled = false;
        }
    }

    const initialSabConfig = loadSabConfig();
    sabUrlInput.value = initialSabConfig.url;
    if (initialSabConfig.apiKey) sabApiKeyInput.placeholder = 'API key saved';
    updateSabButtonVisibility(initialSabConfig);
    sabSaveBtn.addEventListener('click', saveSabConfig);
    sabApiKeyInput.addEventListener('keydown', e => {
        if (e.key === 'Enter') saveSabConfig();
    });
    sabBtn.addEventListener('click', triggerSabFeeds);
    if (hideSabButtonToggle) {
        hideSabButtonToggle.checked = EBDS_HIDE_SAB_BUTTON;
        hideSabButtonToggle.addEventListener('change', () => {
            EBDS_HIDE_SAB_BUTTON = !!hideSabButtonToggle.checked;
            try { localStorage.setItem(EBDS_HIDE_SAB_BUTTON_KEY, EBDS_HIDE_SAB_BUTTON ? '1' : '0'); } catch (e) { }
            updateSabButtonVisibility();
        });
    }

    // Apply vertical limiter state to overlay
    function applyGalleryVerticalLimit() {
        try {
            if (!overlay) return;
            if (EBDS_GALLERY_VERTICAL_LIMIT) overlay.classList.remove('ebds-gallery-no-vertical-limit');
            else overlay.classList.add('ebds-gallery-no-vertical-limit');
        } catch (e) { }
    }

    // initialize toggle states and bind events

    if (hidePicsToggle) {
        hidePicsToggle.checked = EBDS_HIDE_WITHOUT_PICTURES;
        hidePicsToggle.addEventListener('change', () => {
            EBDS_HIDE_WITHOUT_PICTURES = !!hidePicsToggle.checked;
            EBDS_TEMPORARY_FILTER_BYPASS.withoutPictures = false;
            try { localStorage.setItem(EBDS_HIDE_WITHOUT_PICS_STORAGE_KEY, EBDS_HIDE_WITHOUT_PICTURES ? '1' : '0'); } catch (e) { }
            applyRowFiltering();
        });
    }

    if (minFileSizeInput && minFileSizeSetBtn) {
        minFileSizeInput.value = String(EBDS_MIN_FILE_SIZE_MB);
        function setMinimumFileSize() {
            const value = Number(minFileSizeInput.value);
            if (!Number.isFinite(value) || value < 0) {
                minFileSizeInput.setCustomValidity('Enter a value of 0 or greater.');
                minFileSizeInput.reportValidity();
                return;
            }
            minFileSizeInput.setCustomValidity('');
            EBDS_MIN_FILE_SIZE_MB = value;
            EBDS_TEMPORARY_FILTER_BYPASS.minimumSize = false;
            minFileSizeInput.value = String(value);
            try { localStorage.setItem(EBDS_MIN_FILE_SIZE_MB_KEY, String(value)); } catch (e) { }
            applyRowFiltering();
        }
        minFileSizeSetBtn.addEventListener('click', setMinimumFileSize);
        minFileSizeInput.addEventListener('keydown', e => {
            if (e.key === 'Enter') setMinimumFileSize();
        });
    }

    if (verticalLimitToggle) {
        verticalLimitToggle.checked = EBDS_GALLERY_VERTICAL_LIMIT;
        verticalLimitToggle.addEventListener('change', () => {
            EBDS_GALLERY_VERTICAL_LIMIT = !!verticalLimitToggle.checked;
            try { localStorage.setItem('ebdsGalleryVerticalLimit', EBDS_GALLERY_VERTICAL_LIMIT ? '1' : '0'); } catch (e) { }
            applyGalleryVerticalLimit();
        });
    }

    if (scaleHoverToggle) {
        scaleHoverToggle.checked = EBDS_SCALE_HOVER_PREVIEW;
        scaleHoverToggle.addEventListener('change', () => {
            EBDS_SCALE_HOVER_PREVIEW = !!scaleHoverToggle.checked;
            try { localStorage.setItem(EBDS_SCALE_HOVER_PREVIEW_KEY, EBDS_SCALE_HOVER_PREVIEW ? '1' : '0'); } catch (e) { }
        });
    }

    if (filterLinksPositionSelect) {
        filterLinksPositionSelect.value = EBDS_FILTER_LINKS_POSITION;
        filterLinksPositionSelect.addEventListener('change', () => {
            EBDS_FILTER_LINKS_POSITION = filterLinksPositionSelect.value;
            try { localStorage.setItem(EBDS_FILTER_LINKS_POSITION_KEY, EBDS_FILTER_LINKS_POSITION); } catch (e) { }
            updateGalleryFooter();
        });
    }

    if (galleryOnlyNewToggle) {
        galleryOnlyNewToggle.checked = EBDS_GALLERY_SHOW_ONLY_NEW;
        const galleryOnlyNewContainer = galleryOnlyNewToggle.closest('.ebds-config-toggle');
        if (galleryOnlyNewContainer && EBDS_IS_SEARCH_PAGE) galleryOnlyNewContainer.style.display = 'none';
        galleryOnlyNewToggle.addEventListener('change', () => {
            EBDS_GALLERY_SHOW_ONLY_NEW = !!galleryOnlyNewToggle.checked;
            try { localStorage.setItem('ebdsGalleryShowOnlyNew', EBDS_GALLERY_SHOW_ONLY_NEW ? '1' : '0'); } catch (e) { }
            try { if (overlay && overlay.classList && overlay.classList.contains('visible')) openGallery(); } catch (e) { }
            try {
                if (typeof window.ebdsRefreshInfiniteScroll === 'function') window.ebdsRefreshInfiniteScroll();
            } catch (e) { }
        });
    }
    if (infiniteScrollToggle) {
        infiniteScrollToggle.checked = EBDS_INFINITE_SCROLL;
        infiniteScrollToggle.addEventListener('change', () => {
            EBDS_INFINITE_SCROLL = !!infiniteScrollToggle.checked;
            try { localStorage.setItem(EBDS_INFINITE_SCROLL_KEY, EBDS_INFINITE_SCROLL ? '1' : '0'); } catch (e) { }
            document.body.classList.toggle('ebds-infinite-active', EBDS_INFINITE_SCROLL);
            try {
                if (typeof window.ebdsSetInfiniteScrollEnabled === 'function') {
                    window.ebdsSetInfiniteScrollEnabled(EBDS_INFINITE_SCROLL);
                }
            } catch (e) { }
            try { if (overlay.classList.contains('visible')) openGallery(); } catch (e) { }
        });
    }

    const galleryColsSlider = configPanel.querySelector('#ebdsGalleryCols');
    const galleryColsValue = configPanel.querySelector('#ebdsGalleryColsValue');
    if (galleryColsSlider && galleryColsValue) {
        galleryColsSlider.value = EBDS_GALLERY_COLS;
        galleryColsValue.textContent = EBDS_GALLERY_COLS;
        galleryColsSlider.addEventListener('input', () => {
            const val = parseInt(galleryColsSlider.value);
            EBDS_GALLERY_COLS = val;
            galleryColsValue.textContent = val;
            try { localStorage.setItem('ebdsGalleryCols', val.toString()); } catch (e) { }
            // Update grid columns
            grid.style.setProperty('--ebds-cols', val);
        });
    }

    // Session expiration minutes control
    const sessionExpirationInput = configPanel.querySelector('#ebdsSessionExpirationInput');
    const sessionExpirationValue = configPanel.querySelector('#ebdsSessionExpirationValue');
    const sessionExpirationSetBtn = configPanel.querySelector('#ebdsSessionExpirationSet');
    if (sessionExpirationInput && sessionExpirationValue) {
        sessionExpirationInput.value = EBDS_SESSION_EXPIRATION_MINUTES;
        sessionExpirationValue.textContent = EBDS_SESSION_EXPIRATION_MINUTES;
        function applySessionExpiration() {
            let m = parseInt(sessionExpirationInput.value, 10);
            if (!Number.isFinite(m) || m < 1) m = 1;
            if (m > 1440) m = 1440;
            EBDS_SESSION_EXPIRATION_MINUTES = m;
            sessionExpirationInput.value = m;
            sessionExpirationValue.textContent = m;
            try { localStorage.setItem(EBDS_SESSION_EXPIRATION_KEY, String(m)); } catch (e) { }
        }
        sessionExpirationSetBtn && sessionExpirationSetBtn.addEventListener('click', applySessionExpiration);
        sessionExpirationInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') applySessionExpiration(); });
        sessionExpirationInput.addEventListener('blur', applySessionExpiration);
    }


    function renderBlacklistChips() {
        blacklistList.innerHTML = '';
        if (!EBDS_BLACKLIST_TERMS.length) {
            const empty = document.createElement('div');
            empty.className = 'ebds-config-empty';
            empty.textContent = 'No keywords yet.';
            blacklistList.appendChild(empty);
            return;
        }

        EBDS_BLACKLIST_TERMS.forEach(term => {
            const chip = document.createElement('div');
            chip.className = 'ebds-chip';
            const label = document.createElement('span');
            label.textContent = term;
            const removeBtn = document.createElement('button');
            removeBtn.type = 'button';
            removeBtn.title = 'Remove keyword';
            removeBtn.textContent = 'x';
            removeBtn.addEventListener('click', () => {
                EBDS_BLACKLIST_TERMS = EBDS_BLACKLIST_TERMS.filter(t => t !== term);
                EBDS_TEMPORARY_FILTER_BYPASS.keywords = false;
                persistBlacklistTerms(EBDS_BLACKLIST_TERMS);
                applyRowFiltering();
                renderBlacklistChips();
            });
            chip.appendChild(label);
            chip.appendChild(removeBtn);
            blacklistList.appendChild(chip);
        });
    }

    function addBlacklistTerm(raw) {
        const normalized = normalizeBlacklistTerm(raw);
        if (!normalized) return;
        if (!EBDS_BLACKLIST_TERMS.includes(normalized)) {
            EBDS_BLACKLIST_TERMS.push(normalized);
            EBDS_TEMPORARY_FILTER_BYPASS.keywords = false;
            persistBlacklistTerms(EBDS_BLACKLIST_TERMS);
            applyRowFiltering();
            renderBlacklistChips();
        }
    }

    addBlacklistBtn.addEventListener('click', () => {
        addBlacklistTerm(blacklistInput.value);
        blacklistInput.value = '';
        blacklistInput.focus();
    });

    blacklistInput.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            addBlacklistTerm(blacklistInput.value);
            blacklistInput.value = '';
            blacklistInput.focus();
        }
    });



    configBtn.addEventListener('click', () => {
        const nowVisible = !configPanel.classList.contains('visible');
        configPanel.classList.toggle('visible');
        if (nowVisible) {
            renderBlacklistChips();
            if (hidePicsToggle) hidePicsToggle.checked = EBDS_HIDE_WITHOUT_PICTURES;
            if (verticalLimitToggle) verticalLimitToggle.checked = EBDS_GALLERY_VERTICAL_LIMIT;
            if (scaleHoverToggle) scaleHoverToggle.checked = EBDS_SCALE_HOVER_PREVIEW;
            if (filterLinksPositionSelect) filterLinksPositionSelect.value = EBDS_FILTER_LINKS_POSITION;
            if (galleryOnlyNewToggle) galleryOnlyNewToggle.checked = EBDS_GALLERY_SHOW_ONLY_NEW;
            if (infiniteScrollToggle) infiniteScrollToggle.checked = EBDS_INFINITE_SCROLL;
            if (hideSabButtonToggle) hideSabButtonToggle.checked = EBDS_HIDE_SAB_BUTTON;
            try { blacklistInput.focus(); } catch (e) { }
        }
    });

    closeConfigBtn.addEventListener('click', () => {
        configPanel.classList.remove('visible');
    });

    // Keep Config button floating on the right side of the viewport

    // Create overlay
    const overlay = document.createElement('div');
    overlay.id = 'ebds-gallery-overlay';
    const grid = document.createElement('div');
    grid.className = 'ebds-gallery-grid';
    overlay.appendChild(grid);
    document.body.appendChild(overlay);
    // Ensure the vertical limiter state is applied now.
    try { applyGalleryVerticalLimit(); } catch (e) { }

    // Quick state for gallery hover lightboxes
    let galleryHoverLightboxes = new Map();
    let currentHoveredGalleryItem = null;
    // Track whether Control is currently pressed so we don't accidentally close the hover lightbox while it's held
    // using global ctrlPressed

    // preserve previous display value of the main browse container so we can hide/show it
    let browseContainerPrevDisplay = null;

    // Set initial columns
    grid.style.setProperty('--ebds-cols', EBDS_GALLERY_COLS);

    // --- EBDS: per-category session and saved state helpers ---
    const EBDS_SESSION_PREFIX = 'ebdSession_';
    const EBDS_SAVED_PREFIX = 'ebdSaved_';
    const EBDS_SAVED_EXPIRED_PREFIX = 'ebdSavedExpired_';
    const EBDS_BASELINE_PENDING_PREFIX = 'ebdBaselinePending_';

    function getCategoryIdFromUrl() {
        try {
            const params = new URLSearchParams(location.search);
            const t = params.get('t');
            if (t) {
                const top = params.get('top');
                const match = categoryLinks.find(link => {
                    const linkUrl = new URL(link.href, location.origin);
                    if (linkUrl.searchParams.get('t') !== t) return false;
                    return top === '1' ? linkUrl.searchParams.get('top') === '1' : !linkUrl.searchParams.has('top');
                });
                if (match) return match.id;
            }
            // Fallback: try to match a visible category link by pathname/search
            const href = location.pathname + (location.search || '');
            const match = categoryLinks.find(l => href.indexOf(l.href) !== -1 || l.href.indexOf(href) !== -1);
            if (match) return match.id;
        } catch (e) { }
        return null;
    }

    function parseRowTitleEpoch(row) {
        try {
            if (!row) return null;
            // Look for the first time cell with a title attribute: <td class="less mid" title="YYYY-MM-DD HH:MM:SS">...
            const timeCell = row.querySelector('td.less.mid[title]');
            if (!timeCell) return null;
            const t = timeCell.getAttribute('title');
            if (!t) return null;
            // Convert to ISO-ish form to be parsed as local datetime: replace first space with 'T'
            const iso = t.replace(' ', 'T');
            const d = new Date(iso);
            const ms = d.getTime();
            return Number.isFinite(ms) ? ms : null;
        } catch (e) { return null; }
    }

    function getSessionTimestamp(catId) {
        try {
            if (!catId) return 0;
            const v = localStorage.getItem(EBDS_SESSION_PREFIX + catId);
            if (!v) return 0;
            const n = parseInt(v, 10);
            return Number.isFinite(n) ? n : 0;
        } catch (e) { return 0; }
    }

    function setSessionTimestamp(catId, epochMs) {
        try {
            if (!catId || !epochMs) return;
            localStorage.setItem(EBDS_SESSION_PREFIX + catId, String(Number(epochMs)));
        } catch (e) { }
    }

    function getSavedTimestamp(catId) {
        try {
            if (!catId) return 0;
            const v = localStorage.getItem(EBDS_SAVED_PREFIX + catId);
            if (!v) return 0;
            const n = parseInt(v, 10);
            return Number.isFinite(n) ? n : 0;
        } catch (e) { return 0; }
    }

    function setSavedTimestamp(catId, epochMs) {
        try {
            if (!catId || !epochMs) return;
            localStorage.setItem(EBDS_SAVED_PREFIX + catId, String(Number(epochMs)));
        } catch (e) { }
    }

    function getSavedExpiredTimestamp(catId) {
        try {
            if (!catId) return 0;
            const v = localStorage.getItem(EBDS_SAVED_EXPIRED_PREFIX + catId);
            if (!v) return 0;
            const n = parseInt(v, 10);
            return Number.isFinite(n) ? n : 0;
        } catch (e) { return 0; }
    }

    function setSavedExpiredTimestamp(catId, epochMs) {
        try {
            if (!catId || !epochMs) return;
            localStorage.setItem(EBDS_SAVED_EXPIRED_PREFIX + catId, String(Number(epochMs)));
        } catch (e) { }
    }

    function getLastVisitTimestamp(catId) {
        // For highlighting, use ebdSavedExpired
        return getSavedExpiredTimestamp(catId);
    }

    function setBaselinePending(catId, pending) {
        try {
            const key = EBDS_BASELINE_PENDING_PREFIX + catId;
            if (pending) localStorage.setItem(key, '1');
            else localStorage.removeItem(key);
        } catch (e) { }
    }

    function isBaselinePending(catId) {
        try { return localStorage.getItem(EBDS_BASELINE_PENDING_PREFIX + catId) === '1'; }
        catch (e) { return false; }
    }

    function saveFirstPageBaseline(catId) {
        const firstRow = document.querySelector('tr[id^="guid"]');
        const epoch = firstRow ? parseRowTitleEpoch(firstRow) : null;
        if (!epoch) return false;
        setSavedTimestamp(catId, epoch);
        setBaselinePending(catId, false);
        return true;
    }

    function updateUiSessionOnPageLoad() {
        try {
            const now = Date.now();
            const expirationMs = EBDS_SESSION_EXPIRATION_MINUTES * 60 * 1000;
            const previous = parseInt(localStorage.getItem(EBDS_UI_SESSION_KEY), 10);
            if (!Number.isFinite(previous) || (now - previous) >= expirationMs) {
                clearClickedGuids();
                clearHiddenPrefixes();
            }
            localStorage.setItem(EBDS_UI_SESSION_KEY, String(now));
        } catch (e) { }
    }

    function updateLastVisitFromPageOnLoad() {
        try {
            if (EBDS_IS_SEARCH_PAGE) return;
            const catId = getCategoryIdFromUrl();
            if (!catId) return;
            const params = new URLSearchParams(location.search);
            const offset = params.get('offset');
            const now = Date.now();
            const sessionExpirationMs = EBDS_SESSION_EXPIRATION_MINUTES * 60 * 1000;
            const lastSession = getSessionTimestamp(catId);
            const sessionExpired = !lastSession || (now - lastSession) >= sessionExpirationMs;

            if (sessionExpired) {
                // Session expired: backup ebdSaved to ebdSavedExpired if exists
                const currentSaved = getSavedTimestamp(catId);
                if (currentSaved) {
                    setSavedExpiredTimestamp(catId, currentSaved);
                }
                // Update session timestamp
                setSessionTimestamp(catId, now);
                // If on first page, save the first item's timestamp to ebdSaved
                if (!offset || offset === '0') {
                    if (!saveFirstPageBaseline(catId)) setBaselinePending(catId, true);
                } else {
                    setBaselinePending(catId, true);
                }
            } else {
                // Session not expired: just update session timestamp
                setSessionTimestamp(catId, now);
                if ((!offset || offset === '0') && isBaselinePending(catId)) {
                    saveFirstPageBaseline(catId);
                }
            }
        } catch (e) { }
    }

    function updateSessionActivityForInfiniteScroll() {
        updateUiSessionOnPageLoad();
        if (EBDS_IS_SEARCH_PAGE) return;
        const catId = getCategoryIdFromUrl();
        if (!catId) return;
        const now = Date.now();
        const lastSession = getSessionTimestamp(catId);
        const expirationMs = EBDS_SESSION_EXPIRATION_MINUTES * 60 * 1000;
        if (!lastSession || (now - lastSession) >= expirationMs) {
            const currentSaved = getSavedTimestamp(catId);
            if (currentSaved) setSavedExpiredTimestamp(catId, currentSaved);
            // Loaded pages may be older, so defer the new baseline until page one is visited.
            setBaselinePending(catId, true);
        }
        setSessionTimestamp(catId, now);
    }

    function getGalleryOmissionCounts() {
        const counts = { withoutPictures: 0, excludedByKeywords: 0, belowMinimumSize: 0 };
        const currentCategoryId = getCategoryIdFromUrl();
        const lastVisit = currentCategoryId ? getLastVisitTimestamp(currentCategoryId) : 0;
        const showOnlyNew = EBDS_GALLERY_SHOW_ONLY_NEW && !EBDS_IS_SEARCH_PAGE && lastVisit > 0;

        document.querySelectorAll('tr[id^="guid"]').forEach(row => {
            try {
                if (showOnlyNew) {
                    const itemEpoch = parseRowTitleEpoch(row);
                    if (!(itemEpoch && itemEpoch > lastVisit)) return;
                }

                const titleText = getReleaseTitleText(row);
                const blacklisted = EBDS_BLACKLIST_TERMS.some(term => term && titleText.includes(term));
                const hasOriginalImage = row.dataset.ebdsHasImgLink === '1';
                const missingOriginalImage = EBDS_HIDE_WITHOUT_PICTURES && !hasOriginalImage;
                const fileSizeMb = parseFileSizeMb(row.textContent);
                const belowMinimumSize = EBDS_MIN_FILE_SIZE_MB > 0 && fileSizeMb !== null && fileSizeMb < EBDS_MIN_FILE_SIZE_MB;

                if (missingOriginalImage) counts.withoutPictures++;
                if (blacklisted) counts.excludedByKeywords++;
                if (belowMinimumSize) counts.belowMinimumSize++;
            } catch (e) { }
        });

        return counts;
    }

    function updateGalleryFooter() {
        try {
            const existingLinks = overlay.querySelector('.ebds-gallery-filter-links');
            if (existingLinks) existingLinks.remove();
            if (!overlay.classList.contains('visible') || EBDS_FILTER_LINKS_POSITION === 'hidden') return;

            const counts = getGalleryOmissionCounts();
            const filters = [
                {
                    count: counts.withoutPictures,
                    key: 'withoutPictures',
                    label: `${counts.withoutPictures} ${counts.withoutPictures === 1 ? 'item' : 'items'} without pictures`
                },
                {
                    count: counts.excludedByKeywords,
                    key: 'keywords',
                    label: `${counts.excludedByKeywords} excluded by keywords`
                },
                {
                    count: counts.belowMinimumSize,
                    key: 'minimumSize',
                    label: `${counts.belowMinimumSize} below minimum size`
                }
            ].filter(filter => filter.count > 0);
            if (!filters.length) return;

            const linksContainer = document.createElement('div');
            linksContainer.className = 'ebds-gallery-filter-links';
            if (EBDS_FILTER_LINKS_POSITION === 'header') linksContainer.classList.add('ebds-header');
            filters.forEach((filter, index) => {
                if (index > 0) linksContainer.appendChild(document.createTextNode(' • '));
                const link = document.createElement('a');
                link.href = '#';
                const bypassed = EBDS_TEMPORARY_FILTER_BYPASS[filter.key];
                link.textContent = filter.label;
                link.title = bypassed ? 'Apply this filter again' : 'Show these items temporarily';
                link.classList.toggle('ebds-filter-bypassed', bypassed);
                link.addEventListener('click', event => {
                    event.preventDefault();
                    EBDS_TEMPORARY_FILTER_BYPASS[filter.key] = !EBDS_TEMPORARY_FILTER_BYPASS[filter.key];
                    applyRowFiltering();
                });
                linksContainer.appendChild(link);
            });
            if (EBDS_FILTER_LINKS_POSITION === 'header') {
                const topPagination = overlay.querySelector('.ebds-gallery-pagination.ebds-top');
                if (topPagination) topPagination.insertAdjacentElement('afterend', linksContainer);
                else overlay.insertBefore(linksContainer, grid);
            } else {
                overlay.appendChild(linksContainer);
            }
        } catch (e) { }
    }

    window.ebdsUpdateGalleryFooter = updateGalleryFooter;

    function formatGalleryCaption(posted, name, size) {
        const maxLength = 80;
        if (!size) {
            const text = [posted, name].filter(Boolean).join(' • ');
            return text.length > maxLength ? text.substring(0, maxLength - 1) + '…' : text;
        }

        const prefix = posted ? posted + ' • ' : '';
        const suffix = ' • ' + size;
        const fullText = prefix + name + suffix;
        if (fullText.length <= maxLength) return fullText;

        const availableNameLength = maxLength - prefix.length - suffix.length;
        const truncatedName = availableNameLength > 1
            ? name.substring(0, availableNameLength - 1) + '…'
            : '';
        return prefix + truncatedName + suffix;
    }

    function openGallery() {
        grid.innerHTML = '';
        // determine last-visit epoch for the current category (if any)
        const currentCategoryId = getCategoryIdFromUrl();
        const lastVisit = currentCategoryId ? getLastVisitTimestamp(currentCategoryId) : 0;
        const showOnlyNew = EBDS_GALLERY_SHOW_ONLY_NEW && !EBDS_IS_SEARCH_PAGE && lastVisit > 0;

        const imgs = Array.from(document.querySelectorAll('.ebds-preview-img'));
        imgs.forEach((img, idx) => {
            // Skip images from hidden rows (filtered out)
            const rowId = img.dataset.rowId;
            const sourceRow = rowId ? document.getElementById(rowId) : null;
            if (sourceRow && sourceRow.style.display === 'none') return;
            // Skip images that were clicked/added during this session
            try {
                const g = img.dataset.guid || img.getAttribute('data-guid');
                if (g && EBDS_CLICKED_GUIDS && EBDS_CLICKED_GUIDS.has(g)) return;
            } catch (e) { }
            try {
                const p = derivePrefixFromElement(img) || derivePrefixFromSrc(img.src);
                if (p && EBDS_HIDDEN_PREFIXES && EBDS_HIDDEN_PREFIXES.has(p)) return;
            } catch (e) { }

            const item = document.createElement('div');
            item.className = 'ebds-gallery-item';
            if (rowId) item.dataset.rowId = rowId;
            if (sourceRow && sourceRow.classList.contains('ebds-temporarily-shown')) {
                item.classList.add('ebds-temporarily-shown');
            }
            const copy = img.cloneNode(true);
            // Remove inline transition handlers to avoid duplication issues
            copy.style.maxWidth = '';
            copy.style.maxHeight = '';
            copy.addEventListener('click', (e) => {
                // Shift+Left-click => hide by prefix for this session
                try {
                    if (e.shiftKey && e.button === 0) {
                        e.preventDefault();
                        e.stopPropagation();
                        const p = derivePrefixFromElement(copy);
                        if (p) addHiddenPrefix(p);
                        return;
                    }
                } catch (ex) { }

                e.preventDefault();
                const guid = copy.dataset.guid || (copy.getAttribute('data-guid') || null);
                if (guid) addToCartForGuid(guid, copy.src);
                else addToCartBySrc(copy.src);
            });

            item.appendChild(copy);
            // add caption under clone (use stored data attributes if available)
            try {
                const detailsHref = copy.dataset.detailsHref;
                const caption = document.createElement(detailsHref ? 'a' : 'div');
                caption.className = 'ebds-preview-caption';
                if (detailsHref) {
                    caption.href = detailsHref;
                    caption.style.textDecoration = 'none';
                }
                const posted = copy.dataset.posted || '';
                const name = copy.dataset.name || '';
                const size = copy.dataset.size || '';
                caption.textContent = formatGalleryCaption(posted, name, size);
                try {
                    // Mark as new when the parsed row epoch is newer than stored last visit for this category
                    const row = copy.dataset.rowId ? document.getElementById(copy.dataset.rowId) : null;
                    const itemEpoch = row ? parseRowTitleEpoch(row) : null;
                    if (itemEpoch && lastVisit && itemEpoch > lastVisit) {
                        caption.classList.add('ebds-new-post');
                    }
                } catch (e) { }
                // Optionally hide items that aren't new
                try {
                    const row = copy.dataset.rowId ? document.getElementById(copy.dataset.rowId) : null;
                    const itemEpoch = row ? parseRowTitleEpoch(row) : null;
                    const isNew = (itemEpoch && lastVisit && itemEpoch > lastVisit);
                    if (showOnlyNew && !isNew) {
                        // Skip adding this item to the gallery
                        return;
                    }
                } catch (e) { }
                item.appendChild(caption);
            } catch (e) { }
            // Add ctrl+hover behavior: open a large preview while holding Control
            try {
                copy.addEventListener('mouseenter', (e) => {
                    currentHoveredGalleryItem = item;
                    if (e.ctrlKey || ctrlPressed) openGalleryHoverLightbox(item, copy.src);
                });
                copy.addEventListener('mousemove', (e) => {
                    // open when ctrl is indicated (either via event or global flag); don't close while ctrl is held
                    if (e.ctrlKey || ctrlPressed) openGalleryHoverLightbox(item, copy.src);
                    else closeGalleryHoverLightbox(item);
                });
                copy.addEventListener('mouseleave', (e) => {
                    currentHoveredGalleryItem = null;
                    // Only close when Ctrl is NOT pressed
                    if (!ctrlPressed) closeGalleryHoverLightbox(item);
                });
            } catch (ex) { }
            grid.appendChild(item);
        });

        if (!EBDS_HIDE_WITHOUT_PICTURES || EBDS_TEMPORARY_FILTER_BYPASS.withoutPictures) {
            document.querySelectorAll('tr[id^="guid"]').forEach(row => {
                try {
                    if (row.style.display === 'none' || row.dataset.ebdsHasImgLink === '1') return;
                    const guid = row.id.substring(4);
                    if (guid && EBDS_CLICKED_GUIDS.has(guid)) return;
                    const itemEpoch = parseRowTitleEpoch(row);
                    if (showOnlyNew && !(itemEpoch && itemEpoch > lastVisit)) return;

                    const item = document.createElement('div');
                    item.className = 'ebds-gallery-item ebds-gallery-placeholder';
                    if (EBDS_TEMPORARY_FILTER_BYPASS.withoutPictures) {
                        item.classList.add('ebds-temporarily-shown');
                    }
                    item.dataset.guid = guid;
                    item.dataset.rowId = row.id;
                    const detailsLink = row.querySelector('a.title');
                    const visual = document.createElement('div');
                    visual.className = 'ebds-gallery-placeholder-visual';
                    visual.textContent = 'No picture';
                    visual.title = 'Click to add to cart';
                    visual.addEventListener('click', event => {
                        event.preventDefault();
                        addToCartForGuid(guid, null);
                    });
                    item.appendChild(visual);

                    const caption = document.createElement(detailsLink ? 'a' : 'div');
                    caption.className = 'ebds-preview-caption';
                    if (detailsLink) {
                        caption.href = detailsLink.href;
                        caption.style.textDecoration = 'none';
                    }
                    const name = detailsLink ? detailsLink.textContent.trim() : '';
                    const sizeMatch = row.textContent.match(/\b\d+(?:[.,]\d+)?\s*(?:KB|MB|GB|TB)\b/i);
                    const postedCell = row.querySelector('td.less.mid');
                    caption.textContent = formatGalleryCaption(
                        postedCell ? postedCell.textContent.trim() : '',
                        name,
                        sizeMatch ? sizeMatch[0].trim() : ''
                    );
                    if (itemEpoch && lastVisit && itemEpoch > lastVisit) {
                        caption.classList.add('ebds-new-post');
                    }
                    item.appendChild(caption);
                    grid.appendChild(item);
                } catch (e) { }
            });
        }

        // Image cards and no-picture placeholders are built separately; restore source-row order.
        const rowOrder = new Map(
            Array.from(document.querySelectorAll('#browsetable tr[id^="guid"]'))
                .map((row, index) => [row.id, index])
        );
        Array.from(grid.querySelectorAll('.ebds-gallery-item'))
            .sort((left, right) => {
                const leftIndex = rowOrder.get(left.dataset.rowId) ?? Number.MAX_SAFE_INTEGER;
                const rightIndex = rowOrder.get(right.dataset.rowId) ?? Number.MAX_SAFE_INTEGER;
                return leftIndex - rightIndex;
            })
            .forEach(item => grid.appendChild(item));

        // Add category links at the top of gallery
        try {
            // remove existing category links if present
            const oldLinks = overlay.querySelectorAll('.ebds-gallery-category-links');
            oldLinks.forEach(l => l.remove());
            const linksDiv = document.createElement('div');
            linksDiv.className = 'ebds-gallery-category-links';
            const links = categoryLinks;
            const visibleLinks = links.filter(link => EBDS_LINK_VISIBILITY[link.id] !== false);
            for (let i = 0; i < visibleLinks.length; i++) {
                const link = visibleLinks[i];
                const a = document.createElement('a');
                a.href = link.href;
                if (link.title) a.title = link.title;
                a.textContent = link.text;
                a.addEventListener('click', () => { try { localStorage.setItem('ebdsGalleryOpen', '1'); } catch (e) { } });
                linksDiv.appendChild(a);
                if (i < visibleLinks.length - 1) {
                    linksDiv.appendChild(document.createTextNode(' | '));
                }
            }
            overlay.insertBefore(linksDiv, grid);
        } catch (e) { }

        // Append page pagination to top and bottom of gallery overlay (if present on the page)
        try {
            // remove existing gallery pagination clones if present
            const oldPags = overlay.querySelectorAll('.ebds-gallery-pagination, .ebds-gallery-pagination-spacer');
            oldPags.forEach(p => p.remove());
            const pageNav = document.querySelector('nav.pagination[aria-label="Pagination"]');
            if (EBDS_INFINITE_SCROLL) {
                const topSpacer = document.createElement('div');
                topSpacer.className = 'ebds-gallery-pagination-spacer ebds-top';
                topSpacer.setAttribute('aria-hidden', 'true');
                const bottomSpacer = document.createElement('div');
                bottomSpacer.className = 'ebds-gallery-pagination-spacer ebds-bottom';
                bottomSpacer.setAttribute('aria-hidden', 'true');
                overlay.insertBefore(topSpacer, grid);
                grid.insertAdjacentElement('afterend', bottomSpacer);
            } else if (pageNav) {
                const clonedNavTop = pageNav.cloneNode(true);
                clonedNavTop.classList.add('ebds-gallery-pagination', 'ebds-top');
                const clonedNavBottom = pageNav.cloneNode(true);
                clonedNavBottom.classList.add('ebds-gallery-pagination', 'ebds-bottom');
                // Keep gallery open across pages: do NOT close the gallery when pagination links are clicked.
                // Instead ensure the persistent flag is set so the new page will re-open the gallery.
                [clonedNavTop, clonedNavBottom].forEach(clonedNav => {
                    try { clonedNav.querySelectorAll('a').forEach(a => a.addEventListener('click', () => { try { localStorage.setItem('ebdsGalleryOpen', '1'); } catch (e) { } })); } catch (e) { }
                });

                // If "Show only new items" is enabled, and the current page contains at least one non-new item,
                // prune cloned pagination entries after the current page (these pages will contain older/non-new items).
                try {
                    if (showOnlyNew) {
                        const rowsOnPage = Array.from(document.querySelectorAll('tr[id^="guid"]'));
                        const hasNonNew = rowsOnPage.some(r => {
                            try {
                                const epoch = parseRowTitleEpoch(r);
                                // treat missing epoch as non-new
                                if (!epoch) return true;
                                return lastVisit && epoch <= lastVisit;
                            } catch (e) { return false; }
                        });

                        if (hasNonNew) {
                            function pruneAfterCurrent(nav) {
                                try {
                                    const current = nav.querySelector('[aria-current="page"]') || nav.querySelector('.active, li.active, .current') || nav.querySelector('a.active');
                                    const curLi = current && current.closest ? (current.closest('li') || current) : null;
                                    if (curLi) {
                                        let sib = curLi.nextElementSibling;
                                        while (sib) {
                                            const next = sib.nextElementSibling;
                                            try { sib.remove(); } catch (e) { }
                                            sib = next;
                                        }
                                    } else {
                                        // Fallback: remove subsequent anchors after the active anchor
                                        const anchors = Array.from(nav.querySelectorAll('a'));
                                        let idx = anchors.findIndex(a => a === current || a.classList.contains('active') || a.getAttribute('aria-current') === 'page');
                                        if (idx >= 0) {
                                            for (let i = anchors.length - 1; i > idx; i--) {
                                                try { anchors[i].remove(); } catch (e) { }
                                            }
                                        } else {
                                            // As a final fallback, remove obvious "next" links
                                            const labels = /next|›|»|>|right|→/i;
                                            anchors.filter(a => a.getAttribute('rel') === 'next' || labels.test(a.getAttribute('aria-label') || '') || labels.test(a.textContent)).forEach(a => { try { a.remove(); } catch (e) { } });
                                        }
                                    }
                                } catch (e) { }
                            }
                            pruneAfterCurrent(clonedNavTop);
                            pruneAfterCurrent(clonedNavBottom);
                        }
                    }
                } catch (e) { }

                overlay.insertBefore(clonedNavTop, grid);
                overlay.appendChild(clonedNavBottom);
            }
        } catch (e) { }

        try {
            const container = document.querySelector('div.container-fluid');
            if (container) {
                if (!container.hasAttribute('data-ebds-hidden')) {
                    browseContainerPrevDisplay = container.style.display;
                    container.style.display = 'none';
                    container.setAttribute('data-ebds-hidden', '1');
                }
            }
        } catch (e) { }
        try { applyGalleryVerticalLimit(); } catch (e) { }
        overlay.classList.add('visible');
        btn.classList.add('active');
        updateGalleryFooter();
        try { localStorage.setItem('ebdsGalleryOpen', '1'); } catch (e) { }
    }

    function closeGallery() {
        try {
            const container = document.querySelector('div.container-fluid');
            if (container && browseContainerPrevDisplay !== null) {
                container.style.display = browseContainerPrevDisplay;
                container.removeAttribute('data-ebds-hidden');
            }
            browseContainerPrevDisplay = null;
        } catch (e) { }
        overlay.classList.remove('visible');
        btn.classList.remove('active');
        try { localStorage.removeItem('ebdsGalleryOpen'); } catch (e) { }
    }

    // Hover lightboxes inside the gallery (opened while Ctrl is held)
    function openGalleryHoverLightbox(item, src) {
        try {
            if (galleryHoverLightboxes.has(item)) return;
            const lb = document.createElement('div');
            lb.className = 'ebds-gallery-hover-lb';
            lb.style.position = 'fixed';
            lb.style.inset = '0';
            lb.style.background = 'rgba(0,0,0,0.95)';
            lb.style.display = 'flex';
            lb.style.alignItems = 'center';
            lb.style.justifyContent = 'center';
            lb.style.zIndex = '2147483650';
            const im = document.createElement('img');
            im.src = src;
            applyCtrlHoverPreviewSizing(im);
            lb.appendChild(im);
            // image click => add to cart; background click closes lightbox. (Escape handled globally)
            im.style.cursor = 'pointer';
            im.title = 'Click to add to cart';
            im.addEventListener('click', (ev) => {
                ev.stopPropagation();
                try {
                    const guidImg = item.querySelector('img');
                    const guid = guidImg && guidImg.dataset ? (guidImg.dataset.guid || null) : null;
                    if (guid) addToCartForGuid(guid, src);
                    else addToCartBySrc(src);
                } catch (e) { console.log('hover-lightbox add-to-cart error', e); }
                try { lb.remove(); } catch (e) { }
                try { galleryHoverLightboxes.delete(item); } catch (e) { }
            });
            lb.addEventListener('click', () => {
                try { lb.remove(); } catch (e) { }
                galleryHoverLightboxes.delete(item);
            });
            document.body.appendChild(lb);
            galleryHoverLightboxes.set(item, lb);
        } catch (e) { console.log('openGalleryHoverLightbox error', e); }
    }

    function closeGalleryHoverLightbox(item) {
        try {
            const lb = galleryHoverLightboxes.get(item);
            if (lb) {
                try { lb.remove(); } catch (e) { }
                galleryHoverLightboxes.delete(item);
            }
        } catch (e) { }
    }

    // Open hover lightbox when Control is pressed while hovering over a gallery item
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Control') {
            ctrlPressed = true;
            try {
                if (currentHoveredGalleryItem) {
                    const img = currentHoveredGalleryItem.querySelector('img');
                    if (img) openGalleryHoverLightbox(currentHoveredGalleryItem, img.src);
                }
            } catch (ex) { console.log('keydown ctrl openGalleryHoverLightbox error', ex); }
        }
    });

    // Close hover lightboxes when Ctrl is released
    document.addEventListener('keyup', (e) => {
        if (e.key === 'Control') {
            ctrlPressed = false;
            try { galleryHoverLightboxes.forEach((lb, item) => { lb.remove(); }); galleryHoverLightboxes.clear(); } catch (ex) { }
            try { window.hoverLightboxes.forEach((lb, img) => { lb.remove(); }); window.hoverLightboxes.clear(); } catch (ex) { }
        }
    });

    // Also close hover lightboxes when gallery closes
    const originalCloseGallery = closeGallery;
    closeGallery = function () {
        try { galleryHoverLightboxes.forEach((lb, item) => { lb.remove(); }); galleryHoverLightboxes.clear(); } catch (ex) { }
        originalCloseGallery();
    };

    // --- helper: add to cart by guid or src ---
    function addToCartForGuid(guid, src) {
        return addToCartByGuid(guid, src);
    }

    function addToCartBySrc(src) {
        const anchor = Array.from(document.querySelectorAll('a[href]')).find(a => a.href === src);
        if (anchor) {
            const row = anchor.closest('tr');
            const guid = row && row.id && row.id.indexOf('guid') === 0 ? row.id.substring(4) : null;
            if (guid) { addToCartForGuid(guid, src); return; }
        }
        try { if (window.$ && $.pnotify) { $.pnotify({ title: 'CART', text: 'Could not determine item to add', type: 'error' }); } } catch (e) { }
    }

    window.ebdsRefreshGallery = () => {
        if (overlay.classList.contains('visible')) openGallery();
    };
    btn.addEventListener('click', () => {
        if (overlay.classList.contains('visible')) closeGallery();
        else openGallery();
    });



    // Keyboard shortcut helpers for Prev/Next page (ArrowLeft / ArrowRight)
    function findPaginationLink(direction) {
        try {
            // Prefer rel attributes if present
            const rel = direction === 'prev' ? 'prev' : 'next';
            const relLink = document.querySelector('a[rel="' + rel + '"]');
            if (relLink) return relLink;
        } catch (e) { }

        // Try common pagination containers
        const nav = document.querySelector('nav.pagination') || document.querySelector('nav[aria-label="Pagination"]') || document.querySelector('ul.pagination') || document.querySelector('.pagination');
        if (!nav) return null;

        // Find current page item and walk siblings
        const current = nav.querySelector('[aria-current="page"]') || nav.querySelector('.active, li.active, .current') || nav.querySelector('a.active');
        if (current) {
            const curAnchor = current.tagName === 'A' ? current : (current.querySelector('a') || current);
            const li = (curAnchor && curAnchor.closest) ? curAnchor.closest('li') || curAnchor.parentElement : null;
            if (li) {
                let sib = direction === 'prev' ? li.previousElementSibling : li.nextElementSibling;
                while (sib) {
                    const a = sib.querySelector('a');
                    if (a && a.getAttribute('href')) return a;
                    sib = direction === 'prev' ? sib.previousElementSibling : sib.nextElementSibling;
                }
            }
        }

        // Fallback: try aria-label or visible text matching Prev/Next
        const labels = direction === 'prev' ? /prev|previous|‹|«|<|left|←/i : /next|›|»|>|right|→/i;
        const a = Array.from(nav.querySelectorAll('a')).find(a => (a.getAttribute('aria-label') && labels.test(a.getAttribute('aria-label'))) || labels.test(a.textContent));
        return a || null;
    }

    // Keyboard shortcut
    document.addEventListener('keydown', (e) => {
// Ignore when typing in form controls or when a focused element exists (covers custom elements/shadow hosts)
    try {
        const ae = document.activeElement;
        // If focus is on anything other than the document body or root, assume the user is interacting with UI and don't intercept
        if (ae && ae !== document.body && ae !== document.documentElement) return;
        } catch (ex) { }

        if (e.key.toLowerCase() === 'g' && !e.altKey && !e.ctrlKey && !e.metaKey) {
            e.preventDefault();
            if (overlay.classList.contains('visible')) closeGallery();
            else openGallery();
        } else if (e.key === 'Escape') {
            // Close the gallery and any visible enlarged previews
            closeGallery();
            try { document.querySelectorAll('.ebds-enlarged').forEach(el => { if (el.style.display && el.style.display !== 'none') el.style.display = 'none'; }); } catch (ex) { }
        } else if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && !e.altKey && !e.ctrlKey && !e.metaKey) {
            // Jump to previous/next page using site pagination
            const direction = (e.key === 'ArrowLeft') ? 'prev' : 'next';
            e.preventDefault();

            // Prefer global rel links first
            let link = null;
            try { link = document.querySelector('a[rel="' + (direction === 'prev' ? 'prev' : 'next') + '"]'); } catch (ex) { }
            if (!link) link = findPaginationLink(direction);

            // Also check overlay cloned pagination (gallery) for matching links
            if (!link && overlay && overlay.querySelector) {
                try {
                    const cloneNav = overlay.querySelector('.ebds-gallery-pagination');
                    if (cloneNav) {
                        // look for obvious labels
                        const labels = direction === 'prev' ? /prev|previous|‹|«|<|left|←/i : /next|›|»|>|right|→/i;
                        link = Array.from(cloneNav.querySelectorAll('a')).find(a => (a.getAttribute('aria-label') && labels.test(a.getAttribute('aria-label'))) || labels.test(a.textContent));
                        if (!link) {
                            const cur = cloneNav.querySelector('[aria-current="page"]') || cloneNav.querySelector('.active, li.active, a.active');
                            if (cur) {
                                const li = cur.closest ? cur.closest('li') : null;
                                if (li) {
                                    let sib = direction === 'prev' ? li.previousElementSibling : li.nextElementSibling;
                                    while (sib) {
                                        const a = sib.querySelector('a');
                                        if (a && a.getAttribute('href')) { link = a; break; }
                                        sib = direction === 'prev' ? sib.previousElementSibling : sib.nextElementSibling;
                                    }
                                }
                            }
                        }
                    }
                } catch (ex) { }
            }

            if (link) {
                try { link.click(); } catch (ex) { try { window.location = link.href; } catch (e) { } }
                // Keep gallery open if it was visible so the gallery state persists across navigation
                try { if (overlay.classList && overlay.classList.contains('visible')) try { localStorage.setItem('ebdsGalleryOpen', '1'); } catch (e) { } } catch (ex) { }
            }
        }
    });

    // Update per-category last-visit on initial page load when on first page (offset missing or =0)
    try { updateUiSessionOnPageLoad(); } catch (e) { }
    try { updateLastVisitFromPageOnLoad(); } catch (e) { }
    try { applyRowFiltering(); } catch (e) { }

    // Load subsequent result pages into the current listing and gallery near the scroll boundary.
    try {
        const resultTable = document.querySelector('#browsetable');
        if (resultTable) {
            const resultRowsContainer = resultTable.tBodies[0] || resultTable;
            const listingStatus = document.createElement('button');
            listingStatus.type = 'button';
            listingStatus.className = 'ebds-infinite-status';
            resultTable.insertAdjacentElement('afterend', listingStatus);

            const galleryStatus = document.createElement('button');
            galleryStatus.type = 'button';
            galleryStatus.className = 'ebds-infinite-status';
            overlay.appendChild(galleryStatus);

            let loadingNextPage = false;
            let nextPageUrl = findNextPageUrl(document, location.href);
            let listingObserver = null;
            let galleryObserver = null;
            let newItemsBoundaryReached = false;

            function findNextPageUrl(root, baseUrl) {
                const nav = root.querySelector('nav.pagination[aria-label="Pagination"], nav.pagination, ul.pagination, .pagination');
                if (!nav) return null;
                const current = nav.querySelector('[aria-current="page"], li.active, .current, a.active');
                let sibling = current && current.closest ? (current.closest('li') || current).nextElementSibling : null;
                while (sibling) {
                    if (!sibling.classList.contains('disabled')) {
                        const link = sibling.matches('a[href]') ? sibling : sibling.querySelector('a[href]');
                        const href = link && link.getAttribute('href');
                        if (href && href !== '#') {
                            const resolved = new URL(href, baseUrl);
                            return resolved.origin === location.origin ? resolved.href : null;
                        }
                    }
                    sibling = sibling.nextElementSibling;
                }
                return null;
            }

            function updateInfiniteStatus(message, retryable) {
                [listingStatus, galleryStatus].forEach(status => {
                    status.textContent = message;
                    status.disabled = !retryable;
                    status.classList.toggle('ebds-retry', !!retryable);
                });
            }

            function setInfiniteStatusVisible(visible) {
                [listingStatus, galleryStatus].forEach(status => { status.hidden = !visible; });
            }

            function getGalleryPrefetchMargin() {
                const items = Array.from(grid.querySelectorAll('.ebds-gallery-item'));
                if (items.length === 0) return 1000;
                const firstTop = items[0].getBoundingClientRect().top;
                const firstRowHeight = items
                    .filter(item => Math.abs(item.getBoundingClientRect().top - firstTop) < 2)
                    .reduce((height, item) => Math.max(height, item.getBoundingClientRect().height), 0);
                return Math.ceil(600 + firstRowHeight);
            }

            function isNewItemsOnlyActive() {
                if (!EBDS_GALLERY_SHOW_ONLY_NEW || EBDS_IS_SEARCH_PAGE) return false;
                const catId = getCategoryIdFromUrl();
                return !!(catId && getLastVisitTimestamp(catId));
            }

            function pageHasNewItems(rows) {
                if (!isNewItemsOnlyActive()) return true;
                const lastVisit = getLastVisitTimestamp(getCategoryIdFromUrl());
                return rows.some(row => {
                    const epoch = parseRowTitleEpoch(row);
                    return !!(epoch && epoch > lastVisit);
                });
            }

            async function loadNextPage() {
                if (!EBDS_INFINITE_SCROLL || loadingNextPage || !nextPageUrl ||
                    (isNewItemsOnlyActive() && newItemsBoundaryReached)) return;
                loadingNextPage = true;
                if (listingObserver) listingObserver.disconnect();
                if (galleryObserver) galleryObserver.disconnect();
                updateInfiniteStatus('Loading next page…', false);
                const requestedUrl = nextPageUrl;
                let loadedSuccessfully = false;
                const controller = new AbortController();
                const timeout = setTimeout(() => controller.abort(), 20000);
                try {
                    const response = await fetch(requestedUrl, {
                        credentials: 'same-origin',
                        signal: controller.signal
                    });
                    if (!response.ok) throw new Error('HTTP ' + response.status);
                    const fetchedDocument = new DOMParser().parseFromString(await response.text(), 'text/html');
                    if (!fetchedDocument.querySelector('#browsetable')) throw new Error('Result table missing from response');
                    const fetchedRows = Array.from(fetchedDocument.querySelectorAll('#browsetable tr[id^="guid"]'));
                    const newRows = fetchedRows.filter(row => !document.getElementById(row.id));
                    if (isNewItemsOnlyActive() && !pageHasNewItems(fetchedRows)) {
                        newItemsBoundaryReached = true;
                    }
                    newRows.forEach(row => {
                        row.dataset.ebdsInfiniteAdded = '1';
                        resultRowsContainer.appendChild(row);
                    });
                    const followingPageUrl = findNextPageUrl(fetchedDocument, requestedUrl);
                    nextPageUrl = followingPageUrl && followingPageUrl !== requestedUrl ? followingPageUrl : null;
                    if (typeof window.ebdsProcessRows === 'function') window.ebdsProcessRows(newRows);
                    updateSessionActivityForInfiniteScroll();
                    applyRowFiltering();
                    loadedSuccessfully = true;
                    if (!EBDS_INFINITE_SCROLL) updateInfiniteStatus('Infinite scrolling disabled', false);
                    else if (isNewItemsOnlyActive() && newItemsBoundaryReached) {
                        setInfiniteStatusVisible(false);
                    } else if (!nextPageUrl) updateInfiniteStatus('All results loaded', false);
                    else updateInfiniteStatus('Scroll to load more', false);
                } catch (error) {
                    console.error('EBDS infinite scroll failed:', error);
                    updateInfiniteStatus(
                        error && error.name === 'AbortError'
                            ? 'Loading timed out — click to retry'
                            : 'Could not load more — click to retry',
                        true
                    );
                } finally {
                    clearTimeout(timeout);
                    loadingNextPage = false;
                    if (loadedSuccessfully) observeInfiniteScroll();
                }
            }

            function observeInfiniteScroll() {
                document.body.classList.toggle('ebds-infinite-active', EBDS_INFINITE_SCROLL);
                if (listingObserver) listingObserver.disconnect();
                if (galleryObserver) galleryObserver.disconnect();
                if (!EBDS_INFINITE_SCROLL) {
                    setInfiniteStatusVisible(false);
                    updateInfiniteStatus('Infinite scrolling disabled', false);
                    return;
                }
                if (isNewItemsOnlyActive() && newItemsBoundaryReached) {
                    setInfiniteStatusVisible(false);
                    return;
                }
                setInfiniteStatusVisible(true);
                if (!nextPageUrl) {
                    updateInfiniteStatus('All results loaded', false);
                    return;
                }
                updateInfiniteStatus('Scroll to load more', false);
                listingObserver = new IntersectionObserver(entries => {
                    if (entries.some(entry => entry.isIntersecting)) loadNextPage();
                }, { rootMargin: '600px 0px' });
                galleryObserver = new IntersectionObserver(entries => {
                    if (entries.some(entry => entry.isIntersecting)) loadNextPage();
                }, { root: overlay, rootMargin: getGalleryPrefetchMargin() + 'px 0px' });
                listingObserver.observe(listingStatus);
                galleryObserver.observe(galleryStatus);
            }

            listingStatus.addEventListener('click', loadNextPage);
            galleryStatus.addEventListener('click', loadNextPage);
            window.ebdsSetInfiniteScrollEnabled = enabled => {
                EBDS_INFINITE_SCROLL = !!enabled;
                observeInfiniteScroll();
            };
            window.ebdsRefreshInfiniteScroll = observeInfiniteScroll;
            if (isNewItemsOnlyActive()) {
                newItemsBoundaryReached = !pageHasNewItems(
                    Array.from(resultTable.querySelectorAll('tr[id^="guid"]'))
                );
            }
            setInfiniteStatusVisible(false);
            const startObserving = () => requestAnimationFrame(observeInfiniteScroll);
            if (typeof requestIdleCallback === 'function') requestIdleCallback(startObserving, { timeout: 1000 });
            else setTimeout(startObserving, 250);
        }
    } catch (e) {
        console.error('EBDS infinite scroll initialization failed:', e);
    }

    // Restore state across page loads, including search-result pagination.
    try { if (localStorage.getItem('ebdsGalleryOpen') === '1') openGallery(); } catch (e) { }
})();