Booru Tag Search

Modernize and enhance search bar and tag input for booru sites, with modular site-specific configurations, dynamic cheat sheets with caching, e621-specific order syntax, and improved layout for rule34.xxx and e621.net

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// THIS FILE IS AUTO-GENERATED — DO NOT MANUALLY EDIT.
// Source: booru-enhanced-search/index.user.js

// ==UserScript==
// @name         Booru Tag Search
// @namespace    codeberg.org/0xdev/booru-tag-search
// @version      2.3.2
// @description  Modernize and enhance search bar and tag input for booru sites, with modular site-specific configurations, dynamic cheat sheets with caching, e621-specific order syntax, and improved layout for rule34.xxx and e621.net
// @author       0xdev
// @license      LGPL-3.0-or-later
// @match        *://*.booru.org/*
// @match        *://*.booru.com/*
// @match        *://*.booru.ca/*
// @match        *://*.booru.xxx/*
// @match        *://rule34.xxx/*
// @match        *://safebooru.org/*
// @match        *://danbooru.donmai.us/*
// @match        *://e621.net/*
// @grant        none
// ==/UserScript==

// --- Awesomplete hijack: always attach instance to input ---
(function hijackAwesomplete() {
    const origAwesomplete = window.Awesomplete;
    if (typeof origAwesomplete === "function") {
        window.Awesomplete = function AwesompleteHijack(input, options) {
            const instance = new origAwesomplete(input, options);
            input.awesomplete = instance;
            return instance;
        };
        // Copy prototype and static properties
        Object.setPrototypeOf(window.Awesomplete, origAwesomplete);
        window.Awesomplete.prototype = origAwesomplete.prototype;
    }
})();

(function main() {
    'use strict';

    // --- Configuration ---
    const GELBOORU_CHEATSHEET = `
        <section><h4>Basic Search</h4>
            <ul>
                <li><code>tag1 tag2</code> — Posts with <b>tag1</b> and <b>tag2</b></li>
                <li><code>( tag1 ~ tag2 )</code> — Posts with <b>tag1</b> or <b>tag2</b></li>
                <li><code>-tag1</code> — Posts without <b>tag1</b></li>
                <li><code>ta*1</code> — Tags starting with <b>ta</b> and ending with <b>1</b></li>
            </ul>
        </section>
        <section><h4>Metatags</h4>
            <ul>
                <li><code>user:bob</code> — Posts by user <b>bob</b></li>
                <li><code>rating:questionable</code> — Rated <b>questionable</b></li>
                <li><code>score:>=10</code> — Score 10 or higher</li>
                <li><code>width:>=1000</code> — Width 1000px or more</li>
                <li><code>height:>1000</code> — Height greater than 1000px</li>
                <li><code>parent:1234</code> — Has parent <b>1234</b></li>
                <li><code>md5:foo</code> — Posts with MD5 hash <b>foo</b></li>
            </ul>
        </section>
        <section><h4>Sorting</h4>
            <ul>
                <li><code>sort:updated:desc</code> — Sort by updated (descending)</li>
                <li><code>sort:score:asc</code> — Sort by score (ascending)</li>
                <li>Other types: <code>id</code>, <code>rating</code>, <code>user</code>, <code>height</code>, <code>width</code>, <code>parent</code>, <code>source</code></li>
            </ul>
        </section>`;

    const GELBOORU_SORT_OPTIONS = [
        { value: 'score', label: 'Score' },
        { value: 'updated', label: 'Updated' },
        { value: 'id', label: 'ID' },
        { value: 'rating', label: 'Rating' },
        { value: 'user', label: 'User' },
        { value: 'height', label: 'Height' },
        { value: 'width', label: 'Width' },
        { value: 'parent', label: 'Parent' },
        { value: 'source', label: 'Source' }
    ];
    const GELBOORU_RATINGS = [
        { value: 'safe', label: 'Safe' },
        { value: 'questionable', label: 'Questionable' },
        { value: 'explicit', label: 'Explicit' }
    ];
    const GELBOORU_METATAG_REGEX = /^(sort:|rating:|user:|parent:|score:|md5:|width:|height:|source:|\( rating:)/;

    const SITE_CONFIG = {
        "e621.net": {
            site: 'e621',
            sortPrefix: 'order',
            inputTag: 'textarea',
            selectors: { form: 'form.post-search-form', gallery: '#c-posts' },
            cheatSheetUrl: 'https://e621.net/help/cheatsheet',
            cheatSheetSelector: '#c-help #a-show .styled-dtext',
            placeholder: "Enter tags...",
            sortOptions: [
                { value: 'score', label: 'Score' },
                { value: 'favcount', label: 'Favorites' },
                { value: 'comment_count', label: 'Comments' },
                { value: 'id', label: 'ID' },
                { value: 'mpixels', label: 'Megapixels' },
                { value: 'filesize', label: 'File Size' },
                { value: 'landscape', label: 'Landscape' },
                { value: 'portrait', label: 'Portrait' },
                { value: 'created', label: 'Created' },
                { value: 'updated', label: 'Updated' },
                { value: 'tagcount', label: 'Tag Count' },
                { value: 'duration', label: 'Duration' },
                { value: 'random', label: 'Random' }
            ],
            ratings: GELBOORU_RATINGS,
            metatagRegex: /^(order:|rating:|user:|parent:|score:|md5:|width:|height:|source:|id:|favcount:|comment_count:|type:|date:|status:|\( rating:)/,
            cheatSheetContent: `
                <section><h4>Basics</h4>
                    <ul>
                        <li><code>cat dog</code> — Search for posts that are tagged with both cat and dog. Tags are separated by spaces.</li>
                        <li><code>red_panda african_wild_dog</code> — Words within each tag are separated by underscores.</li>
                        <li><code>~cat ~dog</code> — Search for posts that are tagged either cat or dog (or both). May not work well when combined with other syntaxes.</li>
                        <li><code>-chicken</code> — Search for posts that don't have the chicken tag.</li>
                        <li><code>fox -chicken</code> — Search for posts that are tagged with fox and are not tagged with chicken.</li>
                        <li><code>african_*</code> — Search for posts with any tag that starts with african_, such as african_wild_dog or african_golden_cat. May not work well when combined with other syntaxes. Limit one wildcard per search.</li>
                        <li><code>( ~cat ~tiger ~leopard ) ( ~dog ~wolf )</code> — Search for posts that are tagged with one (or more) of cat, tiger or leopard, and one (or more) of dog or wolf.</li>
                    </ul>
                </section>
                <section><h4>Sorting</h4>
                    <ul>
                        <li><code>order:id</code> — Oldest to newest</li>
                        <li><code>order:id_desc</code> — Newest to oldest</li>
                        <li><code>order:score</code> — Highest score first</li>
                        <li><code>order:score_asc</code> — Lowest score first</li>
                        <li><code>order:favcount</code> — Most favorites first</li>
                        <li><code>order:favcount_asc</code> — Least favorites first</li>
                        <li><code>order:comment_count</code> — Most comments first</li>
                        <li><code>order:comment_count_asc</code> — Least comments first</li>
                        <li><code>order:mpixels</code> — Largest resolution first</li>
                        <li><code>order:mpixels_asc</code> — Smallest resolution first</li>
                        <li><code>order:filesize</code> — Largest file size first</li>
                        <li><code>order:filesize_asc</code> — Smallest file size first</li>
                        <li><code>order:landscape</code> — Wide and short to tall and thin</li>
                        <li><code>order:portrait</code> — Tall and thin to wide and short</li>
                        <li><code>order:duration</code> — Video duration longest to shortest</li>
                        <li><code>order:duration_asc</code> — Video duration shortest to longest</li>
                        <li><code>order:random</code> — Orders posts randomly</li>
                    </ul>
                </section>
                <section><h4>User Metatags</h4>
                    <ul>
                        <li><code>user:Bob</code> — Posts uploaded by Bob</li>
                        <li><code>fav:Bob</code> or <code>favoritedby:Bob</code> — Posts favorited by Bob</li>
                        <li><code>voted:anything</code> — Posts you voted on. Only works while logged in.</li>
                        <li><code>votedup:anything</code> or <code>upvote:anything</code> — Posts you upvoted. Only works while logged in.</li>
                        <li><code>voteddown:anything</code> or <code>downvote:anything</code> — Posts you downvoted. Only works while logged in.</li>
                        <li><code>approver:Bob</code> — Posts approved by Bob</li>
                        <li><code>commenter:Bob</code> or <code>comm:Bob</code> — Posts commented on by Bob</li>
                        <li><code>noter:Bob</code> — Posts with notes written by Bob</li>
                    </ul>
                </section>
                <section><h4>Post Metatags - Counts</h4>
                    <ul>
                        <li><code>id:100</code> — Post with an ID of 100</li>
                        <li><code>score:100</code> — Posts with a score of 100</li>
                        <li><code>favcount:100</code> — Posts with exactly 100 favorites</li>
                        <li><code>comment_count:100</code> — Posts with exactly 100 comments</li>
                        <li><code>tagcount:2</code> — Posts with exactly 2 tags</li>
                        <li><code>gentags:2</code> — Posts with exactly 2 general tags</li>
                        <li><code>arttags:2</code> — Posts with exactly 2 artist tags</li>
                        <li><code>chartags:2</code> — Posts with exactly 2 character tags</li>
                        <li><code>copytags:2</code> — Posts with exactly 2 copyright tags</li>
                        <li><code>spectags:2</code> — Posts with exactly 2 species tags</li>
                        <li><code>invtags:2</code> — Posts with exactly 2 invalid tags</li>
                        <li><code>lortags:2</code> — Posts with exactly 2 lore tags</li>
                        <li><code>metatags:2</code> — Posts with exactly 2 meta tags</li>
                    </ul>
                </section>
                <section><h4>Rating</h4>
                    <ul>
                        <li><code>rating:safe</code> or <code>rating:s</code> — Posts rated safe</li>
                        <li><code>rating:questionable</code> or <code>rating:q</code> — Posts rated questionable</li>
                        <li><code>rating:explicit</code> or <code>rating:e</code> — Posts rated explicit</li>
                    </ul>
                </section>
                <section><h4>File Types</h4>
                    <ul>
                        <li><code>type:jpg</code> — Posts that are JPG, a type of image</li>
                        <li><code>type:png</code> — Posts that are PNG, a type of image</li>
                        <li><code>type:gif</code> — Posts that are GIF, a type of image (may be animated)</li>
                        <li><code>type:swf</code> — Posts that are Flash, a format used for animation</li>
                        <li><code>type:webm</code> — Posts that are WebM, a type of video</li>
                    </ul>
                </section>
                <section><h4>Image Size</h4>
                    <ul>
                        <li><code>width:100</code> — Posts with a width of 100 pixels</li>
                        <li><code>height:100</code> — Posts with a height of 100 pixels</li>
                        <li><code>mpixels:1</code> — Posts that are 1 megapixel (a 1000x1000 image equals 1 megapixel)</li>
                        <li><code>ratio:1.33</code> — Search for posts with a ratio of 4:3. All ratios are rounded to two digits, therefore 1.33 will return posts with a ratio of 4:3.</li>
                        <li><code>filesize:200KB</code> — Posts with a file size of 200 kilobytes. File sizes within ±5% of the value are included.</li>
                        <li><code>filesize:2MB</code> — Posts with a file size of 2 megabytes. File sizes within ±5% of the value are included.</li>
                    </ul>
                </section>
                <section><h4>Post Status</h4>
                    <ul>
                        <li><code>status:pending</code> — Posts that are waiting to be approved or deleted</li>
                        <li><code>status:active</code> — Posts that have been approved</li>
                        <li><code>status:deleted</code> — Posts that have been deleted</li>
                        <li><code>status:flagged</code> — Posts that are flagged for deletion</li>
                        <li><code>status:modqueue</code> — Posts that are pending or flagged</li>
                        <li><code>status:any</code> — All active or deleted posts</li>
                    </ul>
                </section>
                <section><h4>Dates</h4>
                    <ul>
                        <li><code>date:2012-04-27</code> or <code>date:april/27/2012</code> — Search for posts uploaded on a specific date</li>
                        <li><code>date:today</code> — Posts from today</li>
                        <li><code>date:yesterday</code> — Posts from yesterday</li>
                        <li><code>date:week</code> — Posts from the last 7 days</li>
                        <li><code>date:month</code> — Posts from the last 30 days</li>
                        <li><code>date:year</code> — Posts from the last 365 days</li>
                        <li><code>date:5_days_ago</code> — Posts from within the last 5 days</li>
                        <li><code>date:5_weeks_ago</code> — Posts from within the last 5 weeks</li>
                        <li><code>date:5_months_ago</code> — Posts from within the last 5 months</li>
                        <li><code>date:5_years_ago</code> — Posts from within the last 5 years</li>
                        <li><code>date:yesterweek</code> — Posts from last week</li>
                        <li><code>date:yestermonth</code> — Posts from last month</li>
                        <li><code>date:yesteryear</code> — Posts from last year</li>
                    </ul>
                </section>
                <section><h4>Text Searching</h4>
                    <ul>
                        <li><code>source:*example.com</code> — Posts with a source that contains "example.com", prefix matched, use wildcards as needed</li>
                        <li><code>source:none</code> — Posts without a source</li>
                        <li><code>description:whatever</code> — Posts with a description that contains the text "whatever"</li>
                        <li><code>description:"hello there"</code> — Posts with a description that contains the text "hello there"</li>
                        <li><code>note:whatever</code> — Posts with a note that contains the text "whatever"</li>
                        <li><code>delreason:*whatever</code> — Deleted posts that contain a reason with the text "whatever", prefix matched, use wildcards as needed</li>
                    </ul>
                </section>
                <section><h4>Parents and Children</h4>
                    <ul>
                        <li><code>ischild:true</code> — Posts that are a child</li>
                        <li><code>ischild:false</code> — Posts that aren't a child</li>
                        <li><code>isparent:true</code> — Posts that are a parent</li>
                        <li><code>isparent:false</code> — Posts that aren't a parent</li>
                        <li><code>parent:1234</code> — Posts with a parent of 1234</li>
                        <li><code>parent:none</code> — Posts with no parent (same as ischild:false)</li>
                    </ul>
                </section>
                <section><h4>Other</h4>
                    <ul>
                        <li><code>hassource:true</code> — Posts with a source</li>
                        <li><code>hassource:false</code> — Posts without a source</li>
                        <li><code>hasdescription:true</code> — Posts with a description</li>
                        <li><code>hasdescription:false</code> — Posts without a description</li>
                        <li><code>inpool:true</code> — Posts that are in a pool</li>
                        <li><code>inpool:false</code> — Posts that aren't in a pool</li>
                        <li><code>pool:4</code> or <code>pool:fox_and_the_grapes</code> — Posts in the pool "Fox and the Grapes"</li>
                        <li><code>set:17</code> or <code>set:cute_rabbits</code> — Posts in the set with the short name "cute_rabbits"</li>
                        <li><code>md5:02dd0...</code> — Post with the given MD5 hash. MD5 hashes will never be shared by more than one image.</li>
                        <li><code>duration:>120</code> — Videos with a duration of at least 120 seconds</li>
                    </ul>
                </section>
                <section><h4>Range Syntax</h4>
                    <ul>
                        <li><code>id:100</code> — Post with an ID of exactly 100</li>
                        <li><code>date:year..month</code> — Posts uploaded between 30 days ago and 1 year ago</li>
                        <li><code>filesize:200KB..300KB</code> — Posts with a file size between 200 kilobytes and 300 kilobytes</li>
                        <li><code>score:25..50</code> — Posts with a score between 25 and 50</li>
                        <li><code>score:>=100</code> — Posts with a score of 100 or greater (100+)</li>
                        <li><code>score:>100</code> — Posts with a score greater than 100 (101+)</li>
                        <li><code>favcount:<=100</code> — Posts with 100 or less favorites (0-100)</li>
                        <li><code>favcount:<100</code> — Posts with less than 100 favorites (0-99)</li>
                    </ul>
                </section>
            `
        },
        "rule34.xxx": {
            site: 'rule34',
            sortPrefix: 'sort',
            inputTag: 'input',
            selectors: { form: '.sidebar .tag-search form', gallery: '#post-list' },
            cheatSheetUrl: 'https://rule34.xxx/index.php?page=help&topic=cheatsheet',
            cheatSheetSelector: '.content',
            placeholder: "Enter tags...",
            sortOptions: GELBOORU_SORT_OPTIONS,
            ratings: GELBOORU_RATINGS,
            metatagRegex: GELBOORU_METATAG_REGEX,
            cheatSheetContent: GELBOORU_CHEATSHEET
        },
        // safebooru uses the same layout/API as rule34
        "safebooru.org": {
            site: 'rule34',
            sortPrefix: 'sort',
            inputTag: 'input',
            selectors: { form: '.sidebar .tag-search form', gallery: '#post-list' },
            placeholder: "Enter tags...",
            sortOptions: GELBOORU_SORT_OPTIONS,
            ratings: GELBOORU_RATINGS,
            metatagRegex: GELBOORU_METATAG_REGEX,
            cheatSheetContent: GELBOORU_CHEATSHEET
        },
        "danbooru.donmai.us": {
            site: 'danbooru',
            sortPrefix: 'order',
            inputTag: 'input',
            selectors: {
                form: 'form[action*="posts"], form.search-form, #search-form, form:has(input[name="tags"])',
                gallery: '#posts, .posts, #post-list, .post-list',
                skipPattern: /^\/posts\/\d+/
            },
            cheatSheetUrl: 'https://danbooru.donmai.us/wiki_pages/help%3Acheatsheet',
            cheatSheetSelector: '#c-wiki-pages #a-show .prose, .wiki-page-body, .dtext-container',
            placeholder: "Enter tags...",
            sortOptions: [
                { value: 'score', label: 'Score' },
                { value: 'favcount', label: 'Favorites' },
                { value: 'comment_count', label: 'Comments' },
                { value: 'id', label: 'ID' },
                { value: 'mpixels', label: 'Megapixels' },
                { value: 'filesize', label: 'File Size' },
                { value: 'landscape', label: 'Landscape' },
                { value: 'portrait', label: 'Portrait' },
                { value: 'created_at', label: 'Created' },
                { value: 'updated_at', label: 'Updated' },
                { value: 'tagcount', label: 'Tag Count' },
                { value: 'duration', label: 'Duration' },
                { value: 'random', label: 'Random' }
            ],
            ratings: [
                { value: 'general', label: 'General' },
                { value: 'sensitive', label: 'Sensitive' },
                { value: 'questionable', label: 'Questionable' },
                { value: 'explicit', label: 'Explicit' }
            ],
            metatagRegex: /^(order:|rating:|user:|parent:|score:|md5:|width:|height:|source:|id:|favcount:|comment_count:|type:|date:|status:|pool:|set:|\( rating:)/,
        },
        "default": {
            site: '',
            sortPrefix: 'sort',
            inputTag: 'input',
            placeholder: "Enter tags...",
            sortOptions: GELBOORU_SORT_OPTIONS,
            ratings: GELBOORU_RATINGS,
            metatagRegex: GELBOORU_METATAG_REGEX,
            cheatSheetContent: GELBOORU_CHEATSHEET
        }
    };

    // --- State ---
    let tags = [];
    const SCRIPT_VERSION = '2.3.1';
    const ui = {};
    const siteConfig = SITE_CONFIG[location.hostname] || SITE_CONFIG['default'];
    const site = siteConfig.site;
    const isWikiPage = location.pathname.includes('/wiki') || location.pathname.includes('/help') || document.title.includes('Wiki');

    // --- Settings State ---
    let settings = {
        showIncludeExclude: true,
        showMetatags: true,
        showAllTags: true
    };

    // Load settings from localStorage
    function loadSettings() {
        const saved = localStorage.getItem('booru-enhancer-settings');
        if (saved) {
            try {
                settings = { ...settings, ...JSON.parse(saved) };
            } catch (e) {
                console.warn('Failed to load settings:', e);
            }
        }
    }

    // Save settings to localStorage
    function saveSettings() {
        try {
            localStorage.setItem('booru-enhancer-settings', JSON.stringify(settings));
        } catch (e) {
            console.warn('Failed to save settings:', e);
        }
    }

    loadSettings();

    // --- Tag Management ---
    function addTag(tag) {
        if (!tag || tags.includes(tag)) return;
        // Cancel out contradicting include/exclude (ass + -ass = remove both)
        const opposite = tag.startsWith('-') ? tag.slice(1) : `-${tag}`;
        const oppIdx = tags.indexOf(opposite);
        if (oppIdx !== -1) {
            tags.splice(oppIdx, 1);
            renderTags();
            return;
        }
        tags.push(tag);
        renderTags();
    }

    function getTagsFromURL() {
        const params = new URLSearchParams(window.location.search);
        let tagString = params.get('tags') || '';


        // Replace + with space
        tagString = tagString.replace(/\+/g, ' ');
        // Custom split: treat parenthesized groups and tags with parentheses as single tags
        const tags = [];
        let buffer = '';
        let parenLevel = 0;
        for (let i = 0; i < tagString.length; ++i) {
            const c = tagString[i];
            if (c === '(') {
                // Check if this looks like a grouping parenthesis (preceded by space or at start)
                // vs a tag name parenthesis (preceded by underscore or alphanumeric)
                const prevChar = i > 0 ? tagString[i - 1] : ' ';
                const isGroupingParen = prevChar === ' ' || i === 0;
                
                if (isGroupingParen && parenLevel === 0 && buffer.trim()) {
                    tags.push(buffer.trim());
                    buffer = '';
                }
                parenLevel++;
                buffer += c;
            } else if (c === ')' && parenLevel > 0) {
                buffer += c;
                parenLevel--;
                // Only treat as end of group if this was a grouping parenthesis
                const nextChar = i < tagString.length - 1 ? tagString[i + 1] : ' ';
                const isGroupingParen = nextChar === ' ' || i === tagString.length - 1;
                
                if (parenLevel === 0 && isGroupingParen) {
                    tags.push(buffer.trim());
                    buffer = '';
                }
            } else if (c === ' ' && parenLevel === 0) {
                if (buffer.trim()) {
                    tags.push(buffer.trim());
                    buffer = '';
                }
            } else {
                buffer += c;
            }
        }
        if (buffer.trim()) tags.push(buffer.trim());



        // Convert old sort syntax to new unified syntax for e621/danbooru
        if (siteConfig.sortPrefix === 'order') {
            const convertedTags = tags.map(tag => {
                // Convert sort:updated_at:desc to order:updated_at
                if (tag.startsWith('sort:') && tag.includes(':')) {
                    const parts = tag.split(':');
                    if (parts.length === 3) {
                        const [, field, direction] = parts;
                        const converted = direction === 'asc' ? `order:${field}_asc` : `order:${field}`;

                        return converted;
                    }
                }
                return tag;
            });

            return convertedTags.filter(Boolean);
        }


        // Filter out Gelbooru's "all" placeholder — it's not a real tag
        return tags.filter(t => t && t !== 'all');
    }

    function syncUiOnRemove(tag) {
        const { sortSelect, orderSwitch, ratingCheckboxes } = ui;
        const isSortTag = siteConfig.sortPrefix === 'order' ? /^order:[a-z_]+(_asc)?$/.test(tag) : /^sort:[a-z_]+:(asc|desc)$/.test(tag);
        if (isSortTag && sortSelect) {
            sortSelect.value = '';
            orderSwitch.style.display = 'none';
        }
        if (/^rating:(safe|questionable|explicit|general|sensitive)$/.test(tag) && ratingCheckboxes) {
            ratingCheckboxes.forEach(cb => {
                if (cb.value === tag.split(':')[1]) cb.checked = false;
            });
        }
    }

    function createTagPill(tag, className, onRemove) {
        const tagEl = document.createElement('span');
        tagEl.className = `bse-tag-item ${className}`;
        tagEl.textContent = tag;
        const removeBtn = document.createElement('span');
        removeBtn.className = 'bse-remove-tag';
        removeBtn.textContent = '×';
        removeBtn.onclick = () => {
            if (onRemove) onRemove(tag);
            const tagIdx = tags.indexOf(tag);
            if (tagIdx !== -1) {
                tags.splice(tagIdx, 1);
                renderTags();
            }
        };
        tagEl.appendChild(removeBtn);
        return tagEl;
    }

    function renderTags() {
        const { metatagList, metatagRowWrap, includeList, excludeList,
            includeRowWrap, excludeRowWrap, allTagsRowWrap, allTagsList,
            sortSelect, orderSwitch, ratingCheckboxes } = ui;
        const metatagRegex = siteConfig.metatagRegex || /^(sort:|rating:|user:|parent:|score:|md5:|width:|height:|source:|\( rating:)/;
        const sortRegex = siteConfig.sortPrefix === 'order'
            ? /^order:([a-z_]+)(_asc)?$/
            : /^sort:([a-z_]+):(asc|desc)$/;

        // Clear all lists
        if (includeList) includeList.innerHTML = '';
        if (excludeList) excludeList.innerHTML = '';
        if (metatagList) metatagList.innerHTML = '';
        if (allTagsList) allTagsList.innerHTML = '';

        // Single pass: classify tags and build UI
        let hasIncludeTags = false;
        let hasExcludeTags = false;
        let hasMetatags = false;
        let sortType = '';
        let sortOrder = 'desc';
        let foundSort = false;
        const ratingsSet = new Set();

        for (const tag of tags) {
            const isMeta = metatagRegex.test(tag);

            if (isMeta) {
                // Metatag pill
                hasMetatags = true;
                if (metatagList && settings.showMetatags) {
                    metatagList.appendChild(createTagPill(tag, 'bse-metatag-item', syncUiOnRemove));
                }

                // Extract sort info
                const sortMatch = tag.match(sortRegex);
                if (sortMatch) {
                    sortType = sortMatch[1];
                    sortOrder = siteConfig.sortPrefix === 'order'
                        ? (sortMatch[2] ? 'asc' : 'desc')
                        : sortMatch[2];
                    foundSort = true;
                }

                // Extract rating info
                const ratingMatch = tag.match(/^rating:(safe|questionable|explicit|general|sensitive)$/);
                if (ratingMatch) {
                    ratingsSet.add(ratingMatch[1]);
                } else {
                    const ratingOrMatch = tag.match(/^\(\s*([^)]+)\s*\)$/);
                    if (ratingOrMatch) {
                        for (const part of ratingOrMatch[1].split('~')) {
                            const m = part.trim().match(/^rating:(safe|questionable|explicit|general|sensitive)$/);
                            if (m) ratingsSet.add(m[1]);
                        }
                    }
                }
            } else {
                // Regular tag — include or exclude
                const isExclude = tag.startsWith('-');
                if (isExclude) {
                    hasExcludeTags = true;
                    if (excludeList && settings.showIncludeExclude) {
                        excludeList.appendChild(createTagPill(tag, 'bse-exclude-item'));
                    }
                } else {
                    hasIncludeTags = true;
                    if (includeList && settings.showIncludeExclude) {
                        includeList.appendChild(createTagPill(tag, 'bse-include-item'));
                    }
                }

                // All-tags list
                if (allTagsList && settings.showAllTags) {
                    const cls = isExclude ? 'bse-exclude-item' : 'bse-include-item';
                    allTagsList.appendChild(createTagPill(tag, cls, syncUiOnRemove));
                }
            }
        }

        // Toggle row visibility
        if (includeRowWrap) includeRowWrap.style.display = hasIncludeTags ? '' : 'none';
        if (excludeRowWrap) excludeRowWrap.style.display = hasExcludeTags ? '' : 'none';
        if (metatagRowWrap) metatagRowWrap.style.display = hasMetatags && settings.showMetatags ? '' : 'none';
        if (allTagsRowWrap) allTagsRowWrap.style.display = (hasIncludeTags || hasExcludeTags) && settings.showAllTags ? '' : 'none';

        // Sync sort dropdown
        if (sortSelect) {
            sortSelect.value = foundSort ? sortType : '';
            if (foundSort) {
                orderSwitch.style.display = '';
                orderSwitch.dataset.state = sortOrder;
                orderSwitch.textContent = sortOrder === 'asc' ? 'Ascent \u2191' : 'Descent \u2193';
            } else {
                orderSwitch.style.display = 'none';
            }
        }

        // Sync rating checkboxes
        if (ratingCheckboxes) {
            ratingCheckboxes.forEach(cb => {
                cb.checked = ratingsSet.has(cb.value);
            });
        }
    }

    // --- Modal Creation ---
    function createModal(id, title, contentNode, actions) {
        // Remove any existing modal with this id
        const old = document.getElementById(id);
        if (old) old.remove();
        const modal = document.createElement('div');
        modal.className = 'modal';
        modal.id = id;
        modal.tabIndex = -1;
        modal.style.display = 'none';

        // Modal overlay
        const modalContent = document.createElement('div');
        modalContent.className = 'modal-content';

        // Close (×) button
        const closeX = document.createElement('button');
        closeX.className = 'modal-close';
        closeX.type = 'button';
        closeX.innerHTML = '&times;';
        closeX.title = 'Close';
        closeX.onclick = () => { modal.style.display = 'none'; };
        modalContent.appendChild(closeX);

        // Title
        const h3 = document.createElement('h3');
        h3.textContent = title;
        modalContent.appendChild(h3);

        modalContent.appendChild(contentNode);
        const actionsDiv = document.createElement('div');
        actionsDiv.className = 'modal-actions';
        actions.forEach(btn => actionsDiv.appendChild(btn));
        modalContent.appendChild(actionsDiv);
        modal.appendChild(modalContent);
        document.body.appendChild(modal);
        // Modal close on Esc/click outside
        function closeModal() { modal.style.display = 'none'; }
        modal.addEventListener('mousedown', e => { if (e.target === modal) closeModal(); });
        modal.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
        return { modal, closeModal };
    }

    // --- Export Modal ---
    function showExportModal() {
        // --- Export Data Preparation ---
        const website = location.hostname;
        const timestamp = new Date().toISOString();
        const rawTags = tags.join(' ');
        const jsonExport = JSON.stringify({ website, tags: [...tags], timestamp }, null, 2);

        // --- Modal Elements ---
        const tabWrap = document.createElement('div');
        tabWrap.className = 'modal-tabs';
        const rawTab = document.createElement('button');
        rawTab.textContent = 'Raw';
        rawTab.type = 'button';
        rawTab.className = 'modal-tab';
        const jsonTab = document.createElement('button');
        jsonTab.textContent = 'JSON';
        jsonTab.type = 'button';
        jsonTab.className = 'modal-tab';
        tabWrap.appendChild(rawTab);
        tabWrap.appendChild(jsonTab);

        const textarea = document.createElement('textarea');
        textarea.readOnly = true;
        textarea.className = 'modal-pastebin';
        textarea.value = rawTags;

        // --- Action Buttons ---
        const copyBtn = document.createElement('button');
        copyBtn.textContent = 'Copy';
        copyBtn.type = 'button';
        copyBtn.onclick = function () {
            navigator.clipboard.writeText(textarea.value);
        };

        const exportBtn = document.createElement('button');
        exportBtn.textContent = 'Export to file';
        exportBtn.type = 'button';
        exportBtn.style.display = 'none';
        exportBtn.onclick = function () {
            const blob = new Blob([jsonExport], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'booru-tags.json';
            document.body.appendChild(a);
            a.click();
            setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
        };

        const closeBtn = document.createElement('button');
        closeBtn.textContent = 'Close';
        closeBtn.type = 'button';

        // --- Tab Switch Logic ---
        function showRaw() {
            textarea.value = rawTags;
            copyBtn.style.display = '';
            exportBtn.style.display = 'none';
            rawTab.classList.add('active');
            jsonTab.classList.remove('active');
        }
        function showJSON() {
            textarea.value = jsonExport;
            copyBtn.style.display = 'none';
            exportBtn.style.display = '';
            rawTab.classList.remove('active');
            jsonTab.classList.add('active');
        }
        rawTab.onclick = showRaw;
        jsonTab.onclick = showJSON;

        // --- Modal Content (Refactored) ---
        const formWrap = document.createElement('div');
        formWrap.className = 'modal-form';
        formWrap.appendChild(tabWrap);
        formWrap.appendChild(textarea);
        // The modal-actions div is handled by createModal, so do not add buttons here

        // --- Modal Actions ---
        const actions = [copyBtn, exportBtn, closeBtn];

        let modalObj = createModal('modal-export', 'Export Tags', formWrap, actions);
        closeBtn.onclick = modalObj.closeModal;
        modalObj.modal.style.display = 'flex';
        modalObj.modal.focus();
        // Default to raw view
        showRaw();
    }

    // --- Settings Modal ---
    function showSettingsModal() {
        const formWrap = document.createElement('div');
        formWrap.className = 'modal-form settings-form';

        // Include/Exclude toggle
        const includeExcludeRow = document.createElement('div');
        includeExcludeRow.className = 'settings-row';
        const includeExcludeLabel = document.createElement('label');
        includeExcludeLabel.className = 'settings-label';
        const includeExcludeCheckbox = document.createElement('input');
        includeExcludeCheckbox.type = 'checkbox';
        includeExcludeCheckbox.checked = settings.showIncludeExclude;
        includeExcludeLabel.appendChild(includeExcludeCheckbox);
        includeExcludeLabel.appendChild(document.createTextNode(' Show Include/Exclude Tags'));
        includeExcludeRow.appendChild(includeExcludeLabel);

        // Metatags toggle
        const metatagsRow = document.createElement('div');
        metatagsRow.className = 'settings-row';
        const metatagsLabel = document.createElement('label');
        metatagsLabel.className = 'settings-label';
        const metatagsCheckbox = document.createElement('input');
        metatagsCheckbox.type = 'checkbox';
        metatagsCheckbox.checked = settings.showMetatags;
        metatagsLabel.appendChild(metatagsCheckbox);
        metatagsLabel.appendChild(document.createTextNode(' Show Metatags'));
        metatagsRow.appendChild(metatagsLabel);

        // All Tags toggle
        const allTagsRow = document.createElement('div');
        allTagsRow.className = 'settings-row';
        const allTagsLabel = document.createElement('label');
        allTagsLabel.className = 'settings-label';
        const allTagsCheckbox = document.createElement('input');
        allTagsCheckbox.type = 'checkbox';
        allTagsCheckbox.checked = settings.showAllTags;
        allTagsLabel.appendChild(allTagsCheckbox);
        allTagsLabel.appendChild(document.createTextNode(' Show All Tags'));
        allTagsRow.appendChild(allTagsLabel);

        formWrap.appendChild(includeExcludeRow);
        formWrap.appendChild(metatagsRow);
        formWrap.appendChild(allTagsRow);

        // Action buttons
        const saveBtn = document.createElement('button');
        saveBtn.textContent = 'Save';
        saveBtn.type = 'button';
        saveBtn.onclick = function () {
            settings.showIncludeExclude = includeExcludeCheckbox.checked;
            settings.showMetatags = metatagsCheckbox.checked;
            settings.showAllTags = allTagsCheckbox.checked;
            saveSettings();
            renderTags();
            modalObj.closeModal();
        };

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = 'Cancel';
        cancelBtn.type = 'button';

        const actions = [saveBtn, cancelBtn];

        let modalObj = createModal('modal-settings', 'Settings', formWrap, actions);
        cancelBtn.onclick = modalObj.closeModal;
        modalObj.modal.style.display = 'flex';
        modalObj.modal.focus();
    }

    // --- Dynamic Cheat Sheet Cache ---
    const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds

    async function fetchCheatSheet(url) {
        try {
            const response = await fetch(url, {
                credentials: 'same-origin',
                headers: {
                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
                }
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');

            const selector = siteConfig.cheatSheetSelector || null;
            const cheatsheetSection = selector && doc.querySelector(selector);
            const content = cheatsheetSection ? cleanCheatSheetHTML(cheatsheetSection) : '';

            return content;
        } catch (error) {
            console.error('Failed to fetch cheat sheet:', error);
            return null;
        }
    }

    function cleanCheatSheetHTML(element) {
        const el = element.cloneNode(true);

        // Remove TOC sections and navigation
        el.querySelectorAll('[class*="toc"], [id*="toc"], nav').forEach(n => n.remove());
        el.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(h => {
            if (/table of contents|contents/i.test(h.textContent)) {
                let next = h.nextElementSibling;
                h.remove();
                while (next && !/^H[1-6]$/.test(next.tagName)) {
                    const rm = next;
                    next = next.nextElementSibling;
                    rm.remove();
                }
            }
        });

        // Remove images and scripts
        el.querySelectorAll('img, script, style, iframe').forEach(n => n.remove());

        // Strip all inline styles and classes from content, keep structure
        el.querySelectorAll('*').forEach(n => {
            n.removeAttribute('style');
            n.removeAttribute('class');
            n.removeAttribute('id');
        });

        // Tag code elements with chip classes based on content
        el.querySelectorAll('code').forEach(code => {
            const text = code.textContent.trim();
            if (text.startsWith('-')) code.classList.add('bse-chip-exclude');
            else if (/^[a-z_]+:/.test(text) || text.startsWith('(')) code.classList.add('bse-chip-meta');
            else code.classList.add('bse-chip-include');
        });

        return `<div class="dynamic-cheatsheet">${el.innerHTML}</div>`;
    }

    async function getCachedCheatSheet(site) {
        const cacheKey = `booru-cheatsheet-${site}`;
        const cached = localStorage.getItem(cacheKey);

        if (cached) {
            try {
                const data = JSON.parse(cached);
                const now = Date.now();
                if (data.timestamp && (now - data.timestamp) < CACHE_DURATION && data.version === SCRIPT_VERSION) {
                    return data.content;
                }
            } catch (e) {
                console.error('Failed to parse cached cheat sheet:', e);
            }
        }

        const url = siteConfig.cheatSheetUrl || '';
        if (url) {
            const content = await fetchCheatSheet(url);
            if (content) {
                // Cache the result
                const cacheData = {
                    content: content,
                    timestamp: Date.now(),
                    url: url,
                    version: SCRIPT_VERSION
                };
                try {
                    localStorage.setItem(cacheKey, JSON.stringify(cacheData));
                } catch (e) {
                    console.error('Failed to cache cheat sheet:', e);
                }
                return content;
            }
        }

        // Fallback to static content
        return null;
    }

    function clearCheatSheetCache() {
        const sites = ['e621', 'rule34', 'danbooru'];
        sites.forEach(site => {
            const cacheKey = `booru-cheatsheet-${site}`;
            localStorage.removeItem(cacheKey);
        });
    }

    // --- Cheat Sheet Modal ---
    async function showCheatSheetModal() {
        // Show loading state
        const loadingDiv = document.createElement('div');
        loadingDiv.className = 'modal-doc';
        loadingDiv.innerHTML = '<p style="text-align: center;">Loading cheat sheet...</p>';

        const closeBtn = document.createElement('button');
        closeBtn.textContent = 'Close';
        closeBtn.type = 'button';

        let modalObj = createModal('modal-cheat', 'Cheat Sheet', loadingDiv, [closeBtn]);
        closeBtn.onclick = modalObj.closeModal;
        modalObj.modal.style.display = 'flex';
        modalObj.modal.focus();

        // Try to get dynamic content
        let cheatSheetContent = null;
        if (site) {
            cheatSheetContent = await getCachedCheatSheet(site);
        }

        // If dynamic fetch failed, use static content
        if (!cheatSheetContent) {
            cheatSheetContent = siteConfig.cheatSheetContent || `
          <section><h4>Basic Search</h4>
            <ul>
              <li><code>tag1 tag2</code> — Posts with <b>tag1</b> and <b>tag2</b></li>
              <li><code>( tag1 ~ tag2 )</code> — Posts with <b>tag1</b> or <b>tag2</b></li>
              <li><code>night~</code> — Fuzzy search for <b>night</b> (e.g. <b>night</b>, <b>fight</b>, <b>bright</b>)</li>
              <li><code>-tag1</code> — Exclude posts with <b>tag1</b></li>
              <li><code>ta*1</code> — Tags starting with <b>ta</b> and ending with <b>1</b></li>
            </ul>
          </section>
          <section><h4>Metatags</h4>
            <ul>
              <li><code>user:bob</code> — Uploaded by user <b>bob</b></li>
              <li><code>md5:foo</code> — Posts with MD5 <b>foo</b></li>
              <li><code>md5:foo*</code> — MD5 starts with <b>foo</b></li>
              <li><code>rating:questionable</code> — Rated <b>questionable</b></li>
              <li><code>-rating:questionable</code> — Not rated <b>questionable</b></li>
              <li><code>parent:1234</code> — Has parent <b>1234</b> (includes 1234)</li>
              <li><code>width:>=1000 height:>1000</code> — Width ≥ 1000, Height > 1000</li>
              <li><code>score:>=10</code> — Score ≥ 10</li>
            </ul>
          </section>
          <section><h4>Sorting</h4>
            <ul>
              <li><code>sort:updated:desc</code> — Sort by <b>updated</b> (descending)</li>
              <li>Other sortable types: <code>id</code>, <code>score</code>, <code>rating</code>, <code>user</code>, <code>height</code>, <code>width</code>, <code>parent</code>, <code>source</code>, <code>updated</code></li>
              <li>Can be sorted by both <b>asc</b> or <b>desc</b></li>
            </ul>
          </section>
          <section><h4>Notes</h4>
            <ul>
              <li>Combining the same metatags (with colons) usually does not work.</li>
              <li>You can combine different metatags (e.g. <code>rating:questionable parent:100</code>).</li>
            </ul>
          </section>
        `;
        }

        // Update modal content
        const docWrap = document.createElement('div');
        docWrap.className = 'modal-doc';
        docWrap.innerHTML = cheatSheetContent;

        // Add reference link and cache info
        const infoBar = document.createElement('div');
        infoBar.className = 'cheatsheet-info-bar';
        infoBar.className = 'cheatsheet-info-bar';

        const refLink = document.createElement('a');
        refLink.href = siteConfig.cheatSheetUrl || '#';
        refLink.target = '_blank';
        refLink.textContent = 'View on site →';
        refLink.className = 'cheatsheet-ref-link';

        const cacheInfo = document.createElement('span');
        cacheInfo.className = 'cheatsheet-cache-info';

        // Check cache status
        const cacheKey = `booru-cheatsheet-${site}`;
        const cached = localStorage.getItem(cacheKey);
        if (cached) {
            try {
                const data = JSON.parse(cached);
                const age = Date.now() - data.timestamp;
                const days = Math.floor(age / (24 * 60 * 60 * 1000));
                const hours = Math.floor((age % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
                cacheInfo.textContent = `Cached: ${days}d ${hours}h ago`;
            } catch (e) {
                cacheInfo.textContent = 'Using static content';
            }
        } else {
            cacheInfo.textContent = 'Using static content';
        }

        const flushBtn = document.createElement('button');
        flushBtn.textContent = 'Refresh Cache';
        flushBtn.type = 'button';
        flushBtn.className = 'cheatsheet-flush-btn';
        flushBtn.onclick = async () => {
            clearCheatSheetCache();
            modalObj.closeModal();
            await showCheatSheetModal(); // Reload the modal
        };

        infoBar.appendChild(refLink);
        infoBar.appendChild(cacheInfo);
        infoBar.appendChild(flushBtn);

        docWrap.appendChild(infoBar);

        // Update modal with new content
        const modalContent = modalObj.modal.querySelector('.modal-content');
        const oldDoc = modalContent.querySelector('.modal-doc');
        if (oldDoc) {
            oldDoc.replaceWith(docWrap);
        }
    }

    function createCollapsibleRow(prefix, title, startExpanded) {
        const rowWrap = document.createElement('div');
        rowWrap.className = `bse-${prefix}-row-wrap`;
        const rowHeader = document.createElement('div');
        rowHeader.className = `bse-${prefix}-row-header`;
        const rowTitle = document.createElement('span');
        rowTitle.textContent = title;
        const toggle = document.createElement('button');
        toggle.type = 'button';
        toggle.className = `bse-${prefix}-toggle`;
        toggle.textContent = startExpanded ? 'See less' : 'See more';
        rowHeader.appendChild(rowTitle);
        rowHeader.appendChild(toggle);
        const list = document.createElement('div');
        list.className = `bse-${prefix}-list`;
        rowWrap.appendChild(rowHeader);
        rowWrap.appendChild(list);
        let expanded = startExpanded;
        list.style.display = expanded ? '' : 'none';
        toggle.onclick = () => {
            expanded = !expanded;
            list.style.display = expanded ? '' : 'none';
            toggle.textContent = expanded ? 'See less' : 'See more';
        };
        return { rowWrap, list };
    }

    // --- Search Bar Creation ---
    function createSearchSection(site) {
        const centerWrap = document.createElement('div');
        centerWrap.className = 'bse-center-wrap';

        const searchForm = document.createElement('form');
        searchForm.className = 'bse-search-form';
        searchForm.method = 'GET';
        searchForm.action = '';

        const searchBarContainer = document.createElement('div');
        searchBarContainer.className = 'bse-modern-searchbar';

        const inputTag = siteConfig.inputTag || 'input';
        const searchInput = document.createElement(inputTag);
        searchInput.name = 'tags';
        searchInput.placeholder = siteConfig.placeholder || 'Enter tags...';
        searchInput.className = 'bse-search-input';
        if (inputTag === 'textarea') {
            searchInput.rows = 1;
            searchInput.id = 'tags';
        } else {
            searchInput.type = 'text';
        }

        // --- Searchbar Buttons ---
        const exportBtn = document.createElement('button');
        exportBtn.type = 'button';
        exportBtn.textContent = 'Export';
        exportBtn.className = 'bse-export-btn';
        exportBtn.title = 'Export tags';
        exportBtn.onclick = showExportModal;

        const cheatBtn = document.createElement('button');
        cheatBtn.type = 'button';
        cheatBtn.textContent = '?';
        cheatBtn.className = 'bse-cheat-btn';
        cheatBtn.title = 'Show cheat sheet';
        cheatBtn.onclick = showCheatSheetModal;

        const settingsBtn = document.createElement('button');
        settingsBtn.type = 'button';
        settingsBtn.textContent = '⚙';
        settingsBtn.className = 'bse-settings-btn';
        settingsBtn.title = 'Settings';
        settingsBtn.onclick = showSettingsModal;

        const searchButton = document.createElement('button');
        searchButton.type = 'submit';
        searchButton.textContent = 'Search';
        searchButton.className = 'bse-search-button';

        // --- New Sort/Order/Ratings Row ---
        const sortRow = document.createElement('div');
        sortRow.className = 'bse-sort-row';
        // Sort dropdown
        const sortSelect = document.createElement('select');
        sortSelect.className = 'bse-sort-select';
        const emptyOption = document.createElement('option');
        emptyOption.value = '';
        emptyOption.textContent = 'Sort';
        sortSelect.appendChild(emptyOption);
        // Use site-specific sort options or fallback to defaults
        const sortOptions = siteConfig.sortOptions || [
            { value: 'score', label: 'Score' },
            { value: 'updated', label: 'Updated' },
            { value: 'id', label: 'ID' },
            { value: 'rating', label: 'Rating' },
            { value: 'user', label: 'User' },
            { value: 'height', label: 'Height' },
            { value: 'width', label: 'Width' },
            { value: 'parent', label: 'Parent' },
            { value: 'source', label: 'Source' }
        ];
        sortOptions.forEach(opt => {
            const option = document.createElement('option');
            option.value = opt.value;
            option.textContent = opt.label;
            sortSelect.appendChild(option);
        });
        sortRow.appendChild(sortSelect);
        // Order switch
        const orderSwitch = document.createElement('button');
        orderSwitch.type = 'button';
        orderSwitch.className = 'bse-order-switch';
        orderSwitch.dataset.state = 'desc';
        orderSwitch.textContent = 'Descent \u2193';
        orderSwitch.style.display = 'none';
        orderSwitch.onclick = function () {
            if (orderSwitch.dataset.state === 'desc') {
                orderSwitch.dataset.state = 'asc';
                orderSwitch.textContent = 'Ascent \u2191';
            } else {
                orderSwitch.dataset.state = 'desc';
                orderSwitch.textContent = 'Descent \u2193';
            }
        };
        sortRow.appendChild(orderSwitch);
        // Show/hide order switch based on sort selection
        sortSelect.addEventListener('change', function () {
            if (sortSelect.value) {
                orderSwitch.style.display = '';
            } else {
                orderSwitch.style.display = 'none';
            }
        });
        // Ratings checkboxes
        const ratings = siteConfig.ratings || [
            { value: 'safe', label: 'Safe' },
            { value: 'questionable', label: 'Questionable' },
            { value: 'explicit', label: 'Explicit' }
        ];
        ratings.forEach(r => {
            const label = document.createElement('label');
            label.className = 'bse-rating-label';
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.value = r.value;
            checkbox.className = 'bse-rating-checkbox';
            label.appendChild(checkbox);
            label.appendChild(document.createTextNode(' ' + r.label));
            sortRow.appendChild(label);
        });

        // Cache rating checkboxes — queried once, reused everywhere
        const ratingCheckboxes = sortRow.querySelectorAll('.bse-rating-checkbox');

        // --- Collapsible Tag Rows ---
        const { rowWrap: includeRowWrap, list: includeList } = createCollapsibleRow('include', 'Include Tags', true);
        const { rowWrap: excludeRowWrap, list: excludeList } = createCollapsibleRow('exclude', 'Exclude Tags', true);
        const { rowWrap: metatagRowWrap, list: metatagList } = createCollapsibleRow('metatag', 'Metatags', false);
        const { rowWrap: allTagsRowWrap, list: allTagsList } = createCollapsibleRow('all-tags', 'All Tags', true);

        // Track if user is navigating autocomplete with arrow keys
        let autocompleteNavigating = false;

        // --- Event Bindings ---
        function bindInputEvents() {
            searchInput.addEventListener('keydown', function (e) {
                if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
                    autocompleteNavigating = true;
                }
            });
            searchInput.addEventListener('input', function (e) {
                autocompleteNavigating = false;
                let value = searchInput.value;
                // Only split on comma or space if not inside parentheses
                const endsWithSeparator = /[ ,]$/.test(value);

                if (endsWithSeparator) {
                    // Parse the input to check if we're inside a parenthetical group
                    // This handles both grouping parentheses like "( rating:safe ~ rating:questionable )"
                    // and tag name parentheses like "silvia_(artist)"
                    let parenLevel = 0;
                    let inGroupParens = false;
                    let lastChar = '';

                    for (let i = 0; i < value.length - 1; i++) { // -1 to exclude the trailing separator
                        const char = value[i];
                        if (char === '(' && (i === 0 || lastChar === ' ')) {
                            // Opening paren at start or after space - likely a grouping paren
                            parenLevel++;
                            inGroupParens = true;
                        } else if (char === ')' && parenLevel > 0) {
                            parenLevel--;
                            if (parenLevel === 0) {
                                inGroupParens = false;
                            }
                        }
                        if (char !== ' ') lastChar = char;
                    }

                    // Only split if we're not inside grouping parentheses
                    if (parenLevel === 0 && !inGroupParens) {
                        value = value.replace(/[ ,]+$/, '').trim();
                        addTag(value);
                        searchInput.value = '';
                    }
                }
            });
            // Only submit form on Enter if input is empty and there are tags
            searchInput.addEventListener('keydown', function (e) {
                if (e.key === 'Enter' && !e.shiftKey) {
                    if (autocompleteNavigating) {
                        // Let autocomplete handle Enter
                        return;
                    }
                    // If input is empty and there are tags, submit
                    if (searchInput.value.trim() === '' && tags.length > 0) {
                        e.preventDefault();
                        searchInput.value = tags.join(' ');
                        searchForm.submit();
                    } else if (searchInput.value.trim() !== '') {
                        // No suggestion selected, add raw input as tag
                        e.preventDefault();
                        const value = searchInput.value.trim();
                        addTag(value);
                        searchInput.value = '';
                        autocompleteNavigating = false;
                        // Close/hide autocomplete dropdown
                        if (site === 'rule34' && searchInput.awesomplete) {
                            searchInput.awesomplete.close();
                        }
                        if (site === 'danbooru') {
                            const $ = window.jQuery;
                            if ($ && $(searchInput).autocomplete('instance')) {
                                $(searchInput).autocomplete('close');
                            }
                        }
                    }
                }
            });
        }

        function buildAndApplyMetatags() {
            const prefix = siteConfig.sortPrefix;
            const prefixes = [prefix + ':', 'rating:', '( rating:'];
            tags = tags.filter(tag => !prefixes.some(p => tag.startsWith(p)));
            if (sortSelect.value) {
                const order = orderSwitch.dataset.state || 'desc';
                if (prefix === 'order') {
                    tags.push(order === 'asc' ? `order:${sortSelect.value}_asc` : `order:${sortSelect.value}`);
                } else {
                    tags.push(`sort:${sortSelect.value}:${order}`);
                }
            }
            const checkedRatings = [...ratingCheckboxes].filter(cb => cb.checked).map(cb => cb.value);
            if (checkedRatings.length === 1) {
                tags.push(`rating:${checkedRatings[0]}`);
            } else if (checkedRatings.length > 1) {
                tags.push('( ' + checkedRatings.map(r => `rating:${r}`).join(' ~ ') + ' )');
            }
        }

        function bindFormEvents() {
            searchForm.addEventListener('submit', function (e) {
                e.preventDefault();
                buildAndApplyMetatags();
                if (tags.length > 0) {
                    searchInput.value = tags.join(' ');
                }
                searchForm.submit();
            });
        }

        // --- Awesomplete integration for rule34 ---
        function bindAwesompleteEvents() {
            if (site === 'rule34') {
                searchInput.addEventListener('awesomplete-selectcomplete', function (e) {
                    const value = searchInput.value.trim();
                    addTag(value);
                    searchInput.value = '';
                });
            }
        }

        // --- e621 Autocomplete integration (vanilla JS — e621 does NOT use jQuery UI) ---
        const E621_TAG_COLORS = {
            0: '#b4b7b4', // general
            1: '#f2ac08', // artist
            3: '#c797ff', // copyright
            4: '#00aa00', // character
            5: '#ed5d1f', // species
            6: '#ff3d3d', // invalid
            7: '#eeeeee', // meta
            8: '#228822', // lore
        };

        function bindE621Autocomplete() {
            if (site !== 'e621') return;
            let dropdown = document.createElement('ul');
            dropdown.className = 'bse-ac-dropdown';
            // Styled via .bse-ac-dropdown in stylesheet
            document.body.appendChild(dropdown);
            let debounceTimer = null;
            let selectedIdx = -1;

            const positionDropdown = () => {
                const rect = searchInput.getBoundingClientRect();
                dropdown.style.left = rect.left + window.scrollX + 'px';
                dropdown.style.top = rect.bottom + window.scrollY + 'px';
                dropdown.style.width = Math.max(rect.width, 300) + 'px';
            };

            const hideDropdown = () => { dropdown.style.display = 'none'; selectedIdx = -1; };

            const showResults = (items) => {
                dropdown.replaceChildren();
                if (!items.length) { hideDropdown(); return; }
                items.forEach((item, i) => {
                    const li = document.createElement('li');
                    const color = E621_TAG_COLORS[item.category] || '#b4b7b4';
                    const tagName = item.antecedent_name || item.name;
                    const alias = item.antecedent_name ? item.name : null;
                    const count = item.post_count ? formatPostCount(item.post_count) : '';
                    li.innerHTML = renderAutocompleteItemHTML(tagName, alias, count, color);
                    li.addEventListener('mousedown', (e) => {
                        e.preventDefault();
                        addTag(item.name);
                        searchInput.value = '';
                        hideDropdown();
                    });
                    li.addEventListener('mouseenter', () => {
                        selectedIdx = i;
                        highlightItem();
                    });
                    dropdown.appendChild(li);
                });
                positionDropdown();
                dropdown.style.display = 'block';
            };

            const highlightItem = () => {
                [...dropdown.children].forEach((li, i) => {
                    li.style.background = i === selectedIdx ? 'rgba(255,255,255,.1)' : 'transparent';
                });
            };

            let abortCtrl = null;
            searchInput.addEventListener('input', () => {
                clearTimeout(debounceTimer);
                const term = searchInput.value.trim();
                if (term.length < 3) { hideDropdown(); return; }
                debounceTimer = setTimeout(() => {
                    if (abortCtrl) abortCtrl.abort();
                    abortCtrl = new AbortController();
                    fetch(`/tags/autocomplete.json?search[name_matches]=${encodeURIComponent(term)}&expiry=7`, { signal: abortCtrl.signal })
                        .then(r => r.json())
                        .then(showResults)
                        .catch(e => { if (e.name !== 'AbortError') hideDropdown(); });
                }, 225);
            });

            searchInput.addEventListener('keydown', (e) => {
                if (dropdown.style.display === 'none') return;
                const items = dropdown.children;
                if (e.key === 'ArrowDown') { e.preventDefault(); selectedIdx = Math.min(selectedIdx + 1, items.length - 1); highlightItem(); }
                else if (e.key === 'ArrowUp') { e.preventDefault(); selectedIdx = Math.max(selectedIdx - 1, 0); highlightItem(); }
                else if (e.key === 'Enter' && selectedIdx >= 0) {
                    e.preventDefault();
                    items[selectedIdx]?.dispatchEvent(new MouseEvent('mousedown'));
                }
                else if (e.key === 'Escape') { hideDropdown(); }
            });

            searchInput.addEventListener('blur', () => setTimeout(hideDropdown, 150));
        }

        // --- Danbooru jQuery UI Autocomplete integration ---
        // Danbooru tag category colors — uses site's CSS custom properties
        // Falls back to hardcoded values if vars aren't available
        const rootStyles = getComputedStyle(document.documentElement);
        const cssVar = (name, fallback) => rootStyles.getPropertyValue(name).trim() || fallback;
        const DANBOORU_TAG_COLORS = {
            0: cssVar('--blue-3', '#009be6'),     // general
            1: cssVar('--red-3', '#c00'),          // artist
            3: cssVar('--purple-3', '#c797ff'),    // copyright
            4: cssVar('--green-3', '#0a0'),        // character
            5: cssVar('--orange-3', '#fd9200'),    // meta
        };

        function formatPostCount(n) {
            if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
            if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
            return String(n);
        }

        function renderAutocompleteItemHTML(tagName, alias, count, color) {
            const aliasPart = alias ? ` <span style="color:#888;font-size:0.85em">\u2192 ${alias.replace(/_/g, ' ')}</span>` : '';
            const countPart = count ? `<span style="color:#888;font-size:0.85em;flex-shrink:0">${count}</span>` : '';
            return `<span style="color:${color};flex-grow:1">${tagName.replace(/_/g, ' ')}${aliasPart}</span>${countPart}`;
        }

        let danbooruRetries = 0;
        function bindDanbooruAutocomplete() {
            if (site !== 'danbooru') return;
            const $ = window.jQuery;
            if (!$ || !$.fn.autocomplete) {
                if (++danbooruRetries < 20) setTimeout(bindDanbooruAutocomplete, 500);
                return;
            }
            $(searchInput).autocomplete({
                delay: 100,
                minLength: 1,
                source: function (request, response) {
                    $.get('/autocomplete.json', {
                        'search[query]': request.term,
                        'search[type]': 'tag_query',
                        limit: 20,
                        version: 3
                    }).then(function (data) {
                        response(data.map(function (item) {
                            // post_count/category may be top-level or nested in item.tag
                            const tag = item.tag || {};
                            return {
                                label: item.label || item.value,
                                value: item.value,
                                category: item.category ?? tag.category ?? 0,
                                post_count: item.post_count ?? tag.post_count ?? 0,
                                antecedent: item.antecedent || null
                            };
                        }));
                    });
                }
            });
            // Custom rendering with category colors and post counts
            const acInstance = $(searchInput).autocomplete('instance');
            acInstance._renderItem = function (ul, item) {
                const color = DANBOORU_TAG_COLORS[item.category] || '#009be6';
                const count = item.post_count ? formatPostCount(item.post_count) : '';
                const tagName = item.antecedent || item.label || item.value;
                const alias = item.antecedent ? item.label : null;
                const li = $('<li>').appendTo(ul);
                li.append(
                    $('<div>').css({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', padding: '4px 8px', gap: '12px' })
                        .html(renderAutocompleteItemHTML(tagName, alias, count, color))
                );
                return li;
            };
            // Style and position the dropdown menu
            acInstance.menu.element.css({
                background: '#1a1a2e',
                border: '1px solid #333',
                maxHeight: '400px',
                overflowY: 'auto',
                zIndex: 99999,
                width: 'auto',
                minWidth: $(searchInput).outerWidth() + 'px',
                maxWidth: '500px'
            });
            // Override positioning to anchor to the search input
            acInstance._resizeMenu = function () {
                this.menu.element.outerWidth(Math.max(this.element.outerWidth(), 300));
            };
            // Intercept selection: add tag to our list instead of inserting into input
            $(searchInput).on('autocompleteselect', function (e, ui) {
                e.preventDefault();
                if (ui.item && ui.item.value) {
                    addTag(ui.item.value);
                    searchInput.value = '';
                }
            });
        }

        function syncMetatagsFromUI() {
            buildAndApplyMetatags();
            renderTags();
        }
        sortSelect.addEventListener('change', syncMetatagsFromUI);
        orderSwitch.addEventListener('click', syncMetatagsFromUI);
        ratingCheckboxes.forEach(cb => {
            cb.addEventListener('change', syncMetatagsFromUI);
        });

        // --- Assemble ---
        searchBarContainer.appendChild(exportBtn);
        searchBarContainer.appendChild(searchInput);
        searchBarContainer.appendChild(cheatBtn);
        searchBarContainer.appendChild(settingsBtn);
        searchBarContainer.appendChild(searchButton);
        searchForm.appendChild(searchBarContainer);
        // Insert the new row below the search bar, above the tag list
        searchForm.appendChild(sortRow);
        centerWrap.appendChild(searchForm);
        centerWrap.appendChild(includeRowWrap);
        centerWrap.appendChild(excludeRowWrap);
        centerWrap.appendChild(metatagRowWrap);
        centerWrap.appendChild(allTagsRowWrap);

        // --- Bind Events ---
        bindInputEvents();
        bindFormEvents();
        bindAwesompleteEvents();
        bindE621Autocomplete();
        bindDanbooruAutocomplete();

        // Store UI references for renderTags and cross-scope access
        Object.assign(ui, {
            includeList, excludeList, includeRowWrap, excludeRowWrap,
            metatagList, metatagRowWrap, allTagsList, allTagsRowWrap,
            sortSelect, orderSwitch, ratingCheckboxes
        });
        return { centerWrap, searchForm, searchInput };
    }

    function setupSite() {
        const sel = siteConfig.selectors;
        if (!sel) return false;

        // Skip pages that shouldn't have the enhanced search
        if (sel.skipPattern && sel.skipPattern.test(location.pathname)) return false;

        const originalForm = document.querySelector(sel.form);
        const gallery = document.querySelector(sel.gallery);
        if (!originalForm || !gallery) return false;

        const { centerWrap, searchForm, searchInput } = createSearchSection(site);
        searchForm.action = originalForm.action || '/posts';
        searchForm.method = originalForm.method || 'GET';
        gallery.parentNode.insertBefore(centerWrap, gallery);
        originalForm.style.display = 'none';
        tags = getTagsFromURL();
        renderTags();
        searchInput.value = '';
        return true;
    }

    function setupWiki() {
        // For wiki pages, create a simple centered search input
        const content = document.querySelector('#content') || document.querySelector('main') || document.body;
        if (!content) return false;

        // Create simple search form
        const searchWrap = document.createElement('div');
        searchWrap.className = 'bse-wiki-search-wrap';

        const searchForm = document.createElement('form');
        searchForm.action = '/posts';
        searchForm.method = 'GET';

        const searchInput = document.createElement('input');
        searchInput.type = 'text';
        searchInput.name = 'tags';
        searchInput.placeholder = 'Search posts...';
        searchInput.className = 'bse-search-input';

        const searchButton = document.createElement('button');
        searchButton.type = 'submit';
        searchButton.textContent = 'Search';
        searchButton.className = 'bse-search-button';

        searchForm.appendChild(searchInput);
        searchForm.appendChild(searchButton);
        searchWrap.appendChild(searchForm);

        // Insert at the top of content
        content.insertBefore(searchWrap, content.firstChild);

        return true;
    }

    function setupGeneric() {
        const originalForm = findSearchForm();
        if (!originalForm) return false;
        const searchField = originalForm.querySelector('input[name="tags"], textarea[name="tags"]');
        if (!searchField) return false;
        const tagList = document.createElement('div');
        tagList.className = 'bse-tag-list';
        searchField.insertAdjacentElement('afterend', tagList);
        tags = getTagsFromURL();
        renderTags();
        searchField.value = '';
        searchField.addEventListener('keydown', function (e) {
            if (e.key === 'Enter' && !e.shiftKey) {
                const value = searchField.value.trim();
                if (value) {
                    e.preventDefault();
                    if (!tags.includes(value)) {
                        tags.push(value);
                        renderTags();
                    }
                    searchField.value = '';
                } else if (tags.length > 0) {
                    e.preventDefault();
                    searchField.value = tags.join(' ');
                    originalForm.submit();
                }
            }
        });
        originalForm.addEventListener('submit', function (e) {
            if (tags.length > 0) {
                searchField.value = tags.join(' ');
            }
        });
        return true;
    }

    function findSearchForm() {
        let form = document.querySelector('.sidebar form input[name="tags"], .sidebar form textarea[name="tags"]');
        if (form) return form.closest('form');
        form = document.querySelector('form input[name="tags"], form textarea[name="tags"]');
        return form ? form.closest('form') : null;
    }

    // --- Style Injection ---
    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
        .bse-center-wrap {
            display: flex;
            flex-direction: column;
            align-items: center;
            width: 100%;
            margin-bottom: 20px;
        }
        .bse-search-form {
            width: 90vw;
            max-width: 700px;
            min-width: 220px;
            margin: 0 auto;
        }
        .bse-modern-searchbar {
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 16px;
            width: 100%;
            margin-bottom: 10px;
            justify-content: center;
        }
                .bse-export-btn, .bse-cheat-btn, .bse-settings-btn {
                    border-radius: 18px;
                    padding: 8px 18px;
                    font-size: 1.1em;
                    border: 2px solid color-mix(in srgb, currentColor 25%, transparent) !important;
                    background: color-mix(in srgb, currentColor 8%, transparent) !important;
                    color: inherit !important;
                    cursor: pointer;
                    transition: border-color 0.2s, background 0.2s;
                    white-space: nowrap;
                    flex-shrink: 0;
                }
                .bse-export-btn:hover, .bse-cheat-btn:hover, .bse-settings-btn:hover {
                    border-color: color-mix(in srgb, currentColor 45%, transparent) !important;
                    background: color-mix(in srgb, currentColor 18%, transparent) !important;
        }
        .bse-search-input {
            flex: 1 1 0%;
            min-width: 0;
            border-radius: 18px;
            padding: 8px 16px;
            font-size: 1.1em;
            border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
            background: color-mix(in srgb, currentColor 5%, transparent);
            color: inherit;
            box-shadow: 0 2px 8px 0 rgba(0,0,0,0.06);
            transition: border-color 0.2s;
            box-sizing: border-box;
            width: 90vw;
            max-width: 500px;
        }
        .bse-search-input:focus {
            border-color: #4a90e2;
            outline: none;
        }
        .bse-search-button {
            border-radius: 18px;
            padding: 8px 28px;
            font-size: 1.1em;
            border: 2px solid color-mix(in srgb, currentColor 25%, transparent) !important;
            background: color-mix(in srgb, currentColor 8%, transparent) !important;
            color: inherit !important;
            cursor: pointer;
            transition: border-color 0.2s, background 0.2s;
            white-space: nowrap;
            flex-shrink: 0;
        }
        .bse-search-button:hover {
            border-color: color-mix(in srgb, currentColor 45%, transparent) !important;
            background: color-mix(in srgb, currentColor 18%, transparent) !important;
        }
        .bse-tag-list {
            margin: 18px 0 0 0;
            padding: 0;
            display: flex;
            flex-direction: row;
            flex-wrap: wrap;
            gap: 10px;
            max-width: 1000px;
            justify-content: flex-start;
        }
        .bse-tag-item {
            border-radius: 18px;
            padding: 4px 18px;
            display: flex;
            align-items: center;
            font-size: 1.08em;
            color: inherit;
            font-weight: 500;
            transition: background 0.2s, color 0.2s, border-color 0.2s;
            /* Default neutral — overridden by type-specific classes */
            background: color-mix(in srgb, currentColor 10%, transparent);
            border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
        }
        .bse-tag-item.bse-include-item {
            background: #d1fae5 !important;
            border-color: #6ee7b7 !important;
            color: #065f46 !important;
        }
        .bse-tag-item.bse-exclude-item {
            background: #fee2e2 !important;
            border-color: #fca5a5 !important;
            color: #991b1b !important;
        }
        .bse-tag-item.bse-metatag-item {
            background: #fef9c3 !important;
            border-color: #fde68a !important;
            color: #854d0e !important;
        }
        .bse-tag-item .bse-remove-tag {
            margin-left: 12px;
            cursor: pointer;
            color: #c00;
            font-weight: bold;
            font-size: 1.1em;
            }
                .modal {
                    display: none;
                    position: fixed;
                    z-index: 1000;
                    left: 0; top: 0; right: 0; bottom: 0;
                    background: rgba(24, 28, 36, 0.55);
                    backdrop-filter: blur(2.5px);
                    align-items: center;
                    justify-content: center;
                    transition: background 0.2s;
                }
                .modal[style*="display: flex"] {
                    display: flex !important;
                }
                .modal-content {
                    background: #fff;
                    color: #222;
                    border-radius: 18px;
                    padding: 32px 28px 24px 28px;
                    min-width: 320px;
                    max-width: 800px;
                    width: 50vw;
                    margin: auto;
                    box-shadow: 0 8px 40px 0 rgba(0,0,0,0.18);
                    display: flex;
                    flex-direction: column;
                    gap: 18px;
                    position: relative;
                    font-size: 1.08em;
                    align-items: center;
                    max-height: 80vh;
                    overflow: auto;
                }
                .modal-close {
                    position: absolute;
                    top: 12px;
                    right: 16px;
                    background: none;
                    border: none;
                    font-size: 1.7em;
                    color: #888;
                    cursor: pointer;
                    z-index: 2;
                    padding: 0 8px;
                    line-height: 1;
                    transition: color 0.2s;
                }
                .modal-close:hover {
                    color: #c00;
                }
                .modal-content h3 {
                    margin: 0 0 8px 0;
                    font-size: 1.35em;
                    font-weight: bold;
                    text-align: left;
                    align-self: flex-start;
                    color: #0066cc;
                    text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
                }
                .modal-actions {
                    display: flex;
                    gap: 12px;
                    justify-content: flex-end;
                    margin-top: 8px;
                    flex-wrap: wrap;
                    width: 100%;
                }
                .modal-actions button {
                    border-radius: 16px;
                    padding: 8px 22px;
                    font-size: 1em;
                    border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
                    background: color-mix(in srgb, currentColor 8%, transparent);
                    cursor: pointer;
                    transition: border-color 0.2s, background 0.2s;
                    font-weight: 500;
                }
                .modal-actions button:hover {
                    border-color: #4a90e2;
                    background: color-mix(in srgb, currentColor 18%, transparent);
                }
                .modal-pastebin {
                    background: #f6f8fa;
                    border: 1.5px solid #e0e0e0;
                    border-radius: 10px;
                    padding: 12px 14px;
                    font-size: 1em;
                    width: 100%;
                    min-width: 220px;
                    max-width: 100%;
                    box-sizing: border-box;
                    color: inherit;
                    margin-bottom: 0;
                    font-family: 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', monospace;
                    overflow: auto;
                    box-shadow: 0 2px 8px 0 rgba(0,0,0,0.04);
                    resize: vertical;
                    height: 260px;
                    min-height: 120px;
                    max-height: 50vh;
                }
                .modal-content pre, .modal-doc {
                    background: #f6f8fa;
                    border: 1.5px solid #e0e0e0;
                    border-radius: 10px;
                    padding: 12px 14px;
                    font-size: 1em;
                    width: 100%;
                    min-width: 220px;
                    max-width: 100%;
                    box-sizing: border-box;
                    color: inherit;
                    margin-bottom: 0;
                    font-family: inherit;
                    overflow: auto;
                }
                .modal-content pre {
                    white-space: pre-wrap;
                    word-break: break-word;
                    max-height: 320px;
                    overflow: auto;
                }
                .modal-doc section {
                    margin-bottom: 18px;
                }
                .modal-doc h4 {
                    margin: 0 0 6px 0;
                    font-size: 1.08em;
                    color: #4a90e2;
                    font-weight: bold;
                }
                .dynamic-cheatsheet, .dynamic-cheatsheet * { color: inherit !important; line-height: 1.6; }
                .dynamic-cheatsheet h1, .dynamic-cheatsheet h2,
                .dynamic-cheatsheet h3, .dynamic-cheatsheet h4,
                .dynamic-cheatsheet h5, .dynamic-cheatsheet h6 {
                    color: #4a90e2 !important;
                    margin: 1.2em 0 0.5em;
                    padding-bottom: 4px;
                    border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
                }
                .dynamic-cheatsheet ul, .dynamic-cheatsheet ol {
                    list-style: none;
                    padding: 0;
                    margin: 0.5em 0;
                }
                .dynamic-cheatsheet li {
                    margin-bottom: 12px;
                    padding: 0;
                }
                .dynamic-cheatsheet p {
                    margin: 0.4em 0;
                    color: inherit;
                }
                /* Code chips — block-level so description wraps below */
                .dynamic-cheatsheet code {
                    display: inline-block;
                    margin-bottom: 4px;
                    padding: 3px 10px;
                    border-radius: 12px;
                    font-family: 'Fira Mono', Consolas, Monaco, monospace;
                    font-size: 0.88em;
                    font-weight: 600;
                }
                .dynamic-cheatsheet code.bse-chip-include {
                    background: #d1fae5 !important;
                    border: 1px solid #6ee7b7;
                    color: #065f46 !important;
                }
                .dynamic-cheatsheet code.bse-chip-exclude {
                    background: #fee2e2 !important;
                    border: 1px solid #fca5a5;
                    color: #991b1b !important;
                }
                .dynamic-cheatsheet code.bse-chip-meta {
                    background: #fef9c3 !important;
                    border: 1px solid #fde68a;
                    color: #854d0e !important;
                }
                /* Fallback for unclassified code */
                .dynamic-cheatsheet code:not([class]) {
                    background: #edf2f7 !important;
                    border: 1px solid #e2e8f0;
                    color: #2d3748 !important;
                }
                .dynamic-cheatsheet table {
                    width: 100%;
                    border-collapse: collapse;
                    margin: 0.5em 0;
                }
                .dynamic-cheatsheet td, .dynamic-cheatsheet th {
                    padding: 6px 10px;
                    border: 1px solid color-mix(in srgb, currentColor 15%, transparent);
                    text-align: left;
                }
                .dynamic-cheatsheet a {
                    color: #4a90e2 !important;
                }
                .modal-doc ul {
                    margin: 0;
                    padding: 0;
                    list-style: none !important;
                }
                .modal-doc b {
                    color: inherit;
                }
                .modal-tabs {
                    display: flex;
                    background: #f0f4fa;
                    border-radius: 999px;
                    padding: 4px;
                    gap: 8px;
                    margin-bottom: 16px;
                    width: 100%;
                    justify-content: flex-start;
                }
                .modal-tab {
                    border: none;
                    background: none;
                    border-radius: 999px;
                    padding: 8px 28px;
                    font-size: 1.08em;
                    font-weight: 500;
                    cursor: pointer;
                    transition: background 0.2s, color 0.2s, box-shadow 0.2s;
                    box-shadow: none;
                    outline: none;
                    margin-bottom: 0;
                    color: inherit;
                }
                .modal-tab.active {
                    background: #4a90e2;
                    color: #fff;
                    box-shadow: 0 2px 8px 0 rgba(74,144,226,0.12);
                }
                .modal-tab:not(.active):hover {
                    background: color-mix(in srgb, currentColor 10%, transparent);
                    color: inherit;
                }
                .modal-tab:focus {
                    outline: 2px solid #4a90e2;
                }
                /* Responsive for mobile */
                @media (max-width: 700px) {
                    .modal-content {
                        min-width: 0;
                        max-width: 98vw;
                        width: 98vw;
                        padding: 18px 2vw 12px 2vw;
                        font-size: 1em;
                        max-height: 98vh;
                    }
                    .modal-content h3 {
                        font-size: 1.08em;
                    }
                    .modal-actions {
                        flex-direction: column;
                        align-items: stretch;
                        gap: 8px;
                    }
                    .modal-pastebin, .modal-content pre, .modal-doc {
                        font-size: 0.98em;
                        padding: 8px 6px;
                        width: 98vw;
                        min-width: 0;
                        max-width: 100vw;
                        height: 120px;
                        min-height: 60px;
                        max-height: 30vh;
                    }
                    .modal-tabs {
                        width: 98vw;
                        min-width: 0;
                        max-width: 100vw;
                    }
                }
                /* --- Add width/centering for modal-tabs and modal-pastebin --- */
                .modal-tabs,
                .modal-pastebin {
                    width: 90%;
                    max-width: 760px;
                    min-width: 220px;
                    margin-left: auto;
                    margin-right: auto;
                }
                .modal-form {
                    width: 90%;
                    max-width: 760px;
                    min-width: 220px;
                    margin-left: auto;
                    margin-right: auto;
                    display: flex;
                    flex-direction: column;
                    align-items: stretch;
                    height: auto;
                    max-height: 100%;
                }
                @media (max-width: 700px) {
                    .modal-tabs,
                    .modal-pastebin {
                        width: 98vw;
                        max-width: 100vw;
                        min-width: 0;
                    }
                    .modal-form {
                        width: 98vw;
                        max-width: 100vw;
                        min-width: 0;
                        height: auto;
                        max-height: 100%;
                    }
        }
        @media (max-width: 600px) {
            body#body .bse-search-input {
                width: 100vw;
                max-width: 100vw;
            }
        }
        .bse-sort-row {
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 18px;
            width: 100%;
            margin: 8px 0 0 0;
            justify-content: flex-start;
        }
        .bse-sort-select {
            appearance: none;
            border-radius: 8px;
            padding: 8px 32px 8px 14px;
            font-size: 0.95em;
            border: 1px solid color-mix(in srgb, currentColor 25%, transparent) !important;
            background-color: color-mix(in srgb, currentColor 8%, transparent) !important;
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23999' d='M1 1l5 5 5-5'/%3E%3C/svg%3E") !important;
            background-repeat: no-repeat !important;
            background-position: right 10px center !important;
            color: inherit !important;
            cursor: pointer;
            transition: border-color 0.2s, background-color 0.2s;
            min-width: 8rem;
        }
        .bse-sort-select:hover {
            background-color: color-mix(in srgb, currentColor 18%, transparent) !important;
            border-color: color-mix(in srgb, currentColor 45%, transparent) !important;
        }
        .bse-sort-select:focus {
            border-color: #4a90e2 !important;
            outline: none !important;
        }
        .bse-sort-select option {
            background: var(--body-background-color, #1a1a2e) !important;
            color: var(--body-font-color, inherit) !important;
        }
        .bse-order-switch {
            appearance: none;
            border-radius: 8px;
            padding: 8px 14px;
            font-size: inherit;
            border: 1px solid color-mix(in srgb, currentColor 25%, transparent) !important;
            background-color: color-mix(in srgb, currentColor 8%, transparent) !important;
            color: inherit !important;
            cursor: pointer;
            transition: border-color 0.2s, background-color 0.2s;
            font-weight: 500;
            white-space: nowrap;
            min-width: 6rem;
            text-align: center;
        }
        .bse-order-switch:hover {
            background-color: color-mix(in srgb, currentColor 18%, transparent) !important;
            border-color: color-mix(in srgb, currentColor 45%, transparent) !important;
        }
        .bse-order-switch:focus {
            border-color: #4a90e2 !important;
            outline: none !important;
        }
        .bse-rating-label {
            display: flex;
            align-items: center;
            gap: 4px;
            font-size: 1.08em;
            font-weight: 500;
            color: inherit;
            background: color-mix(in srgb, currentColor 10%, transparent);
            border-radius: 10px;
            padding: 4px 12px;
            border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
            margin-right: 4px;
            cursor: pointer;
            transition: background 0.2s, border-color 0.2s;
        }
        .bse-rating-checkbox {
            accent-color: #4a90e2;
            width: 1.1em;
            height: 1.1em;
        }
        .bse-rating-label:hover {
            background: #d0eaff;
            border-color: #4a90e2;
        }
        /* Metatag Row */
        .bse-metatag-row-wrap {
            width: 100%;
            margin: 10px 0 0 0;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 4px;
        }
        .bse-metatag-row-header {
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 12px;
            font-size: 1.08em;
            font-weight: 600;
            color: inherit;
            margin-bottom: 2px;
        }
        .bse-metatag-toggle {
            border-radius: 10px;
            padding: 4px 16px;
            font-size: 1em;
            border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
            background: color-mix(in srgb, currentColor 8%, transparent);
            cursor: pointer;
            transition: border-color 0.2s, background 0.2s;
            font-weight: 500;
            color: inherit;
        }
        .bse-metatag-toggle:focus {
            border-color: #4a90e2;
            outline: none;
        }
        .bse-metatag-list {
            display: flex;
            flex-direction: row;
            flex-wrap: wrap;
            gap: 10px;
            width: 100%;
            margin-top: 2px;
            justify-content: center;
            margin-left: auto;
            margin-right: auto;
        }
        .bse-metatag-item {
            background: color-mix(in srgb, currentColor 10%, transparent);
            border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
            color: inherit;
        }
        @media (max-width: 700px) {
            .bse-sort-row {
                flex-direction: column;
                align-items: stretch;
                gap: 10px;
            }
            .bse-metatag-row-header {
                flex-direction: column;
                align-items: flex-start;
                gap: 4px;
            }
            .bse-metatag-list {
                gap: 6px;
            }
        }
        /* Include/Exclude Tag Rows */
        .bse-include-row-wrap, .bse-exclude-row-wrap {
            width: 100%;
            margin: 10px 0 0 0;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 4px;
        }
        .bse-include-row-header, .bse-exclude-row-header {
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 12px;
            font-size: 1.08em;
            font-weight: 600;
            color: inherit;
            margin-bottom: 2px;
        }
        .bse-include-toggle, .bse-exclude-toggle {
            border-radius: 10px;
            padding: 4px 16px;
            font-size: 1em;
            border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
            background: color-mix(in srgb, currentColor 8%, transparent);
            cursor: pointer;
            transition: border-color 0.2s, background 0.2s;
            font-weight: 500;
            color: inherit;
        }
        .bse-include-toggle:focus, .bse-exclude-toggle:focus {
            border-color: #4a90e2;
            outline: none;
        }
        .bse-include-list, .bse-exclude-list {
            display: flex;
            flex-direction: row;
            flex-wrap: wrap;
            gap: 10px;
            width: 100%;
            margin-top: 2px;
            justify-content: center;
            margin-left: auto;
            margin-right: auto;
        }
        .bse-include-item, .bse-exclude-item {
            background: color-mix(in srgb, currentColor 10%, transparent);
            border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
            color: inherit;
        }
        @media (max-width: 700px) {
            .bse-include-row-header, .bse-exclude-row-header {
                flex-direction: column;
                align-items: flex-start;
                gap: 4px;
            }
            .bse-include-list, .bse-exclude-list {
                gap: 6px;
            }
            .bse-all-tags-row-header {
                flex-direction: column;
                align-items: flex-start;
                gap: 4px;
            }
            .bse-all-tags-list {
                gap: 6px;
            }
        }
        /* All Tags Row */
        .bse-all-tags-row-wrap {
            width: 100%;
            margin: 10px 0 0 0;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 4px;
        }
        .bse-all-tags-row-header {
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 12px;
            font-size: 1.08em;
            font-weight: 600;
            color: inherit;
            margin-bottom: 2px;
        }
        .bse-all-tags-toggle {
            border-radius: 10px;
            padding: 4px 16px;
            font-size: 1em;
            border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
            background: color-mix(in srgb, currentColor 8%, transparent);
            cursor: pointer;
            transition: border-color 0.2s, background 0.2s;
            font-weight: 500;
            color: inherit;
        }
        .bse-all-tags-toggle:focus {
            border-color: #4a90e2;
            outline: none;
        }
        .bse-all-tags-list {
            display: flex;
            flex-direction: row;
            flex-wrap: wrap;
            gap: 10px;
            width: 100%;
            margin-top: 2px;
            justify-content: center;
            margin-left: auto;
            margin-right: auto;
        }
        /* Settings Form */
        .settings-form {
            display: flex;
            flex-direction: column;
            gap: 16px;
            padding: 8px 0;
        }
        .settings-row {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .settings-label {
            display: flex;
            align-items: center;
            gap: 8px;
            font-size: 1.08em;
            font-weight: 500;
            color: inherit;
            cursor: pointer;
        }
        .settings-label input[type="checkbox"] {
            accent-color: #4a90e2;
            width: 1.2em;
            height: 1.2em;
        }
        /* Cheat sheet info bar */
        .cheatsheet-info-bar {
            margin-top: 16px;
            padding: 12px;
            background: #f0f4fa;
            border-radius: 8px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 12px;
        }
        .cheatsheet-info-bar a {
            color: #4a90e2;
            text-decoration: none;
            font-weight: 500;
        }
        .cheatsheet-info-bar a:hover {
            text-decoration: underline;
        }
        .cheatsheet-cache-info {
            font-size: 0.9em;
            color: #666;
        }
        .cheatsheet-flush-btn {
            border-radius: 12px;
            padding: 6px 16px;
            font-size: 0.9em;
            border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
            background: color-mix(in srgb, currentColor 8%, transparent);
            cursor: pointer;
            transition: all 0.2s;
        }
        .cheatsheet-flush-btn:hover {
            border-color: color-mix(in srgb, currentColor 45%, transparent);
            background: color-mix(in srgb, currentColor 18%, transparent);
        }
        /* e621 autocomplete dropdown */
        .bse-ac-dropdown {
            display: none;
            position: absolute;
            z-index: 99999;
            background: #1f3c67;
            border: 1px solid #2a5a8a;
            max-height: 400px;
            overflow-y: auto;
            list-style: none;
            margin: 0;
            padding: 0;
            min-width: 300px;
        }
        .bse-ac-dropdown li {
            padding: 4px 8px;
            cursor: pointer;
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 12px;
        }
        /* Wiki search */
        .bse-wiki-search-wrap {
            display: flex;
            justify-content: center;
            margin: 20px auto;
            max-width: 600px;
            padding: 0 20px;
        }
        .bse-wiki-search-wrap form {
            display: flex;
            gap: 10px;
            width: 100%;
            align-items: center;
        }
    `;
        document.head.appendChild(style);
    }

    // --- Main Entrypoint ---
    let initialized = false;
    function init() {
        if (initialized) return;

        injectStyles();

        let setupSuccess = false;
        if (isWikiPage) setupSuccess = setupWiki();
        else if (siteConfig.selectors) setupSuccess = setupSite();
        else setupSuccess = setupGeneric();

        if (setupSuccess !== false) initialized = true;
    }

    // --- Run ---
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    // Also try after a short delay in case of dynamic content
    setTimeout(() => {
        if (!initialized) {
            init();
        }
    }, 1000);

})();