Booru Enhanced Dark Gallery

Modern dark theme with masonry layout, advanced search overlay, smart image loading, and collapsible sidebar for booru sites

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Booru Enhanced Dark Gallery
// @namespace    ko-fi.com/awesome97076
// @version      7
// @description  Modern dark theme with masonry layout, advanced search overlay, smart image loading, and collapsible sidebar for booru sites
// @author       Awesome
// @match        https://rule34.xxx/*
// @match        https://safebooru.org/*
// @match        https://www.safebooru.org/*
// @match        https://xbooru.com/*
// @match        https://www.xbooru.com/*
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=rule34.xxx
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @noframes
// ==/UserScript==

(function() {
    "use strict";

    // ───────────────────────────────────────────────
    //  CONFIG  –  persisted via GM_getValue/GM_setValue
    // ───────────────────────────────────────────────
    const DEFAULTS = {
        debug:                false,
        searchOverlayEnabled: true,
        searchOverlayHotkey:  "/",
        imageReplacement:     true,
        sidebarCollapsible:   true,
        sidebarRememberState: true,
        defaultColumns:       0
        };

    const CFG = {};
    for (const [k, v] of Object.entries(DEFAULTS)) {
        CFG[k] = GM_getValue(k, v);
    }

    function cfgSet(key, val) {
        CFG[key] = val;
        GM_setValue(key, val);
    }

    function cfgToggle(key) {
        cfgSet(key, !CFG[key]);
        location.reload();
    }

    // ───────────────────────────────────────────────
    //  MENU COMMANDS
    // ───────────────────────────────────────────────
    function registerMenuCommands() {
        const flag = v => v ? "\u2705" : "\u274C";

        GM_registerMenuCommand(`${flag(CFG.imageReplacement)} Image Replacement`, () => cfgToggle("imageReplacement"));
        GM_registerMenuCommand(`${flag(CFG.searchOverlayEnabled)} Search Overlay`, () => cfgToggle("searchOverlayEnabled"));
        GM_registerMenuCommand(`${flag(CFG.sidebarCollapsible)} Collapsible Sidebar`, () => cfgToggle("sidebarCollapsible"));
        GM_registerMenuCommand(`${flag(CFG.sidebarRememberState)} Remember Sidebar State`, () => cfgToggle("sidebarRememberState"));
        GM_registerMenuCommand(`${flag(CFG.debug)} Debug Logging`, () => cfgToggle("debug"));
        GM_registerMenuCommand("\uD83D\uDD04 Reset Image Server Cache", () => {
            GM_setValue(`imageServerBase_${currentHost}`, "");
            GM_setValue(`imageServerExpiry_${currentHost}`, 0);
            location.reload();
        });
        GM_registerMenuCommand("\u2699\uFE0F Set Default Columns\u2026", () => {
            const input = prompt(
                "Default column count (1\u20138).\nSet 0 for automatic responsive breakpoints.\nCurrent: " + (CFG.defaultColumns || "auto"),
                CFG.defaultColumns
            );
            if (input === null) return;
            const n = parseInt(input, 10);
            if (isNaN(n) || n < 0 || n > 8) { alert("Invalid value"); return; }
            cfgSet("defaultColumns", n);
            location.reload();
        });
    }

    registerMenuCommands();

    // ───────────────────────────────────────────────
    //  UTILITIES
    // ───────────────────────────────────────────────
    const Utils = {
        log: (...args) => CFG.debug && console.log("[Rule34 Enhanced]", ...args),
        createElement: (tag, props = {}, children = []) => {
            const el = document.createElement(tag);
            Object.assign(el, props);
            if (typeof children === "string") el.innerHTML = children;
            else children.forEach(c => el.appendChild(c));
            return el;
        },
        debounce: (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; },
        throttle: (fn, ms) => { let busy; return (...a) => { if (!busy) { fn(...a); busy = true; setTimeout(() => { busy = false; }, ms); } }; }
    };


    const SITE_CONFIGS = {
        "rule34.xxx": {
            autocompleteUrl: "https://ac.rule34.xxx/autocomplete.php?q=",
            fallbackImageServer: "https://wimg.rule34.xxx/images",
            thumbsOnlyServers: ["miami.rule34.xxx", "ny.rule34.xxx"],
        },
        "safebooru.org": {
            autocompleteUrl: "https://safebooru.org/autocomplete.php?q=",
            fallbackImageServer: null, // derive from thumbnails
            thumbsOnlyServers: [],
        },
        "xbooru.com": {
            autocompleteUrl: "https:/xbooru.com/public/autocomplete.php?q=",
            fallbackImageServer: null,
            thumbsOnlyServers: [],
        }
    };

    // Detect current site from hostname (strip www.)
    const currentHost = location.hostname.replace(/^www\./, "");
    const SiteConfig = SITE_CONFIGS[currentHost] || {
        // Generic fallback for unknown Gelbooru 0.2 sites
        autocompleteUrl: `https://${currentHost}/autocomplete.php?q=`,
        fallbackImageServer: null,
        thumbsOnlyServers: [],
    };
    Utils.log("Site config for", currentHost, SiteConfig);


    const ImageServer = {
        CACHE_TTL: 7 * 24 * 60 * 60 * 1000,

        resolve() {
            // Cache key is per-site so different boorus don't share
            const cacheKey = `imageServerBase_${currentHost}`;
            const expiryKey = `imageServerExpiry_${currentHost}`;
            const cached = GM_getValue(cacheKey, "");
            const expiry = GM_getValue(expiryKey, 0);

            if (cached && Date.now() < expiry) {
                Utils.log("Image server (cached):", cached);
                return cached;
            }

            const fromPage = this.detectFromPage();
            if (fromPage) { this.cache(fromPage); return fromPage; }

            const fallback = SiteConfig.fallbackImageServer
                || `https://${currentHost}/images`;
            this.cache(fallback);
            return fallback;
        },

        detectFromPage() {
            const thumb = document.querySelector('img[src*="/thumbnails/"]');
            if (!thumb) return null;
            try {
                const url = new URL(thumb.src);
                const thumbsOnly = new Set(SiteConfig.thumbsOnlyServers);
                if (thumbsOnly.has(url.host)) {
                    Utils.log("Thumbnail server is thumbs-only, skipping:", url.host);
                    return null;
                }
                const base = `${url.protocol}//${url.host}/images`;
                Utils.log("Image server (page-detected):", base);
                return base;
            } catch { return null; }
        },

        cache(base) {
            GM_setValue(`imageServerBase_${currentHost}`, base);
            GM_setValue(`imageServerExpiry_${currentHost}`, Date.now() + this.CACHE_TTL);
        }
    };

    const StylesManager = {
        init() {
            const style = document.createElement("style");
            style.textContent = this.getCSS();
            document.head.appendChild(style);
        },
        getCSS() {
            return `

:root {
    --bg-color: #121212;
    --bg-secondary: #1e1e1e;
    --bg-tertiary: #2d2d2d;
    --accent-color: #9c64a6;
    --accent-secondary: #ae81ff;
    --text-primary: #e0e0e0;
    --text-secondary: #b0b0b0;
    --text-muted: #707070;
    --border-color: rgba(255, 255, 255, 0.1);
    --tag-artist: #ff79c6;
    --tag-character: #50fa7b;
    --tag-copyright: #bd93f9;
    --tag-metadata: #f1fa8c;
    --grid-columns: 3;
    --grid-gap: 8px;
    --border-radius: 6px;
    --box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    --font-size-base: 14px;
}

html, body {
    margin: 0 !important;
    padding: 0 !important;
    font-size: var(--font-size-base) !important;
    background: var(--bg-color) !important;
    color: var(--text-primary) !important;
    overflow-x: hidden;
    min-height: 100%;
}

body, div, h1, h2, h3, h4, h5, h6, p, ul, li, dd, dt {
    background: unset;
}
div.content {
    background: unset;
}


*, *::before, *::after { box-sizing: border-box; }

.tag-count { color: var(--text-primary) !important; }
#post-list > div.content > span { display: none; }

div#content {
    width: 100% !important;
    max-width: 100% !important;
    margin: 0 auto !important;
    padding: 8px 20px 30px 20px !important;
}

/* ======== HEADER ======== */
div#header {
    background: var(--bg-secondary) !important;
    padding: 0 !important;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
    position: sticky;
    top: 0;
    z-index: 1000;
    margin-bottom: 16px !important;
}

div#header #site-title {
    padding: 6px 26px !important;
}

div#header #site-title a {
    color: var(--accent-color) !important;
    font-size: 22px !important;
    font-weight: bold;
    text-shadow: none !important;
}

div#header ul#navbar, div#header ul#subnavbar {
    background: var(--bg-secondary) !important;
    background-image: none !important;
    flex-wrap: wrap;
    align-items: center;
    list-style: none;
    padding: 10px 16px !important;
    margin: 0 !important;
    border-bottom: 1px solid var(--border-color);
}

#rmainmenu:checked ~ #navbar, #rsubmenu:checked ~ #subnavbar { display: flex; }

div#header ul#navbar li, div#header ul#subnavbar li {
    background: none !important;
    background-image: none !important;
}

div#header ul#navbar li a, div#header ul#subnavbar li a {
    padding: 12px !important;
    color: var(--text-secondary) !important;
    text-decoration: none;
    white-space: nowrap;
    font-size: 14px !important;
    font-weight: normal !important;
}

div#header ul#navbar li a:hover, div#header ul#subnavbar li a:hover {
    color: var(--accent-color) !important;
}

div#header ul#navbar li.current-page {
    background-image: none !important;
    background-color: transparent !important;
}

div#header ul#navbar li.current-page a {
    color: var(--accent-color) !important;
    font-weight: 600 !important;
    border-bottom: 2px solid var(--accent-color);
}

/* ======== LINKS ======== */
a:link, a:visited { color: var(--accent-color) !important; text-decoration: none !important; }
a:hover { color: var(--accent-secondary) !important; text-decoration: underline !important; }
a:active { color: var(--accent-secondary) !important; text-decoration: none !important; }

.tag-type-artist > a, .tag-type-artist { color: var(--tag-artist) !important; }
.tag-type-character > a, .tag-type-character { color: var(--tag-character) !important; }
.tag-type-copyright > a, .tag-type-copyright { color: var(--tag-copyright) !important; }
.tag-type-metadata > a, .tag-type-metadata { color: var(--tag-metadata) !important; }

/* ======== GENERIC TABLE STYLING ======== */
table, table.highlightable {
    background: var(--bg-secondary) !important;
    border-collapse: collapse !important;
    border-radius: var(--border-radius);
    overflow: hidden;
    border: 1px solid var(--border-color) !important;
    width: 100%;
    max-width: 700px;
}

table td, table th {
    padding: 10px 16px !important;
    border-bottom: 1px solid var(--border-color) !important;
    border-color: var(--border-color) !important;
    color: var(--text-primary) !important;
    vertical-align: top;
}

table td:first-child { color: var(--text-secondary) !important; font-weight: 600; white-space: nowrap; }
table td strong { color: var(--text-secondary) !important; }
table tr:last-child td { border-bottom: none !important; }
table tr:hover { background: rgba(255, 255, 255, 0.03) !important; }
table tr:nth-child(odd) { background: rgba(255, 255, 255, 0.02) !important; }
table.highlightable > tbody > tr:hover { background: rgba(156, 100, 166, 0.1) !important; }
table.highlightable th { color: var(--text-primary) !important; background: var(--bg-tertiary) !important; }
tr.tableheader, thead tr { background: var(--bg-tertiary) !important; background-image: none !important; }

/* ======== PROFILE PAGE ======== */
div#content > h2 {
    color: var(--text-primary) !important;
    font-size: 22px;
    font-weight: 700;
    margin: 0 0 16px 0;
    padding-bottom: 12px;
    border-bottom: 2px solid var(--accent-color);
    display: inline-block;
}

div#content h4 {
    color: var(--text-primary) !important;
    font-size: 16px;
    font-weight: 600;
    margin: 16px 0 12px 0;
    padding: 8px 0;
    border-bottom: 1px solid var(--border-color);
}

div#content h4 a { margin-left: 6px; font-size: 13px; }

/* Profile page layout – make favorites section wider */
div#content > div[style*="float: left"] {
    float: none !important;
    clear: both !important;
    width: 100% !important;
    max-width: 100% !important;
}

/* ================================================================
   IMAGE GRID  –  CSS Columns masonry


   ================================================================ */
div.image-list {
    display: grid !important;
    grid-template-columns: repeat(var(--grid-columns), 1fr) !important;
    grid-auto-rows: 10px !important;
    gap: var(--grid-gap) !important;
    flex-flow: unset !important;
    flex-wrap: unset !important;
    flex-direction: unset !important;
    justify-content: unset !important;
    align-items: unset !important;
    align-content: unset !important;
    width: 100% !important;
    margin: 0 auto !important;
    padding: 0 !important;
}

/* Override the site's fixed 200x200 .thumb sizing */
span.thumb, .thumb {
    width: 100% !important;
    height: auto !important;
    display: block !important;
    margin: 0 !important;
    padding: 0 !important;
    overflow: hidden;
    justify-content: unset !important;
    align-items: unset !important;
    grid-row-end: span 22;
}

/* Outer wrapper spans – on favorites and profile pages, the site wraps
   each .thumb in an extra span with inline styles like:
     display:grid; grid-template-rows: auto 10px; align-self: flex-start
   On post listings, .thumb spans are direct children (no wrapper).
   display:contents makes wrappers invisible to the grid so .thumb
   participates directly in the masonry layout.
   IMPORTANT: :not(.thumb) prevents matching the .thumb spans themselves
   which also have inline style attributes (grid-row-end set by JS). */
div.image-list > span[style]:not(.thumb),
div#content > div[style*="float: left"] span[style*="display: grid"]:not(.thumb) {
    display: contents !important;
}

span.thumb > a, .thumb > a {
    display: block !important;
    position: relative;
    overflow: hidden;
    border-radius: 6px;
    background: var(--bg-tertiary);
    box-shadow: var(--box-shadow);
    width: 100% !important;
    height: auto !important;
    text-align: unset !important;
    justify-content: unset !important;
    align-items: unset !important;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

span.thumb > a:hover, .thumb > a:hover {
    transform: scale(1.03);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
    z-index: 100;
}

span.thumb img, span.thumb video,
.thumb img, .thumb video {
    width: 100% !important;
    height: auto !important;
    max-width: 100% !important;
    max-height: unset !important;
    display: block !important;
    object-fit: contain;
    border-radius: 6px;
}

span.thumb img, .thumb img {
    border: none !important;
}

img.preview {
    margin: 0 !important;
}

/* Video/webm border – applied on the <a> wrapper for reliability */
span.thumb > a:has(> video),
span.thumb > a:has(> .webm-thumb),
.thumb > a:has(> video),
.thumb > a:has(> .webm-thumb) {
    border: 2px solid #8e44ad !important;
    border-radius: 6px !important;
}

/* Fallback for browsers without :has() – keep class-based border */
.webm-thumb {
    border: 2px solid #8e44ad !important;
    border-radius: 6px !important;
}

/* ======== COMMENT LISTS ======== */
div#comments div#comment-list {
    display: flex !important;
    flex-direction: column !important;
    gap: 16px;
}

/* Each post card in the comment list */
div#comments div#comment-list > div.post {
    float: none !important;
    clear: none !important;
    display: flex !important;
    flex-direction: row !important;
    background: var(--bg-secondary) !important;
    border: 1px solid var(--border-color) !important;
    border-radius: var(--border-radius) !important;
    overflow: hidden !important;
    margin-bottom: 0 !important;
}

/* Thumbnail column */
div#comments div#comment-list > div.post > div.col1 {
    float: none !important;
    clear: none !important;
    flex-shrink: 0 !important;
    width: 680px !important;
    min-width: 680px !important;
    padding: 12px !important;
    display: flex !important;
    align-items: flex-start !important;
    justify-content: center !important;
    background: var(--bg-tertiary) !important;
}

div#comments div#comment-list > div.post > div.col1 a {
    display: block;
    border-radius: var(--border-radius);
    overflow: hidden;
}

div#comments div#comment-list > div.post > div.col1 img {
    width: 100% !important;
    height: auto !important;
    max-width: 656px !important;
    max-height: unset !important;
    display: block !important;
    border-radius: var(--border-radius);
    transition: transform 0.2s ease;
    object-fit: contain !important;
}

div#comments div#comment-list > div.post > div.col1 img:hover {
    transform: scale(1.05);
}

/* Info + comments column */
div#comments div#comment-list > div.post > div.col2 {
    float: none !important;
    flex: 1 !important;
    min-width: 0 !important;
    width: auto !important;
    display: flex !important;
    flex-direction: column !important;
}

div#comments div#comment-list > div.post > div.col2 > div.header {
    background: var(--bg-tertiary);
    padding: 10px 16px;
    border-bottom: 1px solid var(--border-color);
    margin-bottom: 0 !important;
}

div#comments div#comment-list > div.post > div.col2 > div.header .info {
    margin-right: 16px;
    font-size: 13px;
    color: var(--text-secondary) !important;
}

div#comments div#comment-list > div.post > div.col2 > div.header .info strong {
    color: var(--text-muted) !important;
    font-weight: 600;
    margin-right: 4px;
    text-transform: uppercase;
    font-size: 11px;
    letter-spacing: 0.5px;
}

div#comments div#comment-list > div.post > div.col2 > div.header .tags {
    margin-top: 8px;
    font-size: 12px;
    line-height: 1.8;
    max-width: 100% !important;
    width: 100% !important;
    overflow: hidden;
}

div#comments div#comment-list > div.post > div.col2 > div.header .tags strong {
    color: var(--text-muted) !important;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

div#comments div#comment-list > div.post > div.col2 > div.header .tags a {
    font-size: 12px;
    padding: 1px 4px;
    border-radius: 3px;
    transition: background 0.15s ease;
}

div#comments div#comment-list > div.post > div.col2 > div.header .tags a:hover {
    background: rgba(255, 255, 255, 0.08);
    text-decoration: none !important;
}

/* Comment responses area */
div.response-list {
    padding: 8px 0 !important;
    display: flex !important;
    flex-direction: column !important;
    gap: 2px;
}

div.response-list > div.post {
    float: none !important;
    clear: none !important;
    display: flex !important;
    flex-direction: row !important;
    padding: 10px 16px;
    gap: 12px;
    margin-bottom: 0 !important;
    background: transparent;
    transition: background 0.15s ease;
}

div.response-list > div.post:hover {
    background: rgba(255, 255, 255, 0.03);
}

div.response-list > div.post > div.author {
    float: none !important;
    flex-shrink: 0;
    min-width: 130px !important;
    max-width: 160px !important;
    padding-right: 12px !important;
    border-right: 2px solid var(--border-color);
    overflow-x: visible !important;
}

div.response-list > div.post > div.author h6 {
    margin: 0;
    font-size: 13px !important;
    font-weight: 600;
}

div.response-list > div.post > div.author h6 a { color: var(--accent-color) !important; }

div.response-list > div.post > div.author span.date {
    display: block;
    font-size: 11px;
    color: var(--text-muted) !important;
    margin-top: 2px;
}

div.response-list > div.post > div.content {
    float: none !important;
    flex: 1 !important;
    min-width: 0;
    width: auto !important;
    padding: 0 !important;
    margin: 0 !important;
}

div.response-list > div.post > div.content > div.body {
    color: var(--text-primary) !important;
    font-size: 14px;
    line-height: 1.6;
    word-wrap: break-word;
    overflow-wrap: break-word;
}

div.response-list > div.post > div.content > div.footer {
    margin-top: 6px !important;
    font-size: 11px;
    color: var(--text-muted) !important;
}

div.response-list > div.post > div.content > div.footer a {
    color: var(--text-muted) !important;
    font-size: 11px;
}

div.response-list > div.post > div.content > div.footer a:hover {
    color: #ff5555 !important;
}

/* Vote links in comment lists */
div#comments a[onclick*="post_vote"],
div#post-comments a[onclick*="post_vote"] {
    color: var(--accent-secondary) !important;
    font-size: 12px;
    padding: 2px 6px;
    border-radius: 3px;
    transition: background 0.15s ease;
}

div#comments a[onclick*="post_vote"]:hover,
div#post-comments a[onclick*="post_vote"]:hover {
    background: rgba(174, 129, 255, 0.15);
    text-decoration: none !important;
}

/* ======== POST VIEW PAGE – comments below post ======== */
div#post-comments {
    max-width: 100% !important;
}

div#post-comments div#comment-list > div {
    margin-bottom: 1em;
}

div#post-comments div#comment-list > div > div.col2 {
    margin-top: 10px;
    width: auto !important;
}

div#post-comments div#comment-list > div > div.col2 > div.header {
    margin-bottom: 1em;
}

/* ======== PAGINATION ======== */
div#paginator {
    display: block !important;
    text-align: center;
    padding: 16px 0 !important;
    margin-top: 16px;
    clear: both;
}

div#paginator a, div#paginator b, div#paginator span {
    display: inline-block !important;
    padding: 6px 12px !important;
    margin: 2px 3px !important;
    border-radius: var(--border-radius) !important;
    font-size: 13px;
    font-weight: 500;
    text-decoration: none !important;
    transition: all 0.15s ease;
}

div#paginator a {
    background: var(--bg-secondary) !important;
    color: var(--text-secondary) !important;
    border: 1px solid var(--border-color) !important;
}

div#paginator a:hover {
    background: var(--accent-color) !important;
    color: white !important;
    border-color: var(--accent-color) !important;
}

div#paginator b {
    background: var(--accent-color) !important;
    color: white !important;
    border: 1px solid var(--accent-color) !important;
}

/* ======== NOTICE / MAIL ======== */
div#notice.notice, div.notice {
    background: var(--bg-secondary) !important;
    border: 1px solid var(--accent-color) !important;
    border-radius: var(--border-radius);
    color: var(--text-primary) !important;
    padding: 12px 16px !important;
    margin-bottom: 12px;
}

div.has-mail {
    background: linear-gradient(135deg, rgba(156, 100, 166, 0.15), rgba(174, 129, 255, 0.1)) !important;
    border: 1px solid var(--accent-color) !important;
    border-radius: var(--border-radius);
    padding: 10px 16px !important;
    margin-bottom: 12px;
    text-align: center;
}

div.has-mail a { color: var(--accent-secondary) !important; font-weight: 600; }
div#long-notice { background: var(--bg-secondary) !important; color: var(--text-primary) !important; border-radius: var(--border-radius); }

/* ======== FORMS ======== */
input[type="text"], input[type="password"], input[type="email"],
input[type="search"], select, textarea {
    background: var(--bg-color) !important;
    color: var(--text-primary) !important;
    border: 1px solid var(--border-color) !important;
    border-radius: var(--border-radius);
    padding: 8px 12px;
    font-size: 14px;
    transition: border-color 0.2s ease;
}

input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus,
input[type="search"]:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--accent-color) !important;
    box-shadow: 0 0 0 3px rgba(156, 100, 166, 0.15);
    background: var(--bg-color) !important;
}

input[type="submit"], input[type="button"], button {
    background: var(--accent-color) !important;
    color: white !important;
    border: none !important;
    border-radius: var(--border-radius);
    padding: 8px 16px;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.2s ease, box-shadow 0.2s ease;
}

input[type="submit"]:hover, input[type="button"]:hover, button:hover {
    background: var(--accent-secondary) !important;
    box-shadow: 0 2px 8px rgba(156, 100, 166, 0.3);
}

input[type="checkbox"], input[type="radio"] { accent-color: var(--accent-color); }

/* ======== WIKI / HELP / FORUM ======== */
div#content .section, div#content fieldset {
    background: var(--bg-secondary) !important;
    border: 1px solid var(--border-color) !important;
    border-radius: var(--border-radius);
    padding: 16px;
    margin-bottom: 12px;
}

div#content fieldset legend { color: var(--accent-color) !important; font-weight: 600; padding: 0 8px; }

div.help div.code { border-color: var(--border-color) !important; background: var(--bg-tertiary) !important; }
div.help h4 { color: var(--accent-color) !important; }
div.quote { background: var(--bg-tertiary) !important; border-color: var(--border-color) !important; color: var(--text-primary) !important; }
div.status-notice { border-color: var(--border-color) !important; background: var(--bg-secondary) !important; color: var(--text-primary) !important; }

/* Wiki */
div#wiki-show > div#body { color: var(--text-primary) !important; width: auto !important; }
div#wiki-show > div#body > div#byline { color: var(--text-muted) !important; }
div.wiki > h2.title { color: var(--accent-color) !important; }
div#wiki-diff del { background-color: rgba(255, 85, 85, 0.3) !important; }
div#wiki-diff ins { background-color: rgba(80, 250, 123, 0.3) !important; }

/* Forum */
div#forum { color: var(--text-primary) !important; }

/* Mail */
div.mailbody { background-color: var(--bg-secondary) !important; border-color: var(--border-color) !important; color: var(--text-primary) !important; }
div.mailbuttons { color: var(--text-secondary) !important; }

/* ======== SIDEBAR ======== */
div.sidebar {
    background: var(--bg-secondary) !important;
    border-radius: 8px;
    box-shadow: var(--box-shadow);
    padding: 0 !important;
    margin-right: 16px !important;
    margin-bottom: 16px;
    max-width: 260px !important;
    min-width: 240px !important;
    border: 1px solid var(--border-color);
    overflow: hidden !important;
}

div.sidebar li {
    color: var(--text-primary) !important;
}

#tag-sidebar {
    margin: 0; padding: 0; list-style: none;
    max-height: 60vh;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--accent-color) var(--bg-tertiary);
}

#tag-sidebar h6 {
    background: var(--bg-tertiary) !important;
    color: var(--text-primary) !important;
    margin: 0;
    padding: 10px 16px;
    font-weight: 600;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 1px;
    border-bottom: 1px solid var(--border-color);
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

#tag-sidebar h6:hover { background: rgba(156, 100, 166, 0.2) !important; color: var(--accent-color) !important; }

#tag-sidebar h6::after {
    content: '';
    width: 0; height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 6px solid currentColor;
    opacity: 0.7;
}

#tag-sidebar h6.collapsed::after { transform: rotate(-90deg); }

.tag-section { overflow: hidden; opacity: 1; }
.tag-section.collapsed { max-height: 0; opacity: 0; }

#tag-sidebar li { padding: 0; margin: 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
#tag-sidebar li:not(:has(h6)):hover { background: rgba(255, 255, 255, 0.03); }

/* ======== FOOTER ======== */
div#footer {
    margin-top: 30px !important;
    padding: 16px 0;
    text-align: center;
    color: var(--text-muted) !important;
    border-top: 1px solid var(--border-color);
    font-size: 12px;
}

/* ======== POST VIEW / POST LIST LAYOUT ======== */
div#post-view, div#post-list, div.flexi {
    display: flex !important;
    flex-direction: row !important;
}

/* ======== TAG SEARCH BOX ======== */
div.tag-search {
    background: var(--bg-tertiary) !important;
    padding: 12px !important;
    border-radius: var(--border-radius);
    margin: 0 0 16px 0 !important;
    width: 100%;
    position: relative;
}

div.tag-search input[type="text"] {
    width: 100% !important;
    padding: 8px 12px !important;
    background: var(--bg-color) !important;
    border: 1px solid var(--border-color) !important;
    border-radius: var(--border-radius);
    color: var(--text-primary) !important;
    font-size: 14px;
    margin-bottom: 8px;
}

div.tag-search input[type="submit"] {
    padding: 8px 16px !important;
    background: var(--accent-color) !important;
    color: white !important;
    border: none !important;
    border-radius: var(--border-radius);
    cursor: pointer;
    font-weight: bold;
    font-size: 14px;
    width: 100%;
    max-width: 180px;
    margin-top: 3px;
}

div.tag-search input[type="submit"]:hover { background: var(--accent-secondary) !important; }

/* Autocomplete (awesomplete) overrides */
.awesomplete > ul {
    background: var(--bg-secondary) !important;
    border: 1px solid var(--accent-color) !important;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
}

.awesomplete > ul:before {
    background: var(--bg-secondary) !important;
    border-color: var(--accent-color) !important;
}

.awesomplete > ul > li {
    color: var(--text-primary) !important;
    padding: 8px 12px !important;
    border-bottom: 1px solid var(--border-color);
}

.awesomplete > ul > li:hover,
.awesomplete > ul > li[aria-selected="true"] {
    background: rgba(156, 100, 166, 0.2) !important;
    color: var(--text-primary) !important;
}

.awesomplete mark {
    background: rgba(156, 100, 166, 0.4) !important;
    color: white !important;
}

/* ======== SEARCH OVERLAY ======== */
.r34-search-overlay {
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background: rgba(0, 0, 0, 0.8);
    backdrop-filter: blur(5px);
    z-index: 10000;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    visibility: hidden;
}

.r34-search-overlay.show { opacity: 1; visibility: visible; }

.r34-search-modal {
    background: var(--bg-secondary);
    border-radius: 12px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
    border: 1px solid var(--border-color);
    width: 90%;
    max-width: 600px;
    max-height: 80vh;
    overflow: visible;
}

.r34-search-header {
    background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-secondary) 100%);
    padding: 16px 20px;
    border-bottom: 1px solid var(--border-color);
    display: flex;
    align-items: center;
    justify-content: space-between;
    border-radius: 12px 12px 0 0;
}

.r34-search-title {
    color: white !important;
    font-size: 18px;
    font-weight: 700;
    margin: 0;
    display: flex;
    align-items: center;
    gap: 10px;
}

.r34-search-title::before { content: "\\26A1"; font-size: 20px; }

.r34-search-close {
    background: rgba(255, 255, 255, 0.15) !important;
    color: white !important;
    border: none !important;
    border-radius: 6px;
    width: 28px; height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    font-size: 16px;
    font-weight: bold;
    padding: 0 !important;
}

.r34-search-close:hover { background: rgba(255, 85, 85, 0.8) !important; }
.r34-search-body { padding: 20px; }
.r34-search-form { display: flex; flex-direction: column; gap: 16px; }

.r34-search-input {
    width: 100%;
    padding: 12px 16px !important;
    background: var(--bg-color) !important;
    border: 2px solid rgba(255, 255, 255, 0.1) !important;
    border-radius: 8px;
    color: var(--text-primary) !important;
    font-size: 14px;
    resize: vertical;
    min-height: 60px;
    line-height: 1.4;
}

.r34-search-input:focus {
    outline: none;
    border-color: var(--accent-color) !important;
    box-shadow: 0 0 0 3px rgba(156, 100, 166, 0.15);
}

.r34-search-input::placeholder { color: var(--text-muted) !important; font-style: italic; }

.r34-search-button {
    padding: 12px 20px !important;
    background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-secondary) 100%) !important;
    color: white !important;
    border: none !important;
    border-radius: 8px;
    cursor: pointer;
    font-weight: 600;
    font-size: 14px;
}

.r34-search-button:hover { box-shadow: 0 4px 12px rgba(156, 100, 166, 0.4); }

.r34-search-hint { color: var(--text-muted) !important; font-size: 12px; text-align: center; margin-top: 8px; font-style: italic; }

.r34-search-shortcut {
    background: var(--bg-tertiary);
    color: var(--text-secondary) !important;
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 10px;
    margin: 0 4px;
}

.r34-search-overlay-trigger {
    position: absolute;
    top: 8px; right: 8px;
    background: var(--accent-color) !important;
    color: white !important;
    border: none !important;
    border-radius: 50%;
    width: 32px; height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    font-size: 14px;
    z-index: 10;
    padding: 0 !important;
}

.r34-search-overlay-trigger:hover { background: var(--accent-secondary) !important; }

.mobile-search-link {
    display: flex !important;
    align-items: center;
    gap: 6px;
    font-weight: 600 !important;
    color: var(--accent-color) !important;
}

/* ======== AUTOCOMPLETE DROPDOWN (search overlay) ======== */
.r34-autocomplete-dropdown {
    position: fixed;
    background: var(--bg-color) !important;
    border: 2px solid var(--accent-color);
    border-top: none;
    border-radius: 0 0 8px 8px;
    max-height: 250px;
    overflow-y: auto;
    z-index: 10001;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    scrollbar-width: thin;
    scrollbar-color: var(--accent-color) var(--bg-tertiary);
}

.r34-autocomplete-item {
    padding: 10px 12px;
    cursor: pointer;
    border-bottom: 1px solid rgba(255, 255, 255, 0.05);
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.r34-autocomplete-item:hover, .r34-autocomplete-item.selected { background: rgba(156, 100, 166, 0.15); }

.r34-autocomplete-tag { font-size: 13px; font-weight: 500; flex: 1; margin-right: 8px; }
.r34-autocomplete-count { font-size: 11px; color: var(--text-muted); margin-right: 8px; }

.r34-autocomplete-type {
    font-size: 9px; padding: 2px 6px; border-radius: 8px;
    text-transform: uppercase; font-weight: bold; min-width: 40px; text-align: center;
}

.r34-tag-artist { border-left: 3px solid var(--tag-artist); }
.r34-tag-artist .r34-autocomplete-tag { color: var(--tag-artist); }
.r34-tag-artist .r34-autocomplete-type { background: rgba(255, 121, 198, 0.2); color: var(--tag-artist); }

.r34-tag-character { border-left: 3px solid var(--tag-character); }
.r34-tag-character .r34-autocomplete-tag { color: var(--tag-character); }
.r34-tag-character .r34-autocomplete-type { background: rgba(80, 250, 123, 0.2); color: var(--tag-character); }

.r34-tag-copyright { border-left: 3px solid var(--tag-copyright); }
.r34-tag-copyright .r34-autocomplete-tag { color: var(--tag-copyright); }
.r34-tag-copyright .r34-autocomplete-type { background: rgba(189, 147, 249, 0.2); color: var(--tag-copyright); }

.r34-tag-metadata { border-left: 3px solid var(--tag-metadata); }
.r34-tag-metadata .r34-autocomplete-tag { color: var(--tag-metadata); }
.r34-tag-metadata .r34-autocomplete-type { background: rgba(241, 250, 140, 0.2); color: var(--tag-metadata); }

.r34-tag-general { border-left: 3px solid #b0b0b0; }
.r34-tag-general .r34-autocomplete-tag { color: #b0b0b0; }
.r34-tag-general .r34-autocomplete-type { background: rgba(176, 176, 176, 0.2); color: #b0b0b0; }

/* ======== COLUMN CONTROL ======== */
.r34-column-control {
    margin-bottom: 16px;
    background: var(--bg-secondary);
    padding: 12px;
    border-radius: 8px;
    border: 1px solid var(--border-color);
    display: flex;
    align-items: center;
    gap: 12px;
    box-shadow: var(--box-shadow);
}

.r34-column-control label { color: var(--text-primary) !important; font-weight: 600; min-width: 60px; font-size: 14px; }

.r34-column-control input[type="range"] {
    flex: 1;
    height: 6px;
    background: var(--bg-tertiary) !important;
    border-radius: 3px;
    outline: none;
    cursor: pointer;
    border: none !important;
}

.r34-column-count { color: var(--accent-color); font-weight: bold; font-size: 16px; min-width: 20px; text-align: center; }

/* ======== MISC ======== */
span.data-nosnippet { display: none; }

#navbar li a[rel="sponsored"] { opacity: 0.5; font-size: 12px !important; }
#navbar li a[rel="sponsored"]:hover { opacity: 0.8; }

hr { border-color: var(--border-color) !important; }

/* GDPR */
.gdprinner { background-color: var(--bg-secondary) !important; border-color: var(--border-color) !important; color: var(--text-primary) !important; }
.gdprtext { color: var(--text-primary) !important; }
.gdprtext a { color: var(--accent-color) !important; }

/* Tips bar */
div.tips { background: var(--bg-tertiary) !important; background-image: none !important; border-bottom-color: var(--border-color) !important; color: var(--text-primary) !important; }

/* Spoilers */
a.spoiler { color: var(--bg-color) !important; background: var(--bg-color) !important; }
a.spoiler:hover { color: var(--text-primary) !important; }
span.spoiler { color: var(--bg-color) !important; background: var(--bg-color) !important; }
span.spoiler:hover { color: var(--text-primary) !important; }

/* Blocked */
div.blocked { border-color: #ff5555 !important; background: rgba(255, 85, 85, 0.1) !important; color: #ff5555 !important; }

/* Notes on images */
div#note-container > div.note-body { background: var(--bg-secondary) !important; border-color: var(--border-color) !important; color: var(--text-primary) !important; }
div#note-container > div.note-box { border-color: var(--accent-color) !important; background: rgba(156, 100, 166, 0.15) !important; }

/* Manual page chooser */
.manual-page-chooser > input[type="text"] { background-color: var(--bg-color) !important; }
.manual-page-chooser > input[type="submit"] { background-color: var(--accent-color) !important; }

/* ======== RESPONSIVE ======== */
@media (min-width: 2560px) { :root { --grid-columns: 6; } }
@media (max-width: 2559px) and (min-width: 1920px) { :root { --grid-columns: 5; } }
@media (max-width: 1919px) and (min-width: 1600px) { :root { --grid-columns: 4; } }
@media (max-width: 1599px) and (min-width: 1200px) { :root { --grid-columns: 4; } }
@media (max-width: 1199px) and (min-width: 992px) { :root { --grid-columns: 3; } }
@media (max-width: 991px) and (min-width: 768px) { :root { --grid-columns: 3; } }
@media (max-width: 767px) and (min-width: 576px) { :root { --grid-columns: 2; } }
@media (max-width: 575px) { :root { --grid-columns: 1; } }

@media (max-width: 768px) {
    span.thumb > a:hover, .thumb > a:hover { transform: scale(1.02) !important; }
    div#header ul#navbar { flex-direction: column; align-items: stretch; padding: 0 !important; }
    div#header ul#navbar li { display: block !important; border-top: 1px solid var(--border-color) !important; padding: 0 10px !important; margin: 0 !important; }
    div#header ul#navbar li a { font-size: 14px !important; width: 100% !important; display: block !important; padding: 12px 0 !important; }
    .mobile-search-link { background: var(--accent-color) !important; color: white !important; padding: 8px 12px !important; border-radius: 6px !important; margin: 2px 0 !important; }
    .r34-column-control { padding: 10px !important; margin-bottom: 12px !important; }

    /* Comments responsive */
    div#comments div#comment-list > div.post { flex-direction: column !important; }
    div#comments div#comment-list > div.post > div.col1 { width: 100% !important; min-width: 100% !important; max-height: 200px; overflow: hidden; }
    div#comments div#comment-list > div.post > div.col1 img { max-width: 150px !important; }
    div.response-list > div.post { flex-direction: column !important; gap: 4px !important; }
    div.response-list > div.post > div.author {
        width: auto !important; min-width: unset !important; max-width: unset !important;
        border-right: none !important; border-bottom: 1px solid var(--border-color);
        padding-right: 0 !important; padding-bottom: 6px !important;
        display: flex; align-items: baseline; gap: 8px;
    }
    div#comments div#comment-list > div.post > div.col2 > div.header .tags { display: none; }
}

@media (max-width: 575px) {
    div#content { padding: 8px !important; }
    .r34-search-modal { width: 95% !important; margin: 8px !important; }
    .r34-search-body { padding: 16px !important; }
    div.sidebar { margin-right: 0 !important; max-width: 100% !important; min-width: 100% !important; margin-bottom: 16px !important; }
    #tag-sidebar { max-height: 40vh !important; }
    div#header #site-title { padding: 10px 12px !important; text-align: center !important; }
    div#header #site-title a { font-size: 18px !important; }
}
`;
        }
    };

    // ───────────────────────────────────────────────
    //  MASONRY LAYOUT
    // ───────────────────────────────────────────────

    const MasonryLayout = {
        gridGap: 8,
        gridRowHeight: 10,

        init() {
            const imageList = document.querySelector("div.image-list");
            if (!imageList) return;

            // Initial layout pass after a short delay for CSS to settle
            setTimeout(() => this.layoutAll(imageList), 250);

            // Re-layout when images load or get replaced
            imageList.addEventListener("load", (e) => {
                if (e.target.tagName === "IMG") {
                    this.measureThumb(e.target.closest(".thumb, span.thumb"));
                }
            }, true); // capture phase to catch all img loads

            // Watch for new thumbs (dynamic content)
            new MutationObserver(() => {
                this.layoutAll(imageList);
            }).observe(imageList, { childList: true, subtree: true });

            // Re-layout on window resize (column count may change)
            window.addEventListener("resize", Utils.debounce(() => {
                this.layoutAll(imageList);
            }, 200));
        },

        layoutAll(container) {
            if (!container) container = document.querySelector("div.image-list");
            if (!container) return;

            const thumbs = container.querySelectorAll(".thumb, span.thumb");
            thumbs.forEach(thumb => this.measureThumb(thumb));
        },

        measureThumb(thumb) {
            if (!thumb) return;

            // Double-RAF: first RAF schedules after current paint,
            // second RAF runs after the browser has actually rendered
            requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                    const content = thumb.querySelector("a") || thumb;
                    const img = thumb.querySelector("img, video");

                    // If image hasn't loaded yet, set up a listener and use default span
                    if (img && img.tagName === "IMG" && !img.complete) {
                        if (!img.dataset.masonryBound) {
                            img.dataset.masonryBound = "true";
                            img.addEventListener("load", () => this.measureThumb(thumb), { once: true });
                        }
                        return; // keep the CSS default span for now
                    }

                    // Measure the actual rendered height of the content
                    const h = content.getBoundingClientRect().height;
                    if (h <= 0) return;

                    const span = Math.ceil((h + this.gridGap) / (this.gridRowHeight + this.gridGap));
                    thumb.style.gridRowEnd = `span ${span}`;
                });
            });
        }
    };

    // ───────────────────────────────────────────────
    //  SEARCH OVERLAY
    const SearchOverlay = {
        overlay: null,
        isOpen: false,
        autocompleteCache: new Map(),
        currentSuggestions: [],
        selectedSuggestionIndex: -1,
        pendingRequest: null,
        lastQuery: "",

        init() {
            if (!CFG.searchOverlayEnabled) return;
            setTimeout(() => {
                this.createOverlay();
                this.bindEvents();
                this.addTriggerButton();
            }, 100);
        },

        createOverlay() {
            this.overlay = Utils.createElement("div", {
                className: "r34-search-overlay",
                innerHTML: `
                    <div class="r34-search-modal">
                        <div class="r34-search-header">
                            <h3 class="r34-search-title">Enhanced Search</h3>
                            <div class="r34-search-controls">
                                <button class="r34-search-close" type="button" title="Close">&times;</button>
                            </div>
                        </div>
                        <div class="r34-search-body">
                            <form class="r34-search-form" action="index.php" method="get">
                                <input type="hidden" name="page" value="post">
                                <input type="hidden" name="s" value="list">
                                <div class="r34-search-input-container" style="position: relative;">
                                    <textarea name="tags" class="r34-search-input" placeholder="Enter search tags...&#10;Examples:&#10;\u2022 female solo animated&#10;\u2022 character_name -furry rating:safe&#10;\u2022 artist_name 1girl" rows="4"></textarea>
                                    <div class="r34-autocomplete-dropdown" style="display: none;"></div>
                                </div>
                                <button type="submit" class="r34-search-button">Search</button>
                                <div class="r34-search-hint">
                                    Press <span class="r34-search-shortcut">/</span> to open
                                    <span class="r34-search-shortcut">Esc</span> to close
                                    <span class="r34-search-shortcut">\u2191\u2193</span> navigate
                                    <span class="r34-search-shortcut">Enter</span> select
                                </div>
                            </form>
                        </div>
                    </div>
                `
            });
            document.body.appendChild(this.overlay);
        },

        bindEvents() {
            if (!this.overlay) return;
            const closeBtn = this.overlay.querySelector(".r34-search-close");
            const textarea = this.overlay.querySelector(".r34-search-input");
            const dropdown = this.overlay.querySelector(".r34-autocomplete-dropdown");

            closeBtn?.addEventListener("click", () => this.close());
            this.overlay.addEventListener("click", e => { if (e.target === this.overlay) this.close(); });

            document.addEventListener("keydown", e => {
                const inInput = document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA";
                if (e.key === CFG.searchOverlayHotkey && !this.isOpen && !inInput) { e.preventDefault(); this.open(); }
                else if (e.key === "Escape" && this.isOpen) { e.preventDefault(); this.close(); }
            }, true);

            if (textarea) {
                textarea.addEventListener("input", Utils.debounce(e => this.handleInput(e), 300));
                textarea.addEventListener("keydown", e => this.handleKeyDown(e));
                textarea.addEventListener("blur", () => { this.cancelPendingRequest(); setTimeout(() => this.hideAutocomplete(), 150); });
                this.populateCurrentSearch();
            }

            dropdown?.addEventListener("click", e => {
                const item = e.target.closest(".r34-autocomplete-item");
                if (item) this.selectSuggestion(item.dataset.value);
            });
        },

        async handleInput(e) {
            const ta = e.target;
            const tag = this.getCurrentTag(ta.value, ta.selectionStart);
            if (!tag || tag.length < 2 || tag === this.lastQuery) { this.hideAutocomplete(); return; }
            this.lastQuery = tag;
            this.cancelPendingRequest();
            await this.showAutocomplete(tag);
        },

        cancelPendingRequest() {
            if (this.pendingRequest) { this.pendingRequest.cancelled = true; this.pendingRequest = null; }
        },

        async fetchSuggestions(query) {
            if (this.autocompleteCache.has(query)) return this.autocompleteCache.get(query);
            this.cancelPendingRequest();
            try {
                const req = fetch(`${SiteConfig.autocompleteUrl}${encodeURIComponent(query)}`);
                this.pendingRequest = { promise: req, cancelled: false };
                const resp = await req;
                if (this.pendingRequest?.cancelled || !resp.ok) return [];
                const json = await resp.json();
                if (this.pendingRequest?.cancelled) return [];
                const suggestions = json.slice(0, 6).map(item => ({
                    label: item.label, value: item.value, type: item.type,
                    count: this.extractCount(item.label)
                }));
                this.autocompleteCache.set(query, suggestions);
                if (this.autocompleteCache.size > 30) this.cleanupCache();
                this.pendingRequest = null;
                return suggestions;
            } catch { this.pendingRequest = null; return []; }
        },

        cleanupCache() {
            const keep = Array.from(this.autocompleteCache.entries()).slice(0, 20);
            this.autocompleteCache.clear();
            keep.forEach(([k, v]) => this.autocompleteCache.set(k, v));
        },

        async showAutocomplete(query) {
            const suggestions = await this.fetchSuggestions(query);
            const dropdown = this.overlay.querySelector(".r34-autocomplete-dropdown");
            const textarea = this.overlay.querySelector(".r34-search-input");
            if (!dropdown || !textarea || !suggestions.length) { this.hideAutocomplete(); return; }

            const rect = textarea.getBoundingClientRect();
            dropdown.style.left = rect.left + "px";
            dropdown.style.top = rect.bottom + "px";
            dropdown.style.width = rect.width + "px";

            this.currentSuggestions = suggestions;
            this.selectedSuggestionIndex = -1;

            dropdown.innerHTML = suggestions.map((item, i) => {
                const countDisplay = item.count > 0 ? `(${item.count})` : "";
                return `<div class="r34-autocomplete-item r34-tag-${item.type}" data-value="${item.value}" data-index="${i}">
                    <span class="r34-autocomplete-tag">${item.value}</span>
                    <span class="r34-autocomplete-count">${countDisplay}</span>
                    <span class="r34-autocomplete-type">${item.type}</span>
                </div>`;
            }).join("");
            dropdown.style.display = "block";
        },

        hideAutocomplete() {
            const dd = this.overlay?.querySelector(".r34-autocomplete-dropdown");
            if (dd) dd.style.display = "none";
            this.currentSuggestions = [];
            this.selectedSuggestionIndex = -1;
        },

        handleKeyDown(e) {
            const dd = this.overlay.querySelector(".r34-autocomplete-dropdown");
            if (!dd || dd.style.display === "none") return;
            switch (e.key) {
                case "ArrowDown":
                    e.preventDefault();
                    this.selectedSuggestionIndex = Math.min(this.selectedSuggestionIndex + 1, this.currentSuggestions.length - 1);
                    this.updateSelectedSuggestion(); break;
                case "ArrowUp":
                    e.preventDefault();
                    this.selectedSuggestionIndex = Math.max(this.selectedSuggestionIndex - 1, -1);
                    this.updateSelectedSuggestion(); break;
                case "Enter": case "Tab":
                    if (this.selectedSuggestionIndex >= 0) {
                        e.preventDefault();
                        this.selectSuggestion(this.currentSuggestions[this.selectedSuggestionIndex].value);
                    } break;
                case "Escape": this.hideAutocomplete(); break;
            }
        },

        updateSelectedSuggestion() {
            this.overlay.querySelectorAll(".r34-autocomplete-item").forEach((el, i) => {
                el.classList.toggle("selected", i === this.selectedSuggestionIndex);
            });
        },

        selectSuggestion(tag) {
            const ta = this.overlay.querySelector(".r34-search-input");
            if (!ta) return;
            const pos = ta.selectionStart, text = ta.value;
            const before = text.substring(0, pos), after = text.substring(pos);
            const bTags = before.split(/[\s\n]+/);
            const start = before.lastIndexOf(bTags[bTags.length - 1]);
            const aTags = after.split(/[\s\n]+/);
            const end = pos + (aTags[0] ? aTags[0].length : 0);
            const newText = text.substring(0, start) + tag + " " + text.substring(end);
            ta.value = newText;
            ta.setSelectionRange(start + tag.length + 1, start + tag.length + 1);
            this.hideAutocomplete();
            ta.focus();
        },

        getCurrentTag(text, pos) {
            const before = text.substring(0, pos), after = text.substring(pos);
            const bTags = before.split(/[\s\n]+/), aTags = after.split(/[\s\n]+/);
            let tag = bTags[bTags.length - 1] || "";
            if (aTags[0] && !text[pos]?.match(/[\s\n]/)) tag += aTags[0];
            return tag.trim();
        },

        extractCount(label) { const m = label.match(/\((\d+)\)$/); return m ? parseInt(m[1]) : 0; },

        addTriggerButton() {
            const form = document.querySelector("div.tag-search");
            if (!form || form.querySelector(".r34-search-overlay-trigger")) return;
            const btn = Utils.createElement("button", {
                className: "r34-search-overlay-trigger", type: "button",
                innerHTML: "\u26A1", title: "Open Advanced Search (Press /)"
            });
            btn.addEventListener("click", e => { e.preventDefault(); this.open(); });
            form.appendChild(btn);
        },

        populateCurrentSearch() {
            const tags = new URLSearchParams(location.search).get("tags");
            if (tags) {
                const ta = this.overlay?.querySelector(".r34-search-input");
                if (ta) ta.value = decodeURIComponent(tags);
            }
        },

        open() {
            if (!this.overlay || this.isOpen) return;
            this.isOpen = true;
            this.overlay.classList.add("show");
            document.body.style.overflow = "hidden";
            setTimeout(() => {
                const ta = this.overlay.querySelector(".r34-search-input");
                if (ta) { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); }
            }, 100);
        },

        close() {
            if (!this.overlay || !this.isOpen) return;
            this.isOpen = false;
            this.overlay.classList.remove("show");
            document.body.style.overflow = "";
            this.hideAutocomplete();
        }
    };

    // ───────────────────────────────────────────────
    //  COLUMN CONTROL
    // ───────────────────────────────────────────────
    const ColumnControl = {
        init() {
            this.addControlPanel();
            this.loadSavedColumns();
        },

        addControlPanel() {
            if (document.getElementById("columnSlider")) return;
            const panel = Utils.createElement("div", {
                className: "r34-column-control",
                innerHTML: `
                    <label for="columnSlider">Columns:</label>
                    <input type="range" id="columnSlider" min="1" max="8" value="3">
                    <span class="r34-column-count" id="columnCount">3</span>
                `
            });
            const imageList = document.querySelector(".image-list");
            if (imageList?.parentNode) imageList.parentNode.insertBefore(panel, imageList);
            this.bindEvents();
        },

        bindEvents() {
            const slider = document.getElementById("columnSlider");
            const display = document.getElementById("columnCount");
            if (!slider || !display) return;
            slider.addEventListener("input", e => {
                const v = e.target.value;
                display.textContent = v;
                this.setColumnCount(v);
                GM_setValue("galleryColumns", parseInt(v));
            });
        },

        setColumnCount(n) { document.documentElement.style.setProperty("--grid-columns", n); },

        loadSavedColumns() {
            const saved = GM_getValue("galleryColumns", 0);
            const effective = saved || CFG.defaultColumns;
            if (effective > 0) {
                const slider = document.getElementById("columnSlider");
                const display = document.getElementById("columnCount");
                if (slider && display) {
                    slider.value = effective;
                    display.textContent = effective;
                    this.setColumnCount(effective);
                }
            }
        }
    };

    // ───────────────────────────────────────────────
    //  IMAGE REPLACEMENT
    // ───────────────────────────────────────────────
    const ImageReplacement = {
        processedImages: new Set(),
        extCache: new Map(),
        retryQueue: new Map(),
        serverBase: "",
        observer: null,
        MAX_RETRIES: 3,
        RETRY_DELAYS: [2000, 5000, 12000],

        init() {
            if (!CFG.imageReplacement) return;
            this.serverBase = ImageServer.resolve();
            Utils.log("Using image server:", this.serverBase);

            // Block the browser from loading samples/previews – we only want thumbnails
            // then upgrade to full images ourselves. This prevents double-loading.
            this.blockSampleLoads();

            // Use IntersectionObserver for proper lazy loading
            this.observer = new IntersectionObserver(
                (entries) => {
                    entries.forEach(entry => {
                        if (entry.isIntersecting) {
                            const img = entry.target;
                            this.replaceThumbnail(img);
                            // Keep observing – if it fails and we retry,
                            // we want to know when user scrolls back
                        }
                    });
                },
                { rootMargin: "300px 0px" } // start loading 300px before visible
            );

            // Observe all current thumbnails
            this.observeAll();

            // Watch for dynamically added thumbnails
            new MutationObserver(() => this.observeAll())
                .observe(document.body, { childList: true, subtree: true });
        },

        blockSampleLoads() {
            document.querySelectorAll('img[src*="/thumbnails/"]').forEach(img => {
                img.loading = "lazy";
                img.decoding = "async";
                // Store the original thumbnail src so we always have it
                if (!img.dataset.thumbSrc) {
                    img.dataset.thumbSrc = img.src;
                }
            });
        },

        observeAll() {
            document.querySelectorAll('img[src*="/thumbnails/"]').forEach(img => {
                if (!img.dataset.observed) {
                    img.dataset.observed = "true";
                    img.loading = "lazy";
                    img.decoding = "async";
                    if (!img.dataset.thumbSrc) img.dataset.thumbSrc = img.src;
                    this.observer.observe(img);
                }
            });
        },

        extractHash(url) { const m = url.match(/thumbnail_([a-f0-9]+)/); return m ? m[1] : null; },
        extractDirectory(url) { const m = url.match(/thumbnails\/+(\d+)\//); return m ? m[1] : null; },
        extractID(url) { const m = url.match(/thumbnail_[a-f0-9]+\.\w+\?(\d+)/); return m ? m[1] : null; },

        createBaseUrl(thumbUrl) {
            const hash = this.extractHash(thumbUrl);
            const dir = this.extractDirectory(thumbUrl);
            if (!hash || !dir) return null;
            return { base: `${this.serverBase}/${dir}/${hash}`, hash };
        },

        replaceThumbnail(img) {
            const src = img.dataset.thumbSrc || img.src;
            if (this.processedImages.has(src) || img.dataset.processing === "true" || img.dataset.replaced) return;

            const data = this.createBaseUrl(src);
            if (!data) return;

            const { base, hash } = data;
            this.processedImages.add(src);
            img.dataset.processing = "true";
            const id = this.extractID(src);

            // Fast path: we already know the extension from a previous image
            if (this.extCache.has(hash)) {
                const ext = this.extCache.get(hash);
                this.loadImage(img, `${base}.${ext}`, hash, ext, src, id);
                return;
            }

            // Try extensions in order
            this.tryExtensions(img, base, hash, src, id, 0);
        },

        tryExtensions(img, base, hash, thumbSrc, id, extIdx) {
            const exts = ["jpg", "png", "jpeg", "gif"];
            if (extIdx >= exts.length) {
                // All extensions failed – schedule retry
                Utils.log("All extensions failed for", hash, "– scheduling retry");
                img.dataset.processing = "false";
                this.processedImages.delete(thumbSrc);
                this.scheduleRetry(img, thumbSrc);
                return;
            }

            const ext = exts[extIdx];
            const url = id ? `${base}.${ext}?${id}` : `${base}.${ext}`;

            // Use a probe Image to avoid flickering the visible img on failure
            const probe = new Image();
            probe.onload = () => {
                this.extCache.set(hash, ext);
                img.src = url;
                img.dataset.replaced = "loaded";
                img.dataset.processing = "false";
                // Cancel any pending retry
                this.cancelRetry(thumbSrc);
                // Re-measure masonry span after the new image renders
                img.onload = () => {
                    const thumb = img.closest(".thumb, span.thumb");
                    if (thumb) MasonryLayout.measureThumb(thumb);
                };
            };
            probe.onerror = () => {
                this.tryExtensions(img, base, hash, thumbSrc, id, extIdx + 1);
            };
            probe.src = url;
        },

        loadImage(img, url, hash, ext, thumbSrc, id) {
            const fullUrl = id ? `${url}?${id}` : url;
            const probe = new Image();
            probe.onload = () => {
                img.src = fullUrl;
                img.dataset.replaced = "cached";
                img.dataset.processing = "false";
                this.cancelRetry(thumbSrc);
                // Re-measure masonry span after the new image renders
                img.onload = () => {
                    const thumb = img.closest(".thumb, span.thumb");
                    if (thumb) MasonryLayout.measureThumb(thumb);
                };
            };
            probe.onerror = () => {
                // Cached extension failed – clear cache and retry from scratch
                Utils.log("Cached extension failed for", hash, "– retrying");
                this.extCache.delete(hash);
                img.dataset.processing = "false";
                this.processedImages.delete(thumbSrc);
                this.scheduleRetry(img, thumbSrc);
            };
            probe.src = fullUrl;
        },

        scheduleRetry(img, thumbSrc) {
            const existing = this.retryQueue.get(thumbSrc);
            const retries = existing ? existing.retries : 0;

            if (retries >= this.MAX_RETRIES) {
                Utils.log("Max retries reached for", thumbSrc);
                // Leave the thumbnail as-is
                return;
            }

            const delay = this.RETRY_DELAYS[retries] || this.RETRY_DELAYS[this.RETRY_DELAYS.length - 1];
            Utils.log(`Retry ${retries + 1}/${this.MAX_RETRIES} for`, thumbSrc, `in ${delay}ms`);

            const timer = setTimeout(() => {
                this.retryQueue.delete(thumbSrc);
                // Only retry if still in/near viewport
                const r = img.getBoundingClientRect();
                if (r.top < window.innerHeight + 500 && r.bottom > -500) {
                    this.replaceThumbnail(img);
                }
            }, delay);

            this.retryQueue.set(thumbSrc, { img, retries: retries + 1, timer });
        },

        cancelRetry(thumbSrc) {
            const entry = this.retryQueue.get(thumbSrc);
            if (entry) {
                clearTimeout(entry.timer);
                this.retryQueue.delete(thumbSrc);
            }
        },

        processVisibleThumbnails() {
            // Manual trigger – just observe everything, the IntersectionObserver handles the rest
            this.observeAll();
        }
    };

    // ───────────────────────────────────────────────
    //  COLLAPSIBLE SIDEBAR
    // ───────────────────────────────────────────────
    const CollapsibleSidebar = {
        init() {
            if (!CFG.sidebarCollapsible) return;
            setTimeout(() => this.setup(), 100);
        },

        setup() {
            const sidebar = document.getElementById("tag-sidebar");
            if (!sidebar) return;

            const saved = CFG.sidebarRememberState
                ? JSON.parse(GM_getValue("sidebarCollapsedStates", "{}"))
                : {};

            sidebar.querySelectorAll("h6").forEach(header => {
                const name = header.textContent.toLowerCase().trim();
                const section = document.createElement("div");
                section.className = "tag-section";

                let el = header.parentElement.nextElementSibling;
                const items = [];
                while (el && !el.querySelector("h6")) { items.push(el); el = el.nextElementSibling; }
                items.forEach(item => section.appendChild(item));
                header.parentElement.parentNode.insertBefore(section, header.parentElement.nextElementSibling);

                if (saved[name]) {
                    header.classList.add("collapsed");
                    section.classList.add("collapsed");
                }

                header.addEventListener("click", e => {
                    e.preventDefault();
                    const collapsed = header.classList.contains("collapsed");
                    header.classList.toggle("collapsed");
                    section.classList.toggle("collapsed", !collapsed);
                    this.saveStates();
                });
            });
        },

        saveStates() {
            if (!CFG.sidebarRememberState) return;
            const states = {};
            document.querySelectorAll("#tag-sidebar h6").forEach(h => {
                states[h.textContent.toLowerCase().trim()] = h.classList.contains("collapsed");
            });
            GM_setValue("sidebarCollapsedStates", JSON.stringify(states));
        }
    };

    // ───────────────────────────────────────────────
    //  MOBILE NAV
    // ───────────────────────────────────────────────
    const MobileNavigation = {
        init() {
            setTimeout(() => this.addMobileSearchLink(), 200);
        },

        addMobileSearchLink() {
            const navbar = document.querySelector("#navbar");
            if (!navbar || navbar.querySelector(".mobile-search-link")) return;
            const li = Utils.createElement("li", { className: "mobile-search-item" });
            const a = Utils.createElement("a", {
                className: "mobile-search-link", href: "#",
                innerHTML: "\u26A1 Search", title: "Open Enhanced Search"
            });
            a.addEventListener("click", e => { e.preventDefault(); SearchOverlay.open(); });
            li.appendChild(a);
            navbar.insertBefore(li, navbar.firstChild);
        }
    };

    // ───────────────────────────────────────────────
    //  MAIN
    // ───────────────────────────────────────────────
    const MainApp = {
        init() {
            StylesManager.init();

            if (CFG.defaultColumns > 0) {
                document.documentElement.style.setProperty("--grid-columns", CFG.defaultColumns);
            }

            MasonryLayout.init();
            SearchOverlay.init();
            ColumnControl.init();
            ImageReplacement.init();
            CollapsibleSidebar.init();
            MobileNavigation.init();

            window.processAllThumbnails = () => ImageReplacement.processVisibleThumbnails();
            window.openSearchOverlay = () => SearchOverlay.open();
            window.closeSearchOverlay = () => SearchOverlay.close();
        }
    };

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", () => MainApp.init());
    } else {
        MainApp.init();
    }
})();