Tape Operator

Watch movies on IMDB, TMDB, Kinopoisk and Letterboxd! (+ Pro Search, Cache, Settings)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name            Tape Operator
// @namespace       tape-operator
// @author          Kirlovon + Max Letov
// @description     Watch movies on IMDB, TMDB, Kinopoisk and Letterboxd! (+ Pro Search, Cache, Settings)
// @version         3.3.5
// @icon            https://github.com/Kirlovon/Tape-Operator/raw/main/assets/favicon.png
// @run-at          document-idle
// @grant           GM.info
// @grant           GM.setValue
// @grant           GM.getValue
// @grant           GM.deleteValue
// @grant           GM.openInTab
// @grant           GM.xmlHttpRequest
// @grant           GM_registerMenuCommand
// @match           *://www.kinopoisk.ru/*
// @match           *://hd.kinopoisk.ru/*
// @match           *://*.imdb.com/title/*
// @match           *://www.themoviedb.org/movie/*
// @match           *://www.themoviedb.org/tv/*
// @match           *://letterboxd.com/film/*
// @match           *://tapeop.dev/*
// @connect         api.themoviedb.org
// @connect         lingering-salad-a373.l3towm.workers.dev
// ==/UserScript==

(async function () {
    'use strict';

    const VERSION = GM.info?.script?.version || '5.0.0';
    const PLAYER_URL = 'https://tapeop.dev/';
    const BUTTON_ID = 'tape-operator-button';
    const TMDB_API_KEY = '5fc153497d26350515c189f71fb16ec0';
    const TMDB_PROXY_BASE = 'https://lingering-salad-a373.l3towm.workers.dev/3';
    const PROXY_ROOT = 'https://lingering-salad-a373.l3towm.workers.dev';

    let openInNewTab = await GM.getValue('openInNewTab', true);
    let forceLang = await GM.getValue('forceLang', 'auto');

    const KINOPOISK_MATCHER = /kinopoisk\.ru\/(film|series)\/.*/;
    const IMDB_MATCHER = /imdb\.com\/title\/tt\.*/;
    const TMDB_MATCHER = /themoviedb\.org\/(movie|tv)\/\.*/;
    const LETTERBOXD_MATCHER = /letterboxd\.com\/film\/\.*/;
    const MATCHERS = [KINOPOISK_MATCHER, IMDB_MATCHER, TMDB_MATCHER, LETTERBOXD_MATCHER];

    const i18n = {
        'ru-RU': {
            watchBtn: 'Смотреть онлайн',
            searchPlaceholder: 'Поиск фильмов и сериалов...',
            notFound: 'В базе не найдено',
            apiError: 'Ошибка API (Проверь сеть)',
            tvShow: 'Сериал', movie: 'Фильм', trailer: 'Трейлер 🎬',
            extLink: 'Кинопоиск ↗', searchExt: '🔍 Искать "{query}" на Кинопоиске →'
        },
        'en-US': {
            watchBtn: 'Watch online',
            searchPlaceholder: 'Search for movies and TV shows...',
            notFound: 'Not found in database',
            apiError: 'API Error (Check network)',
            tvShow: 'TV Show', movie: 'Movie', trailer: 'Trailer 🎬',
            extLink: 'IMDb ↗', searchExt: '🔍 Search "{query}" on IMDb →'
        }
    };

    const systemLang = (navigator.language || navigator.userLanguage).startsWith('en') ? 'en-US' : 'ru-RU';
    let currentSearchLang = forceLang === 'auto' ? systemLang : forceLang;
    let previousUrl = '/';
    let searchCache = new Map();
    let selectedIndex = -1;

    const logger = {
        info: (...args) => console.info('[Tape Operator]', ...args),
        error: (...args) => console.error('[Tape Operator]', ...args),
    };

    function throttle(func, limit) {
        let inThrottle;
        return function() {
            const args = arguments, context = this;
            if (!inThrottle) {
                func.apply(context, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        }
    }

    function initMenu() {
        GM_registerMenuCommand(`⚙️ Открывать плеер в новой вкладке: ${openInNewTab ? 'ВКЛ' : 'ВЫКЛ'}`, async () => {
            openInNewTab = !openInNewTab;
            await GM.setValue('openInNewTab', openInNewTab);
            location.reload();
        });
        GM_registerMenuCommand(`🌐 Язык по умолчанию: ${forceLang.toUpperCase()}`, async () => {
            const next = forceLang === 'auto' ? 'ru-RU' : (forceLang === 'ru-RU' ? 'en-US' : 'auto');
            await GM.setValue('forceLang', next);
            location.reload();
        });
    }

    initMenu();
    if (location.href.includes('tapeop.dev')) {
        initPlayer();
    } else {
        initButtonLogic();
    }

    function initButtonLogic() {
        const throttledUpdate = throttle(() => updateButton(), 200);
        const observer = new MutationObserver(throttledUpdate);
        observer.observe(document, { subtree: true, childList: true });
        setInterval(() => checkButtonPositions(), 500);
        updateButton();
    }

    function updateButton() {
        const url = getCurrentURL();
        if (url !== previousUrl) { document.getElementById(BUTTON_ID)?.remove(); }
        if (!MATCHERS.some((m) => url.match(m))) return removeButton();
        if (document.getElementById(BUTTON_ID)) { previousUrl = url; return; }
        if (!extractTitle()) return removeButton();

        previousUrl = url;
        createAndAttachButton();
    }

    function checkButtonPositions() {
        const url = getCurrentURL();
        const btn = document.getElementById(BUTTON_ID);
        if (!btn) return;
        if (url.includes('imdb.com')) fixImdbPosition(btn);
        else if (url.includes('hd.kinopoisk.ru')) fixHdKinopoiskPosition(btn);
    }

    function fixImdbPosition(btn) {
        const targetBtn = document.querySelector('button.ipc-split-button__btn.ipc-split-button__btn--button-radius');
        if (targetBtn && btn.nextElementSibling !== targetBtn.parentElement) {
            targetBtn.parentElement.parentElement.insertBefore(btn, targetBtn.parentElement);
            btn.className = 'tape-op-base tape-op-yellow'; btn.classList.remove('tape-op-fixed');
            btn.style.width = '100%'; btn.style.marginBottom = '12px'; btn.style.marginRight = '0';
        }
    }

    function fixHdKinopoiskPosition(btn) {
        const trailerBtn = document.querySelector('button[class*="styles_button_trailer"]');
        if (trailerBtn && btn.nextElementSibling !== trailerBtn) {
            trailerBtn.parentElement.insertBefore(btn, trailerBtn);
            btn.className = 'tape-op-base tape-op-orange'; btn.classList.remove('tape-op-fixed');
            btn.style.width = 'auto'; btn.style.marginBottom = '0'; btn.style.marginLeft = '0'; btn.style.marginRight = '12px';
        }
    }

    function createAndAttachButton() {
        if (document.getElementById(BUTTON_ID)) return;
        injectStyles();

        const url = getCurrentURL();
        const isImdb = url.includes('imdb.com');
        const btn = document.createElement('div');
        btn.id = BUTTON_ID;
        btn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg><span>${i18n[currentSearchLang].watchBtn}</span>`;
        btn.addEventListener('click', () => openPlayerFromSite());
        btn.addEventListener('mousedown', (e) => e.button === 1 && openPlayerFromSite(true));
        btn.className = `tape-op-base ${isImdb ? 'tape-op-yellow' : 'tape-op-orange'}`;

        if (url.includes('kinopoisk.ru')) {
            const buttonsContainer = document.querySelector('[class*="styles_buttonsContainer"]');
            const foldersButton = document.querySelector('[class^="styles_foldersButton"]');
            if (buttonsContainer) buttonsContainer.prepend(btn);
            else if (foldersButton?.parentElement) foldersButton.parentElement.prepend(btn);
            else { btn.classList.add('tape-op-fixed'); document.body.appendChild(btn); }
        } else if (url.includes('letterboxd.com')) {
            const actions = document.querySelector('.sidebar');
            if (actions) { btn.className = 'tape-op-base tape-op-orange'; actions.prepend(btn); }
            else { btn.classList.add('tape-op-fixed'); document.body.appendChild(btn); }
        } else {
            btn.classList.add('tape-op-fixed'); document.body.appendChild(btn);
        }
    }

    function removeButton() { document.getElementById(BUTTON_ID)?.remove(); }

    async function openPlayerFromSite(forceBg = false) {
        const data = extractMovieData();
        if (!data) return logger.error('Failed to extract movie data');
        await GM.setValue('movie-data', data);
        GM.openInTab(PLAYER_URL, forceBg || openInNewTab);
    }

    function initSearchLogic() {
        if (document.getElementById('kp-search-container')) return;
        injectStyles();

        const container = document.createElement('div'); container.id = 'kp-search-container';
        const inputWrapper = document.createElement('div'); inputWrapper.id = 'kp-search-input-wrapper';
        
        const input = document.createElement('input'); 
        input.id = 'kp-search-input'; 
        input.placeholder = i18n[currentSearchLang].searchPlaceholder; 
        input.autocomplete = 'off';

        const clearBtn = document.createElement('div');
        clearBtn.id = 'kp-clear-btn'; clearBtn.innerHTML = '✕'; clearBtn.style.display = 'none';

        const langToggle = document.createElement('button');
        langToggle.id = 'kp-lang-toggle';
        langToggle.innerText = currentSearchLang === 'ru-RU' ? 'RU' : 'EN';

        const resultsList = document.createElement('div'); resultsList.id = 'kp-search-results';

        inputWrapper.appendChild(input);
        inputWrapper.appendChild(clearBtn);
        inputWrapper.appendChild(langToggle);
        container.appendChild(inputWrapper); 
        container.appendChild(resultsList); 
        document.body.appendChild(container);

        let debounceTimer;

        clearBtn.addEventListener('click', () => {
            input.value = ''; input.focus();
            resultsList.style.display = 'none'; clearBtn.style.display = 'none';
        });

        langToggle.addEventListener('click', () => {
            currentSearchLang = currentSearchLang === 'ru-RU' ? 'en-US' : 'ru-RU';
            langToggle.innerText = currentSearchLang === 'ru-RU' ? 'RU' : 'EN';
            input.placeholder = i18n[currentSearchLang].searchPlaceholder;
            const query = input.value.trim();
            if (query.length >= 2) searchTMDB(query, resultsList, input);
        });

        input.addEventListener('input', (e) => {
            clearTimeout(debounceTimer);
            const query = e.target.value.trim();
            clearBtn.style.display = query.length > 0 ? 'block' : 'none';
            if (query.length < 2) { resultsList.style.display = 'none'; return; }
            
            showLoading(resultsList);
            debounceTimer = setTimeout(() => searchTMDB(query, resultsList, input), 300);
        });

        input.addEventListener('keydown', (e) => {
            const items = resultsList.querySelectorAll('.kp-result-item');
            if (e.key === 'ArrowDown') {
                e.preventDefault();
                selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
                updateSelection(items);
            } else if (e.key === 'ArrowUp') {
                e.preventDefault();
                selectedIndex = Math.max(selectedIndex - 1, -1);
                updateSelection(items);
            } else if (e.key === 'Enter') {
                e.preventDefault();
                if (selectedIndex >= 0 && items[selectedIndex]) {
                    items[selectedIndex].click();
                } else {
                    const query = input.value.trim();
                    if (query) searchTMDB(query, resultsList, input);
                }
            }
        });

        document.addEventListener('click', (e) => {
            if (!container.contains(e.target)) resultsList.style.display = 'none';
        });
    }

    function updateSelection(items) {
        items.forEach(i => i.classList.remove('selected'));
        if (selectedIndex >= 0 && items[selectedIndex]) {
            items[selectedIndex].classList.add('selected');
            items[selectedIndex].scrollIntoView({ block: 'nearest' });
        }
    }

    function showLoading(container) {
        container.innerHTML = '<div class="kp-spinner-container"><div class="kp-spinner"></div></div>';
        container.style.display = 'block';
    }

    async function searchTMDB(query, resultsList, input) {
        selectedIndex = -1;
        const cacheKey = `${currentSearchLang}_${query}`;
        
        if (searchCache.has(cacheKey)) {
            renderResults(searchCache.get(cacheKey), query, resultsList);
            return;
        }

        const url = `${TMDB_PROXY_BASE}/search/multi?api_key=${TMDB_API_KEY}&language=${currentSearchLang}&query=${encodeURIComponent(query)}&page=1&include_adult=false`;
        
        try {
            const res = await new Promise((resolve, reject) => GM.xmlHttpRequest({
                method: "GET", url: url,
                onload: (res) => res.status === 200 ? resolve(res.responseText) : reject(),
                onerror: reject
            }));
            const data = JSON.parse(res);
            searchCache.set(cacheKey, data.results);
            renderResults(data.results, query, resultsList);
        } catch (e) {
            resultsList.innerHTML = `<div class="kp-status-msg">${i18n[currentSearchLang].apiError}</div>`;
        }
    }

    function renderResults(movies, query, resultsList) {
        resultsList.innerHTML = '';
        const filtered = (movies || []).filter(m => m.media_type === 'movie' || m.media_type === 'tv');
        if (!filtered.length) {
            resultsList.innerHTML = `<div class="kp-status-msg">${i18n[currentSearchLang].notFound}</div>`;
            addFallbackButton(query, resultsList);
            return;
        }

        let count = 0;
        filtered.forEach(movie => {
            if (count >= 6) return; count++;
            const item = document.createElement('div'); item.className = 'kp-result-item';
            
            const title = movie.title || movie.name || '???';
            const year = (movie.release_date || movie.first_air_date || '').split('-')[0];
            const poster = movie.poster_path ? `${PROXY_ROOT}/t/p/w92${movie.poster_path}` : 'https://via.placeholder.com/44x66/333/888?text=?';
            const ratingVal = movie.vote_average || 0;
            const ratingText = ratingVal ? ratingVal.toFixed(1) : '';
            const hue = Math.max(0, Math.min(120, ratingVal * 12)); 
            const typeText = movie.media_type === 'tv' ? i18n[currentSearchLang].tvShow : i18n[currentSearchLang].movie;

            item.innerHTML = `
                <img src="${poster}" class="kp-poster">
                <div class="kp-info"><div class="kp-title">${title}</div><div class="kp-meta">${year} • ${typeText}</div></div>
                ${ratingText ? `<div class="kp-rating" style="color: hsl(${hue}, 85%, 50%)">${ratingText}</div>` : ''}
            `;

            item.addEventListener('click', async () => {
                await GM.setValue('movie-data', { tmdb: movie.id, title: title });
                window.location.href = PLAYER_URL;
            });

            const actionsBlock = document.createElement('div');
            actionsBlock.className = 'kp-actions-block';

            const trailerBtn = document.createElement('div');
            trailerBtn.className = 'kp-external-btn trailer-btn';
            trailerBtn.innerHTML = i18n[currentSearchLang].trailer;
            trailerBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                window.open(`https://www.youtube.com/results?search_query=${encodeURIComponent(title + ' ' + year + ' trailer')}`, '_blank');
            });

            const extBtn = document.createElement('div');
            extBtn.className = 'kp-external-btn';
            extBtn.innerHTML = i18n[currentSearchLang].extLink;
            extBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                const qUrl = currentSearchLang === 'ru-RU' 
                    ? `https://www.kinopoisk.ru/index.php?kp_query=${encodeURIComponent(title + (year ? ' ' + year : ''))}`
                    : `https://www.imdb.com/find/?q=${encodeURIComponent(title + (year ? ' ' + year : ''))}`;
                window.open(qUrl, '_blank');
            });

            actionsBlock.appendChild(trailerBtn);
            actionsBlock.appendChild(extBtn);
            item.appendChild(actionsBlock);
            resultsList.appendChild(item);
        });

        addFallbackButton(query, resultsList);
    }

    function addFallbackButton(query, list) {
        const btn = document.createElement('div'); btn.className = 'kp-fallback-btn';
        btn.innerHTML = i18n[currentSearchLang].searchExt.replace('{query}', query);
        btn.onclick = () => window.open(currentSearchLang === 'ru-RU' 
            ? `https://www.kinopoisk.ru/index.php?kp_query=${encodeURIComponent(query)}` 
            : `https://www.imdb.com/find/?q=${encodeURIComponent(query)}`, '_blank');
        list.appendChild(btn);
    }

    async function initPlayer() {
        const data = await GM.getValue('movie-data', {});
        await GM.deleteValue('movie-data');
        if (!data || Object.keys(data).length === 0) { initSearchLogic(); return; }

        const scriptElement = document.createElement('script');
        scriptElement.innerHTML = `globalThis.init(JSON.parse(${JSON.stringify(JSON.stringify(data))}), "${VERSION}");`;
        document.body.appendChild(scriptElement);
        initSearchLogic();
    }

    function extractMovieData() {
        const url = getCurrentURL(), title = extractTitle();
        if (!title) return null;

        if (url.match(KINOPOISK_MATCHER)) {
            if (url.includes('hd.kinopoisk.ru')) {
                try {
                    const apolloState = Object.values(JSON.parse(document.getElementById('__NEXT_DATA__').innerText)?.props?.pageProps?.apolloState?.data || {});
                    const id = apolloState.find(i => i?.__typename === 'TvSeries' || i?.__typename === 'Film')?.id;
                    return id ? { kinopoisk: id, title } : null;
                } catch(e) { return null; }
            }
            return { kinopoisk: url.split('/').at(4), title };
        }
        if (url.match(IMDB_MATCHER)) {
            const sb = document.querySelector('a[data-testid="hero-title-block__series-link"]');
            return { imdb: (sb ? sb.href : url).split('/').at(4), title };
        }
        if (url.match(TMDB_MATCHER)) return { tmdb: url.split('/').at(4).split('-')[0], title };
        if (url.match(LETTERBOXD_MATCHER)) {
            const links = Array.from(document.querySelectorAll('a'));
            const imdb = links.find(l => l?.href?.match(IMDB_MATCHER))?.href?.split('/').at(4);
            if (imdb) return { imdb, title };
            const tmdb = links.find(l => l?.href?.match(TMDB_MATCHER))?.href?.split('/').at(4)?.split('-')[0];
            if (tmdb) return { tmdbId: tmdb, title };
        }
        return null;
    }

    function getCurrentURL() { return location.origin + location.pathname; }
    function extractTitle() {
        try {
            const el = document.querySelector('meta[property="og:title"]') || document.querySelector('meta[name="twitter:title"]');
            if (!el) return null;
            let t = el.content.trim();
            if (t.startsWith('Кинопоиск.')) return null;
            t = t.replace('— смотреть онлайн в хорошем качестве — Кинопоиск', '').trim();
            if (t.includes('⭐')) return t.split('⭐')[0].trim();
            if (t.endsWith('- IMDb') && t.includes(')')) return t.slice(0, t.lastIndexOf(')') + 1).trim();
            return t;
        } catch(e) { return null; }
    }

    function injectStyles() {
        if (document.getElementById('tape-op-styles')) return;
        const s = document.createElement('style'); s.id = 'tape-op-styles';
        s.textContent = `
            .tape-op-base { display: flex; align-items: center; justify-content: center; box-sizing: border-box; height: 52px; border-radius: 26px; font-family: 'Graphik LC', sans-serif; font-weight: 700; font-size: 15px; line-height: 20px; padding: 0 24px; cursor: pointer; border: none; text-decoration: none !important; transition: all 0.2s; z-index: 9999; margin-bottom: 10px; }
            .tape-op-base:hover { transform: scale(1.02); } .tape-op-base svg { margin-right: 8px; width: 24px; height: 24px; }
            .tape-op-orange { background: linear-gradient(90deg, #ff5b35, #ff9e22); color: #fff; box-shadow: 0 4px 12px rgba(255,91,53,0.25); display: inline-flex; }
            .tape-op-orange:hover { box-shadow: 0 6px 16px rgba(255,91,53,0.4); } .tape-op-orange svg { fill: #fff; }
            .tape-op-yellow { background: #F5C518; color: #000; box-shadow: 0 2px 6px rgba(0,0,0,0.15); } .tape-op-yellow svg { fill: #000; }
            .tape-op-fixed { position: fixed; bottom: 20px; right: 20px; width: auto !important; z-index: 2147483647; }
            
            #kp-search-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); width: 550px; z-index: 2147483647; font-family: sans-serif; }
            #kp-search-input-wrapper { position: relative; width: 100%; display: flex; align-items: center; }
            #kp-search-input { width: 100%; padding: 14px 100px 14px 24px; border-radius: 28px; border: 1px solid rgba(255,255,255,0.15); background: rgba(18,18,18,0.95); color: #fff; font-size: 16px; outline: none; box-shadow: 0 8px 32px rgba(0,0,0,0.8); backdrop-filter: blur(12px); transition: all 0.2s; }
            #kp-search-input:focus { background: #191919; border-color: #ff6633; box-shadow: 0 8px 40px rgba(255,102,51,0.3); }
            #kp-clear-btn { position: absolute; right: 65px; color: #888; font-size: 18px; cursor: pointer; padding: 10px; line-height: 1; transition: color 0.2s; }
            #kp-clear-btn:hover { color: #fff; }
            #kp-lang-toggle { position: absolute; right: 12px; background: rgba(255,102,51,0.15); border: 1px solid rgba(255,102,51,0.5); color: #ff6633; border-radius: 16px; padding: 6px 12px; font-size: 13px; font-weight: bold; cursor: pointer; transition: all 0.2s; user-select: none; }
            #kp-lang-toggle:hover { background: #ff6633; color: #fff; }
            #kp-search-results { margin-top: 12px; background: rgba(25,25,25,0.98); border-radius: 16px; overflow: hidden; display: none; box-shadow: 0 10px 50px rgba(0,0,0,0.95); max-height: 500px; overflow-y: auto; border: 1px solid rgba(255,255,255,0.05); }
            
            .kp-result-item { display: flex; align-items: center; padding: 12px 16px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); transition: background 0.15s; }
            .kp-result-item:last-child { border-bottom: none; }
            .kp-result-item:hover, .kp-result-item.selected { background: rgba(255,255,255,0.15); }
            .kp-poster { width: 44px; height: 66px; object-fit: cover; border-radius: 6px; margin-right: 16px; background: #333; flex-shrink: 0; }
            .kp-info { display: flex; flex-direction: column; overflow: hidden; flex: 1; }
            .kp-title { font-weight: 600; color: #eee; font-size: 15px; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
            .kp-meta { color: #aaa; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
            .kp-rating { font-weight: 700; margin: 0 12px; font-size: 14px; }
            
            .kp-actions-block { display: flex; flex-direction: column; gap: 4px; }
            .kp-external-btn { padding: 4px 8px; border-radius: 6px; background: rgba(255,255,255,0.05); color: #888; font-size: 11px; font-weight: 600; border: 1px solid rgba(255,255,255,0.1); transition: all 0.2s; white-space: nowrap; cursor: pointer; text-align: center; }
            .kp-external-btn:hover { background: #ff6633; color: #fff; border-color: #ff6633; }
            .trailer-btn:hover { background: #ff0000; border-color: #ff0000; color: #fff; }
            
            .kp-status-msg { padding: 15px; text-align: center; color: #888; font-size: 14px; }
            .kp-fallback-btn { display: block; width: 100%; padding: 15px; text-align: center; background: #222; color: #ff6633; text-decoration: none; font-weight: 600; cursor: pointer; border-top: 1px solid rgba(255,255,255,0.1); }
            .kp-fallback-btn:hover { background: #333; }
            #kp-search-results::-webkit-scrollbar { width: 6px; }
            #kp-search-results::-webkit-scrollbar-track { background: transparent; }
            #kp-search-results::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
            
            .kp-spinner-container { padding: 20px; display: flex; justify-content: center; }
            .kp-spinner { width: 24px; height: 24px; border: 3px solid rgba(255,255,255,0.1); border-top: 3px solid #ff6633; border-radius: 50%; animation: kp-spin 1s linear infinite; }
            @keyframes kp-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        `;
        document.head.appendChild(s);
    }
})();