Rule34 Enhanced Dark Gallery

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Rule34 Enhanced Dark Gallery
// @namespace    ko-fi.com/awesome97076
// @version      5
// @description  Modern dark theme with masonry layout, advanced search overlay, smart image loading, and collapsible sidebar
// @author       Awesome
// @match        https://rule34.xxx/*
// @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";


    const DEFAULTS = {
        debug:                false,
        searchOverlayEnabled: true,
        searchOverlayHotkey:  "/",
        imageReplacement:     true,
        sidebarCollapsible:   true,
        sidebarRememberState: true,
        defaultColumns:       0,
        imageServerBase:      "",
        imageServerExpiry:    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", () => {
            cfgSet("imageServerBase", "");
            cfgSet("imageServerExpiry", 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 ImageServer = {
        CACHE_TTL: 7 * 24 * 60 * 60 * 1000,
        // Servers known NOT to host /images/
        THUMBS_ONLY: new Set(["miami.rule34.xxx", "ny.rule34.xxx"]),

        resolve() {
            // 1. Cached and not expired
            if (CFG.imageServerBase && Date.now() < CFG.imageServerExpiry) {
                Utils.log("Image server (cached):", CFG.imageServerBase);
                return CFG.imageServerBase;
            }

            // 2. Derive from thumbnail URLs on the page (skip thumbs-only servers)
            const fromPage = this.detectFromPage();
            if (fromPage) { this.cache(fromPage); return fromPage; }

            // 3. Hardcoded fallback
            const fallback = "https://wimg.rule34.xxx/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);
                if (this.THUMBS_ONLY.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) {
            cfgSet("imageServerBase", base);
            cfgSet("imageServerExpiry", 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%;
}

*, *::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
   Uses column-count for true browser-native masonry layout.
   No JS measurement needed – eliminates overlap and gap issues.

   Site default: div.image-list { display: flex; flex-flow: wrap; }
   Site default: .thumb { width: 200px; height: 200px; display: flex; }
   All overridden with !important.
   ================================================================ */
div.image-list {
    display: block !important;
    column-count: var(--grid-columns) !important;
    column-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: inline-block !important;
    margin: 0 0 var(--grid-gap) 0 !important;
    padding: 0 !important;
    overflow: hidden;
    break-inside: avoid !important;
    justify-content: unset !important;
    align-items: unset !important;
}

/* Profile page wrapper spans (the outer span with inline grid style) */
div#content > div[style*="float: left"] span[style*="display: grid"] {
    display: inline-block !important;
    grid-template-rows: unset !important;
    width: 100% !important;
    break-inside: avoid !important;
    margin-bottom: var(--grid-gap) !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;
    border: none !important;
}

img.preview {
    margin: 0 !important;
}

.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; }
}
`;
        }
    };

    //  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(`https://ac.rule34.xxx/autocomplete.php?q=${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(),   // hash -> extension
        retryQueue: new Map(), // src -> { img, retries, timer }
        serverBase: "",
        observer: null,
        MAX_RETRIES: 3,
        RETRY_DELAYS: [2000, 5000, 12000], // exponential-ish backoff

        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 });
        },

        /**
         * Prevent the browser from loading sample-sized images.
         * The site sometimes uses JS to swap thumbnail src to sample URLs.
         * We intercept that by setting loading="lazy" on thumbnails early
         * and letting our own replacement handle the upgrade.
         */
        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);
            };
            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);
            };
            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);
            }

            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();
    }
})();