Greasy Fork

Greasy Fork is available in English.

soop 방송 딜레이 자동 조정

soop 방송 딜레이를 목표 시간 이내로 자동 보정

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         soop 방송 딜레이 자동 조정
// @namespace    http://greasyfork.icu/ko/scripts/539405
// @version      2.4
// @description  soop 방송 딜레이를 목표 시간 이내로 자동 보정
// @icon         https://www.google.com/s2/favicons?sz=64&domain=www.sooplive.co.kr
// @author       다크초코
// @match        https://play.sooplive.com/*
// @grant GM_registerMenuCommand
// @license      MIT
// @contributor  감귤구이
// ==/UserScript==

(function () {
    'use strict';

    const OPTIONS = {
        ENABLE_PER_CHANNEL_SETTINGS: true,
        HIDE_UI_IN_FULLSCREEN: true,
        SHOW_CLOSE_BUTTON: false,
        SHOW_TOGGLE_BUTTON: true,
        SHOW_TARGET_VALUE: true,
        SHOW_DELAY_VALUE: true,
        SHOW_RATE_VALUE: true,
        PANEL_THEME: 'dark',
        UI_MODE: 'modern',
        DRAG_WITH_CTRL_ONLY: false,
        UI_TOGGLE_SHORTCUT: {
            enabled: true,
            ctrl: true,
            alt: false,
            shift: false,
            meta: false,
            key: 'h'
        }
    };  

    const CONFIG = {
        CHECK_INTERVAL_MS: 100,          // 딜레이 체크 주기
        HISTORY_DURATION_MS: 1000,       // 최근 평균 딜레이 계산 구간
        DEFAULT_TARGET_DELAY_MS: 1500,   // 기본 목표 딜레이
        START_THRESHOLD_MS: 50,          // 목표 초과시 조정 시작 임계값 (첫 시작)
        RESTART_THRESHOLD_MS: 200,       // 목표 초과시 조정 재시작 임계값 (해제 후)
        REVERSE_START_THRESHOLD_MS: 200, // 목표 미달시 역방향 조정 시작 임계값
        CONSECUTIVE_REQUIRED: 3,         // 연속 조건 충족 횟수
        ADJUSTMENT_SPEED: 1,             // 배속 조정 속도(1~5)
        MAX_RATE: 1.5,                   // 최대 배속
        MIN_RATE: 0.8,                   // 최소 배속
        PRECISE_DEADZONE_MS: 50,         // 정배속 고정 범위 1
        WIDE_DEADZONE_MS: 200,           // 정배속 고정 범위 2

        RATE_CHANGE_TOLERANCE: 0.02,     // 외부 배속 변경 감지 허용 오차
        DISPLAY_UPDATE_MS: 100,          // UI 표시 갱신 주기
        URL_FALLBACK_CHECK_MS: 500,      // URL 변경 fallback 체크 주기
        RATE_UNLOCK_DELAY_MS: 60         // playbackRate 설정 후 보호 해제 대기시간
    };

    const STORAGE_KEYS = {
        ENABLED: 'soop_delay_enabled',
        TARGET_DELAY: 'soop_delay_target_ms',
        PANEL_POS: 'soop_delay_panel_pos',
        CHANNEL_TARGETS: 'soop_delay_channel_targets',
        UI_VISIBLE: 'soop_delay_ui_visible',
        SETTINGS: 'soop_delay_ui_settings'
    };

    const SPEED_MULTIPLIERS = {
        1: 0.05,
        2: 0.125,
        3: 0.25,
        4: 0.4,
        5: 0.6
    };

    const DEFAULT_USER_SETTINGS = {
        options: {
            ENABLE_PER_CHANNEL_SETTINGS: OPTIONS.ENABLE_PER_CHANNEL_SETTINGS,
            HIDE_UI_IN_FULLSCREEN: OPTIONS.HIDE_UI_IN_FULLSCREEN,
            SHOW_CLOSE_BUTTON: OPTIONS.SHOW_CLOSE_BUTTON,
            SHOW_TOGGLE_BUTTON: OPTIONS.SHOW_TOGGLE_BUTTON,
            SHOW_TARGET_VALUE: OPTIONS.SHOW_TARGET_VALUE,
            SHOW_DELAY_VALUE: OPTIONS.SHOW_DELAY_VALUE,
            SHOW_RATE_VALUE: OPTIONS.SHOW_RATE_VALUE,
            PANEL_THEME: OPTIONS.PANEL_THEME,
            UI_MODE: OPTIONS.UI_MODE,
            DRAG_WITH_CTRL_ONLY: OPTIONS.DRAG_WITH_CTRL_ONLY,
            UI_TOGGLE_SHORTCUT: { ...OPTIONS.UI_TOGGLE_SHORTCUT }
        },
        config: {
            ADJUSTMENT_SPEED: CONFIG.ADJUSTMENT_SPEED,
            MIN_RATE: CONFIG.MIN_RATE,
            MAX_RATE: CONFIG.MAX_RATE
        }
    };

    const MAIN_DISPLAY_OPTION_KEYS = [
        'SHOW_TOGGLE_BUTTON',
        'SHOW_TARGET_VALUE',
        'SHOW_DELAY_VALUE',
        'SHOW_RATE_VALUE'
    ];

    hydrateSavedSettings();

    let video = null;
    let intervalId = null;
    let routeCheckId = null;
    let isSettingRate = false;
    let rateUnlockTimer = null;
    let videoRateChangeHandler = null;

    let delayHistory = [];
    let delayHistorySum = 0;

    let isEnabled = loadEnabled();
    let isUiVisible = loadUiVisible();
    let currentChannelId = getCurrentChannelId();
    let targetDelayMs = loadTargetDelay();

    let isAdjusting = false;
    let isReverseAdjusting = false;
    let hasBeenAdjusted = false;
    let currentPlaybackRate = 1.0;
    let lastDisplayUpdate = 0;
    let lastKnownUrl = location.href;

    let consecutiveOverCount = 0;
    let consecutiveUnderCount = 0;
    let consecutiveReverseCount = 0;
    let consecutiveReverseStopCount = 0;

    let initialized = false;
    let historyHooked = false;
    let isSettingsOpen = false;

    function clamp(v, lo, hi) {
        return v < lo ? lo : v > hi ? hi : v;
    }

    function nearlyEqual(a, b, eps = 0.0001) {
        return Math.abs(a - b) <= eps;
    }

    function findVideo() {
        return document.querySelector('video');
    }

    function bindVideo(nextVideo) {
        if (video === nextVideo) return;
        unbindVideo();
        video = nextVideo;
        if (!video) return;
        attachRateGuard(video);
        currentPlaybackRate = safeGetPlaybackRate(video);
    }

    function unbindVideo() {
        if (video && videoRateChangeHandler) {
            video.removeEventListener('ratechange', videoRateChangeHandler, true);
        }
        videoRateChangeHandler = null;
        video = null;
    }

    function safeGetPlaybackRate(videoElement) {
        try {
            return videoElement ? videoElement.playbackRate : 1.0;
        } catch {
            return 1.0;
        }
    }

    function calculateDelayMs(videoElement) {
        if (!videoElement) return null;

        try {
            const buffered = videoElement.buffered;
            if (!buffered || buffered.length <= 0) return null;

            const end = buffered.end(buffered.length - 1);
            const now = videoElement.currentTime;
            const delaySec = end - now;

            return delaySec >= 0 ? delaySec * 1000 : null;
        } catch {
            return null;
        }
    }

    function clearDelayHistory() {
        delayHistory.length = 0;
        delayHistorySum = 0;
    }

    function pushDelayHistory(delayMs) {
        const now = Date.now();
        delayHistory.push({ delayMs, t: now });
        delayHistorySum += delayMs;

        const cutoff = now - CONFIG.HISTORY_DURATION_MS;
        while (delayHistory.length > 0 && delayHistory[0].t < cutoff) {
            delayHistorySum -= delayHistory[0].delayMs;
            delayHistory.shift();
        }
    }

    function getAverageDelayMs() {
        return delayHistory.length > 0 ? delayHistorySum / delayHistory.length : 0;
    }

    function computeAutoRate(averageDelayMs, isCurrentlyAdjusting) {
        const errorMs = averageDelayMs - targetDelayMs;
        const absError = Math.abs(errorMs);

        if (absError <= CONFIG.PRECISE_DEADZONE_MS) {
            return 1.0;
        }

        if (!isCurrentlyAdjusting && absError <= CONFIG.WIDE_DEADZONE_MS) {
            return 1.0;
        }

        const kp = SPEED_MULTIPLIERS[CONFIG.ADJUSTMENT_SPEED] || 0.125;
        const errorSec = errorMs / 1000;
        const rate = 1.0 + kp * errorSec;

        return clamp(rate, CONFIG.MIN_RATE, CONFIG.MAX_RATE);
    }

    function getExpectedRate(avgMs) {
        if (!isEnabled) return 1.0;
        return computeAutoRate(avgMs, isAdjusting || isReverseAdjusting);
    }

    function setPlaybackRateSafely(rate) {
        if (!video) return;

        const nextRate = clamp(rate, CONFIG.MIN_RATE, CONFIG.MAX_RATE);

        try {
            if (nearlyEqual(video.playbackRate, nextRate, 0.0005)) {
                currentPlaybackRate = nextRate;
                return;
            }

            isSettingRate = true;
            video.playbackRate = nextRate;
            currentPlaybackRate = nextRate;

            if (rateUnlockTimer) clearTimeout(rateUnlockTimer);
            rateUnlockTimer = setTimeout(() => {
                isSettingRate = false;
                rateUnlockTimer = null;
            }, CONFIG.RATE_UNLOCK_DELAY_MS);
        } catch {
            isSettingRate = false;
            if (rateUnlockTimer) {
                clearTimeout(rateUnlockTimer);
                rateUnlockTimer = null;
            }
        }
    }

    function attachRateGuard(videoElement) {
        if (!videoElement) return;

        videoRateChangeHandler = () => {
            if (!video || video !== videoElement || isSettingRate) return;
            if (delayHistory.length === 0) return;

            const avgMs = getAverageDelayMs();
            const expectedRate = getExpectedRate(avgMs);
            const actualRate = safeGetPlaybackRate(videoElement);

            if (Math.abs(actualRate - expectedRate) > CONFIG.RATE_CHANGE_TOLERANCE) {
                setPlaybackRateSafely(expectedRate);
            } else {
                currentPlaybackRate = actualRate;
            }
        };

        videoElement.addEventListener('ratechange', videoRateChangeHandler, true);
    }

    function resetCounters() {
        consecutiveOverCount = 0;
        consecutiveUnderCount = 0;
        consecutiveReverseCount = 0;
        consecutiveReverseStopCount = 0;
    }

    function resetAdjustmentState(resetAdjustedFlag = true) {
        isAdjusting = false;
        isReverseAdjusting = false;
        resetCounters();
        if (resetAdjustedFlag) hasBeenAdjusted = false;
    }

    function formatRateText(rate) {
        return `${rate.toFixed(3)}X`;
    }

    function renderInfo(avgMs) {
        const now = Date.now();
        if (now - lastDisplayUpdate < CONFIG.DISPLAY_UPDATE_MS) return;
        lastDisplayUpdate = now;

        const actualRate = video ? safeGetPlaybackRate(video) : 1.0;
        const avgNode = document.getElementById('soop-delay-avg');
        const rateNode = document.getElementById('soop-delay-rate');

        if (avgNode) avgNode.textContent = `${avgMs.toFixed(0)}ms`;
        if (rateNode) rateNode.textContent = formatRateText(actualRate);
    }

    function tick() {
        const foundVideo = findVideo();

        if (!foundVideo) {
            if (video) {
                unbindVideo();
                clearDelayHistory();
                resetAdjustmentState(false);
                currentPlaybackRate = 1.0;
            }
            renderInfo(0);
            return;
        }

        if (video !== foundVideo) {
            bindVideo(foundVideo);
            clearDelayHistory();
            resetAdjustmentState(false);
        }

        const delayMs = calculateDelayMs(video);
        if (delayMs == null) return;

        pushDelayHistory(delayMs);
        const avgMs = getAverageDelayMs();
        renderInfo(avgMs);

        if (!isEnabled) {
            if (!nearlyEqual(currentPlaybackRate, 1.0, 0.0005) || isAdjusting || isReverseAdjusting) {
                resetAdjustmentState(true);
                setPlaybackRateSafely(1.0);
            }
            return;
        }

        const errorMs = avgMs - targetDelayMs;
        const absErrorMs = Math.abs(errorMs);
        const thresholdToUse = hasBeenAdjusted ? CONFIG.RESTART_THRESHOLD_MS : CONFIG.START_THRESHOLD_MS;
        const avgOverTarget = avgMs > (targetDelayMs + thresholdToUse);
        const avgFarUnderTarget = avgMs < (targetDelayMs - CONFIG.REVERSE_START_THRESHOLD_MS);
        const inPreciseDeadzone = absErrorMs <= CONFIG.PRECISE_DEADZONE_MS;

        if (!isAdjusting && !isReverseAdjusting) {
            if (avgOverTarget) {
                consecutiveOverCount++;
                consecutiveUnderCount = 0;
                consecutiveReverseCount = 0;
                consecutiveReverseStopCount = 0;

                if (consecutiveOverCount >= CONFIG.CONSECUTIVE_REQUIRED) {
                    isAdjusting = true;
                    isReverseAdjusting = false;
                    hasBeenAdjusted = true;
                    consecutiveUnderCount = 0;
                    consecutiveReverseStopCount = 0;
                }
            } else if (avgFarUnderTarget) {
                consecutiveReverseCount++;
                consecutiveOverCount = 0;
                consecutiveUnderCount = 0;
                consecutiveReverseStopCount = 0;

                if (consecutiveReverseCount >= CONFIG.CONSECUTIVE_REQUIRED) {
                    isReverseAdjusting = true;
                    isAdjusting = false;
                    hasBeenAdjusted = true;
                    consecutiveUnderCount = 0;
                    consecutiveReverseStopCount = 0;
                }
            } else {
                resetCounters();
            }
        } else if (isAdjusting) {
            if (inPreciseDeadzone) {
                consecutiveUnderCount++;
                consecutiveOverCount = 0;
                if (consecutiveUnderCount >= CONFIG.CONSECUTIVE_REQUIRED) {
                    isAdjusting = false;
                    consecutiveUnderCount = 0;
                }
            } else {
                consecutiveUnderCount = 0;
            }
        } else if (isReverseAdjusting) {
            if (inPreciseDeadzone) {
                consecutiveReverseStopCount++;
                consecutiveReverseCount = 0;
                if (consecutiveReverseStopCount >= CONFIG.CONSECUTIVE_REQUIRED) {
                    isReverseAdjusting = false;
                    consecutiveReverseStopCount = 0;
                }
            } else {
                consecutiveReverseStopCount = 0;
            }
        }

        const desiredRate = getExpectedRate(avgMs);
        setPlaybackRateSafely(desiredRate);
    }

    function start() {
        stop();
        intervalId = setInterval(tick, CONFIG.CHECK_INTERVAL_MS);
    }

    function stop() {
        if (intervalId) {
            clearInterval(intervalId);
            intervalId = null;
        }
    }

    function cleanup() {
        stop();
        clearDelayHistory();
        resetAdjustmentState(true);
        currentPlaybackRate = 1.0;

        if (video) {
            try {
                video.playbackRate = 1.0;
            } catch {}
        }

        unbindVideo();

        if (rateUnlockTimer) {
            clearTimeout(rateUnlockTimer);
            rateUnlockTimer = null;
        }

        isSettingRate = false;
    }

    function loadEnabled() {
        try {
            const v = localStorage.getItem(STORAGE_KEYS.ENABLED);
            return v == null ? true : v === '1';
        } catch {
            return true;
        }
    }

    function saveEnabled(v) {
        try {
            localStorage.setItem(STORAGE_KEYS.ENABLED, v ? '1' : '0');
        } catch {}
    }

    function loadUiVisible() {
        try {
            const v = localStorage.getItem(STORAGE_KEYS.UI_VISIBLE);
            if (v == null) return true;
            return v === '1';
        } catch {
            return true;
        }
    }

    function saveUiVisible(v) {
        try {
            localStorage.setItem(STORAGE_KEYS.UI_VISIBLE, v ? '1' : '0');
        } catch {}
    }

    function getCurrentChannelId() {
        try {
            const match = location.pathname.match(/\/([^\/]+)\/[^\/]+$/);
            return match ? match[1] : null;
        } catch {
            return null;
        }
    }

    function loadChannelTargets() {
        try {
            const data = localStorage.getItem(STORAGE_KEYS.CHANNEL_TARGETS) || '{}';
            return JSON.parse(data);
        } catch {
            return {};
        }
    }

    function saveChannelTargets(targets) {
        try {
            localStorage.setItem(STORAGE_KEYS.CHANNEL_TARGETS, JSON.stringify(targets));
        } catch {}
    }

    function loadChannelTargetDelay(channelId) {
        const targets = loadChannelTargets();
        const delay = targets[channelId];
        return (delay && delay >= 200 && delay <= 8000) ? delay : CONFIG.DEFAULT_TARGET_DELAY_MS;
    }

    function saveChannelTargetDelay(channelId, ms) {
        const targets = loadChannelTargets();
        targets[channelId] = ms;
        saveChannelTargets(targets);
    }

    function loadTargetDelay() {
        if (OPTIONS.ENABLE_PER_CHANNEL_SETTINGS && currentChannelId) {
            return loadChannelTargetDelay(currentChannelId);
        }

        try {
            const v = parseInt(localStorage.getItem(STORAGE_KEYS.TARGET_DELAY) || '', 10);
            if (isFinite(v) && v >= 200 && v <= 8000) return v;
        } catch {}

        return CONFIG.DEFAULT_TARGET_DELAY_MS;
    }

    function saveTargetDelay(ms) {
        if (OPTIONS.ENABLE_PER_CHANNEL_SETTINGS && currentChannelId) {
            saveChannelTargetDelay(currentChannelId, ms);
        } else {
            try {
                localStorage.setItem(STORAGE_KEYS.TARGET_DELAY, String(ms));
            } catch {}
        }
    }

    function loadPanelPos() {
        try {
            const pos = JSON.parse(localStorage.getItem(STORAGE_KEYS.PANEL_POS) || 'null');
            if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') {
                return { x: pos.x, y: pos.y };
            }
        } catch {}
        return null;
    }

    function savePanelPos(x, y) {
        try {
            localStorage.setItem(STORAGE_KEYS.PANEL_POS, JSON.stringify({ x, y }));
        } catch {}
    }

    function clearPanelPos() {
        try {
            localStorage.removeItem(STORAGE_KEYS.PANEL_POS);
        } catch {}
    }

    function normalizeThemeName(theme) {
        return theme === 'light' ? 'light' : 'dark';
    }

    function normalizeUiMode(mode) {
        return mode === 'legacy' ? 'legacy' : 'modern';
    }

    function isLegacyUiMode() {
        return normalizeUiMode(OPTIONS.UI_MODE) === 'legacy';
    }

    function normalizeShortcutKey(key, fallback = 'h') {
        const next = String(key == null ? fallback : key).trim();
        return next ? next.slice(0, 12) : fallback;
    }

    function cloneShortcutSettings(shortcut) {
        return {
            enabled: !!shortcut.enabled,
            ctrl: !!shortcut.ctrl,
            alt: !!shortcut.alt,
            shift: !!shortcut.shift,
            meta: !!shortcut.meta,
            key: normalizeShortcutKey(shortcut.key, 'h')
        };
    }

    function normalizeShortcutSettings(shortcut) {
        return cloneShortcutSettings({
            enabled: shortcut && typeof shortcut.enabled === 'boolean' ? shortcut.enabled : OPTIONS.UI_TOGGLE_SHORTCUT.enabled,
            ctrl: shortcut && typeof shortcut.ctrl === 'boolean' ? shortcut.ctrl : OPTIONS.UI_TOGGLE_SHORTCUT.ctrl,
            alt: shortcut && typeof shortcut.alt === 'boolean' ? shortcut.alt : OPTIONS.UI_TOGGLE_SHORTCUT.alt,
            shift: shortcut && typeof shortcut.shift === 'boolean' ? shortcut.shift : OPTIONS.UI_TOGGLE_SHORTCUT.shift,
            meta: shortcut && typeof shortcut.meta === 'boolean' ? shortcut.meta : OPTIONS.UI_TOGGLE_SHORTCUT.meta,
            key: shortcut && shortcut.key != null ? shortcut.key : OPTIONS.UI_TOGGLE_SHORTCUT.key
        });
    }

    function normalizePlaybackConfig() {
        const speedLevel = parseInt(CONFIG.ADJUSTMENT_SPEED, 10);
        CONFIG.ADJUSTMENT_SPEED = clamp(isFinite(speedLevel) ? speedLevel : 3, 1, 5);

        const minRate = Number(CONFIG.MIN_RATE);
        const maxRate = Number(CONFIG.MAX_RATE);

        CONFIG.MIN_RATE = clamp(isFinite(minRate) ? minRate : 0.8, 0.5, 1.0);
        CONFIG.MAX_RATE = clamp(isFinite(maxRate) ? maxRate : 1.5, 1.0, 3.0);

        if (CONFIG.MIN_RATE > CONFIG.MAX_RATE) {
            CONFIG.MAX_RATE = CONFIG.MIN_RATE;
        }
    }

    function hydrateSavedSettings() {
        try {
            const raw = localStorage.getItem(STORAGE_KEYS.SETTINGS);
            if (!raw) return;

            const saved = JSON.parse(raw);
            if (!saved || typeof saved !== 'object') return;

            if (saved.options && typeof saved.options === 'object') {
                if (typeof saved.options.ENABLE_PER_CHANNEL_SETTINGS === 'boolean') {
                    OPTIONS.ENABLE_PER_CHANNEL_SETTINGS = saved.options.ENABLE_PER_CHANNEL_SETTINGS;
                }

                if (typeof saved.options.HIDE_UI_IN_FULLSCREEN === 'boolean') {
                    OPTIONS.HIDE_UI_IN_FULLSCREEN = saved.options.HIDE_UI_IN_FULLSCREEN;
                }

                if (typeof saved.options.SHOW_CLOSE_BUTTON === 'boolean') {
                    OPTIONS.SHOW_CLOSE_BUTTON = saved.options.SHOW_CLOSE_BUTTON;
                }

                if (typeof saved.options.SHOW_TOGGLE_BUTTON === 'boolean') {
                    OPTIONS.SHOW_TOGGLE_BUTTON = saved.options.SHOW_TOGGLE_BUTTON;
                }

                if (typeof saved.options.SHOW_TARGET_VALUE === 'boolean') {
                    OPTIONS.SHOW_TARGET_VALUE = saved.options.SHOW_TARGET_VALUE;
                }

                if (typeof saved.options.SHOW_DELAY_VALUE === 'boolean') {
                    OPTIONS.SHOW_DELAY_VALUE = saved.options.SHOW_DELAY_VALUE;
                }

                if (typeof saved.options.SHOW_RATE_VALUE === 'boolean') {
                    OPTIONS.SHOW_RATE_VALUE = saved.options.SHOW_RATE_VALUE;
                }

                if (typeof saved.options.DRAG_WITH_CTRL_ONLY === 'boolean') {
                    OPTIONS.DRAG_WITH_CTRL_ONLY = saved.options.DRAG_WITH_CTRL_ONLY;
                }

                OPTIONS.PANEL_THEME = normalizeThemeName(saved.options.PANEL_THEME);
                OPTIONS.UI_MODE = normalizeUiMode(saved.options.UI_MODE);
                OPTIONS.UI_TOGGLE_SHORTCUT = normalizeShortcutSettings(saved.options.UI_TOGGLE_SHORTCUT);
            }

            if (saved.config && typeof saved.config === 'object') {
                if (saved.config.ADJUSTMENT_SPEED != null) {
                    CONFIG.ADJUSTMENT_SPEED = saved.config.ADJUSTMENT_SPEED;
                }

                if (saved.config.MIN_RATE != null) {
                    CONFIG.MIN_RATE = saved.config.MIN_RATE;
                }

                if (saved.config.MAX_RATE != null) {
                    CONFIG.MAX_RATE = saved.config.MAX_RATE;
                }
            }

            normalizePlaybackConfig();
        } catch {}
    }

    function saveUserSettings() {
        normalizePlaybackConfig();
        OPTIONS.PANEL_THEME = normalizeThemeName(OPTIONS.PANEL_THEME);
        OPTIONS.UI_MODE = normalizeUiMode(OPTIONS.UI_MODE);
        OPTIONS.UI_TOGGLE_SHORTCUT = normalizeShortcutSettings(OPTIONS.UI_TOGGLE_SHORTCUT);

        try {
            localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify({
                options: {
                    ENABLE_PER_CHANNEL_SETTINGS: OPTIONS.ENABLE_PER_CHANNEL_SETTINGS,
                    HIDE_UI_IN_FULLSCREEN: OPTIONS.HIDE_UI_IN_FULLSCREEN,
                    SHOW_CLOSE_BUTTON: OPTIONS.SHOW_CLOSE_BUTTON,
                    SHOW_TOGGLE_BUTTON: OPTIONS.SHOW_TOGGLE_BUTTON,
                    SHOW_TARGET_VALUE: OPTIONS.SHOW_TARGET_VALUE,
                    SHOW_DELAY_VALUE: OPTIONS.SHOW_DELAY_VALUE,
                    SHOW_RATE_VALUE: OPTIONS.SHOW_RATE_VALUE,
                    PANEL_THEME: OPTIONS.PANEL_THEME,
                    UI_MODE: OPTIONS.UI_MODE,
                    DRAG_WITH_CTRL_ONLY: OPTIONS.DRAG_WITH_CTRL_ONLY,
                    UI_TOGGLE_SHORTCUT: cloneShortcutSettings(OPTIONS.UI_TOGGLE_SHORTCUT)
                },
                config: {
                    ADJUSTMENT_SPEED: CONFIG.ADJUSTMENT_SPEED,
                    MIN_RATE: CONFIG.MIN_RATE,
                    MAX_RATE: CONFIG.MAX_RATE
                }
            }));
        } catch {}
    }

    function resetUserSettings() {
        OPTIONS.ENABLE_PER_CHANNEL_SETTINGS = DEFAULT_USER_SETTINGS.options.ENABLE_PER_CHANNEL_SETTINGS;
        OPTIONS.HIDE_UI_IN_FULLSCREEN = DEFAULT_USER_SETTINGS.options.HIDE_UI_IN_FULLSCREEN;
        OPTIONS.SHOW_CLOSE_BUTTON = DEFAULT_USER_SETTINGS.options.SHOW_CLOSE_BUTTON;
        OPTIONS.SHOW_TOGGLE_BUTTON = DEFAULT_USER_SETTINGS.options.SHOW_TOGGLE_BUTTON;
        OPTIONS.SHOW_TARGET_VALUE = DEFAULT_USER_SETTINGS.options.SHOW_TARGET_VALUE;
        OPTIONS.SHOW_DELAY_VALUE = DEFAULT_USER_SETTINGS.options.SHOW_DELAY_VALUE;
        OPTIONS.SHOW_RATE_VALUE = DEFAULT_USER_SETTINGS.options.SHOW_RATE_VALUE;
        OPTIONS.PANEL_THEME = DEFAULT_USER_SETTINGS.options.PANEL_THEME;
        OPTIONS.UI_MODE = DEFAULT_USER_SETTINGS.options.UI_MODE;
        OPTIONS.DRAG_WITH_CTRL_ONLY = DEFAULT_USER_SETTINGS.options.DRAG_WITH_CTRL_ONLY;
        OPTIONS.UI_TOGGLE_SHORTCUT = normalizeShortcutSettings(DEFAULT_USER_SETTINGS.options.UI_TOGGLE_SHORTCUT);

        CONFIG.ADJUSTMENT_SPEED = DEFAULT_USER_SETTINGS.config.ADJUSTMENT_SPEED;
        CONFIG.MIN_RATE = DEFAULT_USER_SETTINGS.config.MIN_RATE;
        CONFIG.MAX_RATE = DEFAULT_USER_SETTINGS.config.MAX_RATE;

        normalizePlaybackConfig();
        saveUserSettings();
    }

    function countEnabledMainDisplayItems(nextValues = {}) {
        return MAIN_DISPLAY_OPTION_KEYS.reduce((count, key) => {
            const enabled = Object.prototype.hasOwnProperty.call(nextValues, key) ? nextValues[key] : OPTIONS[key];
            return count + (enabled ? 1 : 0);
        }, 0);
    }

    function updateMainDisplayOption(optionKey, checked, checkboxInput) {
        if (!checked && countEnabledMainDisplayItems({ [optionKey]: false }) < 1) {
            if (checkboxInput) checkboxInput.checked = true;
            return;
        }

        OPTIONS[optionKey] = checked;
        saveUserSettings();
        rebuildPanel();
    }

    function restartAdjustmentCycle(resetPlayback = true) {
        clearDelayHistory();
        resetAdjustmentState(true);

        if (resetPlayback && video) {
            setPlaybackRateSafely(1.0);
        }
    }

    function syncPlaybackFromCurrentState() {
        if (!video) return;

        if (!isEnabled || delayHistory.length === 0) {
            setPlaybackRateSafely(1.0);
            return;
        }

        setPlaybackRateSafely(getExpectedRate(getAverageDelayMs()));
    }

    function getPanelElement() {
        return document.getElementById('soop-delay-panel') || document.getElementById('delay-info');
    }

    function removePanels() {
        ['soop-delay-panel', 'delay-info'].forEach((id) => {
            const node = document.getElementById(id);
            if (node) node.remove();
        });
    }

    function rebuildPanel() {
        removePanels();
        createPanel();
        lastDisplayUpdate = 0;
        renderInfo(getAverageDelayMs());
    }

    function saveCurrentPanelPosition(panel) {
        try {
            if (isLegacyUiMode()) return;
            if (!panel || !panel.isConnected || panel.getClientRects().length === 0) return;
            const rect = panel.getBoundingClientRect();
            savePanelPos(rect.left, rect.top);
        } catch {}
    }

    function stabilizePanelAfterContentResize(panel, previousRect) {
        try {
            if (!panel || !previousRect || !panel.isConnected || panel.getClientRects().length === 0) {
                return;
            }

            const isTopAnchored = !!panel.style.top && panel.style.top !== 'auto';
            if (isTopAnchored) {
                const desiredTop = previousRect.top;

                if (Number.isFinite(desiredTop)) {
                    panel.style.top = `${desiredTop}px`;
                    panel.style.bottom = 'auto';
                }

                ensurePanelInViewport(panel);
                saveCurrentPanelPosition(panel);
                return;
            }

            ensurePanelInViewport(panel);
        } catch {
            ensurePanelInViewport(panel);
        }
    }

    function getPanelThemeStyle() {
        if (isLegacyUiMode()) {
            return {
                panelBg: 'rgba(0,0,0,0.7)',
                panelBorder: 'none',
                panelColor: '#ffffff',
                inputBg: 'rgba(255,255,255,0.08)',
                inputColor: '#ffffff',
                inputBorder: '1px solid rgba(255,255,255,0.25)',
                switchOn: 'rgba(46, 204, 113, 0.85)',
                switchOff: 'rgba(255,255,255,0.15)',
                switchBorder: '1px solid rgba(255,255,255,0.25)',
                knobBg: '#ffffff',
                closeColor: 'rgba(255,255,255,0.52)',
                closeHover: '#ffffff',
                boxShadow: 'none',
                subtleText: 'rgba(255,255,255,0.82)',
                divider: 'rgba(255,255,255,0.12)',
                buttonBg: 'rgba(255,255,255,0.08)',
                buttonHoverBg: 'rgba(255,255,255,0.12)',
                buttonActiveBg: 'rgba(255,255,255,0.16)',
                buttonBorder: '1px solid rgba(255,255,255,0.20)',
                settingsBg: 'rgba(0,0,0,0.86)',
                optionBg: '#101010',
                optionColor: '#ffffff',
                controlScheme: 'dark'
            };
        }

        if (normalizeThemeName(OPTIONS.PANEL_THEME) === 'dark') {
            return {
                panelBg: 'rgba(18,18,20,0.82)',
                panelBorder: '1px solid rgba(255,255,255,0.12)',
                panelColor: '#f5f5f7',
                inputBg: 'rgba(255,255,255,0.06)',
                inputColor: '#f5f5f7',
                inputBorder: '1px solid rgba(255,255,255,0.14)',
                switchOn: '#4ade80',
                switchOff: 'rgba(255,255,255,0.14)',
                switchBorder: '1px solid rgba(255,255,255,0.14)',
                knobBg: '#ffffff',
                closeColor: 'rgba(255,255,255,0.42)',
                closeHover: '#ffffff',
                boxShadow: '0 10px 30px rgba(0,0,0,0.28)',
                subtleText: 'rgba(255,255,255,0.72)',
                divider: 'rgba(255,255,255,0.10)',
                buttonBg: 'rgba(255,255,255,0.06)',
                buttonHoverBg: 'rgba(255,255,255,0.12)',
                buttonActiveBg: 'rgba(255,255,255,0.16)',
                buttonBorder: '1px solid rgba(255,255,255,0.12)',
                settingsBg: 'rgba(255,255,255,0.03)',
                optionBg: '#1b1b1f',
                optionColor: '#f5f5f7',
                controlScheme: 'dark'
            };
        }

        return {
            panelBg: 'rgba(250,250,252,0.88)',
            panelBorder: '1px solid rgba(15,23,42,0.08)',
            panelColor: '#0f172a',
            inputBg: 'rgba(255,255,255,0.78)',
            inputColor: '#111827',
            inputBorder: '1px solid rgba(15,23,42,0.10)',
            switchOn: '#2563eb',
            switchOff: 'rgba(148,163,184,0.22)',
            switchBorder: '1px solid rgba(15,23,42,0.10)',
            knobBg: '#ffffff',
            closeColor: 'rgba(15,23,42,0.35)',
            closeHover: '#0f172a',
            boxShadow: '0 8px 24px rgba(15,23,42,0.10)',
            subtleText: 'rgba(15,23,42,0.72)',
            divider: 'rgba(15,23,42,0.08)',
            buttonBg: 'rgba(255,255,255,0.74)',
            buttonHoverBg: 'rgba(226,232,240,0.78)',
            buttonActiveBg: 'rgba(191,219,254,0.72)',
            buttonBorder: '1px solid rgba(15,23,42,0.10)',
            settingsBg: 'rgba(255,255,255,0.35)',
            optionBg: '#ffffff',
            optionColor: '#111827',
            controlScheme: 'light'
        };
    }

    function getPanelLayoutMetrics() {
        if (isLegacyUiMode()) {
            return {
                panelOffset: '10px',
                panelGap: '0',
                panelPadding: '3px 5px',
                panelRadius: '3px',
                panelFont: '7pt/1.2 monospace',
                panelBackdropFilter: 'none',
                panelMaxWidth: 'min(92vw, 500px)',
                panelOpacity: '0.8',
                mainRowGap: '0',
                fieldRadius: '2px',
                fieldPadding: '0 2px',
                fieldHeight: '16px',
                fieldFontSize: '9px',
                selectPaddingRight: '14px',
                actionPadding: '0 0 0 5px',
                actionHeight: 'auto',
                actionRadius: '0',
                actionFontSize: '7pt',
                checkboxGap: '2px',
                checkboxFontSize: '9px',
                settingsRowGap: '3px',
                settingsTitleMinWidth: '30px',
                settingsTitleFontSize: '9px',
                switchWidth: '28px',
                switchHeight: '16px',
                switchKnobSize: '12px',
                switchKnobOnLeft: '13px',
                targetInputWidth: '46px',
                avgValueMinWidth: '32px',
                rateValueMinWidth: '34px',
                settingsWrapGap: '3px',
                settingsWrapRadius: '3px',
                settingsWrapPadding: '4px',
                settingsButtonMarginLeft: '5px',
                closeButtonMarginLeft: '5px',
                uiModeSelectWidth: '70px',
                themeSelectWidth: '68px',
                resetButtonHeight: '16px',
                resetButtonPadding: '0 4px',
                resetButtonRadius: '3px',
                resetButtonFontSize: '9px',
                shortcutKeyWidth: '34px',
                speedSelectWidth: '60px',
                rateInputWidth: '40px'
            };
        }

        return {
            panelOffset: '12px',
            panelGap: '6px',
            panelPadding: '4px 6px',
            panelRadius: '8px',
            panelFont: '10px/1.2 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans KR",sans-serif',
            panelBackdropFilter: 'blur(10px)',
            panelMaxWidth: 'min(92vw, 560px)',
            panelOpacity: '1',
            mainRowGap: '4px',
            fieldRadius: '6px',
            fieldPadding: '1px 4px',
            fieldHeight: '18px',
            fieldFontSize: '10px',
            selectPaddingRight: '14px',
            actionPadding: '0 4px',
            actionHeight: '18px',
            actionRadius: '6px',
            actionFontSize: '10px',
            checkboxGap: '4px',
            checkboxFontSize: '10px',
            settingsRowGap: '6px',
            settingsTitleMinWidth: '40px',
            settingsTitleFontSize: '10px',
            switchWidth: '32px',
            switchHeight: '18px',
            switchKnobSize: '14px',
            switchKnobOnLeft: '15px',
            targetInputWidth: '55px',
            avgValueMinWidth: '42px',
            rateValueMinWidth: '44px',
            settingsWrapGap: '6px',
            settingsWrapRadius: '8px',
            settingsWrapPadding: '6px',
            settingsButtonMarginLeft: '1px',
            closeButtonMarginLeft: '0',
            uiModeSelectWidth: '76px',
            themeSelectWidth: '78px',
            resetButtonHeight: '18px',
            resetButtonPadding: '0 6px',
            resetButtonRadius: '6px',
            resetButtonFontSize: '10px',
            shortcutKeyWidth: '48px',
            speedSelectWidth: '72px',
            rateInputWidth: '52px'
        };
    }

    function applyPanelVisibility() {
        const panel = getPanelElement();
        if (!panel) return;

        const fs = !!(
            document.fullscreenElement ||
            document.webkitFullscreenElement ||
            document.mozFullScreenElement ||
            document.msFullscreenElement
        );

        const shouldHideForFs = OPTIONS.HIDE_UI_IN_FULLSCREEN && fs;
        const visible = isUiVisible && !shouldHideForFs;

        panel.style.display = visible ? 'flex' : 'none';

        if (visible) ensurePanelInViewport(panel);
    }

    function handleFullscreenChange() {
        applyPanelVisibility();
    }

    function createPanel() {
        if (getPanelElement()) return;

        const theme = getPanelThemeStyle();
        const ui = getPanelLayoutMetrics();
        const isLegacyUi = isLegacyUiMode();

        if (isLegacyUi) {
            const panel = document.createElement('div');
            panel.id = 'soop-delay-panel';
            panel.style.cssText = [
                'position: fixed',
                'right: 10px',
                'bottom: 10px',
                'display: flex',
                'flex-direction: column',
                'align-items: stretch',
                'gap: 3px',
                'padding: 3px 4px',
                'border-radius: 4px',
                'background: rgba(0,0,0,0.75)',
                'color: #fff',
                'font: 10px/1.2 monospace',
                'font-variant-numeric: tabular-nums',
                'z-index: 10000',
                'user-select: none',
                'cursor: default',
                'white-space: nowrap'
            ].join(';');

            const mainRow = document.createElement('div');
            mainRow.style.cssText = [
                'display: flex',
                'align-items: center',
                'gap: 2px',
                'white-space: nowrap'
            ].join(';');
            panel.appendChild(mainRow);

            let isDragging = false;
            let dragOffsetX = 0;
            let dragOffsetY = 0;

            panel.addEventListener('mousedown', (e) => {
                try {
                    if (e.target instanceof Element && e.target.closest('button, input, select, label, [data-no-drag="1"]')) return;
                    if (OPTIONS.DRAG_WITH_CTRL_ONLY && !e.ctrlKey) return;
                } catch {}

                isDragging = true;
                const rect = panel.getBoundingClientRect();
                dragOffsetX = e.clientX - rect.left;
                dragOffsetY = e.clientY - rect.top;
                e.preventDefault();
            });

            const handleMove = (e) => {
                if (!isDragging) return;
                const x = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, e.clientX - dragOffsetX));
                const y = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, e.clientY - dragOffsetY));
                panel.style.left = `${x}px`;
                panel.style.top = `${y}px`;
                panel.style.right = 'auto';
                panel.style.bottom = 'auto';
            };

            const handleUp = () => {
                if (!isDragging) return;
                isDragging = false;
                const rect = panel.getBoundingClientRect();
                savePanelPos(rect.left, rect.top);
                ensurePanelInViewport(panel);
            };

            window.addEventListener('mousemove', handleMove);
            window.addEventListener('mouseup', handleUp);

            let switchState = isEnabled;
            const switchBtn = document.createElement('button');
            switchBtn.type = 'button';
            switchBtn.style.cssText = [
                'position: relative',
                'width: 32px',
                'height: 18px',
                'border-radius: 9px',
                'border: 1px solid rgba(255,255,255,0.25)',
                'padding: 0',
                'background: transparent',
                'cursor: pointer'
            ].join(';');

            const knob = document.createElement('span');
            knob.style.cssText = [
                'position: absolute',
                'top: 1px',
                'left: 1px',
                'width: 14px',
                'height: 14px',
                'border-radius: 50%',
                'background: #fff',
                'transition: left 120ms ease'
            ].join(';');
            switchBtn.appendChild(knob);

            function updateSwitch() {
                switchBtn.style.background = switchState ? 'rgba(46, 204, 113, 0.85)' : 'rgba(255,255,255,0.15)';
                knob.style.left = switchState ? '16px' : '1px';
            }
            updateSwitch();

            switchBtn.addEventListener('click', (e) => {
                switchState = !switchState;
                isEnabled = switchState;
                saveEnabled(isEnabled);
                updateSwitch();
                if (!isEnabled) setPlaybackRateSafely(1.0);
                hasBeenAdjusted = false;
                consecutiveOverCount = 0;
                consecutiveUnderCount = 0;
                e.preventDefault();
                e.stopPropagation();
                ensurePanelInViewport(panel);
            });

            const targetInput = document.createElement('input');
            targetInput.id = 'soop-delay-target-input';
            targetInput.type = 'number';
            targetInput.min = '200';
            targetInput.max = '8000';
            targetInput.step = '50';
            targetInput.value = String(targetDelayMs);
            targetInput.style.width = '55px';
            targetInput.style.color = '#fff';
            targetInput.style.background = 'rgba(255,255,255,0.08)';
            targetInput.style.border = '1px solid rgba(255,255,255,0.25)';
            targetInput.style.borderRadius = '3px';
            targetInput.style.padding = '1px 3px';
            targetInput.style.height = '18px';
            targetInput.style.fontSize = '10px';
            targetInput.style.boxSizing = 'border-box';
            targetInput.style.outline = 'none';
            targetInput.style.caretColor = '#fff';
            targetInput.addEventListener('input', () => {
                let v = parseInt(targetInput.value || '0', 10);
                if (!isFinite(v)) return;
                v = clamp(v, 200, 8000);
                targetDelayMs = v;
                saveTargetDelay(v);
                hasBeenAdjusted = false;
                consecutiveOverCount = 0;
                consecutiveUnderCount = 0;
            });

            let preservedCompositionState = null;
            targetInput.addEventListener('focus', () => {
                if (preservedCompositionState) {
                    try {
                        const selection = window.getSelection();
                        if (selection && preservedCompositionState.range) {
                            selection.removeAllRanges();
                            selection.addRange(preservedCompositionState.range);
                        }
                    } catch {}
                }
            });

            targetInput.addEventListener('blur', () => {
                try {
                    const selection = window.getSelection();
                    if (selection && selection.rangeCount > 0) {
                        preservedCompositionState = {
                            range: selection.getRangeAt(0).cloneRange(),
                            composition: document.querySelector('input:focus') === targetInput
                        };
                    }
                } catch {}
            });

            targetInput.addEventListener('compositionstart', () => {
                preservedCompositionState = { composing: true };
            });

            targetInput.addEventListener('compositionend', () => {
                if (preservedCompositionState) {
                    preservedCompositionState.composing = false;
                }
            });

            const msText = document.createElement('span');
            msText.textContent = 'ms';

            const avgVal = document.createElement('span');
            avgVal.id = 'soop-delay-avg';
            avgVal.textContent = '-ms';
            avgVal.style.display = 'inline-block';
            avgVal.style.minWidth = '24px';
            avgVal.style.textAlign = 'right';

            const rateVal = document.createElement('span');
            rateVal.id = 'soop-delay-rate';
            rateVal.textContent = '1.00X';
            rateVal.style.display = 'inline-block';
            rateVal.style.minWidth = '22px';
            rateVal.style.textAlign = 'right';

            function applyLegacyFieldStyle(el, width, extra = []) {
                el.dataset.noDrag = '1';
                el.style.cssText = [
                    `width: ${width}`,
                    `color: ${theme.inputColor}`,
                    `background: ${theme.inputBg}`,
                    `border: ${theme.inputBorder}`,
                    `border-radius: ${ui.fieldRadius}`,
                    `padding: ${ui.fieldPadding}`,
                    `height: ${ui.fieldHeight}`,
                    `font-size: ${ui.fieldFontSize}`,
                    'font-weight: 500',
                    'box-sizing: border-box',
                    'outline: none',
                    ...extra
                ].join(';');
                el.style.caretColor = theme.inputColor;
                el.style.colorScheme = theme.controlScheme;
            }

            function applyLegacySelectStyle(selectEl, width) {
                applyLegacyFieldStyle(selectEl, width, [`padding-right: ${ui.selectPaddingRight}`, 'appearance: auto']);
                Array.from(selectEl.options || []).forEach((option) => {
                    option.style.color = theme.optionColor;
                    option.style.backgroundColor = theme.optionBg;
                });
            }

            function setLegacyActionButtonState(button, active = false) {
                button.dataset.active = active ? '1' : '0';
                button.style.background = 'transparent';
                button.style.color = active ? theme.panelColor : theme.closeColor;
            }

            function createLegacyActionButton(text, title, active = false) {
                const button = document.createElement('button');
                button.type = 'button';
                button.dataset.noDrag = '1';
                button.textContent = text;
                button.title = title;
                button.style.cssText = [
                    'cursor: pointer',
                    `padding: ${ui.actionPadding}`,
                    `height: ${ui.actionHeight}`,
                    `border-radius: ${ui.actionRadius}`,
                    'border: none',
                    'background: transparent',
                    `color: ${theme.closeColor}`,
                    `font-size: ${ui.actionFontSize}`,
                    'font-weight: 700',
                    'line-height: 1',
                    'transition: color 0.15s ease'
                ].join(';');

                setLegacyActionButtonState(button, active);

                button.addEventListener('mouseenter', () => {
                    button.style.color = theme.closeHover;
                });

                button.addEventListener('mouseleave', () => {
                    setLegacyActionButtonState(button, button.dataset.active === '1');
                });

                return button;
            }

            function createLegacyCheckbox(labelText, checked, onChange) {
                const label = document.createElement('label');
                label.dataset.noDrag = '1';
                label.style.cssText = [
                    'display: inline-flex',
                    'align-items: center',
                    `gap: ${ui.checkboxGap}`,
                    'cursor: pointer',
                    `font-size: ${ui.checkboxFontSize}`,
                    `color: ${theme.subtleText}`
                ].join(';');

                const input = document.createElement('input');
                input.type = 'checkbox';
                input.checked = !!checked;
                input.dataset.noDrag = '1';
                input.style.accentColor = theme.switchOn;
                input.addEventListener('change', () => onChange(input.checked));

                const text = document.createElement('span');
                text.textContent = labelText;

                label.appendChild(input);
                label.appendChild(text);
                return { label, input };
            }

            function createLegacySettingsRow(title) {
                const row = document.createElement('div');
                row.dataset.noDrag = '1';
                row.style.cssText = [
                    'display: flex',
                    'align-items: center',
                    'flex-wrap: wrap',
                    `gap: ${ui.settingsRowGap}`
                ].join(';');

                const titleNode = document.createElement('span');
                titleNode.textContent = title;
                titleNode.style.cssText = [
                    'display: inline-flex',
                    'align-items: center',
                    `min-width: ${ui.settingsTitleMinWidth}`,
                    `font-size: ${ui.settingsTitleFontSize}`,
                    `color: ${theme.subtleText}`,
                    'font-weight: 600'
                ].join(';');

                row.appendChild(titleNode);
                return row;
            }

            const settingsWrap = document.createElement('div');
            settingsWrap.dataset.noDrag = '1';
            settingsWrap.style.cssText = [
                `display: ${isSettingsOpen ? 'flex' : 'none'}`,
                'flex-direction: column',
                `gap: ${ui.settingsWrapGap}`,
                `border-top: 1px solid ${theme.divider}`,
                `background: ${theme.settingsBg}`,
                `border-radius: ${ui.settingsWrapRadius}`,
                `padding: ${ui.settingsWrapPadding}`
            ].join(';');

            const settingsBtn = createLegacyActionButton('⚙', '설정 열기/닫기', isSettingsOpen);
            settingsBtn.style.marginLeft = ui.settingsButtonMarginLeft;
            settingsBtn.addEventListener('click', () => {
                const previousRect = panel.getBoundingClientRect();
                isSettingsOpen = !isSettingsOpen;
                settingsWrap.style.display = isSettingsOpen ? 'flex' : 'none';
                setLegacyActionButtonState(settingsBtn, isSettingsOpen);
                stabilizePanelAfterContentResize(panel, previousRect);
            });

            const closeBtn = createLegacyActionButton('✖', '패널 숨기기');
            closeBtn.style.marginLeft = ui.closeButtonMarginLeft;
            closeBtn.addEventListener('click', () => {
                isUiVisible = false;
                saveUiVisible(false);
                applyPanelVisibility();
            });

            if (OPTIONS.SHOW_TOGGLE_BUTTON) {
                mainRow.appendChild(switchBtn);
            }
            if (OPTIONS.SHOW_TARGET_VALUE) {
                mainRow.appendChild(document.createTextNode(' 목표:'));
                mainRow.appendChild(targetInput);
                mainRow.appendChild(msText);
            }
            if (OPTIONS.SHOW_DELAY_VALUE) {
                mainRow.appendChild(document.createTextNode(' 딜레이:'));
                mainRow.appendChild(avgVal);
            }
            if (OPTIONS.SHOW_RATE_VALUE) {
                mainRow.appendChild(document.createTextNode(' 배속:'));
                mainRow.appendChild(rateVal);
            }
            mainRow.appendChild(settingsBtn);
            if (OPTIONS.SHOW_CLOSE_BUTTON) {
                mainRow.appendChild(closeBtn);
            }

            const optionRow = createLegacySettingsRow('옵션');
            const perChannelOption = createLegacyCheckbox('채널별 목표', OPTIONS.ENABLE_PER_CHANNEL_SETTINGS, (checked) => {
                OPTIONS.ENABLE_PER_CHANNEL_SETTINGS = checked;
                saveUserSettings();
                targetDelayMs = loadTargetDelay();
                targetInput.value = String(targetDelayMs);
                restartAdjustmentCycle(true);
            });
            const fullscreenOption = createLegacyCheckbox('전체화면 숨김', OPTIONS.HIDE_UI_IN_FULLSCREEN, (checked) => {
                OPTIONS.HIDE_UI_IN_FULLSCREEN = checked;
                saveUserSettings();
                applyPanelVisibility();
            });
            const dragOption = createLegacyCheckbox('Ctrl 드래그', OPTIONS.DRAG_WITH_CTRL_ONLY, (checked) => {
                OPTIONS.DRAG_WITH_CTRL_ONLY = checked;
                saveUserSettings();
            });
            const closeButtonOption = createLegacyCheckbox('닫기 버튼', OPTIONS.SHOW_CLOSE_BUTTON, (checked) => {
                OPTIONS.SHOW_CLOSE_BUTTON = checked;
                saveUserSettings();
                rebuildPanel();
            });
            optionRow.appendChild(perChannelOption.label);
            optionRow.appendChild(fullscreenOption.label);
            optionRow.appendChild(dragOption.label);
            optionRow.appendChild(closeButtonOption.label);
            settingsWrap.appendChild(optionRow);

            const displayRow = createLegacySettingsRow('표시');
            let toggleVisibilityOption;
            toggleVisibilityOption = createLegacyCheckbox('토글', OPTIONS.SHOW_TOGGLE_BUTTON, (checked) => {
                updateMainDisplayOption('SHOW_TOGGLE_BUTTON', checked, toggleVisibilityOption.input);
            });
            let targetVisibilityOption;
            targetVisibilityOption = createLegacyCheckbox('목표값', OPTIONS.SHOW_TARGET_VALUE, (checked) => {
                updateMainDisplayOption('SHOW_TARGET_VALUE', checked, targetVisibilityOption.input);
            });
            let delayVisibilityOption;
            delayVisibilityOption = createLegacyCheckbox('딜레이', OPTIONS.SHOW_DELAY_VALUE, (checked) => {
                updateMainDisplayOption('SHOW_DELAY_VALUE', checked, delayVisibilityOption.input);
            });
            let rateVisibilityOption;
            rateVisibilityOption = createLegacyCheckbox('배속', OPTIONS.SHOW_RATE_VALUE, (checked) => {
                updateMainDisplayOption('SHOW_RATE_VALUE', checked, rateVisibilityOption.input);
            });
            displayRow.appendChild(toggleVisibilityOption.label);
            displayRow.appendChild(targetVisibilityOption.label);
            displayRow.appendChild(delayVisibilityOption.label);
            displayRow.appendChild(rateVisibilityOption.label);
            settingsWrap.appendChild(displayRow);

            const uiRow = createLegacySettingsRow('UI');
            const uiModeSelect = document.createElement('select');
            [['modern', 'Modern'], ['legacy', 'Legacy']].forEach(([value, label]) => {
                const option = document.createElement('option');
                option.value = value;
                option.textContent = label;
                uiModeSelect.appendChild(option);
            });
            applyLegacySelectStyle(uiModeSelect, ui.uiModeSelectWidth);
            uiModeSelect.value = normalizeUiMode(OPTIONS.UI_MODE);
            uiModeSelect.addEventListener('change', () => {
                OPTIONS.UI_MODE = normalizeUiMode(uiModeSelect.value);
                if (OPTIONS.UI_MODE === 'legacy') {
                    isSettingsOpen = false;
                }
                saveUserSettings();
                rebuildPanel();
            });
            uiRow.appendChild(uiModeSelect);
            settingsWrap.appendChild(uiRow);

            const themeRow = createLegacySettingsRow('테마');
            const themeSelect = document.createElement('select');
            [['dark', 'Dark'], ['light', 'Light']].forEach(([value, label]) => {
                const option = document.createElement('option');
                option.value = value;
                option.textContent = label;
                themeSelect.appendChild(option);
            });
            applyLegacySelectStyle(themeSelect, ui.themeSelectWidth);
            themeSelect.value = normalizeThemeName(OPTIONS.PANEL_THEME);
            themeSelect.disabled = true;
            themeSelect.title = '레거시 모드에서는 클래식 테마를 사용합니다.';
            themeSelect.style.opacity = '0.55';
            themeRow.appendChild(themeSelect);

            const resetPosBtn = document.createElement('button');
            resetPosBtn.type = 'button';
            resetPosBtn.dataset.noDrag = '1';
            resetPosBtn.textContent = '위치 초기화';
            resetPosBtn.style.cssText = [
                `height: ${ui.resetButtonHeight}`,
                `padding: ${ui.resetButtonPadding}`,
                `border-radius: ${ui.resetButtonRadius}`,
                `border: ${theme.buttonBorder}`,
                `background: ${theme.buttonBg}`,
                `color: ${theme.panelColor}`,
                'cursor: pointer',
                `font-size: ${ui.resetButtonFontSize}`
            ].join(';');
            resetPosBtn.addEventListener('click', () => {
                clearPanelPos();
                panel.style.left = 'auto';
                panel.style.top = 'auto';
                panel.style.right = ui.panelOffset;
                panel.style.bottom = ui.panelOffset;
                ensurePanelInViewport(panel);
            });
            themeRow.appendChild(resetPosBtn);
            settingsWrap.appendChild(themeRow);

            const manageRow = createLegacySettingsRow('관리');
            const resetSettingsBtn = document.createElement('button');
            resetSettingsBtn.type = 'button';
            resetSettingsBtn.dataset.noDrag = '1';
            resetSettingsBtn.textContent = '설정 초기화';
            resetSettingsBtn.style.cssText = [
                `height: ${ui.resetButtonHeight}`,
                `padding: ${ui.resetButtonPadding}`,
                `border-radius: ${ui.resetButtonRadius}`,
                `border: ${theme.buttonBorder}`,
                `background: ${theme.buttonBg}`,
                `color: ${theme.panelColor}`,
                'cursor: pointer',
                `font-size: ${ui.resetButtonFontSize}`
            ].join(';');
            resetSettingsBtn.addEventListener('click', () => {
                resetUserSettings();
                rebuildPanel();
                syncPlaybackFromCurrentState();
                applyPanelVisibility();
            });
            manageRow.appendChild(resetSettingsBtn);
            settingsWrap.appendChild(manageRow);

            const shortcutRow = createLegacySettingsRow('단축키');
            const shortcutEnabled = createLegacyCheckbox('사용', OPTIONS.UI_TOGGLE_SHORTCUT.enabled, (checked) => {
                OPTIONS.UI_TOGGLE_SHORTCUT.enabled = checked;
                saveUserSettings();
            });
            shortcutRow.appendChild(shortcutEnabled.label);

            [
                ['ctrl', 'Ctrl'],
                ['alt', 'Alt'],
                ['shift', 'Shift'],
                ['meta', 'win/cmd']
            ].forEach(([key, label]) => {
                const checkbox = createLegacyCheckbox(label, OPTIONS.UI_TOGGLE_SHORTCUT[key], (checked) => {
                    OPTIONS.UI_TOGGLE_SHORTCUT[key] = checked;
                    saveUserSettings();
                });
                shortcutRow.appendChild(checkbox.label);
            });

            const shortcutKeyInput = document.createElement('input');
            shortcutKeyInput.type = 'text';
            shortcutKeyInput.value = OPTIONS.UI_TOGGLE_SHORTCUT.key;
            shortcutKeyInput.placeholder = 'h';
            shortcutKeyInput.maxLength = 12;
            applyLegacyFieldStyle(shortcutKeyInput, ui.shortcutKeyWidth);

            function commitShortcutKey() {
                OPTIONS.UI_TOGGLE_SHORTCUT.key = normalizeShortcutKey(shortcutKeyInput.value, OPTIONS.UI_TOGGLE_SHORTCUT.key).toLowerCase();
                shortcutKeyInput.value = OPTIONS.UI_TOGGLE_SHORTCUT.key;
                saveUserSettings();
            }

            shortcutKeyInput.addEventListener('change', commitShortcutKey);
            shortcutKeyInput.addEventListener('blur', commitShortcutKey);
            shortcutKeyInput.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    commitShortcutKey();
                    shortcutKeyInput.blur();
                }
            });

            const shortcutKeyLabel = document.createElement('span');
            shortcutKeyLabel.textContent = 'Key';
            shortcutKeyLabel.style.color = theme.subtleText;
            shortcutRow.appendChild(shortcutKeyLabel);
            shortcutRow.appendChild(shortcutKeyInput);
            settingsWrap.appendChild(shortcutRow);

            const speedRow = createLegacySettingsRow('조정속도');
            const speedSelect = document.createElement('select');
            for (let i = 1; i <= 5; i += 1) {
                const option = document.createElement('option');
                option.value = String(i);
                option.textContent = `x${i}`;
                speedSelect.appendChild(option);
            }
            applyLegacySelectStyle(speedSelect, ui.speedSelectWidth);
            speedSelect.value = String(CONFIG.ADJUSTMENT_SPEED);
            speedSelect.addEventListener('change', () => {
                CONFIG.ADJUSTMENT_SPEED = clamp(parseInt(speedSelect.value || '3', 10), 1, 5);
                saveUserSettings();
                syncPlaybackFromCurrentState();
            });
            speedRow.appendChild(speedSelect);

            const minRateLabel = document.createElement('span');
            minRateLabel.textContent = 'Min';
            minRateLabel.style.color = theme.subtleText;
            speedRow.appendChild(minRateLabel);

            const minRateInput = document.createElement('input');
            minRateInput.type = 'number';
            minRateInput.min = '0.5';
            minRateInput.max = '1.0';
            minRateInput.step = '0.05';
            minRateInput.value = String(CONFIG.MIN_RATE);
            applyLegacyFieldStyle(minRateInput, ui.rateInputWidth);
            speedRow.appendChild(minRateInput);

            const maxRateLabel = document.createElement('span');
            maxRateLabel.textContent = 'Max';
            maxRateLabel.style.color = theme.subtleText;
            speedRow.appendChild(maxRateLabel);

            const maxRateInput = document.createElement('input');
            maxRateInput.type = 'number';
            maxRateInput.min = '1.0';
            maxRateInput.max = '3.0';
            maxRateInput.step = '0.05';
            maxRateInput.value = String(CONFIG.MAX_RATE);
            applyLegacyFieldStyle(maxRateInput, ui.rateInputWidth);
            speedRow.appendChild(maxRateInput);

            function commitRateBounds() {
                const nextMin = parseFloat(minRateInput.value);
                const nextMax = parseFloat(maxRateInput.value);

                if (isFinite(nextMin)) CONFIG.MIN_RATE = nextMin;
                if (isFinite(nextMax)) CONFIG.MAX_RATE = nextMax;

                normalizePlaybackConfig();
                minRateInput.value = CONFIG.MIN_RATE.toFixed(2);
                maxRateInput.value = CONFIG.MAX_RATE.toFixed(2);
                saveUserSettings();
                syncPlaybackFromCurrentState();
            }

            ['change', 'blur'].forEach((eventName) => {
                minRateInput.addEventListener(eventName, commitRateBounds);
                maxRateInput.addEventListener(eventName, commitRateBounds);
            });

            [minRateInput, maxRateInput].forEach((input) => {
                input.addEventListener('keydown', (e) => {
                    if (e.key === 'Enter') {
                        commitRateBounds();
                        input.blur();
                    }
                });
            });

            settingsWrap.appendChild(speedRow);
            panel.appendChild(settingsWrap);

            document.body.appendChild(panel);
            ensurePanelInViewport(panel);

            const saved = loadPanelPos();
            if (saved) {
                panel.style.left = `${saved.x}px`;
                panel.style.top = `${saved.y}px`;
                panel.style.right = 'auto';
                panel.style.bottom = 'auto';
                ensurePanelInViewport(panel);
            }

            applyPanelVisibility();
            return;
        }

        const panel = document.createElement('div');
        panel.id = 'soop-delay-panel';
        panel.style.cssText = [
            'position: fixed',
            `right: ${ui.panelOffset}`,
            `bottom: ${ui.panelOffset}`,
            'display: flex',
            'flex-direction: column',
            'align-items: stretch',
            `gap: ${ui.panelGap}`,
            `padding: ${ui.panelPadding}`,
            `border-radius: ${ui.panelRadius}`,
            `background: ${theme.panelBg}`,
            `border: ${theme.panelBorder}`,
            `color: ${theme.panelColor}`,
            `font: ${ui.panelFont}`,
            'font-variant-numeric: tabular-nums',
            `backdrop-filter: ${ui.panelBackdropFilter}`,
            `-webkit-backdrop-filter: ${ui.panelBackdropFilter}`,
            'z-index: 10000',
            'user-select: none',
            'cursor: default',
            'white-space: nowrap',
            `max-width: ${ui.panelMaxWidth}`,
            `opacity: ${ui.panelOpacity}`,
            `box-shadow: ${theme.boxShadow}`
        ].join(';');

        const mainRow = document.createElement('div');
        mainRow.style.cssText = [
            `display: ${isLegacyUi ? 'block' : 'flex'}`,
            `align-items: ${isLegacyUi ? 'stretch' : 'center'}`,
            `flex-wrap: ${isLegacyUi ? 'nowrap' : 'wrap'}`,
            `gap: ${ui.mainRowGap}`
        ].join(';');
        panel.appendChild(mainRow);

        const fieldBaseStyle = [
            `color: ${theme.inputColor}`,
            `background: ${theme.inputBg}`,
            `border: ${theme.inputBorder}`,
            `border-radius: ${ui.fieldRadius}`,
            `padding: ${ui.fieldPadding}`,
            `height: ${ui.fieldHeight}`,
            `font-size: ${ui.fieldFontSize}`,
            'font-weight: 500',
            'box-sizing: border-box',
            'outline: none'
        ];

        function applyFieldStyle(el, width, extra = []) {
            el.dataset.noDrag = '1';
            el.style.cssText = [`width: ${width}`, ...fieldBaseStyle, ...extra].join(';');
            el.style.caretColor = theme.inputColor;
            el.style.colorScheme = theme.controlScheme;
        }

        function applySelectStyle(selectEl, width) {
            applyFieldStyle(selectEl, width, [`padding-right: ${ui.selectPaddingRight}`, 'appearance: auto']);
            selectEl.style.colorScheme = theme.controlScheme;

            Array.from(selectEl.options || []).forEach((option) => {
                option.style.color = theme.optionColor;
                option.style.backgroundColor = theme.optionBg;
            });
        }

        function setActionButtonState(button, active = false) {
            button.dataset.active = active ? '1' : '0';
            button.style.background = isLegacyUi ? 'transparent' : (active ? theme.buttonActiveBg : 'transparent');
            button.style.color = active ? theme.panelColor : theme.closeColor;
        }

        function createActionButton(text, title, active = false) {
            const button = document.createElement('button');
            button.type = 'button';
            button.dataset.noDrag = '1';
            button.textContent = text;
            button.title = title;
            button.style.cssText = [
                'cursor: pointer',
                `padding: ${ui.actionPadding}`,
                `height: ${ui.actionHeight}`,
                `border-radius: ${ui.actionRadius}`,
                'border: none',
                'background: transparent',
                `color: ${theme.closeColor}`,
                `font-size: ${ui.actionFontSize}`,
                `font-weight: ${isLegacyUi ? '400' : '700'}`,
                `line-height: ${isLegacyUi ? '1.2' : '1'}`,
                'transition: color 0.15s ease, background 0.15s ease'
            ].join(';');

            setActionButtonState(button, active);

            button.addEventListener('mouseenter', () => {
                const isActive = button.dataset.active === '1';
                button.style.color = theme.closeHover;
                button.style.background = isLegacyUi ? 'transparent' : (isActive ? theme.buttonActiveBg : theme.buttonHoverBg);
            });

            button.addEventListener('mouseleave', () => {
                setActionButtonState(button, button.dataset.active === '1');
            });

            return button;
        }

        function createCheckbox(labelText, checked, onChange) {
            const label = document.createElement('label');
            label.dataset.noDrag = '1';
            label.style.cssText = [
                'display: inline-flex',
                'align-items: center',
                `gap: ${ui.checkboxGap}`,
                'cursor: pointer',
                `font-size: ${ui.checkboxFontSize}`,
                `color: ${theme.subtleText}`
            ].join(';');

            const input = document.createElement('input');
            input.type = 'checkbox';
            input.checked = !!checked;
            input.dataset.noDrag = '1';
            input.style.accentColor = theme.switchOn;
            input.addEventListener('change', () => onChange(input.checked));

            const text = document.createElement('span');
            text.textContent = labelText;

            label.appendChild(input);
            label.appendChild(text);
            return { label, input };
        }

        function createSettingsRow(title) {
            const row = document.createElement('div');
            row.dataset.noDrag = '1';
            row.style.cssText = [
                'display: flex',
                'align-items: center',
                'flex-wrap: wrap',
                `gap: ${ui.settingsRowGap}`
            ].join(';');

            const titleNode = document.createElement('span');
            titleNode.textContent = title;
            titleNode.style.cssText = [
                'display: inline-flex',
                'align-items: center',
                `min-width: ${ui.settingsTitleMinWidth}`,
                `font-size: ${ui.settingsTitleFontSize}`,
                `color: ${theme.subtleText}`,
                'font-weight: 600'
            ].join(';');

            row.appendChild(titleNode);
            return row;
        }

        panel.addEventListener('mousedown', (e) => {
            if (isLegacyUi) return;

            try {
                if (e.target instanceof Element && e.target.closest('button, input, select, label, [data-no-drag="1"]')) return;

                if (OPTIONS.DRAG_WITH_CTRL_ONLY && !e.ctrlKey) return;
            } catch {}

            const rect = panel.getBoundingClientRect();
            const dragOffsetX = e.clientX - rect.left;
            const dragOffsetY = e.clientY - rect.top;

            const handleMove = (event) => {
                const x = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, event.clientX - dragOffsetX));
                const y = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, event.clientY - dragOffsetY));
                panel.style.left = `${x}px`;
                panel.style.top = `${y}px`;
                panel.style.right = 'auto';
                panel.style.bottom = 'auto';
            };

            const handleUp = () => {
                window.removeEventListener('mousemove', handleMove);
                const currentRect = panel.getBoundingClientRect();
                savePanelPos(currentRect.left, currentRect.top);
                ensurePanelInViewport(panel);
            };

            window.addEventListener('mousemove', handleMove);
            window.addEventListener('mouseup', handleUp, { once: true });
            e.preventDefault();
        });

        const switchBtn = document.createElement('button');
        switchBtn.type = 'button';
        switchBtn.dataset.noDrag = '1';
        switchBtn.style.cssText = [
            'position: relative',
            `width: ${ui.switchWidth}`,
            `height: ${ui.switchHeight}`,
            'border-radius: 999px',
            `border: ${theme.switchBorder}`,
            'padding: 0',
            'cursor: pointer',
            'outline: none',
            'transition: background 0.16s ease, border-color 0.16s ease'
        ].join(';');

        const knob = document.createElement('span');
        knob.style.cssText = [
            'position: absolute',
            'top: 1px',
            'left: 1px',
            `width: ${ui.switchKnobSize}`,
            `height: ${ui.switchKnobSize}`,
            'border-radius: 50%',
            `background: ${theme.knobBg}`,
            'box-shadow: 0 1px 3px rgba(15,23,42,0.18)',
            'transition: left 120ms ease'
        ].join(';');
        switchBtn.appendChild(knob);

        function updateSwitch() {
            switchBtn.style.background = isEnabled ? theme.switchOn : theme.switchOff;
            knob.style.left = isEnabled ? ui.switchKnobOnLeft : '1px';
        }
        updateSwitch();

        switchBtn.addEventListener('click', (e) => {
            isEnabled = !isEnabled;
            saveEnabled(isEnabled);
            updateSwitch();

            restartAdjustmentCycle(true);

            if (!isEnabled) {
                setPlaybackRateSafely(1.0);
            }

            e.preventDefault();
            e.stopPropagation();
        });

        const targetInput = document.createElement('input');
        targetInput.id = 'soop-delay-target-input';
        targetInput.type = 'number';
        targetInput.min = '200';
        targetInput.max = '8000';
        targetInput.step = '50';
        targetInput.value = String(targetDelayMs);
        applyFieldStyle(targetInput, ui.targetInputWidth);

        function commitTargetInput() {
            let v = parseInt(targetInput.value || '0', 10);
            if (!isFinite(v)) {
                targetInput.value = String(targetDelayMs);
                return;
            }

            v = clamp(v, 200, 8000);
            targetInput.value = String(v);

            if (v !== targetDelayMs) {
                targetDelayMs = v;
                saveTargetDelay(v);
                restartAdjustmentCycle(false);
            }
        }

        targetInput.addEventListener('change', commitTargetInput);
        targetInput.addEventListener('blur', commitTargetInput);
        targetInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                commitTargetInput();
                targetInput.blur();
            }
        });

        const msText = document.createElement('span');
        msText.textContent = 'ms';
        msText.style.color = theme.subtleText;

        const avgVal = document.createElement('span');
        avgVal.id = 'soop-delay-avg';
        avgVal.textContent = '-ms';
        avgVal.style.display = isLegacyUi ? 'inline' : 'inline-block';
        avgVal.style.minWidth = isLegacyUi ? 'auto' : ui.avgValueMinWidth;
        avgVal.style.textAlign = isLegacyUi ? 'left' : 'right';

        const rateVal = document.createElement('span');
        rateVal.id = 'soop-delay-rate';
        rateVal.textContent = formatRateText(1.0);
        rateVal.style.display = isLegacyUi ? 'inline' : 'inline-block';
        rateVal.style.minWidth = isLegacyUi ? 'auto' : ui.rateValueMinWidth;
        rateVal.style.textAlign = isLegacyUi ? 'left' : 'right';

        if (OPTIONS.SHOW_TOGGLE_BUTTON) {
            mainRow.appendChild(switchBtn);
        }
        if (OPTIONS.SHOW_TARGET_VALUE) {
            mainRow.appendChild(document.createTextNode(' 목표:'));
            mainRow.appendChild(targetInput);
            mainRow.appendChild(msText);
        }
        if (OPTIONS.SHOW_DELAY_VALUE) {
            mainRow.appendChild(document.createTextNode(' 딜레이:'));
            mainRow.appendChild(avgVal);
        }
        if (OPTIONS.SHOW_RATE_VALUE) {
            mainRow.appendChild(document.createTextNode(' 배속:'));
            mainRow.appendChild(rateVal);
        }

        const settingsWrap = document.createElement('div');
        settingsWrap.dataset.noDrag = '1';
        settingsWrap.style.cssText = [
            `display: ${isSettingsOpen ? 'flex' : 'none'}`,
            'flex-direction: column',
            `gap: ${ui.settingsWrapGap}`,
            `border-top: 1px solid ${theme.divider}`,
            `background: ${theme.settingsBg}`,
            `border-radius: ${ui.settingsWrapRadius}`,
            `padding: ${ui.settingsWrapPadding}`
        ].join(';');

        const settingsBtn = createActionButton(isLegacyUi ? '설정' : '⚙', '설정 열기/닫기', isSettingsOpen);
        settingsBtn.style.marginLeft = ui.settingsButtonMarginLeft;
        settingsBtn.addEventListener('click', () => {
            const previousRect = panel.getBoundingClientRect();
            isSettingsOpen = !isSettingsOpen;
            settingsWrap.style.display = isSettingsOpen ? 'flex' : 'none';
            setActionButtonState(settingsBtn, isSettingsOpen);
            stabilizePanelAfterContentResize(panel, previousRect);
        });

        const closeBtn = createActionButton(isLegacyUi ? '닫기' : '✖', '패널 숨기기');
        closeBtn.style.marginLeft = ui.closeButtonMarginLeft;
        closeBtn.addEventListener('click', () => {
            isUiVisible = false;
            saveUiVisible(false);
            applyPanelVisibility();
        });

        if (!isLegacyUi) {
            mainRow.appendChild(settingsBtn);
            if (OPTIONS.SHOW_CLOSE_BUTTON) {
                mainRow.appendChild(closeBtn);
            }
        }

        if (isLegacyUi) {
            const basicRow = createSettingsRow('기본');
            const targetLabel = document.createElement('span');
            targetLabel.textContent = '목표';
            targetLabel.style.color = theme.subtleText;
            basicRow.appendChild(switchBtn);
            basicRow.appendChild(targetLabel);
            basicRow.appendChild(targetInput);
            basicRow.appendChild(msText);
            settingsWrap.appendChild(basicRow);
        }

        const optionRow = createSettingsRow('옵션');
        const perChannelOption = createCheckbox('채널별 목표', OPTIONS.ENABLE_PER_CHANNEL_SETTINGS, (checked) => {
            OPTIONS.ENABLE_PER_CHANNEL_SETTINGS = checked;
            saveUserSettings();
            targetDelayMs = loadTargetDelay();
            targetInput.value = String(targetDelayMs);
            restartAdjustmentCycle(true);
        });
        const fullscreenOption = createCheckbox('전체화면 숨김', OPTIONS.HIDE_UI_IN_FULLSCREEN, (checked) => {
            OPTIONS.HIDE_UI_IN_FULLSCREEN = checked;
            saveUserSettings();
            applyPanelVisibility();
        });
        const dragOption = createCheckbox('Ctrl 드래그', OPTIONS.DRAG_WITH_CTRL_ONLY, (checked) => {
            OPTIONS.DRAG_WITH_CTRL_ONLY = checked;
            saveUserSettings();
        });
        const closeButtonOption = createCheckbox('닫기 버튼', OPTIONS.SHOW_CLOSE_BUTTON, (checked) => {
            OPTIONS.SHOW_CLOSE_BUTTON = checked;
            saveUserSettings();
            rebuildPanel();
        });
        optionRow.appendChild(perChannelOption.label);
        optionRow.appendChild(fullscreenOption.label);
        optionRow.appendChild(dragOption.label);
        optionRow.appendChild(closeButtonOption.label);
        settingsWrap.appendChild(optionRow);

        const displayRow = createSettingsRow('표시');
        let toggleVisibilityOption;
        toggleVisibilityOption = createCheckbox('토글', OPTIONS.SHOW_TOGGLE_BUTTON, (checked) => {
            updateMainDisplayOption('SHOW_TOGGLE_BUTTON', checked, toggleVisibilityOption.input);
        });
        let targetVisibilityOption;
        targetVisibilityOption = createCheckbox('목표값', OPTIONS.SHOW_TARGET_VALUE, (checked) => {
            updateMainDisplayOption('SHOW_TARGET_VALUE', checked, targetVisibilityOption.input);
        });
        let delayVisibilityOption;
        delayVisibilityOption = createCheckbox('딜레이', OPTIONS.SHOW_DELAY_VALUE, (checked) => {
            updateMainDisplayOption('SHOW_DELAY_VALUE', checked, delayVisibilityOption.input);
        });
        let rateVisibilityOption;
        rateVisibilityOption = createCheckbox('배속', OPTIONS.SHOW_RATE_VALUE, (checked) => {
            updateMainDisplayOption('SHOW_RATE_VALUE', checked, rateVisibilityOption.input);
        });
        displayRow.appendChild(toggleVisibilityOption.label);
        displayRow.appendChild(targetVisibilityOption.label);
        displayRow.appendChild(delayVisibilityOption.label);
        displayRow.appendChild(rateVisibilityOption.label);
        settingsWrap.appendChild(displayRow);

        const uiRow = createSettingsRow('UI');
        const uiModeSelect = document.createElement('select');
        [['modern', 'Modern'], ['legacy', 'Legacy']].forEach(([value, label]) => {
            const option = document.createElement('option');
            option.value = value;
            option.textContent = label;
            uiModeSelect.appendChild(option);
        });
        applySelectStyle(uiModeSelect, ui.uiModeSelectWidth);
        uiModeSelect.value = normalizeUiMode(OPTIONS.UI_MODE);
        uiModeSelect.addEventListener('change', () => {
            OPTIONS.UI_MODE = normalizeUiMode(uiModeSelect.value);
            if (OPTIONS.UI_MODE === 'legacy') {
                isSettingsOpen = false;
            }
            saveUserSettings();
            rebuildPanel();
        });
        uiRow.appendChild(uiModeSelect);
        settingsWrap.appendChild(uiRow);

        const themeRow = createSettingsRow('테마');
        const themeSelect = document.createElement('select');
        [['dark', 'Dark'], ['light', 'Light']].forEach(([value, label]) => {
            const option = document.createElement('option');
            option.value = value;
            option.textContent = label;
            themeSelect.appendChild(option);
        });
        applySelectStyle(themeSelect, ui.themeSelectWidth);
        themeSelect.value = normalizeThemeName(OPTIONS.PANEL_THEME);
        themeSelect.disabled = isLegacyUi;
        themeSelect.title = isLegacyUi ? '레거시 모드에서는 클래식 테마를 사용합니다.' : '패널 테마';
        themeSelect.style.opacity = isLegacyUi ? '0.55' : '1';
        themeSelect.addEventListener('change', () => {
            OPTIONS.PANEL_THEME = normalizeThemeName(themeSelect.value);
            saveUserSettings();
            rebuildPanel();
        });
        themeRow.appendChild(themeSelect);

        const resetPosBtn = document.createElement('button');
        resetPosBtn.type = 'button';
        resetPosBtn.dataset.noDrag = '1';
        resetPosBtn.textContent = '위치 초기화';
        resetPosBtn.style.cssText = [
            `height: ${ui.resetButtonHeight}`,
            `padding: ${ui.resetButtonPadding}`,
            `border-radius: ${ui.resetButtonRadius}`,
            `border: ${theme.buttonBorder}`,
            `background: ${theme.buttonBg}`,
            `color: ${theme.panelColor}`,
            'cursor: pointer',
            `font-size: ${ui.resetButtonFontSize}`
        ].join(';');
        resetPosBtn.addEventListener('click', () => {
            clearPanelPos();
            panel.style.left = 'auto';
            panel.style.top = 'auto';
            panel.style.right = ui.panelOffset;
            panel.style.bottom = ui.panelOffset;
            ensurePanelInViewport(panel);
        });
        themeRow.appendChild(resetPosBtn);
        settingsWrap.appendChild(themeRow);

        const manageRow = createSettingsRow('관리');
        const resetSettingsBtn = document.createElement('button');
        resetSettingsBtn.type = 'button';
        resetSettingsBtn.dataset.noDrag = '1';
        resetSettingsBtn.textContent = '설정 초기화';
        resetSettingsBtn.style.cssText = [
            `height: ${ui.resetButtonHeight}`,
            `padding: ${ui.resetButtonPadding}`,
            `border-radius: ${ui.resetButtonRadius}`,
            `border: ${theme.buttonBorder}`,
            `background: ${theme.buttonBg}`,
            `color: ${theme.panelColor}`,
            'cursor: pointer',
            `font-size: ${ui.resetButtonFontSize}`
        ].join(';');
        resetSettingsBtn.addEventListener('click', () => {
            resetUserSettings();
            rebuildPanel();
            syncPlaybackFromCurrentState();
            applyPanelVisibility();
        });
        manageRow.appendChild(resetSettingsBtn);
        settingsWrap.appendChild(manageRow);

        const shortcutRow = createSettingsRow('단축키');
        const shortcutEnabled = createCheckbox('사용', OPTIONS.UI_TOGGLE_SHORTCUT.enabled, (checked) => {
            OPTIONS.UI_TOGGLE_SHORTCUT.enabled = checked;
            saveUserSettings();
        });
        shortcutRow.appendChild(shortcutEnabled.label);

        [
            ['ctrl', 'Ctrl'],
            ['alt', 'Alt'],
            ['shift', 'Shift'],
            ['meta', 'win/cmd']
        ].forEach(([key, label]) => {
            const checkbox = createCheckbox(label, OPTIONS.UI_TOGGLE_SHORTCUT[key], (checked) => {
                OPTIONS.UI_TOGGLE_SHORTCUT[key] = checked;
                saveUserSettings();
            });
            shortcutRow.appendChild(checkbox.label);
        });

        const shortcutKeyInput = document.createElement('input');
        shortcutKeyInput.type = 'text';
        shortcutKeyInput.value = OPTIONS.UI_TOGGLE_SHORTCUT.key;
        shortcutKeyInput.placeholder = 'h';
        shortcutKeyInput.maxLength = 12;
        applyFieldStyle(shortcutKeyInput, ui.shortcutKeyWidth);

        function commitShortcutKey() {
            OPTIONS.UI_TOGGLE_SHORTCUT.key = normalizeShortcutKey(shortcutKeyInput.value, OPTIONS.UI_TOGGLE_SHORTCUT.key).toLowerCase();
            shortcutKeyInput.value = OPTIONS.UI_TOGGLE_SHORTCUT.key;
            saveUserSettings();
        }

        shortcutKeyInput.addEventListener('change', commitShortcutKey);
        shortcutKeyInput.addEventListener('blur', commitShortcutKey);
        shortcutKeyInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                commitShortcutKey();
                shortcutKeyInput.blur();
            }
        });

        const shortcutKeyLabel = document.createElement('span');
        shortcutKeyLabel.textContent = 'Key';
        shortcutKeyLabel.style.color = theme.subtleText;
        shortcutRow.appendChild(shortcutKeyLabel);
        shortcutRow.appendChild(shortcutKeyInput);
        settingsWrap.appendChild(shortcutRow);

        const speedRow = createSettingsRow('조정속도');
        const speedSelect = document.createElement('select');
        for (let i = 1; i <= 5; i += 1) {
            const option = document.createElement('option');
            option.value = String(i);
            option.textContent = `x${i}`;
            speedSelect.appendChild(option);
        }
        applySelectStyle(speedSelect, ui.speedSelectWidth);
        speedSelect.value = String(CONFIG.ADJUSTMENT_SPEED);
        speedSelect.addEventListener('change', () => {
            CONFIG.ADJUSTMENT_SPEED = clamp(parseInt(speedSelect.value || '3', 10), 1, 5);
            saveUserSettings();
            syncPlaybackFromCurrentState();
        });
        speedRow.appendChild(speedSelect);

        const minRateLabel = document.createElement('span');
        minRateLabel.textContent = 'Min';
        minRateLabel.style.color = theme.subtleText;
        speedRow.appendChild(minRateLabel);

        const minRateInput = document.createElement('input');
        minRateInput.type = 'number';
        minRateInput.min = '0.5';
        minRateInput.max = '1.0';
        minRateInput.step = '0.05';
        minRateInput.value = String(CONFIG.MIN_RATE);
        applyFieldStyle(minRateInput, ui.rateInputWidth);
        speedRow.appendChild(minRateInput);

        const maxRateLabel = document.createElement('span');
        maxRateLabel.textContent = 'Max';
        maxRateLabel.style.color = theme.subtleText;
        speedRow.appendChild(maxRateLabel);

        const maxRateInput = document.createElement('input');
        maxRateInput.type = 'number';
        maxRateInput.min = '1.0';
        maxRateInput.max = '3.0';
        maxRateInput.step = '0.05';
        maxRateInput.value = String(CONFIG.MAX_RATE);
        applyFieldStyle(maxRateInput, ui.rateInputWidth);
        speedRow.appendChild(maxRateInput);

        function commitRateBounds() {
            const nextMin = parseFloat(minRateInput.value);
            const nextMax = parseFloat(maxRateInput.value);

            if (isFinite(nextMin)) CONFIG.MIN_RATE = nextMin;
            if (isFinite(nextMax)) CONFIG.MAX_RATE = nextMax;

            normalizePlaybackConfig();
            minRateInput.value = CONFIG.MIN_RATE.toFixed(2);
            maxRateInput.value = CONFIG.MAX_RATE.toFixed(2);
            saveUserSettings();
            syncPlaybackFromCurrentState();
        }

        ['change', 'blur'].forEach((eventName) => {
            minRateInput.addEventListener(eventName, commitRateBounds);
            maxRateInput.addEventListener(eventName, commitRateBounds);
        });

        [minRateInput, maxRateInput].forEach((input) => {
            input.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    commitRateBounds();
                    input.blur();
                }
            });
        });

        settingsWrap.appendChild(speedRow);
        panel.appendChild(settingsWrap);

        document.body.appendChild(panel);

        const saved = !isLegacyUi ? loadPanelPos() : null;
        if (saved) {
            panel.style.left = `${saved.x}px`;
            panel.style.top = `${saved.y}px`;
            panel.style.right = 'auto';
            panel.style.bottom = 'auto';
        }

        ensurePanelInViewport(panel);
        applyPanelVisibility();
    }

    function ensurePanelInViewport(panel) {
        try {
            if (!panel || !panel.isConnected || panel.getClientRects().length === 0) {
                return;
            }

            const rect = panel.getBoundingClientRect();
            const margin = 8;
            let newLeft = rect.left;
            let newTop = rect.top;

            if (rect.right > window.innerWidth - margin) newLeft -= rect.right - (window.innerWidth - margin);
            if (rect.left < margin) newLeft = margin;
            if (rect.bottom > window.innerHeight - margin) newTop -= rect.bottom - (window.innerHeight - margin);
            if (rect.top < margin) newTop = margin;

            if (newLeft !== rect.left || newTop !== rect.top) {
                panel.style.left = `${newLeft}px`;
                panel.style.top = `${newTop}px`;
                panel.style.right = 'auto';
                panel.style.bottom = 'auto';
                const r2 = panel.getBoundingClientRect();
                savePanelPos(r2.left, r2.top);
            }
        } catch {}
    }

    function updateChannelSettings() {
        const newChannelId = getCurrentChannelId();
        if (newChannelId === currentChannelId) return;

        currentChannelId = newChannelId;

        if (OPTIONS.ENABLE_PER_CHANNEL_SETTINGS) {
            targetDelayMs = loadTargetDelay();
            const targetInput = document.getElementById('soop-delay-target-input');
            if (targetInput) targetInput.value = String(targetDelayMs);
        }

        restartAdjustmentCycle(true);
    }

    function handleRouteChange() {
        if (location.href === lastKnownUrl) return;
        lastKnownUrl = location.href;

        cleanup();
        updateChannelSettings();
        createPanel();
        start();
    }

    function installHistoryHooks() {
        if (historyHooked) return;
        historyHooked = true;

        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        history.pushState = function () {
            const result = originalPushState.apply(this, arguments);
            setTimeout(handleRouteChange, 0);
            return result;
        };

        history.replaceState = function () {
            const result = originalReplaceState.apply(this, arguments);
            setTimeout(handleRouteChange, 0);
            return result;
        };

        window.addEventListener('popstate', handleRouteChange);
    }

    function startRouteWatcher() {
        if (routeCheckId) clearInterval(routeCheckId);
        routeCheckId = setInterval(handleRouteChange, CONFIG.URL_FALLBACK_CHECK_MS);
    }

    function handleVisibilityChange() {
        if (document.visibilityState === 'visible') {
            clearDelayHistory();
            resetAdjustmentState(false);
            if (video) setPlaybackRateSafely(1.0);
        }
    }

    function preventBackgroundThrottling() {
        document.addEventListener('visibilitychange', handleVisibilityChange);
    }

    function matchesShortcut(e, shortcut) {
        if (!shortcut || !shortcut.enabled) return false;
        if (!!e.ctrlKey !== !!shortcut.ctrl) return false;
        if (!!e.altKey !== !!shortcut.alt) return false;
        if (!!e.shiftKey !== !!shortcut.shift) return false;
        if (!!e.metaKey !== !!shortcut.meta) return false;
        return String(e.key || '').toLowerCase() === String(shortcut.key || '').toLowerCase();
    }

    function init() {
        if (initialized) return;
        initialized = true;

        preventBackgroundThrottling();
        installHistoryHooks();
        startRouteWatcher();
        createPanel();
        start();

        [
            'fullscreenchange',
            'webkitfullscreenchange',
            'mozfullscreenchange',
            'MSFullscreenChange'
        ].forEach(ev => document.addEventListener(ev, handleFullscreenChange));

        window.addEventListener('resize', () => {
            const panel = getPanelElement();
            if (panel && panel.getClientRects().length > 0) ensurePanelInViewport(panel);
        });

        window.addEventListener('keydown', (e) => {
            if (!matchesShortcut(e, OPTIONS.UI_TOGGLE_SHORTCUT)) return;
            e.preventDefault();
            isUiVisible = !isUiVisible;
            saveUiVisible(isUiVisible);
            applyPanelVisibility();
        });

        if (typeof GM_registerMenuCommand !== 'undefined') {
            GM_registerMenuCommand('패널 토글', () => {
                isUiVisible = !isUiVisible;
                saveUiVisible(isUiVisible);
                applyPanelVisibility();
            });

            GM_registerMenuCommand('설정 패널 토글', () => {
                isUiVisible = true;
                saveUiVisible(true);
                isSettingsOpen = !isSettingsOpen;
                rebuildPanel();
                applyPanelVisibility();
            });
        }

        window.addEventListener('beforeunload', () => {
            cleanup();
            if (routeCheckId) {
                clearInterval(routeCheckId);
                routeCheckId = null;
            }
        });
    }

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