Rule34 Enhanced Dark Gallery

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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