Relationship Checker v3

made by ai and me.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Relationship Checker v3
// @namespace    http://tampermonkey.net/
// @version      3.3
// @license      MIT
// @description  made by ai and me. 
// @match        https://f95zone.to/*
// @match        https://www.f95zone.to/*
// @match        https://lewdcorner.com/*
// @match        https://www.lewdcorner.com/*
// @match        https://allthefallen.moe/forum/*
// @match        https://www.allthefallen.moe/forum/*
// @match        https://platinmods.com/*
// @match        https://www.platinmods.com/*
// @match        https://taboo-game.com/*
// @match        https://www.taboo-game.com/*
// @match        https://incgrepacks.com/*
// @match        https://www.incgrepacks.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=f95zone.to
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      gist.githubusercontent.com
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    /* ================= GLOBAL STATE & CONFIG ================= */
    const CONFIG = { PAGE_SIZE: 30 };
    const CLOUD_URL = 'https://gist.githubusercontent.com/oberold/aeb49b8f81691f36a5213a81f044f807/raw/gamelib.json';

    const SETTINGS = Object.assign({
        accent: '#8b5cf6',
        theme: 'auto',
        compactMode: false,
        tagLogic: 'AND',
        customTags: '{}'
    }, JSON.parse(GM_getValue('RC_Settings', '{}')));

    const FAVORITES = new Set(JSON.parse(GM_getValue('RC_Favorites', '[]')));

    const STATE = {
        games: [], facets: { engines: [], statuses: [], tags: [] },
        uiHost: null, shadowRoot: null, dbLoaded: false, scanComplete: false,
        focusedIndex: -1, pinnedGameId: null, isFetching: false, activeTotal: 0,
        activeData: []
    };

    /* ================= DICTIONARIES & GROUPS ================= */
    const MODIFIERS = ['true', 'adopted', 'step', 'blood', 'half', 'fake', 'foster', 'others', 'other', 'mod'];
    const JUNK_TAGS = ['false', 'none', 'n/a', 'incest'];

    let TAG_MAP = {
        'm/s': 'Mother / Son', 'f/d': 'Father / Daughter', 'm/d': 'Mother / Daughter', 'f/s': 'Father / Son',
        'b/s': 'Brother / Sister', 's/s': 'Sister / Sister', 'b/b': 'Brother / Brother',
        'a/np': 'Aunt / Nephew', 'u/nc': 'Uncle / Niece', 'a/nc': 'Aunt / Niece', 'u/np': 'Uncle / Nephew',
        'fc/mc': 'Female Cousin / Male Cousin', 'fc/fc': 'Female Cousin / Female Cousin', 'mc/mc': 'Male Cousin / Male Cousin',
        'gm/gs': 'Grandmother / Grandson', 'gm/gd': 'Grandmother / Granddaughter', 'gf/gd': 'Grandfather / Granddaughter', 'gf/gs': 'Grandfather / Grandson',
        'ggm/ggs': 'Great-Grandmother / Great-Grandson', 'ggm/ggd': 'Great-Grandmother / Great-Granddaughter',
        'ggf/ggd': 'Great-Grandfather / Great-Granddaughter', 'ggf/ggs': 'Great-Grandfather / Great-Grandson',
        'mc': 'Main Character'
    };

    const TAG_GROUPS = {
        'Immediate Family': ['m/s', 'f/d', 'm/d', 'f/s', 'b/s', 's/s', 'b/b'],
        'Extended Family': ['a/np', 'u/nc', 'a/nc', 'u/np', 'fc/mc', 'fc/fc', 'mc/mc'],
        'Generational': ['gm/gs', 'gm/gd', 'gf/gd', 'gf/gs', 'ggm/ggs', 'ggm/ggd', 'ggf/ggd', 'ggf/ggs'],
        'Other': ['mc']
    };

    const DEFINITIONS = {
        'true': 'Sexual activity between blood-related individuals.',
        'blood': 'Sexual activity between blood-related individuals.',
        'step': 'Connected through legal marriage. No blood relation.',
        'half': 'Individuals who share one biological parent.',
        'adopted': 'Legally adopted into the same family.',
        'foster': 'Temporary or foster family arrangement.',
        'others': 'Blood-related individuals not involving the main character.',
        'other': 'Blood-related individuals not involving the main character.',
        'mod': 'Unofficial modification adding incest content.'
    };

    try { Object.assign(TAG_MAP, JSON.parse(SETTINGS.customTags)); } catch(e) {}

    function getFullTagName(abbr) {
        const clean = abbr.toLowerCase().replace(/[^a-z/]/g, '');
        return TAG_MAP[clean] || abbr.toUpperCase();
    }

    function getEngineName(engine) {
        const e = (engine || '').toLowerCase();
        if (e.includes('ren')) return "Ren'Py";
        if (e.includes('unity')) return 'Unity';
        if (e.includes('rpg') || e.includes('mv') || e.includes('mz') || e.includes('vx')) return 'RPG Maker';
        if (e.includes('html') || e.includes('twine')) return 'HTML/Twine';
        if (e.includes('unreal')) return 'Unreal Engine';
        return 'Unknown Engine';
    }

    function getStatusData(status) {
        const s = (status || '').toLowerCase();
        if (s.includes('completed') || s.includes('finished')) return { color: '#10b981', label: 'Completed' };
        if (s.includes('abandoned') || s.includes('cancelled') || s.includes('dropped')) return { color: '#ef4444', label: 'Abandoned' };
        if (s.includes('on hold') || s.includes('hiatus')) return { color: '#f59e0b', label: 'On Hold' };
        return { color: '#3b82f6', label: 'Ongoing' };
    }

    function getBigrams(str) {
        const bigrams = new Set();
        for (let i = 0; i < str.length - 1; i++) bigrams.add(str.slice(i, i + 2));
        return bigrams;
    }

    function fuzzySimilarity(s1, s2) {
        if (!s1 || !s2) return 0;
        const bg1 = getBigrams(s1.toLowerCase().replace(/\s+/g, ''));
        const bg2 = getBigrams(s2.toLowerCase().replace(/\s+/g, ''));
        let intersection = 0;
        for (let bg of bg1) if (bg2.has(bg)) intersection++;
        return (2.0 * intersection) / (Math.max(1, bg1.size + bg2.size));
    }

    /* ================= DATA PROCESSING ================= */
    function parseTags(relString) {
        if (!relString) return [];
        const tokens = relString.split(/\s+/).filter(Boolean);
        const results = [];
        let activeMod = 'true';
        let isMod = false;

        tokens.forEach(token => {
            let cleanToken = token.toLowerCase().trim().replace(/\*$/, '');
            if (cleanToken === 'mod') {
                isMod = true;
            } else if (MODIFIERS.includes(cleanToken)) {
                activeMod = cleanToken;
            } else if (!JUNK_TAGS.includes(cleanToken) && cleanToken.length > 0) {
                let category = 'other';
                if (['true', 'blood'].includes(activeMod)) category = 'blood';
                else if (['adopted', 'step', 'half', 'fake', 'foster'].includes(activeMod)) category = 'step';

                results.push({ base: cleanToken, modifier: activeMod, category: category, full: `${activeMod} ${cleanToken}`.trim(), isMod: isMod });
                isMod = false;
            }
        });

        const unique = []; const seen = new Set();
        results.forEach(r => {
            const key = `${r.modifier}-${r.base}-${r.isMod}`;
            if (!seen.has(key)) { seen.add(key); unique.push(r); }
        });
        return unique;
    }

    /* ================= PRIVILEGED NETWORK ENGINE ================= */
    function fetchCloudData(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET", url: url, nocache: true, timeout: 10000,
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        try { resolve(JSON.parse(response.responseText)); }
                        catch (e) { reject(new Error("Invalid JSON format.")); }
                    } else { reject(new Error(`HTTP Status ${response.status}`)); }
                },
                onerror: function() { reject(new Error("Network connection failed.")); },
                ontimeout: function() { reject(new Error("Request timed out.")); }
            });
        });
    }

    function buildDatabase(data) {
        const engines = new Set(), statuses = new Set(), baseTags = new Set();

        STATE.games = data.map((g, index) => {
            g._id = `game_${index}`;
            g.parsedTags = parseTags(g.relationships);

            let rawStatus = g.status || '';
            let engineField = g.engine || '';

            let determinedEngine = getEngineName(engineField);
            if (determinedEngine === 'Unknown Engine') {
                determinedEngine = getEngineName(rawStatus) !== 'Unknown Engine' ? getEngineName(rawStatus) : 'Unknown Engine';
            }

            g.cleanEngine = determinedEngine;
            let sData = getStatusData(rawStatus);
            g.cleanStatus = sData.label;
            g.statusColor = sData.color;

            // Extract the Dev Note by stripping standard statuses and engine keywords aggressively
            let note = rawStatus
                .replace(/completed|finished|abandoned|cancelled|dropped|on hold|hiatus|ongoing/ig, '')
                .replace(/ren'py|renpy|unity|rpg maker|rpgm|rpg\s*m|rpg|html|twine|unreal/ig, '') // Added rpgm and rpg m fixes
                .replace(/^[*[\]()\-\s]+/, '') // strip leading asterisks, brackets, dashes
                .replace(/[*[\]()\-\s]+$/, '') // strip trailing garbage
                .trim();

            // Garbage Collection: Nuke leftover isolated engine acronyms (M, MV, MZ, MAC, PC)
            if (/^(m|mv|mz|vx|mac|pc)$/i.test(note)) {
                note = '';
            }

            g.devNote = note;

            if(g.cleanEngine !== 'Unknown Engine') engines.add(g.cleanEngine);
            statuses.add(g.cleanStatus);
            g.parsedTags.forEach(pt => baseTags.add(pt.base));

            return g;
        });

        STATE.facets = {
            engines: [...engines].sort(), statuses: [...statuses].sort(),
            tags: [...baseTags].sort((a,b) => getFullTagName(a).localeCompare(getFullTagName(b)))
        };
        STATE.dbLoaded = true;
    }

    function getBaseFilteredList(filters) {
        let list = [...STATE.games];
        if (filters.favsOnly) list = list.filter(g => FAVORITES.has(g._id));

        const query = (filters.search || '').trim().toLowerCase();
        if (query) {
            const tokens = query.split(/\s+/).filter(Boolean);
            list = list.map(g => {
                const targetText = [(g.title_normalized || g.title), ...(g.parsedTags.map(pt => pt.full + ' ' + getFullTagName(pt.base)))].join(' ').toLowerCase();
                const exactMatch = tokens.every(t => targetText.includes(t));
                const fuzzScore = fuzzySimilarity(query, g.title_normalized || g.title);
                return { game: g, score: exactMatch ? 1 : fuzzScore };
            }).filter(item => item.score > 0.35).sort((a, b) => b.score - a.score).map(item => item.game);
        }

        if (filters.engine) list = list.filter(g => g.cleanEngine === filters.engine);
        if (filters.status) list = list.filter(g => g.cleanStatus === filters.status);
        return list;
    }

    function getFilteredList(filters) {
        let list = getBaseFilteredList(filters);

        const incTags = Object.keys(filters.tags).filter(t => filters.tags[t] === 'include');
        const excTags = Object.keys(filters.tags).filter(t => filters.tags[t] === 'exclude');

        if (incTags.length > 0) {
            if (SETTINGS.tagLogic === 'AND') list = list.filter(g => incTags.every(ft => g.parsedTags.some(pt => pt.base === ft)));
            else list = list.filter(g => incTags.some(ft => g.parsedTags.some(pt => pt.base === ft)));
        }
        if (excTags.length > 0) {
            list = list.filter(g => !excTags.some(ft => g.parsedTags.some(pt => pt.base === ft)));
        }
        return list;
    }

    function runFilter(filters, page) {
        let list = getFilteredList(filters);

        if (filters.sort === 'az') list.sort((a,b) => (a.title || '').localeCompare(b.title || ''));
        if (filters.sort === 'za') list.sort((a,b) => (b.title || '').localeCompare(a.title || ''));

        if (STATE.pinnedGameId && !filters.search && !filters.sort && !filters.favsOnly && page === 0) {
            const pinnedIndex = list.findIndex(g => g._id === STATE.pinnedGameId);
            if (pinnedIndex > -1) {
                const pinnedGame = list.splice(pinnedIndex, 1)[0];
                list.unshift(pinnedGame);
            }
        }

        return { total: list.length, items: list.slice(page * CONFIG.PAGE_SIZE, (page * CONFIG.PAGE_SIZE) + CONFIG.PAGE_SIZE), query: filters.search };
    }

    async function initData() {
        try {
            const data = await fetchCloudData(CLOUD_URL);
            buildDatabase(data);
            updateToggleButtonState();
            scanPageForLiveChecker();

            if (STATE.uiHost && STATE.uiHost.classList.contains('open')) {
                const search = STATE.uiHost.querySelector('#real-search');
                if(search) search.dispatchEvent(new Event('input'));
            }
        } catch (err) {
            console.error("Cloud Engine Error:", err);
            const btn = STATE.shadowRoot?.querySelector('.fab');
            if (btn) btn.innerHTML = `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="#ef4444" stroke-width="3" fill="none"/></svg>`;
        }
    }

    /* ================= TITANIUM MASS MATCH & LIVE CHECKER ================= */
    function scanPageForLiveChecker() {
        if (!STATE.dbLoaded) return;
        const nodes = document.querySelectorAll('h1.p-title-value, h1.entry-title, h1, .structItem-title a, .thread-title a, h3.title a');
        let nodesProcessed = false;

        nodes.forEach(node => {
            nodesProcessed = true;
            if (node.dataset.rcScanned) return;
            if (node.closest('.message-body, .bbWrapper, .message-content, blockquote, .quote')) {
                node.dataset.rcScanned = 'true'; return;
            }

            const rawText = node.textContent;
            const cleanTitle = rawText.replace(/\[.*?\]|\(.*?\)/g, '').replace(/v\d+(\.\d+)*/gi, '').trim();
            if (!cleanTitle || cleanTitle.length < 3) return;

            const isH1 = node.tagName === 'H1' || node.classList.contains('p-title-value');
            let urlSlug = '';
            if (isH1) {
                try { urlSlug = window.location.pathname.split('/').filter(Boolean).pop().split('.')[0].replace(/-/g, ' '); }
                catch(e) { urlSlug = ''; }
            }

            let bestMatch = null, highestScore = 0;

            STATE.games.forEach(g => {
                const tTitle = g.title_normalized || g.title;
                const score1 = fuzzySimilarity(cleanTitle, tTitle);
                const score2 = urlSlug ? fuzzySimilarity(urlSlug, tTitle) : 0;
                const bestScore = Math.max(score1, score2);

                if (bestScore > highestScore && bestScore > 0.8) { highestScore = bestScore; bestMatch = g; }
            });

            if (bestMatch) {
                node.dataset.rcScanned = 'true';
                const badgeColor = 'var(--success)';

                if (isH1) {
                    STATE.pinnedGameId = bestMatch._id;
                    const fab = STATE.shadowRoot.querySelector('.fab');
                    if (fab) {
                        fab.classList.add('match-found');
                        const limitedTags = bestMatch.parsedTags.slice(0, 3).map(t => t.base.toUpperCase()).join(', ');
                        const extra = bestMatch.parsedTags.length > 3 ? ` +${bestMatch.parsedTags.length - 3}` : '';
                        fab.innerHTML = `
                            <div class="fab-mini-tags">${limitedTags}${extra}</div>
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L15 9l7 3-7 3-3 7-3-7-7-3 7-3z"/></svg>
                        `;
                    }
                } else {
                    const badge = document.createElement('span');
                    badge.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: text-top; margin-right:4px;"><path d="M12 2L15 9l7 3-7 3-3 7-3-7-7-3 7-3z"/></svg>Library Match`;
                    badge.style.cssText = `
                        display: inline-flex; align-items:center; margin-left: 8px; padding: 2px 6px; border-radius: 6px;
                        background: ${badgeColor}20; color: ${badgeColor}; border: 1px solid ${badgeColor}50;
                        font-size: 10px; font-weight: 700; vertical-align: middle; cursor: help;
                    `;
                    badge.title = bestMatch.parsedTags.map(t => t.full.toUpperCase()).join(' | ');
                    node.parentNode.insertBefore(badge, node.nextSibling);

                    if (bestMatch.cleanStatus.toLowerCase() === 'abandoned' || bestMatch.cleanStatus.toLowerCase() === 'on hold') {
                        const container = node.closest('.structItem, .thread, .item');
                        if (container) {
                            container.style.opacity = '0.35'; container.style.filter = 'grayscale(100%)'; container.style.transition = '0.3s ease';
                            container.addEventListener('mouseenter', () => { container.style.opacity = '1'; container.style.filter = 'none'; });
                            container.addEventListener('mouseleave', () => { container.style.opacity = '0.35'; container.style.filter = 'grayscale(100%)'; });
                        }
                    }
                }
            } else {
                let hasIncestTag = false;
                const searchKeywords = ['incest', 'taboo', 'family'];

                if (isH1) {
                    const pageTags = document.querySelectorAll('.tagItem');
                    pageTags.forEach(t => { if(searchKeywords.some(k => t.textContent.toLowerCase().includes(k))) hasIncestTag = true; });
                } else {
                    const container = node.closest('.structItem');
                    if (container) {
                        const rowTags = container.querySelectorAll('.tagItem');
                        rowTags.forEach(t => { if(searchKeywords.some(k => t.textContent.toLowerCase().includes(k))) hasIncestTag = true; });
                    }
                }

                if (hasIncestTag) {
                    node.dataset.rcScanned = 'true';
                    const badgeColor = '#8b5cf6';
                    const badge = document.createElement('span');
                    badge.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: text-top; margin-right:4px;"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>Uncharted`;
                    badge.style.cssText = `
                        display: inline-flex; align-items:center; margin-left: 8px; padding: 2px 6px; border-radius: 6px;
                        background: ${badgeColor}20; color: ${badgeColor}; border: 1px solid ${badgeColor}50;
                        font-size: 10px; font-weight: 700; vertical-align: middle; cursor: help;
                    `;
                    badge.title = "This thread has taboo tags but is not in your personal library.";
                    node.parentNode.insertBefore(badge, node.nextSibling);
                } else {
                    node.dataset.rcScanned = 'true';
                }
            }
        });

        if (nodesProcessed) STATE.scanComplete = true;
    }
    new MutationObserver(() => scanPageForLiveChecker()).observe(document.body, { childList: true, subtree: true });

    /* ================= LUMA THEMING ENGINE ================= */
    function applyLumaTheme(root) {
        let theme = SETTINGS.theme;
        if (theme === 'auto') {
            const bg = window.getComputedStyle(document.body).backgroundColor;
            const rgb = bg.match(/\d+/g);
            if (rgb && rgb.length >= 3) {
                const luma = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
                theme = luma > 150 ? 'light' : 'dark';
            } else { theme = 'dark'; }
        }
        root.querySelector('.palette-container').setAttribute('data-theme', theme);
    }

    /* ================= UI SETUP ================= */
    function initUI() {
        const host = document.createElement('div');
        host.id = 'relcheck-apex-host';
        host.style.cssText = 'position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; overflow: visible;';
        document.documentElement.appendChild(host);
        STATE.shadowRoot = host.attachShadow({ mode: 'closed' });

        const style = document.createElement('style');
        style.textContent = `
            :host {
                --accent: ${SETTINGS.accent};
                --accent-bg: ${SETTINGS.accent}20;
                --success: #10b981;
                --danger: #ef4444;
                --font: 'Inter', system-ui, sans-serif;
                --font-mono: ui-monospace, monospace;
                --spring: cubic-bezier(0.4, 0, 0.2, 1);
                --glide: cubic-bezier(0.4, 0, 0.2, 1);
            }

            .palette-container[data-theme="dark"] {
                --bg: rgba(14, 16, 21, 0.88); --bg-solid: #0e1015; --bg-glass: rgba(14, 16, 21, 0.6);
                --bg-hover: rgba(255, 255, 255, 0.04); --bg-card: rgba(255, 255, 255, 0.02);
                --border: rgba(255, 255, 255, 0.08);
                --text-main: #f8fafc; --text-muted: #94a3b8; --text-ghost: rgba(255,255,255,0.25);
            }

            .palette-container[data-theme="light"] {
                --bg: rgba(250, 250, 252, 0.95); --bg-solid: #f8fafc; --bg-glass: rgba(250, 250, 252, 0.7);
                --bg-hover: rgba(0, 0, 0, 0.04); --bg-card: rgba(0, 0, 0, 0.02);
                --border: rgba(0, 0, 0, 0.1);
                --text-main: #0f172a; --text-muted: #64748b; --text-ghost: rgba(0,0,0,0.25);
            }

            .backdrop { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.3); opacity: 0; display: none; transition: opacity 0.3s var(--glide); z-index: 999998; backdrop-filter: blur(2px); }

            .palette-container {
                position: fixed; top: 50vh; left: 50vw; transform: translate(-50%, -50%) scale(0.95); width: 860px; max-width: 95vw; height: 85vh; max-height: 850px;
                background: var(--bg); backdrop-filter: blur(40px) saturate(200%);
                border: 1px solid var(--border); border-radius: 16px; color: var(--text-main); font-family: var(--font); display: none; flex-direction: column; opacity: 0;
                box-shadow: 0 40px 100px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.1); transition: opacity 0.3s ease, transform 0.4s var(--spring); z-index: 999999; overflow: hidden;
            }
            .palette-container.open { display: flex; opacity: 1; transform: translate(-50%, -50%) scale(1); }
            .palette-container.closing { opacity: 0; transform: translate(-50%, -50%) scale(0.95); transition: opacity 0.2s ease, transform 0.2s ease; }

            .sticky-header { position: sticky; top: 0; z-index: 50; background: var(--bg-glass); backdrop-filter: blur(24px); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; }

            .search-header { display: flex; align-items: center; padding: 20px 24px; position: relative; }
            .search-header svg.icon-search { width: 22px; height: 22px; fill: var(--text-muted); margin-right: 16px; }
            .search-wrapper { position: relative; flex: 1; display: flex; align-items: center; }
            .search-bar { width: 100%; background: transparent; border: none; color: var(--text-main); font-size: 22px; outline: none; font-family: var(--font); font-weight: 500; letter-spacing: -0.01em; position: relative; z-index: 2; }
            .search-bar::placeholder { color: var(--text-muted); opacity: 0.6; font-weight: 400; }

            .search-ghost {
                position: absolute; top: 0; left: 0; width: 100%; color: var(--text-ghost); font-size: 22px; font-weight: 500; font-family: var(--font); pointer-events: none; z-index: 1; white-space: pre;
                opacity: 0; transform: translateX(8px); transition: opacity 0.25s ease, transform 0.25s var(--spring); will-change: transform, opacity;
            }
            .search-ghost.active { opacity: 1; transform: translateX(0); }

            .header-actions { display: flex; gap: 8px; margin-left: 16px; }
            .icon-btn { background: transparent; border: 1px solid transparent; color: var(--text-muted); padding: 8px; border-radius: 8px; cursor: pointer; transition: 0.2s var(--glide); display: flex; align-items: center; justify-content: center; }
            .icon-btn:hover { background: var(--bg-hover); color: var(--text-main); border-color: var(--border); }
            .icon-btn.active { background: var(--accent-bg); color: var(--accent); border-color: var(--accent); }
            .icon-btn svg { width: 18px; height: 18px; fill: currentColor; }

            .filters-row { display: flex; align-items: center; gap: 10px; padding: 0 24px 12px 24px; flex-wrap: wrap;}
            .select-wrapper { position: relative; display: inline-block; max-width: 160px; }
            .select-wrapper select { appearance: none; background: var(--bg-solid); border: 1px solid var(--border); color: var(--text-main); padding: 8px 30px 8px 14px; border-radius: 20px; font-size: 12px; font-weight: 600; cursor: pointer; font-family: var(--font); outline: none; transition: 0.2s; box-shadow: 0 2px 8px rgba(0,0,0,0.1); width: 100%; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;}
            .select-wrapper select:hover { border-color: var(--accent); }
            .select-wrapper::after { content: ''; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid var(--text-muted); pointer-events: none; }

            .filter-btn { background: var(--bg-solid); border: 1px solid var(--border); color: var(--text-main); padding: 8px 16px; border-radius: 20px; font-size: 12px; font-weight: 600; cursor: pointer; transition: 0.2s; font-family: var(--font); box-shadow: 0 2px 8px rgba(0,0,0,0.1); display:flex; align-items:center; gap:6px;}
            .filter-btn:hover { border-color: var(--accent); color: var(--accent); }
            .filter-btn.active { background: var(--accent-bg); color: var(--accent); border-color: var(--accent); }

            .active-filters-container { display: flex; gap: 8px; padding: 0 24px 12px 24px; flex-wrap: wrap; }
            .pill-filter { display: flex; align-items: center; gap: 6px; border: 1px solid; padding: 6px 12px; border-radius: 8px; font-size: 11px; font-weight: 600; cursor: pointer; transition: 0.2s;}
            .pill-filter.include { background: rgba(16,185,129,0.15); color: var(--success); border-color: var(--success); }
            .pill-filter.include:hover { background: rgba(16,185,129,0.25); }
            .pill-filter.exclude { background: rgba(239,68,68,0.15); color: var(--danger); border-color: var(--danger); text-decoration: line-through; }
            .pill-filter.exclude:hover { background: rgba(239,68,68,0.25); }

            /* --- CONTEXTUAL POPOVER (TAGS) --- */
            .popover-anchor { position: relative; display: inline-block; }
            .tag-popover {
                position: absolute; top: calc(100% + 8px); left: 0; width: 500px; max-height: 400px;
                background: var(--bg-solid); border: 1px solid var(--border); border-radius: 12px;
                box-shadow: 0 20px 40px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
                display: none; flex-direction: column; opacity: 0; transform: translateY(-10px);
                transition: 0.2s var(--spring); z-index: 100; overflow: hidden;
            }
            .tag-popover.active { display: flex; opacity: 1; transform: translateY(0); }

            .tm-header { display: flex; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--border); gap: 12px; background: var(--bg-card); }
            .tm-search { flex: 1; background: transparent; border: none; color: var(--text-main); font-size: 14px; outline: none; font-family: var(--font); }
            .tm-search::placeholder { color: var(--text-muted); }
            .tm-clear { font-size: 10px; text-transform: uppercase; font-weight: 700; color: var(--text-muted); cursor: pointer; padding: 4px 8px; border-radius: 6px; transition: 0.2s; background: transparent; border:none;}
            .tm-clear:hover { background: rgba(239,68,68,0.2); color: var(--danger); }

            .tm-body { padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; }
            .tm-section-title { font-size: 10px; text-transform: uppercase; color: var(--text-muted); font-weight: 800; letter-spacing: 1px; margin-bottom: 10px; border-bottom: 1px solid var(--border); padding-bottom: 4px;}
            .tm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; }

            .tm-btn {
                display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-radius: 6px;
                border: 1px solid var(--border); background: var(--bg); cursor: pointer; transition: 0.15s ease;
                font-size: 12px; font-weight: 600; color: var(--text-muted); font-family: var(--font-mono);
            }
            .tm-btn:hover { border-color: var(--text-muted); color: var(--text-main); }
            .tm-btn.include { background: rgba(16,185,129,0.1); border-color: var(--success); color: var(--success); }
            .tm-btn.exclude { background: rgba(239,68,68,0.1); border-color: var(--danger); color: var(--danger); text-decoration: line-through; }
            .tm-btn.disabled { opacity: 0.4; }
            .tm-count { font-size: 10px; opacity: 0.6; }

            /* --- GHOST SCROLLBAR --- */
            .results-area { flex: 1; overflow-y: scroll; position: relative; scroll-behavior: smooth;}
            .results-area::-webkit-scrollbar { width: 8px; background: transparent; }
            .results-area::-webkit-scrollbar-thumb {
                background: transparent; border-radius: 10px;
                box-shadow: inset 0 0 0 10px rgba(128,128,128,0.15); border: 2px solid transparent; background-clip: padding-box;
            }
            .results-area:hover::-webkit-scrollbar-thumb { box-shadow: inset 0 0 0 10px rgba(128,128,128,0.4); }

            /* --- LIST & ANIMATIONS --- */
            @keyframes glideUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }

            .list-row { display: flex; flex-direction: column; padding: 16px 24px; border-bottom: 1px solid var(--border); transition: background 0.2s var(--glide); cursor: pointer; user-select: none; animation: glideUp 0.4s var(--spring) both; }
            .list-row:hover, .list-row.keyboard-focus { background: var(--bg-hover); }
            .list-row.keyboard-focus { box-shadow: inset 3px 0 0 var(--accent); }
            .list-row:last-child { border-bottom: none; }

            .row-visible { display: flex; justify-content: space-between; align-items: center; gap: 16px; width: 100%; position: relative;}
            .row-main { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; }

            .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; cursor: help; }

            /* ENGINE BRAND BADGES */
            .engine-text { font-size: 11px; font-weight: 600; color: var(--text-main); background: rgba(255,255,255,0.05); padding: 4px 10px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1); letter-spacing: 0.5px; backdrop-filter: blur(8px); transition: 0.2s; }
            .palette-container[data-theme="light"] .engine-text { background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.08); }

            .engine-text[data-engine="Ren'Py"] { color: #f472b6; background: rgba(244, 114, 182, 0.1); border-color: rgba(244, 114, 182, 0.3); }
            .engine-text[data-engine="Unity"] { color: #f8fafc; background: rgba(248, 250, 252, 0.1); border-color: rgba(248, 250, 252, 0.3); }
            .palette-container[data-theme="light"] .engine-text[data-engine="Unity"] { color: #0f172a; border-color: rgba(15, 23, 42, 0.3); }
            .engine-text[data-engine="RPG Maker"] { color: #38bdf8; background: rgba(56, 189, 248, 0.1); border-color: rgba(56, 189, 248, 0.3); }
            .engine-text[data-engine="HTML/Twine"] { color: #fbbf24; background: rgba(251, 191, 36, 0.1); border-color: rgba(251, 191, 36, 0.3); }

            .row-title { font-size: 15px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-main); letter-spacing: -0.01em;}

            .fav-star { width: 18px; height: 18px; fill: none; stroke: var(--text-muted); stroke-width: 2; transition: 0.2s; cursor: pointer; flex-shrink: 0;}
            .fav-star:hover { stroke: #fbbf24; }
            .fav-star.active { fill: #fbbf24; stroke: #fbbf24; }

            .row-tags { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; max-width: 40%; align-items: center; }
            .chevron { width: 18px; height: 18px; fill: var(--text-muted); transition: transform 0.3s var(--glide); margin-left: 8px; opacity: 0.5;}
            .list-row:hover .chevron { opacity: 1; }
            .list-row.expanded .chevron { transform: rotate(180deg); opacity: 1; }

            /* Compact Mode */
            .results-area.compact .list-row { padding: 8px 16px; }
            .results-area.compact .row-title { font-size: 13px; font-weight: 500; }
            .results-area.compact .pill { font-size: 10px; padding: 1px 6px; }

            /* SEMANTIC COLOR PILLS */
            .pill { position: relative; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; font-family: var(--font-mono); background: transparent; border: 1px solid var(--border); color: var(--text-muted);}
            .pill-blood { background: rgba(239, 68, 68, 0.1); border-color: rgba(239, 68, 68, 0.3); color: #ef4444; }
            .pill-step { background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.3); color: #3b82f6; }
            .pill-other { background: rgba(139, 92, 246, 0.1); border-color: rgba(139, 92, 246, 0.3); color: #8b5cf6; }

            .pill[data-tooltip]::after, .status-dot[data-tooltip]::after, .fab-mini-tags {
                -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
                transform: translate3d(-50%, 10px, 0); will-change: transform, opacity;
            }
            .pill[data-tooltip]::after, .status-dot[data-tooltip]::after {
                content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%;
                background: var(--bg-solid); padding: 6px 12px; border-radius: 6px; font-family: var(--font); font-weight: 500;
                font-size: 11px; white-space: nowrap; opacity: 0; pointer-events: none; transition: 0.2s var(--glide);
                border: 1px solid var(--border); color: var(--text-main); z-index: 20; box-shadow: 0 10px 30px rgba(0,0,0,0.5);
            }
            .pill[data-tooltip]:hover::after, .status-dot[data-tooltip]:hover::after { opacity: 1; transform: translate3d(-50%, -8px, 0); }

            /* Bento Matrix Animations */
            @keyframes bentoSlide { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }

            .expanded-wrapper { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.4s var(--spring); }
            .expanded-inner { overflow: hidden; }
            .list-row.expanded .expanded-wrapper { grid-template-rows: 1fr; }
            .list-row.expanded { background: var(--bg-hover); }

            .bento-grid { display: flex; gap: 24px; padding: 16px 0 8px 32px; margin-top: 8px; border-top: 1px dashed var(--border); }
            .bento-col { flex: 1; display: flex; flex-direction: column; gap: 8px; border-left: 1px solid var(--border); padding-left: 16px; }
            .bento-col:first-child { border-left: none; padding-left: 0; }
            .col-header { font-size: 10px; text-transform: uppercase; color: var(--text-muted); font-weight: 800; letter-spacing: 0.5px; margin-bottom: 4px; }

            .matrix-item { display: flex; align-items: baseline; gap: 6px; font-size: 13px; color: var(--text-main); opacity: 0; }
            .list-row.expanded .matrix-item { animation: bentoSlide 0.4s var(--spring) forwards; }
            .item-mod { font-size: 9px; text-transform: uppercase; font-weight: 800; color: var(--text-muted); padding: 2px 4px; background: var(--border); border-radius: 3px; }
            .item-mod.custom-mod { background: rgba(139, 92, 246, 0.2); color: #8b5cf6; border-color: rgba(139, 92, 246, 0.4); }

            /* DEV NOTES EXTRACTOR */
            .dev-note { margin-top: 16px; padding: 12px 16px; background: rgba(0,0,0,0.1); border-left: 3px solid var(--text-muted); border-radius: 0 8px 8px 0; font-size: 12px; font-style: italic; color: var(--text-muted); }
            .palette-container[data-theme="light"] .dev-note { background: rgba(0,0,0,0.03); }

            .omni-actions { display: flex; gap: 12px; margin-top: 12px; }
            .btn-action { background: var(--bg); border: 1px solid var(--border); color: var(--text-main); padding: 8px 14px; border-radius: 8px; cursor: pointer; font-weight: 600; transition: 0.2s; font-size: 12px; text-decoration: none; display: inline-flex; align-items: center; justify-content:center;}
            .btn-action:hover { background: var(--bg-hover); border-color: var(--accent); }

            /* --- FOOTER --- */
            .footer { display: flex; justify-content: space-between; align-items: center; padding: 12px 24px; border-top: 1px solid var(--border); background: var(--bg-solid); }
            .kb-hints { font-size: 11px; color: var(--text-muted); font-family: var(--font-mono); display: flex; gap: 12px; align-items: center; font-weight: 500;}
            .kb-key { background: var(--bg-hover); border: 1px solid var(--border); border-radius: 4px; padding: 2px 6px; color: var(--text-main); font-weight: 700;}

            /* --- FAB --- */
            @keyframes stealth-pulse { 0% { border-color: rgba(16, 185, 129, 0.2); box-shadow: 0 0 10px rgba(0,0,0,0.5), 0 0 5px rgba(16, 185, 129, 0.1), inset 0 0 2px rgba(16, 185, 129, 0.1); } 100% { border-color: rgba(16, 185, 129, 1); box-shadow: 0 0 15px rgba(0,0,0,0.6), 0 0 15px rgba(16, 185, 129, 0.4), inset 0 0 8px rgba(16, 185, 129, 0.3); } }
            @keyframes spin { 100% { transform: rotate(360deg); } }

            .fab { position: fixed; bottom: 24px; right: 24px; z-index: 999999; background: rgba(10, 10, 12, 0.6); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: var(--text-muted); width: 48px; height: 48px; border-radius: 50%; display: flex; justify-content: center; align-items: center; cursor: pointer; box-shadow: 0 10px 20px rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.08); transition: 0.3s var(--glide); }
            .fab span { display: none; }
            .fab:hover { background: rgba(15, 15, 18, 0.8); transform: scale(1.05) translateY(-2px); border-color: rgba(255,255,255,0.2); color: var(--text-main); }
            .fab svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; }
            .fab.match-found { color: var(--success); animation: stealth-pulse 2s infinite alternate ease-in-out; }
            .fab-mini-tags { position: absolute; bottom: 120%; right: 0; background: rgba(15, 23, 42, 0.95); border: 1px solid rgba(255,255,255,0.2); padding: 8px 14px; border-radius: 8px; font-family: var(--font-mono); font-size: 11px; white-space: nowrap; pointer-events: none; box-shadow: 0 10px 30px rgba(0,0,0,0.6); opacity: 0; display: block; color: var(--text-main); }
            .fab:hover .fab-mini-tags { opacity: 1; transform: translate3d(0, 0, 0); }
            .spin-icon { animation: spin 1s linear infinite; }
        `;
        STATE.shadowRoot.appendChild(style);

        const backdrop = document.createElement('div');
        backdrop.className = 'backdrop';
        STATE.shadowRoot.appendChild(backdrop);

        const container = document.createElement('div');
        container.className = 'palette-container';
        STATE.shadowRoot.appendChild(container);
        STATE.uiHost = container;
        STATE.backdrop = backdrop;
    }

    function buildExplorer() {
        const root = STATE.uiHost;
        applyLumaTheme(STATE.shadowRoot);

        const svgSearch = `<svg class="icon-search" viewBox="0 0 24 24"><path d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`;
        const svgList = `<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`;

        root.innerHTML = `
            <div class="sticky-header">
                <div class="search-header">
                    ${svgSearch}
                    <div class="search-wrapper">
                        <input type="text" class="search-bar" id="real-search" placeholder="Type to search library...">
                        <div class="search-ghost" id="ghost-search"></div>
                    </div>
                    <div class="header-actions">
                        <button class="icon-btn ${SETTINGS.compactMode ? 'active' : ''}" id="btn-density" title="Toggle Compact View">${svgList}</button>
                    </div>
                </div>

                <div class="filters-row">
                    <button class="filter-btn" id="btn-fav-filter">★ Favorites</button>
                    <div class="popover-anchor">
                        <button class="filter-btn" id="btn-tag-menu" style="background:var(--accent); color:#fff; border:none; box-shadow:0 4px 12px var(--accent-bg);">🏷️ Tags Select</button>
                        <div class="tag-popover" id="tag-popover">
                            <div class="tm-header">
                                <input type="text" class="tm-search" id="tm-search" placeholder="Filter tags...">
                                <button class="tm-clear" id="tm-clear">Clear</button>
                            </div>
                            <div class="tm-body" id="tm-body"></div>
                        </div>
                    </div>
                    <div class="select-wrapper"><select id="sel-engine"><option value="">Engine: All</option></select></div>
                    <div class="select-wrapper"><select id="sel-status"><option value="">Status: All</option></select></div>
                    <div class="select-wrapper"><select id="sel-sort"><option value="">Sort: Default</option><option value="az">Sort: A-Z</option><option value="za">Sort: Z-A</option></select></div>
                </div>
                <div class="active-filters-container" id="active-filters" style="display:none;"></div>
            </div>

            <div class="results-area ${SETTINGS.compactMode ? 'compact' : ''}" id="results-area">
                <div id="results-content"></div>
            </div>

            <div class="footer">
                <div class="kb-hints">
                    <span class="kb-key">Tab</span> Autocomplete <span class="kb-key">↑↓</span> Nav <span class="kb-key">↵</span> Expand
                </div>
                <button class="btn-action" id="btn-sync" style="padding: 6px 12px; font-size:11px; border:none; opacity:0.6;">☁️ Refresh Cloud Data</button>
            </div>
        `;

        const searchBar = root.querySelector('#real-search');
        const ghostSearch = root.querySelector('#ghost-search');
        const resultsArea = root.querySelector('#results-area');
        const resultsContent = root.querySelector('#results-content');
        const activeFiltersContainer = root.querySelector('#active-filters');

        const tagPopover = root.querySelector('#tag-popover');
        const tmSearch = root.querySelector('#tm-search');
        const tmBody = root.querySelector('#tm-body');
        const btnTagMenu = root.querySelector('#btn-tag-menu');

        function closeUI() {
            if (tagPopover.classList.contains('active')) {
                tagPopover.classList.remove('active');
                return;
            }
            root.classList.add('closing');
            STATE.backdrop.style.opacity = '0';
            setTimeout(() => {
                root.classList.remove('open', 'closing');
                STATE.backdrop.style.display = 'none';
            }, 200);
        }
        STATE.backdrop.onclick = closeUI;

        root.querySelector('#btn-sync').onclick = async (e) => {
            const btn = e.target;
            const originalText = btn.innerHTML;
            btn.innerHTML = '⏳ Syncing...';
            await initData();
            btn.innerHTML = '✅ Synced!';
            setTimeout(() => btn.innerHTML = originalText, 1500);
        };

        root.querySelector('#btn-density').onclick = (e) => {
            SETTINGS.compactMode = !SETTINGS.compactMode;
            GM_setValue('RC_Settings', JSON.stringify(SETTINGS));
            e.currentTarget.classList.toggle('active', SETTINGS.compactMode);
            resultsArea.classList.toggle('compact', SETTINGS.compactMode);
        };

        const viewState = { page: 0, filters: { search: '', tags: {}, engine: '', status: '', sort: '', favsOnly: false } };

        if (STATE.dbLoaded) {
            STATE.facets.engines.forEach(x => root.querySelector('#sel-engine').appendChild(new Option(x, x)));
            STATE.facets.statuses.forEach(x => root.querySelector('#sel-status').appendChild(new Option(x, x)));
        }

        btnTagMenu.onclick = (e) => {
            e.stopPropagation();
            const isActive = tagPopover.classList.contains('active');
            tagPopover.classList.toggle('active', !isActive);
            if (!isActive) tmSearch.focus();
        };
        tagPopover.onclick = e => e.stopPropagation();
        document.addEventListener('click', () => tagPopover.classList.remove('active'));

        root.querySelector('#tm-clear').onclick = () => {
            viewState.filters.tags = {};
            renderTagModal(tmSearch.value);
            renderActiveFilters();
            viewState.page = 0; updateView();
        };

        function renderTagModal(filter = '') {
            tmBody.innerHTML = '';
            const f = filter.toLowerCase();
            const grouped = { 'Immediate Family': [], 'Extended Family': [], 'Generational': [], 'Other / Custom': [] };

            const currentList = getFilteredList(viewState.filters);

            STATE.facets.tags.forEach(tag => {
                const name = getFullTagName(tag);
                if (f && !name.toLowerCase().includes(f) && !tag.toLowerCase().includes(f)) return;

                const count = currentList.filter(g => g.parsedTags.some(pt => pt.base === tag)).length;

                let foundGroup = 'Other / Custom';
                for (const [gName, gTags] of Object.entries(TAG_GROUPS)) {
                    if (gTags.includes(tag)) { foundGroup = gName; break; }
                }
                grouped[foundGroup].push({ tag, name, count });
            });

            for (const [groupName, items] of Object.entries(grouped)) {
                if (items.length === 0) continue;

                const section = document.createElement('div');
                section.innerHTML = `<div class="tm-section-title">${groupName}</div>`;
                const grid = document.createElement('div');
                grid.className = 'tm-grid';

                items.forEach(item => {
                    const state = viewState.filters.tags[item.tag] || 'neutral';
                    const btn = document.createElement('div');

                    let cls = 'tm-btn';
                    if (state === 'include') cls += ' include';
                    if (state === 'exclude') cls += ' exclude';
                    if (item.count === 0 && state === 'neutral') cls += ' disabled';

                    btn.className = cls;
                    btn.innerHTML = `<span>${item.name}</span><span class="tm-count">${item.count}</span>`;

                    btn.onclick = (e) => {
                        e.stopPropagation();
                        if (state === 'neutral') viewState.filters.tags[item.tag] = 'include';
                        else if (state === 'include') viewState.filters.tags[item.tag] = 'exclude';
                        else delete viewState.filters.tags[item.tag];

                        renderTagModal(tmSearch.value);
                        renderActiveFilters();
                        viewState.page = 0; updateView();
                    };
                    grid.appendChild(btn);
                });
                section.appendChild(grid);
                tmBody.appendChild(section);
            }
        }

        tmSearch.addEventListener('input', e => renderTagModal(e.target.value));

        root.querySelector('#btn-fav-filter').onclick = (e) => {
            viewState.filters.favsOnly = !viewState.filters.favsOnly;
            e.target.classList.toggle('active', viewState.filters.favsOnly);
            viewState.page = 0; renderTagModal(tmSearch.value); updateView();
        };

        let longPressTimer;
        resultsContent.addEventListener('mousedown', (e) => {
            const engineTag = e.target.closest('.engine-text');
            if (engineTag) {
                longPressTimer = setTimeout(() => {
                    const eng = engineTag.textContent;
                    viewState.filters.engine = eng;
                    root.querySelector('#sel-engine').value = eng;
                    viewState.page = 0;
                    renderTagModal(tmSearch.value);
                    updateView();
                }, 500);
            }
        });
        resultsContent.addEventListener('mouseup', () => clearTimeout(longPressTimer));
        resultsContent.addEventListener('mouseleave', () => clearTimeout(longPressTimer));

        resultsContent.addEventListener('click', (e) => {
            const favBtn = e.target.closest('.fav-star');
            const copyBtn = e.target.closest('.action-copy');
            const row = e.target.closest('.list-row');

            if (favBtn && row) {
                e.stopPropagation();
                const id = row.dataset.id;
                if (FAVORITES.has(id)) FAVORITES.delete(id); else FAVORITES.add(id);
                GM_setValue('RC_Favorites', JSON.stringify([...FAVORITES]));
                favBtn.classList.toggle('active', FAVORITES.has(id));
            } else if (copyBtn) {
                e.stopPropagation();
                navigator.clipboard.writeText(copyBtn.dataset.text);
                const oldText = copyBtn.innerHTML;
                copyBtn.innerHTML = '✔ Copied!';
                setTimeout(() => copyBtn.innerHTML = oldText, 2000);
            } else if (row) {
                row.classList.toggle('expanded');
            }
        });

        resultsArea.addEventListener('scroll', () => {
            if (STATE.isFetching) return;
            if (resultsArea.scrollTop + resultsArea.clientHeight >= resultsArea.scrollHeight - 50) {
                const maxPages = Math.ceil(STATE.activeTotal / CONFIG.PAGE_SIZE);
                if (viewState.page + 1 < maxPages) { viewState.page++; updateView(true); }
            }
        });

        function renderActiveFilters() {
            activeFiltersContainer.innerHTML = '';
            const activeKeys = Object.keys(viewState.filters.tags);
            btnTagMenu.textContent = activeKeys.length > 0 ? `🏷️ Relationships (${activeKeys.length})` : '🏷️ Tags Select';

            activeKeys.forEach(tag => {
                const state = viewState.filters.tags[tag];
                const pill = document.createElement('div');
                pill.className = `pill-filter ${state}`;
                pill.innerHTML = `${getFullTagName(tag)} &times;`;
                pill.onclick = () => {
                    delete viewState.filters.tags[tag];
                    renderTagModal(tmSearch.value);
                    renderActiveFilters(); viewState.page = 0; updateView();
                };
                activeFiltersContainer.appendChild(pill);
            });
            activeFiltersContainer.style.display = activeKeys.length > 0 ? 'flex' : 'none';
        }

        function updateView(append = false) {
            if (!STATE.dbLoaded) return;
            STATE.isFetching = true;
            const data = runFilter(viewState.filters, viewState.page);
            STATE.activeTotal = data.total;
            STATE.activeData = data.items;
            STATE.focusedIndex = -1;

            if (!append) resultsContent.innerHTML = '';

            const q = viewState.filters.search;
            if (q && data.items.length > 0) {
                const firstTitle = data.items[0].title;
                if (firstTitle.toLowerCase().startsWith(q)) {
                    ghostSearch.textContent = searchBar.value + firstTitle.substring(q.length);
                    requestAnimationFrame(() => ghostSearch.classList.add('active'));
                } else {
                    ghostSearch.classList.remove('active');
                    ghostSearch.textContent = '';
                }
            } else {
                ghostSearch.classList.remove('active');
                ghostSearch.textContent = '';
            }

            if (data.total === 0) {
                resultsContent.innerHTML = '<div style="text-align:center; padding: 60px; color: var(--text-muted); font-size: 14px;">No results found.</div>';
                STATE.isFetching = false;
                return;
            }

            data.items.forEach((game, index) => {
                const row = document.createElement('div');
                row.className = `list-row`;
                row.dataset.id = game._id;
                row.dataset.index = (viewState.page * CONFIG.PAGE_SIZE) + index;
                row.style.animationDelay = `${(index % CONFIG.PAGE_SIZE) * 0.05}s`;

                if (game._id === STATE.pinnedGameId && !q && !viewState.filters.sort) {
                    row.classList.add('expanded');
                }

                const statData = getStatusData(game.cleanStatus);
                const engName = game.cleanEngine;

                let tagsAbbrHtml = '';
                const matrixCats = { blood: [], step: [], other: [] };

                game.parsedTags.forEach(pt => { matrixCats[pt.category].push(pt); });

                const uniqueBases = new Map();
                game.parsedTags.forEach(pt => {
                    if (!uniqueBases.has(pt.base)) uniqueBases.set(pt.base, pt);
                    else if (pt.category === 'blood' && uniqueBases.get(pt.base).category !== 'blood') uniqueBases.set(pt.base, pt);
                });

                Array.from(uniqueBases.values()).forEach(pt => {
                    const definition = DEFINITIONS[pt.modifier] || pt.full;
                    tagsAbbrHtml += `<span class="pill pill-${pt.category}" data-tooltip="${definition}">${pt.base.toUpperCase()}</span>`;
                });

                let matrixHtml = '';
                if (matrixCats.blood.length) matrixHtml += renderMatrixCol('Biological', matrixCats.blood);
                if (matrixCats.step.length) matrixHtml += renderMatrixCol('Non-Biological', matrixCats.step);
                if (matrixCats.other.length) matrixHtml += renderMatrixCol('Other', matrixCats.other);

                const actionsHtml = `
                    <a href="https://www.google.com/search?q=${encodeURIComponent(game.title + ' visual novel')}" target="_blank" class="btn-action">Google</a>
                    <a href="https://vndb.org/v/all?q=${encodeURIComponent(game.title)}" target="_blank" class="btn-action">VNDB</a>
                    <button class="btn-action action-copy" data-text="${game.title} - ${game.relationships}">Copy</button>
                `;

                const isFav = FAVORITES.has(game._id) ? 'active' : '';
                const svgStarHtml = `<svg class="fav-star ${isFav}" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" stroke-linejoin="round" stroke-linecap="round"/></svg>`;
                const svgChevronHtml = `<svg class="chevron" viewBox="0 0 24 24"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`;

                let devNoteHtml = game.devNote ? `<div class="dev-note">💡 ${game.devNote}</div>` : '';

                row.innerHTML = `
                    <div class="row-visible">
                        <div class="row-main">
                            ${svgStarHtml}
                            <span class="status-dot" style="background:${statData.color}; box-shadow: 0 0 8px ${statData.color}60;" data-tooltip="${statData.label}"></span>
                            <span class="engine-text" data-engine="${engName}" title="Hold to isolate">${engName}</span>
                            <div class="row-title">${game.title}</div>
                        </div>
                        <div class="row-tags">${tagsAbbrHtml} ${svgChevronHtml}</div>
                    </div>
                    <div class="expanded-wrapper">
                        <div class="expanded-inner">
                            <div class="bento-grid">
                                ${matrixHtml}
                            </div>
                            ${devNoteHtml}
                            <div class="omni-actions">${actionsHtml}</div>
                        </div>
                    </div>
                `;

                const matrixItems = row.querySelectorAll('.matrix-item');
                matrixItems.forEach((item, i) => item.style.animationDelay = `${i * 0.04}s`);

                resultsContent.appendChild(row);
            });

            STATE.isFetching = false;
        }

        function renderMatrixCol(title, items) {
            let html = `<div class="bento-col"><div class="col-header">${title}</div>`;
            items.forEach(pt => {
                const fullName = getFullTagName(pt.base);
                const modText = (pt.modifier !== 'unknown' && pt.modifier !== 'others') ? pt.modifier : '';
                html += `<div class="matrix-item">
                    ${pt.isMod ? `<span class="item-mod custom-mod">MOD</span>` : ''}
                    ${modText && !pt.isMod ? `<span class="item-mod">${modText}</span>` : ''}
                    <span>${fullName}</span>
                </div>`;
            });
            return html + `</div>`;
        }

        function renderFocus() {
            const rows = resultsContent.querySelectorAll('.list-row');
            rows.forEach((r, i) => r.classList.toggle('keyboard-focus', i === STATE.focusedIndex));
            if (STATE.focusedIndex >= 0 && rows[STATE.focusedIndex]) {
                rows[STATE.focusedIndex].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
            }
        }

        document.addEventListener('keydown', (e) => {
            if (!root.classList.contains('open')) return;
            if (tagPopover.classList.contains('active')) return;

            const rows = resultsContent.querySelectorAll('.list-row');
            if (e.key === 'ArrowDown') {
                e.preventDefault();
                STATE.focusedIndex = Math.min(STATE.focusedIndex + 1, rows.length - 1);
                renderFocus();
            } else if (e.key === 'ArrowUp') {
                e.preventDefault();
                STATE.focusedIndex = Math.max(STATE.focusedIndex - 1, 0);
                renderFocus();
            } else if (e.key === 'Enter' && STATE.focusedIndex >= 0) {
                e.preventDefault();
                const row = rows[STATE.focusedIndex];
                if (row) row.classList.toggle('expanded');
            }
        });

        // Tab Autocomplete & Shielding
        ['keydown', 'keyup', 'keypress'].forEach(evt => {
            searchBar.addEventListener(evt, e => {
                if (evt === 'keydown' && e.key === 'Tab' && ghostSearch.classList.contains('active') && ghostSearch.textContent) {
                    e.preventDefault();
                    searchBar.value = ghostSearch.textContent;
                    searchBar.dispatchEvent(new Event('input'));
                }
                e.stopPropagation();
            });
        });

        let searchTimeout;
        searchBar.addEventListener('input', () => {
            clearTimeout(searchTimeout);
            searchTimeout = setTimeout(() => { viewState.page = 0; viewState.filters.search = searchBar.value; updateView(); }, 150);
        });

        root.querySelector('#sel-engine').addEventListener('change', (e) => { viewState.page = 0; viewState.filters.engine = e.target.value; renderTagModal(tmSearch.value); updateView(); });
        root.querySelector('#sel-status').addEventListener('change', (e) => { viewState.page = 0; viewState.filters.status = e.target.value; renderTagModal(tmSearch.value); updateView(); });
        root.querySelector('#sel-sort').addEventListener('change', (e) => { viewState.page = 0; viewState.filters.sort = e.target.value; updateView(); });

        renderActiveFilters();
        renderTagModal();
        updateView();

        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') closeUI();
            if (e.key.toLowerCase() === 'k' && (e.ctrlKey || e.metaKey)) {
                e.preventDefault();

                if (root.classList.contains('open')) {
                    closeUI();
                } else if (STATE.dbLoaded) {
                    STATE.backdrop.style.display = 'block';
                    setTimeout(() => STATE.backdrop.style.opacity = '1', 10);
                    root.classList.add('open');
                    setTimeout(() => searchBar.focus(), 50);

                    if (STATE.facets.engines.length > 0 && root.querySelector('#sel-engine').options.length === 1) {
                        STATE.facets.engines.forEach(x => root.querySelector('#sel-engine').appendChild(new Option(x, x)));
                        STATE.facets.statuses.forEach(x => root.querySelector('#sel-status').appendChild(new Option(x, x)));
                        renderActiveFilters();
                        renderTagModal();
                        updateView();
                    }
                }
            }
        });
    }

    function updateToggleButtonState() {
        const btn = STATE.shadowRoot.querySelector('.fab');
        if (!btn || STATE.pinnedGameId) return;

        if (STATE.dbLoaded) {
            btn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`;
        } else {
            btn.innerHTML = `<svg viewBox="0 0 24 24" class="spin-icon"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-dasharray="31.4 31.4" fill="none"/></svg>`;
        }
    }

    function addToggle() {
        const btn = document.createElement('button');
        btn.className = 'fab';
        STATE.shadowRoot.appendChild(btn);
        updateToggleButtonState();

        btn.onclick = () => {
            const ui = STATE.uiHost;
            if (ui && ui.classList.contains('open')) {
                ui.classList.add('closing');
                STATE.backdrop.style.opacity = '0';
                setTimeout(() => {
                    ui.classList.remove('open', 'closing');
                    STATE.backdrop.style.display = 'none';
                }, 200);
            } else if (STATE.dbLoaded) {
                if (!ui.querySelector('.search-header')) buildExplorer();

                STATE.backdrop.style.display = 'block';
                setTimeout(() => STATE.backdrop.style.opacity = '1', 10);
                ui.classList.add('open');
                const search = ui.querySelector('#real-search');
                if (search) setTimeout(() => search.focus(), 50);

                if (STATE.facets.engines.length > 0 && ui.querySelector('#sel-engine').options.length === 1) {
                    STATE.facets.engines.forEach(x => ui.querySelector('#sel-engine').appendChild(new Option(x, x)));
                    STATE.facets.statuses.forEach(x => ui.querySelector('#sel-status').appendChild(new Option(x, x)));
                    const tmSearch = ui.querySelector('#tm-search');
                    if (tmSearch) tmSearch.dispatchEvent(new Event('input'));
                    const evt = new Event('input');
                    if (search) search.dispatchEvent(evt);
                }
            }
        };
    }

    initUI();
    addToggle();
    initData();

})();