Sort Youtube Playlist by Duration (Advanced)

Sorts youtube playlist by duration

// ==UserScript==
// @name              Sort Youtube Playlist by Duration (Advanced)
// @namespace         https://github.com/L0garithmic/ytsort/
// @version           4.5.0
// @description       Sorts youtube playlist by duration
// @author            L0garithmic
// @license           GPL-2.0-only
// @match             http://*.youtube.com/*
// @match             https://*.youtube.com/*
// @supportURL        https://github.com/L0garithmic/ytsort/
// @grant             none
// @run-at            document-idle
// ==/UserScript==

/**
 *  Changelog 10/12/2025 (v4.5.0)
 *  - Added Settings Panel for persistent configuration management
 *  - Settings saved to localStorage and persist across sessions
 *  - Settings include: default sort mode, auto-scroll, scroll retry time, log preferences
 *  - Added Dry Run Mode to preview sort order without applying changes
 *  - Dry Run shows before/after comparison with confirmation prompt
 *  - Safer for large playlists - verify sort logic before committing
 *  - Settings panel accessible via dedicated button
 *
 *  Changelog 10/12/2025 (v4.4.0)
 *  - Added "Export" button to export playlist data as CSV
 *  - Export includes: position, title, duration (formatted & seconds), URL, video ID
 *  - Fixed move counter to show actual move number vs total moves needed
 *  - Move counter now shows (1/10) instead of (9/269) for better clarity
 *  - Auto-generates filename with playlist ID and date
 *  - CSV properly escapes special characters in video titles
 *
 *  Changelog 10/12/2025 (v4.3.0)
 *  - Added "Only include specific lengths" filter feature
 *  - Filter inputs now use minutes instead of seconds for easier configuration
 *  - Title tiebreaker always enabled (videos with same duration sorted alphabetically)
 *  - Filtered videos are moved to the end of the playlist
 *  - Filter settings shown in log when sorting starts
 *
 *  Changelog 10/12/2025 (v4.2.0)
 *  - Added "Stats" button (compact size) for playlist analysis
 *  - Shows total duration, average length, shortest/longest videos
 *  - Removed distribution chart for cleaner output
 *  - Auto-opens log console when Stats is clicked
 *  - Counts unavailable/private videos
 *  - Provides quick insights before sorting
 *
 *  Changelog 10/12/2025 (v4.1.1)
 *  - Fixed random scrolling behavior after sort completion
 *  - Increased log retention from 100 to 1000 messages
 *  - Added warning about not switching back to YouTube's auto-sort methods
 *  - Added version number display (visible when expanded)
 *  - Version number now included in verbose logs for troubleshooting
 *  - Scroll to top after sort completes to stabilize view
 *
 *  Changelog 10/10/2025 (v4.1.0)
 *  - Added "Copy Console" button to copy all logs to clipboard
 *  - MAJOR FIX: Completely rewrote lazy loading prevention
 *  - Now reloads entire playlist before each sort iteration
 *  - Scrolls to bottom then top to ensure all videos are loaded
 *  - Verifies video count before each sort
 *  - Reduces sort iterations significantly (more reliable)
 *  - Better error messages when playlist cannot be maintained
 *
 *  Changelog 10/10/2025 (v4.0.3)
 *  - Added scrollable console log with timestamps
 *  - Log auto-scrolls to show latest messages
 *  - Added Clear Log button
 *  - Enhanced logging with emojis and visual separators
 *  - All actions are now logged in real-time
 *  - Console shows up to 100 most recent messages
 *  - Added progress indicators for sorting steps
 *
 *  Changelog 10/10/2025 (v4.0.2)
 *  - Fixed YouTube's lazy loading causing videos to unload during sorting
 *  - Added smart viewport positioning to keep videos loaded
 *  - Both modes now check for new content before sorting
 *  - Waits for stable state (no new videos for 3 attempts) before sorting
 *  - Increased delays between sort operations for better stability
 *  - Better error handling when playlist state cannot be maintained
 *
 *  Changelog 10/10/2025 (v4.0.1)
 *  - Fixed annoying continuous scroll/load/refresh loop
 *  - Added max retry limit to prevent infinite scrolling
 *  - Improved early exit when no progress is detected (3 attempts)
 *  - Better scroll detection to stop when already at bottom
 *  - Clear feedback for "Sort only loaded" mode
 *
 *  Changelog 10/10/2025 (v4.0.0)
 *  - Fixed video count detection to work with new YouTube layout
 *  - Improved "Sort all" mode to reliably load all videos in playlist
 *  - Enhanced progress feedback during video loading
 *  - Modernized UI with better styling (rounded buttons, gradients, smooth transitions)
 *  - Added dark mode support
 *  - Better retry logic with progress tracking
 *
 *  Changelog 08/08/2024
 *  - Attempt to address the most serious of buggy code, script should now work in all but the longest playlist.
 */

/* jshint esversion: 8 */

(function () {
    'use strict';

    const SCRIPT_VERSION = '4.5.0';

    // Settings management with localStorage
    const SETTINGS_KEY = 'yt_playlist_sorter_settings';

    /**
     * Default settings structure
     * @type {Object}
     */
    const DEFAULT_SETTINGS = {
        sortMode: 'asc',
        autoScrollInitialVideoList: 'true',
        scrollLoopTime: 600,
        logVisible: false,
        dryRunEnabled: false,
        filterEnabled: false,
        filterMinDuration: 0,
        filterMaxDuration: 36000
    };

    /**
     * Load settings from localStorage
     * @returns {Object} Settings object
     */
    const loadSettings = () => {
        try {
            const stored = localStorage.getItem(SETTINGS_KEY);
            if (stored) {
                const parsed = JSON.parse(stored);
                return { ...DEFAULT_SETTINGS, ...parsed };
            }
        } catch (e) {
            console.error('Failed to load settings:', e);
        }
        return { ...DEFAULT_SETTINGS };
    };

    /**
     * Save settings to localStorage
     * @param {Object} settings - Settings object to save
     * @returns {void}
     */
    const saveSettings = (settings) => {
        try {
            localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
        } catch (e) {
            console.error('Failed to save settings:', e);
        }
    };

    /**
     * Get current settings from UI state
     * @returns {Object} Current settings
     */
    const getCurrentSettings = () => {
        return {
            sortMode,
            autoScrollInitialVideoList,
            scrollLoopTime,
            logVisible,
            dryRunEnabled,
            filterEnabled,
            filterMinDuration,
            filterMaxDuration
        };
    };

    /**
     * Apply settings to UI state
     * @param {Object} settings - Settings to apply
     * @returns {void}
     */
    const applySettings = (settings) => {
        sortMode = settings.sortMode;
        autoScrollInitialVideoList = settings.autoScrollInitialVideoList;
        scrollLoopTime = settings.scrollLoopTime;
        logVisible = settings.logVisible;
        dryRunEnabled = settings.dryRunEnabled;
        filterEnabled = settings.filterEnabled;
        filterMinDuration = settings.filterMinDuration;
        filterMaxDuration = settings.filterMaxDuration;
    };

    // Load settings on initialization
    const savedSettings = loadSettings();

    /**
     * Wait for element(s) to appear in DOM using MutationObserver
     * @param {string} selector - CSS selector to wait for
     * @param {boolean} [multiple=false] - Whether to wait for multiple elements
     * @param {Function} [callback=()=>{}] - Callback function to execute when element(s) found
     * @returns {void}
     */
    const onElementReady = (selector, multiple = false, callback = () => { }) => {
        const runCallback = () => {
            if (multiple) {
                const elements = document.querySelectorAll(selector);
                if (elements.length) {
                    callback(elements);
                    return true;
                }
            } else {
                const element = document.querySelector(selector);
                if (element) {
                    callback(element);
                    return true;
                }
            }
            return false;
        };

        if (runCallback()) {
            return;
        }

        const observer = new MutationObserver(() => {
            if (runCallback()) {
                observer.disconnect();
            }
        });

        const root = document.documentElement || document;
        observer.observe(root, { childList: true, subtree: true });

        setTimeout(() => observer.disconnect(), 30000);
    };

    /**
     * Variables and constants
     */
    const css =
        `
        .sort-playlist-wrapper {
            margin-top: 12px;
        }
        .sort-playlist-details {
            border: 1px solid rgba(48,48,48,0.4);
            border-radius: 12px;
            background: rgba(0,0,0,0.03);
            overflow: hidden;
        }
        .sort-playlist-summary {
            list-style: none;
            padding: 12px 16px;
            font-weight: 600;
            display: flex;
            align-items: center;
            gap: 8px;
            cursor: pointer;
            color: #0f0f0f;
            user-select: none;
            justify-content: space-between;
        }
        .sort-playlist-summary::-webkit-details-marker {
            display: none;
        }
        .sort-playlist-summary::before {
            content: '▶';
            font-size: 10px;
            transition: transform 0.2s ease;
            flex-shrink: 0;
            display: flex;
            align-items: center;
            transform: translateY(-1px);
        }
        .sort-playlist-details[open] .sort-playlist-summary::before {
            content: '▼';
        }
        .sort-playlist-title {
            flex: 1;
        }
        .sort-playlist-version {
            font-size: 11px;
            font-weight: 500;
            color: #606060;
            opacity: 0;
            transition: opacity 0.2s ease;
            pointer-events: none;
            margin-left: auto;
        }
        .sort-playlist-details[open] .sort-playlist-version {
            opacity: 1;
        }
        .sort-playlist-content {
            padding: 12px 16px 16px;
        }
        .sort-playlist-div {
            font-size: 13px;
            padding: 8px 4px;
            font-family: "Roboto", "Arial", sans-serif;
        }
        .sort-button-wl {
            border: none;
            border-radius: 18px;
            padding: 10px 16px;
            cursor: pointer;
            font-weight: 500;
            font-size: 14px;
            transition: all 0.2s ease;
            box-shadow: none;
            white-space: nowrap;
        }
        .sort-button-wl-default {
            background: rgba(255, 255, 255, 0.1);
            color: #fff;
            font-weight: 500;
            border: 1px solid rgba(255, 255, 255, 0.1);
        }
        .sort-button-wl-stop {
            background: rgba(255, 255, 255, 0.1);
            color: #ff4444;
            font-weight: 600;
            border: 1px solid rgba(255, 255, 255, 0.1);
        }
        .sort-button-wl-default:hover {
            background: rgba(255, 255, 255, 0.15);
            border: 1px solid rgba(255, 255, 255, 0.3);
            transform: translateY(-1px);
        }
        .sort-button-wl-stop:hover {
            background: rgba(255, 68, 68, 0.15);
            border: 1px solid rgba(255, 68, 68, 0.3);
            transform: translateY(-1px);
        }
        .sort-button-wl-default:active {
            background: rgba(62, 166, 255, 0.25);
            transform: translateY(0);
        }
        .sort-button-wl-stop:active {
            background: rgba(255, 68, 68, 0.25);
            transform: translateY(0);
        }
        .sort-select {
            border: 1px solid #303030;
            border-radius: 8px;
            padding: 8px 12px;
            background-color: #f9f9f9;
            color: #0f0f0f;
            font-size: 13px;
            cursor: pointer;
            transition: all 0.2s ease;
        }
        .sort-select:hover {
            border-color: #065fd4;
            background-color: #fff;
        }
        .sort-select:focus {
            outline: none;
            border-color: #065fd4;
            box-shadow: 0 0 0 2px rgba(6,95,212,0.1);
        }
        .sort-number-input {
            border: 1px solid #303030;
            border-radius: 8px;
            padding: 8px 12px;
            background-color: #f9f9f9;
            color: #0f0f0f;
            font-size: 13px;
            width: 100px;
            transition: all 0.2s ease;
        }
        .sort-number-input:hover {
            border-color: #065fd4;
            background-color: #fff;
        }
        .sort-number-input:focus {
            outline: none;
            border-color: #065fd4;
            box-shadow: 0 0 0 2px rgba(6,95,212,0.1);
        }
        .sort-log {
            padding: 12px;
            margin-top: 8px;
            border-radius: 8px;
            background-color: #0f0f0f;
            color: #f1f1f1;
            font-family: 'Roboto Mono', monospace;
            font-size: 12px;
            line-height: 1.5;
            box-shadow: inset 0 1px 3px rgba(0,0,0,0.3);
            max-height: 200px;
            overflow-y: auto;
            white-space: pre-wrap;
            word-wrap: break-word;
        }
        .sort-log::-webkit-scrollbar {
            width: 8px;
        }
        .sort-log::-webkit-scrollbar-track {
            background: #1a1a1a;
            border-radius: 4px;
        }
        .sort-log::-webkit-scrollbar-thumb {
            background: #3ea6ff;
            border-radius: 4px;
        }
        .sort-log::-webkit-scrollbar-thumb:hover {
            background: #4db3ff;
        }
        .sort-log-entry {
            margin-bottom: 4px;
            padding: 2px 0;
        }
        .sort-log.sort-log-empty {
            color: #888;
        }
        .sort-log-timestamp {
            color: #888;
            margin-right: 8px;
        }
        .sort-margin-right-3px {
            margin-right: 8px;
        }
        .sort-input-label {
            display: inline-block;
            margin-right: 6px;
            color: #0f0f0f;
            font-weight: 500;
        }
        .sort-checkbox-container {
            display: inline-flex;
            align-items: center;
            margin-bottom: 4px;
            margin-right: 4px;
            font-size: 11px;
            color: #0f0f0f;
            cursor: pointer;
        }
        .sort-checkbox {
            margin-right: 4px;
            cursor: pointer;
        }
        @media (prefers-color-scheme: dark) {
            .sort-playlist-details {
                border-color: rgba(255,255,255,0.1);
                background: rgba(255,255,255,0.03);
            }
            .sort-playlist-summary {
                color: #f1f1f1;
            }
            .sort-playlist-version {
                color: #aaa;
            }
            .sort-select, .sort-number-input {
                background-color: #272727;
                color: #f1f1f1;
                border-color: #4f4f4f;
            }
            .sort-select:hover, .sort-number-input:hover {
                background-color: #3f3f3f;
                border-color: #3ea6ff;
            }
            .sort-select:focus, .sort-number-input:focus {
                border-color: #3ea6ff;
                box-shadow: 0 0 0 2px rgba(62,166,255,0.1);
            }
            .sort-input-label {
                color: #f1f1f1;
            }
            .sort-checkbox-container {
                color: #f1f1f1;
            }
            .sort-button-wl-default {
                background: rgba(255, 255, 255, 0.08);
                border-color: rgba(255, 255, 255, 0.15);
            }
            .sort-button-wl-default:hover {
                background: rgba(255, 255, 255, 0.15);
            }
            .sort-dry-run-modal {
                background-color: #212121;
                border-color: #4f4f4f;
            }
            .sort-dry-run-title {
                color: #f1f1f1;
                border-bottom-color: #4f4f4f;
            }
            .sort-dry-run-comparison {
                background-color: #181818;
                border-color: #4f4f4f;
            }
            .sort-dry-run-column h4 {
                color: #f1f1f1;
            }
            .sort-dry-run-video {
                background-color: #272727;
                border-color: #3f3f3f;
                color: #f1f1f1;
            }
        }
        .sort-settings-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #212121;
            border: 1px solid #4f4f4f;
            border-radius: 12px;
            padding: 24px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.5);
            z-index: 10000;
            max-width: 500px;
            width: 90%;
            max-height: 80vh;
            overflow-y: auto;
        }
        .sort-settings-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.7);
            z-index: 9999;
        }
        .sort-settings-title {
            font-size: 18px;
            font-weight: 600;
            margin-bottom: 16px;
            padding-bottom: 12px;
            border-bottom: 1px solid #4f4f4f;
            color: #f1f1f1;
        }
        .sort-settings-section {
            margin-bottom: 16px;
        }
        .sort-settings-section-title {
            font-size: 13px;
            font-weight: 600;
            color: #aaa;
            margin-bottom: 8px;
            text-transform: uppercase;
        }
        .sort-settings-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 12px;
        }
        .sort-settings-label {
            font-size: 14px;
            color: #f1f1f1;
            font-weight: normal;
        }
        .sort-settings-buttons {
            display: flex;
            gap: 8px;
            margin-top: 20px;
            justify-content: flex-end;
        }
        .sort-settings-buttons button {
            font-weight: normal;
        }
        .sort-settings-modal .sort-select,
        .sort-settings-modal .sort-number-input,
        .sort-settings-modal .sort-checkbox {
            background-color: #272727;
            color: #f1f1f1;
            border-color: #4f4f4f;
        }
        .sort-settings-modal .sort-select {
            width: 160px;
        }
        .sort-settings-modal .sort-number-input {
            width: 135px;
        }
        .sort-settings-modal .sort-select:hover,
        .sort-settings-modal .sort-number-input:hover {
            background-color: #3f3f3f;
            border-color: #3ea6ff;
        }
        .sort-settings-modal .sort-select:focus,
        .sort-settings-modal .sort-number-input:focus {
            outline: none;
            border-color: #3ea6ff;
            box-shadow: 0 0 0 2px rgba(62,166,255,0.2);
        }
        .sort-dry-run-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            border: 1px solid #ccc;
            border-radius: 12px;
            padding: 24px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
            z-index: 10001;
            max-width: 800px;
            width: 95%;
            max-height: 85vh;
            overflow-y: auto;
        }
        .sort-dry-run-title {
            font-size: 18px;
            font-weight: 600;
            margin-bottom: 16px;
            padding-bottom: 12px;
            border-bottom: 1px solid #e0e0e0;
            color: #0f0f0f;
        }
        .sort-dry-run-info {
            margin-bottom: 16px;
            padding: 12px;
            background: #f0f7ff;
            border-radius: 8px;
            font-size: 13px;
            color: #065fd4;
        }
        .sort-dry-run-comparison {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 16px;
            margin-bottom: 20px;
            max-height: 400px;
            overflow-y: auto;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            padding: 12px;
            background: #fafafa;
        }
        .sort-dry-run-column {
            min-width: 0;
        }
        .sort-dry-run-column h4 {
            font-size: 14px;
            font-weight: 600;
            margin-bottom: 8px;
            position: sticky;
            top: 0;
            background: #fafafa;
            padding: 8px 0;
            color: #0f0f0f;
        }
        .sort-dry-run-video {
            font-size: 12px;
            padding: 6px 8px;
            margin-bottom: 4px;
            background: white;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .sort-dry-run-buttons {
            display: flex;
            gap: 8px;
            justify-content: flex-end;
        }
    `;

    const modeAvailable = [
        { value: 'asc', label: 'Shortest First' },
        { value: 'desc', label: 'Longest First' }
    ];

    const autoScrollOptions = [
        { value: 'true', label: 'Sort all' },
        { value: 'false', label: 'Sort only loaded' }
    ];

    // NEW YouTube architecture selectors
    const NEW_PAGE_HEADER_SELECTOR = 'yt-flexible-actions-view-model';
    const NEW_ACTIONS_ROW_SELECTOR = '.ytFlexibleActionsViewModelActionRow';

    // OLD YouTube architecture selectors
    const PLAYLIST_HEADER_SELECTOR = 'ytd-playlist-header-renderer';
    const PLAYLIST_ACTIONS_SELECTOR = 'ytd-playlist-header-renderer #actions';
    const PLAYLIST_VIDEO_LIST_SELECTOR = 'ytd-playlist-video-list-renderer';
    const PLAYLIST_VIDEO_ITEM_SELECTOR = 'ytd-playlist-video-renderer';

    const debug = false;

    const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));

    let scrollLoopTime = savedSettings.scrollLoopTime;

    let sortMode = savedSettings.sortMode;

    let autoScrollInitialVideoList = savedSettings.autoScrollInitialVideoList;

    const DEFAULT_LOG_MESSAGE = '[Ready] Waiting for sort action...';
    const MAX_LOG_MESSAGES = 1000; // Increased from 100 to retain more logs

    let log = document.createElement('div');
    let logEntries = []; // Store all log messages with metadata
    let verboseMode = false; // Default to non-verbose mode
    let autoScrollLog = true; // Default to auto-scroll enabled
    let logVisible = savedSettings.logVisible; // Load from settings

    let stopSort = false;

    // Global move counter for tracking progress across all sort iterations
    let globalMoveCounter = 0;
    let globalTotalMoves = 0;

    let dryRunEnabled = savedSettings.dryRunEnabled; // Load from settings

    // Filter settings (load from saved settings)
    let filterEnabled = savedSettings.filterEnabled;
    let filterMinDuration = savedSettings.filterMinDuration; // in seconds
    let filterMaxDuration = savedSettings.filterMaxDuration; // in seconds (max)

    // Multi-criteria sorting
    let useTitleTiebreaker = true; // Always enabled by default

    /**
     * Get all playlist video pairs (drag handle, anchor, item)
     * @returns {{drag: Element, anchor: Element, item: Element}[]} Array of video pair objects
     */
    const getPlaylistVideoPairs = () => {
        const scope = document.querySelector(PLAYLIST_VIDEO_LIST_SELECTOR) || document;
        const videoItems = scope.querySelectorAll(PLAYLIST_VIDEO_ITEM_SELECTOR);
        const pairs = [];

        videoItems.forEach(item => {
            const drag = item.querySelector('yt-icon#reorder');
            const anchor = item.querySelector('a#thumbnail');
            if (drag && anchor) {
                pairs.push({ drag, anchor, item });
            }
        });

        return pairs;
    };

    /**
     * Get video duration in seconds from a video anchor element
     * @param {Element} anchor - Video anchor element
     * @return {number|null} Duration in seconds, or null if unavailable
     */
    const getVideoDuration = (anchor) => {
        const timeSpan = anchor.querySelector("#text");
        if (!timeSpan || !timeSpan.innerText.trim()) {
            return null;
        }

        const timeDigits = timeSpan.innerText.trim().split(":").reverse();
        if (timeDigits.length === 1) {
            return null;
        }

        let seconds = parseInt(timeDigits[0]) || 0;
        if (timeDigits[1]) seconds += parseInt(timeDigits[1]) * 60;
        if (timeDigits[2]) seconds += parseInt(timeDigits[2]) * 3600;

        return seconds;
    };

    /**
     * Get video title from a video item element
     * @param {Element} item - Video item element
     * @return {string} Video title
     */
    const getVideoTitle = (item) => {
        const titleElement = item.querySelector('#video-title');
        return titleElement ? titleElement.innerText.trim() : '';
    };

    /**
     * Check if a video passes the duration filter
     * @param {number|null} duration - Video duration in seconds
     * @return {boolean} True if video passes filter
     */
    const passesFilter = (duration) => {
        if (!filterEnabled) return true;
        if (duration === null) return false; // Exclude videos without duration
        return duration >= filterMinDuration && duration <= filterMaxDuration;
    };

    /**
     * Fire a mouse event on an element
     * @param {string} type - Event type (e.g., 'mousemove', 'mousedown', 'dragstart')
     * @param {Element} elem - Target element to fire event on
     * @param {number} centerX - X coordinate for the event
     * @param {number} centerY - Y coordinate for the event
     * @returns {void}
     */
    let fireMouseEvent = (type, elem, centerX, centerY) => {
        const event = new MouseEvent(type, {
            view: window,
            bubbles: true,
            cancelable: true,
            clientX: centerX,
            clientY: centerY
        });

        elem.dispatchEvent(event);
    };

    /**
     * Simulate drag and drop operation between two elements
     * Fires a sequence of mouse events to replicate user drag and drop
     * @see https://ghostinspector.com/blog/simulate-drag-and-drop-javascript-casperjs/
     * @param {Element} elemDrag - Element to drag from
     * @param {Element} elemDrop - Element to drop onto
     * @returns {void}
     */
    let simulateDrag = (elemDrag, elemDrop) => {
        // calculate positions
        let pos = elemDrag.getBoundingClientRect();
        let center1X = Math.floor((pos.left + pos.right) / 2);
        let center1Y = Math.floor((pos.top + pos.bottom) / 2);
        pos = elemDrop.getBoundingClientRect();
        let center2X = Math.floor((pos.left + pos.right) / 2);
        let center2Y = Math.floor((pos.top + pos.bottom) / 2);

        // mouse over dragged element and mousedown
        fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
        fireMouseEvent("mouseenter", elemDrag, center1X, center1Y);
        fireMouseEvent("mouseover", elemDrag, center1X, center1Y);
        fireMouseEvent("mousedown", elemDrag, center1X, center1Y);

        // start dragging process over to drop target
        fireMouseEvent("dragstart", elemDrag, center1X, center1Y);
        fireMouseEvent("drag", elemDrag, center1X, center1Y);
        fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
        fireMouseEvent("drag", elemDrag, center2X, center2Y);
        fireMouseEvent("mousemove", elemDrop, center2X, center2Y);

        // trigger dragging process on top of drop target
        fireMouseEvent("mouseenter", elemDrop, center2X, center2Y);
        fireMouseEvent("dragenter", elemDrop, center2X, center2Y);
        fireMouseEvent("mouseover", elemDrop, center2X, center2Y);
        fireMouseEvent("dragover", elemDrop, center2X, center2Y);

        // release dragged element on top of drop target
        fireMouseEvent("drop", elemDrop, center2X, center2Y);
        fireMouseEvent("dragend", elemDrag, center2X, center2Y);
        fireMouseEvent("mouseup", elemDrag, center2X, center2Y);
    };

    /**
     * Scroll to keep a specific video in view (to prevent lazy unloading)
     * @param {number} videoIndex - Index of video to keep in view
     * @param {NodeList|Element[]} allAnchors - All video anchor elements
     * @returns {void}
     */
    let keepVideoInView = (videoIndex, allAnchors) => {
        if (!allAnchors || videoIndex >= allAnchors.length) return;

        try {
            // Scroll to keep the video in the middle of the viewport
            const targetElement = allAnchors[videoIndex];
            if (targetElement) {
                targetElement.scrollIntoView({ behavior: 'auto', block: 'center' });
            }
        } catch (e) {
            // Ignore errors if element is not found
            if (debug) console.log("Could not scroll to video:", e);
        }
    };

    /**
     * Scroll automatically to the bottom of the page (or specific scroll position)
     * @param {number|null} scrollTop - Target scroll position (null for bottom of page)
     * @returns {Promise<void>} Resolves when scrolling is complete
     */
    let autoScroll = async (scrollTop = null) => {
        let element = document.scrollingElement;
        if (!element) return;

        let currentScroll = element.scrollTop;
        let scrollDestination = scrollTop !== null ? scrollTop : element.scrollHeight;
        let scrollCount = 0;
        let maxAttempts = 3; // Reduced from implicit infinite to 3 attempts

        do {
            if (stopSort) break; // Check stopSort at the start of each iteration

            currentScroll = element.scrollTop;
            element.scrollTop = scrollDestination;
            await wait(scrollLoopTime);
            scrollCount++;

            // If we haven't moved in 2 attempts, we're probably at the bottom
            if (scrollCount > 1 && currentScroll === element.scrollTop) {
                break;
            }
        } while (currentScroll !== scrollDestination && scrollCount < maxAttempts && stopSort === false);
    };

    /**
     * Get current timestamp for log entries in HH:MM:SS format
     * @returns {string} Formatted timestamp string
     */
    let getTimestamp = () => {
        const now = new Date();
        const hours = String(now.getHours()).padStart(2, '0');
        const minutes = String(now.getMinutes()).padStart(2, '0');
        const seconds = String(now.getSeconds()).padStart(2, '0');
        return `${hours}:${minutes}:${seconds}`;
    };

    /**
     * Log activities with scrollable console
     * @param {string} message - Message to log
     * @param {boolean} [append=true] - If true, append to log; if false, replace
     * @param {boolean} [isVerbose=false] - If true, only show when verbose mode is on
     * @returns {void}
     */
    let logActivity = (message, append = true, isVerbose = false) => {
        const timestamp = getTimestamp();
        const entry = {
            raw: `[${timestamp}] ${message}`,
            message,
            isVerbose
        };

        // Always log to console for debugging
        console.log(entry.raw);

        if (append) {
            logEntries.push(entry);
            if (logEntries.length > MAX_LOG_MESSAGES) {
                logEntries.shift();
            }
        } else {
            logEntries = [entry];
        }

        const shouldAutoScroll = !isVerbose || verboseMode;
        renderLogDisplay(shouldAutoScroll);
    };

    /**
     * Render log display by filtering and showing appropriate log entries
     * @param {boolean} [shouldAutoScroll=true] - Whether to auto-scroll to bottom
     * @returns {void}
     */
    function renderLogDisplay(shouldAutoScroll = true) {
        if (!logEntries.length) {
            log.textContent = DEFAULT_LOG_MESSAGE;
            log.classList.add('sort-log-empty');
            return;
        }

        const displayMessages = logEntries
            .filter(entry => !entry.isVerbose || verboseMode)
            .map(entry => entry.message);

        if (displayMessages.length === 0) {
            log.textContent = DEFAULT_LOG_MESSAGE;
            log.classList.add('sort-log-empty');
        } else {
            log.textContent = displayMessages.join('\n');
            log.classList.remove('sort-log-empty');
            if (shouldAutoScroll && autoScrollLog) {
                log.scrollTop = log.scrollHeight;
            }
        }
    }

    /**
     * Clear the log console and reset to default message
     * @returns {void}
     */
    let clearLog = () => {
        logEntries = [];
        log.textContent = DEFAULT_LOG_MESSAGE;
        log.classList.add('sort-log-empty');
    };

    /**
     * Generate menu container element and inject into YouTube page
     * Handles both NEW and OLD YouTube architecture
     * @returns {Element|null} Container element or null if parent not found
     */
    let renderContainerElement = () => {
        // Try NEW YouTube architecture first
        let actionsRow = document.querySelector(NEW_ACTIONS_ROW_SELECTOR);
        let parent = null;

        if (actionsRow) {
            // NEW architecture: Insert BELOW the button row
            parent = actionsRow.parentElement;
        } else {
            // Try OLD architecture selectors (for Watch Later / older layouts)
            // Use thumbnail-and-metadata-wrapper which appears below the buttons on Watch Later
            parent = document.querySelector('div.thumbnail-and-metadata-wrapper');

            if (!parent) {
                parent = document.querySelector(PLAYLIST_ACTIONS_SELECTOR) ||
                    document.querySelector(`${PLAYLIST_HEADER_SELECTOR} #container`) ||
                    document.querySelector(PLAYLIST_HEADER_SELECTOR);
            }
        }

        // Fallback for regular playlists (sidebar layout)
        if (!parent || parent.hasAttribute('hidden')) {
            parent = document.querySelector('ytd-playlist-sidebar-primary-info-renderer #menu');
        }

        if (!parent) {
            if (debug) console.warn('Sort Playlist: container parent not found.');
            return null;
        }

        const existing = document.querySelector('.sort-playlist-wrapper');
        if (existing) {
            existing.remove();
        }

        const wrapper = document.createElement('div');
        wrapper.className = 'sort-playlist-wrapper';

        const details = document.createElement('details');
        details.className = 'sort-playlist-details';
        details.open = false;

        const summary = document.createElement('summary');
        summary.className = 'sort-playlist-summary';

        const titleSpan = document.createElement('span');
        titleSpan.className = 'sort-playlist-title';
        titleSpan.innerText = 'Sort playlist by duration';

        const versionSpan = document.createElement('span');
        versionSpan.className = 'sort-playlist-version';
        versionSpan.innerText = 'v' + SCRIPT_VERSION;

        summary.appendChild(titleSpan);
        summary.appendChild(versionSpan);
        details.appendChild(summary);

        const element = document.createElement('div');
        element.className = 'sort-playlist sort-playlist-div sort-playlist-content';

        // Add buttonChild container
        const buttonChild = document.createElement('div');
        buttonChild.className = 'sort-playlist-div sort-playlist-button';
        element.appendChild(buttonChild);

        // Add selectChild container
        const selectChild = document.createElement('div');
        selectChild.className = 'sort-playlist-div sort-playlist-select';
        element.appendChild(selectChild);

        details.appendChild(element);
        wrapper.appendChild(details);

        if (actionsRow) {
            // NEW architecture: Insert wrapper as a sibling AFTER the actions row
            actionsRow.insertAdjacentElement('afterend', wrapper);
        } else {
            // OLD architecture: Append to parent (thumbnail-and-metadata-wrapper)
            parent.append(wrapper);
        }

        return element;
    };

    /**
     * Generate button element and add to button container
     * @param {Function} [click=()=>{}] - OnClick handler function
     * @param {string} [label=''] - Button label text
     * @param {boolean} [red=false] - Whether to use red styling (for stop/danger actions)
     * @returns {void}
     */
    let renderButtonElement = (click = () => { }, label = '', red = false) => {
        // Create button
        const element = document.createElement('button');
        if (red) {
            element.className = 'style-scope sort-button-wl sort-button-wl-stop sort-margin-right-3px';
        } else {
            element.className = 'style-scope sort-button-wl sort-button-wl-default sort-margin-right-3px';
        }
        element.innerText = label;
        element.onclick = click;

        // Render button
        document.querySelector('.sort-playlist-button').appendChild(element);
    };

    /**
     * Generate select dropdown element
     * @param {number} [variable=0] - Variable to update (0 for sortMode, 1 for autoScrollInitialVideoList)
     * @param {Array<{value: string, label: string}>} [options=[]] - Options to render
     * @param {string} [label=''] - Select label (currently unused)
     * @returns {void}
     */
    let renderSelectElement = (variable = 0, options = [], label = '') => {
        // Create select
        const element = document.createElement('select');
        element.className = 'style-scope sort-select sort-margin-right-3px';
        element.onchange = (e) => {
            if (variable === 0) {
                sortMode = e.target.value;
            } else if (variable === 1) {
                autoScrollInitialVideoList = e.target.value;
            }
        };

        // Create options and set initial selection
        options.forEach((option) => {
            const optionElement = document.createElement('option');
            optionElement.value = option.value;
            optionElement.innerText = option.label;

            // Set selected based on current variable value
            if (variable === 0 && option.value === sortMode) {
                optionElement.selected = true;
            } else if (variable === 1 && option.value === autoScrollInitialVideoList) {
                optionElement.selected = true;
            }

            element.appendChild(optionElement);
        });

        // Render select
        document.querySelector('.sort-playlist-select').appendChild(element);
    };

    /**
     * Generate number input element for scroll retry time configuration
     * @param {number} [defaultValue=0] - Default value for the input
     * @param {string} [label=''] - Label text for the input
     * @returns {void}
     */
    let renderNumberElement = (defaultValue = 0, label = '') => {
        // Create div
        const elementDiv = document.createElement('div');
        elementDiv.className = 'sort-playlist-div sort-margin-right-3px';

        // Create label
        const labelElement = document.createElement('span');
        labelElement.className = 'sort-input-label';
        labelElement.innerText = label;
        elementDiv.appendChild(labelElement);

        // Create input
        const element = document.createElement('input');
        element.type = 'number';
        element.value = defaultValue;
        element.min = '100';
        element.step = '100';
        element.className = 'style-scope sort-number-input';
        element.oninput = (e) => { scrollLoopTime = Math.max(100, +(e.target.value)); };

        // Render input
        elementDiv.appendChild(element);
        document.querySelector('div.sort-playlist').appendChild(elementDiv);
    };

    /**
     * Render filter controls (checkbox + min/max duration inputs)
     * Allows users to filter videos by duration before sorting
     * @returns {void}
     */
    let renderFilterControls = () => {
        const filterDiv = document.createElement('div');
        filterDiv.className = 'sort-playlist-div';
        filterDiv.style.marginTop = '8px';
        filterDiv.style.padding = '8px';
        filterDiv.style.borderTop = '1px solid rgba(0,0,0,0.1)';

        // Filter enable checkbox
        const filterCheckboxContainer = document.createElement('label');
        filterCheckboxContainer.className = 'sort-checkbox-container';
        filterCheckboxContainer.style.display = 'block';
        filterCheckboxContainer.style.marginBottom = '8px';

        const filterCheckbox = document.createElement('input');
        filterCheckbox.type = 'checkbox';
        filterCheckbox.className = 'sort-checkbox';
        filterCheckbox.checked = filterEnabled;
        filterCheckbox.onchange = (e) => {
            filterEnabled = e.target.checked;
            filterInputsContainer.style.display = filterEnabled ? 'block' : 'none';
        };

        const filterLabel = document.createElement('span');
        filterLabel.innerText = 'Only include specific lengths';
        filterLabel.style.fontWeight = '600';

        filterCheckboxContainer.appendChild(filterCheckbox);
        filterCheckboxContainer.appendChild(filterLabel);
        filterDiv.appendChild(filterCheckboxContainer);

        // Container for filter inputs (hidden by default)
        const filterInputsContainer = document.createElement('div');
        filterInputsContainer.style.display = filterEnabled ? 'block' : 'none';
        filterInputsContainer.style.marginLeft = '20px';

        // Min duration input
        const minDurationDiv = document.createElement('div');
        minDurationDiv.style.marginBottom = '4px';

        const minLabel = document.createElement('span');
        minLabel.className = 'sort-input-label';
        minLabel.innerText = 'Min duration (minutes): ';
        minLabel.style.fontSize = '12px';

        const minInput = document.createElement('input');
        minInput.type = 'number';
        minInput.value = Math.floor(filterMinDuration / 60);
        minInput.min = '0';
        minInput.step = '1';
        minInput.className = 'style-scope sort-number-input';
        minInput.style.width = '80px';
        minInput.oninput = (e) => { filterMinDuration = Math.max(0, parseInt(e.target.value) || 0) * 60; };

        minDurationDiv.appendChild(minLabel);
        minDurationDiv.appendChild(minInput);
        filterInputsContainer.appendChild(minDurationDiv);

        // Max duration input
        const maxDurationDiv = document.createElement('div');

        const maxLabel = document.createElement('span');
        maxLabel.className = 'sort-input-label';
        maxLabel.innerText = 'Max duration (minutes): ';
        maxLabel.style.fontSize = '12px';

        const maxInput = document.createElement('input');
        maxInput.type = 'number';
        maxInput.value = Math.floor(filterMaxDuration / 60);
        maxInput.min = '0';
        maxInput.step = '1';
        maxInput.className = 'style-scope sort-number-input';
        maxInput.style.width = '80px';
        maxInput.oninput = (e) => { filterMaxDuration = Math.max(0, parseInt(e.target.value) || 600) * 60; };

        maxDurationDiv.appendChild(maxLabel);
        maxDurationDiv.appendChild(maxInput);
        filterInputsContainer.appendChild(maxDurationDiv);

        filterDiv.appendChild(filterInputsContainer);
        document.querySelector('div.sort-playlist').appendChild(filterDiv);
    };

    /**
     * Generate log element with toggle, copy, and scroll controls
     * Creates the main logging console UI
     * @returns {void}
     */
    let renderLogElement = () => {
        // Create container for log and buttons
        const logContainer = document.createElement('div');
        logContainer.style.marginTop = '8px';

        // Create container for log controls (copy button and checkboxes)
        const logControlsRow = document.createElement('div');
        logControlsRow.style.display = logVisible ? 'flex' : 'none';
        logControlsRow.style.gap = '12px';
        logControlsRow.style.alignItems = 'center';
        logControlsRow.style.marginBottom = '4px';

        // Create copy log button
        const copyButton = document.createElement('button');
        copyButton.className = 'style-scope sort-button-wl sort-button-wl-default';
        copyButton.style.fontSize = '11px';
        copyButton.style.padding = '4px 8px';
        copyButton.innerText = 'Copy Log';
        copyButton.onclick = () => {
            const logText = logEntries.length ? logEntries.map(entry => entry.raw).join('\n') : '';
            navigator.clipboard.writeText(logText).then(() => {
                // Temporarily change button text
                const originalText = copyButton.innerText;
                copyButton.innerText = '✓ Copied!';
                setTimeout(() => {
                    copyButton.innerText = originalText;
                }, 2000);
            }).catch(err => {
                logActivity('❌ Failed to copy to clipboard');
                console.error('Copy failed:', err);
            });
        };

        // Create scroll log checkbox container
        const scrollContainer = document.createElement('label');
        scrollContainer.className = 'sort-checkbox-container';

        const scrollCheckbox = document.createElement('input');
        scrollCheckbox.type = 'checkbox';
        scrollCheckbox.className = 'sort-checkbox';
        scrollCheckbox.checked = autoScrollLog;
        scrollCheckbox.onchange = (e) => {
            autoScrollLog = e.target.checked;
            if (autoScrollLog) {
                log.scrollTop = log.scrollHeight;
            }
        };

        const scrollLabel = document.createElement('span');
        scrollLabel.innerText = 'Scroll Log';

        scrollContainer.appendChild(scrollCheckbox);
        scrollContainer.appendChild(scrollLabel);

        // Create verbose checkbox container
        const verboseContainer = document.createElement('label');
        verboseContainer.className = 'sort-checkbox-container';

        const verboseCheckbox = document.createElement('input');
        verboseCheckbox.type = 'checkbox';
        verboseCheckbox.className = 'sort-checkbox';
        verboseCheckbox.checked = verboseMode;
        verboseCheckbox.onchange = (e) => {
            verboseMode = e.target.checked;
            renderLogDisplay(false);
            logActivity(verboseMode ? 'Verbose mode enabled' : 'Verbose mode disabled');
        };

        const verboseLabel = document.createElement('span');
        verboseLabel.innerText = 'Verbose';

        verboseContainer.appendChild(verboseCheckbox);
        verboseContainer.appendChild(verboseLabel);

        // Populate log div
        log.className = 'style-scope sort-log';
        log.textContent = DEFAULT_LOG_MESSAGE;
        log.classList.add('sort-log-empty');
        log.style.display = logVisible ? 'block' : 'none';

        // Add controls to log controls row
        logControlsRow.appendChild(copyButton);
        logControlsRow.appendChild(scrollContainer);
        logControlsRow.appendChild(verboseContainer);

        // Render elements
        logContainer.appendChild(logControlsRow);
        logContainer.appendChild(log);
        document.querySelector('div.sort-playlist').appendChild(logContainer);

        // Store reference to controls row for toggling
        window.logControlsRow = logControlsRow;
    };

    /**
     * Add CSS styling to the page
     * Injects custom styles for the playlist sorter UI
     * @returns {void}
     */
    let addCssStyle = () => {
        if (document.head.querySelector('#sort-playlist-style')) {
            return;
        }
        const element = document.createElement('style');
        element.id = 'sort-playlist-style';
        element.textContent = css;
        document.head.appendChild(element);
    };

    /**
     * Show settings panel modal for configuration management
     * Allows users to configure and save default settings
     * @returns {void}
     */
    let showSettingsPanel = () => {
        // Create overlay
        const overlay = document.createElement('div');
        overlay.className = 'sort-settings-overlay';

        // Create modal
        const modal = document.createElement('div');
        modal.className = 'sort-settings-modal';

        // Title with close button
        const titleBar = document.createElement('div');
        titleBar.className = 'sort-settings-title';
        titleBar.style.display = 'flex';
        titleBar.style.justifyContent = 'space-between';
        titleBar.style.alignItems = 'center';

        const titleText = document.createElement('span');
        titleText.textContent = '⚙️ Settings';

        const closeButton = document.createElement('button');
        closeButton.innerHTML = '✕';
        closeButton.className = 'sort-settings-close-btn';
        closeButton.style.background = 'none';
        closeButton.style.border = 'none';
        closeButton.style.color = '#aaa';
        closeButton.style.fontSize = '24px';
        closeButton.style.cursor = 'pointer';
        closeButton.style.padding = '0';
        closeButton.style.lineHeight = '1';
        closeButton.style.transition = 'color 0.2s';
        closeButton.onmouseover = () => { closeButton.style.color = '#f1f1f1'; };
        closeButton.onmouseout = () => { closeButton.style.color = '#aaa'; };
        closeButton.onclick = () => {
            document.body.removeChild(overlay);
            document.body.removeChild(modal);
        };

        titleBar.appendChild(titleText);
        titleBar.appendChild(closeButton);
        modal.appendChild(titleBar);

        // Sort Settings Section
        const sortSection = document.createElement('div');
        sortSection.className = 'sort-settings-section';

        const sortTitle = document.createElement('div');
        sortTitle.className = 'sort-settings-section-title';
        sortTitle.textContent = 'Sort Preferences';
        sortSection.appendChild(sortTitle);

        // Sort Mode
        const sortModeRow = document.createElement('div');
        sortModeRow.className = 'sort-settings-row';
        const sortModeLabel = document.createElement('span');
        sortModeLabel.className = 'sort-settings-label';
        sortModeLabel.textContent = 'Default Sort Mode:';
        const sortModeSelect = document.createElement('select');
        sortModeSelect.className = 'sort-select';
        modeAvailable.forEach(opt => {
            const option = document.createElement('option');
            option.value = opt.value;
            option.textContent = opt.label;
            option.selected = sortMode === opt.value;
            sortModeSelect.appendChild(option);
        });
        sortModeRow.appendChild(sortModeLabel);
        sortModeRow.appendChild(sortModeSelect);
        sortSection.appendChild(sortModeRow);

        // Auto Scroll Mode
        const autoScrollRow = document.createElement('div');
        autoScrollRow.className = 'sort-settings-row';
        const autoScrollLabel = document.createElement('span');
        autoScrollLabel.className = 'sort-settings-label';
        autoScrollLabel.textContent = 'Default Auto-Scroll:';
        const autoScrollSelect = document.createElement('select');
        autoScrollSelect.className = 'sort-select';
        autoScrollOptions.forEach(opt => {
            const option = document.createElement('option');
            option.value = opt.value;
            option.textContent = opt.label;
            option.selected = autoScrollInitialVideoList === opt.value;
            autoScrollSelect.appendChild(option);
        });
        autoScrollRow.appendChild(autoScrollLabel);
        autoScrollRow.appendChild(autoScrollSelect);
        sortSection.appendChild(autoScrollRow);

        // Scroll Retry Time
        const scrollTimeRow = document.createElement('div');
        scrollTimeRow.className = 'sort-settings-row';
        const scrollTimeLabel = document.createElement('span');
        scrollTimeLabel.className = 'sort-settings-label';
        scrollTimeLabel.textContent = 'Scroll Retry Time (ms):';
        const scrollTimeInput = document.createElement('input');
        scrollTimeInput.type = 'number';
        scrollTimeInput.className = 'sort-number-input';
        scrollTimeInput.value = scrollLoopTime;
        scrollTimeInput.min = '100';
        scrollTimeInput.step = '100';
        scrollTimeRow.appendChild(scrollTimeLabel);
        scrollTimeRow.appendChild(scrollTimeInput);
        sortSection.appendChild(scrollTimeRow);

        modal.appendChild(sortSection);

        // Filter Settings Section
        const filterSection = document.createElement('div');
        filterSection.className = 'sort-settings-section';

        const filterTitle = document.createElement('div');
        filterTitle.className = 'sort-settings-section-title';
        filterTitle.textContent = 'Filter Preferences';
        filterSection.appendChild(filterTitle);

        // Enable Filter
        const filterEnableRow = document.createElement('div');
        filterEnableRow.className = 'sort-settings-row';
        const filterEnableLabel = document.createElement('span');
        filterEnableLabel.className = 'sort-settings-label';
        filterEnableLabel.textContent = 'Only Include Specific Lengths:';
        const filterEnableCheckbox = document.createElement('input');
        filterEnableCheckbox.type = 'checkbox';
        filterEnableCheckbox.className = 'sort-checkbox';
        filterEnableCheckbox.checked = filterEnabled;
        filterEnableRow.appendChild(filterEnableLabel);
        filterEnableRow.appendChild(filterEnableCheckbox);
        filterSection.appendChild(filterEnableRow);

        // Min Duration
        const minDurationRow = document.createElement('div');
        minDurationRow.className = 'sort-settings-row sort-filter-duration-row';
        minDurationRow.style.marginLeft = '20px';
        minDurationRow.style.display = filterEnabled ? 'flex' : 'none';
        const minLabel = document.createElement('span');
        minLabel.className = 'sort-settings-label';
        minLabel.textContent = 'Min Duration (minutes):';
        const minInput = document.createElement('input');
        minInput.type = 'number';
        minInput.className = 'sort-number-input';
        minInput.value = Math.floor(filterMinDuration / 60);
        minInput.min = '0';
        minInput.step = '1';
        minDurationRow.appendChild(minLabel);
        minDurationRow.appendChild(minInput);
        filterSection.appendChild(minDurationRow);

        // Max Duration
        const maxDurationRow = document.createElement('div');
        maxDurationRow.className = 'sort-settings-row sort-filter-duration-row';
        maxDurationRow.style.marginLeft = '20px';
        maxDurationRow.style.display = filterEnabled ? 'flex' : 'none';
        const maxLabel = document.createElement('span');
        maxLabel.className = 'sort-settings-label';
        maxLabel.textContent = 'Max Duration (minutes):';
        const maxInput = document.createElement('input');
        maxInput.type = 'number';
        maxInput.className = 'sort-number-input';
        maxInput.value = Math.floor(filterMaxDuration / 60);
        maxInput.min = '0';
        maxInput.step = '1';
        maxDurationRow.appendChild(maxLabel);
        maxDurationRow.appendChild(maxInput);
        filterSection.appendChild(maxDurationRow);

        // Add change handler to toggle filter inputs visibility
        filterEnableCheckbox.onchange = (e) => {
            const display = e.target.checked ? 'flex' : 'none';
            minDurationRow.style.display = display;
            maxDurationRow.style.display = display;
        };

        modal.appendChild(filterSection);

        // Other Settings Section
        const otherSection = document.createElement('div');
        otherSection.className = 'sort-settings-section';

        const otherTitle = document.createElement('div');
        otherTitle.className = 'sort-settings-section-title';
        otherTitle.textContent = 'Other Preferences';
        otherSection.appendChild(otherTitle);

        // Dry Run Mode
        const dryRunRow = document.createElement('div');
        dryRunRow.className = 'sort-settings-row';
        const dryRunLabel = document.createElement('span');
        dryRunLabel.className = 'sort-settings-label';
        dryRunLabel.textContent = 'Enable Dry Run (Preview Before Sort):';
        const dryRunCheckbox = document.createElement('input');
        dryRunCheckbox.type = 'checkbox';
        dryRunCheckbox.className = 'sort-checkbox';
        dryRunCheckbox.checked = dryRunEnabled;
        dryRunRow.appendChild(dryRunLabel);
        dryRunRow.appendChild(dryRunCheckbox);
        otherSection.appendChild(dryRunRow);

        // Log Visible by Default
        const logVisibleRow = document.createElement('div');
        logVisibleRow.className = 'sort-settings-row';
        const logVisibleLabel = document.createElement('span');
        logVisibleLabel.className = 'sort-settings-label';
        logVisibleLabel.textContent = 'Show Log by Default:';
        const logVisibleCheckbox = document.createElement('input');
        logVisibleCheckbox.type = 'checkbox';
        logVisibleCheckbox.className = 'sort-checkbox';
        logVisibleCheckbox.checked = logVisible;
        logVisibleRow.appendChild(logVisibleLabel);
        logVisibleRow.appendChild(logVisibleCheckbox);
        otherSection.appendChild(logVisibleRow);

        modal.appendChild(otherSection);

        // Buttons
        const buttonsDiv = document.createElement('div');
        buttonsDiv.className = 'sort-settings-buttons';

        const saveButton = document.createElement('button');
        saveButton.className = 'sort-button-wl sort-button-wl-default';
        saveButton.textContent = 'Save Settings';
        saveButton.onclick = () => {
            // Update current values
            sortMode = sortModeSelect.value;
            autoScrollInitialVideoList = autoScrollSelect.value;
            scrollLoopTime = parseInt(scrollTimeInput.value);
            dryRunEnabled = dryRunCheckbox.checked;
            filterEnabled = filterEnableCheckbox.checked;
            filterMinDuration = Math.max(0, parseInt(minInput.value) || 0) * 60;
            filterMaxDuration = Math.max(0, parseInt(maxInput.value) || 600) * 60;
            logVisible = logVisibleCheckbox.checked;

            // Save to localStorage
            saveSettings(getCurrentSettings());

            // Update UI (this will show/hide log based on settings)
            updateUIFromSettings();

            // Log success message (before closing modal so log is visible)
            logActivity('✅ Settings saved successfully');

            // Close modal
            document.body.removeChild(overlay);
            document.body.removeChild(modal);
        };

        const cancelButton = document.createElement('button');
        cancelButton.className = 'sort-button-wl sort-button-wl-default';
        cancelButton.style.background = '#888';
        cancelButton.textContent = 'Cancel';
        cancelButton.onclick = () => {
            document.body.removeChild(overlay);
            document.body.removeChild(modal);
        };

        const resetButton = document.createElement('button');
        resetButton.className = 'sort-button-wl sort-button-wl-stop';
        resetButton.textContent = 'Reset to Defaults';
        resetButton.onclick = () => {
            if (confirm('Reset all settings to default values?')) {
                applySettings(DEFAULT_SETTINGS);
                saveSettings(DEFAULT_SETTINGS);
                document.body.removeChild(overlay);
                document.body.removeChild(modal);
                updateUIFromSettings();
                logActivity('🔄 Settings reset to defaults');
            }
        };

        buttonsDiv.appendChild(resetButton);
        buttonsDiv.appendChild(saveButton);
        modal.appendChild(buttonsDiv);

        // Close on overlay click
        overlay.onclick = () => {
            document.body.removeChild(overlay);
            document.body.removeChild(modal);
        };

        document.body.appendChild(overlay);
        document.body.appendChild(modal);
    };

    /**
     * Update UI elements to reflect current settings
     * Updates log visibility based on saved settings
     * @returns {void}
     */
    let updateUIFromSettings = () => {
        // Update log visibility based on saved settings
        const toggleButton = document.querySelector('.sort-log-toggle-btn');
        if (toggleButton) {
            toggleButton.innerText = logVisible ? 'Hide Log' : 'Show Log';
        }
        const logControlsContainer = document.querySelector('.sort-log-controls');
        if (logControlsContainer) {
            logControlsContainer.style.display = logVisible ? 'block' : 'none';
        }
        if (log) {
            log.style.display = logVisible ? 'block' : 'none';
        }
    };

    /**
     * Perform a dry run simulation of the sort
     * Shows before/after comparison without actually moving videos
     * @returns {Promise<void>} Resolves when dry run is complete
     */
    let performDryRun = async () => {
        logActivity('🔍 Starting Dry Run Mode...');
        logActivity('📋 Analyzing current playlist order...');

        // Get current videos
        let videoPairs = getPlaylistVideoPairs();

        if (videoPairs.length === 0) {
            logActivity('❌ No videos found for dry run');
            return;
        }

        // Create before/after arrays
        const beforeOrder = [];
        const afterOrder = [];

        videoPairs.forEach((pair, index) => {
            const duration = getVideoDuration(pair.anchor);
            const title = getVideoTitle(pair.item);
            const passes = passesFilter(duration);

            const videoInfo = {
                index: index + 1,
                title: title || 'Unknown Title',
                duration: duration,
                durationFormatted: duration !== null ? formatSeconds(duration) : 'N/A',
                passes: passes
            };

            beforeOrder.push(videoInfo);
        });

        // Sort the after order based on current settings
        const sortedVideos = [...beforeOrder];

        // Separate filtered and non-filtered videos
        const passedVideos = sortedVideos.filter(v => v.passes);
        const filteredVideos = sortedVideos.filter(v => !v.passes);

        // Sort passed videos by duration (and title tiebreaker)
        passedVideos.sort((a, b) => {
            const durationA = a.duration !== null ? a.duration : -1;
            const durationB = b.duration !== null ? b.duration : -1;

            if (sortMode === 'asc') {
                if (durationA === durationB) {
                    return a.title.localeCompare(b.title);
                }
                return durationA - durationB;
            } else {
                if (durationA === durationB) {
                    return a.title.localeCompare(b.title);
                }
                return durationB - durationA;
            }
        });

        // Combine: sorted passed videos + filtered videos at end
        afterOrder.push(...passedVideos, ...filteredVideos);

        // Renumber after order
        afterOrder.forEach((video, index) => {
            video.newIndex = index + 1;
        });

        // Show modal with comparison
        showDryRunComparison(beforeOrder, afterOrder);

        logActivity('✅ Dry run complete - review changes in popup');
    };

    /**
     * Format seconds to readable time string
     * @param {number} seconds - Seconds to format
     * @returns {string} Formatted time string
     */
    let formatSeconds = (seconds) => {
        const hours = Math.floor(seconds / 3600);
        const minutes = Math.floor((seconds % 3600) / 60);
        const secs = seconds % 60;

        if (hours > 0) {
            return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
        } else {
            return `${minutes}:${String(secs).padStart(2, '0')}`;
        }
    };

    /**
     * Show dry run comparison modal
     * @param {Array} beforeOrder - Original video order
     * @param {Array} afterOrder - Predicted sorted order
     * @returns {void}
     */
    let showDryRunComparison = (beforeOrder, afterOrder) => {
        // Create overlay
        const overlay = document.createElement('div');
        overlay.className = 'sort-settings-overlay';

        // Create modal
        const modal = document.createElement('div');
        modal.className = 'sort-dry-run-modal';

        // Title
        const title = document.createElement('div');
        title.className = 'sort-dry-run-title';
        title.textContent = '🔍 Dry Run - Sort Preview';
        modal.appendChild(title);

        // Info box
        const info = document.createElement('div');
        info.className = 'sort-dry-run-info';
        info.innerHTML = `
            <strong>Preview Mode:</strong> This shows how your playlist will be sorted without making any changes.<br>
            <strong>Mode:</strong> ${sortMode === 'asc' ? 'Shortest First' : 'Longest First'}<br>
            <strong>Videos:</strong> ${beforeOrder.length} total${filterEnabled ? ` (filter: ${Math.floor(filterMinDuration / 60)}-${Math.floor(filterMaxDuration / 60)} min)` : ''}
        `;
        modal.appendChild(info);

        // Comparison grid
        const comparison = document.createElement('div');
        comparison.className = 'sort-dry-run-comparison';

        // Before column
        const beforeColumn = document.createElement('div');
        beforeColumn.className = 'sort-dry-run-column';
        const beforeTitle = document.createElement('h4');
        beforeTitle.textContent = '📋 Current Order';
        beforeColumn.appendChild(beforeTitle);

        beforeOrder.forEach(video => {
            const videoDiv = document.createElement('div');
            videoDiv.className = 'sort-dry-run-video';
            videoDiv.textContent = `${video.index}. ${video.title.substring(0, 40)}${video.title.length > 40 ? '...' : ''} (${video.durationFormatted})`;
            if (!video.passes) {
                videoDiv.style.opacity = '0.5';
                videoDiv.style.textDecoration = 'line-through';
            }
            beforeColumn.appendChild(videoDiv);
        });

        // After column
        const afterColumn = document.createElement('div');
        afterColumn.className = 'sort-dry-run-column';
        const afterTitle = document.createElement('h4');
        afterTitle.textContent = '✨ After Sort';
        afterColumn.appendChild(afterTitle);

        afterOrder.forEach(video => {
            const videoDiv = document.createElement('div');
            videoDiv.className = 'sort-dry-run-video';
            videoDiv.textContent = `${video.newIndex}. ${video.title.substring(0, 40)}${video.title.length > 40 ? '...' : ''} (${video.durationFormatted})`;
            if (!video.passes) {
                videoDiv.style.opacity = '0.5';
                videoDiv.title = 'Filtered out - will be moved to end';
            }
            // Highlight if position changed
            if (video.index !== video.newIndex) {
                videoDiv.style.borderLeft = '3px solid #3ea6ff';
            }
            afterColumn.appendChild(videoDiv);
        });

        comparison.appendChild(beforeColumn);
        comparison.appendChild(afterColumn);
        modal.appendChild(comparison);

        // Buttons
        const buttonsDiv = document.createElement('div');
        buttonsDiv.className = 'sort-dry-run-buttons';

        const applyButton = document.createElement('button');
        applyButton.className = 'sort-button-wl sort-button-wl-default';
        applyButton.textContent = '✓ Apply Sort';
        applyButton.onclick = async () => {
            document.body.removeChild(overlay);
            document.body.removeChild(modal);
            logActivity('✅ Dry run approved - starting actual sort...');

            // Temporarily disable dry run to perform actual sort
            const wasDryRunEnabled = dryRunEnabled;
            dryRunEnabled = false;
            await activateSort();
            dryRunEnabled = wasDryRunEnabled; // Restore dry run setting
        };

        const cancelButton = document.createElement('button');
        cancelButton.className = 'sort-button-wl sort-button-wl-default';
        cancelButton.style.background = '#888';
        cancelButton.textContent = 'Cancel';
        cancelButton.onclick = () => {
            document.body.removeChild(overlay);
            document.body.removeChild(modal);
            logActivity('🚫 Dry run cancelled - no changes made');
        };

        buttonsDiv.appendChild(cancelButton);
        buttonsDiv.appendChild(applyButton);
        modal.appendChild(buttonsDiv);

        // Close on overlay click
        overlay.onclick = () => {
            document.body.removeChild(overlay);
            document.body.removeChild(modal);
            logActivity('🚫 Dry run cancelled - no changes made');
        };

        document.body.appendChild(overlay);
        document.body.appendChild(modal);
    };

    /**
     * Analyze playlist and show statistics (total duration, average, min/max, etc.)
     * Loads all videos in the playlist first, then calculates and displays statistics
     * @returns {Promise<void>} Resolves when analysis is complete
     */
    let analyzePlaylist = async () => {
        // Show log if it's hidden
        if (!logVisible) {
            logVisible = true;
            const toggleButton = document.querySelector('.sort-log-toggle-btn');
            if (toggleButton) {
                toggleButton.innerText = 'Hide Log';
            }
            const logControlsContainer = document.querySelector('.sort-log-controls');
            if (logControlsContainer) {
                logControlsContainer.style.display = 'block';
            }
            if (log) {
                log.style.display = 'block';
            }
        }

        clearLog();
        const details = document.querySelector('.sort-playlist-details');
        if (details && !details.open) {
            details.open = true;
        }

        logActivity("📊 Analyzing playlist...");
        logActivity("🔄 Loading all videos in playlist...");

        // Load ALL videos first (same logic as sort)
        let videoPairs = getPlaylistVideoPairs();
        let initialCount = videoPairs.length;
        let scrollRetryCount = 0;
        let maxScrollRetries = 10;
        let noProgressCount = 0;

        // Scroll to load all videos
        while (scrollRetryCount < maxScrollRetries && stopSort === false) {
            let previousCount = videoPairs.length;

            logActivity("Loading videos... " + videoPairs.length + " loaded so far", true, true);

            await autoScroll();
            await wait(scrollLoopTime * 2);

            videoPairs = getPlaylistVideoPairs();

            if (previousCount === videoPairs.length) {
                noProgressCount++;
                scrollRetryCount++;

                if (noProgressCount >= 3) {
                    logActivity("✓ All available videos loaded");
                    break;
                }
            } else {
                noProgressCount = 0;
                scrollRetryCount = 0;
                logActivity("📈 Loaded " + (videoPairs.length - previousCount) + " more videos");
            }
        }

        // Get all video pairs after loading
        let allAnchors = videoPairs.map(pair => pair.anchor);

        if (allAnchors.length === 0) {
            logActivity("❌ No videos found to analyze");
            return;
        }

        logActivity("📥 Found " + allAnchors.length + " loaded videos");

        // Extract duration data
        let durations = [];
        let totalSeconds = 0;
        let validVideos = 0;
        let unavailableVideos = 0;

        for (let i = 0; i < allAnchors.length; i++) {
            let thumb = allAnchors[i];
            let timeSpan = thumb.querySelector("#text");

            if (!timeSpan || !timeSpan.innerText.trim()) {
                unavailableVideos++;
                continue;
            }

            let timeDigits = timeSpan.innerText.trim().split(":").reverse();

            // Skip if no valid time
            if (timeDigits.length === 1) {
                unavailableVideos++;
                continue;
            }

            let seconds = parseInt(timeDigits[0]) || 0;
            if (timeDigits[1]) seconds += parseInt(timeDigits[1]) * 60;
            if (timeDigits[2]) seconds += parseInt(timeDigits[2]) * 3600;

            durations.push(seconds);
            totalSeconds += seconds;
            validVideos++;
        }

        if (validVideos === 0) {
            logActivity("❌ No videos with valid durations found");
            return;
        }

        // Calculate statistics
        const avgSeconds = Math.floor(totalSeconds / validVideos);
        const minSeconds = Math.min(...durations);
        const maxSeconds = Math.max(...durations);

        // Format time helper
        const formatTime = (sec) => {
            const hours = Math.floor(sec / 3600);
            const minutes = Math.floor((sec % 3600) / 60);
            const seconds = sec % 60;

            if (hours > 0) {
                return hours + "h " + minutes + "m " + seconds + "s";
            } else if (minutes > 0) {
                return minutes + "m " + seconds + "s";
            } else {
                return seconds + "s";
            }
        };

        const formatTimeShort = (sec) => {
            const hours = Math.floor(sec / 3600);
            const minutes = Math.floor((sec % 3600) / 60);
            const seconds = sec % 60;

            if (hours > 0) {
                return hours + ":" + String(minutes).padStart(2, '0') + ":" + String(seconds).padStart(2, '0');
            } else {
                return minutes + ":" + String(seconds).padStart(2, '0');
            }
        };

        // Display results
        logActivity("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        logActivity("📊 PLAYLIST ANALYSIS");
        logActivity("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        logActivity("📹 Videos analyzed: " + validVideos);
        if (unavailableVideos > 0) {
            logActivity("⚠️  Unavailable/Private: " + unavailableVideos);
        }
        logActivity("");
        logActivity("⏱️  DURATION STATISTICS:");
        logActivity("   Total Duration: " + formatTime(totalSeconds));
        logActivity("   Average Length: " + formatTime(avgSeconds));
        logActivity("   Shortest Video: " + formatTimeShort(minSeconds));
        logActivity("   Longest Video:  " + formatTimeShort(maxSeconds));
        logActivity("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        logActivity("✅ Analysis complete!");
    };

    /**
     * Export playlist data to CSV file
     * Loads all videos and generates a CSV with position, title, duration, and URL
     * @returns {Promise<void>} Resolves when export is complete and file is downloaded
     */
    let exportPlaylist = async () => {
        // Show log if it's hidden
        if (!logVisible) {
            logVisible = true;
            const toggleButton = document.querySelector('.sort-log-toggle-btn');
            if (toggleButton) {
                toggleButton.innerText = 'Hide Log';
            }
            const logControlsContainer = document.querySelector('.sort-log-controls');
            if (logControlsContainer) {
                logControlsContainer.style.display = 'block';
            }
            if (log) {
                log.style.display = 'block';
            }
        }

        clearLog();
        const details = document.querySelector('.sort-playlist-details');
        if (details && !details.open) {
            details.open = true;
        }

        logActivity("📤 Exporting playlist...");
        logActivity("🔄 Loading all videos in playlist...");

        // Load ALL videos first (same logic as sort)
        let videoPairs = getPlaylistVideoPairs();
        let scrollRetryCount = 0;
        let maxScrollRetries = 10;
        let noProgressCount = 0;

        // Scroll to load all videos
        while (scrollRetryCount < maxScrollRetries && stopSort === false) {
            let previousCount = videoPairs.length;

            logActivity("Loading videos... " + videoPairs.length + " loaded so far", true, true);

            await autoScroll();
            await wait(scrollLoopTime * 2);

            videoPairs = getPlaylistVideoPairs();

            if (previousCount === videoPairs.length) {
                noProgressCount++;
                scrollRetryCount++;

                if (noProgressCount >= 3) {
                    logActivity("✓ All available videos loaded");
                    break;
                }
            } else {
                noProgressCount = 0;
                scrollRetryCount = 0;
                logActivity("📈 Loaded " + (videoPairs.length - previousCount) + " more videos");
            }
        }

        if (videoPairs.length === 0) {
            logActivity("❌ No videos found to export");
            return;
        }

        logActivity("📥 Found " + videoPairs.length + " loaded videos");
        logActivity("📝 Generating CSV file...");

        // Build CSV content
        let csvContent = "Position,Title,Duration,URL\n";

        for (let i = 0; i < videoPairs.length; i++) {
            const pair = videoPairs[i];
            const anchor = pair.anchor;
            const item = pair.item;

            // Get video title
            const title = getVideoTitle(item);
            const escapedTitle = '"' + (title || 'Unknown').replace(/"/g, '""') + '"';

            // Get duration
            const durationSeconds = getVideoDuration(anchor);
            let durationFormatted = "N/A";

            if (durationSeconds !== null) {
                const hours = Math.floor(durationSeconds / 3600);
                const minutes = Math.floor((durationSeconds % 3600) / 60);
                const seconds = durationSeconds % 60;

                // Always format as HH:MM:SS for consistency
                durationFormatted = String(hours).padStart(2, '0') + ":" +
                    String(minutes).padStart(2, '0') + ":" +
                    String(seconds).padStart(2, '0');
            }

            // Get clean video URL (remove query parameters after video ID)
            let videoUrl = anchor.href || "";
            if (videoUrl.includes('v=')) {
                const videoId = videoUrl.split('v=')[1].split('&')[0];
                videoUrl = "https://www.youtube.com/watch?v=" + videoId;
            }

            // Build CSV row
            csvContent += (i + 1) + ",";
            csvContent += escapedTitle + ",";
            csvContent += durationFormatted + ",";
            csvContent += videoUrl + "\n";
        }

        // Create download link
        const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');

        // Generate filename with current date
        const now = new Date();
        const dateStr = now.getFullYear() + '-' +
            String(now.getMonth() + 1).padStart(2, '0') + '-' +
            String(now.getDate()).padStart(2, '0');
        const playlistId = window.location.search.match(/list=([^&]+)/);
        const filename = 'youtube_playlist_' + (playlistId ? playlistId[1] : 'export') + '_' + dateStr + '.csv';

        link.href = url;
        link.download = filename;
        link.style.display = 'none';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);

        logActivity("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        logActivity("✅ Export complete!");
        logActivity("📁 File: " + filename);
        logActivity("📊 Exported " + videoPairs.length + " videos");
        logActivity("💾 CSV file downloaded to your Downloads folder");
        logActivity("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    };

    /**
     * Sort videos by duration (and optionally by title as tiebreaker)
     * Applies filtering if enabled and performs drag-and-drop operations
     * @param {Element[]} allAnchors - Array of video anchor elements
     * @param {Element[]} allDragPoints - Array of draggable handle elements
     * @param {Element[]} allItems - Array of video item elements
     * @param {number} expectedCount - Expected number of videos (for verification)
     * @returns {number} Number of videos sorted (position of last sorted video)
     */
    let sortVideos = (allAnchors, allDragPoints, allItems, expectedCount) => {
        let videos = [];
        let sorted = 0;
        let dragged = false;

        // Sometimes after dragging, the page is not fully loaded yet
        // This can be seen by the number of anchors not being a multiple of 100
        if (allDragPoints.length !== expectedCount || allAnchors.length !== expectedCount) {
            logActivity("Playlist is not fully loaded, waiting...");
            return 0;
        }

        for (let j = 0; j < allDragPoints.length; j++) {
            let thumb = allAnchors[j];
            let drag = allDragPoints[j];
            let item = allItems[j];

            const duration = getVideoDuration(thumb);
            const title = getVideoTitle(item);

            // Check if video passes duration filter
            if (filterEnabled && !passesFilter(duration)) {
                // Videos that don't pass filter are pushed to the end
                videos.push({
                    anchor: drag,
                    time: sortMode === "asc" ? 999999999999999999 : -1,
                    title: title,
                    originalIndex: j,
                    filtered: true
                });
                continue;
            }

            let time;
            if (duration === null) {
                // Videos without duration (unavailable/private) go to the end
                time = sortMode === "asc" ? 999999999999999999 : -1;
            } else {
                time = duration;
            }

            videos.push({
                anchor: drag,
                time: time,
                title: title,
                originalIndex: j,
                filtered: false
            });
        }

        // Sort by duration (and optionally title as tiebreaker)
        if (sortMode === "asc") {
            videos.sort((a, b) => {
                if (a.time !== b.time) {
                    return a.time - b.time;
                }
                // If durations are equal and tiebreaker is enabled, sort by title
                if (useTitleTiebreaker && a.title && b.title) {
                    return a.title.localeCompare(b.title);
                }
                return 0;
            });
        } else {
            videos.sort((a, b) => {
                if (a.time !== b.time) {
                    return b.time - a.time;
                }
                // If durations are equal and tiebreaker is enabled, sort by title
                if (useTitleTiebreaker && a.title && b.title) {
                    return a.title.localeCompare(b.title);
                }
                return 0;
            });
        }

        // Calculate total number of moves needed (videos out of place)
        // This is calculated per iteration but we'll use the global counter for display
        let totalMovesNeeded = 0;
        for (let j = 0; j < videos.length; j++) {
            if (videos[j].originalIndex !== j) {
                totalMovesNeeded++;
            }
        }

        for (let j = 0; j < videos.length; j++) {
            let originalIndex = videos[j].originalIndex;

            if (debug) {
                console.log("Loaded: " + videos.length + ". Current: " + j + ". Original: " + originalIndex + ".");
            }

            if (originalIndex !== j) {
                globalMoveCounter++; // Increment global counter for each move

                let elemDrag = videos[j].anchor;
                let elemDrop = null;
                for (let k = 0; k < videos.length; k++) {
                    if (videos[k].originalIndex === j) {
                        elemDrop = videos[k].anchor;
                        break;
                    }
                }

                if (!elemDrop) {
                    continue;
                }

                simulateDrag(elemDrag, elemDrop);
                dragged = true;
            }

            sorted = j;

            if (stopSort || dragged) {
                break;
            }
        }

        if (sorted > 0 && globalMoveCounter > 0) {
            // Calculate remaining moves
            let remainingMoves = globalTotalMoves - globalMoveCounter;
            if (remainingMoves < 0) remainingMoves = 0;
            logActivity("🔄 Moved #" + videos[sorted].originalIndex + " → #" + sorted + " (" + globalMoveCounter + "/" + globalTotalMoves + ")");
        }

        return sorted;
    };

    /**
     * Main sort activation function
     * Handles the complete sort process including:
     * - Loading all videos (or keeping current loaded set)
     * - Verifying playlist state
     * - Iteratively sorting videos by duration
     * - Managing lazy loading prevention
     * - Progress logging and error recovery
     * 
     * Note: There is an inherent limit in how fast you can sort videos due to YouTube's
     * refresh behavior. This limit also applies to manual sorting. Performance degrades
     * with playlist size (approximately +2-4 seconds per 100 videos).
     * 
     * @returns {Promise<void>} Resolves when sort is complete or stopped
     */
    let activateSort = async () => {
        // Check if dry run mode is enabled
        if (dryRunEnabled) {
            await performDryRun();
            return; // Exit early - performDryRun will call activateSort again if user confirms
        }

        // Proceed with actual sort
        clearLog();
        const details = document.querySelector('.sort-playlist-details');
        if (details && !details.open) {
            details.open = true;
        }
        logActivity("🚀 Starting sort process...");
        logActivity("ℹ️  Script version: " + SCRIPT_VERSION, true, true);

        // Try multiple selectors to get video count
        let reportedVideoCountElement = null;
        let reportedVideoCount = 0;

        // Try NEW YouTube architecture first (yt-content-metadata-view-model)
        const metadataRows = document.querySelectorAll('.yt-content-metadata-view-model__metadata-row');
        for (const row of metadataRows) {
            const spans = row.querySelectorAll('span.yt-core-attributed-string');
            for (const span of spans) {
                if (span.textContent.includes('video')) {
                    reportedVideoCountElement = span;
                    break;
                }
            }
            if (reportedVideoCountElement) break;
        }

        // Fallback to old selectors
        if (!reportedVideoCountElement) {
            reportedVideoCountElement = document.querySelector("ytd-playlist-byline-renderer .metadata-stats .byline-item.style-scope.ytd-playlist-byline-renderer span");
        }

        if (!reportedVideoCountElement) {
            reportedVideoCountElement = document.querySelector(".metadata-stats span.yt-formatted-string:first-of-type");
        }

        // Try to parse the video count
        if (reportedVideoCountElement) {
            reportedVideoCount = parseInt(reportedVideoCountElement.innerText.replace(/[^0-9]/g, ''));
        }

        // If we still don't have a count, we'll estimate it by loading all videos
        if (isNaN(reportedVideoCount) || reportedVideoCount === 0) {
            logActivity("⚠️  Could not find video count in page. Will load and count videos...");
            reportedVideoCount = -1; // Flag to indicate we need to count manually
        }

        logActivity("📊 Detected " + reportedVideoCount + " videos in playlist");
        logActivity("🔧 Sort mode: " + (sortMode === 'asc' ? 'Shortest First' : 'Longest First'));
        logActivity("📜 Auto-scroll: " + (autoScrollInitialVideoList === 'true' ? 'Sort all videos' : 'Sort only loaded'));

        if (filterEnabled) {
            const formatTime = (sec) => {
                const hours = Math.floor(sec / 3600);
                const minutes = Math.floor((sec % 3600) / 60);
                if (hours > 0) return hours + "h " + minutes + "m";
                if (minutes > 0) return minutes + "m";
                return "0m";
            };
            logActivity("🎯 Filter: " + formatTime(filterMinDuration) + " - " + formatTime(filterMaxDuration));
        }

        let videoPairs = getPlaylistVideoPairs();
        let allDragPoints = videoPairs.map(pair => pair.drag);
        let allAnchors = videoPairs.map(pair => pair.anchor);
        let allItems = videoPairs.map(pair => pair.item);
        let sortedCount = 0;
        let initialVideoCount = videoPairs.length;
        logActivity("📥 Currently loaded: " + initialVideoCount + " videos");

        let scrollRetryCount = 0;
        let maxScrollRetries = 10; // Maximum number of scroll retries
        let noProgressCount = 0; // Track consecutive attempts with no progress
        stopSort = false;

        // Always check for new content first (whether "Sort all" or "Sort only loaded")
        // Keep scrolling until no new videos load for 3 consecutive attempts
        while (
            document.URL.includes("playlist?list=") &&
            stopSort === false &&
            scrollRetryCount < maxScrollRetries
        ) {

            let previousCount = videoPairs.length;

            if (autoScrollInitialVideoList === 'true') {
                logActivity("Loading more videos - " + allDragPoints.length + " / " + reportedVideoCount + " videos loaded", true, true);

                if (initialVideoCount > 600) {
                    logActivity("⚠️  Sorting may take extremely long time/is likely to bug out");
                } else if (initialVideoCount > 300) {
                    logActivity("⚠️  Number of videos loaded is high, sorting may take a long time");
                }

                await autoScroll();
                // Wait for YouTube to load more content
                await wait(scrollLoopTime * 2);
            } else {
                logActivity("Checking for videos - " + allDragPoints.length + " loaded (will sort when stable)", true, true);
                // Give YouTube a moment to stabilise without loading more content
                await wait(scrollLoopTime);
            }

            videoPairs = getPlaylistVideoPairs();
            allDragPoints = videoPairs.map(pair => pair.drag);
            allAnchors = videoPairs.map(pair => pair.anchor);
            allItems = videoPairs.map(pair => pair.item);
            initialVideoCount = videoPairs.length;

            // Check if we're making progress
            if (previousCount === initialVideoCount) {
                noProgressCount++;
                scrollRetryCount++;
                logActivity("No new videos loaded. Attempt " + noProgressCount + "/3", true, true);

                // If no progress after 3 attempts, we're done loading
                if (noProgressCount >= 3) {
                    logActivity("✓ No new content detected. Ready to sort!");
                    break;
                }
            } else {
                noProgressCount = 0; // Reset counter if we're making progress
                scrollRetryCount = 0;
                logActivity("📈 Progress: Loaded " + (initialVideoCount - previousCount) + " new videos");
            }

            // For "Sort all" mode, check if we've reached the target
            if (autoScrollInitialVideoList === 'true') {
                // If we're close to the target (within 10 videos), give it one more try
                if (((reportedVideoCount - initialVideoCount) <= 10) && noProgressCount < 2) {
                    logActivity("Almost there! " + (reportedVideoCount - initialVideoCount) + " videos remaining...", true, true);
                    continue;
                }

                // If count matches, we're done!
                if (reportedVideoCount === initialVideoCount) {
                    logActivity("🎉 All " + reportedVideoCount + " videos loaded successfully!");
                    break;
                }
            }

            // For "Sort only loaded" mode, stop after we confirm no new content
            if (autoScrollInitialVideoList === 'false' && noProgressCount >= 3) {
                break;
            }
        }

        if (scrollRetryCount >= maxScrollRetries) {
            logActivity("⚠️  Max retry attempts reached. Proceeding with " + initialVideoCount + " videos.");
        }

        logActivity("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
        logActivity(initialVideoCount + " videos loaded. Starting sort...");
        if (scrollRetryCount > 5) logActivity("ℹ️  Note: Video count mismatch. This may be due to unavailable/private videos.");

        // For large playlists, we need to ensure all videos stay loaded
        logActivity("Preparing playlist for sorting...");

        // Calculate total moves needed for progress tracking
        // This gives us the initial count of videos that need to be moved
        const calculateTotalMoves = (anchors, items) => {
            let videos = [];
            for (let i = 0; i < anchors.length; i++) {
                const duration = getVideoDuration(anchors[i]);
                const title = getVideoTitle(items[i]);

                let time;
                if (filterEnabled && !passesFilter(duration)) {
                    time = sortMode === "asc" ? 999999999999999999 : -1;
                } else if (duration === null) {
                    time = sortMode === "asc" ? 999999999999999999 : -1;
                } else {
                    time = duration;
                }

                videos.push({ time, title, originalIndex: i });
            }

            // Sort to determine final order
            if (sortMode === "asc") {
                videos.sort((a, b) => {
                    if (a.time !== b.time) return a.time - b.time;
                    if (useTitleTiebreaker && a.title && b.title) {
                        return a.title.localeCompare(b.title);
                    }
                    return 0;
                });
            } else {
                videos.sort((a, b) => {
                    if (a.time !== b.time) return b.time - a.time;
                    if (useTitleTiebreaker && a.title && b.title) {
                        return a.title.localeCompare(b.title);
                    }
                    return 0;
                });
            }

            // Count videos that need to move
            let totalMoves = 0;
            for (let i = 0; i < videos.length; i++) {
                if (videos[i].originalIndex !== i) {
                    totalMoves++;
                }
            }
            return totalMoves;
        };

        // Initialize global move counters
        globalMoveCounter = 0;
        globalTotalMoves = calculateTotalMoves(allAnchors, allItems);
        if (globalTotalMoves > 0) {
            logActivity("📊 Total moves needed: " + globalTotalMoves);
        }

        scrollRetryCount = 0;
        let reloadFailures = 0; // Track consecutive reload failures
        let maxReloadFailures = 3; // Max times we can fail to reload before giving up

        while (sortedCount < initialVideoCount && stopSort === false) {
            // CRITICAL: Re-load entire playlist before each sort iteration to prevent lazy unloading
            logActivity("⚙️  Ensuring all videos are loaded before sort iteration...", true, true);

            if (autoScrollInitialVideoList === 'true') {
                // Scroll to bottom to load all videos
                await autoScroll();
                await wait(scrollLoopTime);

                // Scroll to top to ensure top videos are loaded too
                if (document.scrollingElement) {
                    document.scrollingElement.scrollTop = 0;
                }
                await wait(scrollLoopTime);

                // Now get fresh references
                videoPairs = getPlaylistVideoPairs();
                allDragPoints = videoPairs.map(pair => pair.drag);
                allAnchors = videoPairs.map(pair => pair.anchor);
                allItems = videoPairs.map(pair => pair.item);
                const loadedCount = videoPairs.length;

                // Verify we have all videos
                if (loadedCount !== initialVideoCount) {
                    logActivity("⚠️  Video count mismatch! Expected: " + initialVideoCount + ", Got: " + loadedCount, true, true);
                    logActivity("Re-loading playlist (attempt " + (reloadFailures + 1) + "/" + maxReloadFailures + ")...", true, true);

                    // Try to reload with more aggressive scrolling
                    await autoScroll();
                    await wait(scrollLoopTime * 3); // Longer wait
                    if (document.scrollingElement) {
                        document.scrollingElement.scrollTop = 0;
                    }
                    await wait(scrollLoopTime * 3);

                    // Scroll one more time to bottom and back
                    await autoScroll();
                    await wait(scrollLoopTime * 2);
                    if (document.scrollingElement) {
                        document.scrollingElement.scrollTop = 0;
                    }
                    await wait(scrollLoopTime * 2);

                    videoPairs = getPlaylistVideoPairs();
                    allDragPoints = videoPairs.map(pair => pair.drag);
                    allAnchors = videoPairs.map(pair => pair.anchor);
                    allItems = videoPairs.map(pair => pair.item);
                    const reloadedCount = videoPairs.length;

                    if (reloadedCount !== initialVideoCount) {
                        reloadFailures++;
                        if (reloadFailures >= maxReloadFailures) {
                            logActivity("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                            logActivity("❌ YouTube keeps unloading videos - playlist too large for current method");
                            logActivity("💡 Solutions:");
                            logActivity("   1. Refresh the page and try 'Sort only loaded' mode");
                            logActivity("   2. Refresh and try again with a higher 'Scroll Retry Time' (1000-2000ms)");
                            logActivity("   3. For playlists >200 videos, sort in smaller batches");
                            logActivity("📊 Progress saved: Sorted " + sortedCount + " of " + initialVideoCount + " videos");
                            return;
                        }
                        // If not max failures yet, continue the loop and try again
                        logActivity("⚠️  Still can't load all videos. Will retry on next iteration...", true, true);
                        continue; // Skip this iteration and try again
                    } else {
                        // Successfully reloaded, reset failure counter
                        reloadFailures = 0;
                    }
                } else {
                    // Video count is correct, reset failure counter
                    reloadFailures = 0;
                }

                logActivity("✓ All " + initialVideoCount + " videos confirmed loaded", true, true);
            } else {
                // Keep working set limited to what was already loaded
                await wait(scrollLoopTime / 2);
                videoPairs = getPlaylistVideoPairs();
                allDragPoints = videoPairs.map(pair => pair.drag);
                allAnchors = videoPairs.map(pair => pair.anchor);
                allItems = videoPairs.map(pair => pair.item);
                const loadedCount = videoPairs.length;

                if (loadedCount !== initialVideoCount) {
                    logActivity("ℹ️  Loaded video count changed from " + initialVideoCount + " to " + loadedCount + ". Using current loaded set.", true, true);
                    initialVideoCount = loadedCount;
                    if (sortedCount >= initialVideoCount) {
                        sortedCount = Math.max(0, initialVideoCount - 1);
                    }
                }

                if (initialVideoCount === 0) {
                    logActivity("❌ No videos remain loaded. Stopping sort.");
                    return;
                }

                reloadFailures = 0;
                logActivity("✓ Using currently loaded " + initialVideoCount + " videos", true, true);
            }

            // Position viewport near current sort position
            let viewportTarget = Math.max(0, Math.min(sortedCount, initialVideoCount - 1));
            keepVideoInView(viewportTarget, allAnchors);
            await wait(scrollLoopTime / 2);

            logActivity("Running sort iteration on position " + sortedCount + "...", true, true);
            sortedCount = Number(sortVideos(allAnchors, allDragPoints, allItems, initialVideoCount) + 1);

            // After each sort operation, give YouTube time to process
            await wait(scrollLoopTime * 3);
        }

        if (stopSort === true) {
            logActivity("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
            logActivity("⛔ Sort cancelled by user.");
            stopSort = false;
        } else {
            // Scroll to top to stabilize the view after sorting
            if (document.scrollingElement) {
                document.scrollingElement.scrollTop = 0;
            }

            logActivity("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
            logActivity("✅ Sort complete! Videos sorted: " + sortedCount);
            logActivity("🎉 Playlist is now sorted by duration!");
            logActivity("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
            logActivity("⚠️  WARNING: Do NOT switch back to any of YouTube's");
            logActivity("   automatic sorting methods (Date added, Most popular, etc.)");
            logActivity("   or it will break your custom sort!");
            logActivity("   Keep it on 'Manual' to preserve your duration sort.");
        }
    };

    /**
     * Initialization wrapper for all on-screen elements
     * Sets up the UI, buttons, and event listeners for the playlist sorter
     * Handles both NEW and OLD YouTube architectures
     * @returns {void}
     */
    let init = () => {
        // Wait for either NEW or OLD architecture to load
        const waitForPlaylist = () => {
            const newArch = document.querySelector(NEW_PAGE_HEADER_SELECTOR);
            const oldArch = document.querySelector(PLAYLIST_HEADER_SELECTOR);

            if (newArch || oldArch) {
                if (!renderContainerElement()) {
                    return;
                }
                addCssStyle();

                // Apply saved settings to UI
                applySettings(savedSettings);

                // Create button container
                const buttonContainer = document.querySelector('.sort-playlist-button');

                // Main action buttons row (50% width each)
                const mainButtonsRow = document.createElement('div');
                mainButtonsRow.style.display = 'flex';
                mainButtonsRow.style.gap = '8px';
                mainButtonsRow.style.marginBottom = '12px';

                const sortButton = document.createElement('button');
                sortButton.className = 'style-scope sort-button-wl sort-button-wl-default';
                sortButton.style.flex = '1';
                sortButton.style.fontWeight = '600';
                sortButton.style.padding = '9px 16px';
                sortButton.innerText = '▶ Sort Videos';
                sortButton.onclick = async () => { await activateSort(); };

                const stopButton = document.createElement('button');
                stopButton.className = 'style-scope sort-button-wl sort-button-wl-stop';
                stopButton.style.flex = '1';
                stopButton.style.fontWeight = '600';
                stopButton.style.padding = '9px 16px';
                stopButton.innerText = '🛑 Stop Sort';
                stopButton.onclick = () => { stopSort = true; };

                mainButtonsRow.appendChild(sortButton);
                mainButtonsRow.appendChild(stopButton);
                buttonContainer.appendChild(mainButtonsRow);

                // Small utility buttons row
                const utilityButtonsRow = document.createElement('div');
                utilityButtonsRow.style.display = 'flex';
                utilityButtonsRow.style.gap = '8px';

                // Create Stats button
                const statsButton = document.createElement('button');
                statsButton.className = 'style-scope sort-button-wl sort-button-wl-default';
                statsButton.style.fontSize = '11px';
                statsButton.style.padding = '4px 8px';
                statsButton.innerText = '📊 Stats';
                statsButton.onclick = async () => { await analyzePlaylist(); };

                // Create Export button
                const exportButton = document.createElement('button');
                exportButton.className = 'style-scope sort-button-wl sort-button-wl-default';
                exportButton.style.fontSize = '11px';
                exportButton.style.padding = '4px 8px';
                exportButton.innerText = '📥 Export';
                exportButton.onclick = async () => { await exportPlaylist(); };

                // Create Settings button
                const settingsButton = document.createElement('button');
                settingsButton.className = 'style-scope sort-button-wl sort-button-wl-default';
                settingsButton.style.fontSize = '11px';
                settingsButton.style.padding = '4px 8px';
                settingsButton.innerText = '⚙️ Settings';
                settingsButton.title = 'Configure default settings';
                settingsButton.onclick = () => { showSettingsPanel(); };

                // Create Show Log button
                const showLogButton = document.createElement('button');
                showLogButton.className = 'style-scope sort-button-wl sort-button-wl-default sort-log-toggle-btn';
                showLogButton.style.fontSize = '11px';
                showLogButton.style.padding = '4px 8px';
                showLogButton.innerText = logVisible ? 'Hide Log' : 'Show Log';
                showLogButton.onclick = () => {
                    logVisible = !logVisible;
                    showLogButton.innerText = logVisible ? 'Hide Log' : 'Show Log';
                    if (window.logControlsRow) {
                        window.logControlsRow.style.display = logVisible ? 'flex' : 'none';
                    }
                    log.style.display = logVisible ? 'block' : 'none';
                };

                utilityButtonsRow.appendChild(statsButton);
                utilityButtonsRow.appendChild(exportButton);
                utilityButtonsRow.appendChild(settingsButton);
                utilityButtonsRow.appendChild(showLogButton);
                buttonContainer.appendChild(utilityButtonsRow);

                // Only render log element (all settings now in Settings panel)
                renderLogElement();

                // Apply saved settings to UI (ensures log visibility matches saved preference)
                updateUIFromSettings();
            }
        };

        // Try immediate initialization
        waitForPlaylist();

        // Also set up observer for dynamic loading
        onElementReady(NEW_PAGE_HEADER_SELECTOR, false, waitForPlaylist);
        onElementReady(PLAYLIST_HEADER_SELECTOR, false, waitForPlaylist);
    };

    /**
     * Initialise script - IIFE
     */
    (() => {
        init();
        if (window.navigation && typeof window.navigation.addEventListener === 'function') {
            window.navigation.addEventListener('navigate', navigateEvent => {
                const url = new URL(navigateEvent.destination.url);
                if (url.pathname.includes('playlist?')) {
                    init();
                }
            });
        }
    })();

})(); // Close the main IIFE wrapper