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

})();