TokyoMotion Enhancer

TokyoMotionをより便利にするスクリプト - 投稿者ブロック、単語ミュート、高評価保存、視聴履歴、プレイリスト、購読・友達動画の簡易閲覧

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         TokyoMotion Enhancer
// @namespace    http://tampermonkey.net/
// @version      3.4
// @description  TokyoMotionをより便利にするスクリプト - 投稿者ブロック、単語ミュート、高評価保存、視聴履歴、プレイリスト、購読・友達動画の簡易閲覧
// @author       meranoa
// @license MIT
// @homepageURL  https://greasyfork.org/ja/scripts/564032-tokyomotion-enhancer
// @supportURL   https://greasyfork.org/ja/scripts/564032-tokyomotion-enhancer/feedback
// @match        https://www.tokyomotion.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tokyomotion.net
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_openInTab
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    console.log('[TM Enhancer] Userscript started (Version 3.4)');

    // ========================================
    // 言語・翻訳管理 (Localization)
    // ========================================
    const TranslationManager = {
        getLanguage() {
            const config = GM_getValue('appLanguage', 'auto');
            if (config === 'auto') {
                const navLang = (navigator.language || navigator.userLanguage || 'ja').toLowerCase();
                return navLang.startsWith('ja') ? 'ja' : 'en';
            }
            return config;
        },
        setLanguage(lang) {
            GM_setValue('appLanguage', lang);
        },
        getText(key, replacements = {}) {
            const lang = this.getLanguage();
            let text = (TRANSLATIONS[lang] && TRANSLATIONS[lang][key]) || TRANSLATIONS['en'][key] || key;
            Object.keys(replacements).forEach(k => {
                text = text.replace(`{${k}}`, replacements[k]);
            });
            return text;
        }
    };

    const t = (key, repl) => TranslationManager.getText(key, repl);

    const FETCH_RANGE_TEXTS = {
        ja: {
            title: '購読・友達動画の取得範囲',
            feedTitle: '購読フィード',
            friendsTitle: '友達フィード',
            modePages: '指定したページ数まで取得',
            modeDays: '指定した日数以内の動画を取得',
            modeSinceLast: '前回取得した時刻以降の動画を取得',
            pagesUnit: 'ページ',
            daysUnit: '日以内',
            activePages: 'ページ数で取得します。',
            activeDays: '{days}日前以降の動画を取得します。',
            activeSinceLast: '前回取得時刻以降の動画を取得します。',
            conflict: '取得方式は1つだけオンにしてください。',
            none: '取得方式がオフです。取得する時は1つだけオンにしてください。',
            noLastFetch: '前回取得時刻がまだありません。先にページ数か日数指定で一度取得してください。'
        },
        en: {
            title: 'Feed/Friends fetch range',
            feedTitle: 'Subscription feed',
            friendsTitle: 'Friends feed',
            modePages: 'Fetch by page count',
            modeDays: 'Fetch videos within the last N days',
            modeSinceLast: 'Fetch videos posted since the previous fetch',
            pagesUnit: 'pages',
            daysUnit: 'days',
            activePages: 'Fetching by page count.',
            activeDays: 'Fetching videos from the last {days} days.',
            activeSinceLast: 'Fetching videos since the previous fetch.',
            conflict: 'Please enable only one fetch method.',
            none: 'No fetch method is enabled. Enable exactly one before updating.',
            noLastFetch: 'No previous fetch time exists yet. Fetch once by page count or day range first.'
        }
    };

    function fetchRangeText(key, replacements = {}) {
        const lang = TranslationManager.getLanguage() === 'ja' ? 'ja' : 'en';
        let text = (FETCH_RANGE_TEXTS[lang] && FETCH_RANGE_TEXTS[lang][key]) || FETCH_RANGE_TEXTS.en[key] || key;
        Object.keys(replacements).forEach(k => {
            text = text.replace(`{${k}}`, replacements[k]);
        });
        return text;
    }

    const TRANSLATIONS = {
        ja: {
            'tab_liked': '❤️ 高評価',
            'tab_history': '📺 履歴',
            'tab_playlists': '📁 リスト',
            'tab_feed': '📡 購読',
            'tab_friends': '🤝 友達',
            'tab_settings': '⚙️ 設定',
            'btn_close': '閉じる',
            'btn_update': '更新',
            'btn_create': '作成',
            'btn_delete': '削除',
            'btn_remove': '削除',
            'btn_export': '📥 エクスポート',
            'btn_import': '📤 インポート',
            'btn_clear_all': '🗑️ データ全削除',
            'btn_back': '← 戻る',
            'btn_rename': '名前を変更',
            'btn_sort_new': '▼ 新しい順',
            'btn_sort_old': '▲ 古い順',
            'label_private': 'PRIVATE',
            'msg_empty_liked': 'まだ高評価した動画がありません',
            'msg_empty_history': 'まだ視聴履歴がありません',
            'msg_empty_playlist': 'プレイリストがありません',
            'msg_empty_videos': '動画がありません',
            'msg_empty_feed': 'なし',
            'msg_saved_liked': '高評価に保存しました',
            'msg_added_to': '「{name}」に追加',
            'msg_removed_from': '「{name}」から削除',
            'msg_created_added': '「{name}」を作成して追加',
            'msg_fetching': '取得中...',
            'msg_fetching_users': '{count}人の動画を取得中...',
            'msg_fetching_progress': '取得中... ({current}/{total}人)',
            'msg_complete': '完了 ({count}件)',
            'msg_error': 'エラー: {msg}',
            'msg_no_users': 'ユーザーなし',
            'msg_auto_login': '自動ログイン中...',
            'msg_import_done': 'インポート完了。ページをリロードします。',
            'msg_import_error': '読み込みエラー: {msg}',
            'msg_data_cleared': '全データを削除しました。',
            'msg_scroll_reset': '長時間経過のためスクロールをリセットしました',
            'confirm_delete_liked': '高評価した動画消しますか?',
            'confirm_delete_playlist': 'プレイリスト「{name}」を削除しますか?',
            'confirm_settings_hidden': '設定タブを非表示にすると、後から再表示するのが難しくなります。本当によろしいですか?',
            'confirm_overwrite': '現在のデータを上書きします。よろしいですか?',
            'confirm_clear_all': '全てのデータ(高評価、履歴、プレイリスト)を削除しますか?\nこの操作は取り消せません。',
            'prompt_playlist_name': '新しいプレイリスト名を入力してください',
            'alert_exists': '既に存在します',
            'alert_name_used': 'その名前は既に使用されています。',
            'stg_language': '言語 / Language',
            'stg_startup_tab': '起動時に表示するタブ',
            'stg_tab_last_open': '🔄 最後に開いた項目',
            'stg_auto_login': '🔐 自動ログイン',
            'stg_auto_login_desc': 'ログインページを開いた際に自動でログインボタンを押します。',
            'stg_ui_mode': '📱 表示・操作モード',
            'stg_ui_mode_desc': '自動判定では画面幅とタッチ操作の種類に合わせます。PC向けは動画ページで常時表示、スマホ向けは他のページと同じく閉じた状態で開始します。',
            'stg_ui_mode_auto': '自動判定(推奨)',
            'stg_ui_mode_desktop': 'PC向け',
            'stg_ui_mode_mobile': 'スマホ向け',
            'stg_tab_visibility': '📑 タブ表示設定',
            'stg_grid_cols': '動画一覧の列数',
            'stg_feed_pages': '購読フィード取得ページ数',
            'stg_friend_pages': '友達フィード取得ページ数',
            'stg_page_unit': 'ページ',
            'stg_unlimited': '無制限 (全て取得)',
            'stg_col_unit': '列',
            'stg_scroll_reset': '非アクティブ時のスクロールリセット時間',
            'unit_sec': '秒',
            'unit_min': '分',
            'unit_hour': '時間',
            'time_just_now': 'たった今',
            'time_min_ago': '分前',
            'time_hour_ago': '時間前',
            'time_day_ago': '日前',
            'time_long_ago': 'かなり前',
            'time_videos_count': '{count} 動画',
            'modal_title': 'Myリスト',
            'placeholder_new_playlist': '新規リスト名',
            'filter_title': '🚫 検索結果フィルター',
            'filter_desc': '検索結果や動画一覧から、指定した投稿者またはタイトル語句に一致する動画を隠します。投稿者名は完全一致、タイトル語句は部分一致です。1行に1件ずつ入力してください。',
            'filter_enabled': 'フィルターを有効にする',
            'filter_uploaders': 'ブロックする投稿者',
            'filter_uploaders_placeholder': '例:spam_user\nannoying_poster',
            'filter_words': 'ミュートするタイトル語句',
            'filter_words_placeholder': '例:無関係なタグ\n広告',
            'filter_save': '保存して適用',
            'filter_clear': 'フィルターを全削除',
            'filter_saved': 'フィルターを保存しました',
            'filter_cleared': 'フィルターを削除しました',
            'filter_hidden_count': '{count}件を非表示',
            'filter_revealed_count': '{count}件を一時表示中',
            'filter_show_temporarily': '一時表示',
            'filter_hide_again': '再び隠す',
            'filter_block_uploader': 'この投稿者をブロック',
            'filter_blocked_uploader': '「{name}」をブロックしました',
            'filter_confirm_clear': '投稿者ブロックとタイトルミュートをすべて削除しますか?',
            'drag_thumbnail_hint': '表示される領域へドロップすると、画面を切り替えず別タブで開きます',
            'drag_drop_new_tab': 'ここにドロップしてバックグラウンドで開く',
        },
        en: {
            'tab_liked': '❤️ Liked',
            'tab_history': '📺 History',
            'tab_playlists': '📁 Playlists',
            'tab_feed': '📡 Feed',
            'tab_friends': '🤝 Friends',
            'tab_settings': '⚙️ Settings',
            'btn_close': 'Close',
            'btn_update': 'Update',
            'btn_create': 'Create',
            'btn_delete': 'Delete',
            'btn_remove': 'Remove',
            'btn_export': '📥 Export',
            'btn_import': '📤 Import',
            'btn_clear_all': '🗑️ Clear All Data',
            'btn_back': '← Back',
            'btn_rename': 'Rename',
            'btn_sort_new': '▼ Newest',
            'btn_sort_old': '▲ Oldest',
            'label_private': 'PRIVATE',
            'msg_empty_liked': 'No liked videos yet.',
            'msg_empty_history': 'No watch history yet.',
            'msg_empty_playlist': 'No playlists created.',
            'msg_empty_videos': 'No videos found.',
            'msg_empty_feed': 'Empty',
            'msg_saved_liked': 'Saved to Liked Videos',
            'msg_added_to': 'Added to "{name}"',
            'msg_removed_from': 'Removed from "{name}"',
            'msg_created_added': 'Created "{name}" and added video',
            'msg_fetching': 'Fetching...',
            'msg_fetching_users': 'Fetching videos from {count} users...',
            'msg_fetching_progress': 'Fetching... ({current}/{total} users)',
            'msg_complete': 'Done ({count} videos)',
            'msg_error': 'Error: {msg}',
            'msg_no_users': 'No users found',
            'msg_auto_login': 'Auto logging in...',
            'msg_import_done': 'Import complete. Reloading page.',
            'msg_import_error': 'Import Error: {msg}',
            'msg_data_cleared': 'All data cleared.',
            'msg_scroll_reset': 'Inactive for too long. Scroll reset.',
            'confirm_delete_liked': 'Remove this video from Liked?',
            'confirm_delete_playlist': 'Delete playlist "{name}"?',
            'confirm_settings_hidden': 'If you hide the Settings tab, it will be difficult to show it again. Are you sure?',
            'confirm_overwrite': 'This will overwrite current data. Are you sure?',
            'confirm_clear_all': 'Delete ALL data (Liked, History, Playlists)?\nThis cannot be undone.',
            'prompt_playlist_name': 'Enter new playlist name',
            'alert_exists': 'Already exists',
            'alert_name_used': 'That name is already taken.',
            'stg_language': 'Language / 言語',
            'stg_startup_tab': 'Startup Tab',
            'stg_tab_last_open': '🔄 Last Opened',
            'stg_auto_login': '🔐 Auto Login',
            'stg_auto_login_desc': 'Automatically clicks the login button when opening the login modal.',
            'stg_ui_mode': '📱 Display and interaction mode',
            'stg_ui_mode_desc': 'Auto follows the viewport and pointer type. Desktop keeps the panel open on video pages; Mobile starts closed like other pages.',
            'stg_ui_mode_auto': 'Auto (recommended)',
            'stg_ui_mode_desktop': 'Desktop',
            'stg_ui_mode_mobile': 'Mobile',
            'stg_tab_visibility': '📑 Tab Visibility',
            'stg_grid_cols': 'Video Grid Columns',
            'stg_feed_pages': 'Feed Fetch Pages',
            'stg_friend_pages': 'Friends Fetch Pages',
            'stg_page_unit': ' pages',
            'stg_unlimited': 'Unlimited',
            'stg_col_unit': ' cols',
            'stg_scroll_reset': 'Scroll Reset Time (Inactive)',
            'unit_sec': 'Seconds',
            'unit_min': 'Minutes',
            'unit_hour': 'Hours',
            'time_just_now': 'Just now',
            'time_min_ago': 'm ago',
            'time_hour_ago': 'h ago',
            'time_day_ago': 'd ago',
            'time_long_ago': 'Long ago',
            'time_videos_count': '{count} videos',
            'modal_title': 'My Playlists',
            'placeholder_new_playlist': 'New Playlist Name',
            'filter_title': '🚫 Search Result Filter',
            'filter_desc': 'Hide videos in search results and listings when the uploader or title matches. Uploader names use exact matching; title terms use substring matching. Enter one item per line.',
            'filter_enabled': 'Enable filtering',
            'filter_uploaders': 'Blocked uploaders',
            'filter_uploaders_placeholder': 'Example: spam_user\nannoying_poster',
            'filter_words': 'Muted title terms',
            'filter_words_placeholder': 'Example: unrelated tag\nadvertisement',
            'filter_save': 'Save and apply',
            'filter_clear': 'Clear filters',
            'filter_saved': 'Filters saved',
            'filter_cleared': 'Filters cleared',
            'filter_hidden_count': '{count} hidden',
            'filter_revealed_count': '{count} temporarily visible',
            'filter_show_temporarily': 'Show temporarily',
            'filter_hide_again': 'Hide again',
            'filter_block_uploader': 'Block this uploader',
            'filter_blocked_uploader': 'Blocked "{name}"',
            'filter_confirm_clear': 'Clear all blocked uploaders and muted title terms?',
            'drag_thumbnail_hint': 'Drop on the target to open in a background tab without switching',
            'drag_drop_new_tab': 'Drop here to open in the background',
        }
    };

    // ========================================
    // 定数・初期設定
    // ========================================
    const DEFAULT_TAB_ORDER = ['liked', 'history', 'playlists', 'feed', 'friends', 'settings'];
    const SCROLLBAR_MARGIN = 25;

    // ========================================
    // ストレージマネージャー
    // ========================================
    const StorageManager = {
        async getLikedVideos() { return GM_getValue('likedVideos', []); },
        async addLikedVideo(videoData) {
            const videos = await this.getLikedVideos();
            if (!videos.some(v => v.id === videoData.id)) {
                videos.unshift(videoData);
                GM_setValue('likedVideos', videos);
            }
        },
        async removeLikedVideo(videoId) {
            let videos = await this.getLikedVideos();
            videos = videos.filter(v => v.id !== videoId);
            GM_setValue('likedVideos', videos);
        },
        async getHistory() { return GM_getValue('history', []); },
        async addToHistory(videoData) {
            let history = await this.getHistory();
            history.unshift({ ...videoData, watchedAt: Date.now() });
            GM_setValue('history', history);
        },
        async clearHistory() { GM_setValue('history', []); },
        async getPlaylists() { return GM_getValue('playlists', {}); },
        async createPlaylist(name) {
            const playlists = await this.getPlaylists();
            if (playlists[name]) return false;
            playlists[name] = [];
            GM_setValue('playlists', playlists);
            return true;
        },
        async deletePlaylist(name) {
            const playlists = await this.getPlaylists();
            delete playlists[name];
            GM_setValue('playlists', playlists);
            if (this.getActivePlaylist() === name) {
                this.setActivePlaylist(null);
            }
        },
        async renamePlaylist(oldName, newName) {
            if (oldName === newName) return true;
            const playlists = await this.getPlaylists();
            if (playlists[newName]) return false;
            playlists[newName] = playlists[oldName];
            delete playlists[oldName];
            GM_setValue('playlists', playlists);
            const order = this.getPlaylistOrder();
            const idx = order.indexOf(oldName);
            if (idx !== -1) {
                order[idx] = newName;
                GM_setValue('playlistOrder', order);
            }
            if (this.getActivePlaylist() === oldName) {
                this.setActivePlaylist(newName);
            }
            return true;
        },
        async addToPlaylist(name, videoData) {
            const playlists = await this.getPlaylists();
            if (!playlists[name]) return;
            if (!playlists[name].some(v => v.id === videoData.id)) {
                playlists[name].unshift(videoData);
                GM_setValue('playlists', playlists);
            }
        },
        async removeFromPlaylist(name, videoId) {
            const playlists = await this.getPlaylists();
            if (!playlists[name]) return;
            playlists[name] = playlists[name].filter(v => v.id !== videoId);
            GM_setValue('playlists', playlists);
        },
        getPrivateCache() { return GM_getValue('privateVideoCache', {}); },
        addPrivateToCache(ids) {
            const cache = this.getPrivateCache();
            let changed = false;
            ids.forEach(id => {
                if (!cache[id]) { cache[id] = 1; changed = true; }
            });
            if (changed) GM_setValue('privateVideoCache', cache);
        },
        isPrivateCached(id) {
            const cache = this.getPrivateCache();
            return !!cache[id];
        },
        getDefaultTab() { return GM_getValue('defaultTab', 'liked'); },
        setDefaultTab(tab) { GM_setValue('defaultTab', tab); },
        getLastActiveTab() { return GM_getValue('lastActiveTab', 'liked'); },
        setLastActiveTab(tab) { GM_setValue('lastActiveTab', tab); },
        isAutoLoginEnabled() { return GM_getValue('autoLoginEnabled', false); },
        setAutoLoginEnabled(enabled) { GM_setValue('autoLoginEnabled', enabled); },
        getPlaylistOrder() { return GM_getValue('playlistOrder', []); },
        setPlaylistOrder(order) { GM_setValue('playlistOrder', order); },
        async getOrderedPlaylistNames() {
            const playlists = await this.getPlaylists();
            const savedOrder = this.getPlaylistOrder();
            const allNames = Object.keys(playlists);
            const ordered = savedOrder.filter(name => allNames.includes(name));
            const remaining = allNames.filter(name => !ordered.includes(name));
            return [...ordered, ...remaining];
        },
        getFeedData() { return GM_getValue('feedData', []); },
        setFeedData(data) { GM_setValue('feedData', data); },
        getFeedLastUpdated() { return GM_getValue('feedLastUpdated', 0); },
        setFeedLastUpdated(time) { GM_setValue('feedLastUpdated', time); },
        getFriendsFeedData() { return GM_getValue('friendsFeedData', []); },
        setFriendsFeedData(data) { GM_setValue('friendsFeedData', data); },
        getFriendsLastUpdated() { return GM_getValue('friendsLastUpdated', 0); },
        setFriendsLastUpdated(time) { GM_setValue('friendsLastUpdated', time); },
        getFeedMaxPages() { return GM_getValue('feedMaxPages', 1); },
        setFeedMaxPages(pages) { GM_setValue('feedMaxPages', pages); },
        getFriendsMaxPages() { return GM_getValue('friendsMaxPages', 1); },
        setFriendsMaxPages(pages) { GM_setValue('friendsMaxPages', pages); },
        getDefaultFetchModes() { return { pages: true, days: false, sinceLast: false }; },
        normalizeFetchModes(modes) {
            const merged = { pages: false, days: false, sinceLast: false, ...(modes || {}) };
            const selected = ['pages', 'days', 'sinceLast'].find(key => !!merged[key]) || 'pages';
            return { pages: selected === 'pages', days: selected === 'days', sinceLast: selected === 'sinceLast' };
        },
        getFeedFetchModes() { return this.normalizeFetchModes(GM_getValue('feedFetchModes', this.getDefaultFetchModes())); },
        setFeedFetchModes(modes) { GM_setValue('feedFetchModes', this.normalizeFetchModes(modes)); },
        getFriendsFetchModes() { return this.normalizeFetchModes(GM_getValue('friendsFetchModes', this.getDefaultFetchModes())); },
        setFriendsFetchModes(modes) { GM_setValue('friendsFetchModes', this.normalizeFetchModes(modes)); },
        getFeedMaxDays() { return GM_getValue('feedMaxDays', 3); },
        setFeedMaxDays(days) { GM_setValue('feedMaxDays', days); },
        getFriendsMaxDays() { return GM_getValue('friendsMaxDays', 3); },
        setFriendsMaxDays(days) { GM_setValue('friendsMaxDays', days); },
        getModalCols() { return GM_getValue('modalCols', 3); },
        setModalCols(cols) { GM_setValue('modalCols', cols); },
        getPlaylistGridCols() { return GM_getValue('playlistGridCols', 2); },
        setPlaylistGridCols(cols) { GM_setValue('playlistGridCols', cols); },
        getVideoGridCols() { return GM_getValue('videoGridCols', 2); },
        setVideoGridCols(cols) { GM_setValue('videoGridCols', cols); },
        getUIMode() {
            const mode = GM_getValue('uiMode', 'auto');
            return ['auto', 'desktop', 'mobile'].includes(mode) ? mode : 'auto';
        },
        setUIMode(mode) { GM_setValue('uiMode', ['auto', 'desktop', 'mobile'].includes(mode) ? mode : 'auto'); },
        getTabOrder() { return GM_getValue('tabOrder', DEFAULT_TAB_ORDER); },
        setTabOrder(order) { GM_setValue('tabOrder', order); },
        getTabVisibility() {
            const defaults = {};
            DEFAULT_TAB_ORDER.forEach(k => defaults[k] = true);
            const visibility = { ...defaults, ...GM_getValue('tabVisibility', defaults) };
            // 設定画面への入口を失わないよう、設定タブは常に表示する。
            visibility.settings = true;
            return visibility;
        },
        setTabVisibility(vis) { GM_setValue('tabVisibility', { ...vis, settings: true }); },
        getPanelState(mobile = false) { return GM_getValue(mobile ? 'mobilePanelState' : 'panelState', null); },
        setPanelState(state, mobile = false) { GM_setValue(mobile ? 'mobilePanelState' : 'panelState', state); },
        getBtnPosition(mobile = false) { return GM_getValue(mobile ? 'mobileBtnPosition' : 'btnPosition', null); },
        setBtnPosition(pos, mobile = false) { GM_setValue(mobile ? 'mobileBtnPosition' : 'btnPosition', pos); },
        getTabScroll(tab) {
            const scrolls = GM_getValue('tabScrolls', {});
            return scrolls[tab] || 0;
        },
        setTabScroll(tab, val) {
            const scrolls = GM_getValue('tabScrolls', {});
            scrolls[tab] = val;
            GM_setValue('tabScrolls', scrolls);
        },
        getActivePlaylist() { return GM_getValue('activePlaylist', null); },
        setActivePlaylist(name) { GM_setValue('activePlaylist', name); },

        // スクロールリセット設定用
        getScrollResetValue() { return GM_getValue('scrollResetValue', 5); }, // デフォルト5
        setScrollResetValue(val) { GM_setValue('scrollResetValue', val); },
        getScrollResetUnit() { return GM_getValue('scrollResetUnit', 'minutes'); }, // デフォルトminutes
        setScrollResetUnit(unit) { GM_setValue('scrollResetUnit', unit); },
        getLastClosedTime() { return GM_getValue('lastClosedTime', 0); },
        setLastClosedTime(time) { GM_setValue('lastClosedTime', time); },

        // 検索結果・動画一覧フィルター
        isContentFilterEnabled() { return GM_getValue('contentFilterEnabled', true); },
        setContentFilterEnabled(enabled) { GM_setValue('contentFilterEnabled', !!enabled); },
        getBlockedUploaders() { return GM_getValue('blockedUploaders', []); },
        setBlockedUploaders(names) { GM_setValue('blockedUploaders', names); },
        getMutedTitleTerms() { return GM_getValue('mutedTitleTerms', []); },
        setMutedTitleTerms(terms) { GM_setValue('mutedTitleTerms', terms); },
        getVideoUploaderCache() { return GM_getValue('videoUploaderCache', {}); },
        setVideoUploaderCache(cache) { GM_setValue('videoUploaderCache', cache); },
    };

    // リセット時間を計算するヘルパー
    function getScrollResetMs() {
        const val = StorageManager.getScrollResetValue();
        const unit = StorageManager.getScrollResetUnit();
        let multiplier = 1000; // seconds
        if (unit === 'minutes') multiplier = 60 * 1000;
        if (unit === 'hours') multiplier = 60 * 60 * 1000;
        console.log(`[TokyoMotion Enhancer] Reset time: ${val} ${unit} = ${val * multiplier}ms`); // Debug log
        return val * multiplier;
    }

    // ========================================
    // Privateスキャナー
    // ========================================
    const PrivateScanner = {
        scan() {
            // TokyoMotion applies a WebKit filter to PRIVATE thumbnails.
            // Apply the override directly as well as through GM_addStyle so that it
            // also works in iOS Safari and for thumbnails inserted after page load.
            document.querySelectorAll('.img-private').forEach(thumbnail => {
                thumbnail.style.setProperty('-webkit-filter', 'unset', 'important');
                thumbnail.style.setProperty('filter', 'none', 'important');
            });
            const privateIds = [];
            const cards = document.querySelectorAll('.col-sm-4, .video-card, .thumb-block');
            cards.forEach(card => {
                const isPrivate = card.querySelector('.label-private') ||
                    card.querySelector('.img-private') ||
                    (card.textContent && card.textContent.includes('PRIVATE'));
                if (isPrivate) {
                    const link = card.querySelector('a[href*="/video/"]');
                    if (link) {
                        const match = link.getAttribute('href').match(/\/video\/(\d+)/);
                        if (match && match[1]) privateIds.push(match[1]);
                    }
                }
            });
            if (privateIds.length > 0) StorageManager.addPrivateToCache(privateIds);
        },
        startObserver() {
            this.scan();
            new MutationObserver(() => this.scan()).observe(document.body, { childList: true, subtree: true });
        }
    };

    // ========================================
    // 購読(フォロー)マネージャー
    // ========================================
    const SubscriptionManager = {
        async getMyProfileUrl() {
            const profileLink = document.querySelector('a[href^="/user/"]:not([href*="logout"]):not([href*="login"])');
            if (profileLink) return profileLink.href;
            const userLink = document.querySelector('.username a');
            if (userLink) return userLink.href;
            const avatarLink = document.querySelector('.avatar-container a, .header-avatar a');
            if (avatarLink) return avatarLink.href;
            return null;
        },
        async getSubscriptionsBaseUrl() {
            const profileUrl = await this.getMyProfileUrl();
            if (!profileUrl) return null;
            return profileUrl.split('/').slice(0, 5).join('/') + '/subscriptions';
        },
        async getFriendsBaseUrl() {
            const profileUrl = await this.getMyProfileUrl();
            if (!profileUrl) return null;
            return profileUrl.split('/').slice(0, 5).join('/') + '/friends';
        },
        async fetchDocument(url) {
            try {
                const response = await fetch(url);
                return new DOMParser().parseFromString(await response.text(), 'text/html');
            } catch (e) { return null; }
        },
        async getFollowedUsers(statusCallback) {
            const baseUrl = await this.getSubscriptionsBaseUrl();
            if (!baseUrl) throw new Error('ログインしていないか、プロフィールが見つかりません');
            return this._getUsersFromPages(baseUrl, statusCallback);
        },
        async getFriends(statusCallback) {
            const baseUrl = await this.getFriendsBaseUrl();
            if (!baseUrl) throw new Error('ログインしていないか、プロフィールが見つかりません');
            return this._getUsersFromPages(baseUrl, statusCallback);
        },
        async _getUsersFromPages(baseUrl, statusCallback) {
            const usersMap = new Map();
            let page = 1;
            let hasNextPage = true;
            const myUsernameMatch = baseUrl.match(/\/user\/([^\/]+)/);
            const myUsername = myUsernameMatch ? myUsernameMatch[1] : null;
            while (hasNextPage) {
                const url = page === 1 ? baseUrl : `${baseUrl}?page=${page}`;
                if (statusCallback) statusCallback(`${t('msg_fetching')} (${page} p)`);
                const doc = await this.fetchDocument(url);
                if (!doc) break;
                const userCards = Array.from(doc.querySelectorAll('.thumb-block, .user-card, .col-sm-6, .col-sm-4, .col-xs-6'));
                userCards.forEach(card => {
                    const userLink = card.querySelector('a[href*="/user/"]');
                    if (!userLink) return;
                    const href = userLink.getAttribute('href');
                    if (href.includes('/video/')) return;
                    const userMatch = href.match(/\/user\/([^\/\?#]+)/);
                    if (!userMatch || !userMatch[1]) return;
                    const username = userMatch[1];
                    if (myUsername && username.toLowerCase() === myUsername.toLowerCase()) return;
                    const invalidUsernames = ['edit', 'avatar', 'logout', 'login', 'register', 'settings', 'upload', 'search', 'help', 'contact', 'about', 'terms', 'privacy', 'dmca'];
                    if (invalidUsernames.includes(username.toLowerCase())) return;
                    const normalizedUrl = `https://www.tokyomotion.net/user/${username}`;
                    if (usersMap.has(normalizedUrl)) return;
                    let iconSrc = '';
                    const img = card.querySelector('img');
                    if (img) iconSrc = img.src || img.dataset.src || '';
                    if (!iconSrc) iconSrc = 'https://www.tokyomotion.net/img/user-avatar.png';
                    usersMap.set(normalizedUrl, { url: normalizedUrl, icon: iconSrc });
                });
                const userLinks = Array.from(doc.querySelectorAll('a[href*="/user/"]'));
                userLinks.forEach(a => {
                    const href = a.getAttribute('href');
                    if (!href || href.includes('/video/') || href.includes('/subscriptions') || href.includes('/friends') || href.includes('/favorites')) return;
                    const userMatch = href.match(/\/user\/([^\/\?#]+)/);
                    if (!userMatch || !userMatch[1]) return;
                    const username = userMatch[1];
                    if (myUsername && username.toLowerCase() === myUsername.toLowerCase()) return;
                    const invalidUsernames = ['edit', 'avatar', 'logout', 'login', 'register', 'settings', 'upload', 'search', 'help', 'contact', 'about', 'terms', 'privacy', 'dmca'];
                    if (invalidUsernames.includes(username.toLowerCase())) return;
                    const normalizedUrl = `https://www.tokyomotion.net/user/${username}`;
                    if (usersMap.has(normalizedUrl)) return;
                    let iconSrc = '';
                    const img = a.querySelector('img') || (a.parentElement ? a.parentElement.querySelector('img') : null);
                    if (img) iconSrc = img.src || img.dataset.src || '';
                    if (!iconSrc) iconSrc = 'https://www.tokyomotion.net/img/user-avatar.png';
                    usersMap.set(normalizedUrl, { url: normalizedUrl, icon: iconSrc });
                });
                const paginationLinks = Array.from(doc.querySelectorAll('.pagination a'));
                const hasNext = paginationLinks.some(a => a.href.includes(`page=${page + 1}`));
                if (hasNext) page++; else hasNextPage = false;
                await new Promise(r => setTimeout(r, 500));
            }
            return Array.from(usersMap.values());
        },
        async getUserVideos(userData, fetchOptions = {}) {
            if (typeof fetchOptions === 'number') fetchOptions = { mode: 'pages', maxPages: fetchOptions };
            const mode = fetchOptions.mode || 'pages';
            const maxPages = mode === 'pages' ? Math.max(1, parseInt(fetchOptions.maxPages || 1, 10)) : 99999;
            const cutoffTimestamp = Number(fetchOptions.cutoffTimestamp || 0);
            const baseTime = fetchOptions.baseTime || Date.now();
            const userUrl = userData.url;
            const userIcon = userData.icon;
            const videosBaseUrl = userUrl.replace(/\/$/, '') + '/videos';
            const allVideos = [];
            let page = 1;
            let hasNextPage = true;
            while (hasNextPage && page <= maxPages) {
                const targetUrl = page === 1 ? videosBaseUrl : `${videosBaseUrl}?page=${page}`;
                try {
                    const doc = await this.fetchDocument(targetUrl);
                    if (!doc) break;
                    const videoLinks = Array.from(doc.querySelectorAll('a[href*="/video/"]'));
                    let foundInPage = 0;
                    let reachedCutoffInPage = false;
                    for (const link of videoLinks) {
                        try {
                            const img = link.querySelector('img') || (link.parentElement ? link.parentElement.querySelector('img') : null);
                            if (!img) continue;
                            const container = link.closest('.col-sm-4') || link.closest('.col-xs-6') || link.closest('.video-card') || link.closest('.thumb-block') || link.parentElement;
                            let title = '', duration = '', dateStr = '';
                            let isPrivate = false;
                            if (container) {
                                const titleEl = container.querySelector('.video-card-title, .title, .video-title, h4, h5');
                                if (titleEl) title = titleEl.innerText.trim();
                                const durationEl = container.querySelector('.duration');
                                if (durationEl) duration = durationEl.innerText.trim();
                                const dateEl = container.querySelector('.video-added');
                                if (dateEl) dateStr = dateEl.innerText.trim();
                                if (container.querySelector('.label-private') || container.querySelector('.img-private')) isPrivate = true;
                                const overlay = container.querySelector('.thumb-overlay');
                                if (!isPrivate && overlay && overlay.textContent.toUpperCase().includes('PRIVATE')) isPrivate = true;
                            }
                            if (!title && img.alt) title = img.alt.trim();
                            if (!duration) {
                                const insideDuration = link.querySelector('.duration');
                                if (insideDuration) duration = insideDuration.innerText.trim();
                            }
                            if (!title) title = 'Untitled';
                            const href = link.getAttribute('href');
                            const fullUrl = href.startsWith('http') ? href : (new URL(href, userUrl).href);
                            const idMatch = fullUrl.match(/\/video\/(\d+)/);
                            if (!idMatch) continue;
                            if (allVideos.some(v => v.id === idMatch[1])) continue;
                            foundInPage++;
                            const postedAt = parseTokyoMotionPostedAt(dateStr, baseTime);
                            if ((mode === 'days' || mode === 'sinceLast') && cutoffTimestamp > 0 && postedAt !== null && postedAt < cutoffTimestamp) {
                                reachedCutoffInPage = true;
                                break;
                            }
                            if (isPrivate) StorageManager.addPrivateToCache([idMatch[1]]);
                            allVideos.push({
                                id: idMatch[1],
                                title: title,
                                thumbnail: img.src || img.dataset.src,
                                url: fullUrl,
                                author: userUrl.split('/').pop(),
                                authorIcon: userIcon,
                                duration: duration,
                                date: dateStr,
                                uploadedAt: postedAt,
                                isPrivate: isPrivate,
                                timestamp: Date.now()
                            });
                        } catch (e) { }
                    }
                    const paginationLinks = Array.from(doc.querySelectorAll('.pagination a'));
                    const hasNext = paginationLinks.some(a => a.href.includes(`page=${page + 1}`));
                    if (reachedCutoffInPage) hasNextPage = false;
                    else if (hasNext && foundInPage > 0) page++;
                    else hasNextPage = false;
                    await new Promise(r => setTimeout(r, 500));
                } catch (err) { break; }
            }
            return allVideos;
        }
    };

    function formatRelativeTime(rawTime) {
        if (!rawTime) return '';
        let cleaned = rawTime.replace(/\s+/g, '').trim();
        if (cleaned.match(/^\d+時前$/)) cleaned = cleaned.replace('時前', '時間前');
        cleaned = cleaned.replace(/時\s*前/g, '時間前').replace(/分\s*前/g, '分前').replace(/日\s*前/g, '日前').replace(/週\s*前/g, '週間前').replace(/月\s*前/g, 'ヶ月前').replace(/年\s*前/g, '年前');
        return cleaned;
    }

    function calcTimeAgo(timestamp) {
        if (!timestamp) return '-';
        const diff = Date.now() - timestamp;
        const minute = 60 * 1000;
        const hour = 60 * minute;
        const day = 24 * hour;
        if (diff < minute) return t('time_just_now');
        if (diff < hour) return Math.floor(diff / minute) + t('time_min_ago');
        if (diff < day) return Math.floor(diff / hour) + t('time_hour_ago');
        if (diff < day * 30) return Math.floor(diff / day) + t('time_day_ago');
        return t('time_long_ago');
    }

    function parseTokyoMotionAgeMs(rawTime) {
        if (!rawTime) return null;
        const text = String(rawTime)
            .normalize('NFKC')
            .replace(/\s+/g, ' ')
            .trim()
            .toLowerCase();
        if (!text) return null;
        if (/たった今|今|just now|moments? ago/.test(text)) return 0;
        if (!/前|ago/.test(text)) return null;

        const numberMatch = text.match(/(\d+(?:\.\d+)?)/);
        if (!numberMatch) return null;
        const value = parseFloat(numberMatch[1]);
        if (!Number.isFinite(value)) return null;

        const minute = 60 * 1000;
        const hour = 60 * minute;
        const day = 24 * hour;

        if (/秒|sec(?:ond)?s?\b/.test(text)) return value * 1000;
        if (/分|mins?\b|minutes?\b/.test(text)) return value * minute;
        if (/時|時間|hours?\b|\bhrs?\b/.test(text)) return value * hour;
        if (/日|days?\b/.test(text)) return value * day;
        if (/週|週間|weeks?\b/.test(text)) return value * 7 * day;
        if (/ヶ月|か月|カ月|月|months?\b/.test(text)) return value * 30 * day;
        if (/年|years?\b/.test(text)) return value * 365 * day;
        return null;
    }

    function parseTokyoMotionPostedAt(rawTime, baseTime = Date.now()) {
        const ageMs = parseTokyoMotionAgeMs(rawTime);
        if (ageMs !== null) return baseTime - ageMs;

        if (!rawTime) return null;
        const normalized = String(rawTime).normalize('NFKC').trim();
        const absoluteMatch = normalized.match(/(\d{4})[\/\-.年](\d{1,2})[\/\-.月](\d{1,2})/);
        if (absoluteMatch) {
            const year = parseInt(absoluteMatch[1], 10);
            const month = parseInt(absoluteMatch[2], 10) - 1;
            const day = parseInt(absoluteMatch[3], 10);
            const date = new Date(year, month, day);
            if (!Number.isNaN(date.getTime())) return date.getTime();
        }
        return null;
    }

    // ========================================
    // スタイル注入
    // ========================================
    GM_addStyle(`
        /* PRIVATEサムネイルの暗さを解除(iOS Safariでは接頭辞付き指定が必要) */
        .img-private, .img-private img {
            -webkit-filter: unset !important;
            filter: none !important;
        }
        .tm-panel, .tm-panel *, .tm-modal-overlay, .tm-modal-overlay * { box-sizing: border-box; }
        .tm-panel {
            position: fixed; width: 500px; height: 600px; min-width: 300px; min-height: 250px;
            background: #1a1a1a; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
            z-index: 999999; display: none; flex-direction: column; font-family: 'Segoe UI', sans-serif;
            bottom: 90px; right: 20px;
        }
        .tm-panel.active { display: flex; }
        .tm-panel-header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 20px;
            font-weight: 600; display: flex; justify-content: space-between; align-items: center;
            cursor: move; user-select: none; touch-action: none; border-radius: 12px 12px 0 0;
        }
        .tm-panel-close {
            -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center;
            background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px;
            flex: 0 0 28px; padding: 0; border-radius: 50%; line-height: 1; text-align: center; cursor: pointer;
        }
        .tm-toggle-btn {
            position: fixed; width: 56px; height: 56px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center;
            border: none; padding: 0; border-radius: 50%; color: white; line-height: 1; text-align: center; cursor: pointer;
            box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); z-index: 999998; user-select: none; touch-action: none;
            bottom: 20px; right: 20px;
        }
        .tm-toggle-icon, .tm-close-icon { display: block; flex: none; pointer-events: none; }
        .tm-toggle-icon { width: 27px; height: 27px; }
        .tm-close-icon { width: 16px; height: 16px; }
        .tm-toggle-btn:hover { transform: scale(1.1); }
        .tm-toggle-btn:active { transform: scale(0.95); }
        .tm-tabs { display: flex; background: #252525; border-bottom: 2px solid #333; overflow-x: auto; }
        .tm-tab {
            flex: 1; padding: 12px 8px; background: transparent; border: none; color: #aaa; cursor: pointer; font-size: 12px;
            border-bottom: 3px solid transparent; min-width: 60px; text-align: center; white-space: nowrap; user-select: none;
        }
        .tm-tab.active { background: #1a1a1a; border-bottom-color: #667eea; color: #fff; font-weight: 600; }
        .tm-tab:hover { background: #333; }
        .tm-tab.dragging { opacity: 0.5; background: #444; }
        .tm-content { flex: 1; overflow-y: auto; padding: 10px; background: #111; min-height: 0; }
        .tm-tab-content { display: none; height: 100%; }
        .tm-tab-content.active { display: block; }
        #tm-playlists.tm-tab-content { height: auto; min-height: 100%; }
        .tm-playlist-detail-toolbar {
            position: sticky; top: -10px; z-index: 40;
            margin: -5px -5px 10px; padding: 5px 5px 8px;
            background: linear-gradient(to bottom, #111 78%, rgba(17,17,17,0.92) 100%);
        }
        .tm-playlist-detail-toolbar #tm-back-to-playlists {
            width: 100%; margin: 0; box-shadow: 0 3px 10px rgba(0,0,0,0.35);
        }
        .tm-feed-header { display: flex; justify-content: center; align-items: center; gap: 15px; padding: 10px; margin-bottom: 5px; }
        .tm-time-label { font-size: 11px; color: #888; width: 90px; text-align: center; white-space: nowrap; }
        .tm-grid-view { display: grid; grid-template-columns: repeat(var(--tm-video-cols, 2), 1fr); gap: 15px; padding: 5px; }
        .tm-card { display: flex; flex-direction: column; background: transparent; cursor: pointer; border: none; position: relative; transition: opacity 0.2s; }
        .tm-card:hover { opacity: 0.9; }
        .tm-card-thumb-box { position: relative; width: 100%; aspect-ratio: 16/9; background: #000; border-radius: 4px; overflow: hidden; margin-bottom: 6px; }
        .tm-card-thumb { width: 100%; height: 100%; object-fit: cover; }
        .tm-card-duration { position: absolute; bottom: 5px; right: 5px; background: rgba(0,0,0,0.7); color: #fff; padding: 2px 5px; font-size: 11px; border-radius: 3px; line-height: 1; }
        .tm-card-private { position: absolute; bottom: 5px; left: 5px; background: #cc0000; color: #fff; padding: 2px 5px; font-size: 10px; border-radius: 3px; line-height: 1; font-weight: bold; }
        .tm-card-title {
            color: #ff5e5e; font-size: 13px; font-weight: 500; line-height: 1.3; max-height: 2.6em; overflow: hidden; margin-bottom: 5px;
            display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
        }
        .tm-card:hover .tm-card-title { text-decoration: underline; }
        .tm-card-meta { font-size: 11px; color: #888; line-height: 1.3; }
        .tm-user-row { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; flex-wrap: wrap; }
        .tm-user-icon { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; background: #333; flex-shrink: 0; }
        .tm-user-link { color: #aaa; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px; }
        .tm-user-link:hover { color: #fff; text-decoration: underline; }
        .tm-relative-time { color: #888; font-size: 10px; margin-left: 6px; white-space: nowrap; }
        .tm-input { width: 100%; padding: 10px; background: #2d2d2d; border: 2px solid #444; border-radius: 6px; color: #e0e0e0; font-size: 13px; margin-bottom: 10px; }
        .tm-btn-row { display: flex; gap: 8px; }
        .tm-btn-primary, .tm-btn-secondary { flex: 1; padding: 10px; border: none; border-radius: 6px; cursor: pointer; color: white; }
        .tm-btn-primary { background: #667eea; }
        .tm-btn-secondary { background: #555; }
        .tm-btn-danger { width: 100%; padding: 10px; background: #e74c3c; color: white; border: none; border-radius: 6px; cursor: pointer; margin-top: 10px; }
        .tm-empty { text-align: center; color: #666; padding: 30px; }
        .tm-toast {
            position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%) translateY(20px);
            background: #333; color: #fff; padding: 12px 24px; border-radius: 8px;
            opacity: 0; transition: all 0.3s; z-index: 9999999;
        }
        .tm-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
        .tm-card-remove {
            position: absolute; top: 5px; right: 5px; background: rgba(231, 76, 60, 0.8); color: white;
            border: none; width: 20px; height: 20px; border-radius: 50%; cursor: pointer; display: none; z-index: 10; font-size: 12px;
            align-items: center; justify-content: center;
        }
        .tm-card:hover .tm-card-remove { display: flex; }
        .tm-playlist-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
        .tm-playlist-card { padding: 15px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 8px; cursor: pointer; position: relative; transition: all 0.3s; }
        .tm-playlist-name { font-weight: bold; margin-bottom: 5px; }
        .tm-playlist-count { font-size: 12px; opacity: 0.9; }
        .tm-playlist-delete { position: absolute; top: 5px; right: 5px; background: rgba(0,0,0,0.3); border: none; color: white; width: 22px; height: 22px; border-radius: 50%; cursor: pointer; }
        .tm-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 9999990; display: flex; align-items: center; justify-content: center; }
        .tm-modal-content { background: #1a1a1a; border-radius: 12px; width: auto; min-width: 320px; max-width: 90vw; transition: width 0.3s; }
        .tm-modal-header { background: #667eea; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center; gap: 10px; border-radius: 12px 12px 0 0; }
        .tm-playlist-list { padding: 10px; display: grid; gap: 8px; max-height: 350px; overflow-y: auto; }
        .tm-playlist-item { display: flex; gap: 5px; padding: 8px; background: #2d2d2d; border-radius: 4px; color: #fff; cursor: pointer; }
        .tm-modal-footer { padding: 15px; }
        .tm-new-playlist-form { display: flex; gap: 5px; }
        .tm-playlist-btn-inline { margin-left: 5px !important; cursor: pointer !important; }
        .tm-toggle-switch { width: 44px; height: 24px; background: #555; border-radius: 12px; cursor: pointer; position: relative; }
        .tm-toggle-switch.active { background: #667eea; }
        .tm-toggle-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: 0.3s; }
        .tm-toggle-switch.active::after { transform: translateX(20px); }
        .tm-col-select { background: rgba(0,0,0,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; padding: 2px 5px; font-size: 12px; cursor: pointer; outline: none; }
        .tm-col-select option { background: #333; color: white; }
        .tm-tab-visibility-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #333; }
        .tm-tab-visibility-item:last-child { border-bottom: none; }
        .tm-fetch-range-panel {
            background: #1c1c1c; border: 1px solid #333; border-radius: 8px; padding: 10px; margin-bottom: 10px;
        }
        .tm-fetch-range-title { color: #e0e0e0; font-size: 13px; font-weight: 600; margin-bottom: 8px; }
        .tm-fetch-mode-row {
            display: grid; grid-template-columns: auto 1fr minmax(76px, 120px); align-items: center; gap: 8px;
            color: #ddd; font-size: 12px; margin: 7px 0;
        }
        .tm-fetch-mode-row input[type="checkbox"], .tm-fetch-mode-row input[type="radio"] { width: 16px; height: 16px; accent-color: #667eea; }
        .tm-fetch-mode-row .tm-input { margin-bottom: 0; padding: 6px 8px; font-size: 12px; }
        .tm-fetch-mode-status { min-height: 16px; margin-top: 6px; color: #f1c40f; font-size: 11px; line-height: 1.35; }
        .tm-fetch-mode-status.ok { color: #2ecc71; }
        .tm-fetch-mode-status.error { color: #ff6b6b; }
        .tm-resizer { position: absolute; z-index: 100; touch-action: none; }
        .tm-resizer.n { top: -5px; left: 0; right: 0; height: 10px; cursor: ns-resize; }
        .tm-resizer.s { bottom: -5px; left: 0; right: 0; height: 10px; cursor: ns-resize; }
        .tm-resizer.e { right: -5px; top: 0; bottom: 0; width: 10px; cursor: ew-resize; }
        .tm-resizer.w { left: -5px; top: 0; bottom: 0; width: 10px; cursor: ew-resize; }
        .tm-resizer.ne { top: -5px; right: -5px; width: 15px; height: 15px; cursor: nesw-resize; }
        .tm-resizer.nw { top: -5px; left: -5px; width: 15px; height: 15px; cursor: nwse-resize; }
        .tm-resizer.se { bottom: -5px; right: -5px; width: 15px; height: 15px; cursor: nwse-resize; }
        .tm-resizer.sw { bottom: -5px; left: -5px; width: 15px; height: 15px; cursor: nesw-resize; }
        body.tm-dragging { user-select: none; }
        .tm-btn-icon { background: transparent; border: none; color: #aaa; cursor: pointer; font-size: 14px; margin-left: 8px; padding: 2px; transition: color 0.2s; }
        .tm-btn-icon:hover { color: #fff; }
        .tm-added-time { color: #888; font-size: 10px; margin-top: 3px; }
        .tm-filter-textarea {
            min-height: 82px; resize: vertical; line-height: 1.45; font-family: inherit;
        }
        .tm-filter-status {
            position: fixed; left: 14px; bottom: 14px; z-index: 999997; display: none;
            align-items: center; gap: 8px; padding: 7px 10px; border-radius: 999px;
            background: rgba(20, 20, 20, 0.92); color: #fff; box-shadow: 0 3px 12px rgba(0,0,0,0.35);
            font: 12px/1.2 'Segoe UI', sans-serif;
        }
        .tm-filter-status.active { display: flex; }
        .tm-filter-status button {
            border: 0; border-radius: 999px; padding: 4px 8px; background: #667eea; color: #fff; cursor: pointer;
        }
        .tm-filter-quick-block {
            display: inline-flex; align-items: center; justify-content: center; margin-left: 4px; padding: 1px 4px;
            border: 1px solid rgba(180, 40, 40, 0.55); border-radius: 4px; background: rgba(40, 40, 40, 0.85);
            color: #d66; cursor: pointer; font-size: 11px; line-height: 1.2; vertical-align: middle;
        }
        .tm-filter-quick-block:hover { background: #8b1f1f; color: #fff; }
        .tm-filter-resolved-row {
            display: flex; align-items: center; gap: 4px; margin-top: 5px; color: #888; font-size: 11px;
            overflow: hidden; white-space: nowrap;
        }
        .tm-filter-resolved-row span { overflow: hidden; text-overflow: ellipsis; }
        .tm-filter-hidden { display: none !important; }
        .tm-card-thumb-box[draggable="true"] { cursor: grab; }
        .tm-card-thumb-box[draggable="true"]:active { cursor: grabbing; }
        .tm-card-thumb-box.tm-thumbnail-dragging { opacity: 0.6; outline: 3px solid #8fa2ff; }
        .tm-thumbnail-drop-zone {
            position: fixed; top: 18px; left: 50%; transform: translate(-50%, -18px);
            z-index: 10000000; display: flex; align-items: center; justify-content: center;
            min-width: 300px; max-width: calc(100vw - 30px); padding: 16px 24px;
            border: 2px dashed rgba(255,255,255,0.8); border-radius: 12px;
            background: linear-gradient(135deg, rgba(102,126,234,0.96), rgba(118,75,162,0.96));
            color: #fff; font: 600 14px/1.3 'Segoe UI', sans-serif; text-align: center;
            box-shadow: 0 8px 28px rgba(0,0,0,0.45); opacity: 0; pointer-events: none;
            transition: opacity 0.15s, transform 0.15s;
        }
        body.tm-thumbnail-drag-active .tm-thumbnail-drop-zone {
            opacity: 1; pointer-events: auto; transform: translate(-50%, 0);
        }
        body.tm-thumbnail-drag-active .tm-thumbnail-drop-zone.tm-drag-over {
            background: linear-gradient(135deg, #27ae60, #1e8449); transform: translate(-50%, 0) scale(1.04);
        }

        /* スマホ向け表示。保存済みサイズは維持しつつ、必ず表示領域内に収める。 */
        .tm-panel.tm-mobile-mode {
            width: calc(100vw - 16px); height: calc(100vh - 16px); height: calc(100dvh - 16px);
            min-width: 0; min-height: 0; max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); max-height: calc(100dvh - 16px);
            left: 8px; top: 8px; right: auto; bottom: auto; border-radius: 10px;
            overscroll-behavior: contain;
        }
        .tm-panel.tm-mobile-mode .tm-panel-header { min-height: 48px; padding: 7px 9px 7px 12px; border-radius: 10px 10px 0 0; }
        .tm-panel.tm-mobile-mode .tm-panel-close { width: 38px; height: 38px; flex: 0 0 38px; }
        .tm-panel.tm-mobile-mode .tm-close-icon { width: 20px; height: 20px; }
        .tm-panel.tm-mobile-mode .tm-tabs {
            flex: 0 0 auto; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; scrollbar-width: none;
        }
        .tm-panel.tm-mobile-mode .tm-tabs::-webkit-scrollbar { display: none; }
        .tm-panel.tm-mobile-mode .tm-tab {
            flex: 1 1 0; min-width: 50px; min-height: 44px; padding: 10px 3px; font-size: 11px; touch-action: manipulation;
        }
        .tm-panel.tm-mobile-mode .tm-content { padding: 8px; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
        .tm-panel.tm-mobile-mode .tm-grid-view { gap: 10px; padding: 3px; }
        .tm-panel.tm-mobile-mode .tm-card-remove { display: flex; width: 28px; height: 28px; font-size: 16px; }
        .tm-panel.tm-mobile-mode .tm-btn-primary,
        .tm-panel.tm-mobile-mode .tm-btn-secondary,
        .tm-panel.tm-mobile-mode .tm-btn-danger { min-height: 42px; touch-action: manipulation; }
        .tm-panel.tm-mobile-mode .tm-toggle-switch { flex: 0 0 44px; }
        .tm-panel.tm-mobile-mode .tm-fetch-mode-row { grid-template-columns: auto minmax(0, 1fr) minmax(72px, 92px); }
        .tm-panel.tm-mobile-mode .tm-resizer.n,
        .tm-panel.tm-mobile-mode .tm-resizer.s { height: 22px; }
        .tm-panel.tm-mobile-mode .tm-resizer.n { top: -8px; }
        .tm-panel.tm-mobile-mode .tm-resizer.s { bottom: -8px; }
        .tm-panel.tm-mobile-mode .tm-resizer.e,
        .tm-panel.tm-mobile-mode .tm-resizer.w { width: 22px; }
        .tm-panel.tm-mobile-mode .tm-resizer.e { right: -8px; }
        .tm-panel.tm-mobile-mode .tm-resizer.w { left: -8px; }
        .tm-panel.tm-mobile-mode .tm-resizer.ne,
        .tm-panel.tm-mobile-mode .tm-resizer.nw,
        .tm-panel.tm-mobile-mode .tm-resizer.se,
        .tm-panel.tm-mobile-mode .tm-resizer.sw { width: 34px; height: 34px; }
        .tm-panel.tm-mobile-mode .tm-resizer.ne { top: -8px; right: -8px; }
        .tm-panel.tm-mobile-mode .tm-resizer.nw { top: -8px; left: -8px; }
        .tm-panel.tm-mobile-mode .tm-resizer.se { bottom: -8px; right: -8px; }
        .tm-panel.tm-mobile-mode .tm-resizer.sw { bottom: -8px; left: -8px; }
        .tm-panel.tm-mobile-mode .tm-resizer.se::after {
            content: ''; position: absolute; right: 9px; bottom: 9px; width: 12px; height: 12px;
            border-right: 3px solid rgba(255,255,255,0.7); border-bottom: 3px solid rgba(255,255,255,0.7); border-radius: 1px;
        }
        .tm-toggle-btn.tm-mobile-mode {
            width: 52px; height: 52px; right: calc(12px + env(safe-area-inset-right));
            bottom: calc(12px + env(safe-area-inset-bottom));
        }
        .tm-toggle-btn.tm-mobile-mode .tm-toggle-icon { width: 25px; height: 25px; }
        html.tm-enhancer-mobile .tm-modal-overlay { padding: 8px; }
        html.tm-enhancer-mobile .tm-modal-content {
            width: calc(100vw - 16px) !important; min-width: 0; max-width: calc(100vw - 16px);
            max-height: calc(100vh - 16px); max-height: calc(100dvh - 16px); overflow: hidden;
        }
        html.tm-enhancer-mobile .tm-playlist-list {
            grid-template-columns: 1fr !important; max-height: calc(100vh - 170px); max-height: calc(100dvh - 170px);
            -webkit-overflow-scrolling: touch;
        }
        html.tm-enhancer-mobile .tm-modal-header { padding: 9px 10px; }
        html.tm-enhancer-mobile .tm-new-playlist-form { flex-wrap: wrap; }
        html.tm-enhancer-mobile .tm-new-playlist-form .tm-input { min-width: 0; flex: 1 1 180px; }
    `);

    // ========================================
    // ユーティリティ
    // ========================================
    function formatDate(timestamp) {
        if (!timestamp) return '';
        const d = new Date(timestamp);
        const pad = (n) => String(n).padStart(2, '0');
        return `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
    }

    function formatSeconds(seconds) {
        if (!seconds || isNaN(seconds)) return null;
        seconds = Math.floor(seconds);
        const h = Math.floor(seconds / 3600);
        const m = Math.floor((seconds % 3600) / 60);
        const s = seconds % 60;
        const pad = n => n.toString().padStart(2, '0');
        if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
        return `${m}:${pad(s)}`;
    }

    function showToast(message) {
        const existing = document.querySelector('.tm-toast');
        if (existing) existing.remove();
        const toast = document.createElement('div');
        toast.className = 'tm-toast';
        toast.textContent = message;
        document.body.appendChild(toast);
        requestAnimationFrame(() => toast.classList.add('show'));
        setTimeout(() => {
            toast.classList.remove('show');
            setTimeout(() => toast.remove(), 3000); // 通知時間
        }, 3000);
    }

    // ========================================
    // 投稿者ブロック・タイトルミュート
    // ========================================
    const ContentFilter = {
        observer: null,
        scanTimer: null,
        revealTemporarily: false,
        uploaderCache: null,
        uploaderQueue: [],
        pendingVideoIds: new Set(),
        activeUploaderFetches: 0,
        maxConcurrentFetches: 5,

        normalize(value) {
            return String(value || '').normalize('NFKC').trim().toLocaleLowerCase();
        },

        parseLines(value) {
            const seen = new Set();
            return String(value || '').split(/\r?\n/).map(v => v.trim()).filter(v => {
                const normalized = this.normalize(v);
                if (!normalized || seen.has(normalized)) return false;
                seen.add(normalized);
                return true;
            });
        },

        getVideoCards() {
            const cards = new Set();
            const cardSelector = [
                '.thumb-block', '.video-card', '.video-item', '.video-box',
                '.col-sm-4', '.col-sm-3', '.col-md-4', '.col-md-3', '.col-lg-3', '.col-xs-6',
                'article'
            ].join(',');
            document.querySelectorAll('a[href*="/video/"]').forEach(link => {
                if (link.closest('.tm-panel, .tm-modal-overlay, .tm-filter-status')) return;
                let card = link.closest(cardSelector);
                if (!card) card = link.parentElement;
                if (card && !card.classList.contains('tm-card')) cards.add(card);
            });
            return [...cards];
        },

        getVideoReference(card) {
            const link = card.querySelector('a[href*="/video/"]');
            if (!link) return null;
            const href = link.getAttribute('href') || '';
            const match = href.match(/\/video\/(\d+)/i);
            if (!match) return null;
            return { id: match[1], url: new URL(href, location.origin).href };
        },

        getTitle(card) {
            const titleEl = card.querySelector('.video-card-title, .video-title, .thumb-title, .title, h3, h4, h5');
            const videoLink = card.querySelector('a[href*="/video/"]');
            const image = videoLink ? videoLink.querySelector('img') : card.querySelector('img');
            const candidates = [
                titleEl && (titleEl.getAttribute('title') || titleEl.textContent),
                videoLink && videoLink.getAttribute('title'),
                image && image.getAttribute('alt'),
                videoLink && videoLink.textContent
            ];
            return String(candidates.find(v => v && v.trim()) || '').trim();
        },

        getUploader(card) {
            if (card.dataset.tmResolvedUploader) return card.dataset.tmResolvedUploader;
            const userLink = card.querySelector('a[href*="/user/"]');
            if (userLink) {
                const match = (userLink.getAttribute('href') || '').match(/\/user\/([^/?#]+)/i);
                if (match && match[1]) {
                    try { return decodeURIComponent(match[1]).trim(); } catch (e) { return match[1].trim(); }
                }
                if (userLink.textContent.trim()) return userLink.textContent.trim();
            }
            const dataOwner = card.getAttribute('data-username') || card.getAttribute('data-user') || card.dataset.username || card.dataset.user;
            if (dataOwner) return String(dataOwner).trim();
            const ownerEl = card.querySelector('.username, .user-name, .video-user, .video-author, .uploader, .author, .byline');
            if (ownerEl) return ownerEl.textContent.replace(/^\s*(?:by|投稿者)\s*[::]?\s*/i, '').trim();
            const video = this.getVideoReference(card);
            if (!video) return '';
            if (!this.uploaderCache) this.uploaderCache = StorageManager.getVideoUploaderCache();
            const cached = this.uploaderCache[video.id];
            const cachedName = typeof cached === 'string' ? cached : cached && cached.name;
            if (cachedName) {
                card.dataset.tmResolvedUploader = cachedName;
                return cachedName;
            }
            return '';
        },

        queueUploaderResolution(card) {
            const video = this.getVideoReference(card);
            if (!video || this.pendingVideoIds.has(video.id)) return;
            if (!this.uploaderCache) this.uploaderCache = StorageManager.getVideoUploaderCache();
            if (Object.prototype.hasOwnProperty.call(this.uploaderCache, video.id)) {
                const cached = this.uploaderCache[video.id];
                const cachedName = typeof cached === 'string' ? cached : cached && cached.name;
                const cachedAt = typeof cached === 'object' && cached ? cached.cachedAt || 0 : 0;
                if (cachedName || Date.now() - cachedAt < 6 * 60 * 60 * 1000) return;
                delete this.uploaderCache[video.id];
            }
            this.pendingVideoIds.add(video.id);
            this.uploaderQueue.push(video);
            this.pumpUploaderQueue();
        },

        pumpUploaderQueue() {
            if (!StorageManager.isContentFilterEnabled()) {
                this.uploaderQueue.forEach(video => this.pendingVideoIds.delete(video.id));
                this.uploaderQueue = [];
                return;
            }
            while (this.activeUploaderFetches < this.maxConcurrentFetches && this.uploaderQueue.length > 0) {
                const video = this.uploaderQueue.shift();
                this.activeUploaderFetches++;
                this.fetchUploader(video).finally(() => {
                    this.activeUploaderFetches--;
                    this.pendingVideoIds.delete(video.id);
                    this.pumpUploaderQueue();
                    this.scheduleScan();
                });
            }
        },

        async fetchUploader(video) {
            let name = '';
            try {
                const response = await fetch(video.url, { credentials: 'same-origin' });
                if (!response.ok) throw new Error(`HTTP ${response.status}`);
                const doc = new DOMParser().parseFromString(await response.text(), 'text/html');
                const link = doc.querySelector([
                    '.user-container a[href*="/user/"]',
                    '.video-info a[href*="/user/"]',
                    '.user-info a[href*="/user/"]',
                    'a.username[href*="/user/"]'
                ].join(','));
                if (link) {
                    const match = (link.getAttribute('href') || '').match(/\/user\/([^/?#]+)/i);
                    if (match && match[1]) {
                        try { name = decodeURIComponent(match[1]).trim(); } catch (e) { name = match[1].trim(); }
                    } else name = link.textContent.trim();
                }
            } catch (error) {
                console.warn(`[TM Enhancer] Could not resolve uploader for video ${video.id}`, error);
            }
            this.uploaderCache[video.id] = { name, cachedAt: Date.now() };
            const entries = Object.entries(this.uploaderCache);
            if (entries.length > 1000) {
                entries.sort((a, b) => (a[1].cachedAt || 0) - (b[1].cachedAt || 0));
                entries.slice(0, entries.length - 1000).forEach(([id]) => delete this.uploaderCache[id]);
            }
            StorageManager.setVideoUploaderCache(this.uploaderCache);
            if (name) {
                document.querySelectorAll(`a[href*="/video/${video.id}"]`).forEach(link => {
                    const card = link.closest('.thumb-block, .video-card, .video-item, .video-box, .col-sm-4, .col-sm-3, .col-md-4, .col-md-3, .col-lg-3, .col-xs-6, article') || link.parentElement;
                    if (card) card.dataset.tmResolvedUploader = name;
                });
            }
        },

        getMatch(card, blockedUploaders, mutedTerms) {
            const uploader = this.getUploader(card);
            const normalizedUploader = this.normalize(uploader).replace(/^@/, '');
            const uploaderMatched = normalizedUploader && blockedUploaders.has(normalizedUploader);
            const title = this.getTitle(card);
            const normalizedTitle = this.normalize(title);
            const termMatched = normalizedTitle && mutedTerms.find(term => normalizedTitle.includes(term));
            if (uploaderMatched) return { type: 'uploader', value: uploader };
            if (termMatched) return { type: 'title', value: termMatched };
            return null;
        },

        injectQuickBlock(card, uploader) {
            if (!uploader || card.querySelector('.tm-filter-quick-block')) return;
            let userLink = card.querySelector('a[href*="/user/"]') || card.querySelector('.username, .user-name, .video-user, .video-author, .uploader, .author, .tm-filter-resolved-row span');
            if (!userLink || !userLink.parentNode) {
                const row = document.createElement('div');
                row.className = 'tm-filter-resolved-row';
                userLink = document.createElement('span');
                userLink.textContent = `@${uploader}`;
                row.appendChild(userLink);
                (card.querySelector('.well') || card).appendChild(row);
            }
            const button = document.createElement('button');
            button.type = 'button';
            button.className = 'tm-filter-quick-block';
            button.textContent = '🚫';
            button.title = t('filter_block_uploader');
            button.setAttribute('aria-label', t('filter_block_uploader'));
            button.addEventListener('click', (event) => {
                event.preventDefault();
                event.stopPropagation();
                this.blockUploader(uploader);
            });
            userLink.insertAdjacentElement('afterend', button);
        },

        blockUploader(uploader) {
            const current = StorageManager.getBlockedUploaders();
            const normalized = this.normalize(uploader).replace(/^@/, '');
            if (!normalized) return;
            if (!current.some(name => this.normalize(name).replace(/^@/, '') === normalized)) current.push(uploader);
            StorageManager.setBlockedUploaders(current);
            StorageManager.setContentFilterEnabled(true);
            this.revealTemporarily = false;
            this.refreshSettingsFields();
            this.scan();
            showToast(t('filter_blocked_uploader', { name: uploader }));
        },

        refreshSettingsFields() {
            const uploaderInput = document.getElementById('tm-filter-uploaders');
            const wordInput = document.getElementById('tm-filter-words');
            const toggle = document.getElementById('tm-content-filter-toggle');
            if (uploaderInput) uploaderInput.value = StorageManager.getBlockedUploaders().join('\n');
            if (wordInput) wordInput.value = StorageManager.getMutedTitleTerms().join('\n');
            if (toggle) toggle.classList.toggle('active', StorageManager.isContentFilterEnabled());
        },

        updateStatus(matchedCount) {
            let status = document.querySelector('.tm-filter-status');
            if (!status) {
                status = document.createElement('div');
                status.className = 'tm-filter-status';
                status.innerHTML = '<span></span><button type="button"></button>';
                status.querySelector('button').addEventListener('click', () => {
                    this.revealTemporarily = !this.revealTemporarily;
                    this.scan();
                });
                document.body.appendChild(status);
            }
            status.classList.toggle('active', matchedCount > 0 && StorageManager.isContentFilterEnabled());
            status.querySelector('span').textContent = t(this.revealTemporarily ? 'filter_revealed_count' : 'filter_hidden_count', { count: matchedCount });
            status.querySelector('button').textContent = this.revealTemporarily ? t('filter_hide_again') : t('filter_show_temporarily');
        },

        scan() {
            const enabled = StorageManager.isContentFilterEnabled();
            const blockedUploaders = new Set(StorageManager.getBlockedUploaders().map(v => this.normalize(v).replace(/^@/, '')).filter(Boolean));
            const mutedTerms = StorageManager.getMutedTitleTerms().map(v => this.normalize(v)).filter(Boolean);
            let matchedCount = 0;
            this.getVideoCards().forEach(card => {
                if (!enabled) {
                    card.classList.remove('tm-filter-hidden');
                    delete card.dataset.tmFilterReason;
                    card.querySelectorAll('.tm-filter-quick-block').forEach(button => button.remove());
                    card.querySelectorAll('.tm-filter-resolved-row').forEach(row => row.remove());
                    return;
                }
                const uploader = this.getUploader(card);
                this.injectQuickBlock(card, uploader);
                const match = this.getMatch(card, blockedUploaders, mutedTerms);
                if (!uploader && (!match || match.type !== 'title')) this.queueUploaderResolution(card);
                if (match) {
                    matchedCount++;
                    card.dataset.tmFilterReason = match.type;
                    card.classList.toggle('tm-filter-hidden', !this.revealTemporarily);
                } else {
                    card.classList.remove('tm-filter-hidden');
                    delete card.dataset.tmFilterReason;
                }
            });
            this.updateStatus(enabled ? matchedCount : 0);
        },

        scheduleScan() {
            clearTimeout(this.scanTimer);
            this.scanTimer = setTimeout(() => this.scan(), 120);
        },

        start() {
            this.uploaderCache = StorageManager.getVideoUploaderCache();
            this.scan();
            if (this.observer) this.observer.disconnect();
            this.observer = new MutationObserver(mutations => {
                if (mutations.some(mutation => [...mutation.addedNodes].some(node =>
                    node.nodeType === Node.ELEMENT_NODE && !node.closest?.('.tm-panel, .tm-filter-status')
                ))) this.scheduleScan();
            });
            this.observer.observe(document.body, { childList: true, subtree: true });
        }
    };

    function extractVideoId() {
        const match = window.location.pathname.match(/\/video\/(\d+)/);
        return match ? match[1] : null;
    }

    function extractVideoData(forceDuration = null) {
        const videoId = extractVideoId();
        if (!videoId) return null;
        let title = 'Untitled';
        const titleEl = document.querySelector('h3.big-title-truncate, h4.big-title-truncate');
        if (titleEl) title = titleEl.textContent.trim();
        else if (document.title) title = document.title.replace(' - TokyoMotion', '').trim();
        let duration = '';
        const videoEl = document.querySelector('video');
        if (forceDuration) {
            duration = forceDuration;
        } else if (videoEl && videoEl.duration && !isNaN(videoEl.duration) && videoEl.duration !== Infinity && videoEl.duration > 0) {
            duration = formatSeconds(videoEl.duration) || '';
        }
        if (!duration) {
            const durEl = document.querySelector('.vjs-duration-display') || document.querySelector('.duration');
            if (durEl) {
                const text = durEl.innerText.replace(/[^\d:]/g, '');
                if (text && text !== '0:00' && text !== '00:00') duration = text;
            }
        }
        let author = 'Unknown';
        let authorIcon = '';
        const userContainer = document.querySelector('.user-container');
        if (userContainer) {
            const link = userContainer.querySelector('a[href^="/user/"]');
            if (link) {
                const span = link.querySelector('span');
                author = span ? span.textContent.trim() : link.textContent.trim();
                const img = link.querySelector('img');
                if (img) authorIcon = img.src;
            }
        } else {
            const userLink = document.querySelector('.user-container a[href^="/user/"], .video-info a[href^="/user/"], .user-info a[href^="/user/"], a.username');
            if (userLink) author = userLink.innerText.trim();
            const avatarImg = document.querySelector('.avatar-container img, .video-info img.avatar, .user-avatar img');
            if (avatarImg) authorIcon = avatarImg.src;
        }
        if (!authorIcon) authorIcon = 'https://www.tokyomotion.net/img/user-avatar.png';
        let isPrivate = false;
        if (StorageManager.isPrivateCached(videoId)) isPrivate = true;
        if (!isPrivate) {
            try {
                if (document.querySelector('.label-private') || document.querySelector('.img-private')) isPrivate = true;
            } catch (e) { }
        }
        return {
            id: videoId,
            title: title,
            thumbnail: document.querySelector('video[poster]')?.getAttribute('poster') || '',
            url: window.location.href,
            duration: duration,
            author: author,
            authorIcon: authorIcon,
            isPrivate: isPrivate,
            timestamp: Date.now()
        };
    }

    function getDurationFromPlayer() {
        const videoEl = document.querySelector('video');
        if (videoEl && videoEl.duration && !isNaN(videoEl.duration) && videoEl.duration !== Infinity && videoEl.duration > 0) {
            const formatted = formatSeconds(videoEl.duration);
            if (formatted) return formatted;
        }
        const durEl = document.querySelector('.vjs-duration-display');
        if (durEl) {
            const text = durEl.innerText.replace(/[^\d:]/g, '');
            if (text && text !== '0:00' && text !== '00:00') return text;
        }
        return null;
    }

    // ========================================
    // パネル・ボタン移動 & リサイズ
    // ========================================
    function shouldUseMobileUI() {
        const configuredMode = StorageManager.getUIMode();
        if (configuredMode === 'mobile') return true;
        if (configuredMode === 'desktop') return false;
        const narrowViewport = window.innerWidth <= 767;
        const coarsePointer = typeof window.matchMedia === 'function' && window.matchMedia('(pointer: coarse)').matches;
        return narrowViewport || (coarsePointer && window.innerWidth <= 1024);
    }

    function isMobileUIMode() {
        return document.documentElement.classList.contains('tm-enhancer-mobile');
    }

    function clearFloatingUIStyles(panel, btn) {
        ['width', 'height', 'left', 'top', 'right', 'bottom'].forEach(prop => panel.style.removeProperty(prop));
        ['left', 'top', 'right', 'bottom'].forEach(prop => btn.style.removeProperty(prop));
    }

    function applyUIMode(panel, btn) {
        const mobile = shouldUseMobileUI();
        clearFloatingUIStyles(panel, btn);
        document.documentElement.classList.toggle('tm-enhancer-mobile', mobile);
        panel.classList.toggle('tm-mobile-mode', mobile);
        btn.classList.toggle('tm-mobile-mode', mobile);
        restoreUIState(panel, btn, mobile);
        applyVideoGridCols();
        return mobile;
    }

    function setupDraggableButton(btn) {
        let isDragging = false;
        let startX, startY, initialLeft, initialTop;
        const DRAG_THRESHOLD = 5;
        let hasMoved = false;
        let activePointerId = null;
        let dragMobileMode = false;

        btn.addEventListener('pointerdown', (e) => {
            if (e.pointerType === 'mouse' && e.button !== 0) return;
            isDragging = true;
            hasMoved = false;
            activePointerId = e.pointerId;
            dragMobileMode = isMobileUIMode();
            const rect = btn.getBoundingClientRect();
            btn.style.bottom = 'auto'; btn.style.right = 'auto';
            btn.style.left = rect.left + 'px'; btn.style.top = rect.top + 'px';
            startX = e.clientX; startY = e.clientY;
            initialLeft = rect.left; initialTop = rect.top;
            document.body.classList.add('tm-dragging');
            try { btn.setPointerCapture(e.pointerId); } catch (_) { }
            e.preventDefault();
        });
        window.addEventListener('pointermove', (e) => {
            if (!isDragging || e.pointerId !== activePointerId) return;
            const dx = e.clientX - startX; const dy = e.clientY - startY;
            if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) hasMoved = true;
            const winW = window.innerWidth; const winH = window.innerHeight;
            const btnW = btn.offsetWidth; const btnH = btn.offsetHeight;
            let newLeft = initialLeft + dx; let newTop = initialTop + dy;
            newLeft = Math.max(0, Math.min(newLeft, winW - btnW));
            newTop = Math.max(0, Math.min(newTop, winH - btnH));
            btn.style.left = newLeft + 'px'; btn.style.top = newTop + 'px';
        });
        const finishDrag = (e) => {
            if (!isDragging || e.pointerId !== activePointerId) return;
            isDragging = false; activePointerId = null; document.body.classList.remove('tm-dragging');
            if (hasMoved) StorageManager.setBtnPosition({ left: btn.style.left, top: btn.style.top }, dragMobileMode);
        };
        window.addEventListener('pointerup', finishDrag);
        window.addEventListener('pointercancel', finishDrag);
        btn.addEventListener('click', (e) => {
            if (hasMoved) { hasMoved = false; e.stopImmediatePropagation(); e.preventDefault(); }
        }, true);
    }

    function setupDraggablePanel(panel) {
        const header = panel.querySelector('.tm-panel-header');
        let isDragging = false;
        let startX, startY, initialLeft, initialTop;
        let activePointerId = null;
        let dragMobileMode = false;
        header.addEventListener('pointerdown', (e) => {
            if (e.pointerType === 'mouse' && e.button !== 0) return;
            if (e.target.closest('button') || e.target.closest('.tm-resizer')) return;
            isDragging = true;
            activePointerId = e.pointerId;
            dragMobileMode = isMobileUIMode();
            const rect = panel.getBoundingClientRect();
            panel.style.bottom = 'auto'; panel.style.right = 'auto';
            panel.style.left = rect.left + 'px'; panel.style.top = rect.top + 'px';
            startX = e.clientX; startY = e.clientY;
            initialLeft = rect.left; initialTop = rect.top;
            document.body.classList.add('tm-dragging');
            try { header.setPointerCapture(e.pointerId); } catch (_) { }
            e.preventDefault();
        });
        window.addEventListener('pointermove', (e) => {
            if (!isDragging || e.pointerId !== activePointerId) return;
            const dx = e.clientX - startX; const dy = e.clientY - startY;
            let newLeft = initialLeft + dx; let newTop = initialTop + dy;
            const winW = window.innerWidth; const winH = window.innerHeight;
            const panelW = panel.offsetWidth; const panelH = panel.offsetHeight;
            const edgeGap = dragMobileMode ? 8 : 0;
            const rightGap = dragMobileMode ? 8 : SCROLLBAR_MARGIN;
            newLeft = Math.max(edgeGap, Math.min(newLeft, Math.max(edgeGap, winW - panelW - rightGap)));
            newTop = Math.max(edgeGap, Math.min(newTop, Math.max(edgeGap, winH - panelH - edgeGap)));
            panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px';
        });
        const finishDrag = (e) => {
            if (!isDragging || e.pointerId !== activePointerId) return;
            isDragging = false; activePointerId = null; document.body.classList.remove('tm-dragging'); savePanelState(panel, dragMobileMode);
        };
        window.addEventListener('pointerup', finishDrag);
        window.addEventListener('pointercancel', finishDrag);
    }

    function setupResizablePanel(panel) {
        const directions = ['n', 'e', 's', 'w', 'ne', 'nw', 'se', 'sw'];
        directions.forEach(dir => {
            const resizer = document.createElement('div');
            resizer.className = `tm-resizer ${dir}`;
            panel.appendChild(resizer);
            resizer.addEventListener('pointerdown', (e) => initResize(e, dir));
        });
        let isResizing = false;
        let currentDir = '';
        let startX, startY, startW, startH, startLeft, startTop;
        let activePointerId = null;
        let resizeMobileMode = false;
        function initResize(e, dir) {
            if (e.pointerType === 'mouse' && e.button !== 0) return;
            e.preventDefault(); e.stopPropagation();
            isResizing = true; currentDir = dir;
            activePointerId = e.pointerId;
            resizeMobileMode = isMobileUIMode();
            const rect = panel.getBoundingClientRect();
            startX = e.clientX; startY = e.clientY;
            startW = rect.width; startH = rect.height;
            startLeft = rect.left; startTop = rect.top;
            panel.style.left = startLeft + 'px'; panel.style.top = startTop + 'px';
            panel.style.right = 'auto'; panel.style.bottom = 'auto';
            panel.style.width = startW + 'px'; panel.style.height = startH + 'px';
            document.body.classList.add('tm-dragging');
            document.body.style.cursor = window.getComputedStyle(e.target).cursor;
            try { e.target.setPointerCapture(e.pointerId); } catch (_) { }
        }
        window.addEventListener('pointermove', (e) => {
            if (!isResizing || e.pointerId !== activePointerId) return;
            const dx = e.clientX - startX; const dy = e.clientY - startY;
            const winW = window.innerWidth; const winH = window.innerHeight;
            const edgeGap = resizeMobileMode ? 8 : 0;
            const rightGap = resizeMobileMode ? 8 : SCROLLBAR_MARGIN;
            let newW = startW; let newH = startH; let newLeft = startLeft; let newTop = startTop;
            if (currentDir.includes('e')) newW = Math.min(startW + dx, winW - startLeft - rightGap);
            if (currentDir.includes('w')) { const actualDx = Math.max(dx, edgeGap - startLeft); newW = startW - actualDx; newLeft = startLeft + actualDx; }
            if (currentDir.includes('s')) newH = Math.min(startH + dy, winH - startTop - edgeGap);
            if (currentDir.includes('n')) { const actualDy = Math.max(dy, edgeGap - startTop); newH = startH - actualDy; newTop = startTop + actualDy; }
            const minW = Math.min(resizeMobileMode ? 280 : 300, Math.max(1, winW - edgeGap - rightGap));
            const minH = Math.min(resizeMobileMode ? 220 : 200, Math.max(1, winH - (edgeGap * 2)));
            if (newW < minW) { if (currentDir.includes('w')) newLeft = startLeft + (startW - minW); newW = minW; }
            if (newH < minH) { if (currentDir.includes('n')) newTop = startTop + (startH - minH); newH = minH; }
            panel.style.width = newW + 'px'; panel.style.height = newH + 'px';
            panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px';
        });
        const finishResize = (e) => {
            if (!isResizing || e.pointerId !== activePointerId) return;
            isResizing = false; activePointerId = null; document.body.classList.remove('tm-dragging'); document.body.style.cursor = ''; savePanelState(panel, resizeMobileMode);
        };
        window.addEventListener('pointerup', finishResize);
        window.addEventListener('pointercancel', finishResize);
    }

    function savePanelState(panel, mobile = isMobileUIMode()) {
        const style = window.getComputedStyle(panel);
        StorageManager.setPanelState({ width: style.width, height: style.height, left: style.left, top: style.top }, mobile);
    }

    function restoreUIState(panel, btn, mobile = isMobileUIMode()) {
        const winW = window.innerWidth; const winH = window.innerHeight;
        const edgeGap = mobile ? 8 : 0;
        const rightGap = mobile ? 8 : SCROLLBAR_MARGIN;
        const btnPos = StorageManager.getBtnPosition(mobile);
        if (btnPos) {
            const btnW = btn.offsetWidth || (mobile ? 52 : 56);
            const btnH = btn.offsetHeight || (mobile ? 52 : 56);
            const left = Math.max(0, Math.min(parseFloat(btnPos.left) || 0, Math.max(0, winW - btnW)));
            const top = Math.max(0, Math.min(parseFloat(btnPos.top) || 0, Math.max(0, winH - btnH)));
            btn.style.bottom = 'auto'; btn.style.right = 'auto'; btn.style.left = left + 'px'; btn.style.top = top + 'px';
        }
        const panelState = StorageManager.getPanelState(mobile);
        if (panelState) {
            panel.style.bottom = 'auto'; panel.style.right = 'auto';
            let w = parseFloat(panelState.width); let h = parseFloat(panelState.height);
            let l = parseFloat(panelState.left); let t = parseFloat(panelState.top);
            if (![w, h, l, t].every(Number.isFinite)) return;
            const maxW = Math.max(1, winW - edgeGap - rightGap);
            const maxH = Math.max(1, winH - (edgeGap * 2));
            const minW = Math.min(mobile ? 280 : 300, maxW);
            const minH = Math.min(mobile ? 220 : 200, maxH);
            w = Math.max(minW, Math.min(w, maxW)); h = Math.max(minH, Math.min(h, maxH));
            l = Math.max(edgeGap, Math.min(l, Math.max(edgeGap, winW - w - rightGap)));
            t = Math.max(edgeGap, Math.min(t, Math.max(edgeGap, winH - h - edgeGap)));
            panel.style.left = l + 'px'; panel.style.top = t + 'px';
            panel.style.width = w + 'px'; panel.style.height = h + 'px';
        }
    }

    function setupScrollPersistence(panel) {
        const content = panel.querySelector('.tm-content');
        if (!content) return;
        content.addEventListener('scroll', () => {
            const currentTab = StorageManager.getLastActiveTab();
            if (currentTab) {
                if (content.scrollTimeout) clearTimeout(content.scrollTimeout);
                content.scrollTimeout = setTimeout(() => {
                    StorageManager.setTabScroll(currentTab, content.scrollTop);
                }, 100);
            }
        });
    }

    function restoreScrollPosition(panel, tabName) {
        const content = panel.querySelector('.tm-content');
        if (content) {
            const savedScroll = StorageManager.getTabScroll(tabName);
            setTimeout(() => { content.scrollTop = savedScroll; }, 50);
        }
    }

    // ========================================
    // メインUI
    // ========================================
    const UI_ICONS = Object.freeze({
        toggle: `<svg class="tm-toggle-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7.5h16v11H4zM4 7.5l2.5-4h13.5l-2.5 4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M10 10.4l5.2 3.1-5.2 3.1z" fill="currentColor"/></svg>`,
        close: `<svg class="tm-close-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 7l10 10M17 7L7 17" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>`
    });

    function createMainUI() {
        const toggleBtn = document.createElement('button');
        toggleBtn.className = 'tm-toggle-btn';
        toggleBtn.type = 'button';
        toggleBtn.innerHTML = UI_ICONS.toggle;
        toggleBtn.title = 'TokyoMotion Enhancer';
        toggleBtn.setAttribute('aria-label', 'TokyoMotion Enhancer');
        document.body.appendChild(toggleBtn);

        const panel = document.createElement('div');
        panel.className = 'tm-panel';

        const isVideoPage = location.pathname.startsWith('/video/');
        panel.innerHTML = `
            <div class="tm-panel-header">
                <span>🎬 TokyoMotion Enhancer</span>
                <button type="button" class="tm-panel-close" aria-label="${t('btn_close')}" title="${t('btn_close')}">${UI_ICONS.close}</button>
            </div>
            <div class="tm-tabs" id="tm-tabs-container"></div>
            <div class="tm-content">
                <div class="tm-tab-content" id="tm-liked"></div>
                <div class="tm-tab-content" id="tm-history"></div>
                <div class="tm-tab-content" id="tm-playlists"></div>

                <div class="tm-tab-content" id="tm-feed">
                    <div class="tm-feed-header">
                        <span id="tm-feed-time-ago" class="tm-time-label"></span>
                        <button class="tm-btn-primary" id="tm-feed-update">${t('tab_feed')} ${t('btn_update')}</button>
                        <span id="tm-feed-time-absolute" class="tm-time-label"></span>
                    </div>
                    <div id="tm-feed-status" style="margin:0 0 10px 0; text-align:center; font-size:11px; color:#888;"></div>
                    <div id="tm-feed-list"></div>
                </div>

                <div class="tm-tab-content" id="tm-friends">
                    <div class="tm-feed-header">
                        <span id="tm-friends-time-ago" class="tm-time-label"></span>
                        <button class="tm-btn-primary" id="tm-friends-update">${t('tab_friends')} ${t('btn_update')}</button>
                        <span id="tm-friends-time-absolute" class="tm-time-label"></span>
                    </div>
                    <div id="tm-friends-status" style="margin:0 0 10px 0; text-align:center; font-size:11px; color:#888;"></div>
                    <div id="tm-friends-list"></div>
                </div>

                <div class="tm-tab-content" id="tm-settings">
                    <!-- Settings will be rendered by JS -->
                </div>
            </div>
        `;
        document.body.appendChild(panel);

        const initialMobileMode = applyUIMode(panel, toggleBtn);
        panel.classList.toggle('active', isVideoPage && !initialMobileMode);

        // 初期描画
        renderTabs(panel);
        renderSettingsTab(panel);
        setupDraggableButton(toggleBtn);
        setupDraggablePanel(panel);
        setupResizablePanel(panel);
        setupScrollPersistence(panel);
        applyVideoGridCols();

        // ----------------------------------------
        // ★自動スクロールリセット機能
        // ----------------------------------------
        let leaveTime = 0;
        document.addEventListener('visibilitychange', () => {
            if (document.hidden) {
                // サイトを離れた(タブを隠した)時間を記録
                leaveTime = Date.now();
            } else {
                // サイトに戻ってきた時
                if (leaveTime > 0) {
                    const diff = Date.now() - leaveTime;
                    const threshold = getScrollResetMs();
                    // 設定時間を経過していたらリセット
                    if (diff > threshold) {
                        const content = panel.querySelector('.tm-content');
                        if (content) {
                            content.scrollTop = 0;
                        }

                        // 全タブのスクロール位置をリセット
                        const tabsToReset = ['liked', 'history', 'playlists', 'feed', 'friends'];
                        tabsToReset.forEach(tab => {
                            StorageManager.setTabScroll(tab, 0);
                        });

                        // プレイリストタブは詳細画面から一覧に戻す
                        StorageManager.setActivePlaylist(null);

                        // 現在のタブを再読み込みしてリセット反映
                        const currentTab = StorageManager.getLastActiveTab();
                        if (currentTab === 'liked') {
                            loadLikedVideos();
                        } else if (currentTab === 'history') {
                            loadHistory();
                        } else if (currentTab === 'playlists') {
                            loadPlaylists();
                        }

                        showToast(t('msg_scroll_reset')); // 通知
                    }
                    leaveTime = 0;
                }
            }
        });

        panel.addEventListener('click', (e) => e.stopPropagation());

        toggleBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            panel.classList.toggle('active');
            if (panel.classList.contains('active')) {
                let defaultTab = StorageManager.getDefaultTab();
                if (defaultTab === 'last_open') defaultTab = StorageManager.getLastActiveTab();
                const visibility = StorageManager.getTabVisibility();
                if (!visibility[defaultTab]) {
                    const order = StorageManager.getTabOrder();
                    defaultTab = order.find(t => visibility[t]) || 'settings';
                }
                switchToTab(panel, defaultTab);
            }
        });

        panel.querySelector('.tm-panel-close').addEventListener('click', () => {
            panel.classList.remove('active');
        });

        document.addEventListener('click', (e) => {
            if (panel.classList.contains('active')) {
                if (document.querySelector('.tm-modal-overlay')) return;
                if (!isVideoPage || isMobileUIMode()) {
                    if (!panel.contains(e.target) && !toggleBtn.contains(e.target)) {
                        panel.classList.remove('active');
                    }
                }
            }
        });

        if (panel.classList.contains('active')) {
            let defaultTab = StorageManager.getDefaultTab();
            if (defaultTab === 'last_open') defaultTab = StorageManager.getLastActiveTab();
            switchToTab(panel, defaultTab);
        }

        document.addEventListener('fullscreenchange', () => {
            if (document.fullscreenElement) {
                panel.style.display = 'none';
                toggleBtn.style.display = 'none';
            } else {
                panel.style.display = '';
                toggleBtn.style.display = '';
            }
        });

        let uiResizeTimer = null;
        window.addEventListener('resize', () => {
            clearTimeout(uiResizeTimer);
            uiResizeTimer = setTimeout(() => applyUIMode(panel, toggleBtn), 120);
        });
    }

    // ========================================
    // 設定タブの動的描画
    // ========================================
    function renderSettingsTab(panel = document.querySelector('.tm-panel')) {
        const container = panel.querySelector('#tm-settings');
        if (!container) return;

        // オプション生成ヘルパー
        const generatePageOptions = () => {
            let opts = '';
            for (let i = 1; i <= 10; i++) {
                opts += `<option value="${i}">${i}${t('stg_page_unit')}</option>`;
            }
            opts += `<option value="99999">${t('stg_unlimited')}</option>`;
            return opts;
        };

        const generateFetchRangePanel = (type, title) => {
            const modes = type === 'feed' ? StorageManager.getFeedFetchModes() : StorageManager.getFriendsFetchModes();
            return `
                <div class="tm-fetch-range-panel" data-fetch-range="${type}">
                    <div class="tm-fetch-range-title">${title}</div>
                    <label class="tm-fetch-mode-row">
                        <input type="radio" name="tm-${type}-fetch-mode" id="tm-${type}-mode-pages" value="pages" ${modes.pages ? 'checked' : ''}>
                        <span>${fetchRangeText('modePages')}</span>
                        <select id="tm-${type}-max-pages" class="tm-input">${generatePageOptions()}</select>
                    </label>
                    <label class="tm-fetch-mode-row">
                        <input type="radio" name="tm-${type}-fetch-mode" id="tm-${type}-mode-days" value="days" ${modes.days ? 'checked' : ''}>
                        <span>${fetchRangeText('modeDays')}</span>
                        <input type="number" id="tm-${type}-max-days" class="tm-input" min="1" max="365" step="1">
                    </label>
                    <label class="tm-fetch-mode-row">
                        <input type="radio" name="tm-${type}-fetch-mode" id="tm-${type}-mode-since-last" value="sinceLast" ${modes.sinceLast ? 'checked' : ''}>
                        <span>${fetchRangeText('modeSinceLast')}</span>
                        <span></span>
                    </label>
                    <div class="tm-fetch-mode-status" id="tm-${type}-fetch-mode-status"></div>
                </div>
            `;
        };

        const currentScrollVal = StorageManager.getScrollResetValue();
        const currentScrollUnit = StorageManager.getScrollResetUnit();

        container.innerHTML = `
            <div style="margin-bottom:15px;">
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_language')}</label>
                <select id="tm-language-selector" class="tm-input">
                    <option value="auto">Auto (自動)</option>
                    <option value="ja">日本語</option>
                    <option value="en">English</option>
                </select>
            </div>
            <div style="border-top:1px solid #444; padding-top:15px; margin-bottom:15px;">
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_ui_mode')}</label>
                <select id="tm-ui-mode" class="tm-input">
                    <option value="auto">${t('stg_ui_mode_auto')}</option>
                    <option value="desktop">${t('stg_ui_mode_desktop')}</option>
                    <option value="mobile">${t('stg_ui_mode_mobile')}</option>
                </select>
                <div style="font-size:12px;color:#888;line-height:1.5;">${t('stg_ui_mode_desc')}</div>
            </div>
            <div style="border-top:1px solid #444; padding-top:15px; margin-bottom:15px;">
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_startup_tab')}</label>
                <select id="tm-default-tab" class="tm-input">
                    <option value="last_open">${t('stg_tab_last_open')}</option>
                    <option value="liked">${t('tab_liked')}</option>
                    <option value="history">${t('tab_history')}</option>
                    <option value="playlists">${t('tab_playlists')}</option>
                    <option value="feed">${t('tab_feed')}</option>
                    <option value="friends">${t('tab_friends')}</option>
                    <option value="settings">${t('tab_settings')}</option>
                </select>
            </div>

            <div style="margin-top:15px; border-top:1px solid #444; padding-top:15px;">
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_scroll_reset')}</label>
                <div style="display:flex; gap:10px;">
                    <input type="number" id="tm-scroll-val" class="tm-input" style="flex:1;" min="1" value="${currentScrollVal}">
                    <select id="tm-scroll-unit" class="tm-input" style="flex:1;">
                        <option value="seconds">${t('unit_sec')}</option>
                        <option value="minutes">${t('unit_min')}</option>
                        <option value="hours">${t('unit_hour')}</option>
                    </select>
                </div>
            </div>

            <div class="tm-login-section" style="border-top:1px solid #444; padding-top:15px;">
                <div style="color:#e0e0e0;font-size:14px;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:8px;">
                    ${t('stg_auto_login')} <div class="tm-toggle-switch" id="tm-auto-login-toggle"></div>
                </div>
                <div style="font-size:12px;color:#888;">${t('stg_auto_login_desc')}</div>
            </div>

            <div style="margin-top:15px; border-top:1px solid #444; padding-top:15px;">
                <div style="color:#e0e0e0;font-size:14px;font-weight:600;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;gap:8px;">
                    <span>${t('filter_title')}</span>
                    <div class="tm-toggle-switch" id="tm-content-filter-toggle" role="switch" aria-label="${t('filter_enabled')}"></div>
                </div>
                <div style="font-size:12px;color:#888;line-height:1.5;margin-bottom:12px;">${t('filter_desc')}</div>
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:6px;">${t('filter_uploaders')}</label>
                <textarea id="tm-filter-uploaders" class="tm-input tm-filter-textarea" placeholder="${t('filter_uploaders_placeholder')}"></textarea>
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:6px;">${t('filter_words')}</label>
                <textarea id="tm-filter-words" class="tm-input tm-filter-textarea" placeholder="${t('filter_words_placeholder')}"></textarea>
                <div class="tm-btn-row">
                    <button class="tm-btn-primary" id="tm-filter-save">${t('filter_save')}</button>
                    <button class="tm-btn-secondary" id="tm-filter-clear">${t('filter_clear')}</button>
                </div>
            </div>

            <div style="margin-top:15px; border-top:1px solid #444; padding-top:15px;">
                <div style="color:#e0e0e0;font-size:14px;font-weight:600;margin-bottom:12px;">${t('stg_tab_visibility')}</div>
                <div id="tm-tab-visibility-settings"></div>
            </div>

            <div style="margin-top:15px; border-top:1px solid #444; padding-top:15px;">
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_grid_cols')}</label>
                <select id="tm-video-grid-cols" class="tm-input">
                    <option value="1">1${t('stg_col_unit')}</option>
                    <option value="2">2${t('stg_col_unit')}</option>
                    <option value="3">3${t('stg_col_unit')}</option>
                    <option value="4">4${t('stg_col_unit')}</option>
                    <option value="5">5${t('stg_col_unit')}</option>
                </select>
            </div>

            <div style="margin-top:15px; border-top:1px solid #444; padding-top:15px;">
                <div style="color:#e0e0e0;font-size:14px;font-weight:600;margin-bottom:12px;">${fetchRangeText('title')}</div>
                ${generateFetchRangePanel('feed', fetchRangeText('feedTitle'))}
                ${generateFetchRangePanel('friends', fetchRangeText('friendsTitle'))}
            </div>
            <div class="tm-btn-row" style="margin-top:20px;">
                <button class="tm-btn-secondary" id="tm-export">${t('btn_export')}</button>
                <button class="tm-btn-secondary" id="tm-import">${t('btn_import')}</button>
            </div>
            <button class="tm-btn-danger" id="tm-clear-all">${t('btn_clear_all')}</button>
        `;

        // イベントバインドの再実行
        renderTabVisibilitySettings();
        initLoginSettings();
        initContentFilterSettings();

        document.getElementById('tm-export').addEventListener('click', exportData);
        document.getElementById('tm-import').addEventListener('click', importData);
        document.getElementById('tm-clear-all').addEventListener('click', clearAllData);

        const defaultTabSelect = document.getElementById('tm-default-tab');
        defaultTabSelect.value = StorageManager.getDefaultTab();
        defaultTabSelect.addEventListener('change', (e) => StorageManager.setDefaultTab(e.target.value));

        const uiModeSelect = document.getElementById('tm-ui-mode');
        uiModeSelect.value = StorageManager.getUIMode();
        uiModeSelect.addEventListener('change', (e) => {
            StorageManager.setUIMode(e.target.value);
            const toggleBtn = document.querySelector('.tm-toggle-btn');
            if (toggleBtn) applyUIMode(panel, toggleBtn);
        });

        const scrollValInput = document.getElementById('tm-scroll-val');
        const saveScrollVal = (e) => {
            const val = parseInt(e.target.value);
            if (val > 0) {
                StorageManager.setScrollResetValue(val);
                console.log(`[TokyoMotion Enhancer] Saved scroll val: ${val}`);
            }
        };
        // 'input'だと急な変更で保存が追いつかない場合があるので'change'も併用
        scrollValInput.addEventListener('input', saveScrollVal);
        scrollValInput.addEventListener('change', saveScrollVal);

        const scrollUnitSelect = document.getElementById('tm-scroll-unit');
        scrollUnitSelect.value = currentScrollUnit;
        scrollUnitSelect.addEventListener('change', (e) => {
            StorageManager.setScrollResetUnit(e.target.value);
            console.log(`[TokyoMotion Enhancer] Saved scroll unit: ${e.target.value}`);
        });

        const videoGridColsSelect = document.getElementById('tm-video-grid-cols');
        videoGridColsSelect.value = StorageManager.getVideoGridCols();
        videoGridColsSelect.addEventListener('change', (e) => {
            const val = parseInt(e.target.value);
            StorageManager.setVideoGridCols(val);
            applyVideoGridCols();
        });

        initFetchRangeSettings('feed');
        initFetchRangeSettings('friends');

        const langSelect = document.getElementById('tm-language-selector');
        langSelect.value = GM_getValue('appLanguage', 'auto');
        langSelect.addEventListener('change', (e) => {
            TranslationManager.setLanguage(e.target.value);
            // リロードではなく、描画関数を呼び出して即時反映
            renderTabs(panel);
            renderSettingsTab(panel);

            // 現在のタブが設定以外(既に開いていたタブ)の場合、その内容も更新する必要がある
            // ただし設定タブにいるので、次にフィードタブを開いたときに更新されればよい
            // _setupFeedLogicがタブ切り替え時に呼ばれ、そこで言語更新を行うように修正済み
        });
    }

    // ========================================
    // タブ関連ロジック
    // ========================================
    function getFetchRangeStorage(type) {
        if (type === 'feed') {
            return {
                getModes: () => StorageManager.getFeedFetchModes(),
                setModes: (modes) => StorageManager.setFeedFetchModes(modes),
                getPages: () => StorageManager.getFeedMaxPages(),
                setPages: (pages) => StorageManager.setFeedMaxPages(pages),
                getDays: () => StorageManager.getFeedMaxDays(),
                setDays: (days) => StorageManager.setFeedMaxDays(days),
                getLastUpdated: () => StorageManager.getFeedLastUpdated()
            };
        }
        return {
            getModes: () => StorageManager.getFriendsFetchModes(),
            setModes: (modes) => StorageManager.setFriendsFetchModes(modes),
            getPages: () => StorageManager.getFriendsMaxPages(),
            setPages: (pages) => StorageManager.setFriendsMaxPages(pages),
            getDays: () => StorageManager.getFriendsMaxDays(),
            setDays: (days) => StorageManager.setFriendsMaxDays(days),
            getLastUpdated: () => StorageManager.getFriendsLastUpdated()
        };
    }

    function getEnabledFetchModes(modes) {
        return ['pages', 'days', 'sinceLast'].filter(key => !!modes[key]);
    }

    function validateFetchRange(type, options = {}) {
        const storage = getFetchRangeStorage(type);
        const modes = storage.getModes();
        const enabled = getEnabledFetchModes(modes);
        const days = Math.max(1, parseInt(storage.getDays(), 10) || 1);

        if (enabled.length > 1) return { valid: false, type: 'conflict', message: fetchRangeText('conflict') };
        if (enabled.length === 0) return { valid: false, type: 'none', message: fetchRangeText('none') };
        if (enabled[0] === 'sinceLast' && options.requireLastUpdated && storage.getLastUpdated() <= 0) {
            return { valid: false, type: 'noLastFetch', message: fetchRangeText('noLastFetch') };
        }
        if (enabled[0] === 'pages') return { valid: true, mode: 'pages', message: fetchRangeText('activePages') };
        if (enabled[0] === 'days') return { valid: true, mode: 'days', message: fetchRangeText('activeDays', { days }) };
        return { valid: true, mode: 'sinceLast', message: fetchRangeText('activeSinceLast') };
    }

    function buildFetchOptions(type, baseTime, previousLastUpdated) {
        const storage = getFetchRangeStorage(type);
        const validation = validateFetchRange(type, { requireLastUpdated: true });
        if (!validation.valid) return validation;

        if (validation.mode === 'pages') {
            return {
                valid: true,
                mode: 'pages',
                maxPages: Math.max(1, parseInt(storage.getPages(), 10) || 1),
                baseTime
            };
        }
        if (validation.mode === 'days') {
            const days = Math.max(1, parseInt(storage.getDays(), 10) || 1);
            return {
                valid: true,
                mode: 'days',
                cutoffTimestamp: baseTime - (days * 24 * 60 * 60 * 1000),
                baseTime
            };
        }
        return {
            valid: true,
            mode: 'sinceLast',
            cutoffTimestamp: previousLastUpdated,
            baseTime
        };
    }

    function initFetchRangeSettings(type) {
        const storage = getFetchRangeStorage(type);
        const pagesToggle = document.getElementById(`tm-${type}-mode-pages`);
        const daysToggle = document.getElementById(`tm-${type}-mode-days`);
        const sinceLastToggle = document.getElementById(`tm-${type}-mode-since-last`);
        const pagesSelect = document.getElementById(`tm-${type}-max-pages`);
        const daysInput = document.getElementById(`tm-${type}-max-days`);
        const status = document.getElementById(`tm-${type}-fetch-mode-status`);
        if (!pagesToggle || !daysToggle || !sinceLastToggle || !pagesSelect || !daysInput || !status) return;

        pagesSelect.value = String(storage.getPages());
        daysInput.value = String(storage.getDays());

        const ensureOneSelected = () => {
            if (!pagesToggle.checked && !daysToggle.checked && !sinceLastToggle.checked) {
                pagesToggle.checked = true;
            }
        };

        const save = () => {
            ensureOneSelected();
            storage.setModes({
                pages: pagesToggle.checked,
                days: daysToggle.checked,
                sinceLast: sinceLastToggle.checked
            });
            storage.setPages(parseInt(pagesSelect.value, 10) || 1);
            storage.setDays(Math.max(1, parseInt(daysInput.value, 10) || 1));
            updateFetchRangeStatus(type);
        };

        [pagesToggle, daysToggle, sinceLastToggle, pagesSelect, daysInput].forEach(el => {
            el.addEventListener('change', save);
            if (el === daysInput) el.addEventListener('input', save);
        });
        updateFetchRangeStatus(type);
    }

    function updateFetchRangeStatus(type) {
        const status = document.getElementById(`tm-${type}-fetch-mode-status`);
        if (!status) return;
        const validation = validateFetchRange(type);
        status.textContent = validation.message;
        status.classList.toggle('ok', validation.valid);
        status.classList.toggle('error', !validation.valid);
    }

    function renderTabs(panel) {
        const container = panel.querySelector('#tm-tabs-container');
        container.innerHTML = '';
        const order = StorageManager.getTabOrder();
        DEFAULT_TAB_ORDER.forEach(def => { if (!order.includes(def)) order.push(def); });
        const visibility = StorageManager.getTabVisibility();
        order.forEach(tabKey => {
            if (!visibility[tabKey]) return;
            const btn = document.createElement('button');
            btn.className = 'tm-tab';
            btn.dataset.tab = tabKey;
            btn.textContent = t(`tab_${tabKey}`);
            btn.draggable = true;
            if (tabKey === StorageManager.getLastActiveTab()) btn.classList.add('active'); // アクティブ状態維持
            btn.addEventListener('click', () => switchToTab(panel, tabKey));
            btn.addEventListener('dragstart', (e) => {
                btn.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', tabKey);
            });
            btn.addEventListener('dragend', () => { btn.classList.remove('dragging'); });
            container.appendChild(btn);
        });
        container.addEventListener('dragover', (e) => {
            e.preventDefault();
            const afterElement = getDragAfterElementHorizontal(container, e.clientX);
            const dragging = document.querySelector('.tm-tab.dragging');
            if (afterElement == null) container.appendChild(dragging); else container.insertBefore(dragging, afterElement);
        });
        container.addEventListener('drop', (e) => {
            e.preventDefault();
            const newOrder = [...container.querySelectorAll('.tm-tab')].map(el => el.dataset.tab);
            const currentFullOrder = StorageManager.getTabOrder();
            const hiddenTabs = currentFullOrder.filter(t => !newOrder.includes(t));
            const finalOrder = [...newOrder, ...hiddenTabs];
            StorageManager.setTabOrder(finalOrder);
        });
    }

    function getDragAfterElementHorizontal(container, x) {
        const draggableElements = [...container.querySelectorAll('.tm-tab:not(.dragging)')];
        return draggableElements.reduce((closest, child) => {
            const box = child.getBoundingClientRect();
            const offset = x - box.left - box.width / 2;
            if (offset < 0 && offset > closest.offset) return { offset: offset, element: child }; else return closest;
        }, { offset: Number.NEGATIVE_INFINITY }).element;
    }

    function renderTabVisibilitySettings() {
        const container = document.getElementById('tm-tab-visibility-settings');
        if (!container) return;
        container.innerHTML = '';
        const visibility = StorageManager.getTabVisibility();
        const order = StorageManager.getTabOrder();
        DEFAULT_TAB_ORDER.forEach(def => { if (!order.includes(def)) order.push(def); });
        order.forEach(tabKey => {
            // 設定タブは常時表示のため、表示切り替えの対象には含めない。
            if (tabKey === 'settings') return;
            const row = document.createElement('div');
            row.className = 'tm-tab-visibility-item';
            const label = document.createElement('span');
            label.textContent = t(`tab_${tabKey}`);
            label.style.color = '#e0e0e0';
            label.style.fontSize = '13px';
            const toggle = document.createElement('div');
            toggle.className = 'tm-toggle-switch';
            if (visibility[tabKey]) toggle.classList.add('active');
            toggle.addEventListener('click', () => {
                const newVis = !toggle.classList.contains('active');
                if (newVis) toggle.classList.add('active'); else toggle.classList.remove('active');
                const currentVis = StorageManager.getTabVisibility();
                currentVis[tabKey] = newVis;
                StorageManager.setTabVisibility(currentVis);
                const panel = document.querySelector('.tm-panel');
                renderTabs(panel);
            });
            row.appendChild(label); row.appendChild(toggle); container.appendChild(row);
        });
    }

    function initLoginSettings() {
        const toggle = document.getElementById('tm-auto-login-toggle');
        if (!toggle) return;
        if (StorageManager.isAutoLoginEnabled()) toggle.classList.add('active');
        toggle.addEventListener('click', () => {
            const newState = !StorageManager.isAutoLoginEnabled();
            StorageManager.setAutoLoginEnabled(newState);
            toggle.classList.toggle('active', newState);
        });
    }

    function initContentFilterSettings() {
        const toggle = document.getElementById('tm-content-filter-toggle');
        const saveButton = document.getElementById('tm-filter-save');
        const clearButton = document.getElementById('tm-filter-clear');
        const uploaderInput = document.getElementById('tm-filter-uploaders');
        const wordInput = document.getElementById('tm-filter-words');
        if (!toggle || !saveButton || !clearButton || !uploaderInput || !wordInput) return;

        ContentFilter.refreshSettingsFields();
        toggle.classList.toggle('active', StorageManager.isContentFilterEnabled());
        toggle.setAttribute('aria-checked', String(StorageManager.isContentFilterEnabled()));
        toggle.addEventListener('click', () => {
            const enabled = !StorageManager.isContentFilterEnabled();
            StorageManager.setContentFilterEnabled(enabled);
            ContentFilter.revealTemporarily = false;
            toggle.classList.toggle('active', enabled);
            toggle.setAttribute('aria-checked', String(enabled));
            ContentFilter.scan();
        });

        saveButton.addEventListener('click', () => {
            StorageManager.setBlockedUploaders(ContentFilter.parseLines(uploaderInput.value));
            StorageManager.setMutedTitleTerms(ContentFilter.parseLines(wordInput.value));
            ContentFilter.revealTemporarily = false;
            ContentFilter.refreshSettingsFields();
            ContentFilter.scan();
            showToast(t('filter_saved'));
        });

        clearButton.addEventListener('click', () => {
            if (!confirm(t('filter_confirm_clear'))) return;
            StorageManager.setBlockedUploaders([]);
            StorageManager.setMutedTitleTerms([]);
            ContentFilter.revealTemporarily = false;
            ContentFilter.refreshSettingsFields();
            ContentFilter.scan();
            showToast(t('filter_cleared'));
        });
    }

    function switchToTab(panel, tabName) {
        panel.querySelectorAll('.tm-tab').forEach(t => t.classList.remove('active'));
        panel.querySelectorAll('.tm-tab-content').forEach(c => c.classList.remove('active'));
        const targetBtn = panel.querySelector(`.tm-tab[data-tab="${tabName}"]`);
        if (targetBtn) targetBtn.classList.add('active');
        const targetContent = document.getElementById(`tm-${tabName}`);
        if (targetContent) targetContent.classList.add('active');
        StorageManager.setLastActiveTab(tabName);
        const restore = () => restoreScrollPosition(panel, tabName);
        if (tabName === 'liked') loadLikedVideos().then(restore);
        else if (tabName === 'history') loadHistory().then(restore);
        else if (tabName === 'playlists') loadPlaylists().then(restore);
        else if (tabName === 'feed') { setupFeedTab(); restore(); } // restoreを追加
        else if (tabName === 'friends') { setupFriendsTab(); restore(); } // restoreを追加
        else restore();
    }

    // ========================================
    // HTML生成 & イベント (既存機能)
    // ========================================
    function generateVideoCard(v, extraButton = '', addedTime = null) {
        const iconSrc = v.authorIcon || 'https://www.tokyomotion.net/img/user-avatar.png';
        const authorLink = v.author ? `/user/${v.author}/videos` : '#';
        const relativeTime = formatRelativeTime(v.date);
        const privateLabel = v.isPrivate ? `<div class="tm-card-private">${t('label_private')}</div>` : '';
        const addedTimeDisplay = addedTime ? `<div class="tm-added-time">${formatDate(addedTime)}</div>` : '';
        return `
            <div class="tm-card" data-url="${v.url}">
                ${extraButton}
                <div class="tm-card-thumb-box">
                    <img src="${v.thumbnail || ''}" class="tm-card-thumb" onerror="this.style.display='none'">
                    ${v.duration ? `<div class="tm-card-duration">${v.duration}</div>` : ''}
                    ${privateLabel}
                </div>
                <div class="tm-card-title">${v.title}</div>
                <div class="tm-card-meta">
                    ${v.author ? `
                    <div class="tm-user-row">
                        <img src="${iconSrc}" class="tm-user-icon" onerror="this.onerror=null;this.src='https://www.tokyomotion.net/img/user-avatar.png'">
                        <a href="${authorLink}" class="tm-user-link" target="_blank">${v.author}</a>
                        ${relativeTime ? `<span class="tm-relative-time">${relativeTime}</span>` : ''}
                    </div>
                    ` : ''}
                    ${addedTimeDisplay}
                </div>
            </div>
        `;
    }

    let draggedThumbnailUrl = null;

    function resetThumbnailDragUI() {
        draggedThumbnailUrl = null;
        document.body.classList.remove('tm-thumbnail-drag-active');
        document.querySelector('.tm-thumbnail-drop-zone')?.classList.remove('tm-drag-over');
        document.querySelectorAll('.tm-thumbnail-dragging').forEach(el => el.classList.remove('tm-thumbnail-dragging'));
    }

    function ensureThumbnailDropZone() {
        let zone = document.querySelector('.tm-thumbnail-drop-zone');
        if (!zone) {
            zone = document.createElement('div');
            zone.className = 'tm-thumbnail-drop-zone';
            zone.setAttribute('role', 'button');
            zone.addEventListener('dragenter', (event) => {
                event.preventDefault();
                zone.classList.add('tm-drag-over');
            });
            zone.addEventListener('dragover', (event) => {
                event.preventDefault();
                if (event.dataTransfer) event.dataTransfer.dropEffect = 'link';
                zone.classList.add('tm-drag-over');
            });
            zone.addEventListener('dragleave', () => zone.classList.remove('tm-drag-over'));
            zone.addEventListener('drop', (event) => {
                event.preventDefault();
                event.stopPropagation();
                const transferredUrl = event.dataTransfer && event.dataTransfer.getData('application/x-tm-video-url');
                const url = transferredUrl || draggedThumbnailUrl;
                resetThumbnailDragUI();
                if (url) GM_openInTab(url, { active: false, insert: true, setParent: true });
            });
            document.body.appendChild(zone);
        }
        zone.textContent = `↗ ${t('drag_drop_new_tab')}`;
        zone.setAttribute('aria-label', t('drag_drop_new_tab'));
        return zone;
    }

    function setupThumbnailDrag(card) {
        const thumbnail = card.querySelector('.tm-card-thumb-box');
        if (!thumbnail || thumbnail.dataset.tmDragReady === 'true' || !card.dataset.url) return;
        let videoUrl;
        try { videoUrl = new URL(card.dataset.url, location.href).href; } catch (error) { return; }
        thumbnail.dataset.tmDragReady = 'true';
        thumbnail.draggable = true;
        thumbnail.title = t('drag_thumbnail_hint');
        const image = thumbnail.querySelector('img');
        if (image) image.draggable = false;
        thumbnail.addEventListener('dragstart', (event) => {
            if (!event.dataTransfer) return;
            draggedThumbnailUrl = videoUrl;
            event.dataTransfer.effectAllowed = 'link';
            event.dataTransfer.setData('application/x-tm-video-url', videoUrl);
            thumbnail.classList.add('tm-thumbnail-dragging');
            ensureThumbnailDropZone();
            document.body.classList.add('tm-thumbnail-drag-active');
        });
        thumbnail.addEventListener('dragend', () => setTimeout(resetThumbnailDragUI, 0));
    }

    function attachCardEvents(container) {
        container.querySelectorAll('.tm-card').forEach(card => {
            setupThumbnailDrag(card);
            card.addEventListener('click', (e) => {
                if (e.target.closest('a') || e.target.closest('button')) return;
                window.location.href = card.dataset.url;
            });
        });
        container.querySelectorAll('.tm-user-link').forEach(link => { link.addEventListener('click', (e) => e.stopPropagation()); });
        container.querySelectorAll('.tm-card-thumb').forEach(img => {
            if (!img.src || !img.src.includes('/media/videos/')) return;
            const baseUrl = img.src.substring(0, img.src.lastIndexOf('/') + 1);
            img.dataset.baseurl = baseUrl;
            img.addEventListener('mousemove', function (e) {
                if (!this.dataset.preloaded) {
                    this.dataset.preloaded = 'true';
                    for (let i = 1; i <= 20; i++) (new Image()).src = `${this.dataset.baseurl}${i}.jpg`;
                }
                const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left;
                let percent = (x / rect.width) * 100; let num = Math.ceil(percent / 5); num = Math.max(1, Math.min(20, num));
                const targetSrc = `${this.dataset.baseurl}${num}.jpg`;
                if (this.src !== targetSrc) this.src = targetSrc;
            });
        });
    }

    async function loadLikedVideos(sortOrder = 'desc') {
        const container = document.getElementById('tm-liked');
        let videos = await StorageManager.getLikedVideos();
        if (videos.length === 0) { container.innerHTML = `<div class="tm-empty">${t('msg_empty_liked')}</div>`; return; }
        videos.sort((a, b) => sortOrder === 'desc' ? b.timestamp - a.timestamp : a.timestamp - b.timestamp);
        container.innerHTML = `
            <div style="margin-bottom:10px; text-align:right;">
                <button class="tm-btn-secondary" id="tm-sort-liked" style="width:auto; padding:4px 8px; font-size:11px;">${sortOrder === 'desc' ? t('btn_sort_new') : t('btn_sort_old')}</button>
            </div>
            <div class="tm-grid-view">
                ${videos.map(v => generateVideoCard(v, `<button class="tm-card-remove" data-remove-liked="${v.id}" title="${t('btn_remove')}">×</button>`, v.timestamp)).join('')}
            </div>
        `;
        document.getElementById('tm-sort-liked').addEventListener('click', (e) => { e.stopPropagation(); loadLikedVideos(sortOrder === 'desc' ? 'asc' : 'desc'); });
        container.querySelectorAll('[data-remove-liked]').forEach(btn => {
            btn.addEventListener('click', async (e) => {
                e.stopPropagation();
                if (confirm(t('confirm_delete_liked'))) { await StorageManager.removeLikedVideo(btn.dataset.removeLiked); loadLikedVideos(sortOrder); }
            });
        });
        attachCardEvents(container);
    }

    async function loadHistory(sortOrder = 'desc') {
        const container = document.getElementById('tm-history');
        let videos = await StorageManager.getHistory();
        if (videos.length === 0) { container.innerHTML = `<div class="tm-empty">${t('msg_empty_history')}</div>`; return; }
        videos.sort((a, b) => sortOrder === 'desc' ? b.watchedAt - a.watchedAt : a.watchedAt - b.watchedAt);
        container.innerHTML = `
            <div style="margin-bottom:10px; text-align:right;">
                <button class="tm-btn-secondary" id="tm-sort-history" style="width:auto; padding:4px 8px; font-size:11px;">${sortOrder === 'desc' ? t('btn_sort_new') : t('btn_sort_old')}</button>
            </div>
            <div class="tm-grid-view">
                ${videos.map(v => generateVideoCard(v, '', v.watchedAt)).join('')}
            </div>
        `;
        document.getElementById('tm-sort-history').addEventListener('click', (e) => { e.stopPropagation(); loadHistory(sortOrder === 'desc' ? 'asc' : 'desc'); });
        attachCardEvents(container);
    }

    async function loadPlaylists() {
        const activeName = StorageManager.getActivePlaylist();
        const playlists = await StorageManager.getPlaylists();
        if (activeName && playlists[activeName]) { showPlaylistDetail(activeName); return; }
        const container = document.getElementById('tm-playlists');
        const names = await StorageManager.getOrderedPlaylistNames();
        const currentCols = StorageManager.getPlaylistGridCols();
        const effectiveCols = isMobileUIMode() ? Math.min(currentCols, 2) : currentCols;
        container.innerHTML = `
            <div style="margin-bottom:15px; display:flex; gap:5px; align-items:center;">
                <input type="text" class="tm-input" id="tm-new-playlist-name" placeholder="${t('placeholder_new_playlist')}" style="margin:0; flex:1;">
                <button class="tm-btn-primary" id="tm-create-playlist" style="flex:0 0 60px;">${t('btn_create')}</button>
                <select id="tm-playlist-col-selector" class="tm-col-select" style="margin-left:auto; background:#333;">
                    <option value="1">1${t('stg_col_unit')}</option>
                    <option value="2">2${t('stg_col_unit')}</option>
                    <option value="3">3${t('stg_col_unit')}</option>
                    <option value="4">4${t('stg_col_unit')}</option>
                    <option value="5">5${t('stg_col_unit')}</option>
                </select>
            </div>
            ${names.length === 0 ? `<div class="tm-empty">${t('msg_empty_playlist')}</div>` : `
                <div class="tm-playlist-grid" id="tm-playlist-grid" style="grid-template-columns: repeat(${effectiveCols}, 1fr);">
                    ${names.map((name) => `
                        <div class="tm-playlist-card" data-playlist="${name}" draggable="true">
                            <button class="tm-playlist-delete" data-delete="${name}">×</button>
                            <div class="tm-playlist-name">${name}</div>
                            <div class="tm-playlist-count">${t('time_videos_count', { count: playlists[name].length })}</div>
                        </div>
                    `).join('')}
                </div>
            `}
        `;
        const colSelector = document.getElementById('tm-playlist-col-selector');
        if (colSelector) {
            colSelector.value = currentCols;
            colSelector.addEventListener('change', (e) => {
                const val = parseInt(e.target.value); StorageManager.setPlaylistGridCols(val); loadPlaylists();
            });
        }
        document.getElementById('tm-create-playlist').addEventListener('click', async (e) => {
            e.stopPropagation();
            const input = document.getElementById('tm-new-playlist-name'); const name = input.value.trim();
            if (!name) return;
            if (await StorageManager.createPlaylist(name)) {
                const order = StorageManager.getPlaylistOrder(); order.push(name); StorageManager.setPlaylistOrder(order); loadPlaylists();
            } else alert(t('alert_exists'));
        });
        document.getElementById('tm-new-playlist-name').addEventListener('click', e => e.stopPropagation());
        container.querySelectorAll('[data-playlist]').forEach(card => {
            card.addEventListener('click', (e) => {
                e.stopPropagation(); if (!e.target.dataset.delete) showPlaylistDetail(card.dataset.playlist);
            });
            setupDragAndDrop(card, container);
        });
        container.querySelectorAll('[data-delete]').forEach(btn => {
            btn.addEventListener('click', async (e) => {
                e.stopPropagation();
                if (confirm(t('confirm_delete_playlist', { name: btn.dataset.delete }))) {
                    await StorageManager.deletePlaylist(btn.dataset.delete);
                    const order = StorageManager.getPlaylistOrder().filter(n => n !== btn.dataset.delete);
                    StorageManager.setPlaylistOrder(order); loadPlaylists();
                }
            });
        });
    }

    function setupDragAndDrop(card, container) {
        card.addEventListener('dragstart', (e) => { e.dataTransfer.effectAllowed = 'move'; container.draggedEl = card; card.style.opacity = '0.5'; });
        card.addEventListener('dragend', () => { card.style.opacity = '1'; container.draggedEl = null; });
        card.addEventListener('dragover', (e) => e.preventDefault());
        card.addEventListener('drop', (e) => {
            e.preventDefault(); e.stopPropagation();
            if (container.draggedEl && container.draggedEl !== card) {
                const dragged = container.draggedEl; const target = card; const parent = target.parentNode;
                const temp = document.createTextNode('');
                parent.insertBefore(temp, target); parent.insertBefore(target, dragged); parent.insertBefore(dragged, temp); temp.remove();
                const newOrder = [...container.querySelectorAll('.tm-playlist-card')].map(c => c.dataset.playlist);
                StorageManager.setPlaylistOrder(newOrder);
            }
        });
    }

    async function showPlaylistDetail(name) {
        StorageManager.setActivePlaylist(name);
        const container = document.getElementById('tm-playlists');
        const playlists = await StorageManager.getPlaylists();
        const videos = playlists[name] || [];
        container.innerHTML = `
            <div class="tm-playlist-detail-toolbar">
                <button class="tm-btn-secondary" id="tm-back-to-playlists">${t('btn_back')}</button>
            </div>
            <div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
                <h3 style="color:#e0e0e0; margin:0;">${name}</h3>
                <button class="tm-btn-icon" id="tm-rename-playlist" title="${t('btn_rename')}">✏️</button>
            </div>
            ${videos.length === 0 ? `<div class="tm-empty">${t('msg_empty_videos')}</div>` : `
                <div class="tm-grid-view">
                    ${videos.map(v => generateVideoCard(v, `<button class="tm-card-remove" data-remove-from="${v.id}" title="${t('btn_remove')}">×</button>`, v.timestamp)).join('')}
                </div>
            `}
        `;
        document.getElementById('tm-back-to-playlists').addEventListener('click', (e) => {
            e.stopPropagation();
            const content = e.currentTarget.closest('.tm-content');
            if (content) content.scrollTop = 0;
            StorageManager.setTabScroll('playlists', 0);
            StorageManager.setActivePlaylist(null);
            loadPlaylists();
        });
        document.getElementById('tm-rename-playlist').addEventListener('click', async (e) => {
            e.stopPropagation();
            const newName = prompt(t('prompt_playlist_name'), name);
            if (newName && newName.trim() && newName !== name) {
                const success = await StorageManager.renamePlaylist(name, newName.trim());
                if (success) showPlaylistDetail(newName.trim()); else alert(t('alert_name_used'));
            }
        });
        container.querySelectorAll('[data-remove-from]').forEach(btn => {
            btn.addEventListener('click', async (e) => { e.stopPropagation(); await StorageManager.removeFromPlaylist(name, btn.dataset.removeFrom); showPlaylistDetail(name); });
        });
        attachCardEvents(container);
    }

    function setupFeedTab() {
        _setupFeedLogic('feed', document.getElementById('tm-feed-update'), document.getElementById('tm-feed-status'), document.getElementById('tm-feed-list'), document.getElementById('tm-feed-time-ago'), document.getElementById('tm-feed-time-absolute'));
    }

    function setupFriendsTab() {
        _setupFeedLogic('friends', document.getElementById('tm-friends-update'), document.getElementById('tm-friends-status'), document.getElementById('tm-friends-list'), document.getElementById('tm-friends-time-ago'), document.getElementById('tm-friends-time-absolute'));
    }

    function _setupFeedLogic(type, btn, status, list, timeAgoEl, timeAbsEl) {
        if (!btn) return;

        // 言語設定に合わせてボタンのテキストを更新
        const labelKey = type === 'feed' ? 'tab_feed' : 'tab_friends';
        btn.textContent = `${t(labelKey)} ${t('btn_update')}`;

        const saved = type === 'feed' ? StorageManager.getFeedData() : StorageManager.getFriendsFeedData();
        const lastUpdated = type === 'feed' ? StorageManager.getFeedLastUpdated() : StorageManager.getFriendsLastUpdated();
        const updateTimeDisplay = (timestamp) => {
            if (timestamp > 0) { timeAgoEl.innerText = calcTimeAgo(timestamp); timeAbsEl.innerText = formatDate(timestamp); }
            else { timeAgoEl.innerText = '-'; timeAbsEl.innerText = '-'; }
        };
        updateTimeDisplay(lastUpdated);
        if (saved.length) {
            const privateIds = saved.filter(v => v.isPrivate).map(v => v.id);
            if (privateIds.length > 0) StorageManager.addPrivateToCache(privateIds);
            renderFeed(saved, list);
        }
        const newBtn = btn.cloneNode(true);
        btn.parentNode.replaceChild(newBtn, btn);
        newBtn.disabled = false;
        newBtn.dataset.tmFetching = 'false';
        newBtn.addEventListener('click', async (e) => {
            e.stopPropagation();
            if (newBtn.dataset.tmFetching === 'true') return;
            const setStatus = (message) => { if (status) status.innerText = message; };
            const setBusy = (busy) => {
                newBtn.dataset.tmFetching = busy ? 'true' : 'false';
                newBtn.disabled = !!busy;
            };
            setBusy(true);
            try {
                setStatus(t('msg_fetching'));
                const previousLastUpdated = type === 'feed' ? StorageManager.getFeedLastUpdated() : StorageManager.getFriendsLastUpdated();
                const fetchStartedAt = Date.now();
                const fetchOptions = buildFetchOptions(type, fetchStartedAt, previousLastUpdated);
                updateFetchRangeStatus(type);
                if (!fetchOptions.valid) { setStatus(fetchOptions.message); return; }
                const users = type === 'feed' ? await SubscriptionManager.getFollowedUsers(msg => setStatus(msg)) : await SubscriptionManager.getFriends(msg => setStatus(msg));
                if (users.length === 0) { setStatus(t('msg_no_users')); return; }
                setStatus(t('msg_fetching_users', { count: users.length }));
                let allVideos = [], completed = 0;
                for (let i = 0; i < users.length; i += 5) {
                    const chunk = users.slice(i, i + 5);
                    await Promise.all(chunk.map(async u => { try { allVideos.push(...await SubscriptionManager.getUserVideos(u, fetchOptions)); } catch (e) { } }));
                    completed += chunk.length;
                    setStatus(t('msg_fetching_progress', { current: Math.min(completed, users.length), total: users.length }));
                }
                setStatus(t('msg_complete', { count: allVideos.length }));
                const now = Date.now();
                if (type === 'feed') { StorageManager.setFeedData(allVideos); StorageManager.setFeedLastUpdated(now); }
                else { StorageManager.setFriendsFeedData(allVideos); StorageManager.setFriendsLastUpdated(now); }
                updateTimeDisplay(now); renderFeed(allVideos, list);
            } catch (e) { setStatus(t('msg_error', { msg: e.message })); } finally { setBusy(false); }
        });
    }

    function renderFeed(videos, container) {
        if (!videos.length) { container.innerHTML = `<div class="tm-empty">${t('msg_empty_feed')}</div>`; return; }
        const unique = []; const seen = new Set();
        videos.forEach(v => { if (!seen.has(v.id)) { seen.add(v.id); unique.push(v); } });
        unique.sort((a, b) => parseInt(b.id) - parseInt(a.id));
        container.innerHTML = `<div class="tm-grid-view">${unique.map(v => generateVideoCard(v)).join('')}</div>`;
        attachCardEvents(container);
    }

    function setupVideoPage() {
        const videoId = extractVideoId();
        if (!videoId) return;
        let historyRecorded = false;
        const handleVideoElement = (videoEl) => {
            if (videoEl.dataset.tmEnhanced) return;
            videoEl.dataset.tmEnhanced = 'true';
            const recordHistory = () => {
                if (historyRecorded) return;
                if (!videoEl.duration || isNaN(videoEl.duration) || videoEl.duration === Infinity) {
                    const waitForMeta = () => {
                        if (historyRecorded) return;
                        if (videoEl.duration && !isNaN(videoEl.duration) && videoEl.duration !== Infinity) {
                            save(); videoEl.removeEventListener('loadedmetadata', waitForMeta); videoEl.removeEventListener('durationchange', waitForMeta);
                        }
                    };
                    videoEl.addEventListener('loadedmetadata', waitForMeta); videoEl.addEventListener('durationchange', waitForMeta);
                    return;
                }
                save();
            };
            const save = () => {
                let duration = formatSeconds(videoEl.duration); if (!duration) duration = getDurationFromPlayer();
                const videoData = extractVideoData(duration);
                if (videoData) { StorageManager.addToHistory(videoData); historyRecorded = true; }
            };
            videoEl.addEventListener('play', recordHistory);
            videoEl.addEventListener('timeupdate', () => { if (!historyRecorded && videoEl.currentTime > 0.5) recordHistory(); });
        };
        const v = document.querySelector('video'); if (v) handleVideoElement(v);
        new MutationObserver((mutations) => {
            for (const m of mutations) for (const n of m.addedNodes) {
                if (n.tagName === 'VIDEO') handleVideoElement(n);
                if (n.querySelector) { const v = n.querySelector('video'); if (v) handleVideoElement(v); }
            }
        }).observe(document.body, { childList: true, subtree: true });
        setupLikeObserver(videoId);
        injectPlaylistButton(videoId);
    }

    function setupLikeObserver(videoId) {
        const observer = new MutationObserver(() => {
            const likeCountEl = document.querySelector('#video_likes');
            if (likeCountEl && !likeCountEl.dataset.observed) {
                likeCountEl.dataset.observed = 'true';
                let lastCount = parseInt(likeCountEl.innerText) || 0;
                new MutationObserver(() => {
                    const current = parseInt(likeCountEl.innerText);
                    if (current > lastCount) {
                        const data = extractVideoData();
                        if (data) { StorageManager.addLikedVideo(data); showToast(t('msg_saved_liked')); }
                    }
                    lastCount = current;
                }).observe(likeCountEl, { childList: true, characterData: true, subtree: true });
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function injectPlaylistButton(videoId) {
        const findTarget = () => {
            const selectors = [`#favorite_video_${videoId}`, `#vote_like_${videoId}`, '.fa-heart', '.fa-thumbs-up', '.fa-share-alt'];
            for (const sel of selectors) { const el = document.querySelector(sel); if (el) return el.closest('a, button') || el; }
            return document.querySelector('.video-actions') || document.querySelector('.video-info .pull-right');
        };
        const attemptInject = () => {
            if (document.querySelector('.tm-playlist-btn-inline')) return true;
            const targetBtn = findTarget();
            if (targetBtn && targetBtn.parentNode) {
                const btn = document.createElement('a');
                const btnClass = targetBtn.tagName === 'BUTTON' ? 'btn btn-default' : (targetBtn.className || 'btn btn-default');
                btn.className = btnClass + ' tm-playlist-btn-inline';
                btn.href = 'javascript:void(0);';
                btn.innerHTML = `<i class="fa fa-folder-open"></i><span style="margin-left:4px;">${t('modal_title')}</span>`;
                btn.title = 'TokyoMotion Enhancer List';
                btn.style.marginLeft = '5px';
                btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openPlaylistModal(videoId); });
                targetBtn.parentNode.insertBefore(btn, targetBtn.nextSibling);
                return true;
            }
            return false;
        };
        if (!attemptInject()) {
            const observer = new MutationObserver((mutations, obs) => { if (attemptInject()) obs.disconnect(); });
            observer.observe(document.body, { childList: true, subtree: true });
            setTimeout(() => observer.disconnect(), 10000);
        }
    }

    async function openPlaylistModal(videoId) {
        const existing = document.querySelector('.tm-modal-overlay'); if (existing) existing.remove();
        const currentDuration = getDurationFromPlayer();
        const videoData = extractVideoData(currentDuration);
        const playlists = await StorageManager.getPlaylists();
        const names = Object.keys(playlists);
        const currentCols = StorageManager.getModalCols();
        const overlay = document.createElement('div');
        overlay.className = 'tm-modal-overlay';
        overlay.innerHTML = `
            <div class="tm-modal-content">
                <div class="tm-modal-header">
                    <div style="display:flex; align-items:center; gap:10px;">
                        <span>${t('modal_title')}</span>
                        <select id="tm-col-selector" class="tm-col-select" title="Cols">
                            <option value="1">1${t('stg_col_unit')}</option>
                            <option value="2">2${t('stg_col_unit')}</option>
                            <option value="3">3${t('stg_col_unit')}</option>
                            <option value="4">4${t('stg_col_unit')}</option>
                            <option value="5">5${t('stg_col_unit')}</option>
                        </select>
                    </div>
                    <button type="button" class="tm-panel-close" aria-label="${t('btn_close')}" title="${t('btn_close')}">${UI_ICONS.close}</button>
                </div>
                <div class="tm-playlist-list" style="grid-template-columns: repeat(${currentCols}, 1fr);">
                    ${names.map(name => {
            const checked = playlists[name].some(v => v.id === videoId) ? 'checked' : '';
            return `<label class="tm-playlist-item"><input type="checkbox" data-name="${name}" ${checked}> ${name}</label>`;
        }).join('')}
                </div>
                <div class="tm-modal-footer">
                    <div class="tm-new-playlist-form">
                        <input type="text" class="tm-input" id="tm-modal-new-name" placeholder="${t('placeholder_new_playlist')}" style="margin:0;">
                        <button class="tm-btn-primary" id="tm-modal-create">${t('btn_create')}</button>
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);
        const updateModalSize = (cols) => {
            const content = overlay.querySelector('.tm-modal-content');
            const list = overlay.querySelector('.tm-playlist-list');
            list.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
            let width = 320;
            if (cols === 2) width = 450; if (cols === 3) width = 600; if (cols === 4) width = 750; if (cols === 5) width = 900;
            content.style.width = `${width}px`;
        };
        updateModalSize(currentCols);
        overlay.querySelector('#tm-col-selector').value = currentCols;
        overlay.querySelector('.tm-panel-close').addEventListener('click', () => overlay.remove());
        overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
        overlay.querySelector('#tm-col-selector').addEventListener('change', (e) => {
            const val = parseInt(e.target.value); StorageManager.setModalCols(val); updateModalSize(val);
        });
        overlay.querySelectorAll('input[type="checkbox"]').forEach(cb => {
            cb.addEventListener('change', async (e) => {
                const name = e.target.dataset.name;
                if (e.target.checked) { await StorageManager.addToPlaylist(name, videoData); showToast(t('msg_added_to', { name })); }
                else { await StorageManager.removeFromPlaylist(name, videoId); showToast(t('msg_removed_from', { name })); }
            });
        });
        overlay.querySelector('#tm-modal-create').addEventListener('click', async () => {
            const name = overlay.querySelector('#tm-modal-new-name').value.trim();
            if (name && await StorageManager.createPlaylist(name)) {
                await StorageManager.addToPlaylist(name, videoData); showToast(t('msg_created_added', { name })); overlay.remove();
            }
        });
    }

    function attemptAutoLogin() {
        if (!StorageManager.isAutoLoginEnabled()) return;
        const loginLink = document.querySelector('a[href="#login-modal"]'); if (!loginLink) return;
        const modal = document.getElementById('login-modal');
        if (!(modal && (modal.style.display === 'block' || modal.classList.contains('in')))) loginLink.click();
        let attempts = 0;
        const interval = setInterval(() => {
            const user = document.getElementById('login_username'); const pass = document.getElementById('login_password'); const btn = document.getElementById('login_submit');
            if (user && pass && btn) {
                if (!user.value && document.activeElement !== user) { user.focus(); user.click(); }
                else if (user.value && !pass.value && document.activeElement !== pass) { pass.focus(); pass.click(); }
                if (user.value && pass.value) { clearInterval(interval); showToast(t('msg_auto_login')); btn.click(); }
            }
            if (++attempts > 50) clearInterval(interval);
        }, 100);
    }

    function exportData() {
        const data = {
            liked: GM_getValue('likedVideos', []),
            history: GM_getValue('history', []),
            playlists: GM_getValue('playlists', {}),
            playlistOrder: GM_getValue('playlistOrder', []),
            settings: {
                defaultTab: GM_getValue('defaultTab', 'liked'),
                autoLogin: GM_getValue('autoLoginEnabled', false),
                uiMode: StorageManager.getUIMode(),
                tabOrder: GM_getValue('tabOrder', DEFAULT_TAB_ORDER),
                tabVisibility: StorageManager.getTabVisibility(),
                panelState: GM_getValue('panelState', null),
                btnPosition: GM_getValue('btnPosition', null),
                mobilePanelState: GM_getValue('mobilePanelState', null),
                mobileBtnPosition: GM_getValue('mobileBtnPosition', null),
                contentFilterEnabled: GM_getValue('contentFilterEnabled', true),
                blockedUploaders: GM_getValue('blockedUploaders', []),
                mutedTitleTerms: GM_getValue('mutedTitleTerms', []),
                feedFetchModes: GM_getValue('feedFetchModes', StorageManager.getDefaultFetchModes()),
                friendsFetchModes: GM_getValue('friendsFetchModes', StorageManager.getDefaultFetchModes()),
                feedMaxDays: GM_getValue('feedMaxDays', 3),
                friendsMaxDays: GM_getValue('friendsMaxDays', 3),
                feedMaxPages: GM_getValue('feedMaxPages', 1),
                friendsMaxPages: GM_getValue('friendsMaxPages', 1)
            }
        };
        const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a'); a.href = url;
        a.download = `tokyomotion_backup_${new Date().toISOString().slice(0, 10)}.json`;
        a.click(); URL.revokeObjectURL(url);
    }

    function importData() {
        const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json';
        input.onchange = (e) => {
            const file = e.target.files[0]; if (!file) return;
            const reader = new FileReader();
            reader.onload = (ev) => {
                try {
                    const data = JSON.parse(ev.target.result);
                    if (confirm(t('confirm_overwrite'))) {
                        if (data.liked) GM_setValue('likedVideos', data.liked);
                        if (data.history) GM_setValue('history', data.history);
                        if (data.playlists) GM_setValue('playlists', data.playlists);
                        if (data.playlistOrder) GM_setValue('playlistOrder', data.playlistOrder);
                        if (data.settings) {
                            GM_setValue('defaultTab', data.settings.defaultTab);
                            GM_setValue('autoLoginEnabled', data.settings.autoLogin);
                            if (data.settings.uiMode) StorageManager.setUIMode(data.settings.uiMode);
                            if (data.settings.tabOrder) GM_setValue('tabOrder', data.settings.tabOrder);
                            if (data.settings.tabVisibility) StorageManager.setTabVisibility(data.settings.tabVisibility);
                            if (data.settings.panelState) GM_setValue('panelState', data.settings.panelState);
                            if (data.settings.btnPosition) GM_setValue('btnPosition', data.settings.btnPosition);
                            if (data.settings.mobilePanelState) GM_setValue('mobilePanelState', data.settings.mobilePanelState);
                            if (data.settings.mobileBtnPosition) GM_setValue('mobileBtnPosition', data.settings.mobileBtnPosition);
                            if (typeof data.settings.contentFilterEnabled === 'boolean') GM_setValue('contentFilterEnabled', data.settings.contentFilterEnabled);
                            if (Array.isArray(data.settings.blockedUploaders)) GM_setValue('blockedUploaders', data.settings.blockedUploaders);
                            if (Array.isArray(data.settings.mutedTitleTerms)) GM_setValue('mutedTitleTerms', data.settings.mutedTitleTerms);
                            if (data.settings.feedFetchModes) GM_setValue('feedFetchModes', StorageManager.normalizeFetchModes(data.settings.feedFetchModes));
                            if (data.settings.friendsFetchModes) GM_setValue('friendsFetchModes', StorageManager.normalizeFetchModes(data.settings.friendsFetchModes));
                            if (data.settings.feedMaxDays) GM_setValue('feedMaxDays', data.settings.feedMaxDays);
                            if (data.settings.friendsMaxDays) GM_setValue('friendsMaxDays', data.settings.friendsMaxDays);
                            if (data.settings.feedMaxPages) GM_setValue('feedMaxPages', data.settings.feedMaxPages);
                            if (data.settings.friendsMaxPages) GM_setValue('friendsMaxPages', data.settings.friendsMaxPages);
                        }
                        alert(t('msg_import_done')); location.reload();
                    }
                } catch (err) { alert(t('msg_import_error', { msg: err })); }
            };
            reader.readAsText(file);
        };
        input.click();
    }

    function clearAllData() {
        if (confirm(t('confirm_clear_all'))) {
            GM_setValue('likedVideos', []); GM_setValue('history', []); GM_setValue('playlists', {}); GM_setValue('feedData', []); GM_setValue('friendsFeedData', []);
            alert(t('msg_data_cleared')); location.reload();
        }
    }

    function applyVideoGridCols() {
        const configuredCols = Math.max(1, parseInt(StorageManager.getVideoGridCols(), 10) || 1);
        const cols = isMobileUIMode() ? Math.min(configuredCols, 2) : configuredCols;
        document.documentElement.style.setProperty('--tm-video-cols', cols);
    }

    function init() {
        if (window.self !== window.top) return;

        // 終了時間の記録
        window.addEventListener('beforeunload', () => {
            StorageManager.setLastClosedTime(Date.now());
        });

        // 起動時のリセットチェック
        const lastClosed = StorageManager.getLastClosedTime();
        if (lastClosed > 0) {
            const diff = Date.now() - lastClosed;
            const threshold = getScrollResetMs();
            if (diff > threshold) {
                // リセット対象のデータをクリア
                const tabsToReset = ['liked', 'history', 'playlists', 'feed', 'friends'];
                tabsToReset.forEach(tab => {
                    StorageManager.setTabScroll(tab, 0);
                });
                StorageManager.setActivePlaylist(null);

                // 次回起動時にリセット通知を出すためのフラグを立てる(オプション)
                // 今回は単純にすべてのタブスクロールを0にするため、UI生成時にそれが反映されるはず
                // ただし、パネルがまだ生成されていないので、パネル生成後に適用される必要がある。
                // restoreScrollPosition はパネル生成後に呼ばれるので、ここで値を0にしておけば0が復元される。

                // 通知はDOMがないので出せないが、コンソールに出しておく
                console.log(`[TokyoMotion Enhancer] Startup scroll reset triggered. (Closed for ${diff}ms > ${threshold}ms)`);
                // DOMが準備できているはずなので通知を試みる
                if (document.body) {
                    setTimeout(() => showToast(t('msg_scroll_reset')), 500); // UI描画と被らないよう少し遅延
                } else {
                    document.addEventListener('DOMContentLoaded', () => setTimeout(() => showToast(t('msg_scroll_reset')), 500));
                }
            }
        }

        createMainUI();
        if (location.pathname.startsWith('/video/')) setupVideoPage();
        if (StorageManager.isAutoLoginEnabled()) {
            if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', attemptAutoLogin);
            else attemptAutoLogin();
        }
        PrivateScanner.startObserver();
        ContentFilter.start();
    }

    init();
})();