Roblox FACS Numbering

Add numbers next to FACS names for easier reference | Discord: https://discord.gg/BE7k9Xxm5z

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Roblox FACS Numbering
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Add numbers next to FACS names for easier reference | Discord: https://discord.gg/BE7k9Xxm5z
// @author       Cloud Guy
// @license      MIT
// @match        https://create.roblox.com/docs/art/characters/facial-animation/facs-poses-reference*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const FACS = ['EyesLookDown','EyesLookLeft','EyesLookRight','EyesLookUp','JawDrop','LeftEyeClosed','LeftLipCornerPuller','LeftLipStretcher','LeftLowerLipDepressor','LeftUpperLipRaiser','LipsTogether','Pucker','RightEyeClosed','RightLipCornerPuller','RightLipStretcher','RightLowerLipDepressor','RightUpperLipRaiser','ChinRaiser','ChinRaiserUpperLip','FlatPucker','Funneler','LowerLipSuck','LipPresser','MouthLeft','MouthRight','UpperLipSuck','LeftCheekPuff','LeftDimpler','LeftLipCornerDown','RightCheekPuff','RightDimpler','RightLipCornerDown','JawLeft','JawRight','Corrugator','LeftBrowLowerer','LeftOuterBrowRaiser','LeftNoseWrinkler','LeftInnerBrowRaiser','RightBrowLowerer','RightOuterBrowRaiser','RightNoseWrinkler','RightInnerBrowRaiser','LeftCheekRaiser','LeftEyeUpperLidRaiser','RightCheekRaiser','RightEyeUpperLidRaiser','TongueDown','TongueOut','TongueUp'];

    const PAIRS = {
        'LeftEyeClosed': 'RightEyeClosed',
        'RightEyeClosed': 'LeftEyeClosed',
        'LeftLipCornerPuller': 'RightLipCornerPuller',
        'RightLipCornerPuller': 'LeftLipCornerPuller',
        'LeftLipStretcher': 'RightLipStretcher',
        'RightLipStretcher': 'LeftLipStretcher',
        'LeftLowerLipDepressor': 'RightLowerLipDepressor',
        'RightLowerLipDepressor': 'LeftLowerLipDepressor',
        'LeftUpperLipRaiser': 'RightUpperLipRaiser',
        'RightUpperLipRaiser': 'LeftUpperLipRaiser',
        'LeftCheekPuff': 'RightCheekPuff',
        'RightCheekPuff': 'LeftCheekPuff',
        'LeftDimpler': 'RightDimpler',
        'RightDimpler': 'LeftDimpler',
        'LeftLipCornerDown': 'RightLipCornerDown',
        'RightLipCornerDown': 'LeftLipCornerDown',
        'LeftBrowLowerer': 'RightBrowLowerer',
        'RightBrowLowerer': 'LeftBrowLowerer',
        'LeftOuterBrowRaiser': 'RightOuterBrowRaiser',
        'RightOuterBrowRaiser': 'LeftOuterBrowRaiser',
        'LeftNoseWrinkler': 'RightNoseWrinkler',
        'RightNoseWrinkler': 'LeftNoseWrinkler',
        'LeftInnerBrowRaiser': 'RightInnerBrowRaiser',
        'RightInnerBrowRaiser': 'LeftInnerBrowRaiser',
        'LeftCheekRaiser': 'RightCheekRaiser',
        'RightCheekRaiser': 'LeftCheekRaiser',
        'LeftEyeUpperLidRaiser': 'RightEyeUpperLidRaiser',
        'RightEyeUpperLidRaiser': 'LeftEyeUpperLidRaiser',
        'JawLeft': 'JawRight',
        'JawRight': 'JawLeft',
        'MouthLeft': 'MouthRight',
        'MouthRight': 'MouthLeft',
        'EyesLookLeft': 'EyesLookRight',
        'EyesLookRight': 'EyesLookLeft',
        'EyesLookUp': 'EyesLookDown',
        'EyesLookDown': 'EyesLookUp'
    };

    const THEME = {
        primary: '#60A5FA',
        primaryDark: '#3B82F6',
        accent: '#93C5FD',
        background: '#111216',
        cardBg: '#1a1b20',
        textPrimary: '#ffffff',
        textSecondary: '#cccccc',
        highlight: 'rgba(96, 165, 250, 0.10)',
        highlightHover: 'rgba(96, 165, 250, 0.18)',
        inactive: '#3a3b40',
        inactiveDark: '#2a2b30',
        shadow: 'rgba(0, 0, 0, 0.3)'
    };

    const elemCache = {
        sections: [],
        tocItems: [],
        lastHighlighted: '',
        sidebar: null,
        discordButton: null,
        toggleButton: null,
        indicator: null,
        marker: null
    };

    function addStyles() {
        const css = `
            .facs-highlight {
                background: linear-gradient(135deg, ${THEME.highlight} 0%, rgba(96, 165, 250, 0.18) 100%);
                font-weight: 600;
                border-radius: 8px;
                box-shadow: 0 4px 12px ${THEME.shadow}, inset 0 1px 0 rgba(255, 255, 255, 0.05);
                opacity: 1;
                transform: translateX(0);
                transition: all 0.2s ease-out;
            }

            .facs-highlight-sidebar {
                border-left: 3px solid ${THEME.primary};
                padding-left: 8px;
            }

            .facs-highlight:hover {
                background: linear-gradient(135deg, ${THEME.highlightHover} 0%, rgba(96, 165, 250, 0.28) 100%);
                transform: translateX(4px) scale(1.02);
                box-shadow: 0 6px 20px ${THEME.shadow}, inset 0 1px 0 rgba(255, 255, 255, 0.1);
                transition: all 0.15s ease-out;
            }

            .toc-active {
                background: linear-gradient(135deg, rgba(0, 162, 255, 0.15) 0%, rgba(0, 102, 204, 0.25) 100%);
                font-weight: 600;
                border-left: 3px solid ${THEME.primary};
                padding-left: 8px;
                border-radius: 8px;
                box-shadow: 0 2px 8px ${THEME.shadow}, inset 0 1px 0 rgba(255, 255, 255, 0.05);
                transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            }

            .toc-active:hover {
                background: linear-gradient(135deg, rgba(0, 162, 255, 0.25) 0%, rgba(0, 102, 204, 0.35) 100%);
                transform: translateX(4px);
            }

            #facs-discord {
                position: fixed;
                top: 15px;
                right: 20px;
                background: linear-gradient(135deg, ${THEME.inactive} 0%, ${THEME.inactiveDark} 100%);
                color: ${THEME.textSecondary};
                padding: 10px;
                border-radius: 12px;
                text-decoration: none;
                z-index: 9999;
                box-shadow: 0 4px 16px ${THEME.shadow};
                display: flex;
                align-items: center;
                justify-content: center;
                transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
                border: 1px solid ${THEME.cardBg};
                width: 40px;
                height: 40px;
            }

            #facs-discord:hover {
                transform: scale(1.15) translateY(-4px);
                box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4), 0 4px 15px rgba(255, 255, 255, 0.1);
                background: linear-gradient(135deg, #5865F2 0%, #4752C4 100%);
                color: ${THEME.textPrimary};
                border-color: #7289DA;
                animation: pulse 1.5s infinite;
            }

            #facs-highlight-toggle {
                position: fixed;
                top: 70px;
                right: 20px;
                background: linear-gradient(135deg, ${THEME.inactive} 0%, ${THEME.inactiveDark} 100%);
                color: ${THEME.textPrimary};
                padding: 8px 12px;
                border-radius: 10px;
                border: none;
                font-weight: 500;
                font-size: 12px;
                font-family: system-ui, -apple-system, sans-serif;
                cursor: pointer;
                z-index: 9999;
                box-shadow: 0 3px 12px ${THEME.shadow};
                transition: all 0.3s ease;
                display: flex;
                align-items: center;
                justify-content: center;
                min-width: auto;
                letter-spacing: 0.025em;
            }

            #facs-highlight-toggle:hover {
                transform: scale(1.08) translateY(-2px);
                box-shadow: 0 8px 25px rgba(96, 165, 250, 0.3), 0 4px 12px ${THEME.shadow};
                animation: togglePulse 1.5s infinite;
            }

            #facs-highlight-toggle.enabled {
                background: linear-gradient(135deg, ${THEME.primary} 0%, ${THEME.primaryDark} 100%);
            }

            #facs-highlight-toggle.enabled:hover {
                background: linear-gradient(135deg, ${THEME.accent} 0%, ${THEME.primary} 100%);
            }

            #facs-scrollbar-indicator {
                position: fixed;
                top: 15%;
                width: 3px;
                height: 70%;
                background: rgba(255, 255, 255, 0.08);
                border-radius: 2px;
                z-index: 9998;
                pointer-events: none;
                opacity: 0;
                transition: opacity 0.2s ease;
            }

            #facs-indicator-marker {
                position: absolute;
                left: -2px;
                width: 7px;
                height: 18px;
                background: ${THEME.primary};
                border-radius: 3px;
                box-shadow: 0 2px 8px rgba(96, 165, 250, 0.4);
                transition: all 0.15s ease;
                opacity: 0;
            }

            @keyframes pulse {
                0%, 100% { box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4), 0 4px 15px rgba(255, 255, 255, 0.1), 0 0 0 0 rgba(88, 101, 242, 0.7); }
                50% { box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4), 0 4px 15px rgba(255, 255, 255, 0.1), 0 0 0 8px rgba(88, 101, 242, 0); }
            }

            @keyframes togglePulse {
                0%, 100% { box-shadow: 0 8px 25px rgba(96, 165, 250, 0.3), 0 4px 12px ${THEME.shadow}, 0 0 0 0 rgba(96, 165, 250, 0.7); }
                50% { box-shadow: 0 8px 25px rgba(96, 165, 250, 0.3), 0 4px 12px ${THEME.shadow}, 0 0 0 8px rgba(96, 165, 250, 0); }
            }
        `;

        const styleElement = document.createElement('style');
        styleElement.textContent = css;
        document.head.appendChild(styleElement);
    }

    const createTextNodeWalker = (root) => {
        return document.createTreeWalker(
            root || document.body,
            NodeFilter.SHOW_TEXT,
            null,
            false
        );
    };

    const throttle = (callback, delay) => {
        let lastCall = 0;
        return function(...args) {
            const now = new Date().getTime();
            if (now - lastCall < delay) {
                return;
            }
            lastCall = now;
            return callback(...args);
        };
    };

    function numberFACS() {
        try {
            const walker = createTextNodeWalker();
            let node;

            function processTextNodes(deadline) {
                const startTime = performance.now();

                while ((node = walker.nextNode()) && (performance.now() - startTime < 10)) {
                    const text = node.textContent.trim();
                    const index = FACS.indexOf(text);
                    if (index !== -1 && !text.match(/^\d+\.\s/)) {
                        node.textContent = `${index + 1}. ${text}`;
                    }
                }

                if (node) {
                    requestIdleCallback(processTextNodes);
                } else {
                    numberSidebarElements();
                }
            }

            requestIdleCallback(processTextNodes);
        } catch (e) {
            console.log('FACS numbering error:', e);
        }
    }

    function numberSidebarElements() {
        try {
            const sidebarLinksSelector = 'nav a, .toc a, aside a, [class*="sidebar"] a, [class*="navigation"] a, [class*="nav"] a';
            const sidebarLinks = document.querySelectorAll(sidebarLinksSelector);

            const fragment = document.createDocumentFragment();

            sidebarLinks.forEach(link => {
                const href = link.getAttribute('href');
                if (href && href.startsWith('#')) {
                    const facsName = href.substring(1);
                    const index = FACS.indexOf(facsName);

                    if (index !== -1) {
                        const text = link.textContent.trim();
                        if (!text.match(/^\d+\.\s/)) {
                            const updatedLink = link.cloneNode(true);
                            updatedLink.textContent = `${index + 1}. ${text}`;
                            fragment.appendChild(updatedLink);
                            link.parentNode.replaceChild(updatedLink, link);
                        }
                    }
                }
            });

            const sidebarContainers = document.querySelectorAll('nav, .toc, aside, [class*="sidebar"], [class*="navigation"], [class*="nav"]');

            sidebarContainers.forEach(container => {
                const walker = createTextNodeWalker(container);
                let node;

                while (node = walker.nextNode()) {
                    const text = node.textContent.trim();
                    const index = FACS.indexOf(text);
                    if (index !== -1 && !node.textContent.match(/^\d+\.\s/)) {
                        node.textContent = `${index + 1}. ${text}`;
                    }
                }
            });
        } catch (e) {
            console.log('Sidebar numbering error:', e);
        }
    }

    function forceNumberAllFACS() {
        try {
            const walker = createTextNodeWalker();
            let node;

            const facsIndexMap = new Map();
            FACS.forEach((name, index) => {
                facsIndexMap.set(name, index);
            });

            while (node = walker.nextNode()) {
                const text = node.textContent.trim();
                const index = facsIndexMap.get(text);
                if (index !== undefined) {
                    node.textContent = `${index + 1}. ${text}`;
                    continue;
                }

                for (let i = 0; i < FACS.length; i++) {
                    if (text === FACS[i] || text.endsWith(FACS[i])) {
                        if (!text.startsWith(`${i + 1}.`)) {
                            node.textContent = `${i + 1}. ${FACS[i]}`;
                        }
                        break;
                    }
                }
            }

            numberSidebarElements();
        } catch (e) {
            console.log('Force numbering error:', e);
        }
    }

    function addDiscord() {
        try {
            if (document.getElementById('facs-discord') || elemCache.discordButton) return;

            const link = document.createElement('a');
            link.id = 'facs-discord';
            link.href = 'https://discord.gg/BE7k9Xxm5z';
            link.target = '_blank';
            link.rel = 'noopener';
            link.title = 'Join My Discord';

            link.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink: 0;">
                <path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.3-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02zM8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12zm6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12z"/>
            </svg>`;

            link.addEventListener('click', function() {
                this.style.transform = 'scale(0.95)';
                setTimeout(() => {
                    this.style.transform = 'scale(1.1) translateY(-2px)';
                }, 100);
            });

            elemCache.discordButton = link;
            document.body.appendChild(link);
        } catch (e) {
            console.log('Discord button error:', e);
        }
    }

    function addHighlightToggle() {
        try {
            if (document.getElementById('facs-highlight-toggle') || elemCache.toggleButton) return;

            const toggleButton = document.createElement('button');
            toggleButton.id = 'facs-highlight-toggle';
            toggleButton.title = 'Toggle FACS Highlighting';

            document.body.setAttribute('data-facs-highlight', 'disabled');
            toggleButton.innerHTML = `
                <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 4px;">
                    <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
                    <path d="M3 3L21 21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                </svg>
                Toggle Highlight
            `;

            toggleButton.addEventListener('click', toggleHighlightMode);

            elemCache.toggleButton = toggleButton;
            document.body.appendChild(toggleButton);
        } catch (e) {
            console.log('Highlight toggle button error:', e);
        }
    }

    function toggleHighlightMode() {
        const button = elemCache.toggleButton || document.getElementById('facs-highlight-toggle');
        if (!button) return;

        button.style.transform = 'scale(1.05) translateY(-2px)';
        button.style.transition = 'transform 0.1s ease';

        setTimeout(() => {
            button.style.transform = 'scale(1) translateY(0)';
            button.style.transition = 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)';
        }, 100);

        const isHighlightEnabled = document.body.getAttribute('data-facs-highlight') !== 'disabled';
        if (isHighlightEnabled) {
            document.body.setAttribute('data-facs-highlight', 'disabled');
            button.classList.remove('enabled');
            button.innerHTML = `
                <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 4px;">
                    <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
                    <path d="M3 3L21 21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                </svg>
                Toggle Highlight
            `;

            document.querySelectorAll('.facs-highlight').forEach(el => {
                el.style.transition = 'all 0.15s ease-out';
                el.style.background = 'transparent';
                el.style.boxShadow = 'none';

                requestAnimationFrame(() => {
                    el.classList.remove('facs-highlight', 'facs-highlight-sidebar');
                    setTimeout(() => {
                        el.style.cssText = '';
                    }, 150);
                });
            });

            document.querySelectorAll('.toc-active').forEach(el => {
                el.style.transition = 'all 0.25s ease';
                el.style.transform = 'scale(0.95)';
                el.style.opacity = '0.7';

                requestAnimationFrame(() => {
                    setTimeout(() => {
                        el.classList.remove('toc-active');
                        el.style.transform = '';
                        el.style.opacity = '';
                        el.style.transition = '';
                    }, 250);
                });
            });

            if (elemCache.indicator) {
                elemCache.indicator.style.opacity = '0';
            }
        } else {
            document.body.setAttribute('data-facs-highlight', 'enabled');
            button.classList.add('enabled');
            button.innerHTML = `
                <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 4px;">
                    <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
                </svg>
                Toggle Highlight
            `;

            highlightPairs();
            checkActiveSection();
        }
    }

    function highlightPairs() {
        try {
            const isHighlightEnabled = document.body.getAttribute('data-facs-highlight') !== 'disabled';
            if (!isHighlightEnabled) return;

            document.querySelectorAll('.facs-highlight').forEach(el => {
                el.classList.remove('facs-highlight', 'facs-highlight-sidebar');
                el.style.cssText = '';
            });

            const hash = window.location.hash.slice(1);
            if (!hash) return;

            const matchedFACS = FACS.find(f => f.toLowerCase() === hash.toLowerCase());
            if (!matchedFACS) return;

            const pair = PAIRS[matchedFACS];
            if (!pair && matchedFACS !== 'neutral') return;

            const fragment = document.createDocumentFragment();

            const walker = createTextNodeWalker();
            let node;

            const targetTexts = new Set([matchedFACS]);
            if (pair) targetTexts.add(pair);

            function processNodes(deadline) {
                const startTime = performance.now();

                while ((node = walker.nextNode()) && (performance.now() - startTime < 5)) {
                    const text = node.textContent.replace(/^\d+\.\s*/, '').trim();
                    if (targetTexts.has(text)) {
                        const parent = node.parentElement;
                        if (parent) {
                            parent.classList.add('facs-highlight');

                            parent.style.opacity = '0';
                            parent.style.transform = 'translateX(-8px)';

                            requestAnimationFrame(() => {
                                parent.style.opacity = '1';
                                parent.style.transform = 'translateX(0)';

                                if (parent.tagName === 'A' || parent.closest('nav, .toc')) {
                                    parent.classList.add('facs-highlight-sidebar');
                                }
                            });
                        }
                    }
                }

                if (node) {
                    requestAnimationFrame(() => processNodes());
                } else {
                    elemCache.lastHighlighted = hash;
                    updateScrollbarIndicator();
                }
            }

            requestAnimationFrame(processNodes);
        } catch (e) {
            console.log('Highlight error:', e);
        }
    }

    function addScrollbarIndicator() {
        try {
            if (document.getElementById('facs-scrollbar-indicator') || elemCache.indicator) return;

            let sidebar = document.querySelector('nav, .sidebar, [class*="sidebar"], [class*="navigation"]');
            if (!sidebar) {
                const facsElements = document.querySelectorAll('a[href^="#EyesLook"], a[href^="#JawDrop"]');
                if (facsElements.length > 0) {
                    sidebar = facsElements[0].closest('nav, div, ul, ol') || facsElements[0].parentElement;
                }
            }

            if (!sidebar) return;

            elemCache.sidebar = sidebar;

            const indicator = document.createElement('div');
            indicator.id = 'facs-scrollbar-indicator';

            const marker = document.createElement('div');
            marker.id = 'facs-indicator-marker';

            indicator.appendChild(marker);

            const updateIndicatorPosition = () => {
                const sidebarRect = elemCache.sidebar.getBoundingClientRect();
                indicator.style.left = `${sidebarRect.right + 8}px`;
            };

            elemCache.indicator = indicator;
            elemCache.marker = marker;

            updateIndicatorPosition();

            window.addEventListener('resize', throttle(updateIndicatorPosition, 100));

            document.body.appendChild(indicator);
        } catch (e) {
            console.log('Scrollbar indicator error:', e);
        }
    }

    function updateScrollbarIndicator() {
        try {
            const indicator = elemCache.indicator || document.getElementById('facs-scrollbar-indicator');
            const marker = elemCache.marker || document.getElementById('facs-indicator-marker');

            if (!indicator || !marker) return;

            const isHighlightEnabled = document.body.getAttribute('data-facs-highlight') !== 'disabled';
            if (!isHighlightEnabled) {
                indicator.style.opacity = '0';
                return;
            }

            const activeHighlight = document.querySelector('.facs-highlight');
            if (activeHighlight) {
                indicator.style.opacity = '1';

                const facsMap = new Map();
                FACS.forEach(name => facsMap.set(name, true));

                const allFacsItems = document.querySelectorAll('a[href^="#"], nav a, .sidebar a');
                let facsIndex = -1;
                let totalFacs = 0;
                const currentHash = window.location.hash.slice(1);

                allFacsItems.forEach((item) => {
                    const href = item.getAttribute('href');
                    if (href && href.startsWith('#')) {
                        const facsName = href.substring(1);
                        if (facsMap.has(facsName)) {
                            if (item.classList.contains('facs-highlight') || item.textContent.includes(currentHash)) {
                                facsIndex = totalFacs;
                            }
                            totalFacs++;
                        }
                    }
                });

                if (facsIndex >= 0 && totalFacs > 0) {
                    const relativePosition = facsIndex / Math.max(1, totalFacs - 1);
                    const indicatorHeight = indicator.offsetHeight;
                    const markerPosition = relativePosition * (indicatorHeight - 18);

                    marker.style.top = `${Math.max(0, Math.min(markerPosition, indicatorHeight - 18))}px`;
                    marker.style.opacity = '1';
                } else {
                    marker.style.opacity = '0';
                }
            } else {
                marker.style.opacity = '0';
                indicator.style.opacity = '0';
            }
        } catch (e) {
            console.log('Scrollbar indicator update error:', e);
        }
    }

    function collectElements() {
        const facsSet = new Set(FACS);

        elemCache.sections = Array.from(document.querySelectorAll('h2, h3, h4')).filter(heading => {
            const headingText = heading.textContent;
            for (const facs of FACS) {
                if (headingText.includes(facs)) {
                    return true;
                }
            }
            return heading.id && facsSet.has(heading.id);
        });

        elemCache.tocItems = Array.from(document.querySelectorAll('nav a[href^="#"], .toc a[href^="#"]'));
    }

    function updateTOCActiveState(sectionId) {
        const isHighlightEnabled = document.body.getAttribute('data-facs-highlight') !== 'disabled';
        if (!isHighlightEnabled) return;

        const tocItems = elemCache.tocItems.length ? elemCache.tocItems :
            document.querySelectorAll('nav a[href^="#"], .toc a[href^="#"]');

        tocItems.forEach(item => {
            item.classList.remove('toc-active');
        });

        const activeItem = tocItems.find(item => item.getAttribute('href') === `#${sectionId}`);
        if (activeItem) {
            activeItem.classList.add('toc-active');
        }
    }

    function checkActiveSection() {
        try {
            const isHighlightEnabled = document.body.getAttribute('data-facs-highlight') !== 'disabled';
            if (!isHighlightEnabled) return;

            if (elemCache.sections.length === 0 || elemCache.tocItems.length === 0) {
                collectElements();
            }

            const scrollPosition = window.scrollY || document.documentElement.scrollTop;

            const viewportHeight = window.innerHeight;
            const referencePoint = scrollPosition + (viewportHeight * 0.2);

            let activeSection = null;
            let minDistance = Infinity;

            elemCache.sections.forEach(section => {
                const rect = section.getBoundingClientRect();
                const sectionTop = rect.top + scrollPosition;
                const distance = Math.abs(sectionTop - referencePoint);

                if (distance < minDistance) {
                    minDistance = distance;
                    activeSection = section;
                }
            });

            if (activeSection) {
                let sectionId = activeSection.id;

                if (!sectionId) {
                    for (const facs of FACS) {
                        if (activeSection.textContent.includes(facs)) {
                            sectionId = facs;
                            break;
                        }
                    }
                }

                if (sectionId && sectionId !== window.location.hash.slice(1)) {
                    history.replaceState(null, null, `#${sectionId}`);
                    highlightPairs();
                    updateTOCActiveState(sectionId);
                }
            }
        } catch (e) {
            console.log('Section check error:', e);
        }
    }

    function init() {
        addStyles();

        numberFACS();

        requestIdleCallback(() => {
            forceNumberAllFACS();
            addDiscord();
            addHighlightToggle();
            addScrollbarIndicator();
            collectElements();

            setTimeout(() => {
                checkActiveSection();
            }, 500);
        });
    }

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

    window.addEventListener('hashchange', () => {
        highlightPairs();

        const hash = window.location.hash.slice(1);
        if (hash) {
            updateTOCActiveState(hash);
        }
    });

    window.addEventListener('scroll', throttle(checkActiveSection, 150), { passive: true });

    const observer = new MutationObserver(function(mutations) {
        const needsUpdate = mutations.some(mutation =>
            mutation.type === 'childList' ||
            (mutation.type === 'attributes' && mutation.target.nodeName !== 'BODY')
        );

        if (needsUpdate) {
            clearTimeout(window.facsUpdateTimeout);
            window.facsUpdateTimeout = setTimeout(() => {
                forceNumberAllFACS();
                collectElements();
                checkActiveSection();
            }, 300);
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['class', 'id', 'href']
    });
})();