FlowX

The ultimate X experience: Seamless Audio, Liquid Glass, Perfect Layouts, and Custom Branding.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         FlowX
// @namespace    https://greasyfork.org/users/ghosty
// @version      2.1.1
// @description  The ultimate X experience: Seamless Audio, Liquid Glass, Perfect Layouts, and Custom Branding.
// @author       ghosty
// @match        https://x.com/*
// @match        https://www.x.com/*
// @match        https://twitter.com/*
// @match        https://www.twitter.com/*
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// @license      MIT
// @icon         https://abs.twimg.com/icons/apple-touch-icon-192x192.png
// ==/UserScript==

(function() {
    'use strict';

    // --- Config & State ---
    const CONFIG = {
        storageKeyVol: 'flowx_volume',
        storageKeyTheme: 'flowx_theme',
        storageKeyFont: 'flowx_font',
        storageKeyPos: 'flowx_position',
        storageKeyWide: 'flowx_widescreen',
        storageKeyDl: 'flowx_download',

        adKeywords: /^(Ad|Promoted|Sponsored|Sponsrad|Gesponsert|Publicité|Promocionado)$/i,
    };

    let state = {
        vol: parseFloat(localStorage.getItem(CONFIG.storageKeyVol)) || 0.5,
        theme: localStorage.getItem(CONFIG.storageKeyTheme) || 'liquid',
        font: localStorage.getItem(CONFIG.storageKeyFont) || 'default',
        wide: localStorage.getItem(CONFIG.storageKeyWide) === 'true',
        download: true, // Hardcoded ON to guarantee local storage doesn't hide it
        pos: JSON.parse(localStorage.getItem(CONFIG.storageKeyPos)) || { x: 20, y: window.innerHeight - 80 }
    };

    // --- THEME & TYPOGRAPHY ENGINE ---
    const STYLES = `
        :root {
            --spring-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
            --smooth-flow: cubic-bezier(0.25, 0.8, 0.25, 1);
            --glide-ease: cubic-bezier(0.22, 1, 0.36, 1);
            --fx-accent: #fff;
        }

        body.fx-font-cyber div[dir="auto"], body.fx-font-cyber span { font-family: "Courier New", Consolas, monospace !important; letter-spacing: -0.5px; }
        body.fx-font-serif div[dir="auto"], body.fx-font-serif span { font-family: Georgia, "Times New Roman", serif !important; }
        body.fx-font-rounded div[dir="auto"], body.fx-font-rounded span { font-family: ui-rounded, "Nunito", "Quicksand", sans-serif !important; }

        /* --- TRUE WIDESCREEN / FOCUS MODE --- */
        body.fx-mode-wide div[data-testid="sidebarColumn"] { display: none !important; width: 0 !important; }

        body.fx-mode-wide header[role="banner"] {
            position: absolute !important;
            left: 0; top: 0;
            transform: translateX(-95%) !important;
            opacity: 0 !important;
            transition: transform 0.3s var(--glide-ease), opacity 0.3s ease !important;
            z-index: 9999 !important;
            background: rgba(0,0,0,0.95) !important;
            height: 100vh !important;
            border-right: 1px solid rgba(255,255,255,0.1);
        }

        body.fx-mode-wide header[role="banner"]:hover {
            transform: translateX(0) !important;
            opacity: 1 !important;
        }

        body.fx-mode-wide header[role="banner"]::after {
            content: ''; position: absolute; top: 0; right: -25px; width: 25px; height: 100%;
        }

        body.fx-mode-wide main[role="main"],
        body.fx-mode-wide main[role="main"] > div,
        body.fx-mode-wide main[role="main"] > div > div {
            display: flex !important;
            justify-content: center !important;
            width: 100% !important;
            max-width: 100% !important;
        }

        body.fx-mode-wide div[data-testid="primaryColumn"] {
            margin: 0 auto !important;
            max-width: 700px !important;
            width: 100% !important;
            border: none !important;
        }

        body.fx-mode-wide div:has(> a[href="/i/grok"]),
        body.fx-mode-wide div:has(> div[data-testid="DMDrawer"]),
        body.fx-mode-wide [data-testid="DMDrawer"],
        body.fx-mode-wide a[href="/i/grok"] {
            display: none !important;
            opacity: 0 !important;
            pointer-events: none !important;
        }

        /* --- UI ELEMENTS --- */
        #flowx-btn {
            position: fixed; width: 42px; height: 42px; border-radius: 14px;
            display: flex; align-items: center; justify-content: center;
            font-weight: 800; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;
            cursor: grab; user-select: none;
            transition: transform 0.2s var(--smooth-flow), box-shadow 0.3s;
            box-shadow: 0 4px 12px rgba(0,0,0,0.4);
            z-index: 2147483647 !important;
        }
        #flowx-btn:active { cursor: grabbing; transform: scale(0.92); }
        #flowx-btn.fx-glide { transition: left 0.5s var(--glide-ease), top 0.5s var(--glide-ease), transform 0.3s; }
        #flowx-btn:hover { transform: scale(1.05) !important; }

        #flowx-panel {
            position: fixed; width: 340px; border-radius: 20px; padding: 20px;
            font-family: "Segoe UI", system-ui, sans-serif; display: none;
            max-height: 85vh; overflow-y: auto; overflow-x: hidden;
            z-index: 2147483647 !important;
        }

        .fx-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
        .fx-txt { font-size: 14px; font-weight: 500; opacity: 0.9; }

        .fx-toggle { position: relative; width: 44px; height: 24px; border-radius: 99px; background: rgba(120,120,120,0.3); cursor: pointer; transition: 0.3s; border: 1px solid rgba(255,255,255,0.1); flex-shrink: 0;}
        .fx-toggle::after { content: ''; position: absolute; left: 2px; top: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: transform 0.3s var(--spring-bounce); box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
        .fx-toggle.active { background: #34C759; }
        .fx-toggle.active::after { transform: translateX(20px); }

        .fx-slider-wrap { position: relative; width: 100%; height: 16px; display: flex; align-items: center; margin: 10px 0 24px 0; }
        .fx-slider-bg { position: absolute; left: 0; right: 0; height: 4px; border-radius: 99px; background: rgba(255,255,255,0.2); }
        .fx-slider-fill { position: absolute; left: 0; height: 4px; border-radius: 99px; pointer-events: none; }
        .fx-slider { -webkit-appearance: none; width: 100%; height: 20px; background: transparent; position: absolute; margin: 0; outline: none; cursor: pointer; }
        .fx-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.3); transition: transform 0.1s; margin-top: -7px; }
        .fx-slider::-webkit-slider-runnable-track { height: 4px; background: transparent; }

        .fx-select { width: 48%; padding: 10px; border-radius: 10px; font-size: 13px; outline: none; border:none; margin-bottom: 20px; cursor: pointer; background: rgba(0,0,0,0.2); color: inherit; font-weight: 600; }
        .fx-select-full { width: 100%; padding: 10px; border-radius: 10px; font-size: 13px; outline: none; border:none; margin-bottom: 20px; cursor: pointer; background: rgba(0,0,0,0.2); color: inherit; font-weight: 600; }
        .fx-select-wrap { display: flex; justify-content: space-between; }

        .fx-anim-open { animation: panelOpen 0.4s var(--spring-bounce) forwards; }
        .fx-anim-min { animation: panelMin 0.3s var(--smooth-flow) forwards; pointer-events: none; }
        @keyframes panelOpen { 0% { opacity: 0; transform: scale(0.8); filter: blur(10px); } 100% { opacity: 1; transform: scale(1); filter: blur(0px); } }
        @keyframes panelMin { 0% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(0.6); filter: blur(10px); } }

        .fx-theme-liquid { background: rgba(20, 25, 35, 0.9); backdrop-filter: blur(16px) saturate(180%); -webkit-backdrop-filter: blur(16px) saturate(180%); border: 1px solid rgba(255, 255, 255, 0.15); box-shadow: 0 20px 50px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.3); color: #fff; }
        .fx-theme-liquid .fx-slider-fill { background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%); }

        .fx-theme-rgb { background: rgba(0, 0, 0, 0.9); border: 1px solid #333; color: #fff; animation: rgbBorder 4s linear infinite; }
        @keyframes rgbBorder { 0% { box-shadow: 0 0 10px #f00; border-color: #f00; } 50% { box-shadow: 0 0 10px #0f0; border-color: #0f0; } 100% { box-shadow: 0 0 10px #00f; border-color: #00f; } }
        .fx-theme-rgb .fx-slider-fill { background: linear-gradient(90deg, red, yellow, lime, cyan, blue, magenta, red); }

        .fx-theme-cyber { background: rgba(5, 10, 16, 0.95); border: 1px solid #0ff; color: #0ff; box-shadow: 0 0 15px rgba(0,255,255,0.2); }
        .fx-theme-cyber .fx-slider-fill, .fx-theme-cyber .fx-toggle.active { background: #0ff; box-shadow: 0 0 10px #0ff; }
        .fx-theme-cyber .fx-toggle.active::after { background: #000; }

        .fx-theme-sunset { background: linear-gradient(135deg, rgba(255, 95, 109, 0.95), rgba(255, 195, 113, 0.95)); color: #fff; border: 1px solid rgba(255,255,255,0.4); }
        .fx-theme-sunset .fx-slider-fill { background: #fff; }

        .fx-theme-frost { background: rgba(255,255,255,0.85); backdrop-filter: blur(20px); color: #222; border: 1px solid rgba(0,0,0,0.1); }
        .fx-theme-frost .fx-slider-fill { background: #007AFF; }
        .fx-theme-frost .fx-toggle { background: rgba(0,0,0,0.1); }

        /* --- CUSTOM BRANDING ENGINE --- */
        .fx-logo-text {
            font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
            font-size: 22px !important;
            font-weight: 700 !important;
            color: #fff !important;
            margin-right: 10px !important;
            margin-left: 0 !important;
            vertical-align: middle !important;
            display: inline-block !important;
            letter-spacing: -0.5px !important;
            line-height: 1 !important;
            transition: opacity 0.3s ease;
        }

        /* Adjust the X SVG to align perfectly next to the text */
        div[role="banner"] h1[role="heading"] a[href="/home"] svg {
            display: inline-block !important;
            margin-left: 0 !important;
            position: relative;
            top: 1px;
        }

        body.fx-mode-wide header[role="banner"] .fx-logo-text {
            opacity: 0;
            pointer-events: none;
        }

        body.fx-mode-wide header[role="banner"]:hover .fx-logo-text {
            opacity: 1;
            pointer-events: auto;
        }

        /* --- THEMED KO-FI DONATION BUTTON --- */
        .fx-donate-wrapper {
            display: flex;
            justify-content: center;
            margin-top: 25px;
            padding-top: 15px;
            border-top: 1px solid rgba(255, 255, 255, 0.08);
        }

        .fx-donate-btn {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            padding: 8px 18px;
            background: rgba(255, 255, 255, 0.05);
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: 99px;
            color: #ffffff !important;
            text-decoration: none;
            font-weight: 700;
            font-size: 11px;
            letter-spacing: 0.5px;
            transition: all 0.3s var(--smooth-flow);
            text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
        }

        .fx-donate-btn span {
            font-size: 14px;
            transition: transform 0.4s var(--spring-bounce);
        }

        .fx-donate-btn:hover {
            transform: translateY(-2px) scale(1.02);
            color: #ffffff !important;
        }

        .fx-donate-btn:hover span {
            transform: scale(1.2) rotate(15deg);
        }

        .fx-donate-btn:active {
            transform: translateY(0) scale(0.95);
        }

        .fx-theme-liquid .fx-donate-btn:hover {
            background: rgba(0, 242, 254, 0.15);
            border-color: rgba(0, 242, 254, 0.5);
            box-shadow: 0 6px 15px rgba(0, 242, 254, 0.3);
        }
        .fx-theme-rgb .fx-donate-btn:hover {
            background: rgba(255, 255, 255, 0.1);
            border-color: #fff;
            animation: rgbBorder 4s linear infinite;
        }
        .fx-theme-cyber .fx-donate-btn:hover {
            background: rgba(0, 255, 255, 0.1);
            border-color: #0ff;
            color: #0ff !important;
            box-shadow: 0 0 15px rgba(0, 255, 255, 0.4);
        }
        .fx-theme-sunset .fx-donate-btn {
            border-color: rgba(255, 255, 255, 0.2);
            color: #ffffff !important;
        }
        .fx-theme-sunset .fx-donate-btn:hover {
            background: rgba(255, 255, 255, 0.2);
            border-color: #fff;
            box-shadow: 0 6px 15px rgba(255, 255, 255, 0.3);
        }
        .fx-theme-frost .fx-donate-wrapper {
            border-top: 1px solid rgba(0, 0, 0, 0.1);
        }
        .fx-theme-frost .fx-donate-btn {
            background: rgba(0, 0, 0, 0.05);
            border-color: rgba(0, 0, 0, 0.1);
            color: #333 !important;
            text-shadow: none;
        }
        .fx-theme-frost .fx-donate-btn:hover {
            background: #007AFF;
            border-color: #007AFF;
            color: #fff !important;
            box-shadow: 0 6px 15px rgba(0, 122, 255, 0.3);
        }
    `;

    const styleSheet = document.createElement("style");
    styleSheet.innerText = STYLES;
    document.head.appendChild(styleSheet);


    // --- THE AUDIO ENGINE ---
    document.addEventListener('pause', (e) => {
        if (e.target && e.target.tagName === 'VIDEO') {
            const rect = e.target.getBoundingClientRect();
            const screenCenter = window.innerHeight / 2;
            const vidCenterY = rect.top + (rect.height / 2);
            if (Math.abs(screenCenter - vidCenterY) < (window.innerHeight * 0.4)) {
                e.target.dataset.fxUserPaused = 'true';
            }
        }
    }, true);

    document.addEventListener('play', (e) => {
        if (e.target && e.target.tagName === 'VIDEO') {
            e.target.dataset.fxUserPaused = 'false';
        }
    }, true);

    const syncAudio = () => {
        const videos = document.querySelectorAll('video');
        if (videos.length === 0) return;

        const viewportHeight = window.innerHeight;
        const screenCenter = viewportHeight / 2;

        let closestVideo = null;
        let minDistance = Infinity;

        videos.forEach(v => {
            const rect = v.getBoundingClientRect();
            if (rect.height < 50) return;

            const vidCenterY = rect.top + (rect.height / 2);
            const distance = Math.abs(screenCenter - vidCenterY);
            if (distance < minDistance) {
                minDistance = distance;
                closestVideo = v;
            }
        });

        videos.forEach(v => {
            if (v === closestVideo && minDistance < (viewportHeight * 0.45)) {

                if (v.dataset.fxUserPaused === 'true') return;

                if (v.paused) {
                    v.muted = true;

                    const playPromise = v.play();
                    if (playPromise !== undefined) {
                        playPromise.then(() => {
                            v.muted = false;
                            v.volume = state.vol;
                        }).catch(() => {
                            const container = v.closest('[data-testid="videoComponent"]');
                            if (container) {
                                const playBtn = container.querySelector('[data-testid="playButton"]');
                                if (playBtn) playBtn.click();
                            }
                        });
                    }
                } else {
                    if (v.muted) v.muted = false;
                    if (Math.abs(v.volume - state.vol) > 0.05) v.volume = state.vol;
                }

            } else {
                if (!v.paused) v.pause();
            }
        });
    };

    let scrollTimeout;
    window.addEventListener('scroll', () => {
        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(syncAudio, 100);
    }, { passive: true });
    setInterval(syncAudio, 600);


    // --- THE TARGETED, IMMUNE DOWNLOAD ENGINE ---
    function getTweetUrl(btn) {
        const article = btn.closest('article');
        if (!article) {
            if (window.location.pathname.includes('/status/')) return window.location.href;
            return null;
        }
        const links = Array.from(article.querySelectorAll('a[href*="/status/"]'));
        const timeLink = links.find(a => /\/status\/[0-9]+$/.test(a.href) && !a.href.includes('/photo/') && !a.href.includes('/video/'));
        return timeLink ? timeLink.href : null;
    }

    function injectDownloadButtons() {
        if (!state.download) return;

        // Hunt down the Bookmark button directly. We completely ignore the Action Bar [role="group"] because X moved it!
        const bookmarks = document.querySelectorAll('[data-testid="bookmark"], [data-testid="removeBookmark"]');

        bookmarks.forEach(bookmark => {
            try {
                // Get the physical button circle
                const btn = bookmark.closest('[role="button"]') || bookmark.closest('button') || bookmark;

                // Get the wrapper that physically separates it from the Share button
                const wrapper = btn.parentElement;
                if (!wrapper) return;

                // Get the master container that holds both Bookmark and Share
                const container = wrapper.parentElement;
                if (!container) return;

                // Check purely based on sibling DOM elements. If our button is to the left, stop.
                if (wrapper.previousElementSibling && wrapper.previousElementSibling.classList.contains('fx-dl-btn-raw')) return;

                // Build a pure HTML button from scratch.
                // We steal the exact layout CSS from the wrapper so X handles the Flexbox math perfectly.
                const dlBtn = document.createElement('div');
                dlBtn.className = wrapper.className + ' fx-dl-btn-raw';
                dlBtn.setAttribute('title', 'Download High-Res');

                // Hardcode X's native 34.75px circle geometry to absolutely prevent flexbox squashing
                dlBtn.style.cssText = 'display: flex; align-items: center; justify-content: center; width: 34.75px; height: 34.75px; border-radius: 9999px; cursor: pointer; color: rgb(113, 118, 123); transition: background-color 0.2s, color 0.2s;';

                // Pure vector SVG arrow matching X's 1.25em size spec
                dlBtn.innerHTML = '<svg viewBox="0 0 24 24" style="width: 1.25em; height: 1.25em; fill: currentColor; pointer-events: none;"><path d="M12 18.5l-6.5-6.5 1.414-1.414 4.086 4.086V3h2v11.672l4.086-4.086 1.414 1.414L12 18.5zm7 3v-2H5v2h14z"></path></svg>';

                // Native hover effects driven entirely by JS so React can't strip them
                dlBtn.addEventListener('mouseenter', () => {
                    dlBtn.style.backgroundColor = 'rgba(29, 155, 240, 0.1)';
                    dlBtn.style.color = 'rgb(29, 155, 240)';
                });
                dlBtn.addEventListener('mouseleave', () => {
                    dlBtn.style.backgroundColor = 'transparent';
                    dlBtn.style.color = 'rgb(113, 118, 123)';
                });

                // Execution Logic
                dlBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();

                    const url = getTweetUrl(dlBtn);
                    if (!url) {
                        alert("FlowX: Couldn't extract the Tweet URL. Try clicking the tweet to expand it first.");
                        return;
                    }

                    // Green Success Pulse
                    dlBtn.style.color = '#34C759';
                    dlBtn.style.transform = 'scale(1.15)';
                    setTimeout(() => {
                        dlBtn.style.color = 'rgb(113, 118, 123)';
                        dlBtn.style.transform = 'scale(1)';
                    }, 800);

                    const apiUrl = url.replace(/(twitter\.com|x\.com)/, 'api.vxtwitter.com');

                    GM_xmlhttpRequest({
                        method: "GET",
                        url: apiUrl,
                        onload: function(response) {
                            try {
                                const data = JSON.parse(response.responseText);
                                if (data && data.mediaURLs && data.mediaURLs.length > 0) {
                                    data.mediaURLs.forEach((mediaUrl, index) => {
                                        let finalUrl = mediaUrl;

                                        const ext = finalUrl.split('.').pop().split('?')[0] || 'mp4';
                                        const filename = `FlowX_${data.tweetID}_${index}.${ext}`;

                                        GM_download({
                                            url: finalUrl,
                                            name: filename,
                                            onload: () => console.log('FlowX: Payload Secured ->', filename),
                                            onerror: () => window.open(finalUrl, '_blank')
                                        });
                                    });
                                } else {
                                    alert("FlowX: No media detected in this tweet.");
                                }
                            } catch(err) {
                                window.open(`https://x-downloader.net/?url=${encodeURIComponent(url)}`, '_blank');
                            }
                        },
                        onerror: function() {
                            window.open(`https://x-downloader.net/?url=${encodeURIComponent(url)}`, '_blank');
                        }
                    });
                });

                // Drop it perfectly inside the container, precisely to the left of the Bookmark button!
                container.insertBefore(dlBtn, wrapper);

            } catch (err) {
                // Catch any rendering oddities silently
            }
        });
    }

    // Heavy interval polling guarantees the button stays alive during fast scrolling
    setInterval(injectDownloadButtons, 500);
    const downloadObserver = new MutationObserver(() => injectDownloadButtons());
    downloadObserver.observe(document.body, { childList: true, subtree: true });


    // --- LOGO ENGINE ---
    function injectLogoText() {
        const logoAnchor = document.querySelector('h1[role="heading"] a[href="/home"], div[role="banner"] a[href="/home"]');

        if (!logoAnchor || logoAnchor.querySelector('.fx-logo-text')) return;

        const flowText = document.createElement('span');
        flowText.className = 'fx-logo-text';
        flowText.textContent = 'Flow';

        logoAnchor.style.display = 'flex';
        logoAnchor.style.alignItems = 'center';
        logoAnchor.style.flexDirection = 'row';

        logoAnchor.prepend(flowText);
    }
    setInterval(injectLogoText, 2000);


    // --- AD BLOCKER ---
    const processedNodes = new WeakSet();
    function processArticle(article, wrapper) {
        if (processedNodes.has(article)) return;
        processedNodes.add(article);

        const spans = article.querySelectorAll('span');
        let isAd = false;
        for (const span of spans) {
            const txt = span.textContent.trim();
            if (CONFIG.adKeywords.test(txt) && span.closest('[data-testid="User-Name"]')) {
                isAd = true; break;
            }
        }
        if (isAd) {
            if (wrapper) wrapper.style.display = 'none';
            else article.style.display = 'none';
            return;
        }
    }

    const observer = new MutationObserver((mutations) => {
        mutations.forEach(m => {
            m.addedNodes.forEach(node => {
                if (node.nodeType === 1) {
                    if (node.matches && node.matches('div[data-testid="cellInnerDiv"]')) {
                        const article = node.querySelector('article[data-testid="tweet"]');
                        if (article) processArticle(article, node);
                    } else if (node.querySelectorAll) {
                        const wrappers = node.querySelectorAll('div[data-testid="cellInnerDiv"]');
                        wrappers.forEach(w => {
                            const a = w.querySelector('article[data-testid="tweet"]');
                            if (a) processArticle(a, w);
                        });
                    }
                }
            });
        });
    });
    observer.observe(document.body, { childList: true, subtree: true });


    // --- UI CONSTRUCTION ---
    function applyModes() {
        const b = document.body.classList;
        state.wide ? b.add('fx-mode-wide') : b.remove('fx-mode-wide');
        b.remove('fx-font-default', 'fx-font-cyber', 'fx-font-serif', 'fx-font-rounded');
        b.add(`fx-font-${state.font}`);
    }

    function createUI() {
        const btn = document.createElement('div');
        btn.id = 'flowx-btn';
        btn.textContent = 'FX';
        btn.className = `fx-theme-${state.theme} fx-glide`;
        btn.style.left = state.pos.x + 'px';
        btn.style.top = state.pos.y + 'px';

        const panel = document.createElement('div');
        panel.id = 'flowx-panel';
        panel.className = `fx-theme-${state.theme}`;

        panel.innerHTML = `
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
                <div style="font-weight:700; font-size:18px;">FlowX <span style="font-size:11px; opacity:0.6; font-weight:400;">v2.1.1</span></div>
                <div class="fx-close" style="cursor:pointer; font-size:12px; opacity:0.7;">✕</div>
            </div>

            <div class="fx-select-wrap">
                <select class="fx-select" id="theme-select">
                    <option value="liquid">💧 Liquid</option>
                    <option value="rgb">🌈 RGB</option>
                    <option value="frost">❄️ Frost</option>
                    <option value="cyber">⚡ Cyber</option>
                    <option value="sunset">🌅 Sunset</option>
                </select>

                <select class="fx-select" id="font-select">
                    <option value="default">Aa System</option>
                    <option value="cyber">Aa Mono</option>
                    <option value="serif">Aa Serif</option>
                    <option value="rounded">Aa Smooth</option>
                </select>
            </div>

            <div style="margin-bottom:20px;">
                <div class="fx-row" style="margin-bottom:5px;">
                    <span class="fx-txt">Volume</span>
                    <span class="fx-vol-val" style="font-size:12px; font-weight:bold;">${Math.round(state.vol * 100)}%</span>
                </div>
                <div class="fx-slider-wrap">
                    <div class="fx-slider-bg"></div>
                    <div class="fx-slider-fill" style="width:${state.vol * 100}%"></div>
                    <input class="fx-slider" type="range" min="0" max="100" value="${state.vol * 100}">
                </div>
            </div>

            <div class="fx-txt" style="opacity:0.5; font-size:11px; margin-bottom:10px; text-transform:uppercase; letter-spacing:1px;">Features</div>

            <div id="feat-list"></div>

            <div class="fx-donate-wrapper">
                <a href="https://ko-fi.com/ghostyy69" target="_blank" class="fx-donate-btn">
                    <span>☕</span> support ghosty
                </a>
            </div>
        `;

        const features = [
            { id: 'wide', label: 'Widescreen', desc: 'Focus Mode' },
            { id: 'download', label: 'Download Button', desc: '1-Click API Rip' }
        ];

        const featList = panel.querySelector('#feat-list');
        features.forEach(f => {
            const row = document.createElement('div');
            row.className = 'fx-row';
            row.innerHTML = `
                <div>
                    <div class="fx-txt">${f.label}</div>
                    <div style="font-size:10px; opacity:0.5;">${f.desc}</div>
                </div>
                <div class="fx-toggle ${state[f.id] ? 'active' : ''}" id="tog-${f.id}"></div>
            `;
            featList.appendChild(row);

            row.querySelector('.fx-toggle').addEventListener('click', (e) => {
                state[f.id] = !state[f.id];
                e.currentTarget.classList.toggle('active');

                const map = {
                    'wide': CONFIG.storageKeyWide, 'download': CONFIG.storageKeyDl
                };
                localStorage.setItem(map[f.id], state[f.id]);

                applyModes();
            });
        });

        const slider = panel.querySelector('.fx-slider');
        const sliderFill = panel.querySelector('.fx-slider-fill');
        const volVal = panel.querySelector('.fx-vol-val');

        slider.addEventListener('input', (e) => {
            state.vol = e.target.value / 100;
            localStorage.setItem(CONFIG.storageKeyVol, state.vol);
            sliderFill.style.width = `${state.vol * 100}%`;
            volVal.innerText = `${Math.round(state.vol * 100)}%`;
        });

        const themeSelect = panel.querySelector('#theme-select');
        themeSelect.value = state.theme;
        themeSelect.addEventListener('change', (e) => {
            state.theme = e.target.value;
            localStorage.setItem(CONFIG.storageKeyTheme, state.theme);
            panel.className = `fx-theme-${state.theme} fx-anim-open`;
            btn.className = `fx-theme-${state.theme} fx-glide`;
        });

        const fontSelect = panel.querySelector('#font-select');
        fontSelect.value = state.font;
        fontSelect.addEventListener('change', (e) => {
            state.font = e.target.value;
            localStorage.setItem(CONFIG.storageKeyFont, state.font);
            applyModes();
        });

        panel.querySelector('.fx-close').addEventListener('click', () => togglePanel(false));

        applyModes();

        let isDragging = false, hasMoved = false, startX, startY, initX, initY;
        btn.addEventListener('mousedown', (e) => {
            isDragging = true; hasMoved = false;
            btn.classList.remove('fx-glide');
            startX = e.clientX; startY = e.clientY;
            initX = btn.offsetLeft; initY = btn.offsetTop;
        });

        document.documentElement.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            const dx = e.clientX - startX; const dy = e.clientY - startY;
            if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasMoved = true;
            btn.style.left = `${initX + dx}px`; btn.style.top = `${initY + dy}px`;
        }, true);

        document.documentElement.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                if (hasMoved) {
                    btn.classList.add('fx-glide');
                    const rect = btn.getBoundingClientRect();
                    const winW = window.innerWidth;
                    const snapX = (rect.left + rect.width/2 < winW/2) ? 20 : winW - rect.width - 20;
                    let snapY = rect.top;
                    if(snapY < 20) snapY = 20;
                    if(snapY > window.innerHeight - 80) snapY = window.innerHeight - 80;

                    btn.style.left = snapX + 'px'; btn.style.top = snapY + 'px';
                    state.pos = { x: snapX, y: snapY };
                    localStorage.setItem(CONFIG.storageKeyPos, JSON.stringify(state.pos));
                }
            }
        }, true);

        btn.addEventListener('click', () => {
            if (!hasMoved) togglePanel(panel.style.display !== 'block');
        });

        function togglePanel(show) {
            if (show) {
                panel.style.display = 'block';
                panel.classList.remove('fx-anim-min');
                panel.classList.add('fx-anim-open');

                const bRect = btn.getBoundingClientRect();

                let left = bRect.left + 60;
                if (left + 360 > window.innerWidth) left = bRect.left - 360;
                panel.style.left = Math.max(10, left) + 'px';

                if (bRect.top > window.innerHeight / 2) {
                    panel.style.top = 'auto';
                    panel.style.bottom = Math.max(20, window.innerHeight - bRect.bottom) + 'px';
                } else {
                    panel.style.bottom = 'auto';
                    panel.style.top = Math.max(20, bRect.top) + 'px';
                }

            } else {
                panel.classList.remove('fx-anim-open');
                panel.classList.add('fx-anim-min');
                setTimeout(() => { panel.style.display = 'none'; }, 280);
            }
        }

        document.body.appendChild(btn);
        document.body.appendChild(panel);
    }

    createUI();

})();