Yet Another E-hentai Viewer

Fullscreen Image Viewer for E-hentai/Exhentai with a sidebar, preloading, dual page mode, and other stuff.

// ==UserScript==
// @name        Yet Another E-hentai Viewer
// @description Fullscreen Image Viewer for E-hentai/Exhentai with a sidebar, preloading, dual page mode, and other stuff.
// @namespace   Violentmonkey Scripts
// @match       https://exhentai.org/g/*
// @match       https://exhentai.org/s/*
// @match       https://e-hentai.org/g/*
// @match       https://e-hentai.org/s/*
// @icon        https://e-hentai.org/favicon.ico
// @version     1.12
// @author      shlsdv
// @license     MIT

// For configuration managment
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue

// Optional, can be removed. Removing breaks save image command (Shift+S).
// @grant       GM_download

// Optional, can be removed. Removing breaks Ctrl+V image paste if url is cross-origin.
// @grant       GM_xmlhttpRequest
// @connect     *

// @homepageURL https://greasyfork.org/en/scripts/531375-yet-another-e-hentai-viewer
// ==/UserScript==

(function () {
    "use strict";
    // ----------------------------------------------------------------------------------------------
    // thumbCollection.js
    // ----------------------------------------------------------------------------------------------

    /**
     * Represents an ordered collection of objects (typically representing thumbnails)
     * or null values. Objects are expected to potentially have a unique `href`.
     *
     * This collection provides features of both an Array and a Map:
     * - **Ordered Elements:** Maintains the insertion order of elements (including nulls),
     *   accessible via index (primarily through iteration or methods like `forEach`).
     *   Standard array manipulation methods like `push`, `unshift`, `splice`, and index-based
     *   removal are provided. Supports storing `null` or `undefined` values directly
     *   in the collection.
     * - **Fast Key Lookup:** Offers efficient O(1) average time lookup of non-null elements
     *   using their unique `href` (via `getByHref`). Null/undefined elements or elements
     *   without an `href` are not included in this lookup map.
     * - **Fast First/Last Item Access:** Provides O(1) access to the first and last
     *   non-null/undefined items in the collection via `first()` and `last()`.
     *
     * It synchronizes an internal array (`items`) for order and iteration with an internal
     * Map (`lookup` for href) for fast key-based access for applicable elements. It also
     * maintains indices (`_firstValidIndex`, `_lastValidIndex`) pointing to the first
     * and last non-null/undefined items for efficient retrieval.
     * All modification methods ensure this synchronization is maintained. Null/undefined
     * values are preserved in the `items` array but ignored by the `lookup` map and
     * the first/last item tracking.
     *
     * Note: While the class itself doesn't directly support bracket notation access
     * (e.g., `collection[0]`), it's designed to be easily wrapped by a Proxy
     * (e.g., via the `createThumbCollection` factory function)
     * to enable such syntax transparently while preserving the lookup and first/last item functionality.
     */
    class ThumbCollection {
        /**
         * @param {Array<object|null|undefined>} [initialItems=[]] - Optional initial array of items.
         *        Object items are expected to potentially have an `href` property (any type,
         *        used as key). `null` or `undefined` values are preserved in the order.
         */
        constructor(initialItems = []) {
            this.items = [];              // The array storing the items (including nulls) in order
            this.lookup = new Map();      // Map<href, itemObject> for fast href lookup (non-null items only)
            this._firstValidIndex = -1;   // Index of the first non-null/undefined item, or -1 if none
            this._lastValidIndex = -1;    // Index of the last non-null/undefined item, or -1 if none

            // Use internal helper to populate initial state
            this._initializeItems(initialItems);
        }

        /**
         * @private Internal helper to populate initial items and state.
         * @param {Array<object|null|undefined>} initialItems
         */
        _initializeItems(initialItems) {
            initialItems.forEach((item) => {
                // Always add the item (or null/undefined) to the main array
                this.items.push(item);
                const index = this.items.length - 1;

                // Handle valid items (non-null/undefined)
                if (item != null) {
                    // Update first/last valid indices
                    if (this._firstValidIndex === -1) {
                        this._firstValidIndex = index;
                    }
                    this._lastValidIndex = index; // Always update last valid index seen so far

                    // Add to href lookup if href exists
                    if (item.href != null) {
                        if (this.lookup.has(item.href)) {
                            console.warn(`[Constructor] Duplicate href "${item.href}" detected. Lookup will point to the last instance encountered.`);
                        }
                        this.lookup.set(item.href, item);
                    }
                }
            });
        }


        // --- Core Accessors ---

        /** Get item by its unique href. Returns undefined if href not found or item associated with href is null. */
        getByHref(href) {
            return this.lookup.get(href);
        }

        /** Get item by its array index (less common if using Proxy) */
        get(index) {
            // Handle potential sparse arrays if 'delete' was used unwisely, though splice is preferred.
            if (index >= 0 && index < this.items.length) {
                return this.items[index];
            }
            return undefined; // Consistent with array behavior for out-of-bounds
        }

        /** Get the first non-null/undefined item in the collection. O(1). */
        first() {
            return this._firstValidIndex !== -1 ? this.items[this._firstValidIndex] : undefined;
        }

        /** Get the last non-null/undefined item in the collection. O(1). */
        last() {
            return this._lastValidIndex !== -1 ? this.items[this._lastValidIndex] : undefined;
        }

        /** Get the internal array (use with caution, direct modification bypasses lookups and index tracking) */
        get array() {
            return this.items;
        }

        /** Get the number of items in the collection (including nulls/undefined) */
        get length() {
            return this.items.length;
        }

        // --- Modification Methods (Updating array, lookup map, and first/last indices) ---

        push(...itemsToAdd) {
            const originalLength = this.items.length;
            itemsToAdd.forEach((item, i) => {
                const currentIndex = originalLength + i;
                // Always add the item to the array
                this.items.push(item);

                // Handle valid items
                if (item != null) {
                    // Update first/last valid indices
                    if (this._firstValidIndex === -1) {
                        this._firstValidIndex = currentIndex;
                    }
                    this._lastValidIndex = currentIndex; // Pushed item is always the new last valid

                    // Update lookup map if href exists
                    if (item.href != null) {
                        if (this.lookup.has(item.href)) {
                            console.warn(`[push] Item with href "${item.href}" already exists or was added multiple times. Overwriting in lookup.`);
                        }
                        this.lookup.set(item.href, item);
                    }
                }
            });
            return this.items.length;
        }

        unshift(...itemsToAdd) {
            const numAdded = itemsToAdd.length;
            if (numAdded === 0) return this.items.length;

            const originalLength = this.items.length;
            const wasEmptyOrAllNull = this._firstValidIndex === -1;

            // Add all items to the beginning of the array first
            // We need to iterate *backwards* through itemsToAdd to add them correctly to the lookup
            // and maintain the 'last one wins' logic for duplicate hrefs within the added items.
            // However, Array.prototype.unshift does this automatically. Let's rebuild after.

            this.items.unshift(...itemsToAdd);

            // --- Rebuild Lookups and Boundaries ---
            // Unshift potentially changes many indices and can introduce duplicate hrefs
            // in complex ways. Rebuilding is the most reliable way to ensure correctness.
            this.rebuildLookupsAndBoundaries();

            return this.items.length;
        }

        removeByHref(href) {
            const itemToRemove = this.lookup.get(href);
            if (itemToRemove) { // Check if the href exists in the lookup
                // Find the *specific instance* in the array that the lookup points to
                // Use findIndex for potentially better performance than indexOf with objects
                const index = this.items.findIndex(item => item === itemToRemove);
                if (index > -1) {
                    // Now delegate to removeByIndex internal logic
                    this._removeIndexInternal(index, itemToRemove);
                    return true; // Successfully removed
                } else {
                    // This indicates an inconsistency: item was in lookup but not found in array.
                    console.warn(`[removeByHref] Item with href "${href}" found in lookup but corresponding instance not found in array. Removing from lookup only.`);
                    this.lookup.delete(href);
                    // Note: first/last indices might be inaccurate now, consider rebuildLookupsAndBoundaries() if this happens often.
                    return false; // Indicate potential inconsistency
                }
            }
            return false; // Not found by href in the lookup map
        }

        removeByIndex(index) {
            if (index < 0 || index >= this.items.length) {
                return false; // Index out of bounds
            }
            const itemToRemove = this.items[index];
            this._removeIndexInternal(index, itemToRemove);
            return true;
        }

        /**
         * Removes elements from the collection and optionally inserts new elements in their place,
         * returning the deleted elements. Updates lookups and boundaries.
         * Mimics Array.prototype.splice.
         *
         * @param {number} start - The zero-based index at which to start changing the array.
         *        If < 0, it will begin that many elements from the end.
         * @param {number} [deleteCount] - An integer indicating the number of elements in the array
         *        to remove from `start`. If omitted, or if >= number of elements from start to end,
         *        all elements from `start` to the end of the array will be deleted.
         * @param {...any} itemsToAdd - The elements to add to the array, beginning from `start`.
         *        If you don't specify any elements, splice() will only remove elements from the array.
         * @returns {Array<object|null|undefined>} An array containing the deleted elements.
         */
        splice(start, deleteCount, ...itemsToAdd) {
            // 1. Normalize start index (handle negative values like Array.splice)
            let actualStart = start;
            if (actualStart < 0) {
                actualStart = Math.max(this.items.length + actualStart, 0);
            } else {
                actualStart = Math.min(actualStart, this.items.length); // Clamp to length
            }

            // 2. Normalize deleteCount
            // If deleteCount is undefined, delete all from start
            let actualDeleteCount = (deleteCount === undefined)
                ? this.items.length - actualStart
                : Math.max(0, deleteCount);
            // Clamp deleteCount to number of available items from start
            actualDeleteCount = Math.min(actualDeleteCount, this.items.length - actualStart);

            // 3. Perform the splice on the internal array and get deleted items
            const deletedItems = this.items.splice(actualStart, actualDeleteCount, ...itemsToAdd);

            // 4. Update lookup map: Remove deleted items
            // We need to be careful if duplicate hrefs existed. Rebuilding the whole state
            // is the safest approach after a complex operation like splice.
            // deletedItems.forEach(item => {
            //     if (item != null && item.href != null) {
            //         // Only delete from lookup if THIS specific instance was mapped
            //         if (this.lookup.get(item.href) === item) {
            //             this.lookup.delete(item.href);
            //             // If another item with same href exists, rebuild will find it.
            //         }
            //     }
            // });

            // 5. Update lookup map: Add new items (will be handled by rebuild)
            // itemsToAdd.forEach(item => { ... });

            // 6. Update boundaries and lookup map
            // Rebuilding ensures consistency after complex removals/insertions/shifts.
            this.rebuildLookupsAndBoundaries();

            // 7. Return deleted items
            return deletedItems;
        }

        /**
         * @private Internal helper for removing an item by index and updating state.
         * @param {number} index - The index to remove.
         * @param {object|null|undefined} itemToRemove - The item being removed (pre-fetched).
         */
        _removeIndexInternal(index, itemToRemove) {
            // 1. Remove from array
            this.items.splice(index, 1);

            // 2. Remove from lookup if applicable
            if (itemToRemove && itemToRemove.href != null && this.lookup.get(itemToRemove.href) === itemToRemove) {
                this.lookup.delete(itemToRemove.href);
                // If a duplicate href exists earlier in the array, a rebuild might be needed
                // to restore the lookup correctly. Let's rely on rebuild for now.
                // Consider adding a targeted lookup fix here later if needed.
            }

            // 3. Update first/last valid indices
            // This requires careful handling of edge cases. Rebuilding is simpler.
            // this._updateBoundariesAfterRemoval(index, itemToRemove != null); // Old way
            this.rebuildLookupsAndBoundaries(); // Safer after removal, especially with duplicate hrefs
        }


        // --- Iteration ---

        forEach(callbackFn, thisArg) {
            this.items.forEach(callbackFn, thisArg);
        }

        map(callbackFn, thisArg) {
            return this.items.map(callbackFn, thisArg);
        }

        filter(callbackFn, thisArg) {
            // Note: filter creates a standard array, not a new ThumbCollection
            return this.items.filter(callbackFn, thisArg);
        }

        [Symbol.iterator]() {
            return this.items[Symbol.iterator]();
        }

        // --- Maintenance ---

        /** Rebuilds the href lookup map AND first/last valid indices based on the current state of the items array. */
        rebuildLookupsAndBoundaries() {
            this.lookup.clear();
            this._firstValidIndex = -1;
            this._lastValidIndex = -1;
            this.items.forEach((item, index) => {
                if (item != null) {
                    // Update boundaries
                    if (this._firstValidIndex === -1) {
                        this._firstValidIndex = index;
                    }
                    this._lastValidIndex = index;

                    // Update lookup
                    if (item.href != null) {
                        // Allow overwrites, last one wins during rebuild
                        this.lookup.set(item.href, item);
                    }
                }
            });
        }
        /** Alias for potential backward compatibility (if needed) */
        rebuildLookups() {
            this.rebuildLookupsAndBoundaries();
        }

        // --- PRIVATE HELPER METHODS ---

        /**
         * @private Internal helper to find and update the _firstValidIndex.
         * @param {number} [startIndex=0] - Index to start searching from.
         */
        _findAndUpdateFirstValidIndex(startIndex = 0) {
            for (let i = startIndex; i < this.items.length; i++) {
                if (this.items[i] != null) {
                    this._firstValidIndex = i;
                    return;
                }
            }
            // If no valid item found from startIndex onwards
            this._firstValidIndex = -1;
            // If first becomes -1, last must also be -1 if the search covered the whole array
            if (startIndex === 0) {
                this._lastValidIndex = -1;
            }
        }

        /**
         * @private Internal helper to find and update the _lastValidIndex.
         * @param {number} [startIndex=this.items.length - 1] - Index to start searching from (backwards).
         */
        _findAndUpdateLastValidIndex(startIndex = this.items.length - 1) {
            for (let i = startIndex; i >= 0; i--) {
                if (this.items[i] != null) {
                    this._lastValidIndex = i;
                    return;
                }
            }
            // If no valid item found from startIndex backwards
            this._lastValidIndex = -1;
            // If last becomes -1, first must also be -1 if the search covered the whole array
            if (startIndex === this.items.length - 1) {
                this._firstValidIndex = -1;
            }
        }


        /**
         * @private Updates lookup map and potentially first/last indices when an item is set via index (Proxy).
         * Called *after* the item has been set in the `items` array.
         * @param {number} index - The index being modified.
         * @param {object | null | undefined} newItem - The new item now at the index.
         * @param {object | null | undefined} oldItem - The item previously at the index.
         */
        _updateLookupsAndBoundaries(index, newItem, oldItem) {
            const oldItemWasValid = oldItem != null;
            const newItemIsValid = newItem != null;
            let needsBoundaryRebuild = false;

            // 1. Update href lookup map
            // Remove old item from lookup if it was the one mapped
            if (oldItemWasValid && oldItem.href != null && this.lookup.get(oldItem.href) === oldItem) {
                this.lookup.delete(oldItem.href);
                // Check if another item with the same href exists now needs to be in the lookup
                const existingItemWithSameHrefIndex = this.items.findLastIndex(item => item != null && item.href === oldItem.href);
                if (existingItemWithSameHrefIndex !== -1 && existingItemWithSameHrefIndex !== index) {
                    this.lookup.set(oldItem.href, this.items[existingItemWithSameHrefIndex]);
                }
            }
            // Add new item to lookup if it's valid and has an href
            if (newItemIsValid && newItem.href != null) {
                const existingHrefItem = this.lookup.get(newItem.href);
                if (existingHrefItem && existingHrefItem !== newItem) {
                    console.warn(`[_updateLookupsAndBoundaries] Overwriting href lookup for "${newItem.href}" which pointed to a different instance.`);
                }
                this.lookup.set(newItem.href, newItem);
            }

            // 2. Update first/last valid indices - check if boundaries might have changed
            if (oldItemWasValid !== newItemIsValid) {
                // Item changed validity status
                needsBoundaryRebuild = true;
            } else if (newItemIsValid) {
                // Item remained valid, check if it's at a boundary index
                if (index <= this._firstValidIndex || index >= this._lastValidIndex) {
                    needsBoundaryRebuild = true;
                }
            } else {
                // Item remained null/undefined, no boundary change possible from this specific index
            }


            // If the change *might* have affected boundaries, rebuild them.
            // It's simpler and safer than complex incremental updates for set().
            if (needsBoundaryRebuild || this.items.length <= 1) {
                this._findAndUpdateFirstValidIndex();
                // Only update last if first was found (avoids extra loop if empty)
                if (this._firstValidIndex !== -1) {
                    this._findAndUpdateLastValidIndex();
                } else {
                    this._lastValidIndex = -1; // Ensure last is also -1 if first is
                }
            }
            // If no rebuild needed (e.g. valid->valid in middle), indices remain correct.
        }

        // _updateBoundariesAfterRemoval is removed as _removeIndexInternal now uses rebuildLookupsAndBoundaries()
    }

    // --- Proxy Factory Function (No changes needed for splice, relies on method forwarding) ---

    /**
     * Creates a ThumbCollection instance wrapped in a Proxy for array-like bracket access.
     * Handles null/undefined values and updates the href lookup map and first/last item
     * tracking correctly.
     * @param {Array<object|null|undefined>} [initialItems=[]] - Optional initial array of items.
     * @returns {ThumbCollection & Proxy} - The proxied collection instance.
     */
    function createThumbCollection(initialItems = []) {
        const collectionInstance = new ThumbCollection(initialItems);

        const handler = {
            get(target, prop, receiver) {
                // Handle symbols (like Symbol.iterator) correctly
                if (typeof prop === 'symbol') {
                    const value = Reflect.get(target, prop, receiver);
                    if (typeof value === 'function') {
                        return value.bind(target);
                    }
                    return value;
                }

                // Check if prop is a valid array index (numeric string)
                const index = parseInt(prop, 10);
                // Check if it parses cleanly and the string representation matches
                if (String(prop) === String(index) && index >= 0) {
                    // Access the item directly from the internal array
                    // Use Reflect.get for consistency and potential future features
                    return Reflect.get(target.items, prop, receiver);
                }

                // Access other properties or methods (like getByHref, first, last, push, length etc.)
                const value = Reflect.get(target, prop, receiver);
                if (typeof value === 'function') {
                    // Bind methods to the collection instance
                    return value.bind(target);
                }
                return value; // Return other properties like length
            },

            set(target, prop, value, receiver) {
                // Check if prop is a valid array index (numeric string)
                const index = parseInt(prop, 10);
                if (String(prop) === String(index) && index >= 0) {
                    // Allow setting one past the end like arrays (causes length increase)
                    if (index > target.items.length) {
                        console.warn(`[Proxy set] Index ${index} is out of bounds (length is ${target.items.length}). Setting will create undefined holes.`);
                        // Allow Reflect.set to handle potential sparseness if desired
                    }

                    // Get the item currently at the index *before* overwriting it
                    const oldItem = target.items[index]; // Will be undefined if index >= target.items.length

                    // Set the value in the internal array. Use Reflect.set on the array itself.
                    const success = Reflect.set(target.items, prop, value, target.items);

                    if (success) {
                        // Update the lookup map and first/last indices based on the old and new items
                        // Pass the actual index used, the new value, and the old value
                        target._updateLookupsAndBoundaries(index, value, oldItem);
                    }
                    return success;
                }
                // Allow setting other properties directly on the ThumbCollection instance
                // Note: This bypasses _updateLookupsAndBoundaries; only intended for internal props if any
                // or potentially custom properties added outside the collection's core data.
                return Reflect.set(target, prop, value, target);
            },

            deleteProperty(target, prop) {
                // Prevent deleting symbol properties
                if (typeof prop === 'symbol') {
                    console.warn(`Attempted to delete symbol property: ${prop.toString()}`);
                    return false;
                }

                // Check if prop is a valid array index (numeric string)
                const index = parseInt(prop, 10);
                if (String(prop) === String(index) && index >= 0) {
                    // Check if the index is within the current bounds of the array
                    if (index < target.items.length) {
                        // Use the public removeByIndex method which handles everything
                        return target.removeByIndex(index);
                    } else {
                        // Index out of bounds, standard array delete returns true but does nothing
                        // For consistency with potential strict modes or expectations, return false.
                        return false;
                    }
                }

                // Prevent deletion of non-numeric properties (methods, internal properties) by default
                console.warn(`Attempted to delete non-index property: ${prop}. Deletion disallowed.`);
                return false;
            },

            has(target, prop) {
                // Handle symbols
                if (typeof prop === 'symbol') {
                    return Reflect.has(target, prop);
                }

                // Check if prop is a valid array index (numeric string)
                const index = parseInt(prop, 10);
                if (String(prop) === String(index) && index >= 0) {
                    // Standard array 'has' check (index must be within bounds and not an empty slot)
                    // Note: `target.items.hasOwnProperty(prop)` might be more accurate for sparse arrays
                    // but `index < target.items.length` is typical array behavior for `in`.
                    return index < target.items.length;
                }

                // Check for methods/properties defined on the ThumbCollection instance itself
                return Reflect.has(target, prop);
            },

            // --- Other traps (optional but good for robustness) ---

            ownKeys(target) {
                // Combine array indices with the target object's own keys
                const itemKeys = Object.keys(target.items);
                const targetKeys = Reflect.ownKeys(target);
                // Filter out duplicates ('length' might be in both) and non-enumerable if needed
                // Simple approach: combine and create a Set to deduplicate
                return [...new Set([...itemKeys, ...targetKeys])];
            },

            getOwnPropertyDescriptor(target, prop) {
                // Check if prop is an index
                const index = parseInt(prop, 10);
                if (String(prop) === String(index) && index >= 0 && index < target.items.length) {
                    // Get descriptor from the items array
                    return Reflect.getOwnPropertyDescriptor(target.items, prop);
                }
                // Otherwise, get descriptor from the target object itself
                return Reflect.getOwnPropertyDescriptor(target, prop);
            }
        };

        return new Proxy(collectionInstance, handler);
    }/**
 * Manages running asynchronous tasks (functions returning Promises) with a limited concurrency.
 */
    class PromisePool {
        /**
         * Creates an instance of PromisePool.
         * @param {number} maxConcurrency - The maximum number of tasks to run concurrently. Must be at least 1.
         */
        constructor(maxConcurrency = 5) {
            this.maxConcurrency = Math.max(1, Math.floor(maxConcurrency)); // Ensure positive integer >= 1
            this.activePromises = new Set(); // Stores the wrapper promises of active tasks
            this.queuedTasks = []; // Stores tasks waiting for a slot { taskFactory: Function, resolve: Function, reject: Function }
        }

        /**
         * Waits until a concurrency slot is available.
         * Should only be called when the pool is actually full.
         * @private
         */
        async #waitForSlot() {
            // This promise resolves when any active task finishes, freeing a slot.
            // We race against all active promises. The first one to settle (resolve/reject)
            // will cause Promise.race to settle. The finally block on that task's
            // wrapper promise will remove it from activePromises.
            try {
                await Promise.race(this.activePromises);
            } catch (err) {
                // Ignore errors here. Promise.race rejects if the first promise to settle rejects.
                // The error is handled by the individual task's catch block (or should be).
                // We only care that *a* slot became free.
            }
            // After race settles, a slot *should* be free due to the finally() block
            // in the wrapper promise created in _runTask. We might need to loop
            // in run(), but let's try without first. A check before adding is safer.
        }


        /**
         * Internal method to actually execute a task and manage its lifecycle within the pool.
         * @param {Function} taskFactory - A function that returns the Promise for the task.
         * @private
         */
        #runTask(taskFactory) {
            const taskPromise = taskFactory(); // Execute the function to get the actual promise

            // Create a wrapper promise that handles removing itself from the active set
            // regardless of whether the original task resolves or rejects.
            const managedPromise = taskPromise
                .catch(err => {
                    // Catch errors from the taskFactory's promise here
                    // to prevent unhandled rejections potentially crashing Promise.race
                    // or Promise.allSettled if the caller doesn't handle them.
                    // The caller should still handle errors on the promise returned by run().
                    // We don't re-throw here to allow other tasks to continue smoothly.
                    // console.error("PromisePool: Task encountered an error:", err); // Optional internal logging
                })
                .finally(() => {
                    this.activePromises.delete(managedPromise); // Remove itself from active set
                    // Check if there are queued tasks waiting
                    if (this.queuedTasks.length > 0) {
                        const nextTaskInfo = this.queuedTasks.shift();
                        // Run the next queued task directly since we know a slot is free
                        const nextTaskPromise = this.#runTask(nextTaskInfo.taskFactory);
                        // Link the original promise resolves/rejects
                        nextTaskPromise.then(nextTaskInfo.resolve, nextTaskInfo.reject);
                    }
                });

            this.activePromises.add(managedPromise); // Add the wrapper promise to the active set
            return taskPromise; // Return the *original* task's promise to the caller
        }


        /**
         * Submits a task to the pool. The task will run when a concurrency slot is available.
         *
         * @param {Function} taskFactory - A function that returns a Promise when called.
         *                                Example: () => fetch(url)
         * @returns {Promise<any>} A Promise that resolves or rejects with the result of the taskFactory's promise.
         */
        run(taskFactory) {
            return new Promise((resolve, reject) => {
                if (this.activePromises.size < this.maxConcurrency) {
                    // Enough slots, run immediately
                    const taskPromise = this.#runTask(taskFactory);
                    // Link the resolution/rejection back to the promise we return
                    taskPromise.then(resolve, reject);
                } else {
                    // Pool is full, queue the task
                    // console.log(`PromisePool: Pool full (${this.activePromises.size}). Queuing task.`);
                    this.queuedTasks.push({ taskFactory, resolve, reject });
                }
            });

            // ---- Simpler approach without explicit queue ----
            // Need async here to use await
            /* async run(taskFactory) {
                 // Wait if the pool is full
                 while (this.activePromises.size >= this.maxConcurrency) {
                     await this.#waitForSlot();
                 }
                 // Now a slot is free, run the task
                 return this.#runTask(taskFactory);
            } */
            // Chose the queue approach as it feels slightly more robust in managing
            // the exact start time rather than potentially multiple checks after Promise.race.
        }

        /**
         * Returns a Promise that resolves when all currently active and queued tasks have settled (completed or failed).
         * @returns {Promise<void>}
         */
        async waitAll() {
            // Wait until both the active set AND the queue are empty.
            while (this.activePromises.size > 0 || this.queuedTasks.length > 0) {
                if (this.activePromises.size > 0) {
                    try {
                        // Wait for *all* currently active promises to settle.
                        // Using allSettled ensures we wait even if some fail.
                        await Promise.allSettled(this.activePromises);
                    } catch (e) { /* Should not happen with allSettled */ }
                } else {
                    // If active is empty but queue is not, it might mean tasks finished
                    // very quickly. Add a small delay to allow the event loop to potentially
                    // process the next queued item triggered by a finally() block.
                    await new Promise(resolve => setTimeout(resolve, 0));
                }
                // Loop continues check in case new items were queued while waiting
            }
        }
    }
    // ----------------------------------------------------------------------------------------------
    // config.js
    // ----------------------------------------------------------------------------------------------

    function sanitizeKeyForHtmlId(key) {
        // Replace spaces, parentheses, and dots with underscores
        return key.replace(/[\s().]/g, '_');
    }

    class Config {
        constructor(title, configDefinitions = {}, closeOnClickAway = true, requiresReload = false) {
            this.title = title;
            this.originalDefinitions = configDefinitions;
            this.data = {}; // Will hold the nested configuration data
            this.flatDefinitions = {}; // Will hold the flattened definitions map (flatKey -> definition)
            this._reloadListeners = []; // Array of reload listeners
            this.hiddenTabs = new Set(); // Stores names of tabs explicitly marked as hidden
            this._listeners = []; // Array of save click listeners
            this._buildDataAndFlatDefinitions(this.originalDefinitions, this.data);

            // Load needs the data structure and flat definitions
            this.load();

            // Accessors need the loaded data
            this._createAccessors(); // Modified call

            this.registerMenuCommand();
            this.overlay = null;
            this.boundEscapeHandler = this.handleEscape.bind(this);
            this.boundClickAwayHandler = this.handleClickAway.bind(this);
            this.activeTab = null;
            this.closeOnClickAway = closeOnClickAway;
            this.requiresReload = requiresReload;
        }

        onSaveClick(callback) {
            if (typeof callback === 'function') {
                this._listeners.push(callback);
            }
            // return unsubscribe function
            return () => { this.offSaveClick(callback); };
        }

        offSaveClick(callback) {
            this._listeners = this._listeners.filter(listener => listener !== callback);
        }

        _notifyListeners() {
            this._listeners.forEach((listener, index) => {
                try {
                    listener();
                } catch (error) {
                    console.error(`Config: Error executing save listener at index ${index}:`, error);
                }
            });
        }

        onReload(callback) {
            if (typeof callback === 'function') {
                this._reloadListeners.push(callback);
            }
            return () => { this.offReload(callback); }; // Return unsubscribe function
        }

        offReload(callback) {
            this._reloadListeners = this._reloadListeners.filter(listener => listener !== callback);
        }

        _notifyReloadListeners() {
            this._reloadListeners.forEach((listener, index) => {
                try {
                    listener();
                } catch (error) {
                    console.error(`Config: Error executing reload listener at index ${index}:`, error);
                }
            });
        }

        _buildDataAndFlatDefinitions(sourceDefinitions, currentDataLevel, pathParts = [], currentTab = 'General', hideAll = false) {
            for (const key in sourceDefinitions) {
                if (!Object.prototype.hasOwnProperty.call(sourceDefinitions, key)) continue;

                // Skip special metadata keys
                if (key.startsWith('_')) continue;

                const value = sourceDefinitions[key];
                const newPathParts = [...pathParts, key];

                if (typeof value === 'object' && value !== null) {
                    // Check if it's a leaf setting (has 'default') OR a description text (has 'text: true')
                    const isLeafSetting = Object.prototype.hasOwnProperty.call(value, 'default');
                    const isDescriptionText = value.text === true; // Check for the description flag

                    if (isLeafSetting || isDescriptionText) {
                        // Leaf definition (setting or description)
                        const flatKey = newPathParts.join('.');
                        const definition = { ...value, tab: currentTab, originalKey: key }; // Store original key
                        if (hideAll) definition.hidden = true;

                        this.flatDefinitions[flatKey] = definition;
                        // Assign a default value: use definition.default if present, otherwise null (for description)
                        currentDataLevel[key] = isLeafSetting ? definition.default : null;
                    } else {
                        // It's a nested sub-config object (potentially a Tab Group definition, but
                        // structure in `this.data` should always reflect the definition nesting).
                        // The _subgroup flag is ignored for data structure purposes here.
                        // Flat keys in flatDefinitions will still retain the full path.
                        // Check for _tabName in the group definition
                        const newTabName = value._tabName || ((pathParts.length === 0) ? key : currentTab);

                        if (!Object.prototype.hasOwnProperty.call(currentDataLevel, key)) {
                            currentDataLevel[key] = {};
                        }
                        // Check if this group itself should be hidden
                        if (value._hidden === true) {
                            // Mark the determined tab name as hidden
                            this.hiddenTabs.add(newTabName);
                            // This will set the "hidden" property of all children to true
                            hideAll = true;
                        }
                        // Pass the determined tab name down in the recursive call
                        this._buildDataAndFlatDefinitions(value, currentDataLevel[key], newPathParts, newTabName, hideAll);
                    }
                } else {
                    console.warn(`Config: Invalid definition format for key "${key}" at path "${newPathParts.join('.')}"`, value);
                }
            }
        }

        _getValueByFlatKey(flatKey) {
            const keys = flatKey.split('.');
            let current = this.data;
            for (const key of keys) {
                if (current === null || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, key)) {
                    return undefined;
                }
                current = current[key];
            }
            return current;
        }

        _setValueByFlatKey(flatKey, value) {
            const keys = flatKey.split('.');
            let current = this.data;
            for (let i = 0; i < keys.length - 1; i++) {
                const key = keys[i];
                if (current === null || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, key)) {
                    console.error(`Config: Cannot set value for key "${flatKey}". Invalid path segment "${key}".`);
                    return false;
                }
                current = current[key];
            }
            if (current === null || typeof current !== 'object') {
                console.error(`Config: Cannot set value for key "${flatKey}". Final path segment parent is not an object.`);
                return false;
            }
            current[keys[keys.length - 1]] = value;
            return true;
        }

        _updateDataFromForm(formElement) {
            console.log("Config: Reading values from form to update internal data.");
            Object.entries(this.flatDefinitions).forEach(([flatKey, definition]) => {
                if (definition.hidden === true || definition.text === true) return; // Skip hidden and description text

                const sanitizedKey = sanitizeKeyForHtmlId(flatKey);
                const input = formElement.querySelector(`#config-${sanitizedKey}`);
                if (!input) {
                    console.warn(`Config: Could not find input element for key ${flatKey}`);
                    return;
                }

                let newValue;
                if (input.tagName.toLowerCase() === "select") {
                    newValue = input.value;
                } else if (input.type === "checkbox") {
                    newValue = input.checked;
                } else if (input.type === "number") {
                    newValue = input.valueAsNumber;
                    if (isNaN(newValue)) newValue = definition.default; // Revert to default if invalid number
                } else { // text, etc.
                    newValue = input.value;
                }

                this._setValueByFlatKey(flatKey, newValue); // Update internal data
            });
        }

        _createAccessors() {
            const accessorRoot = this;

            for (const flatKey in this.flatDefinitions) {
                if (!Object.prototype.hasOwnProperty.call(this.flatDefinitions, flatKey)) continue;

                const pathParts = flatKey.split('.');
                const accessorKey = pathParts.pop(); // The leaf key name, e.g., "numColumns" or "replaceDefaultGridView"

                let currentTargetForProp = accessorRoot; // Where the final property 'accessorKey' will be defined
                let currentTargetForNesting = accessorRoot; // Tracks the object needed for the *next* level down (if not flattening)
                let currentDefinitionLevel = this.originalDefinitions;

                // Determine the target object based on _subgroup flags in the *original* definition structure
                for (const part of pathParts) { // Iterate parent path segments e.g., "Gallery (Embedded)", "embeddedGridViewConfig"
                    if (!currentDefinitionLevel || typeof currentDefinitionLevel !== 'object') {
                        console.error(`Config: Invalid definition structure encountered while processing "${part}" for flat key "${flatKey}"`);
                        currentTargetForProp = null; // Mark as invalid target
                        break;
                    }
                    let nextDefinitionLevel = currentDefinitionLevel[part];
                    const isFlattened = nextDefinitionLevel && typeof nextDefinitionLevel === 'object' && nextDefinitionLevel._subgroup === false;

                    // This logic determines where the *final* property (accessorKey) should live
                    if (!isFlattened) {
                        // If the *current* level definition (nextDefinitionLevel) is NOT flattened,
                        // the final property will live *inside* an object corresponding to 'part'.
                        // Ensure this nesting object exists on the *previous* target (currentTargetForNesting).
                        let nestingObject;
                        if (!Object.prototype.hasOwnProperty.call(currentTargetForNesting, part)) {
                            nestingObject = {};
                            Object.defineProperty(currentTargetForNesting, part, {
                                value: nestingObject,
                                enumerable: true,
                                configurable: false, // Prevent deletion of structure
                                writable: false
                            });
                        } else {
                            nestingObject = currentTargetForNesting[part];
                            // Check if it's a valid object for nesting
                            if (typeof nestingObject !== 'object' || nestingObject === null) {
                                console.error(`Config: Structure conflict. Expected object at "${pathParts.slice(0, pathParts.indexOf(part) + 1).join('.')}" but found non-object when defining "${flatKey}".`);
                                currentTargetForProp = null; // Mark as invalid target
                                break; // Stop processing this flatKey
                            }
                        }
                        currentTargetForProp = nestingObject; // The prop will live here
                        currentTargetForNesting = nestingObject; // Next potential nesting starts from here

                    } else {
                        // If the *current* level definition IS flattened (_subgroup: false),
                        // the final property lives directly on the *previous* target (currentTargetForNesting).
                        currentTargetForProp = currentTargetForNesting; // Reaffirm target is the parent level
                        // currentTargetForNesting technically stays the same for the next iteration of this loop,
                        // as the flattened group doesn't introduce a new level in the *accessor* hierarchy.
                    }

                    // Move down the definition structure for the next iteration's check
                    currentDefinitionLevel = nextDefinitionLevel;
                } // End loop through parent path parts

                // Define the final leaf accessor property if the target is valid
                if (currentTargetForProp) {
                    if (!Object.prototype.hasOwnProperty.call(currentTargetForProp, accessorKey)) {
                        Object.defineProperty(currentTargetForProp, accessorKey, {
                            get: () => this._getValueByFlatKey(flatKey),
                            set: (newValue) => {
                                // Optional: Add type validation/coercion based on definition.default type here?
                                if (this._setValueByFlatKey(flatKey, newValue)) {
                                    this.save();
                                }
                            },
                            enumerable: true,
                            configurable: true // Allows user to modify/delete later if needed
                        });
                    } else {
                        // Property already exists. This could be due to multiple flatKeys attempting
                        // to define the same property path (e.g., an error in definitions or complex flattening).
                        console.warn(`Config: Accessor key "${accessorKey}" already exists on target for flat key "${flatKey}". Check for definition conflicts.`);
                    }
                } else {
                    // Error occurred during path traversal, logged above.
                    //  console.error(`Config: Could not define accessor for "${flatKey}" due to structure issues.`);
                }
            } // End loop through flatKeys
        }

        load() {
            console.log("Config: Loading config values from local storage");
            for (const flatKey in this.flatDefinitions) {
                const storedValue = GM_getValue(flatKey);
                if (typeof storedValue !== 'undefined') {
                    this._setValueByFlatKey(flatKey, storedValue);
                }
            }
        }

        save() {
            console.log("Config: Saving config values to local storage");
            for (const flatKey in this.flatDefinitions) {
                const definition = this.flatDefinitions[flatKey];
                const currentValue = this._getValueByFlatKey(flatKey);
                if (currentValue !== undefined && currentValue !== definition.default) {
                    GM_setValue(flatKey, currentValue);
                } else {
                    GM_deleteValue(flatKey);
                }
            }
        }

        handleEscape(e) {
            if (e.key === "Escape" && this.overlay) {
                this.closeUI();
            }
        }

        handleClickAway(e) {
            if (this.closeOnClickAway && this.overlay) {
                const configContainer = this.overlay.querySelector('div');
                if (configContainer && !configContainer.contains(e.target)) {
                    this.closeUI();
                }
            }
        }

        closeUI() {
            if (this.overlay && this.overlay.parentNode) {
                this.overlay.parentNode.removeChild(this.overlay);
                this.overlay = null;
                document.removeEventListener("keydown", this.boundEscapeHandler);
                document.removeEventListener("click", this.boundClickAwayHandler);
            }
        }

        showingUI() {
            return !!this.overlay;
        }

        _updateElementDisabledState(elementWrapper, inputElement, isDisabled, definition) {
            const onConditionFail = definition.onConditionFail;

            if (!onConditionFail || onConditionFail == "disable") {
                inputElement.disabled = isDisabled;
                elementWrapper.style.opacity = isDisabled ? '0.5' : '1';
                elementWrapper.style.pointerEvents = isDisabled ? 'none' : 'auto';
            } else if (typeof onConditionFail === 'object') {
                if (onConditionFail.label) {
                    const labelElement = elementWrapper.querySelector('label');
                    if (isDisabled) {
                        labelElement.textContent = onConditionFail.label;
                    } else {
                        labelElement.textContent = definition.label;
                    }
                }
            } else {
                console.warn("Invalid onConditionFail: ", onConditionFail);
            }
        }

        _createTabButton(tabName, tabContainer, contentContainer) {
            const button = document.createElement("button");
            button.textContent = tabName;
            button.type = "button";
            button.style.padding = "0.5em 1em";
            button.style.marginRight = "0.5em";
            button.style.marginBottom = "-1px";
            button.style.border = "1px solid #555";
            button.style.borderRadius = "3px 3px 0 0";
            button.style.cursor = "pointer";
            button.style.backgroundColor = "#444";
            button.style.color = "#ccc";

            button.addEventListener("click", () => {
                Array.from(tabContainer.children).forEach(btn => {
                    btn.style.backgroundColor = "#444";
                    btn.style.color = "#ccc";
                    btn.style.borderBottom = "1px solid #555";
                });
                Array.from(contentContainer.children).forEach(pane => pane.style.display = "none");

                button.style.backgroundColor = "#333";
                button.style.color = "#fff";
                button.style.borderBottom = "1px solid #333";
                const contentPane = contentContainer.querySelector(`[data-tab-content="${tabName}"]`);
                if (contentPane) contentPane.style.display = "block";
                this.activeTab = tabName;
            });
            return button;
        }

        showUI(parent) {
            if (this.showingUI()) return;
            if (!parent) parent = document.body;
            const overlay = document.createElement("div");
            overlay.style.position = "fixed";
            overlay.style.top = 0;
            overlay.style.left = 0;
            overlay.style.width = "100%";
            overlay.style.height = "100%";
            overlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
            overlay.style.zIndex = 1000000;
            overlay.style.display = "flex";
            overlay.style.justifyContent = "center";
            this.overlay = overlay;
            const configContainer = document.createElement("div");
            configContainer.style.padding = "1em";
            configContainer.style.backgroundColor = "#333";
            configContainer.style.color = "#fff";
            configContainer.style.maxHeight = "80vh";
            configContainer.style.alignSelf = "flex-start";
            configContainer.style.margin = "15vh auto 0 auto";
            configContainer.style.overflowY = "auto";
            configContainer.style.border = "1px solid #555";
            configContainer.style.boxShadow = "0 2px 8px rgba(0, 0, 0, 0.3)";
            configContainer.style.width = "500px";
            configContainer.style.borderRadius = "4px";
            configContainer.style.fontFamily = "Arial, sans-serif";
            if (this.title) {
                const titleElement = document.createElement("h2");
                titleElement.textContent = this.title;
                titleElement.style.marginTop = 0;
                titleElement.style.marginBottom = "1em";
                titleElement.style.textAlign = "center";
                configContainer.appendChild(titleElement);
            }

            const tabButtonContainer = document.createElement("div");
            tabButtonContainer.style.borderBottom = "1px solid #555";
            tabButtonContainer.style.marginBottom = "1em";
            const tabContentContainer = document.createElement("div");

            const tabs = {};
            // Map to store dependencies: controllerKey -> [dependentKey1, dependentKey2, ...]
            const dependencyMap = {};

            Object.entries(this.flatDefinitions).forEach(([flatKey, definition]) => {
                if (definition.hidden === true) return;
                const tabName = definition.tab || 'General';

                // --- Build Dependency Map ---
                if (definition.condition && Array.isArray(definition.condition) && definition.condition.length === 2) {
                    const [controllerKey, /* requiredValue */] = definition.condition;
                    if (!dependencyMap[controllerKey]) {
                        dependencyMap[controllerKey] = [];
                    }
                    dependencyMap[controllerKey].push(flatKey); // Store the dependent key
                }
                // --- End Build Dependency Map ---
                if (!tabs[tabName]) {
                    tabs[tabName] = [];
                }
                tabs[tabName].push(flatKey);
            });

            const allTabNames = Object.keys(tabs);
            const visibleTabNames = allTabNames.filter(name => !this.hiddenTabs.has(name));
            this.activeTab = visibleTabNames.length > 0 ? visibleTabNames[0] : null;

            const form = document.createElement("form");
            let previousFieldType = null; // Track the type of the previous field

            // --- Create Save & Reload button upfront if needed ---
            let saveReloadButton = null; // Reference to the Save & Reload button
            const anySettingRequiresReload = Object.values(this.flatDefinitions).some(def => def.requiresReload === true);
            const needsReloadButton = this.requiresReload || anySettingRequiresReload;

            if (needsReloadButton) {
                saveReloadButton = document.createElement("button");
                saveReloadButton.textContent = "Save & Reload";
                saveReloadButton.type = "button"; // Important: prevent form submission
                saveReloadButton.style.marginRight = "0.5em";
                saveReloadButton.style.padding = "0.5em 1em";
                saveReloadButton.style.backgroundColor = "#2196F3";
                saveReloadButton.style.border = "none";
                saveReloadButton.style.borderRadius = "3px";
                saveReloadButton.style.color = "#fff";
                saveReloadButton.style.cursor = "pointer";
                saveReloadButton.style.display = 'none'; // Initially hidden
            }
            const createFieldElement = (key) => {
                const self = this; // Capture 'this' for use in listeners
                const definition = this.flatDefinitions[key];
                const value = this._getValueByFlatKey(key);
                const fieldWrapper = document.createElement("div");
                fieldWrapper.style.marginBottom = "0.75em";
                fieldWrapper.style.display = "flex";
                fieldWrapper.style.alignItems = "center";

                // Add extra spacing only when switching between checkbox/description and other fields
                const currentIsCheckbox = typeof value === "boolean";
                const isDescription = definition.text === true;
                if (previousFieldType !== null && (currentIsCheckbox || isDescription) !== previousFieldType) {
                    fieldWrapper.style.marginTop = "2em";
                }
                previousFieldType = (currentIsCheckbox || isDescription);

                const labelElement = document.createElement("label");
                const sanitizedKey = sanitizeKeyForHtmlId(key); // Use the helper function
                labelElement.textContent = definition.label || key.split('.').pop();
                labelElement.htmlFor = `config-${sanitizedKey}`; // Use sanitized key
                labelElement.style.marginRight = "0.5em";
                labelElement.style.fontWeight = "bold";
                labelElement.style.textAlign = "left";
                let input;

                // Only set fixed width for non-checkbox inputs
                if (typeof value !== "boolean") {
                    labelElement.style.width = "200px";
                }

                // Check if it's a description text
                if (definition.text === true) {
                    const descriptionElement = document.createElement('p');
                    descriptionElement.textContent = definition.label || definition.originalKey || "Description text missing"; // Use label or original key
                    descriptionElement.style.margin = "0 0 0.75em 0"; // Adjust margin as needed
                    descriptionElement.style.fontStyle = "italic";
                    descriptionElement.style.color = "#bbb"; // Lighter color for description
                    return descriptionElement; // Return only the description paragraph
                }
                if (definition.choices && Array.isArray(definition.choices)) {
                    input = document.createElement("select");
                    input.id = `config-${sanitizedKey}`; // Use sanitized key
                    input.style.flexGrow = "1";
                    input.style.padding = "0.3em";
                    input.style.border = "1px solid #777";
                    input.style.borderRadius = "2px";
                    input.style.backgroundColor = "#555";
                    input.style.color = "#fff";
                    definition.choices.forEach((choice) => {
                        const option = document.createElement("option");
                        option.value = choice;
                        option.textContent = choice;
                        if (choice === value) {
                            option.selected = true;
                        }
                        input.appendChild(option);
                    });
                }
                else if (typeof value === "boolean") {
                    input = document.createElement("input");
                    input.id = `config-${sanitizedKey}`; // Use sanitized key
                    input.type = "checkbox";
                    input.checked = value;
                    input.style.transform = "scale(1.2)";
                } else if (typeof value === "number") {
                    input = document.createElement("input");
                    input.id = `config-${sanitizedKey}`; // Use sanitized key
                    input.type = "number";
                    input.value = value;
                    input.style.flexGrow = "1";
                    input.style.padding = "0.3em";
                    input.style.border = "1px solid #777";
                    input.style.borderRadius = "2px";
                    input.style.backgroundColor = "#555";
                    input.style.color = "#fff";
                } else {
                    input = document.createElement("input");
                    input.id = `config-${sanitizedKey}`; // Use sanitized key
                    input.type = "text";
                    input.value = value;
                    input.style.flexGrow = "1";
                    input.style.padding = "0.3em";
                    input.style.border = "1px solid #777";
                    input.style.borderRadius = "2px";
                    input.style.backgroundColor = "#555";
                    input.style.color = "#fff";
                }
                // Append elements in the correct order (input first for checkbox visual)
                if (input.type === 'checkbox') {
                    input.style.marginRight = "0.5em"; // Add margin to separate from label
                    fieldWrapper.appendChild(input);
                    fieldWrapper.appendChild(labelElement);
                } else {
                    fieldWrapper.appendChild(labelElement);
                    fieldWrapper.appendChild(input);
                }

                // --- Initial Condition Check ---
                if (definition.condition && Array.isArray(definition.condition) && definition.condition.length === 2) {
                    const [controllerKey, requiredValue] = definition.condition;
                    const controllerDefinition = this.flatDefinitions[controllerKey];
                    if (controllerDefinition) {
                        const controllerValue = this._getValueByFlatKey(controllerKey);
                        const isDisabled = controllerValue !== requiredValue;
                        this._updateElementDisabledState(fieldWrapper, input, isDisabled, definition);
                    } else {
                        console.warn(`Config: Controller key "${controllerKey}" for dependent key "${key}" not found in definitions.`);
                    }
                }
                // --- End Initial Condition Check ---

                // Add change listener to potentially show the Save & Reload button
                // AND add listener if this element is a *controller* for other elements
                if (input) { // Check if input exists (not a description text)
                    // Use 'click' for checkboxes for immediate feedback, 'input' for others
                    const eventType = (input.type === 'checkbox') ? 'click' : 'input';

                    // --- Listener for CONTROLLER elements ---
                    if (dependencyMap[key]) { // Check if this key controls others
                        input.addEventListener(eventType, (event) => {
                            let newValue;
                            if (input.type === 'checkbox') newValue = event.target.checked;
                            else if (input.type === 'number') newValue = event.target.valueAsNumber;
                            else newValue = event.target.value; // string, select

                            // Update dependent elements
                            dependencyMap[key].forEach(dependentKey => {
                                const dependentDefinition = self.flatDefinitions[dependentKey];
                                const requiredValue = dependentDefinition.condition[1];
                                const depSanitizedKey = sanitizeKeyForHtmlId(dependentKey);
                                const depInput = form.querySelector(`#config-${depSanitizedKey}`);
                                const depWrapper = depInput ? depInput.closest('div') : null; // Find the wrapper div

                                if (depWrapper && depInput) {
                                    const shouldBeDisabled = newValue !== requiredValue;
                                    self._updateElementDisabledState(depWrapper, depInput, shouldBeDisabled, dependentDefinition);
                                }
                            });
                        });
                    }
                    // --- End Listener for CONTROLLER elements ---

                    input.addEventListener(eventType, () => {
                        const settingDefinition = this.flatDefinitions[key];
                        const settingRequiresReload = settingDefinition.requiresReload === true ||
                            (settingDefinition.requiresReload !== false && this.requiresReload === true);

                        if (settingRequiresReload && saveReloadButton) { // saveReloadButton reference is now correct
                            saveReloadButton.style.display = ''; // Show the button
                        }
                    });
                }
                return fieldWrapper;
            };

            visibleTabNames.forEach(tabName => {
                const button = this._createTabButton(tabName, tabButtonContainer, tabContentContainer);
                tabButtonContainer.appendChild(button);

                const contentPane = document.createElement("div");
                contentPane.dataset.tabContent = tabName;
                contentPane.style.display = "none";
                tabs[tabName].forEach(key => {
                    contentPane.appendChild(createFieldElement(key));
                });
                tabContentContainer.appendChild(contentPane);
            });

            const buttonsWrapper = document.createElement("div");
            buttonsWrapper.style.marginTop = "1.5em";
            buttonsWrapper.style.paddingTop = "1em";
            buttonsWrapper.style.textAlign = "right";
            const saveButton = document.createElement("button");
            saveButton.textContent = "Save Config";
            saveButton.type = "submit";
            saveButton.style.marginRight = "0.5em";
            saveButton.style.padding = "0.5em 1em";
            saveButton.style.backgroundColor = "#4CAF50";
            saveButton.style.border = "none";
            saveButton.style.borderRadius = "3px";
            saveButton.style.color = "#fff";
            saveButton.style.cursor = "pointer";
            const cancelButton = document.createElement("button");
            cancelButton.textContent = "Cancel";
            cancelButton.type = "button";
            cancelButton.style.padding = "0.5em 1em";
            cancelButton.style.backgroundColor = "#f44336";
            cancelButton.style.border = "none";
            cancelButton.style.borderRadius = "3px";
            cancelButton.style.color = "#fff";
            cancelButton.style.cursor = "pointer";
            cancelButton.addEventListener("click", () => {
                this.closeUI();
            });
            // Append the Save & Reload button if it was created
            if (saveReloadButton) {
                saveReloadButton.addEventListener("click", () => {
                    this._updateDataFromForm(form);
                    this.save();
                    this._notifyListeners();
                    this._notifyReloadListeners();
                    this.closeUI();
                    window.location.reload();
                });
                buttonsWrapper.appendChild(saveReloadButton);
            }
            buttonsWrapper.appendChild(saveButton);
            buttonsWrapper.appendChild(cancelButton);

            form.appendChild(tabContentContainer);
            form.appendChild(buttonsWrapper);
            configContainer.appendChild(tabButtonContainer);
            configContainer.appendChild(form);
            form.addEventListener("submit", (e) => {
                e.preventDefault();
                this._updateDataFromForm(form);
                this.save();
                this._notifyListeners();
                this.closeUI();
            });

            overlay.appendChild(configContainer);
            const firstTabButton = tabButtonContainer.querySelector('button');
            if (firstTabButton) firstTabButton.click();

            parent.appendChild(overlay);
            document.addEventListener("keydown", this.boundEscapeHandler);
            // Add slight delay to prevent instant close when opening
            setTimeout(() => {
                document.addEventListener("click", this.boundClickAwayHandler);
            }, 50);
        }

        registerMenuCommand(name = "Configuration") {
            GM_registerMenuCommand(name, () => this.showUI());
        }
    }
    // ----------------------------------------------------------------------------------------------
    // utils.js
    // ----------------------------------------------------------------------------------------------

    // Determine the current page index from URL parameter.
    function getPageIndexFromUrl(url) {
        const params = new URL(url).searchParams;
        const p = params.get("p");
        return p ? parseInt(p, 10) : 0;
    }

    function getTotalPages() {
        const pageLinks = Array.from(document.querySelectorAll('table.ptt a'))
            .filter(a => /^\d+$/.test(a.textContent));
        if (pageLinks.length === 0) return 1;
        const lastPageLink = pageLinks[pageLinks.length - 1];
        const pParam = getPageIndexFromUrl(lastPageLink.href);
        return pParam ? pParam + 1 : parseInt(lastPageLink.textContent, 10);
    }

    function getTotalImages() {
        const elements = document.querySelectorAll('td.gdt2');
        let pageCount = null;

        elements.forEach(el => {
            const text = el.textContent.trim();
            const match = text.match(/(\d+)\s*pages/i);
            if (match) {
                pageCount = parseInt(match[1], 10);
            }
        });

        if (pageCount !== null) {
            return pageCount;
        } else {
            console.warn('Image count not found.');
        }
    }

    function getThumbOffset(backgroundString, itemWidth) {
        const sanitizedBackground = backgroundString.replace(/url\([^)]+\)/, '');
        const tempDiv = document.createElement('div');
        tempDiv.style.cssText = `
    position: absolute; visibility: hidden; width: 0; height: 0;
    background: ${sanitizedBackground};
    `;
        document.body.appendChild(tempDiv);
        let backgroundPositionX = window.getComputedStyle(tempDiv).backgroundPositionX;
        document.body.removeChild(tempDiv);

        backgroundPositionX = parseFloat(backgroundPositionX);

        if (itemWidth) {
            const scale = itemWidth / THUMB_WIDTH;
            return scale * backgroundPositionX;
        }
        return backgroundPositionX;
    }

    function imageIndexToPageIndex(imageIndex) {
        return Math.floor(imageIndex / PAGINATION);
    }

    function getGalleryId() {
        try {
            const pathname = window.location.pathname;
            return pathname.split('/')[2];
        } catch {
            console.warn(`Error extracting gallery id from url ${window.location.pathname}`);
            return null;
        }
    }

    // requires thumbs.length global variable to be set
    function getSpritesheetWidthForPage(pageIndex) {
        const startIndex = pageIndex * PAGINATION;
        if (startIndex >= thumbs.length) {
            return 0;
        }
        const numImagesOnPage = Math.min(PAGINATION, thumbs.length - startIndex);
        return THUMB_WIDTH * numImagesOnPage;
    }

    function determineIfSpritesheets(initialThumbs) {
        if (!initialThumbs?.length) {
            return true;
        }
        const firstBg = initialThumbs[0].background;

        if (firstBg.includes(baseUrl) || firstBg.includes(".jpg")) {
            return false;
        }
        return true;
    }

    async function fetchDocument(url, signal = null) {
        try {
            const fetchOptions = {};
            if (signal instanceof AbortSignal) { // Only add signal if it's a valid AbortSignal
                fetchOptions.signal = signal;
            }
            const response = await fetch(url, fetchOptions);
            // Check if the request was aborted *after* the fetch promise resolves
            // Although fetch throws AbortError, this is an extra check in some edge cases.
            if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
            if (!response.ok)
                throw new Error(`HTTP error! status: ${response.status}`);
            const htmlText = await response.text();
            const parser = new DOMParser();
            return parser.parseFromString(htmlText, "text/html");
        } catch (error) {
            if (error.name === 'AbortError') {
                console.log(`Fetch aborted for ${url}`);
            } else {
                console.error("Error fetching document:", error);
            }
            throw error; // Re-throw the error after logging
        }
    }

    function getFirstByXpath(xpath, doc = document) {
        return doc.evaluate(
            xpath,
            doc,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        ).singleNodeValue;
    }

    const FULL_URL_XPATH = '//img[@id="img"]';
    const ORIGINAL_URL_XPATH = '//a[starts-with(normalize-space(text()), "Download original")]';
    async function extractImageUrls(url, signal = null) {
        const doc = await fetchDocument(url, signal); // Pass signal down
        if (!doc) return [null, null]; // fetchDocument might throw or return null/undefined on error
        const imageElem = getFirstByXpath(FULL_URL_XPATH, doc);
        const imageUrl = imageElem ? imageElem.src : null;
        const linkElem = getFirstByXpath(ORIGINAL_URL_XPATH, doc);

        // Not all images have a button do download the original image
        // If no button is present, then imageUrl already points to the original.
        const downloadHref = linkElem ? linkElem.getAttribute("href") : imageUrl;

        return [imageUrl, downloadHref];
    }

    // Return objects that include the link element and the page index.
    function extractThumbnailLinks(doc, pageIndex) {
        const gdtDiv = doc.getElementById("gdt");
        if (!gdtDiv) return [];

        const array = Array.from(gdtDiv.querySelectorAll(`a[href^="${baseUrl}/s/"]`)).map((a, index) => {
            const href = a.href;
            const divElement = a.querySelector('div');
            let thumbUrl = null;
            let background = null;
            let width = null;
            let height = null;

            if (divElement) {
                background = divElement.style.background;
                thumbUrl = background.match(/url\(["']?(.*?)["']?\)/)?.[1] || null;
                width = divElement.offsetWidth;
                height = divElement.offsetHeight;
            }

            return {
                link: a,
                page: pageIndex,
                imageIndex: PAGINATION * pageIndex + index,
                fullImageUrl: null,
                originalImageUrl: null,
                href,
                background,
                width,
                height
            };
        });

        return array;
    }

    function extractHashFromEhentaiUrl(imageUrl) {
        // Parse the URL; throws if invalid
        const url = new URL(imageUrl);

        // Split the pathname into segments
        // e.g. "/h/abc123-foo/bar" → ["", "h", "abc123-foo", "bar"]
        const segments = url.pathname.split('/');
        // Find the "h" segment and the next segment
        const idx = segments.indexOf('h');
        if (idx === -1 || segments.length <= idx + 1) {
            return null;
        }

        const hashSegment = segments[idx + 1];
        const [hash] = hashSegment.split('-');
        return hash;
    }

    function showGalleriesWithThisImage(imageUrl) {
        const hash = extractHashFromEhentaiUrl(imageUrl);

        if (!hash) {
            console.error("Could not extract a valid hash from url: " + imageUrl);
            return;
        }

        const searchUrl = `${baseUrl}/?f_shash=${hash}`;
        window.open(searchUrl, '_blank', 'noopener,noreferrer');
    }

    function createNotification(text, duration = 2) {
        const existingNotification = document.getElementById('Notification');
        if (existingNotification) {
            existingNotification.remove();
        }
        const notificationContainer = document.createElement('div');
        notificationContainer.id = 'Notification';
        notificationContainer.style.zIndex = 999999;
        notificationContainer.style.position = 'fixed';
        notificationContainer.style.bottom = '30px';
        notificationContainer.style.left = '50%';
        notificationContainer.style.transform = 'translateX(-50%)';
        const notificationButton = document.createElement('input');
        notificationButton.type = 'button';
        notificationButton.value = text;
        notificationButton.style.padding = '10px';
        notificationButton.style.fontSize = '16px';
        notificationContainer.appendChild(notificationButton);
        document.body.appendChild(notificationContainer);
        setTimeout(() => {
            if (notificationContainer.parentElement) {
                notificationContainer.parentElement.removeChild(notificationContainer);
            }
        }, duration * 1000);
    }

    function ensureElement(element, name) {
        if (!element) {
            console.error(`${name} element not found!`);
            return false;
        }
        return true;
    }

    function saveImage(url) {
        console.log("Saving image from url " + url);
        let filename = url.split('/').pop();

        // for some reason downloading a webp image opens it in a new tab
        if (filename.endsWith('.webp')) {
            filename += '.png';
        }

        GM_download({
            url: url,
            name: filename,
            saveAs: true
        });
    }

    let styleElement = null;
    const NO_SCROLL_CLASS = 'disable-no-scroll';

    function lockPageScroll() {
        function createNoScrollStyle() {
            if (styleElement) return;

            const css = `
          .${NO_SCROLL_CLASS}, .${NO_SCROLL_CLASS} body {
            overflow: hidden !important;
          }
          /* Optional: Add padding rule here too */
          .${NO_SCROLL_CLASS} body.userscript-compensate-scrollbar {
             /* padding-right will be set via JS */
          }
        `;
            styleElement = document.createElement('style');
            styleElement.type = 'text/css';
            styleElement.appendChild(document.createTextNode(css));
            (document.head || document.documentElement).appendChild(styleElement);
        }
        createNoScrollStyle();
        if (document.documentElement.classList.contains(NO_SCROLL_CLASS)) return;
        document.documentElement.classList.add(NO_SCROLL_CLASS);
    }

    function unlockPageScroll() {
        if (!document.documentElement.classList.contains(NO_SCROLL_CLASS)) return;
        document.documentElement.classList.remove(NO_SCROLL_CLASS);
    }

    let hideStyleElement = null;
    const HIDE_CLASS = 'y_hidden';
    function hideElement(element) {
        function createHideStyle() {
            if (hideStyleElement) return; // only create the style element once

            const css = `
        .${HIDE_CLASS} {
        display: none !important;
        }
    `;

            hideStyleElement = document.createElement('style');
            hideStyleElement.type = 'text/css';
            hideStyleElement.appendChild(document.createTextNode(css));
            (document.head || document.documentElement).appendChild(hideStyleElement);
        }
        if (!element) {
            console.error('hideElement: No element provided.');
            return;
        }

        createHideStyle();

        element.classList.add(HIDE_CLASS);
    }

    function unhideElement(element) {
        if (!element) {
            console.error('unhideElement: No element provided.');
            return;
        }

        element.classList.remove(HIDE_CLASS);
    }

    function isElementHidden(element) {
        if (!element) {
            console.error('unhideElement: No element provided.');
            return;
        }
        return element.classList.contains(HIDE_CLASS);
    }

    function goToPageAnywhere(index) {
        console.log('Going to page index ' + index);
        if (imageViewer.isActive()) {
            imageViewer.goOrScrollToIndex(index);
        } else if (embeddedGridView) {
            if (config.embeddedGridGotoOpensViewer) {
                imageViewer.loadAndShowIndex(index);
            } else {
                embeddedGridView.scrollToIndex(index);
            }
        } else { // main index view without embedded custom grid view
            imageViewer.loadAndShowIndex(index);
        }
    }

    function isFullscreen() {
        return (
            document.fullscreenElement ||
            document.webkitFullscreenElement ||
            document.msFullscreenElement
        );
    }

    function requestFullscreen(element) {
        if (!isFullscreen()) {
            element.requestFullscreen?.() ||
                element.webkitRequestFullscreen?.() ||
                element.msRequestFullscreen?.();
        }
    }

    function exitFullscreen() {
        if (isFullscreen()) {
            document.exitFullscreen?.() ||
                document.webkitExitFullscreen?.() ||
                document.msExitFullscreen?.();
        }
    }

    /**
     * Inserts one or more items into an array *after* a specified index,
     * modifying the original array in place.
     * @param {Array<any>} array - The array to modify.
     * @param {number} index - The index *after* which items should be inserted.
     *                         - If index is -1, items are inserted at the beginning.
     *                         - If index is >= array.length - 1, items are appended to the end.
     *                         - Behavior for index < -1 is to insert at the beginning (same as index = -1).
     * @param {...any} itemsToInsert - The item(s) to insert. Can be zero or more arguments.
     * @returns {Array<any>} The modified original array.
     * @throws {TypeError} If the first argument is not an array.
     * @throws {TypeError} If the index is not an integer.
     */
    function insertAfterIndex(array, index, ...itemsToInsert) {
        if (itemsToInsert.length === 0) {
            return array;
        }
        const targetInsertionIndex = Math.max(0, Math.min(index + 1, array.length));
        array.splice(targetInsertionIndex, 0, ...itemsToInsert);
        return array;
    }

    /**
     * Fetches media (image/video) from a potentially cross-origin URL using GM_xmlhttpRequest
     * and returns a Promise that resolves with an object containing the temporary blob: URL
     * and the detected Content-Type.
     * IMPORTANT: The caller is responsible for calling URL.revokeObjectURL() on the
     * returned blob URL when it's no longer needed to prevent memory leaks.
     *
     * Requires @grant GM_xmlhttpRequest and appropriate @connect directives.
     *
     * @param {string} mediaUrl The URL of the media to fetch.
     * @returns {Promise<{ blobUrl: string, type: string | null }>} A Promise that resolves with an object
     *          containing the blobUrl and the Content-Type string (e.g., 'image/jpeg', 'video/mp4').
     *          The 'type' property might be null if the Content-Type header is missing or unparsable.
     *          Rejects with an Error object on failure.
     */
    function fetchMediaBlobUrl(mediaUrl) { // Renamed imageUrl to mediaUrl for clarity
        return new Promise((resolve, reject) => {
            console.log(`Initiating GM fetch for media: ${mediaUrl}`);

            GM_xmlhttpRequest({
                method: "GET",
                url: mediaUrl,
                responseType: "blob", // Fetch as a Blob
                onload: function (response) {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            const mediaBlob = response.response;
                            if (!mediaBlob || mediaBlob.size === 0) {
                                reject(new Error(`Received empty blob for ${mediaUrl}`));
                                return;
                            }

                            // --- Extract Content-Type ---
                            let contentType = null;
                            if (response.responseHeaders) {
                                // Match 'content-type:' case-insensitive, trim whitespace
                                const match = response.responseHeaders.match(/^content-type:\s*(.*)$/im);
                                if (match && match[1]) {
                                    contentType = match[1].trim().split(';')[0]; // Get primary type (e.g., 'image/jpeg') before any params like charset
                                    console.log(`Detected Content-Type for ${mediaUrl}: ${contentType}`);
                                } else {
                                    console.warn(`Could not find Content-Type header for ${mediaUrl}. Headers:\n${response.responseHeaders}`);
                                }
                            } else {
                                console.warn(`No responseHeaders found for ${mediaUrl}.`);
                            }
                            // --- End Extract Content-Type ---


                            // --- Create Blob URL ---
                            const blobUrl = URL.createObjectURL(mediaBlob);
                            console.log(`Blob URL created for ${mediaUrl}: ${blobUrl.substring(0, 100)}...`);
                            // --- End Create Blob URL ---

                            // Resolve with the object containing both blobUrl and type
                            resolve({ blobUrl: blobUrl, type: contentType });

                        } catch (e) {
                            reject(new Error(`Error processing response for ${mediaUrl}: ${e.message}`));
                        }
                    } else {
                        console.error(`GM_xmlhttpRequest failed for ${mediaUrl}: Status ${response.status} ${response.statusText}`);
                        reject(new Error(`Failed to fetch ${mediaUrl}: Server responded with status ${response.status} ${response.statusText}`));
                    }
                },
                onerror: function (response) {
                    console.error(`GM_xmlhttpRequest network error for ${mediaUrl}:`, response.error);
                    reject(new Error(`Network error fetching ${mediaUrl}: ${response.error || 'Unknown error'}`));
                },
                ontimeout: function () {
                    console.error(`GM_xmlhttpRequest timed out for ${mediaUrl}`);
                    reject(new Error(`Timeout fetching ${mediaUrl}`));
                },
                onabort: function () {
                    console.warn(`GM_xmlhttpRequest aborted for ${mediaUrl}`);
                    reject(new Error(`Request aborted for ${mediaUrl}`));
                }
            });
        });
    }

    /**
     * Fetches the Content-Type header for a potentially cross-origin URL
     * using GM_xmlhttpRequest with a HEAD request, without downloading the full file.
     *
     * Requires @grant GM_xmlhttpRequest and appropriate @connect directives.
     *
     * @param {string} mediaUrl The URL to check.
     * @returns {Promise<string | null>} A Promise that resolves with the primary Content-Type
     *          string (e.g., 'image/jpeg', 'video/mp4', 'text/html') or null if the
     *          header is missing, couldn't be parsed, or the request failed.
     *          Rejects with an Error object on network/request errors.
     */
    function getMediaContentType(mediaUrl) {
        return new Promise((resolve, reject) => {
            console.log(`Initiating GM HEAD request for type: ${mediaUrl}`);

            GM_xmlhttpRequest({
                method: "HEAD", // Use HEAD request!
                url: mediaUrl,
                // responseType: "blob", // Not needed for HEAD, we only care about headers
                headers: {
                    // You might include standard headers if needed, but often not necessary for HEAD
                    // 'Accept': '*/*'
                },
                onload: function (response) {
                    // Even errors (like 404) might have headers, but we usually only care about success
                    if (response.status >= 200 && response.status < 300) {
                        let contentType = null;
                        if (response.responseHeaders) {
                            // Match 'content-type:' case-insensitive, trim whitespace
                            const match = response.responseHeaders.match(/^content-type:\s*(.*)$/im);
                            if (match && match[1]) {
                                // Get primary type (e.g., 'image/jpeg') before any params like charset
                                contentType = match[1].trim().split(';')[0].trim();
                                console.log(`Detected Content-Type for ${mediaUrl}: ${contentType} (from full header: ${match[1].trim()})`);
                                resolve(contentType); // Resolve with the found type
                            } else {
                                console.warn(`Could not find Content-Type header for ${mediaUrl}. Status: ${response.status}. Headers:\n${response.responseHeaders}`);
                                resolve(null); // Successfully got headers, but Content-Type is missing
                            }
                        } else {
                            console.warn(`No responseHeaders found for ${mediaUrl}, Status: ${response.status}.`);
                            resolve(null); // Successfully completed request, but no headers (unlikely for 2xx)
                        }
                    } else {
                        // Handle non-2xx responses (404, 500, etc.)
                        // You might still want to check headers even on error in some cases,
                        // but generally, a non-2xx means the resource isn't available as expected.
                        console.warn(`GM_xmlhttpRequest (HEAD) non-2xx status for ${mediaUrl}: Status ${response.status} ${response.statusText}`);
                        // Decide if a non-2xx should reject or resolve with null
                        // Rejecting might be better to indicate failure accessing the resource.
                        reject(new Error(`Failed to get headers for ${mediaUrl}: Server responded with status ${response.status} ${response.statusText}`));
                    }
                },
                onerror: function (response) {
                    console.error(`GM_xmlhttpRequest (HEAD) network error for ${mediaUrl}:`, response.error);
                    reject(new Error(`Network error checking type for ${mediaUrl}: ${response.error || 'Unknown error'}`));
                },
                ontimeout: function () {
                    console.error(`GM_xmlhttpRequest (HEAD) timed out for ${mediaUrl}`);
                    reject(new Error(`Timeout checking type for ${mediaUrl}`));
                },
                onabort: function () {
                    console.warn(`GM_xmlhttpRequest (HEAD) aborted for ${mediaUrl}`);
                    reject(new Error(`Request aborted checking type for ${mediaUrl}`));
                }
            });
        });
    }

    function createGoToPageInput() {
        const input = document.createElement('input');
        input.type = 'text';
        input.id = 'GoToPageInput';
        input.inputMode = 'numeric';
        input.pattern = '[0-9]*';
        input.placeholder = `Go to page 1-${thumbs.length}...`;
        Object.assign(input.style, {
            position: 'fixed',
            bottom: '80px',
            left: '50%',
            transform: 'translateX(-50%)',
            padding: '8px 16px',
            borderRadius: '8px',
            border: '1px solid #ccc',
            boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
            zIndex: '99999',
            fontSize: '16px',
            outline: 'none',
            display: 'none',
            WebkitAppearance: 'none',
            MozAppearance: 'textfield'
        });

        // Remove spinner buttons
        input.style.WebkitAppearance = 'none';
        input.style.MozAppearance = 'textfield';

        const handleClose = () => {
            input.style.display = 'none';
            input.value = '';
            // Only remove the document-level click listener
            document.removeEventListener('click', handleClickOutside);
        };

        const handleInputKeydown = (e) => {
            // Allow browser shortcuts with modifiers (Ctrl+C, Ctrl+R, etc.)
            if (e.ctrlKey || e.altKey || e.metaKey) {
                return;
            }

            // *** Crucial: Stop the event from bubbling up to the document keydown listener ***
            // This prevents keys like Backspace from triggering document-level actions (like showing grid view)
            // when the input is focused.
            e.stopPropagation();

            if (e.key === 'g' || e.key === 'G') {
                handleClose();
                return;
            }

            if (e.key === 'Enter') {
                e.preventDefault(); // Prevent form submission if it were in a form
                const pageNumStr = input.value;
                // Allow negative numbers for parsing, use regex for validation
                if (/^-?\d+$/.test(pageNumStr)) {
                    const pageNum = parseInt(pageNumStr, 10);
                    // Convert page number (1-based, potentially negative) to 0-based index
                    let targetIndex = (pageNum >= 1) ? pageNum - 1 : thumbs.length + pageNum; // Negative wraps from end (-1 goes to last image)

                    if (targetIndex >= 0 && targetIndex < thumbs.length) {
                        goToPageAnywhere(targetIndex);
                    } else {
                        // Provide more specific feedback if the index is out of range
                        alert(`Invalid input: Effective index ${targetIndex} is out of range [0, ${thumbs.length - 1}]`);
                    }
                } else {
                    alert("Invalid input: Please enter an integer.");
                }
                handleClose();
            } else if (e.key === 'Escape') {
                handleClose();
                // Allow '-' only as the first character. Allow all digits. Prevent other single characters.
            } else if (e.key.length === 1 && !/\d/.test(e.key) && !(e.key === '-' && input.value.length === 0)) {
                // Prevent single non-numeric characters from being entered into the input
                e.preventDefault();
                // Do not return here. Let the event bubble up if necessary,
                // although preventDefault might stop other handlers.
                // The main goal is browser shortcuts are already allowed by the modifier check above.
            }
            // Allow numbers, Backspace, Delete, Arrow Keys, etc. by default
        };

        // Add click outside handler
        const handleClickOutside = (e) => {
            if (!input.contains(e.target)) {
                handleClose();
            }
        };

        // Attach keydown listener directly to the input and keep it
        // This listener doesn't need to be removed and re-added.
        input.addEventListener('keydown', handleInputKeydown);

        // Store the handler reference on the element for easy access later
        input.__handleClickOutside = handleClickOutside;
        input.__handleClose = handleClose;

        document.body.appendChild(input);
        return input;
    }

    let goToPageInput = null;
    function showGotoPageInput() {
        if (!goToPageInput || goToPageInput.style.display === "none") {
            if (!goToPageInput) {
                goToPageInput = createGoToPageInput();
            }
            goToPageInput.style.display = 'block';
            goToPageInput.focus();
            // Add the click-outside listener when the input is shown
            setTimeout(() => {
                document.addEventListener('click', goToPageInput.__handleClickOutside);
            }, 100);

        } else {
            goToPageInput.__handleClose();
        }
    }
    // ----------------------------------------------------------------------------------------------
    // fetching.js
    // ----------------------------------------------------------------------------------------------

    async function fetchGalleryPage(pageIndex) {
        if (pageIndex < 0 || pageIndex >= totalPages)
            return null;
        const url = new URL(window.location.href);
        url.searchParams.set("p", pageIndex);
        console.log(`Fetching page: ${url.href}`);
        const doc = await fetchDocument(url.href);
        return doc ? extractThumbnailLinks(doc, pageIndex) : [];
    }

    function populateThumbsOnPage(pageIndex, newThumbs) {
        if (!newThumbs || newThumbs.length === 0)
            return;
        let start = pageIndex * PAGINATION;
        for (let i = 0; i < newThumbs.length; i++) {
            thumbs[start + i] = newThumbs[i];
        }
    }

    function getPageIndexByImageIndex(imageIndex) {
        return Math.floor(imageIndex / PAGINATION);
    }

    const pageLocks = {};
    async function fetchAndPopulateThumbsOnPage(pageIndex) {
        if (pageLocks[pageIndex]) {
            return pageLocks[pageIndex];
        }

        const lockPromise = (async () => {
            const start = pageIndex * PAGINATION;
            const end = Math.min(start + PAGINATION, thumbs.length);
            let needFetch = false;

            for (let i = start; i < end; i++) {
                if (!thumbs[i]) {
                    needFetch = true;
                    break;
                }
            }
            if (!needFetch) {
                return null;
            }

            const newThumbs = await fetchGalleryPage(pageIndex);
            if (!newThumbs || newThumbs.length === 0) {
                return newThumbs;
            }

            populateThumbsOnPage(pageIndex, newThumbs);
            return newThumbs;
        })();

        pageLocks[pageIndex] = lockPromise;

        try {
            return await lockPromise;
        } finally {
            delete pageLocks[pageIndex];
        }
    }

    function preloadRange(start, end, reverse = false, useOriginalImages = false, maxWorkers = 5) {
        if (start < 0) start = 0;
        if (end >= thumbs.length) end = thumbs.length - 1;

        const indices = [];
        if (reverse) {
            for (let i = end; i >= start; i--) {
                indices.push(i);
            }
        } else {
            for (let i = start; i <= end; i++) {
                indices.push(i);
            }
        }
        return preloadIndices(indices, useOriginalImages, maxWorkers);
    }

    async function preloadIndices(indicesToPreload, useOriginalImages = false, maxWorkers = 5) {
        console.log(`Preloading indices: [${indicesToPreload.join(', ')}] with maxWorkers=${maxWorkers}`);

        const mediaLoadPool = new PromisePool(maxWorkers); // Renamed for clarity

        for (const index of indicesToPreload) {
            if (index < 0 || index >= thumbs.length) {
                console.log(`Preload: Index ${index} is out of bounds [0, ${thumbs.length - 1}]. Skipping.`);
                continue;
            }

            // --- Sequential Part: Load Media Metadata/URL ---
            let item;
            try {
                // Ensure the item data (including URL and isVideo flag) is available
                await loadImageUrlAtIndex(index); // Fetches metadata/URL if not already present
                item = thumbs[index]; // Get the potentially updated item
            } catch (error) {
                console.error(`Preload: Error fetching metadata/URL for index ${index}:`, error);
                continue; // Skip this index if metadata loading fails
            }
            // --- End Sequential Part ---

            // Check if item is valid and determine media type
            if (!item || item === 'deleted') {
                console.log(`Preload: Item at index ${index} is invalid or deleted. Skipping.`);
                continue;
            }

            const isVideo = item.isVideo === true;

            if (isVideo) {
                // For videos, preload metadata
                // Hint to the browser to fetch metadata by creating a temporary video element.
                const videoUrl = item.fullImageUrl;
                if (videoUrl) {
                    const tempVideo = document.createElement('video');
                    tempVideo.referrerPolicy = "no-referrer";
                    tempVideo.preload = 'metadata';
                    tempVideo.src = videoUrl;
                    // Don't append to DOM, don't wait. Just setting src triggers the browser based on 'preload'.
                    console.log(`Preload: Index ${index} is a video. Hinting browser to preload metadata for ${videoUrl}`);
                } else {
                    console.log(`Preload: Index ${index} is a video, but no URL found.`);
                }
                // No need to add to the pool, the browser handles this asynchronously.
            } else {
                // --- Handle Image Preloading ---
                let imageUrl = null;
                // Determine the correct image URL based on configuration
                if (item.originalImageUrl && (useOriginalImages || item.originalImageIsCached)) {
                    imageUrl = item.originalImageUrl;
                } else {
                    imageUrl = item.fullImageUrl;
                }

                if (imageUrl) {
                    // --- Concurrent Part: Submit Image Data Load Task to Pool ---
                    // Define the *task function* that the pool will execute later
                    const imageLoadTaskFactory = async () => {
                        // console.log(`Preload Pool: Starting image data load for index ${index} (URL: ${imageUrl})`);
                        const imgElement = new Image();
                        imgElement.referrerPolicy = "no-referrer";
                        imgElement.src = imageUrl; // Start loading
                        try {
                            await waitForMediaLoad(imgElement);
                            // console.log(`Preload Pool: Successfully loaded image data for index ${index}`);
                        } catch (err) {
                            // Handle/log errors for *individual* image loads within the task
                            console.error(`Preload Pool: Error loading image data for index ${index} (URL: ${imageUrl}):`, err);
                            // Do not re-throw here if you want other preloads to continue even if one fails
                        }
                    };

                    // Submit the task factory to the pool.
                    mediaLoadPool.run(imageLoadTaskFactory);
                } else {
                    console.log(`Preload: No valid image URL found for index ${index} after metadata load. Skipping image data load.`);
                }
            }
        }

        // After iterating through all indices and submitting tasks,
        // wait for all tasks managed by the pool (now only image loads) to complete.
        // console.log(`Preload: All indices processed. Waiting for ${mediaLoadPool.activePromises.size} active + ${mediaLoadPool.queuedTasks.length} queued image loads to complete...`);
        await mediaLoadPool.waitAll();

        // console.log("Preload: All requested indices processed and all image loading tasks finished.");
    }


    const indexLocks = {};
    async function loadImageUrlAtIndex(index, signal = null) { // Accept signal
        if (indexLocks[index]) {
            return indexLocks[index];
        }

        const lockPromise = _loadImageUrlAtIndexInternal(index, signal); // Pass signal
        indexLocks[index] = lockPromise;

        try {
            return await lockPromise;
        } finally {
            delete indexLocks[index];
        }
    }

    async function _loadImageUrlAtIndexInternal(index, signal = null) { // Accept signal
        console.log(`Loading image URL at index ${index}`);

        // 1. Check if the requested index is valid within the total possible range.
        if (index < 0 || index >= thumbs.length) {
            console.log(`Image index ${index} is out of range [0, ${thumbs.length - 1}]`);
            return;
        }

        // 2. Check if the data for this index needs to be fetched.
        if (thumbs[index] === null) {
            const pageIndex = Math.floor(index / PAGINATION);
            console.log(`Data for index ${index} is null. Fetching page ${pageIndex}.`);

            // Ensure the calculated page index is valid (should be due to initial bounds check, but good safety)
            if (pageIndex < 0 || pageIndex >= totalPages) {
                console.error(`Calculated invalid page index ${pageIndex} for image index ${index}.`);
                return; // Should not happen if thumbs.length is correct
            }

            // Fetch the page and let fetchAndPopulateThumbsOnPage update the global thumbs array.
            const newThumbs = await fetchAndPopulateThumbsOnPage(pageIndex);

            // Check if fetching was successful and if the specific index was populated.
            if (!newThumbs || newThumbs.length === 0) {
                console.error(`Failed to fetch or populate page ${pageIndex} containing index ${index}.`);
                // Decide how to handle: retry? show error? For now, just return.
                return;
            }
            // Verify that the specific index we need is now populated
            if (thumbs[index] === null) {
                console.error(`Index ${index} remains null after fetching page ${pageIndex}. Population failed.`);
                return;
            }
        }

        // 3. Check if the item at the index is marked as deleted.
        //    (Using 'deleted' string as per original code example)
        if (thumbs[index] === 'deleted') {
            console.log(`Image at index ${index} is marked as deleted.`);
            return;
        }

        // 4. We should now have valid thumb data at thumbs[index].
        const currentThumb = thumbs[index]; // Use a local variable for clarity

        // Ensure the thumb object and required properties exist
        if (!currentThumb || !currentThumb.link || !currentThumb.link.href) {
            console.error(`Thumb data at index ${index} is invalid or missing link.href.`, currentThumb);
            return;
        }
        const thumbHref = currentThumb.link.href;

        // 5. Check if the full image URL is already cached for this thumb.
        if (currentThumb.fullImageUrl) {
            console.log(`Image URL(s) for index ${index} already cached.`);
            // Even if fullImageUrl exists, we might still need originalImageUrl
            if (!currentThumb.originalImageUrl) {
                // Continue to fetch potentially missing original URL
            } else {
                return; // Both URLs likely present or handled
            }
        }

        // 6. Full image URL is not cached, fetch it.
        // console.log(`Fetching full image URL for index ${index}`);
        let fullUrl = null;
        let originalUrl = null;
        try {
            [fullUrl, originalUrl] = await extractImageUrls(thumbHref, signal); // Pass signal
        } catch (error) {
            // Check if the error is due to the operation being aborted
            if (error.name === 'AbortError') {
                console.log(`Fetch aborted for image URL at index ${index} (${thumbHref})`);
            } else {
                console.error(`Error fetching image URL for index ${index} (${thumbHref}):`, error);
            }
            return;
        }


        // 7. Process the fetched URL.
        // Check if the item still exists at the index before assigning
        // (it might have been marked 'deleted' concurrently)
        if (thumbs[index] && thumbs[index] !== 'deleted') {
            if (thumbs[index] && thumbs[index] !== 'deleted') {
                thumbs[index].fullImageUrl = fullUrl;
                // If originalUrl wasn't found via separate link, assume fullUrl is the original
                if (!originalUrl && fullUrl) {
                    thumbs[index].originalImageUrl = fullUrl;
                } else if (originalUrl) {
                    thumbs[index].originalImageUrl = originalUrl;
                }
            }
        } else {
            console.error(`Could not extract any image URL for index ${index} from ${thumbHref}.`);
            // Optional: Mark as failed?
            // if (thumbs[index] && thumbs[index] !== 'deleted') {
            //    thumbs[index].loadError = true;
            // }
        }
    }

    /**
     * Returns a Promise that resolves when the media element has loaded sufficiently
     * (image fully loaded, video metadata loaded for dimensions), or rejects on error/timeout.
     * Handles cases where the media might already be ready.
     *
     * @param {HTMLImageElement | HTMLVideoElement} element The media element to monitor.
     * @param {number | null} [timeout=null] Optional timeout in milliseconds.
     * @returns {Promise<void>} A Promise that resolves on success, rejects on error/timeout.
     */
    async function waitForMediaLoad(element, timeout = null) {
        if (element instanceof HTMLImageElement) {
            // --- Initial Synchronous Checks ---
            if (element.complete && element.naturalWidth > 0) {
                return Promise.resolve();
            }
            if (!element.src || element.src === window.location.href) {
                // console.warn("Image src invalid or missing, resolving early:", element.src);
                return Promise.resolve();
            }

            // --- Asynchronous Waiting ---
            return new Promise((resolve, reject) => {
                const currentSrc = element.src;
                let timeoutId = null;

                const cleanup = () => {
                    element.removeEventListener('load', loadHandler);
                    element.removeEventListener('error', errorHandler);
                    if (timeoutId) clearTimeout(timeoutId);
                };

                const loadHandler = () => { cleanup(); resolve(); };
                const errorHandler = (event) => {
                    console.error("Image failed to load:", currentSrc, event);
                    cleanup();
                    reject(new Error(`Failed to load image: ${currentSrc}`));
                };

                element.addEventListener('load', loadHandler);
                element.addEventListener('error', errorHandler);

                if (typeof timeout === 'number' && timeout > 0) {
                    timeoutId = setTimeout(() => {
                        cleanup();
                        reject(new Error(`Image load timed out after ${timeout}ms: ${currentSrc}`));
                    }, timeout);
                }

                if (element.complete && element.naturalWidth > 0) {
                    cleanup();
                    resolve();
                }
            });

        } else if (element instanceof HTMLVideoElement) {
            // --- Implement video logic (wait for metadata) ---

            // --- Initial Synchronous Checks ---
            // video.readyState >= 1 means metadata (including dimensions) is loaded.
            if (element.readyState >= 1) {
                // console.log("Video metadata already available:", element.src);
                return Promise.resolve(); // Already ready
            }

            if (!element.src || element.src === window.location.href) {
                // console.warn("Video src invalid or missing, resolving early:", element.src);
                // Resolve like the image case; allows Promise.all to proceed.
                return Promise.resolve();
            }

            // --- Asynchronous Waiting ---
            return new Promise((resolve, reject) => {
                const currentSrc = element.src;
                let timeoutId = null;

                // Define cleanup logic
                const cleanup = () => {
                    // console.log("Cleaning up video listeners/timeout for:", currentSrc);
                    element.removeEventListener('loadedmetadata', metadataHandler);
                    element.removeEventListener('error', errorHandler);
                    // Videos can also emit 'stalled' or 'suspend' which might be relevant
                    // but 'error' usually covers critical load failures.
                    if (timeoutId) {
                        clearTimeout(timeoutId);
                        timeoutId = null;
                    }
                };

                // Define event handlers
                const metadataHandler = () => {
                    // console.log("Video metadata loaded:", currentSrc);
                    cleanup();
                    resolve(); // Signal success (metadata ready)
                };

                const errorHandler = (event) => {
                    // The event itself might contain a MediaError object with more details
                    const error = element.error;
                    console.error("Video failed to load:", currentSrc, error ? `Code: ${error.code}, Message: ${error.message}` : 'Unknown error', event);
                    cleanup();
                    reject(new Error(`Failed to load video metadata: ${currentSrc}${error ? ` (${error.message})` : ''}`));
                };

                // Attach event listeners
                element.addEventListener('loadedmetadata', metadataHandler);
                element.addEventListener('error', errorHandler);

                // Set up timeout if specified
                if (typeof timeout === 'number' && timeout > 0) {
                    timeoutId = setTimeout(() => {
                        // console.warn(`Video metadata load timed out (${timeout}ms):`, currentSrc);
                        cleanup();
                        reject(new Error(`Video metadata load timed out after ${timeout}ms: ${currentSrc}`));
                    }, timeout);
                }

                // Double-check readyState *after* adding listeners.
                // This catches cases where metadata loaded synchronously between the initial
                // check and listener attachment.
                if (element.readyState >= 1) {
                    // console.log("Video became ready just after listeners were added:", currentSrc);
                    cleanup();
                    resolve();
                }
                // Note: If the video 'src' was set *before* this function was called,
                // and loading is in progress but not yet complete, the attached
                // listeners will catch the eventual 'loadedmetadata' or 'error' event.
                // Browsers might start loading metadata as soon as 'preload="metadata"' and 'src' are set.
            });

        } else {
            // Handle unsupported element types
            console.error("Unsupported element type for waitForMediaLoad:", element);
            return Promise.reject(new Error('Unsupported element type for waitForMediaLoad'));
        }
    }
    // ----------------------------------------------------------------------------------------------
    // imageViewer.js
    // ----------------------------------------------------------------------------------------------

    class ImageViewer {
        parent = null;
        buttonContainer = null;

        // Toolbar buttons
        helpButton = null;
        chaptersButton = null;
        settingsButton = null;
        exitButton = null;
        prevButton = null;
        nextButton = null;
        rotateLeftButton = null;
        rotateRightButton = null;
        zoomInButton = null;
        zoomOutButton = null;
        galleryViewButton = null;
        dualPageButton = null;
        fullscreenButton = null;
        downloadButton = null;
        findGalleriesButton = null;
        gotoPageButton = null;

        imgContainer = null;
        imgDisplay = null;
        imgDisplay2 = null;
        isNavigating = false;
        sidebarVisible = false;
        useFullscreen = false;
        hasEnteredFullscreenOnce = false;
        sidebarHiddenTransform = null;
        sidebar = null; // GridView instance
        gridView = null; // GridView instance
        onExit = null; // Function accepting bool exitToPage
        pinSidebar = false;
        backwardNavigationCount = 0;
        currentRotation = 0;
        currentIndex = 0;
        currentAbortController = null; // Controller for the latest load operation
        currentLoadToken = null; // Add this to track the latest load request
        fitMode = "fit-window";
        currentZoom = 1;
        userChangedZoom = false;
        config;

        UI_TRANSPARENCY = 0.6;

        constructor(config, onExit = null) {
            this.config = config;
            this.onExit = onExit;

            this.fitMode = config.fitMode;
            this.pinSidebar = config.pinSidebar;
            this.useFullscreen = config.useFullscreen;
            this.rightToLeftMode = config.openInRightToLeftMode;
            this.dualPageMode = config.openInDualPageMode;
            this.dualLayout = config.dualLayout;
            this.autoplayEnabled = config.videoConfig.autoplay;
            this.loopEnabled = config.videoConfig.loop;

            this.blobCache = new Map(); // Stores { blob, size, type, lastAccessed }
            this.pendingFetches = new Map(); // Stores Promises for ongoing fetches
            this.currentCacheSize = 0; // In bytes
            this.cacheLimitMB = 50;

            this._createImageViewer();
            this._initializeEventListeners();

            this.inputHandler = new ViewerInputHandler(this, this.config);

            this._initializeHelpOverlay();

            this._initializeSidebar();

            this.parent.appendChild(this.gridParent);
            this.gridView = new GridView(this.gridParent, config.gridViewConfig, false);
        }

        isActive() {
            return this.parent.style.display === "block";
        }

        /**
         * Checks if a video element is currently being displayed in the specified slot.
         * @param {'primary' | 'secondary' | null} [position=null] - The display slot to check ('left' for primary, 'right' for secondary).
         *                          If null, checks if *any* video is displayed.
         * @returns {boolean} True if a video is displayed in the specified slot(s), false otherwise.
         */
        isShowingVideo(position = null) {
            const isPrimaryVideoShowing = this.videoDisplay.style.display === 'block';
            const isSecondaryVideoShowing = this.dualPageMode && this.videoDisplay2.style.display === 'block';

            if (position === 'primary') {
                return isPrimaryVideoShowing;
            } else if (position === 'secondary') {
                return isSecondaryVideoShowing;
            } else if (position === null) {
                return isPrimaryVideoShowing || isSecondaryVideoShowing;
            } else {
                console.warn(`isShowingVideo called with invalid position: ${position}`);
                return false; // Invalid position provided
            }
        }

        /**
          * Toggles the playback state (play/pause) of the specified video display(s).
          * If targetDisplay is null (default), toggles both visible videos, ensuring the secondary
          * video's state matches the primary video's resulting state.
          *
          * @param {'primary' | 'secondary' | null} [targetDisplay=null] - Which video display to toggle.
          *                      'primary', 'secondary', or null (default) for both.
          */
        toggleVideoPlayback(targetDisplay = null) { // Default set to null here
            const primaryVisible = this.isShowingVideo('primary');
            const secondaryVisible = this.isShowingVideo('secondary'); // Checks dualPageMode internally

            // Determine the target action (play or pause)
            // Priority: Primary state if primary is targeted (or null target),
            // otherwise Secondary state if only secondary is targeted.
            let shouldPlay = false;
            if (primaryVisible && (targetDisplay === 'primary' || targetDisplay === null)) {
                shouldPlay = this.videoDisplay.paused;
            } else if (secondaryVisible && targetDisplay === 'secondary') {
                shouldPlay = this.videoDisplay2.paused;
            } else if (secondaryVisible && targetDisplay === null && !primaryVisible) {
                // Toggling both, but primary isn't visible, use secondary's state
                shouldPlay = this.videoDisplay2.paused;
            }

            // Helper to apply the action and handle errors
            const applyState = (videoElement, name) => {
                if (shouldPlay) {
                    videoElement.play()?.catch(error => {
                        // Simplified notification - avoid flooding if both fail
                        if (name === 'primary' || !primaryVisible || targetDisplay !== null) {
                            console.error(`Error playing video (${name}):`, error);
                            createNotification(`Could not play video (${name}).`);
                        }
                    });
                } else {
                    videoElement.pause();
                }
            };

            // Apply to primary if it's targeted and visible
            if (primaryVisible && (targetDisplay === 'primary' || targetDisplay === null)) {
                applyState(this.videoDisplay, 'primary');
            } else if (targetDisplay === 'primary') {
                console.log("Primary video not visible, cannot toggle.");
            }

            // Apply to secondary if it's targeted and visible
            // Note: The *same* `shouldPlay` action is applied for synchronization when targetDisplay is null.
            if (secondaryVisible && (targetDisplay === 'secondary' || targetDisplay === null)) {
                applyState(this.videoDisplay2, 'secondary');
            } else if (targetDisplay === 'secondary') {
                console.log("Secondary video not visible, cannot toggle.");
            }

            if (targetDisplay !== 'primary' && targetDisplay !== 'secondary' && targetDisplay !== null) {
                console.warn(`Invalid targetDisplay specified for toggleVideoPlayback: ${targetDisplay}`);
            }
        }

        /**
         * Seeks the specified video display(s) by a given amount of time.
         * If targetDisplay is null (default), seeks both visible videos by the same amount.
         *
         * @param {number} time - The amount of time in seconds to seek.
         *            Positive values seek forward, negative values seek backward.
         * @param {'primary' | 'secondary' | null} [targetDisplay=null] - Which video display to seek.
         *                      'primary', 'secondary', or null (default) for both.
         */
        seekVideo(time, targetDisplay = null) {
            if (typeof time !== 'number' || isNaN(time)) {
                console.warn(`seekVideo called with invalid time value: ${time}`);
                return;
            }

            const primaryVisible = this.isShowingVideo('primary');
            const secondaryVisible = this.isShowingVideo('secondary'); // Checks dualPageMode internally

            // Helper function to apply the seek operation safely
            const applySeek = (videoElement, name) => {
                // Check if duration is available and valid before seeking
                if (videoElement.duration && !isNaN(videoElement.duration)) {
                    // currentTime assignment automatically clamps between 0 and duration
                    videoElement.currentTime += time;
                    // console.log(`Seeking video (${name}) by ${time}s. New time: ${videoElement.currentTime}`);
                } else {
                    console.log(`Cannot seek video (${name}): duration not available.`);
                }
            };

            // Seek primary if it's targeted and visible
            if (primaryVisible && (targetDisplay === 'primary' || targetDisplay === null)) {
                applySeek(this.videoDisplay, 'primary');
            } else if (targetDisplay === 'primary') {
                console.log("Primary video not visible, cannot seek.");
            }

            // Seek secondary if it's targeted and visible
            if (secondaryVisible && (targetDisplay === 'secondary' || targetDisplay === null)) {
                applySeek(this.videoDisplay2, 'secondary');
            } else if (targetDisplay === 'secondary') {
                console.log("Secondary video not visible, cannot seek.");
            }

            if (targetDisplay !== 'primary' && targetDisplay !== 'secondary' && targetDisplay !== null) {
                console.warn(`Invalid targetDisplay specified for seekVideo: ${targetDisplay}`);
            }
        }

        isGridViewActive() {
            return this.gridParent.style.display === "block";
        }

        toggleGridView(goToIndex = false) {
            if (this.isGridViewActive()) {
                this.gridParent.style.display = "none";
                this.gridView.stopLoading();
                if (goToIndex) this.loadAndShowIndex(this.savedIndexBeforeGridView);
                this.savedIndexBeforeGridView = null;
            } else {
                this.savedIndexBeforeGridView = this.currentIndex;
                this.gridParent.style.display = "block";
                if (!this.gridView.showCalled) {
                    this.gridView.showGridView();
                } else {
                    this.gridView.enableLoading();
                }
                this.gridView.scrollToIndex(this.currentIndex, false);
            }
        }

        rotateImage(delta) {
            this.currentRotation = (this.currentRotation + delta) % 360;
            if (this.currentRotation < 0) {
                this.currentRotation += 360;
            }
            this.updateTransforms();
        }

        /**
         * Zooms the image.
         * @param {number} direction - Positive to zoom in, negative to zoom out.
         * @param {number} [zoomFactor=1.1] - Zoom factor
         * @param {boolean} [centerX=false] - Keep the horizontal center of the viewport anchored.
         * @param {boolean} [centerY=false] - Keep the vertical center of the viewport anchored.
         */
        zoom(direction, zoomFactor = 1.1, centerX = false, centerY = false) {
            const oldZoom = this.currentZoom;
            if (oldZoom <= 0) return;

            let newZoom = direction > 0 ? oldZoom * zoomFactor : oldZoom / zoomFactor;


            // Clamp zoom level
            newZoom = Math.max(this.minZoomLevel ?? 1, newZoom);

            if (Math.abs(newZoom - oldZoom) < 0.001) {
                return; // No significant change
            }

            // --- Store pre-zoom state for potential anchor calculation ---
            const parent = this.parent;
            const oldScrollLeft = parent.scrollLeft;
            const oldScrollTop = parent.scrollTop;
            const viewportWidth = parent.clientWidth;
            const viewportHeight = parent.clientHeight;

            // Calculate the coordinates of the viewport center relative to the scrollable content's top-left
            // This represents the "anchor point" we *might* want to keep centered.
            const centerX_coord = oldScrollLeft + viewportWidth / 2;
            const centerY_coord = oldScrollTop + viewportHeight / 2;
            const zoomRatio = newZoom / oldZoom;

            this.currentZoom = newZoom;

            // --- Update Visuals (applies new scale and container size/pos) ---
            this.updateTransforms();

            // --- Adjust Scroll Position Conditionally to Maintain Center Anchor ---
            if (this.currentZoom > 1) {
                // Use requestAnimationFrame to ensure scroll update happens *after* the browser
                requestAnimationFrame(() => {
                    const maxScrollLeft = parent.scrollWidth - viewportWidth;
                    const maxScrollTop = parent.scrollHeight - viewportHeight;

                    let targetScrollLeft = parent.scrollLeft;
                    let targetScrollTop = parent.scrollTop;

                    // --- Horizontal Anchoring ---
                    if (centerX) {
                        const newAnchorX = centerX_coord * zoomRatio;
                        const newScrollLeftTarget = newAnchorX - viewportWidth / 2;
                        targetScrollLeft = Math.max(0, Math.min(newScrollLeftTarget, maxScrollLeft));
                    }

                    // --- Vertical Anchoring ---
                    if (centerY) {
                        const newAnchorY = centerY_coord * zoomRatio;
                        const newScrollTopTarget = newAnchorY - viewportHeight / 2;
                        targetScrollTop = Math.max(0, Math.min(newScrollTopTarget, maxScrollTop));
                    }

                    if (parent.scrollLeft !== targetScrollLeft) {
                        parent.scrollLeft = targetScrollLeft;
                    }
                    if (parent.scrollTop !== targetScrollTop) {
                        parent.scrollTop = targetScrollTop;
                    }
                });

            } else { // Zooming out to 1 or less. Reset scroll.
                requestAnimationFrame(() => {
                    if (this.currentZoom <= 1) {
                        parent.scrollTop = 0;
                        parent.scrollLeft = 0;
                    }
                });
            }
        }

        /**
         * Marks an item as deleted, cleans up associated resources, refreshes grids,
         * and navigates away if the currently viewed item was deleted.
         * @param {number} index The index of the item to delete.
         */
        delete(index) {
            // 1. Validate Index
            if (index < 0 || index >= thumbs.length || thumbs[index] === 'deleted') {
                console.warn(`ImageViewer.delete: Invalid or already deleted index ${index}.`);
                return;
            }

            console.log(`ImageViewer.delete: Deleting index ${index}.`);
            const itemToDelete = thumbs[index]; // Get item data before marking as deleted

            // 2. Mark as Deleted
            thumbs[index] = 'deleted';

            // 3. Clean Up Blob Cache (if item existed and fallback was used)
            if (itemToDelete) {
                // Determine the URL that *might* be cached (prefer original if available)
                const potentialCachedUrl = itemToDelete.originalImageUrl || itemToDelete.fullImageUrl;
                if (potentialCachedUrl) {
                    this._evictCacheEntry(potentialCachedUrl); // Call the helper
                }
            }

            // 4. Refresh Grids
            this.refreshGrids();

            // 5. Handle Current Index Deletion & Navigation
            const displayedIndices = this._getDualPageIndices(this.currentIndex);
            const wasDisplayed = index === this.currentIndex || index === displayedIndices.primary || index === displayedIndices.secondary;

            if (wasDisplayed) {
                if (index < thumbs.length - 1) {
                    this.navigateForward();
                } else if (index > 0) {
                    this.navigateBack();
                }
            }
            // If the deleted index wasn't the current one, no navigation is needed.
        }

        refreshGrids() {
            this.sidebar?.refreshAll(); // Refresh sidebar grid
            this.gridView?.refreshAll(); // Refresh main gallery grid (when active)
        }

        _getOneToOneZoom() {
            // --- Determine Active Element and Get Dimensions ---
            const primaryElement = (this.videoDisplay.style.display === 'block') ? this.videoDisplay : this.imgDisplay;
            const primaryIsVideo = primaryElement === this.videoDisplay && primaryElement.style.display === 'block';
            let naturalW = 0, naturalH = 0;
            const MIN_ERROR_DIM = 100; // Consistent min dimension

            if (primaryElement.style.display === 'block') {
                naturalW = primaryIsVideo ? primaryElement.videoWidth : primaryElement.naturalWidth;
                naturalH = primaryIsVideo ? primaryElement.videoHeight : primaryElement.naturalHeight;
                // Apply MIN_ERROR_DIM logic for invalid or error states
                if (!primaryIsVideo && primaryElement.classList.contains('viewer-error-state')) {
                    naturalW = Math.max(naturalW, MIN_ERROR_DIM);
                    naturalH = Math.max(naturalH, MIN_ERROR_DIM);
                } else if (!primaryIsVideo && (!naturalW || naturalW <= 0 || !naturalH || naturalH <= 0)) {
                    // console.warn("1:1 Zoom: Primary image dimensions invalid, using min error dim.");
                    naturalW = MIN_ERROR_DIM; naturalH = MIN_ERROR_DIM;
                } else if (primaryIsVideo && (!naturalW || naturalW <= 0 || !naturalH || naturalH <= 0)) {
                    // console.warn("1:1 Zoom: Primary video dimensions invalid, using min error dim.");
                    naturalW = MIN_ERROR_DIM; naturalH = MIN_ERROR_DIM;
                }
            } else { // If primary not visible, use min dim as fallback
                // console.warn("1:1 Zoom: Primary element not visible, using min error dim.");
                naturalW = MIN_ERROR_DIM; naturalH = MIN_ERROR_DIM;
            }
            // Ensure not zero if still possible
            if (naturalW <= 0) naturalW = MIN_ERROR_DIM;
            if (naturalH <= 0) naturalH = MIN_ERROR_DIM;


            // --- Get Rotation and Viewport ---
            const currentRotation = this.currentRotation;
            const rotIs90 = currentRotation % 180 !== 0;
            const viewportWidth = this.parent.clientWidth;
            const viewportHeight = this.parent.clientHeight;
            if (viewportWidth <= 0 || viewportHeight <= 0) {
                console.warn("Cannot calculate 1:1 zoom: Viewport dimensions not available.");
                return 1; // Fallback
            }

            // Effective dimensions after rotation
            const effectiveW = rotIs90 ? naturalH : naturalW;
            const effectiveH = rotIs90 ? naturalW : naturalH;

            // --- Calculate baseScale (fit-to-screen scale) ---
            let baseScale = 1;
            const secondaryElement = (this.videoDisplay2.style.display === 'block') ? this.videoDisplay2 : this.imgDisplay2;
            // Check if secondary is *actually* displayed for dual page calculations
            const isDualPageVisible = this.dualPageMode && secondaryElement.style.display === 'block';
            let naturalW2 = 0, naturalH2 = 0;

            if (isDualPageVisible) {
                const secondaryIsVideo = secondaryElement === this.videoDisplay2;
                naturalW2 = secondaryIsVideo ? secondaryElement.videoWidth : secondaryElement.naturalWidth;
                naturalH2 = secondaryIsVideo ? secondaryElement.videoHeight : secondaryElement.naturalHeight;

                // Apply MIN_ERROR_DIM logic for invalid or error states
                if (!secondaryIsVideo && secondaryElement.classList.contains('viewer-error-state')) {
                    naturalW2 = Math.max(naturalW2, MIN_ERROR_DIM);
                    naturalH2 = Math.max(naturalH2, MIN_ERROR_DIM);
                } else if (!secondaryIsVideo && (!naturalW2 || naturalW2 <= 0 || !naturalH2 || naturalH2 <= 0)) {
                    // Use primary's dimensions if valid, else use min error dim.
                    if (naturalW > 0 && naturalH > 0 && naturalW !== MIN_ERROR_DIM) { // Check if primary has real dimensions
                        // console.warn("1:1 Zoom: Secondary dimensions invalid, using primary fallback.");
                        naturalW2 = naturalW; naturalH2 = naturalH;
                    } else {
                        // console.warn("1:1 Zoom: Secondary dimensions invalid, using min error dim.");
                        naturalW2 = MIN_ERROR_DIM; naturalH2 = MIN_ERROR_DIM;
                    }
                } else if (secondaryIsVideo && (!naturalW2 || naturalW2 <= 0 || !naturalH2 || naturalH2 <= 0)) {
                    // console.warn("1:1 Zoom: Secondary video dimensions invalid, using min error dim.");
                    naturalW2 = MIN_ERROR_DIM; naturalH2 = MIN_ERROR_DIM;
                }
                // Ensure W2/H2 are not zero if dual page is visible
                if (naturalW2 <= 0) naturalW2 = MIN_ERROR_DIM;
                if (naturalH2 <= 0) naturalH2 = MIN_ERROR_DIM;
            }

            // Calculate base scale based on whether dual page is effectively active
            let useDualForScale = isDualPageVisible;
            if (useDualForScale && (naturalW2 <= 0 || naturalH2 <= 0)) {
                // This check might be redundant due to above assignment, but safe
                useDualForScale = false;
            }

            if (useDualForScale) {
                let requiredWidthPerScale, requiredHeightPerScale;
                if (!rotIs90) { // Side-by-side
                    requiredWidthPerScale = naturalW + naturalW2;
                    requiredHeightPerScale = Math.max(naturalH, naturalH2);
                } else { // Top-and-bottom
                    requiredWidthPerScale = Math.max(naturalH, naturalH2);
                    requiredHeightPerScale = naturalW + naturalW2;
                }
                // Check for zero denominators
                const scaleLimitW = requiredWidthPerScale > 0 ? viewportWidth / requiredWidthPerScale : Infinity;
                const scaleLimitH = requiredHeightPerScale > 0 ? viewportHeight / requiredHeightPerScale : Infinity;
                baseScale = Math.min(scaleLimitW, scaleLimitH);
            } else { // Single page mode (or fallback)
                // effectiveW/H derived from naturalW/H which are guaranteed > 0 here
                const scaleLimitW = effectiveW > 0 ? viewportWidth / effectiveW : Infinity;
                const scaleLimitH = effectiveH > 0 ? viewportHeight / effectiveH : Infinity;
                baseScale = Math.min(scaleLimitW, scaleLimitH);
            }
            baseScale = Math.max(baseScale, 0); // Ensure non-negative


            // --- Calculate 1:1 Zoom Factor ---
            if (baseScale > 0) {
                return 1 / baseScale;
            } else {
                console.warn("Cannot calculate 1:1 zoom: baseScale is zero.");
                return 1; // Fallback
            }
        }

        _getFitWidthZoom() {
            // --- Determine Active Elements and Get Dimensions ---
            const primaryElement = (this.videoDisplay.style.display === 'block') ? this.videoDisplay : this.imgDisplay;
            const primaryIsVideo = primaryElement === this.videoDisplay && primaryElement.style.display === 'block';
            let naturalW1 = 0, naturalH1 = 0;
            const MIN_ERROR_DIM = 100; // Consistent min dimension

            if (primaryElement.style.display === 'block') {
                naturalW1 = primaryIsVideo ? primaryElement.videoWidth : primaryElement.naturalWidth;
                naturalH1 = primaryIsVideo ? primaryElement.videoHeight : primaryElement.naturalHeight;
                // Apply MIN_ERROR_DIM logic
                if (!primaryIsVideo && primaryElement.classList.contains('viewer-error-state')) {
                    naturalW1 = Math.max(naturalW1, MIN_ERROR_DIM);
                    naturalH1 = Math.max(naturalH1, MIN_ERROR_DIM);
                } else if (!primaryIsVideo && (!naturalW1 || naturalW1 <= 0 || !naturalH1 || naturalH1 <= 0)) {
                    // console.warn("Fit Width: Primary image dimensions invalid, using min error dim.");
                    naturalW1 = MIN_ERROR_DIM; naturalH1 = MIN_ERROR_DIM;
                } else if (primaryIsVideo && (!naturalW1 || naturalW1 <= 0 || !naturalH1 || naturalH1 <= 0)) {
                    // console.warn("Fit Width: Primary video dimensions invalid, using min error dim.");
                    naturalW1 = MIN_ERROR_DIM; naturalH1 = MIN_ERROR_DIM;
                }
            } else {
                // console.warn("Fit Width: Primary element not visible, using min error dim.");
                naturalW1 = MIN_ERROR_DIM; naturalH1 = MIN_ERROR_DIM;
            }
            // Ensure not zero
            if (naturalW1 <= 0) naturalW1 = MIN_ERROR_DIM;
            if (naturalH1 <= 0) naturalH1 = MIN_ERROR_DIM;

            // --- Get Rotation and Viewport ---
            const currentRotation = this.currentRotation;
            const rotIs90 = currentRotation % 180 !== 0;
            const viewportWidth = this.parent.clientWidth;
            const viewportHeight = this.parent.clientHeight;
            if (viewportWidth <= 0 || viewportHeight <= 0) {
                console.warn("Cannot calculate fit-width zoom: Viewport dimensions not available.");
                return 1; // Fallback
            }

            // --- Calculate the baseScale (fit-to-viewport scale) ---
            let baseScale = 1;
            const secondaryElement = (this.videoDisplay2.style.display === 'block') ? this.videoDisplay2 : this.imgDisplay2;
            let isDualPageVisible = this.dualPageMode && secondaryElement.style.display === 'block';
            let naturalW2 = 0, naturalH2 = 0;

            if (isDualPageVisible) {
                const secondaryIsVideoCheck = secondaryElement === this.videoDisplay2;
                naturalW2 = secondaryIsVideoCheck ? secondaryElement.videoWidth : secondaryElement.naturalWidth;
                naturalH2 = secondaryIsVideoCheck ? secondaryElement.videoHeight : secondaryElement.naturalHeight;

                // Apply MIN_ERROR_DIM logic
                if (!secondaryIsVideoCheck && secondaryElement.classList.contains('viewer-error-state')) {
                    naturalW2 = Math.max(naturalW2, MIN_ERROR_DIM);
                    naturalH2 = Math.max(naturalH2, MIN_ERROR_DIM);
                } else if (!secondaryIsVideoCheck && (!naturalW2 || naturalW2 <= 0 || !naturalH2 || naturalH2 <= 0)) {
                    if (naturalW1 > 0 && naturalH1 > 0 && naturalW1 !== MIN_ERROR_DIM) {
                        // console.warn("Fit Width: Secondary dimensions invalid, using primary fallback.");
                        naturalW2 = naturalW1; naturalH2 = naturalH1;
                    } else {
                        // console.warn("Fit Width: Secondary dimensions invalid, using min error dim.");
                        naturalW2 = MIN_ERROR_DIM; naturalH2 = MIN_ERROR_DIM;
                    }
                } else if (secondaryIsVideoCheck && (!naturalW2 || naturalW2 <= 0 || !naturalH2 || naturalH2 <= 0)) {
                    // console.warn("Fit Width: Secondary video dimensions invalid, using min error dim.");
                    naturalW2 = MIN_ERROR_DIM; naturalH2 = MIN_ERROR_DIM;
                }
                // Ensure W2/H2 are not zero if dual page is visible
                if (naturalW2 <= 0) naturalW2 = MIN_ERROR_DIM;
                if (naturalH2 <= 0) naturalH2 = MIN_ERROR_DIM;

                // If dimensions are still invalid after checks, treat as single page for scaling
                if (naturalW2 <= 0 || naturalH2 <= 0) {
                    isDualPageVisible = false;
                    // console.warn("Fit Width: Secondary dimensions invalid after fallback, treating as single for scale.");
                }
            }

            // Calculate base scale
            let useDualForScale = isDualPageVisible; // Use separate flag for clarity

            if (useDualForScale) {
                let requiredWidthPerScale, requiredHeightPerScale;
                if (!rotIs90) { // Side-by-side
                    requiredWidthPerScale = naturalW1 + naturalW2;
                    requiredHeightPerScale = Math.max(naturalH1, naturalH2);
                } else { // Top-and-bottom
                    requiredWidthPerScale = Math.max(naturalH1, naturalH2);
                    requiredHeightPerScale = naturalW1 + naturalW2;
                }
                const scaleLimitW = requiredWidthPerScale > 0 ? viewportWidth / requiredWidthPerScale : Infinity;
                const scaleLimitH = requiredHeightPerScale > 0 ? viewportHeight / requiredHeightPerScale : Infinity;
                baseScale = Math.min(scaleLimitW, scaleLimitH);
            } else { // Single page mode (or fallback)
                const effectiveW = rotIs90 ? naturalH1 : naturalW1; // Uses W1/H1 which are guaranteed > 0
                const effectiveH = rotIs90 ? naturalW1 : naturalH1;
                const scaleLimitW = effectiveW > 0 ? viewportWidth / effectiveW : Infinity;
                const scaleLimitH = effectiveH > 0 ? viewportHeight / effectiveH : Infinity;
                baseScale = Math.min(scaleLimitW, scaleLimitH);
            }
            baseScale = Math.max(baseScale, 0);


            // --- Calculate the desired final scale to achieve fit-width ---
            let desiredFinalScale = 1.0;
            let targetWidth = 0; // The width we want the content to scale to (viewport width)

            // Use potentially adjusted dimensions, use useDualForScale flag
            if (useDualForScale) { // W2/H2 validity checked above
                if (!rotIs90) { // Side-by-side: target width is W1+W2
                    targetWidth = naturalW1 + naturalW2;
                } else { // Top-and-bottom: target width is max(H1, H2)
                    targetWidth = Math.max(naturalH1, naturalH2);
                }
            } else { // Single page: target width is effectiveW
                targetWidth = rotIs90 ? naturalH1 : naturalW1; // Uses W1/H1 which are guaranteed > 0
            }

            if (targetWidth > 0) {
                desiredFinalScale = viewportWidth / targetWidth;
            } else {
                console.warn("Cannot calculate fit-width zoom: Target width is zero.");
                desiredFinalScale = baseScale > 0 ? baseScale : 1.0; // Fallback
            }
            desiredFinalScale = Math.max(desiredFinalScale, 0);


            // --- Calculate the required currentZoom ---
            let finalZoom = 1.0;
            if (baseScale > 0) {
                finalZoom = desiredFinalScale / baseScale;
            } else {
                console.warn("Cannot calculate fit-width zoom: baseScale is zero.");
                finalZoom = 1.0; // Fallback
            }

            // Ensure zoom doesn't go below the base 'fit' level (zoom=1).
            finalZoom = Math.max(1.0, finalZoom);
            return finalZoom;
        }

        pan(deltaX, deltaY) {
            if (this.currentZoom > 1) {
                this.parent.scrollBy(deltaX, deltaY);
            }
        }

        toggleDualPageMode() {
            this.dualPageMode = !this.dualPageMode;
            // Call loadAndShowIndex to refresh the view with the current index,
            // applying the new dual page mode logic.
            this.loadAndShowIndex(this.currentIndex);
        }

        toggleRightToLeftMode() {
            if (!this.dualPageMode) {
                this.toggleDualPageMode();
            }
            this.rightToLeftMode = !this.rightToLeftMode;
            this.loadAndShowIndex(this.currentIndex);
        }

        enableDualPageModeOrCycleLayout() {
            if (!this.dualPageMode) {
                this.dualPageMode = true;
            } else {
                const { primary: currentPrimaryIndex } = this._getDualPageIndices(this.currentIndex);
                if (currentPrimaryIndex === null) {
                    this.dualPageMode = false;
                    console.log("Could not determine left page, toggling back to Single Page Mode.");
                } else {
                    // The logic toggles between 'odd-first' and 'even-first' layouts.
                    // In LTR, if the primary (left) index is even, we're in `odd-first` layout and switch to `even-first`.
                    // In RTL, page order is swapped, so if the primary (left) index is even, we're already in `even-first` layout
                    // and must switch to `odd-first`. This XOR condition correctly handles the toggle for both modes.
                    if ((currentPrimaryIndex % 2 === 0) !== this.rightToLeftMode) {
                        this.dualLayout = 'even-first';
                    } else {
                        this.dualLayout = 'odd-first';
                    }
                }
            }
            this.loadAndShowIndex(this.currentIndex);
        }

        downloadCurrentImage() {
            if (this.isGridViewActive()) return;

            const { primary, secondary } = this._getDualPageIndices(this.currentIndex);
            const sStr = secondary ? "s" : "";

            createNotification(`Downloading original image${sStr}..`, 30);
            const items = [thumbs[primary]];
            if (secondary) items.push(thumbs[secondary]);

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

                if (item.originalImageUrl) {
                    const imgElement = new Image();
                    imgElement.referrerPolicy = "no-referrer";
                    imgElement.src = item.originalImageUrl;
                    waitForMediaLoad(imgElement)
                        .then(() => {
                            saveImage(item.originalImageUrl);
                        })
                        .catch((error) => {
                            console.error("Failed to load original, downloading downscaled image:", error);
                            createNotification("Failed to load original, using downscaled.");
                            saveImage(item.fullImageUrl);
                        });
                } else {
                    saveImage(item.fullImageUrl);
                }
            }

            createNotification(`Image${sStr} saved`);
        }

        showGalleriesWithCurrentImage() {
            showGalleriesWithThisImage(thumbs[this.currentIndex].fullImageUrl);
        }

        async loadAndShowIndex(index) {
            const wasActive = this.isActive();
            const gridViewWasActive = this.isGridViewActive();
            if (this.isGridViewActive()) this.toggleGridView();

            // Ensure viewer is active/visible
            this._ensureViewerActive();

            // Prepare and Validate
            const prepResult = this._prepareLoadOperation(index);
            if (!prepResult) return; // Invalid index or error during prep
            const { controller, loadToken, targetIndex, navigatedBackwards } = prepResult;

            // Determine Display Indices & Update Label
            const { displayPrimaryIndex, displaySecondaryIndex } = this._getDisplayIndicesAndLabel(targetIndex);

            // Initiate Dimming
            const dimTimeoutId = this._initiateDimming(targetIndex, loadToken, wasActive, gridViewWasActive, displayPrimaryIndex, displaySecondaryIndex);

            try {
                // Fetch Required URLs
                await this._fetchRequiredUrls([displayPrimaryIndex, displaySecondaryIndex], controller.signal, loadToken);
                // Note: _fetchRequiredUrls throws AbortError if cancelled

                // Display Loaded Images (This now handles un-dimming internally per image)
                await this._displayLoadedImages(targetIndex, displayPrimaryIndex, displaySecondaryIndex, controller.signal, loadToken);
                // Note: _displayLoadedImages calls _show which throws AbortError if cancelled during its await

                // Post-Display Tasks (Sidebar, Scroll Anchor, Preload)
                this._performPostDisplayTasks(targetIndex, displayPrimaryIndex, displaySecondaryIndex, navigatedBackwards, controller.signal, loadToken);

            } catch (error) {
                // Handle Errors (This also handles un-dimming via helper)
                this._handleLoadError(error, targetIndex, loadToken);

            } finally {
                // Final Cleanup (This also handles final un-dimming via helper)
                this._finalizeLoadAttempt(loadToken, dimTimeoutId);
            }
        }

        navigateForward() {
            this.backwardNavigationCount = 0;

            // 1. Get current display state
            const { primary: currentPrimary, secondary: currentSecondary } = this._getDualPageIndices(this.currentIndex);

            // 2. Find the highest index currently displayed
            let maxCurrent = -1;
            if (currentPrimary !== null) {
                maxCurrent = currentPrimary;
            }
            if (currentSecondary !== null && currentSecondary > maxCurrent) {
                maxCurrent = currentSecondary;
            }
            if (maxCurrent === -1) {
                // If nothing is displayed (error state?), try navigating from currentIndex
                maxCurrent = this.currentIndex;
            }


            // 3. Calculate initial next target
            let nextTargetIndex = maxCurrent + 1;

            // 4. Skip deleted indices forward
            while (nextTargetIndex < thumbs.length && thumbs[nextTargetIndex] === 'deleted') {
                nextTargetIndex++;
            }

            // 5. Check bounds
            if (nextTargetIndex >= thumbs.length) {
                createNotification("Last page!");
                return;
            }

            // 6. Load the determined target index
            this.loadAndShowIndex(nextTargetIndex);
        }

        navigateBack() {
            this.backwardNavigationCount++;

            // 1. Get current display state
            const { primary: currentPrimary, secondary: currentSecondary } = this._getDualPageIndices(this.currentIndex);

            // 2. Find the lowest index currently displayed
            let minCurrent = -1;
            if (currentPrimary !== null) {
                minCurrent = currentPrimary;
            }
            if (currentSecondary !== null && currentSecondary < minCurrent) {
                minCurrent = currentSecondary;
            }
            if (minCurrent === -1) {
                // If nothing is displayed (error state?), try navigating from currentIndex
                minCurrent = this.currentIndex;
            }

            // 3. Calculate initial previous target
            let prevTargetIndex = minCurrent - 1;

            // 4. Skip deleted indices backward
            while (prevTargetIndex >= 0 && thumbs[prevTargetIndex] === 'deleted') {
                prevTargetIndex--;
            }

            // Needed for 'selected-first', as otherwise it would only go back one page
            // at a time. It should not affect the other layouts in theory, but let's
            // make it a special case anyways.
            if (this.dualPageMode && prevTargetIndex > 0 && this.dualLayout === 'selected-first') {
                prevTargetIndex--;
            }

            // 5. Check bounds
            if (prevTargetIndex < 0) {
                // Special case check for 'even-first' mode at index 1 or 0
                // If mode is 'even-first' and current primary was 1 (showing 1,2), target is 0.
                // If target 0 is valid, we should allow navigating to it.
                // The _getDualPageIndices handles showing only 0 if needed.
                if (this.dualLayout === 'even-first' && minCurrent === 1 && prevTargetIndex === -1 && thumbs.length > 0 && thumbs[0] !== 'deleted') {
                    prevTargetIndex = 0; // Allow navigating to index 0
                } else {
                    createNotification("First page!");
                    return;
                }
            }

            // 6. Load the determined target index
            this.loadAndShowIndex(prevTargetIndex);
        }

        goOrScrollToIndex(index) {
            if (this.isGridViewActive()) {
                this.gridView.scrollToIndex(index);
            } else {
                this.loadAndShowIndex(index);
            }
        }

        enterFullscreen() {
            this.hasEnteredFullscreenOnce = true;
            requestFullscreen(document.body);
        }

        exitFullscreen() {
            exitFullscreen();
        }

        toggleFullscreen() {
            if (isFullscreen()) {
                this.exitFullscreen();
            } else {
                this.enterFullscreen();
            }
        }

        toggleFullscreen() {
            if (isFullscreen()) {
                this.exitFullscreen();
            } else {
                this.enterFullscreen();
            }
        }

        closeViewer(exitToPage) {
            console.log('Closing viewer');
            unlockPageScroll();
            this.parent.style.display = "none";

            // --- Pause Videos ---
            if (!this.videoDisplay.paused) {
                this.videoDisplay.currentTime = 0;
                this.videoDisplay.pause();
            }
            if (!this.videoDisplay2.paused) {
                this.videoDisplay2.currentTime = 0;
                this.videoDisplay2.pause();
            }
            // Clean up blob URLs
            this._revokeBlobUrl(this.videoDisplay);
            this._revokeBlobUrl(this.videoDisplay2);

            // Clear src to release resources
            this.videoDisplay.removeAttribute('src');
            this.videoDisplay2.removeAttribute('src');
            this.imgDisplay.removeAttribute('src');
            this.imgDisplay2.removeAttribute('src');


            // Abort any ongoing load operation
            if (this.currentAbortController) {
                this.currentAbortController.abort("Viewer closing"); // Add reason
                this.currentAbortController = null;
            }
            this.currentLoadToken = null; // Clear token as well


            // Reset state (maybe?)
            // Let's preserve zoom and rotation for now.
            this.currentIndex = 0;
            this.userChangedZoom = false;
            // this.currentZoom = 1.0;
            // this.currentRotation = 0;

            const wasFullscreen = isFullscreen();
            if (wasFullscreen) {
                const onFullscreenChange = () => {
                    if (document.fullscreenElement) return; // Still in fullscreen (maybe entered again?)
                    // Ensure cleanup happens only once
                    document.removeEventListener("fullscreenchange", onFullscreenChange);
                    if (this.onExit) {
                        console.log("Calling onExit after fullscreen exit.");
                        this.onExit(exitToPage);
                    }
                };
                document.addEventListener("fullscreenchange", onFullscreenChange);
                this.exitFullscreen();
            }

            if (this.isGridViewActive()) {
                this.toggleGridView(); // Ensure grid view is hidden and stopped
            }
            if (this.sidebarIsActive() && !this.pinSidebar) {
                this.sidebarHideInstant(); // Hide sidebar immediately if not pinned
            }
            if (this.chapterSidebarIsActive()) {
                this.toggleChapterSidebar(); // Hide chapter sidebar
            }
            if (this.isHelpOverlayVisible()) {
                this.toggleHelpOverlay(); // Hide help overlay
            }

            // Restore scroll position if we entered fullscreen
            // Only restore if we actually *used* fullscreen during this session
            if (this.hasEnteredFullscreenOnce && this.lastScrollPosition) {
                // *** ADDED *** Revoke any leftover blob URLs
                this._revokeBlobUrl(this.videoDisplay);
                this._revokeBlobUrl(this.videoDisplay2);
                // Delay slightly to allow fullscreen exit animation to complete?
                requestAnimationFrame(() => {
                    window.scrollTo(this.lastScrollPosition.x, this.lastScrollPosition.y);
                    this.lastScrollPosition = null; // Clear after restoring
                });
            } else {
                this.lastScrollPosition = null; // Clear if not used
            }


            this.hasEnteredFullscreenOnce = false; // Reset flag for next opening

            if (this.config.helpAndSettingsButtons === 'proximity') {
                this.buttonContainer.style.opacity = '0';
                this.buttonContainer.style.pointerEvents = 'none';
            }

            // Call onExit callback immediately if not handling fullscreen exit
            if (!wasFullscreen && this.onExit) {
                console.log("Calling onExit (not fullscreen).");
                this.onExit(exitToPage);
            }
        }

        updateTransforms() {
            // --- Determine Active Elements and Media Types ---
            const primaryElement = (this.videoDisplay.style.display === 'block') ? this.videoDisplay : this.imgDisplay;
            const primaryIsVideo = primaryElement === this.videoDisplay && primaryElement.style.display === 'block';
            const secondaryElement = (this.videoDisplay2.style.display === 'block') ? this.videoDisplay2 : this.imgDisplay2;
            // isDualPageVisible: Check if dual mode is on AND secondary element is actually being displayed
            const isDualPageVisible = this.dualPageMode && secondaryElement.style.display === 'block';
            const secondaryIsVideo = isDualPageVisible && secondaryElement === this.videoDisplay2;


            // --- Handle Rotation ---
            let currentRotation = this.currentRotation;
            const rotIs90 = currentRotation % 180 !== 0;

            // --- Get Natural Media Dimensions (from active elements) ---
            let naturalW1 = 0, naturalH1 = 0;
            const MIN_ERROR_DIM = 100; // Min width/height for error placeholder visual

            if (primaryElement.style.display === 'block') {
                naturalW1 = primaryIsVideo ? primaryElement.videoWidth : primaryElement.naturalWidth;
                naturalH1 = primaryIsVideo ? primaryElement.videoHeight : primaryElement.naturalHeight;

                // Check for error state or invalid dimensions on the *image* element specifically
                // Videos have their own dimensions which should be valid if displayed.
                if (!primaryIsVideo && primaryElement.classList.contains('viewer-error-state')) {
                    // Assign minimum dimensions for error placeholder
                    naturalW1 = Math.max(naturalW1, MIN_ERROR_DIM);
                    naturalH1 = Math.max(naturalH1, MIN_ERROR_DIM);
                } else if (!primaryIsVideo && (!naturalW1 || naturalW1 <= 0 || !naturalH1 || naturalH1 <= 0)) {
                    // Treat other invalid image dimensions as error case for sizing.
                    // console.warn("Primary image dimensions invalid or zero, assigning min error dimensions.");
                    naturalW1 = MIN_ERROR_DIM;
                    naturalH1 = MIN_ERROR_DIM;
                } else if (primaryIsVideo && (!naturalW1 || naturalW1 <= 0 || !naturalH1 || naturalH1 <= 0)) {
                    // If video dimensions are somehow invalid/zero after load, treat similarly?
                    // console.warn("Primary video dimensions invalid or zero after load, assigning min error dimensions.");
                    naturalW1 = MIN_ERROR_DIM;
                    naturalH1 = MIN_ERROR_DIM;
                }
            } else {
                // If primary element isn't displayed at all, use MIN dimensions for robustness
                // This case shouldn't ideally happen if _show logic is correct.
                // console.warn("Primary element not displayed in updateTransforms, assigning min error dimensions.");
                naturalW1 = MIN_ERROR_DIM;
                naturalH1 = MIN_ERROR_DIM;
            }


            let naturalW2 = 0, naturalH2 = 0;
            if (isDualPageVisible) { // isDualPageVisible already implies secondaryElement is set and display=block
                naturalW2 = secondaryIsVideo ? secondaryElement.videoWidth : secondaryElement.naturalWidth;
                naturalH2 = secondaryIsVideo ? secondaryElement.videoHeight : secondaryElement.naturalHeight;

                // Check for error state or invalid dimensions on the *image* element specifically
                if (!secondaryIsVideo && secondaryElement.classList.contains('viewer-error-state')) {
                    naturalW2 = Math.max(naturalW2, MIN_ERROR_DIM);
                    naturalH2 = Math.max(naturalH2, MIN_ERROR_DIM);
                } else if (!secondaryIsVideo && (!naturalW2 || naturalW2 <= 0 || !naturalH2 || naturalH2 <= 0)) {
                    // Use primary's dimensions if valid, else use min error dim.
                    if (naturalW1 > 0 && naturalH1 > 0 && naturalW1 !== MIN_ERROR_DIM) { // Check if primary has real dimensions
                        // console.warn("Secondary image dimensions invalid, using primary as fallback.");
                        naturalW2 = naturalW1; naturalH2 = naturalH1;
                    } else {
                        // console.warn("Secondary image dimensions invalid, assigning min error dimensions.");
                        naturalW2 = MIN_ERROR_DIM; naturalH2 = MIN_ERROR_DIM;
                    }
                } else if (secondaryIsVideo && (!naturalW2 || naturalW2 <= 0 || !naturalH2 || naturalH2 <= 0)) {
                    // console.warn("Secondary video dimensions invalid or zero after load, assigning min error dimensions.");
                    naturalW2 = MIN_ERROR_DIM;
                    naturalH2 = MIN_ERROR_DIM;
                }
            }


            // --- Get Viewport ---
            const viewportWidth = this.parent.clientWidth;
            const viewportHeight = this.parent.clientHeight;
            // If viewport is zero, calculations below are meaningless. Return early.
            if (viewportWidth <= 0 || viewportHeight <= 0) {
                console.warn("Viewport dimensions are zero or negative in updateTransforms.");
                return;
            }


            // --- Set Fit Mode Zoom Level (if not manually zoomed) ---
            if (!this.userChangedZoom) {
                if (this.fitMode == "fit-width") {
                    this.currentZoom = this._getFitWidthZoom(); // Uses MIN_ERROR_DIM internally now
                } else if (this.fitMode == "one-to-one") {
                    this.currentZoom = this._getOneToOneZoom(); // Uses MIN_ERROR_DIM internally now
                } else { // Default to fit-window
                    this.currentZoom = 1.0;
                }
                // Set min zoom based on the calculated 'fit' zoom, but ensure it's not > 1
                // Ensure minZoomLevel is calculated based on the potentially adjusted fit-mode zoom.
                // The base 'fit-window' zoom (currentZoom=1) acts as the lower bound here.
                this.minZoomLevel = Math.min(this.currentZoom, 1.0);
            }

            // --- Calculate Base Scale (Fit-to-Viewport) ---
            let baseScale = 1;
            // Use the potentially adjusted naturalW1/H1/W2/H2 (which now include MIN_ERROR_DIM)
            // Ensure primary dimensions are positive before calculating scale
            if (naturalW1 <= 0 || naturalH1 <= 0) {
                console.error("Primary dimensions are still <= 0 before scale calculation. Setting scale to 0.");
                baseScale = 0; // Avoid division by zero
            } else {
                // Check secondary dimensions validity only if dual page is intended
                let useDualForScale = isDualPageVisible;
                if (useDualForScale && (naturalW2 <= 0 || naturalH2 <= 0)) {
                    console.warn("Secondary dimensions are <= 0 during scale calc, treating as single page for scaling.");
                    useDualForScale = false;
                }

                if (useDualForScale) {
                    let requiredWidthPerScale, requiredHeightPerScale;
                    if (!rotIs90) { // Side-by-side
                        requiredWidthPerScale = naturalW1 + naturalW2;
                        requiredHeightPerScale = Math.max(naturalH1, naturalH2);
                    } else { // Top-and-bottom
                        requiredWidthPerScale = Math.max(naturalH1, naturalH2);
                        requiredHeightPerScale = naturalW1 + naturalW2;
                    }
                    // Check for zero denominator before division
                    const scaleLimitW = requiredWidthPerScale > 0 ? viewportWidth / requiredWidthPerScale : Infinity;
                    const scaleLimitH = requiredHeightPerScale > 0 ? viewportHeight / requiredHeightPerScale : Infinity;
                    baseScale = Math.min(scaleLimitW, scaleLimitH);
                } else { // Single page mode (or fallback)
                    const effectiveW = rotIs90 ? naturalH1 : naturalW1;
                    const effectiveH = rotIs90 ? naturalW1 : naturalH1;
                    // Check for zero denominator before division
                    const scaleLimitW = effectiveW > 0 ? viewportWidth / effectiveW : Infinity;
                    const scaleLimitH = effectiveH > 0 ? viewportHeight / effectiveH : Infinity;
                    baseScale = Math.min(scaleLimitW, scaleLimitH);
                }
            }

            baseScale = Math.max(baseScale, 0); // Ensure non-negative scale

            // --- Final Scale incorporating User Zoom ---
            const finalScale = baseScale * this.currentZoom;

            // --- Calculate Final CSS Dimensions for Media ---
            // Uses potentially adjusted W/H. If error, these will be MIN_ERROR_DIM * finalScale
            const primaryCssWidth = naturalW1 * finalScale;
            const primaryCssHeight = naturalH1 * finalScale;
            let secondaryCssWidth = 0, secondaryCssHeight = 0;
            // Use isDualPageVisible which confirms secondary is displayed and dimensions should be valid (>=MIN_ERROR_DIM)
            if (isDualPageVisible) {
                secondaryCssWidth = naturalW2 * finalScale;
                secondaryCssHeight = naturalH2 * finalScale;
            }


            // --- Apply Styles to Visible Media Elements ---
            const rotationTransform = `rotate(${currentRotation}deg)`;
            const primaryTransition = this.config.enableAnimations ? 'transform 0.3s ease-out' : 'none'; // Smoother ease-out
            const secondaryTransition = this.config.enableAnimations ? 'transform 0.3s ease-out' : 'none';

            primaryElement.style.width = primaryCssWidth + 'px';
            primaryElement.style.height = primaryCssHeight + 'px';
            primaryElement.style.transform = rotationTransform;
            // Apply transition only if animations are enabled AND it's not the error state
            // (error state might look weird transforming)
            primaryElement.style.transition = (this.config.enableAnimations && !primaryElement.classList.contains('viewer-error-state')) ? primaryTransition : 'none';


            if (isDualPageVisible) {
                secondaryElement.style.width = secondaryCssWidth + 'px';
                secondaryElement.style.height = secondaryCssHeight + 'px';
                secondaryElement.style.transform = rotationTransform;
                secondaryElement.style.transition = (this.config.enableAnimations && !secondaryElement.classList.contains('viewer-error-state')) ? secondaryTransition : 'none';
            } else {
                // Ensure *both* potential secondary elements have styles cleared if not visible
                this.imgDisplay2.style.width = 'auto'; this.imgDisplay2.style.height = 'auto';
                this.imgDisplay2.style.transform = ''; this.imgDisplay2.style.transition = '';
                this.videoDisplay2.style.width = 'auto'; this.videoDisplay2.style.height = 'auto';
                this.videoDisplay2.style.transform = ''; this.videoDisplay2.style.transition = '';
            }

            // --- Calculate Container Dimensions (based on final scaled media) ---
            let containerContentWidth, containerContentHeight;
            if (isDualPageVisible) {
                if (!rotIs90) { // Side-by-side
                    containerContentWidth = primaryCssWidth + secondaryCssWidth;
                    containerContentHeight = Math.max(primaryCssHeight, secondaryCssHeight);
                    this.imgContainer.style.flexDirection = "row";
                } else { // Top-and-bottom
                    containerContentWidth = Math.max(primaryCssWidth, secondaryCssWidth);
                    containerContentHeight = primaryCssHeight + secondaryCssHeight;
                    this.imgContainer.style.flexDirection = "column";
                }
            } else { // Single page
                containerContentWidth = primaryCssWidth;
                containerContentHeight = primaryCssHeight;
                this.imgContainer.style.flexDirection = "row"; // Default for single
            }

            // --- Calculate Container Visual Size and Position ---
            const visualContainerWidth = rotIs90 ? containerContentHeight : containerContentWidth;
            const visualContainerHeight = rotIs90 ? containerContentWidth : containerContentHeight;

            // Avoid negative dimensions which can cause issues
            const clampedVisualWidth = Math.max(0, visualContainerWidth);
            const clampedVisualHeight = Math.max(0, visualContainerHeight);


            const needsScrollX = clampedVisualWidth > viewportWidth;
            const needsScrollY = clampedVisualHeight > viewportHeight;

            // Center container if smaller than viewport, align top-left if larger
            let containerTop = needsScrollY ? 0 : (viewportHeight - clampedVisualHeight) / 2;
            let containerLeft = needsScrollX ? 0 : (viewportWidth - clampedVisualWidth) / 2;

            // Ensure top/left are not negative if calculation somehow results in it
            containerTop = Math.max(0, containerTop);
            containerLeft = Math.max(0, containerLeft);


            // Apply position and size to the container
            this.imgContainer.style.top = `${containerTop}px`;
            this.imgContainer.style.left = `${containerLeft}px`;
            this.imgContainer.style.width = `${clampedVisualWidth}px`;
            this.imgContainer.style.height = `${clampedVisualHeight}px`;

            // --- Update Parent Overflow ---
            const oldOverflowX = this.parent.style.overflowX;
            const oldOverflowY = this.parent.style.overflowY;
            const newOverflowX = needsScrollX ? "auto" : "hidden";
            const newOverflowY = needsScrollY ? "auto" : "hidden";

            if (newOverflowX !== oldOverflowX) {
                this.parent.style.overflowX = newOverflowX;
                // Reset scroll only if changing *to* hidden (prevents jumpiness if already hidden)
                if (newOverflowX === 'hidden' && this.parent.scrollLeft > 0) {
                    // Use requestAnimationFrame to avoid potential race conditions with layout updates
                    requestAnimationFrame(() => { this.parent.scrollLeft = 0; });
                }
            }
            if (newOverflowY !== oldOverflowY) {
                this.parent.style.overflowY = newOverflowY;
                if (newOverflowY === 'hidden' && this.parent.scrollTop > 0) {
                    requestAnimationFrame(() => { this.parent.scrollTop = 0; });
                }
            }
        }

        _revokeBlobUrl(videoElement) {
            if (videoElement._currentBlobUrl) {
                URL.revokeObjectURL(videoElement._currentBlobUrl);
                videoElement._currentBlobUrl = null;
            }
        }

        _createImageViewer() {
            this.parent = document.createElement('div');
            this.parent.id = "Overlay";
            Object.assign(this.parent.style, {
                position: "fixed",
                top: "0",
                left: "0",
                width: "100vw",
                height: "100vh",
                backgroundColor: "rgba(0, 0, 0, 1)",
                display: "none", // Initially hidden
                overflowX: "hidden",
                overflowY: "hidden",
                zIndex: "9999",
                cursor: "default",
            });

            // imgContainer: Positioned absolutely, calculations done in JS
            this.imgContainer = document.createElement("div");
            Object.assign(this.imgContainer.style, {
                position: "absolute", // Manual positioning
                display: "flex", // Still use flex for internal layout (dual page)
                alignItems: "center", // Align images/videos within the flex row/column
                justifyContent: "center", // Center images/videos within the flex row/column
                gap: "0",
                margin: "",
                transform: "", // Rotations applied to images/videos directly
                transformOrigin: "",
                // Top/Left will be set dynamically
            });

            // --- Add Basic CSS for Error State and Message ---
            const styleSheet = document.createElement("style");
            styleSheet.textContent = `
      .viewer-error-state {
        background-color: #222; /* Dark background for the placeholder */
        border: 1px dashed #555; /* Dashed border */
        padding: 20px;
        box-sizing: border-box;
        display: flex;
        align-items: center;
        justify-content: center;
        object-fit: contain; /* Prevent tiny gif from stretching weirdly */
        image-rendering: pixelated; /* Keep tiny gif sharp if scaled up */
        /* position: relative; /* Needed if error message was child */
      }
      .viewer-error-message {
        /* Position it relative to the imgContainer */
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: rgba(0, 0, 0, 0.75);
        color: white;
        padding: 8px 12px;
        border-radius: 4px;
        font-size: 13px;
        font-family: Arial, sans-serif;
        z-index: 10; /* Ensure it's above the image */
        text-align: center;
        max-width: 90%;
        word-break: break-word; /* Important for long URLs */
        box-sizing: border-box;
        pointer-events: all; /* Allow clicking links */
      }
      .viewer-error-message a {
         color: #9cf; /* Lighter blue for links */
         text-decoration: underline;
      }
    `;
            // Prepend to head to allow user styles potentially override later
            document.head.insertBefore(styleSheet, document.head.firstChild);


            // --- Create Image Elements ---
            this.imgDisplay = document.createElement("img");
            Object.assign(this.imgDisplay.style, {
                // transition: "transform 0.3s", // Apply transition dynamically if needed
                transformOrigin: "center",
                margin: "0",
                padding: "0",
                display: "none", // Initially hidden, shown based on media type
                maxWidth: "none",
                maxHeight: "none",
                width: "auto",
                height: "auto",
                // Transform (rotation) set dynamically
            });

            this.imgDisplay2 = document.createElement("img");
            Object.assign(this.imgDisplay2.style, {
                // transition: "transform 0.3s",
                transformOrigin: "center",
                margin: "0",
                padding: "0",
                display: "none", // Initially hidden
                maxWidth: "none",
                maxHeight: "none",
                width: "auto",
                height: "auto",
                // Transform (rotation) set dynamically
            });

            // --- Create Video Elements ---
            // Helper function to add hover controls to video elements
            const addVideoHoverControls = (videoElement) => {
                // Initially hide controls
                videoElement.controls = false;

                // Show controls on mouse enter
                videoElement.addEventListener('mouseenter', () => {
                    // Only show controls if the video element is currently displayed
                    if (videoElement.style.display === 'block') {
                        videoElement.controls = true;
                    }
                });

                // Hide controls on mouse leave
                videoElement.addEventListener('mouseleave', () => {
                    videoElement.controls = false;
                });
            };

            this.videoDisplay = document.createElement("video");
            this.videoDisplay.playsInline = true; // Important for mobile & some desktop contexts
            this.videoDisplay.preload = "metadata"; // Load dimensions without full video
            this.videoDisplay.muted = false;
            this.videoDisplay.loop = this.loopEnabled; // Set initial loop state
            Object.assign(this.videoDisplay.style, {
                // No transition on video transform by default
                transformOrigin: "center",
                margin: "0",
                padding: "0",
                display: "none", // Initially hidden, shown based on media type
                maxWidth: "none", // Allow explicit sizing
                maxHeight: "none",
                width: "auto",
                height: "auto",
                // Transform (rotation) set dynamically, potentially disabled for video
            });
            addVideoHoverControls(this.videoDisplay);

            this.videoDisplay2 = document.createElement("video");
            this.videoDisplay2.playsInline = true;
            this.videoDisplay2.preload = "metadata";
            this.videoDisplay2.muted = false;
            this.videoDisplay2.loop = this.loopEnabled; // Set initial loop state
            Object.assign(this.videoDisplay2.style, {
                // No transition on video transform by default
                transformOrigin: "center",
                margin: "0",
                padding: "0",
                display: "none", // Initially hidden
                maxWidth: "none",
                maxHeight: "none",
                width: "auto",
                height: "auto",
                // Transform (rotation) set dynamically
            });
            addVideoHoverControls(this.videoDisplay2);


            this.gridParent = document.createElement('div');
            Object.assign(this.gridParent.style, {
                position: "fixed",
                top: "0",
                left: "0",
                width: "100vw",
                height: "100vh",
                backgroundColor: "rgba(0, 0, 0, 1)",
                display: "none",
                zIndex: "99999",
                cursor: "default",
                overflow: "auto",
                // display: "none", // Already set
            });

            // Create container for buttons
            this.buttonContainer = document.createElement('div');
            Object.assign(this.buttonContainer.style, {
                position: 'fixed',
                top: '10px',
                right: '20px',
                display: 'flex',
                gap: '8px',
                zIndex: '10000', // Ensure buttons are above media container
                transition: 'opacity 0.3s ease',
            });

            this._initializeToolbarButtons();
            this.parent.appendChild(this.buttonContainer);
            this._updateToolbarButtons();


            if (this.config.viewerLabels !== 'disabled') {
                this.pageNumberLabel = document.createElement('span');
                Object.assign(this.pageNumberLabel.style, {
                    position: 'fixed',
                    bottom: '10px',
                    right: '20px',
                    backgroundColor: `rgba(0, 0, 0, ${this.UI_TRANSPARENCY})`,
                    color: 'white',
                    padding: '6px',
                    paddingTop: '4px',
                    fontSize: '18px',
                    borderRadius: '6px',
                    fontWeight: 'bold',
                    zIndex: '10000',
                    pointerEvents: 'none',
                    transition: 'opacity 0.3s ease',
                });

                if (this.config.viewerLabels === 'proximity') {
                    this.pageNumberLabel.style.opacity = '0';
                }

                this.parent.appendChild(this.pageNumberLabel);
            }

            // Append images AND videos to container, container to parent
            // Order might matter for flex layout if items have different dimensions before scaling
            this.imgContainer.appendChild(this.imgDisplay);
            this.imgContainer.appendChild(this.videoDisplay); // Append video counterpart
            this.imgContainer.appendChild(this.imgDisplay2);
            this.imgContainer.appendChild(this.videoDisplay2); // Append video counterpart
            this.parent.appendChild(this.imgContainer);
            document.body.appendChild(this.parent);
            // Note: gridParent is appended separately when grid view is toggled.
        }

        _initializeToolbarButtons() {
            // Defines all toolbar buttons and their actions.
            this.buttonDefinitions = [
                {
                    prop: 'exitButton',
                    text: '⤬',
                    onClick: () => this.closeViewer(false),
                    altText: 'Exit viewer (Esc)',
                    offsetY: -2
                },
                {
                    prop: 'prevButton',
                    text: '←',
                    onClick: () => this.navigateBack(),
                    altText: 'Previous Page (Left Arrow)',
                    offsetY: 0
                },
                {
                    prop: 'nextButton',
                    text: '→',
                    onClick: () => this.navigateForward(),
                    altText: 'Next Page (Right Arrow)',
                    offsetY: 0
                },
                {
                    prop: 'rotateLeftButton',
                    text: '↺',
                    onClick: () => this.rotateImage(-90),
                    altText: 'Rotate Left (L)',
                    offsetY: -2
                },
                {
                    prop: 'rotateRightButton',
                    text: '↻',
                    onClick: () => this.rotateImage(90),
                    altText: 'Rotate Right (R)',
                    offsetY: -2
                },
                {
                    prop: 'zoomOutButton',
                    text: '-',
                    onClick: () => this.zoom(-1),
                    altText: 'Zoom Out (-)',
                    offsetY: 0
                },
                {
                    prop: 'zoomInButton',
                    text: '+',
                    onClick: () => this.zoom(1),
                    altText: 'Zoom In (+)',
                    offsetY: 0
                },
                {
                    prop: 'galleryViewButton',
                    text: '▦',
                    onClick: () => this.toggleGridView(),
                    altText: 'Toggle Gallery View (Backspace)',
                    offsetY: -1
                },
                {
                    prop: 'dualPageButton',
                    text: '◫',
                    onClick: () => this.toggleDualPageMode(),
                    altText: 'Toggle Dual Page Mode (Shift+D)',
                    offsetY: -1
                },
                {
                    prop: 'fullscreenButton',
                    text: '⛶',
                    onClick: () => this.toggleFullscreen(),
                    altText: 'Toggle Fullscreen (F)',
                    offsetY: -1
                },
                {
                    prop: 'downloadButton',
                    text: '⤓',
                    onClick: () => this.downloadCurrentImage(),
                    altText: 'Download Original Image (Shift+S)',
                    offsetY: 0
                },
                {
                    prop: 'findGalleriesButton',
                    text: '∈',
                    onClick: () => this.showGalleriesWithCurrentImage(),
                    altText: 'Find other galleries with this image (Shift+F)',
                    offsetY: -1
                },
                {
                    prop: 'gotoPageButton',
                    text: '⌕',
                    onClick: () => showGotoPageInput(),
                    altText: 'Goto Page (G)',
                    offsetY: -1,
                    offsetX: 1,
                },
                {
                    prop: 'chaptersButton',
                    text: '☰',
                    onClick: () => { this.toggleChapterSidebar(); },
                    altText: "Toggle Chapters (c)",
                    offsetY: -1,
                    condition: () => typeof chapterList !== 'undefined' && chapterList?.length,
                },
                {
                    prop: 'settingsButton',
                    text: '⚙',
                    onClick: () => {
                        if (this.config.showingUI()) {
                            this.config.closeUI();
                        } else {
                            this.config.showUI();
                        }
                    },
                    altText: "Edit Preferences (p)",
                    offsetY: -1,
                    offsetX: 1
                },
                {
                    prop: 'helpButton',
                    text: '?',
                    onClick: () => { this.toggleHelpOverlay(); },
                    altText: "Show Help (h)",
                    offsetY: 0
                }
            ];
        }

        _updateToolbarButtons() {
            this.buttonDefinitions.forEach(def => {
                if (def.condition === undefined || def.condition()) {
                    this[def.prop] = this._createButton(def.text, def.onClick, def.altText, def.offsetY, def.offsetX);
                }
            });

            const mode = this.config.helpAndSettingsButtons;

            if (mode === 'disabled') {
                this.buttonContainer.style.display = 'none';
                return;
            }

            this.buttonContainer.style.display = 'flex';

            if (mode === 'always') {
                this.buttonContainer.style.opacity = '1';
                this.buttonContainer.style.pointerEvents = 'auto';
            } else if (mode === 'proximity') {
                // Set initial state for proximity; mousemove handler will take over
                this.buttonContainer.style.opacity = '0';
                this.buttonContainer.style.pointerEvents = 'none';
            }

            // Clear container
            while (this.buttonContainer.firstChild) {
                this.buttonContainer.removeChild(this.buttonContainer.firstChild);
            }

            const buttonOrder = [
                { config: 'showHelpButton', button: this.helpButton },
                { config: 'showSettingsButton', button: this.settingsButton },
                { config: 'showChaptersButton', button: this.chaptersButton },
                { config: 'showGalleryViewButton', button: this.galleryViewButton },
                { config: 'showDualPageButton', button: this.dualPageButton },
                { config: 'showDownloadButton', button: this.downloadButton },
                { config: 'showFindGalleriesButton', button: this.findGalleriesButton },
                { config: 'showGotoPageButton', button: this.gotoPageButton },

                { config: 'showRotateButtons', button: this.rotateLeftButton },
                { config: 'showRotateButtons', button: this.rotateRightButton },
                { config: 'showZoomButtons', button: this.zoomOutButton },
                { config: 'showZoomButtons', button: this.zoomInButton },
                { config: 'showNavButtons', button: this.prevButton },
                { config: 'showNavButtons', button: this.nextButton },

                { config: 'showFullscreenButton', button: this.fullscreenButton },
                { config: 'showExitButton', button: this.exitButton },
            ];

            buttonOrder.forEach(item => {
                if (this.config[item.config] && item.button) {
                    this.buttonContainer.appendChild(item.button);
                }
            });
        }

        _initializeEventListeners() {
            this.parent.addEventListener("mousemove", (e) => {
                this._handleToolbarProximity(e);
                this._handlePageLabelProximity(e);
            });

            window.addEventListener("resize", () => {
                if (this.isActive()) this.updateTransforms();
            });
            document.addEventListener("fullscreenchange", () => {
                if (this.useFullscreen && !isFullscreen() && this.isActive()) {
                    this.closeViewer(this.config.exitToViewerPage);
                }
            });

            this._onSaveUnsubscribe = this.config.onSaveClick(() => this._handleConfigSave());
        }

        _handleToolbarProximity(e) {
            if (this.config.helpAndSettingsButtons !== 'proximity' || !this.isActive()) {
                return;
            }

            const proximityThreshold = 120; // pixels from the top
            if (e.clientY < proximityThreshold) {
                this.buttonContainer.style.opacity = '1';
                this.buttonContainer.style.pointerEvents = 'auto';
            } else {
                this.buttonContainer.style.opacity = '0';
                this.buttonContainer.style.pointerEvents = 'none';
            }
        }

        _handlePageLabelProximity(e) {
            if (this.config.viewerLabels !== 'proximity' || !this.isActive() || !this.pageNumberLabel) {
                return;
            }

            const verticalProximity = 120; // pixels from the bottom edge
            const horizontalProximity = this.pageNumberLabel.offsetWidth + 120;

            if (e.clientX > window.innerWidth - horizontalProximity && e.clientY > window.innerHeight - verticalProximity) {
                this.pageNumberLabel.style.opacity = '1';
            } else {
                this.pageNumberLabel.style.opacity = '0';
            }
        }

        // Applies new config values
        // Currently does not fully update the UI
        _handleConfigSave() {
            this.fitMode = this.config.fitMode;
            this.pinSidebar = this.config.pinSidebar;
            this.useFullscreen = this.config.useFullscreen;
            this.rightToLeftMode = this.config.openInRightToLeftMode;
            this.dualPageMode = this.config.openInDualPageMode;
            this.dualLayout = this.config.dualLayout;
            this.autoplayEnabled = this.config.videoConfig.autoplay;
            this.loopEnabled = this.config.videoConfig.loop;
            this.currentZoom = 1;

            this._updateToolbarButtons();

            // Handle page number label update
            if (this.pageNumberLabel) {
                this.pageNumberLabel.remove();
                this.pageNumberLabel = null;
            }
            if (this.config.viewerLabels !== 'disabled') {
                this.pageNumberLabel = document.createElement('span');
                Object.assign(this.pageNumberLabel.style, {
                    position: 'fixed',
                    bottom: '10px',
                    right: '20px',
                    backgroundColor: `rgba(0, 0, 0, ${this.UI_TRANSPARENCY})`,
                    color: 'white',
                    padding: '6px',
                    paddingTop: '4px',
                    fontSize: '18px',
                    borderRadius: '6px',
                    fontWeight: 'bold',
                    zIndex: '10000',
                    pointerEvents: 'none',
                    transition: 'opacity 0.3s ease',
                });
                if (this.config.viewerLabels === 'proximity') {
                    this.pageNumberLabel.style.opacity = '0';
                }
                this.parent.appendChild(this.pageNumberLabel);
            }

            if (!this.config.enableSidebar && this.sidebarIsActive()) {
                this.sidebarHideInstant();
            }

            if (this.isActive()) {
                this.loadAndShowIndex(this.currentIndex);
            }
        }

        _createButton(text, onClick, altText = "", offsetY = 0, offsetX = 0) {
            const button = document.createElement('button');
            button.title = altText || text; // Fallback to button text if no altText provided
            Object.assign(button.style, {
                width: this.config.buttonSize + 'px',
                height: this.config.buttonSize + 'px',
                borderRadius: '50%',
                border: '1px solid rgba(255,255,255,0.2)',
                background: `rgba(0,0,0,${this.UI_TRANSPARENCY})`,
                color: 'white',
                cursor: 'pointer',
                fontSize: '20px',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                position: 'relative'
            });

            // Create a span for the text content
            const textSpan = document.createElement('span');
            textSpan.textContent = text;
            textSpan.style.display = 'block';
            if (offsetX !== 0) {
                textSpan.style.transform += `translateX(${offsetX}px) `;
            }
            if (offsetY !== 0) {
                textSpan.style.transform += `translateY(${offsetY}px)`;
            }
            button.appendChild(textSpan);

            button.addEventListener('click', onClick);

            // Add hover feedback
            button.addEventListener('mouseenter', () => {
                button.style.background = 'rgba(255, 255, 255, 0.9)';
                button.style.color = 'black';
            });
            button.addEventListener('mouseleave', () => {
                button.style.background = `rgba(0,0,0,${this.UI_TRANSPARENCY})`;
                button.style.color = 'white';
            });

            // Add button to tracked buttons array
            if (!this._trackedButtons) {
                this._trackedButtons = [];
            }
            this._trackedButtons.push(button);

            return button;
        }

        _addClickAwayEventListener(element, activeCheckFunc, onClickAway, removeOnClickAway = true) {
            const clickAwayHandler = (e) => {
                if (activeCheckFunc() && !element.contains(e.target)) {
                    const isTrackedButton = this._trackedButtons?.some(button =>
                        button.contains(e.target)
                    );
                    if (!isTrackedButton) {
                        onClickAway();
                        if (removeOnClickAway) {
                            document.removeEventListener('click', clickAwayHandler);
                        }
                    }
                }
            };
            document.addEventListener('click', clickAwayHandler);

            // Return cleanup function
            return () => {
                document.removeEventListener('click', clickAwayHandler);
            };
        }

        isHelpOverlayVisible() {
            return this.helpOverlay.style.display === 'flex';
        }

        toggleHelpOverlay() {
            const isVisible = this.isHelpOverlayVisible();
            this.helpOverlay.style.display = isVisible ? 'none' : 'flex';

            if (!isVisible) {
                // Add click away listener when showing
                this._helpOverlayClickAwayCleanup = this._addClickAwayEventListener(
                    this.helpBox,
                    () => this.isHelpOverlayVisible(),
                    () => this.toggleHelpOverlay()
                );
            } else if (this._helpOverlayClickAwayCleanup) {
                this._helpOverlayClickAwayCleanup();
                this._helpOverlayClickAwayCleanup = null;
            }
        }

        _initializeHelpOverlay() {
            // Create overlay element with styles
            this.helpOverlay = document.createElement("div");
            this.helpOverlay.id = 'HelpOverlay';
            Object.assign(this.helpOverlay.style, {
                position: "fixed",
                top: "0px", // Use strings for CSS values
                left: "0px",
                width: "100%",
                height: "100%",
                backgroundColor: "rgba(0, 0, 0, 0.8)",
                display: "none", // Initially hidden
                alignItems: "center",
                justifyContent: "center",
                zIndex: "2147483647", // Max z-index is usually fine
                padding: "14px",
                boxSizing: "border-box"
            });

            // Create help box element - This is now a Flex Container
            this.helpBox = document.createElement("div");
            Object.assign(this.helpBox.style, {
                backgroundColor: "rgb(34, 34, 34)", // Use rgb() format or hex #222
                color: "rgb(241, 241, 241)", // Use rgb() format or hex #f1f1f1
                borderRadius: "8px",
                // Padding moved to inner elements or adjusted later if needed
                // padding: "12px", // Remove outer padding if header/content manage their own
                maxWidth: "600px",
                width: "100%",
                maxHeight: "100%", // Constrains the overall box size
                // overflowY: "auto", // REMOVE - Scrolling handled by inner div
                boxShadow: "0 6px 12px rgba(0, 0, 0, 0.5)",
                fontFamily: "Arial, sans-serif",
                lineHeight: "1.6",
                fontSize: "13px",
                // position: "relative", // Not strictly needed if not using absolute inside
                display: "flex",    // Use Flexbox
                flexDirection: "column", // Stack children vertically
                overflow: "hidden"   // Hide any potential overflow from children before scroll kicks in
            });

            // --- Create Header Div ---
            const headerDiv = document.createElement("div");
            Object.assign(headerDiv.style, {
                display: "flex",
                justifyContent: "space-between", // Pushes title left, button right
                alignItems: "center",
                padding: "10px 12px", // Vertical and horizontal padding for the header
                borderBottom: "1px solid #444", // Separator line
                flexShrink: "0" // Prevent header from shrinking if content is large
            });

            // Create Title
            const title = document.createElement("strong"); // Use <strong> or h2/h3
            title.textContent = "Shortcuts";
            Object.assign(title.style, {
                fontSize: "1.1em", // Slightly larger than base font
                fontWeight: "bold"
            });

            // Create Close Button (remove position: absolute)
            const closeButton = document.createElement("button");
            closeButton.textContent = "×";
            Object.assign(closeButton.style, {
                // position: "absolute", // REMOVED
                // top: "8px",     // REMOVED
                // right: "8px",     // REMOVED
                background: "transparent",
                border: "none",
                color: "rgb(241, 241, 241)",
                fontSize: "24px",
                cursor: "pointer",
                padding: "0 4px", // Adjust padding slightly if needed
                lineHeight: "1", // Ensure it aligns nicely vertically
                marginLeft: "10px" // Add some space between title and button
            });
            closeButton.addEventListener("click", () => {
                this.toggleHelpOverlay(); // Assuming this function exists to hide the overlay
            });

            // Add title and button to header
            headerDiv.appendChild(title);
            headerDiv.appendChild(closeButton);

            // --- Create Table Wrapper for Scrolling ---
            const tableWrapper = document.createElement("div");
            Object.assign(tableWrapper.style, {
                overflowY: "auto",   // Enable vertical scrolling ONLY for this div
                flexGrow: "1",     // Allow this div to take up remaining vertical space
                minHeight: "0",    // Crucial for flex-grow + overflow to work correctly
                padding: "0 12px 12px 12px" // Add padding around the table (top padding from header)
            });


            const shortcuts = [
                { key: "Esc/Q", action: "Exit viewer (Shift+Q: exit to current page)" },
                { key: "Right/D/./Space", action: "Next Page (Shift+key: skip panning)" },
                { key: "Left/A/,", action: "Previous Page (Shift+key: skip panning)" },
                { key: "Shift+PageDown", action: "Next Chapter" },
                { key: "Shift+PageUp", action: "Previous Chapter" },
                { key: "Home", action: "First Page" },
                { key: "End", action: "Last Page" },
                { key: "+ / =", action: "Zoom In" },
                { key: "- / _", action: "Zoom Out (Min: Fit screen)" },
                { key: "R", action: "Rotate all 90°" },
                { key: "L", action: "Rotate all -90°" },
                { key: "S", action: "Toggle sidebar pin" },
                { key: "C", action: "Show/hide chapter list" },
                { key: "G", action: "Go to page (available everywhere)" },
                { key: "Backspace", action: "Show/Hide gallery view" },
                { key: "Shift+D", action: "Toggle dual page mode" },
                { key: "Shift+A", action: "Enable dual page mode or cycle layout" },
                { key: "Shift+R", action: "Toggle right to left mode" },
                { key: "F", action: "Toggle fullscreen" },
                { key: "Delete", action: "Remove image" },
                { key: "Ctrl+C", action: "Copy image url" },
                { key: "Ctrl+V", action: "Replace image with clipboard image or url" },
                { key: "Shift+S", action: "Save original image" },
                { key: "P", action: "Open preferences (available everywhere)" },
                { key: "1", action: "Reset zoom to fit screen" },
                { key: "2", action: "Set 1:1 pixel zoom" },
                { key: "3", action: "Zoom to fill view width" },
                { key: "H or ?", action: "Show/Hide this help overlay" },
            ];

            // Build table inner HTML using a loop
            let rows = "";
            shortcuts.forEach((item, index) => {
                // Remove bottom border from the very last row for cleaner look
                const borderStyle = index === shortcuts.length - 1 ? "" : "border-bottom: 1px solid #333;";
                rows += `<tr style="${borderStyle}">
        <td style="padding: 8px; text-align: right; width: 30%; vertical-align: top; font-weight: bold;"><code>${item.key}</code></td>
        <td style="padding: 8px; text-align: left; vertical-align: top;">${item.action}</td>
      </tr>`;
            });

            // Use innerHTML on a new table element
            const table = document.createElement('table');
            Object.assign(table.style, {
                width: "100%",
                borderCollapse: "collapse"
                // Removed overflow and height styles
            });
            table.innerHTML = `<tbody>${rows}</tbody>`;

            // Add the table to the scrolling wrapper
            tableWrapper.appendChild(table);

            // --- Assemble the helpBox ---
            this.helpBox.appendChild(headerDiv);   // Add fixed header first
            this.helpBox.appendChild(tableWrapper);  // Add scrollable content below

            // Add helpBox to the main overlay
            this.helpOverlay.appendChild(this.helpBox);

            // Append overlay to the parent element (ensure 'this.parent' is defined correctly)
            if (this.parent) {
                this.parent.appendChild(this.helpOverlay);
            } else {
                console.error("Parent element for HelpOverlay not found!");
                // Fallback to body, though might not be ideal depending on context
                document.body.appendChild(this.helpOverlay);
            }

            // Add event listener to close overlay when clicking the background
            this.helpOverlay.addEventListener('click', (event) => {
                if (event.target === this.helpOverlay) { // Only close if backdrop itself is clicked
                    this.toggleHelpOverlay();
                }
            });
        }

        _initializeSidebar() {
            this._createSidebarParent();
            this.sidebar = new GridView(this.sidebarParent, this.config.sidebarGridConfig, false);

            document.addEventListener('mousemove', async (e) => {
                this._sidebarShowOrHide(e);
            });
        }

        _createSidebarParent() {
            this.sidebarParent = document.createElement('div');
            let posCss = "";

            if (this.config.sidebarPosition === "right") {
                this.sidebarHiddenTransform = "translateX(100%)";
                posCss = `
      top: 0;
      right: 0;
      width: ${this.config.sidebarWidth}px;
      height: 100%;
      overflow-y: auto;
      box-shadow: -2px 0 5px rgba(0,0,0,0.2);
    `;
            } else if (this.config.sidebarPosition === "left") {
                this.sidebarHiddenTransform = "translateX(-100%)";
                posCss = `
      top: 0;
      left: 0;
      width: ${this.config.sidebarWidth}px;
      height: 100%;
      overflow-y: auto;
      box-shadow: 2px 0 5px rgba(0,0,0,0.2);
    `;
            }

            this.sidebarTransition = "transform 0.3s";
            const baseCss = `
    position: fixed;
    background: rgb(24,24,24);
    transform: ${this.sidebarHiddenTransform};
    transition: ${this.sidebarTransition};
    z-index: 999999;
    `;
            // Apply the combined CSS
            this.sidebarParent.style.cssText = baseCss + posCss;

            if (config.sidebarPosition === "top" || config.sidebarPosition === "bottom") {
                this.sidebarVisibleTransform = "translateY(0)";
            } else {
                this.sidebarVisibleTransform = "translateX(0)";
            }

            // Append to parent, not viewerContainer
            this.parent.appendChild(this.sidebarParent);
        }

        sidebarIsActive() {
            return this.sidebarVisible;
        }

        sidebarHideInstant() {
            this.sidebarParent.style.transition = "none";
            this.sidebarParent.style.transform = this.sidebarHiddenTransform;
            this.sidebarVisible = false;
            this.sidebar.stopLoading();
            setTimeout(() => {
                this.sidebarParent.style.transition = this.sidebarTransition;
            }, 0);
        }

        _sidebarShowOrHide(e = null) {
            // Only expand sidebar if the viewer is active AND grid view is not shown
            if (!this.config.enableSidebar || !this.isActive() || this.isGridViewActive()) {
                // Ensure sidebar is hidden if grid view is shown or sidebar is disabled
                if (this.isGridViewActive() || !this.config.enableSidebar) {
                    this.sidebarParent.style.transform = this.sidebarHiddenTransform;
                    this.sidebarVisible = false;
                    this.sidebar.stopLoading();
                }
                return;
            }


            // Determine if the sidebar was hidden based on its transform value.
            const sidebarWasHidden = this.sidebarParent.style.transform === this.sidebarHiddenTransform;
            this.sidebarVisible = false;

            if (e) {
                let nearEdge = false;
                let overSidebar = false;

                if (config.sidebarPosition === "left") {
                    overSidebar = e.clientX <= config.sidebarWidth && !sidebarWasHidden;
                    nearEdge = e.clientX < 50;
                } else if (config.sidebarPosition === "right") {
                    overSidebar = e.clientX >= (window.innerWidth - config.sidebarWidth) && !sidebarWasHidden;
                    nearEdge = e.clientX > (window.innerWidth - 50);
                } else if (config.sidebarPosition === "top") {
                    overSidebar = e.clientY <= config.sidebarWidth && !sidebarWasHidden;
                    nearEdge = e.clientY < 50;
                } else if (config.sidebarPosition === "bottom") {
                    overSidebar = e.clientY >= (window.innerHeight - config.sidebarWidth) && !sidebarWasHidden;
                    nearEdge = e.clientY > (window.innerHeight - 50);
                }
                this.sidebarVisible = nearEdge || overSidebar;
            }

            // config.pinSidebar can force the sidebar visible.
            this.sidebarVisible |= this.pinSidebar;

            if (this.sidebarVisible) {
                this.sidebarParent.style.transform = this.sidebarVisibleTransform;
                if (!this.sidebar.showCalled) {
                    this.sidebar.showGridView();
                } else {
                    this.sidebar.enableLoading();
                }
                if (sidebarWasHidden) {
                    this.sidebar.scrollToIndex(this.currentIndex, false);
                }
            } else {
                this.sidebarParent.style.transform = this.sidebarHiddenTransform;
                this.sidebar.stopLoading();
            }
        }

        toggleChapterSidebar() {
            if (!chapterList || chapterList.length === 0) {
                createNotification("No chapters available");
                return;
            }

            if (!this.chapterSidebar) {
                this._createChapterSidebar();
            }

            const isVisible = this.chapterSidebarIsActive();
            this.chapterSidebar.style.display = isVisible ? 'none' : 'block';

            if (!isVisible) {
                this._chapterSidebarClickAwayCleanup = this._addClickAwayEventListener(
                    this.chapterSidebar,
                    () => this.chapterSidebarIsActive(),
                    () => this.toggleChapterSidebar()
                );
            } else if (this._chapterSidebarClickAwayCleanup) {
                this._chapterSidebarClickAwayCleanup();
                this._chapterSidebarClickAwayCleanup = null;
            }
        }

        chapterSidebarIsActive() {
            return this.chapterSidebar && this.chapterSidebar.style.display === 'block';
        }

        _createChapterSidebar() {
            // Create sidebar container
            this.chapterSidebar = document.createElement('div');
            Object.assign(this.chapterSidebar.style, {
                position: 'fixed',
                top: '0',
                left: '0',
                width: `${this.config.sidebarWidth}px`,
                height: '100vh',
                backgroundColor: 'rgb(24,24,24)',
                color: '#fff',
                overflowY: 'auto',
                padding: '20px',
                boxSizing: 'border-box',
                zIndex: '9999999',
                boxShadow: '2px 0 5px rgba(0,0,0,0.2)',
                display: 'none'
            });

            // Create title
            const title = document.createElement('h3');
            title.textContent = 'Chapters';
            title.style.margin = '0 0 20px 0';
            title.style.fontSize = '18px';
            title.style.fontWeight = 'bold';
            title.style.textAlign = 'center';
            this.chapterSidebar.appendChild(title);

            // Create a <style> element for our table CSS
            const style = document.createElement('style');
            style.textContent = `
      /* Table for chapter sidebar */
      .chapter-table {
        width: 100%;
        border-collapse: collapse;
      }
      .chapter-table tr {
        border-bottom: 1px solid #333;
        cursor: pointer;
      }
      .chapter-table td {
        padding: 8px;
        vertical-align: top;
      }
      .chapter-number {
        text-align: right;
        font-size: 13px;
        font-weight: bold;
        color: #f1f1f1;
        white-space: nowrap;
        width: 1%; /* Let the browser allocate enough width for the content */
      }
      .chapter-desc {
        text-align: left;
        font-size: 13px;
        color: #aaa;
        word-break: break-word;
      }
      .chapter-table tr:hover .chapter-desc {
        color: #fff;
      }
    `;
            this.chapterSidebar.appendChild(style);

            // Create table container
            const table = document.createElement('table');
            table.className = 'chapter-table';

            chapterList.forEach((chapter) => {
                const tr = document.createElement('tr');

                // Add click event so the whole row is clickable
                tr.addEventListener('click', () => {
                    this.loadAndShowIndex(chapter.index);
                });

                // Create chapter number cell
                const tdNumber = document.createElement('td');
                tdNumber.className = 'chapter-number';
                tdNumber.textContent = chapter.index + 1;

                // Create description cell
                const tdDesc = document.createElement('td');
                tdDesc.className = 'chapter-desc';

                // Create description text, remove any duplicate chapter number at the start
                const first = (chapter.linkText || "").trim();
                const second = (chapter.description || "").trim();
                const sep = first.endsWith('.') && second.startsWith('.') ? "" : " ";
                let text = (first + sep + second).trim();

                // Remove a potential leading chapter number (with optional leading zeroes)
                const pageNumber = (chapter.index + 1).toString();
                const numberPattern = new RegExp(`^(?:P)?0*${pageNumber}`);
                if (numberPattern.test(text)) {
                    text = text.replace(numberPattern, '').trim();
                    // Remove a following punctuation or whitespace if present:
                    if (text.startsWith('.') || text.startsWith('-') || text.startsWith(':') ||
                        text.startsWith('•') || text.startsWith('■') || text.startsWith('・')) {
                        text = text.slice(1).trim();
                    }
                }

                // Find longest section of dots (2 or more)
                text = text.replaceAll('…', '...').replaceAll(' .', '.');
                const dotSections = text.match(/\.{2,}|-{2,}/g) || [];
                const longestDotSection = dotSections.reduce((longest, dots) =>
                    dots.length > longest.length ? dots : longest, '');

                if (longestDotSection.length >= 2) {
                    // Split text by the longest dot section
                    const [firstPart, secondPart] = text.split(longestDotSection);
                    tdDesc.innerHTML = `${firstPart.trim()}<br>${secondPart.trim()}`;
                } else {
                    tdDesc.textContent = text;
                }

                tr.appendChild(tdNumber);
                tr.appendChild(tdDesc);
                table.appendChild(tr);
            });

            this.chapterSidebar.appendChild(table);
            this.parent.appendChild(this.chapterSidebar);
        }

        /**
         * Determines the primary and secondary indices to display based on the target index and dual page mode setting.
         * @param {number} targetIndex - The index the user intends to navigate to or view.
         * @returns {{primary: number | null, secondary: number | null}} - The indices to display. Returns null for an index if it's invalid, out of bounds, or marked as 'deleted'.
         */
        _getDualPageIndices(targetIndex) {
            if (!this.dualPageMode || targetIndex < 0 || targetIndex >= thumbs.length) {
                // Handle single page mode, or invalid target index
                const isValid = targetIndex >= 0 && targetIndex < thumbs.length && thumbs[targetIndex] !== 'deleted';
                return { primary: isValid ? targetIndex : null, secondary: null };
            }

            const layout = this.dualLayout;
            let primary = null;
            let secondary = null;
            const lastIndex = thumbs.length - 1;

            // --- Determine potential primary/secondary based on mode ---
            if (layout === 'selected-first') {
                // Always show targetIndex on left, targetIndex+1 on right if possible.
                primary = targetIndex;
                if (targetIndex < lastIndex) {
                    secondary = targetIndex + 1;
                }
            } else if (layout === 'odd-first') {
                // Left page (primary) must have odd page number (index is even)
                if (targetIndex % 2 === 0) { // Target is even (Page is odd) -> Target is primary
                    primary = targetIndex;
                    if (targetIndex < lastIndex) secondary = targetIndex + 1;
                } else { // Target is odd (Page is even) -> Previous index is primary
                    primary = targetIndex - 1;
                    secondary = targetIndex;
                }
            } else if (layout === 'even-first') {
                // Left page (primary) must have even page number (index is odd)
                if (targetIndex % 2 !== 0) { // Target is odd (Page is even) -> Target is primary
                    primary = targetIndex;
                    if (targetIndex < lastIndex) secondary = targetIndex + 1;
                } else { // Target is even (Page is odd) -> Previous index is primary (if possible)
                    if (targetIndex > 0) {
                        primary = targetIndex - 1;
                        secondary = targetIndex;
                    } else { // Target is 0 (Page 1) -> Show only page 1
                        primary = 0;
                        secondary = null;
                    }
                }
            }

            // --- Validate and Clean Up Indices ---
            const isValid = (idx) => idx !== null && idx >= 0 && idx < thumbs.length && thumbs[idx] !== 'deleted';

            if (!isValid(primary)) primary = null;
            if (!isValid(secondary)) secondary = null;

            // If primary became invalid, but secondary is valid, maybe promote secondary?
            // Or maybe the target was invalid to begin with. Let's keep it simple:
            // If the intended primary (based on logic) is invalid, it's null.
            if (primary === null && secondary !== null && this.dualPageMode) {
                // If aiming for dual page, but primary is invalid, show secondary as single?
                // This edge case depends on desired behavior. Showing only secondary might be confusing.
                // Let's return null primary, valid secondary for now. loadAndShowIndex will handle it.
                // console.warn(`_getDualPageIndices: Primary index ${primary} invalid, secondary ${secondary} valid.`);
            }

            // Ensure primary and secondary are not the same
            if (primary === secondary) secondary = null;

            // Interchange the indices in right-to-left mode *before* final promotion.
            if (this.rightToLeftMode) {
                [primary, secondary] = [secondary, primary];
            }

            // Final check: If we ended up with only a secondary index in dual mode, treat as single page secondary.
            // This can happen if the pairing logic results in an invalid primary index (e.g., at the start of a book).
            // It's also critical for RTL mode, where swapping might leave the primary slot empty.
            // Enforce that the primary slot is filled if at least one page is available.
            if (primary === null && secondary !== null) {
                primary = secondary;
                secondary = null;
            }

            return { primary, secondary };
        }

        /**
         * Creates and displays an error message div for a specific slot.
         * Removes any existing error message for that slot first.
         * @param {'primary' | 'secondary'} slot - The slot ('primary' or 'secondary').
         * @param {string} messageHtml - The HTML content for the error message.
         */
        _displayErrorMessage(slot, messageHtml) {
            // 1. Remove any existing error message specifically for this slot
            const existingErrorMsg = this.parent.querySelector(`.viewer-error-message[data-slot="${slot}"]`);
            if (existingErrorMsg) {
                existingErrorMsg.remove();
            }

            // 2. Create the new error message div
            const errorDiv = document.createElement('div');
            errorDiv.className = 'viewer-error-message'; // Apply base class
            errorDiv.setAttribute('data-slot', slot);
            errorDiv.innerHTML = messageHtml;

            // 3. Apply positioning styles based on mode
            if (this.dualPageMode) {
                errorDiv.style.maxWidth = '300px';
                errorDiv.style.transform = 'translateY(-50%)';
                errorDiv.style.left = (slot === 'primary') ? '0' : 'auto';
                errorDiv.style.right = (slot === 'primary') ? 'auto' : '0';
            } else {
                // Non-dual mode: center it
                errorDiv.style.maxWidth = '90%'; // Default max-width
                errorDiv.style.transform = 'translate(-50%, -50%)';
                errorDiv.style.left = '50%';
                errorDiv.style.right = 'auto'; // Important to reset right
            }

            // 4. Append the newly created and styled div
            this.parent.appendChild(errorDiv);
        }

        /**
         * Core display function. Sets sources, handles visibility of img/video elements,
         * waits for media load, and updates transforms. Handles null URLs by displaying an error state.
         * Attempts to load a fallback URL if the primary URL fails and `config.useFallbackImages` is true.
         * @param {object} urlObj - Object with primary and optional secondary URLs and their fallbacks.
         *            e.g., { primary: 'url1', primaryFallback: 'fallback1', secondary: 'url2', secondaryFallback: 'fallback2' }
         *            If urlObj.secondary is undefined, the secondary display is hidden.
         *            If a primary URL (e.g., urlObj.primary) is null, an error state is shown or fallback is attempted.
         * @param {object} isVideoObj - Object indicating if primary/secondary are videos. e.g., { primary: false, secondary: true }
         */
        async _show(urlObj, isVideoObj) {
            if (!this.isActive() && this.parent.style.display !== "block") {
                console.warn("_show called while viewer inactive, aborting.");
                return;
            }

            const mediaSetups = [];
            const ERROR_CLASS = 'viewer-error-state';

            const removeDim = (element) => {
                if (element && element.style.opacity !== '' && parseFloat(element.style.opacity) < 1) {
                    element.style.transition = 'none';
                    element.style.opacity = 1;
                    requestAnimationFrame(() => {
                        if (element.style.opacity === '1') {
                            element.style.removeProperty('opacity');
                            element.style.removeProperty('transition');
                        }
                    });
                } else if (element) {
                    element.style.removeProperty('opacity');
                    element.style.removeProperty('transition');
                }
            };

            // Helper to attempt loading a single URL (either primary or fallback)
            const attemptLoadMediaInternal = async (urlToLoad, isVideo, imgElement, videoElement, slotName, attemptType /* 'initial' or 'fallback' */) => {
                console.log(`_show: Attempting to load ${attemptType} URL for ${slotName}: ${urlToLoad}`);
                if (urlToLoad === null) { // Should not happen if called correctly, but safety.
                    return Promise.reject(new Error(`URL for ${slotName} (${attemptType}) is null.`));
                }

                let loadPromise;
                if (isVideo) {
                    imgElement.style.display = "none";
                    if (imgElement.src) imgElement.removeAttribute('src');
                    videoElement.referrerPolicy = "no-referrer";
                    this._revokeBlobUrl(videoElement);
                    if (videoElement.currentSrc !== urlToLoad) videoElement.src = urlToLoad;
                    videoElement.style.display = "block";
                    loadPromise = waitForMediaLoad(videoElement)
                        .catch(async (initialError) => {
                            const mediaErrorCode = videoElement.error?.code;
                            // This 'useFetchFallback' is for GM_xmlhttpRequest for videos, separate from image fallback.
                            if ((mediaErrorCode === 2 || mediaErrorCode === 4) && this.config.useFetchFallback) {
                                console.log(`Direct video load failed (Code: ${mediaErrorCode}). Attempting GM_xmlhttpRequest fallback for ${slotName} ${attemptType} URL: ${urlToLoad}`);
                                try {
                                    const fetchResult = await this.getCachedMediaBlob(urlToLoad);
                                    const blobUrl = fetchResult.blobUrl;
                                    this._revokeBlobUrl(videoElement);
                                    videoElement._currentBlobUrl = blobUrl;
                                    videoElement.src = blobUrl;
                                    return waitForMediaLoad(videoElement);
                                } catch (fetchError) {
                                    console.error(`GM_xmlhttpRequest fallback failed for ${slotName} ${attemptType} URL (${urlToLoad}):`, fetchError);
                                    throw initialError; // Propagate original error if GM_XHR fallback fails
                                }
                            }
                            throw initialError; // Propagate original error if not eligible for GM_XHR fallback
                        });
                } else { // Handle Image
                    videoElement.style.display = "none";
                    if (videoElement.src) videoElement.removeAttribute('src');
                    imgElement.referrerPolicy = "no-referrer";
                    if (imgElement.src !== urlToLoad) imgElement.src = urlToLoad;
                    imgElement.style.display = "block";
                    loadPromise = waitForMediaLoad(imgElement);
                }
                return loadPromise;
            };

            // --- Helper to manage element visibility and src, now with fallback logic ---
            const setupMediaElement = (imgElement, videoElement, initialAttemptUrl, fallbackAttemptUrl, isVideo, isPrimary) => {
                const slot = isPrimary ? 'primary' : 'secondary';

                const existingErrorMsg = this.parent.querySelector(`.viewer-error-message[data-slot="${slot}"]`);
                if (existingErrorMsg) existingErrorMsg.remove();
                imgElement.classList.remove(ERROR_CLASS);
                imgElement.style.backgroundColor = ''; imgElement.style.border = '';
                if (!videoElement.paused) { videoElement.loop = false; videoElement.pause(); videoElement.currentTime = 0; }

                let loadSequencePromise = (() => {
                    if (initialAttemptUrl === null) {
                        if (this.config.useFallbackImages && fallbackAttemptUrl) {
                            // console.log(`Initial URL for ${slot} is null. Attempting fallback: ${fallbackAttemptUrl}`);
                            return attemptLoadMediaInternal(fallbackAttemptUrl, isVideo, imgElement, videoElement, slot, 'fallback (due to null initial)')
                                .catch(errorFromFallback => {
                                    throw { type: 'load_failure', primaryError: new Error('Initial URL was null.'), fallbackError: errorFromFallback, initialUrl: null, fallbackUrl: fallbackAttemptUrl };
                                });
                        } else {
                            // No fallback or fallback not viable for an initially null URL
                            // console.log(`Initial URL for ${slot} is null, and no fallback specified or usable.`);
                            return Promise.reject({ type: 'explicit_null', url: null });
                        }
                    }
                    // Initial URL is not null, attempt to load it
                    return attemptLoadMediaInternal(initialAttemptUrl, isVideo, imgElement, videoElement, slot, 'initial')
                        .catch(errorFromPrimary => {
                            if (this.config.useFallbackImages && fallbackAttemptUrl && fallbackAttemptUrl !== initialAttemptUrl) {
                                console.warn(`Media load for ${slot} (${initialAttemptUrl}) failed. Attempting fallback: ${fallbackAttemptUrl}`);
                                return attemptLoadMediaInternal(fallbackAttemptUrl, isVideo, imgElement, videoElement, slot, 'fallback')
                                    .catch(errorFromFallback => {
                                        throw { type: 'load_failure', primaryError: errorFromPrimary, fallbackError: errorFromFallback, initialUrl: initialAttemptUrl, fallbackUrl: fallbackAttemptUrl };
                                    });
                            }
                            // No fallback configured, or fallback URL is same/invalid
                            throw { type: 'load_failure', primaryError: errorFromPrimary, initialUrl: initialAttemptUrl, fallbackUrl: null };
                        });
                })();


                const loadCompletionPromise = loadSequencePromise
                    .then(result => ({
                        status: 'fulfilled',
                        value: result,
                        setup: { imgElement, videoElement, isPrimary, initialUrl: initialAttemptUrl, fallbackUrl: fallbackAttemptUrl }
                    }))
                    .catch(errorInfo => ({
                        status: 'rejected',
                        reason: errorInfo, // Contains type, errors, urls
                        setup: { imgElement, videoElement, isPrimary, initialUrl: initialAttemptUrl, fallbackUrl: fallbackAttemptUrl }
                    }))
                    .finally(() => {
                        removeDim(imgElement);
                        removeDim(videoElement);
                    });

                // isErrorState for originalLoadPromise is now less relevant as completionPromise handles the outcome.
                // The key is how 'reason' is structured in case of rejection.
                return { originalLoadPromise: loadSequencePromise, loadCompletionPromise, isErrorState: false /* Deprecate this direct flag */, imgElement, videoElement, isPrimary, url: initialAttemptUrl };
            };

            // --- Setup Primary Media ---
            mediaSetups.push(setupMediaElement(this.imgDisplay, this.videoDisplay, urlObj.primary, urlObj.primaryFallback, isVideoObj.primary, true));

            // --- Setup Secondary Media ---
            const shouldDisplaySecondary = urlObj.secondary !== undefined;
            if (shouldDisplaySecondary) {
                mediaSetups.push(setupMediaElement(this.imgDisplay2, this.videoDisplay2, urlObj.secondary, urlObj.secondaryFallback, isVideoObj.secondary, false));
            } else {
                this.imgDisplay2.style.display = "none";
                this.imgDisplay2.classList.remove(ERROR_CLASS);
                if (this.imgDisplay2.src) this.imgDisplay2.removeAttribute('src');
                this.videoDisplay2.style.display = "none";
                if (!this.videoDisplay2.paused) { this.videoDisplay2.pause(); this.videoDisplay2.currentTime = 0; }
                if (this.videoDisplay2.src) this.videoDisplay2.removeAttribute('src');
                const existingSecondaryErrorMsg = this.parent.querySelector('.viewer-error-message[data-slot="secondary"]');
                if (existingSecondaryErrorMsg) existingSecondaryErrorMsg.remove();
            }

            const completionPromises = mediaSetups.map(setup => setup.loadCompletionPromise);

            try {
                const loadTimeout = 20000;
                const settledResults = await Promise.race([
                    Promise.all(completionPromises),
                    new Promise((_, reject) => setTimeout(() => reject(new Error('Media load process timeout')), loadTimeout))
                ]);

                if (Array.isArray(settledResults)) {
                    settledResults.forEach((result) => {
                        const { status, reason, setup } = result;
                        const { imgElement, videoElement, isPrimary } = setup; // initialUrl/fallbackUrl from setup can be used if needed
                        const slot = isPrimary ? 'primary' : 'secondary';

                        if (status === 'rejected') {
                            const errorDetails = reason; // This is our structured error object

                            videoElement.style.display = "none";
                            if (videoElement.src) videoElement.removeAttribute('src');
                            imgElement.style.display = "block";
                            imgElement.classList.add(ERROR_CLASS);

                            let messageHtml;
                            if (errorDetails.type === 'explicit_null') {
                                if (imgElement.src) imgElement.removeAttribute('src');
                                messageHtml = "Image not available";
                                console.warn(`_show: Explicit null URL for ${slot}, no viable fallback was found or used. URL was: ${errorDetails.url}`);
                            } else if (errorDetails.type === 'load_failure') {
                                const attemptedInitialUrl = errorDetails.initialUrl;
                                const attemptedFallbackUrl = errorDetails.fallbackUrl;

                                const displayInitial = typeof attemptedInitialUrl === 'string' ? attemptedInitialUrl : (attemptedInitialUrl === null ? '[Initial URL was null]' : '[Unknown Initial URL]');
                                const safeInitial = typeof attemptedInitialUrl === 'string' ? attemptedInitialUrl.replace(/"/g, '"').replace(/'/g, "'") : '#';

                                if (errorDetails.fallbackError && attemptedFallbackUrl) {
                                    console.error(`_show: Media load failed for ${slot} (tried initial: ${attemptedInitialUrl}, then fallback: ${attemptedFallbackUrl}):`, errorDetails.primaryError, errorDetails.fallbackError);
                                    const displayFallback = typeof attemptedFallbackUrl === 'string' ? attemptedFallbackUrl : '[Unknown Fallback URL]';
                                    const safeFallback = typeof attemptedFallbackUrl === 'string' ? attemptedFallbackUrl.replace(/"/g, '"').replace(/'/g, "'") : '#';
                                    messageHtml = `Failed to load: <a href="${safeInitial}" target="_blank" rel="noopener noreferrer">${displayInitial}</a>` +
                                        `<br>and fallback: <a href="${safeFallback}" target="_blank" rel="noopener noreferrer">${displayFallback}</a>`;
                                } else {
                                    console.error(`_show: Media load failed for ${slot} (tried initial: ${attemptedInitialUrl}, no fallback attempted or fallback failed silently/wasn't applicable):`, errorDetails.primaryError);
                                    messageHtml = `Failed to load: <a href="${safeInitial}" target="_blank" rel="noopener noreferrer">${displayInitial}</a>`;
                                }
                            } else {
                                console.error(`_show: Unknown error structure in _show for ${slot}:`, errorDetails);
                                messageHtml = "An unexpected error occurred loading media.";
                            }
                            this._displayErrorMessage(slot, messageHtml);
                        } else if (status === 'fulfilled') {
                            if (this.autoplayEnabled && videoElement.style.display === 'block' && videoElement.paused) {
                                videoElement.play().catch(e => console.warn(`Autoplay ${slot} failed:`, e));
                            }
                        }
                    });
                } else {
                    console.error("Media load process timed out.");
                    createNotification("Media timed out loading");
                    mediaSetups.forEach(({ imgElement, videoElement }) => {
                        removeDim(imgElement);
                        removeDim(videoElement);
                    });
                }
                this.updateTransforms();
            } catch (error) {
                console.error("Error during media loading phase (_show):", error);
                createNotification("Error loading media");
                mediaSetups.forEach(({ imgElement, videoElement }) => {
                    removeDim(imgElement);
                    removeDim(videoElement);
                });
                this.updateTransforms();
            } finally {
                clearTimeout(this._dimTimeout);
                this.parent.style.cursor = "default";
            }
        }

        _ensureViewerActive() {
            if (!this.isActive()) {
                console.log("Activating viewer in loadAndShowIndex");
                this.hasEnteredFullscreenOnce = false; // Reset flag when activating
                this.lastScrollPosition = { x: window.scrollX, y: window.scrollY };
                lockPageScroll();
                this.parent.style.display = "block";

                // Initial fullscreen check on activation
                if (this.useFullscreen && !isFullscreen()) {
                    this.enterFullscreen();
                } else if (!this.useFullscreen && isFullscreen()) {
                    this.exitFullscreen();
                }
                // Ensure sidebar state is correct on activation
                if (this.pinSidebar) {
                    this._sidebarShowOrHide(); // Make sure pinned sidebar shows
                } else {
                    this.sidebarHideInstant(); // Ensure non-pinned sidebar is hidden initially
                }
            }
        }

        // Validates index, handles 'deleted', sets up cancellation, pauses existing video
        _prepareLoadOperation(index) {
            // --- Pause Currently Playing Videos ---
            // Do this before validating the new index or aborting previous loads,
            // to ensure the currently visible video stops.
            if (this.videoDisplay.style.display === 'block' && !this.videoDisplay.paused) {
                // console.log("Pausing primary video before navigation.");
                this.videoDisplay.currentTime = 0;
                this.videoDisplay.pause();
            }
            if (this.videoDisplay2.style.display === 'block' && !this.videoDisplay2.paused) {
                // console.log("Pausing secondary video before navigation.");
                this.videoDisplay2.currentTime = 0;
                this.videoDisplay2.pause();
            }

            // --- Validate target index ---
            if (index === null || index < 0 || index >= thumbs.length) { // Added null check
                console.log(`Image index ${index} is out of range [0, ${thumbs.length - 1}]`);
                // Adjust for dual mode start or return null if truly invalid
                if (index === -1 && this.dualPageMode) index = 0;
                else return null;
            }
            // Handle deleted target index
            if (thumbs[index] === 'deleted') {
                console.log(`Image index ${index} is marked as deleted. Trying next available.`);
                let nextAvailable = index + 1;
                while (nextAvailable < thumbs.length && thumbs[nextAvailable] === 'deleted') {
                    nextAvailable++;
                }
                if (nextAvailable < thumbs.length) {
                    index = nextAvailable;
                } else {
                    let prevAvailable = index - 1;
                    while (prevAvailable >= 0 && thumbs[prevAvailable] === 'deleted') {
                        prevAvailable--;
                    }
                    if (prevAvailable >= 0) {
                        index = prevAvailable;
                    } else {
                        console.error("Cannot load index: Target and adjacent indices are deleted or out of bounds.");
                        createNotification("Cannot load image: Not available.");
                        return null; // No valid index found
                    }
                }
            }

            // --- Cancellation of Previous Load ---
            if (this.currentAbortController) {
                console.log("Aborting previous load operation.");
                this.currentAbortController.abort();
                // *** ADDED *** Also revoke blob URLs from the operation being cancelled
                this._revokeBlobUrl(this.videoDisplay);
                this._revokeBlobUrl(this.videoDisplay2);
            }
            const controller = new AbortController();
            this.currentAbortController = controller;
            const loadToken = Symbol();
            this.currentLoadToken = loadToken;

            this.isNavigating = true;
            const navigatedBackwards = this.backwardNavigationCount > 0; // Store direction before updating index
            this.currentIndex = index; // Update the main current index state

            return { controller, loadToken, targetIndex: index, navigatedBackwards };
        }

        // Determines display indices and updates label
        _getDisplayIndicesAndLabel(targetIndex) {
            const { primary: displayPrimaryIndex, secondary: displaySecondaryIndex } = this._getDualPageIndices(targetIndex);

            if (this.config.viewerLabels !== 'disabled' && this.pageNumberLabel) {
                if (displayPrimaryIndex !== null) {
                    let pageText = `${displayPrimaryIndex + 1}`;
                    if (displaySecondaryIndex !== null) {
                        pageText += `-${displaySecondaryIndex + 1}`;
                    }
                    pageText += ` / ${thumbs.length}`
                    this.pageNumberLabel.textContent = pageText;
                } else {
                    this.pageNumberLabel.textContent = "?";
                }
            }
            return { displayPrimaryIndex, displaySecondaryIndex };
        }

        // Sets up the dimming timeout
        _initiateDimming(targetIndex, loadToken, wasActive, gridViewWasActive, displayPrimaryIndex, displaySecondaryIndex) {
            const delayDimming = wasActive && !gridViewWasActive;
            const dimDelay = delayDimming ? 10 : 0; // Short delay only if viewer was already active
            const dimOpacity = 0.4;
            const dimTransition = delayDimming ? 'opacity 0.15s ease-in' : 'none'; // Apply transition only if delaying

            clearTimeout(this._dimTimeout); // Clear any previous dimming timeout

            this._dimTimeout = setTimeout(() => {
                // Check if the load operation associated with this timeout is still the current one
                if (this.currentIndex === targetIndex && this.currentLoadToken === loadToken) {
                    // Set cursor for the parent element
                    this.parent.style.cursor = "progress";

                    // Helper to apply dimming styles
                    const applyDim = (element) => {
                        if (element && element.style.display !== 'none') { // Only dim visible elements
                            element.style.transition = dimTransition;
                            element.style.opacity = dimOpacity;
                        }
                    };

                    // Dim primary slot (image or video) if it exists
                    if (displayPrimaryIndex !== null) {
                        applyDim(this.imgDisplay);
                        applyDim(this.videoDisplay);
                    }

                    // Dim secondary slot (image or video) if it exists and we are in dual page mode
                    if (this.dualPageMode && displaySecondaryIndex !== null) {
                        applyDim(this.imgDisplay2);
                        applyDim(this.videoDisplay2);
                    }
                }
            }, dimDelay);

            return this._dimTimeout; // Return the timeout ID
        }

        // Fetches required image URLs
        async _fetchRequiredUrls(indices, signal, loadToken) {
            const loadPromises = [];
            const uniqueIndices = [...new Set(indices.filter(idx => idx !== null))]; // Get unique, non-null indices

            for (const index of uniqueIndices) {
                // Check cancellation before initiating each load request
                if (this.currentLoadToken !== loadToken || signal.aborted) throw new DOMException('Aborted', 'AbortError');
                loadPromises.push(loadImageUrlAtIndex(index, signal));
            }

            // Wait for all load attempts to settle (resolve or reject)
            // Note: loadImageUrlAtIndex handles its internal errors and should resolve (e.g., with null)
            // rather than rejecting the Promise.all promise unless an unexpected error occurs.
            await Promise.all(loadPromises);

            // Post-await validation (check if token/signal still valid after waiting)
            if (this.currentLoadToken !== loadToken || signal.aborted) {
                throw new DOMException('Aborted', 'AbortError'); // Throw abort error to be caught
            }

            // We no longer validate URLs here. _displayLoadedImages will call _getMediaUrlAtIndex
            // which returns the result (URL or null) of the completed loadImageUrlAtIndex call.
        }

        // Adjusts scroll position before showing images
        _updateScrollPosition(navigatedBackwards) {
            if (this.currentZoom > 1) {
                let targetScrollY = navigatedBackwards ? this.parent.scrollHeight : 0;
                // TODO: Refine scroll anchoring if needed for specific dual modes.
                this.parent.scrollTo({ top: targetScrollY });
            }
        }

        // Displays the loaded images using _show*Page methods
        async _displayLoadedImages(targetIndex, displayPrimaryIndex, displaySecondaryIndex, signal, loadToken) {
            if (this.currentLoadToken !== loadToken || signal.aborted) {
                throw new DOMException('Aborted', 'AbortError');
            }

            // Fetch URL objects (which now include url and fallbackUrl)
            const primaryMediaInfo = this._getMediaUrlAtIndex(displayPrimaryIndex);
            const secondaryMediaInfo = this._getMediaUrlAtIndex(displaySecondaryIndex);

            const primaryItem = (displayPrimaryIndex !== null && displayPrimaryIndex >= 0 && displayPrimaryIndex < thumbs.length) ? thumbs[displayPrimaryIndex] : null;
            const secondaryItem = (displaySecondaryIndex !== null && displaySecondaryIndex >= 0 && displaySecondaryIndex < thumbs.length) ? thumbs[displaySecondaryIndex] : null;

            const primaryIsVideo = primaryItem?.isVideo ?? false;
            const secondaryIsVideo = secondaryItem?.isVideo ?? false;

            // Log failures if primary URL is null but item was expected
            if (displayPrimaryIndex !== null && primaryMediaInfo.url === null && primaryItem && primaryItem !== 'deleted') {
                console.warn(`Primary media (index ${displayPrimaryIndex}) has no valid URL to attempt.`);
            }

            const showSecondary = this.dualPageMode && displaySecondaryIndex !== null && secondaryItem !== 'deleted';
            if (showSecondary && displaySecondaryIndex !== null && secondaryMediaInfo.url === null && secondaryItem && secondaryItem !== 'deleted') {
                console.warn(`Secondary media (index ${displaySecondaryIndex}) has no valid URL to attempt for dual page mode.`);
            }

            await this._show(
                {
                    primary: primaryMediaInfo.url,
                    primaryFallback: primaryMediaInfo.fallbackUrl,
                    secondary: showSecondary ? secondaryMediaInfo.url : undefined,
                    secondaryFallback: showSecondary ? secondaryMediaInfo.fallbackUrl : undefined
                },
                {
                    primary: primaryIsVideo,
                    secondary: showSecondary ? secondaryIsVideo : false
                }
            );

            if (this.currentLoadToken !== loadToken || signal.aborted) {
                throw new DOMException('Aborted', 'AbortError');
            }
        }
        // Performs tasks after images are displayed
        _performPostDisplayTasks(targetIndex, displayPrimaryIndex, displaySecondaryIndex, navigatedBackwards, signal, loadToken) {
            if (this.currentLoadToken !== loadToken || signal.aborted) return; // Check before proceeding

            if (this.currentLoadToken === loadToken) {
                this.parent.style.cursor = "default"; // Reset cursor only if still current
            }

            // Update sidebar scroll position to the *target* index
            if (this.sidebarVisible) {
                this.sidebar.scrollToIndex(targetIndex, false);
            }

            // Set scroll position after layout update
            this._updateScrollPosition(navigatedBackwards);


            // Trigger preloading for neighbours
            if (displayPrimaryIndex !== null && this.config.preloadCount > 0) {
                const preloadCount = this.config.preloadCount;
                const preloadStartIndex = displaySecondaryIndex !== null ? displaySecondaryIndex + 1 : displayPrimaryIndex + 1;
                const preloadEndIndex = displayPrimaryIndex - 1;
                const useOriginal = this.config.useOriginalImages;
                const indicesToPreload = [];

                if (!navigatedBackwards) {
                    // Preload forward range
                    const forwardEnd = Math.min(preloadStartIndex + preloadCount - 1, thumbs.length - 1);
                    for (let i = preloadStartIndex; i <= forwardEnd; i++) {
                        indicesToPreload.push(i);
                    }
                    // Append the previous index (opposite direction)
                    indicesToPreload.push(preloadEndIndex);
                } else {
                    // Preload backward range
                    const backwardStart = Math.max(preloadEndIndex - preloadCount + 1, 0);
                    for (let i = preloadEndIndex; i >= backwardStart; i--) {
                        indicesToPreload.push(i);
                    }
                    // Append the next index (opposite direction)
                    indicesToPreload.push(preloadStartIndex);
                }
                preloadIndices(indicesToPreload, useOriginal);

            }
        }

        // Handles errors during the load process
        _handleLoadError(error, index, loadToken) {
            if (error.name === 'AbortError') {
                console.log(`Load operation for index ${index} aborted.`);
                // Don't show notification for aborts
            } else {
                console.error(`Error during loadAndShowIndex for index ${index}:`, error);
                createNotification(`Failed to load image ${index + 1}.`);
            }
            // Reset UI state only if this token is still current
            if (this.currentLoadToken === loadToken) {
                clearTimeout(this._dimTimeout); // Ensure dimming stops on error
                this.parent.style.cursor = "default";

                // Helper function to remove dimming effect from an element
                const removeDim = (element) => {
                    if (element && element.style.opacity !== '' && element.style.opacity !== '1') {
                        element.style.transition = 'none';
                        element.style.opacity = 1;
                        requestAnimationFrame(() => {
                            element.style.removeProperty('opacity');
                            element.style.removeProperty('transition');
                        });
                    } else if (element) {
                        element.style.removeProperty('opacity');
                        element.style.removeProperty('transition');
                    }
                };

                // Remove dimming from all elements explicitly on error/abort
                removeDim(this.imgDisplay);
                removeDim(this.videoDisplay);
                removeDim(this.imgDisplay2);
                removeDim(this.videoDisplay2);
                // *** ADDED *** Ensure final revoke just in case something failed silently
                this._revokeBlobUrl(this.videoDisplay);
                this._revokeBlobUrl(this.videoDisplay2);
                // *** ADDED *** Also revoke potential blob URLs on error/abort
                this._revokeBlobUrl(this.videoDisplay);
                this._revokeBlobUrl(this.videoDisplay2);
            }
        }

        // Finalizes the load attempt, resetting state
        _finalizeLoadAttempt(loadToken, dimTimeoutId) {
            // Helper function to remove dimming effect from an element
            const removeDim = (element) => {
                if (element && element.style.opacity !== '' && element.style.opacity !== '1') {
                    element.style.transition = 'none';
                    element.style.opacity = 1;
                    requestAnimationFrame(() => {
                        element.style.removeProperty('opacity');
                        element.style.removeProperty('transition');
                    });
                } else if (element) {
                    element.style.removeProperty('opacity');
                    element.style.removeProperty('transition');
                }
            };

            // Only reset state if this token is still the current one
            if (this.currentLoadToken === loadToken) {
                clearTimeout(dimTimeoutId); // Clear timeout regardless of success/fail
                this.parent.style.cursor = "default";
                this.currentAbortController = null; // Clear controller only if this was the last op
                this.isNavigating = false;

                // Final safety check: Ensure no dimming is left
                removeDim(this.imgDisplay);
                removeDim(this.videoDisplay);
                removeDim(this.imgDisplay2);
                removeDim(this.videoDisplay2);

            } else {
                // If a newer operation is already running, don't clear its state,
                // but maybe ensure dimming is cleared for elements associated with *this* old token?
                // The current approach handles this because removeDim is now called individually
                // when promises settle in _show. This block primarily handles the state variables.
            }
        }

        /**
        * Gets the appropriate media URL (image or video) for a given index,
        * considering configuration like useOriginalImages and useFallbackImages.
        * @param {number} index - The index of the thumb item.
        * @returns {{url: string | null, fallbackUrl: string | null}} - An object containing the primary URL to attempt (`url`)
        *                                and a potential fallback URL (`fallbackUrl`).
        *                                Both can be null if no suitable URL is found.
        */
        _getMediaUrlAtIndex(index) {
            if (index === null || index < 0 || index >= thumbs.length) {
                return { url: null, fallbackUrl: null };
            }
            const item = thumbs[index];
            if (!item || item === 'deleted') {
                return { url: null, fallbackUrl: null };
            }

            let primaryUrl = null;
            let fallbackUrl = null;

            if (item.isVideo) {
                primaryUrl = item.fullImageUrl || null;
                // No image fallback logic for videos in this implementation
            } else { // Is Image
                const originalImg = item.originalImageUrl;
                const fullImg = item.fullImageUrl;

                // Determine primary URL based on config.useOriginalImages and caching status
                if (this.config.useOriginalImages) {
                    if (originalImg && (item.originalImageIsCached || !fullImg)) {
                        // Prefer original if it exists AND (is cached OR fullImg is missing)
                        primaryUrl = originalImg;
                    } else if (fullImg) {
                        primaryUrl = fullImg;
                    } else { // Fallback to originalImg if fullImg was also missing
                        primaryUrl = originalImg || null;
                    }
                } else { // Not using original images
                    if (fullImg) {
                        primaryUrl = fullImg;
                    } else { // Fallback to originalImg if fullImg is missing
                        primaryUrl = originalImg || null;
                    }
                }

                // Determine fallback URL if config.useFallbackImages is true
                if (this.config.useFallbackImages) {
                    if (primaryUrl === originalImg && fullImg && fullImg !== originalImg) {
                        fallbackUrl = fullImg;
                    } else if (primaryUrl === fullImg && originalImg && originalImg !== fullImg) {
                        fallbackUrl = originalImg;
                    }
                    // If primaryUrl itself is null, but one of the image URLs exists,
                    // and wasn't chosen as primary (e.g., original not cached, full missing),
                    // it could potentially be a fallback if primaryUrl ended up null.
                    // However, the current logic aims to set primaryUrl if *any* URL is available.
                    // If primaryUrl is null, it means both originalImg and fullImg were likely null.
                }
            }
            return { url: primaryUrl, fallbackUrl: fallbackUrl };
        }

        /**
         * Retrieves media, utilizing an in-memory Blob cache with an LRU eviction strategy.
         * Creates a blob: URL for the retrieved Blob (either cached or newly fetched).
         * Handles concurrent requests for the same URL.
         * IMPORTANT: The caller is responsible for revoking the returned blobUrl.
         *
         * @param {string} mediaUrl The URL of the media to retrieve.
         * @returns {Promise<{ blobUrl: string, type: string | null }>} Resolves with blob URL and type.
         */
        async getCachedMediaBlob(mediaUrl) {
            // 1. Check for pending fetch
            if (this.pendingFetches.has(mediaUrl)) {
                console.log(`CACHE: Request for ${mediaUrl} already pending. Returning existing promise.`);
                return this.pendingFetches.get(mediaUrl);
            }

            // 2. Check cache
            if (this.blobCache.has(mediaUrl)) {
                const entry = this.blobCache.get(mediaUrl);
                console.log(`CACHE: Hit for ${mediaUrl} (Size: ${entry.size / 1024 / 1024} MB)`);

                // Update LRU: Move entry to the end by deleting and re-inserting
                this.blobCache.delete(mediaUrl);
                entry.lastAccessed = Date.now(); // Update timestamp (optional but good practice)
                this.blobCache.set(mediaUrl, entry);

                // Create a NEW blob URL for the cached blob
                const newBlobUrl = URL.createObjectURL(entry.blob);
                return Promise.resolve({ blobUrl: newBlobUrl, type: entry.type });
            }

            // 3. Not in cache, not pending - Initiate fetch
            console.log(`CACHE: Miss for ${mediaUrl}. Fetching...`);
            const fetchPromise = (async () => { // IIAFE to handle async/await inside promise constructor pattern
                try {
                    // Fetch the raw blob and type
                    const { blob, type } = await this._fetchMediaBlobAndType(mediaUrl);

                    // Check if blob exceeds total cache limit *before* eviction logic
                    const blobSize = blob.size;
                    const limitBytes = this.cacheLimitMB * 1024 * 1024;
                    if (blobSize > limitBytes) {
                        console.warn(`CACHE: Fetched blob size (${(blobSize / 1024 / 1024).toFixed(2)} MB) exceeds total cache limit (${this.cacheLimitMB} MB). Cannot cache ${mediaUrl}.`);
                        // Still create URL for immediate use, but don't cache
                        const blobUrl = URL.createObjectURL(blob);
                        return { blobUrl, type }; // Return directly without caching
                    }

                    // Ensure enough space in cache, evicting LRU if necessary
                    this._ensureCacheLimit(blobSize);

                    // Add the new blob to the cache
                    const newEntry = {
                        blob: blob,
                        size: blobSize,
                        type: type,
                        lastAccessed: Date.now()
                    };
                    this.blobCache.set(mediaUrl, newEntry);
                    this.currentCacheSize += blobSize;
                    console.log(`CACHE: Stored ${mediaUrl}. Cache size: ${(this.currentCacheSize / 1024 / 1024).toFixed(2)} MB / ${this.cacheLimitMB} MB`);

                    // Create blob URL for the newly cached blob
                    const blobUrl = URL.createObjectURL(blob);
                    return { blobUrl, type };

                } catch (error) {
                    console.error(`CACHE: Failed to fetch/process ${mediaUrl}:`, error);
                    throw error; // Re-throw error to be caught by the caller
                } finally {
                    // Remove from pending fetches once resolved or rejected
                    this.pendingFetches.delete(mediaUrl);
                }
            })(); // End IIAFE

            // Store the promise for concurrent requests
            this.pendingFetches.set(mediaUrl, fetchPromise);
            return fetchPromise;
        }

        /**
         * Internal function to fetch media using GM_xmlhttpRequest.
         * Resolves with the raw Blob object and its Content-Type.
         * NOTE: This does NOT create a blob: URL.
         *
         * @param {string} mediaUrl The URL of the media to fetch.
         * @returns {Promise<{ blob: Blob, type: string | null }>} A Promise resolving with the Blob and type.
         * @private
         */
        async _fetchMediaBlobAndType(mediaUrl) {
            // No config check here; the wrapper decides whether to call this.
            return new Promise((resolve, reject) => {
                console.log(`CACHE: Initiating GM fetch for: ${mediaUrl}`);
                GM_xmlhttpRequest({
                    method: "GET",
                    url: mediaUrl,
                    responseType: "blob",
                    onload: (response) => {
                        if (response.status >= 200 && response.status < 300) {
                            try {
                                const mediaBlob = response.response;
                                if (!mediaBlob || mediaBlob.size === 0) {
                                    reject(new Error(`CACHE: Received empty blob for ${mediaUrl}`));
                                    return;
                                }

                                let contentType = null;
                                if (response.responseHeaders) {
                                    const match = response.responseHeaders.match(/^content-type:\s*(.*)$/im);
                                    if (match && match[1]) {
                                        contentType = match[1].trim().split(';')[0];
                                    }
                                }
                                // Resolve with Blob and type
                                resolve({ blob: mediaBlob, type: contentType });
                            } catch (e) {
                                reject(new Error(`CACHE: Error processing response for ${mediaUrl}: ${e.message}`));
                            }
                        } else {
                            reject(new Error(`CACHE: Failed to fetch ${mediaUrl}: Server responded with status ${response.status} ${response.statusText}`));
                        }
                    },
                    onerror: (response) => {
                        reject(new Error(`CACHE: Network error fetching ${mediaUrl}: ${response.error || 'Unknown error'}`));
                    },
                    ontimeout: () => {
                        reject(new Error(`CACHE: Timeout fetching ${mediaUrl}`));
                    },
                    onabort: () => { // Handle abort if the outer operation is cancelled
                        reject(new Error(`CACHE: Request aborted for ${mediaUrl}`));
                    }
                    // We might need to pass the AbortSignal here if GM_xmlhttpRequest supports it
                    // signal: signal // If supported
                });
            });
        }

        /**
         * Ensures the cache size doesn't exceed the limit after adding an item of proposedSize.
         * Evicts items using an LRU strategy
         *
         * @param {number} proposedNewItemSize The size in bytes of the item about to be added.
         * @private
         */
        _ensureCacheLimit(proposedNewItemSize) {
            const limitBytes = this.cacheLimitMB * 1024 * 1024;

            // Calculate potential size if item is added
            let potentialSize = this.currentCacheSize + proposedNewItemSize;

            // Evict LRU items (first items in Map iteration) while over limit
            const cacheIterator = this.blobCache.keys(); // Get iterator for keys
            while (potentialSize > limitBytes && this.blobCache.size > 0) {
                const keyToRemove = cacheIterator.next().value; // Get the first (oldest) key
                if (!keyToRemove) break; // Should not happen if size > 0, but safety check

                const evicted = this._evictCacheEntry(keyToRemove);
                if (evicted) {
                    potentialSize = this.currentCacheSize + proposedNewItemSize; // Recalculate potential size
                } else {
                    console.error(`CACHE: Inconsistency during eviction for key ${keyToRemove}`);
                    break;
                }
            }

            // Optional: Final check if the new item *alone* is too big (already handled in getCachedMediaBlob)
            // if (proposedNewItemSize > limitBytes && this.blobCache.size === 0) {
            //  console.warn(`CACHE: New item size (${proposedNewItemSize} bytes) exceeds limit (${limitBytes} bytes) even after clearing cache.`);
            // }
        }

        /**
         * Removes a specific entry from the blob cache and updates the total cache size.
         * Logs the eviction.
         *
         * @param {string} mediaUrl The URL (key) of the cache entry to remove.
         * @returns {boolean} True if an entry was found and removed, false otherwise.
         * @private
         */
        _evictCacheEntry(mediaUrl) {
            if (this.blobCache.has(mediaUrl)) {
                const entryToRemove = this.blobCache.get(mediaUrl);
                if (entryToRemove) {
                    this.currentCacheSize -= entryToRemove.size;
                    // Ensure size doesn't go negative due to float precision or errors
                    if (this.currentCacheSize < 0) this.currentCacheSize = 0;
                    this.blobCache.delete(mediaUrl);
                    console.log(`CACHE: Evicted/Removed ${mediaUrl} (Size: ${(entryToRemove.size / 1024 / 1024).toFixed(2)} MB). New size: ${(this.currentCacheSize / 1024 / 1024).toFixed(2)} MB`);
                    return true;
                } else {
                    // Should not happen if .has() was true, but handle defensively
                    console.error(`CACHE: Inconsistency - found key ${mediaUrl} with .has() but .get() failed.`);
                    this.blobCache.delete(mediaUrl); // Attempt deletion anyway
                    return false;
                }
            } else {
                // console.warn(`CACHE: Attempted to evict non-existent key ${mediaUrl}`); // Optional warning
                return false;
            }
        }

    }// ----------------------------------------------------------------------------------------------
    // imageViewerInputHandler.js
    // ----------------------------------------------------------------------------------------------

    class ViewerInputHandler {
        constructor(imageViewerInstance, config) {
            this.viewer = imageViewerInstance;
            this.config = config;

            // State for smooth scroll acceleration
            this._lastSmoothScrollTime = 0;
            this._lastSmoothScrollDirection = 0; // 1 for down/pageDown, -1 for up/pageUp, 0 for none
            this._smoothScrollMultiplier = 1.0;

            // Constants for tuning acceleration
            this.SMOOTH_ACCEL_RESET_TIMEOUT_MS = 100; // Time in ms to reset multiplier
            this.SMOOTH_ACCEL_FACTOR = 1.15;         // Factor to increase multiplier by
            this.SMOOTH_ACCEL_MAX_MULTIPLIER = 2.5; // Maximum speed multiplier
            this.BASE_PAGE_SCROLL_FACTOR = 0.8;     // Base scroll amount for PageUp/Down
            this.BASE_SCROLL_STEP = 250;            // Base scroll amount for Arrow keys

            this._initializeListeners();
        }

        _initializeListeners() {
            // Bind handlers to maintain 'this' context and allow removal
            this._boundHandlePasteEvent = this._handlePasteEvent.bind(this);
            this._boundHandleCopyEvent = this._handleCopyEvent.bind(this);
            this._boundHandleKeyDown = this._handleKeyDown.bind(this);
            this._boundHandleWheel = this._handleWheel.bind(this);

            document.addEventListener("paste", this._boundHandlePasteEvent);
            document.addEventListener("copy", this._boundHandleCopyEvent);
            this.viewer.parent.addEventListener("wheel", this._boundHandleWheel, { passive: false }); // Listen on the parent div for wheel events
            document.addEventListener("keydown", this._boundHandleKeyDown);
        }

        /**
         * Performs a smooth scroll, increasing the distance if called
         * repeatedly in the same direction within a short timeframe.
         * @param {Element} element The element to scroll.
         * @param {object} options Original scroll options (must include top and behavior: 'smooth').
         */
        smoothScrollWithAcceleration(element, options) {
            if (!element || typeof options?.top !== 'number' || options?.behavior !== 'smooth') {
                console.warn("Invalid arguments for smoothScrollWithAcceleration");
                if (element && options) element.scrollBy(options); // Try fallback
                return;
            }

            const now = Date.now();
            const intendedDeltaY = options.top;
            const currentDirection = Math.sign(intendedDeltaY); // -1 for up, 1 for down

            // --- Reset Check ---
            // Reset multiplier if direction changes or too much time has passed
            if (currentDirection !== this._lastSmoothScrollDirection ||
                (now - this._lastSmoothScrollTime) > this.SMOOTH_ACCEL_RESET_TIMEOUT_MS) {
                this._smoothScrollMultiplier = 1.0;
            }
            // --- Acceleration ---
            else {
                // Increase multiplier if conditions are met (same direction, quick succession)
                this._smoothScrollMultiplier = Math.min(
                    this.SMOOTH_ACCEL_MAX_MULTIPLIER,
                    this._smoothScrollMultiplier * this.SMOOTH_ACCEL_FACTOR
                );
            }

            const finalDeltaY = intendedDeltaY * this._smoothScrollMultiplier;
            element.scrollBy({ top: finalDeltaY, behavior: 'smooth' });

            // Update state for next call
            this._lastSmoothScrollTime = now;
            this._lastSmoothScrollDirection = currentDirection;
        }

        _navigateOrPan(direction, forceNavigate) {
            if (direction === 1) {
                const canScrollDown = this.viewer.currentZoom > 1 && this.viewer.parent.scrollHeight > this.viewer.parent.clientHeight;
                const isAtBottom = this.viewer.parent.scrollTop >= this.viewer.parent.scrollHeight - this.viewer.parent.clientHeight - 5; // Allow 5px tolerance

                if (this.config.panFirst && canScrollDown && !isAtBottom && !forceNavigate) {
                    // Pan down smoothly if panFirst is enabled and not at bottom
                    this.viewer.parent.scrollBy({ top: this.config.panStep * this.viewer.currentZoom, behavior: 'smooth' });
                } else {
                    // Otherwise, navigate forward
                    this.viewer.navigateForward();
                }
            } else {
                const canScrollUp = this.viewer.currentZoom > 1 && this.viewer.parent.scrollHeight > this.viewer.parent.clientHeight;
                const isAtTop = this.viewer.parent.scrollTop <= 5;

                if (this.config.panFirst && canScrollUp && !isAtTop && !forceNavigate) {
                    // Pan up smoothly if panFirst is enabled and not at top
                    this.viewer.parent.scrollBy({ top: -this.config.panStep * this.viewer.currentZoom, behavior: 'smooth' });
                } else {
                    // Otherwise, navigate back
                    this.viewer.navigateBack();
                }
            }
        }

        _handleWheel(e) {
            // Handles Ctrl+Mousewheel and Touchpad Pinch-to-Zoom (which often sends wheel events with ctrlKey=true)
            // Only handle wheel events if the viewer is active, config UI isn't showing, and Ctrl key is pressed (for zoom)
            if (!this.viewer.isActive() || this.config.showingUI() || !e.ctrlKey || this.viewer.isGridViewActive()) {
                // Allow default scroll or other behaviors if Ctrl isn't pressed or viewer isn't right state
                return;
            }

            // Prevent default browser zoom or scroll when Ctrl + Wheel is used for image zoom
            e.preventDefault();

            // Determine zoom direction based on deltaY
            // Negative deltaY means scroll up or pinch out (zoom in)
            // Positive deltaY means scroll down or pinch in (zoom out)
            const zoomDirection = e.deltaY < 0 ? 1 : -1; // +1 for zoom in, -1 for zoom out

            // Set zoomChanged flag and call zoom function, anchoring to the center
            this.viewer.userChangedZoom = true;
            this.viewer.zoom(zoomDirection, 1.07, true, true); // Use direction, anchor to center X and Y
        }

        _handleKeyDown(e) {
            if (e.ctrlKey || e.altKey || e.metaKey || e.key === 'Alt') return;

            if (!this.viewer.isActive() || this.config.showingUI() || this.mediaExtractorUi?.isShown()) {
                return;
            }

            const targetElement = e.target;
            if (targetElement && (
                targetElement.tagName === 'INPUT' ||
                targetElement.tagName === 'TEXTAREA' ||
                targetElement.isContentEditable
            )) {
                return;
            }

            switch (e.key) {
                case "Escape":
                    if (this.viewer.isHelpOverlayVisible()) {
                        this.viewer.toggleHelpOverlay();
                    } else if (this.config.showingUI()) {
                        this.config.closeUI();
                    } else {
                        this.viewer.closeViewer(this.config.exitToViewerPage ^ e.shiftKey);
                    }
                    break;
                case "Backspace":
                    if (this.viewer.chapterSidebarIsActive()) this.viewer.toggleChapterSidebar();
                    if (this.viewer.sidebarIsActive()) this.viewer.sidebarHideInstant();
                    this.viewer.toggleGridView(true);
                    break;
                case "Home":
                    if (this.viewer.isGridViewActive()) {
                        this.viewer.gridView.scrollToIndex(0, false);
                    } else {
                        this.viewer.loadAndShowIndex(0);
                    }
                    break;
                case "End":
                    if (this.viewer.isGridViewActive()) {
                        this.viewer.gridView.scrollToIndex(thumbs.length - 1, false);
                    } else {
                        this.viewer.loadAndShowIndex(thumbs.length - 1);
                    }
                    break;
                case "PageUp":
                    if (e.shiftKey) {
                        if (this.viewer.currentIndex == 0) {
                            createNotification("First page!");
                            break;
                        }
                        if (!chapterList?.length) {
                            createNotification("No chapters available");
                            break;
                        }
                        const prevChapter = chapterList.slice().reverse().find(ch => ch.index < this.viewer.currentIndex);
                        if (prevChapter) {
                            this.viewer.loadAndShowIndex(prevChapter.index);
                        } else {
                            this.viewer.loadAndShowIndex(0);
                        }
                    } else if (this.viewer.isGridViewActive()) {
                        this.smoothScrollWithAcceleration(this.viewer.gridView.getContainer(), { top: -this.viewer.gridView.getContainer().clientHeight * this.BASE_PAGE_SCROLL_FACTOR, behavior: 'smooth' });
                        e.preventDefault(); // Prevent default page scroll
                    } else {
                        const canScrollUp = this.viewer.currentZoom > 1 && this.viewer.parent.scrollHeight > this.viewer.parent.clientHeight;
                        const isAtTop = this.viewer.parent.scrollTop <= 5; // 5px tolerance

                        if (canScrollUp && !isAtTop) {
                            this.viewer.parent.scrollTo({ top: 0, behavior: 'smooth' });
                        } else {
                            this.viewer.navigateBack();
                        }

                        e.preventDefault(); // Prevent default page scroll
                    }
                    break;
                case "PageDown":
                    if (e.shiftKey) {
                        if (this.viewer.currentIndex === thumbs.length - 1) {
                            createNotification("Last page!");
                            break;
                        }
                        if (!chapterList?.length) {
                            createNotification("No chapters available");
                            break;
                        }
                        const nextChapter = chapterList.find(ch => ch.index > this.viewer.currentIndex);
                        if (nextChapter) {
                            this.viewer.loadAndShowIndex(nextChapter.index);
                        } else {
                            this.viewer.loadAndShowIndex(thumbs.length - 1);
                        }
                    } else if (this.viewer.isGridViewActive()) {
                        this.smoothScrollWithAcceleration(this.viewer.gridView.getContainer(), { top: this.viewer.gridView.getContainer().clientHeight * this.BASE_PAGE_SCROLL_FACTOR, behavior: 'smooth' });
                        e.preventDefault(); // Prevent default page scroll
                    } else {
                        const canScrollDown = this.viewer.currentZoom > 1 && this.viewer.parent.scrollHeight > this.viewer.parent.clientHeight;
                        const isAtBottom = this.viewer.parent.scrollTop >= this.viewer.parent.scrollHeight - this.viewer.parent.clientHeight - 5; // Allow 5px tolerance

                        if (canScrollDown && !isAtBottom) {
                            this.viewer.parent.scrollTo({ top: this.viewer.parent.scrollHeight, behavior: 'smooth' });
                        } else {
                            this.viewer.navigateForward();
                        }

                        e.preventDefault(); // Prevent default page scroll
                    }
                    break;
                case "q":
                case "Q":
                    this.viewer.closeViewer(this.config.exitToViewerPage ^ e.shiftKey);
                    break;
                case "d":
                case ".":
                case "ArrowRight":
                case "Space":
                case " ":
                    if (!this.viewer.isGridViewActive()) {
                        e.preventDefault();

                        if (e.shiftKey && e.key === "ArrowRight" && this.viewer.isShowingVideo()) {
                            this.viewer.seekVideo(5)
                            break;
                        }

                        if (this.viewer.rightToLeftMode && this.config.reverseNavigationInRtlMode) {
                            this._navigateOrPan(-1, e.shiftKey);
                        } else {
                            this._navigateOrPan(1, e.shiftKey);
                        }
                    }
                    break;
                case "a":
                case ",":
                case "ArrowLeft":
                    if (!this.viewer.isGridViewActive()) {
                        e.preventDefault();

                        if (e.shiftKey && e.key === "ArrowLeft" && this.viewer.isShowingVideo()) {
                            this.viewer.seekVideo(-5)
                            break;
                        }

                        if (this.viewer.rightToLeftMode && this.config.reverseNavigationInRtlMode) {
                            this._navigateOrPan(1, e.shiftKey);
                        } else {
                            this._navigateOrPan(-1, e.shiftKey);
                        }
                    }
                    break;
                case "ArrowUp":
                    if (!this.viewer.isGridViewActive() && this.viewer.currentZoom > 1 && this.viewer.parent.scrollTop > 0) {
                        this.smoothScrollWithAcceleration(this.viewer.parent, { top: -this.BASE_SCROLL_STEP, behavior: 'smooth' });
                        e.preventDefault(); // Prevent default page scroll
                    }
                    else if (this.viewer.isGridViewActive()) {
                        this.smoothScrollWithAcceleration(this.viewer.gridView.getContainer(), { top: -this.BASE_SCROLL_STEP, behavior: 'smooth' });
                        e.preventDefault(); // Prevent default page scroll
                    }
                    break;
                case "ArrowDown":
                    if (!this.viewer.isGridViewActive() && this.viewer.currentZoom > 1 && this.viewer.parent.scrollTop < this.viewer.parent.scrollHeight - this.viewer.parent.clientHeight) {
                        this.smoothScrollWithAcceleration(this.viewer.parent, { top: this.BASE_SCROLL_STEP, behavior: 'smooth' });
                        e.preventDefault(); // Prevent default page scroll
                    }
                    else if (this.viewer.isGridViewActive()) {
                        this.smoothScrollWithAcceleration(this.viewer.gridView.getContainer(), { top: this.BASE_SCROLL_STEP, behavior: 'smooth' });
                        e.preventDefault(); // Prevent default page scroll
                    }
                    break;
                case "+":
                case "=": // Often shifted version of +
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.userChangedZoom = true;
                        this.viewer.zoom(1, 1.1, true, false);
                    }
                    break;
                case "-":
                case "_": // Often shifted version of -
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.userChangedZoom = true;
                        this.viewer.zoom(-1, 1.1, true, false);
                    }
                    break;
                case "r":
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.rotateImage(90);
                    }
                    break;
                case "l":
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.rotateImage(-90);
                    }
                    break;
                case "s":
                    if (!this.viewer.isGridViewActive() && this.config.enableSidebar) {
                        this.viewer.pinSidebar = !this.viewer.pinSidebar;
                        this.viewer._sidebarShowOrHide();
                    }
                    break;
                case "S":
                    this.viewer.downloadCurrentImage();
                    break;
                case "F":
                    this.viewer.showGalleriesWithCurrentImage();
                    break;
                case "D":
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.toggleDualPageMode();
                    }
                    break;
                case "R":
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.toggleRightToLeftMode();
                    }
                    break;
                case "A":
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.enableDualPageModeOrCycleLayout();
                    }
                    break;
                case "f":
                    this.viewer.useFullscreen = !isFullscreen();
                    if (this.viewer.useFullscreen) {
                        this.viewer.enterFullscreen();
                    } else {
                        this.viewer.exitFullscreen();
                    }
                    break;
                case "Delete":
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.delete(this.viewer.currentIndex);
                        embeddedGridView?.refreshAll();
                    }
                    break;
                case "1": // Reset Zoom (Fit Screen)
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.userChangedZoom = false;
                        this.viewer.fitMode = "fit-window";
                        this.viewer.currentZoom = 1;
                        this.viewer.updateTransforms();
                    }
                    break;
                case "2": // 1:1 Pixel Zoom
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.userChangedZoom = false;
                        this.viewer.fitMode = "one-to-one";
                        this.viewer.updateTransforms();
                    }
                    break;
                case "3": // Zoom to Fill Width
                    if (!this.viewer.isGridViewActive()) {
                        this.viewer.userChangedZoom = false;
                        this.viewer.fitMode = "fit-width";
                        this.viewer.updateTransforms();
                    }
                    break;
                case "h":
                case "?": // Show/hide help overlay
                    this.viewer.toggleHelpOverlay();
                    break;
                case "c": // Show/hide chapter sidebar
                    this.viewer.toggleChapterSidebar();
                    break;
                case " ":
                case "Spacebar":
                    this.viewer.toggleVideoPlayback();
                    break;
                case "E":
                    if (!this.mediaExtractorUi) {
                        this.mediaExtractorUi = new MediaExtractorUI();
                    } else if (this.mediaExtractorUi.isShown()) {
                        this.mediaExtractorUi.close();
                        break;
                    }
                    this.mediaExtractorUi.onExtractionComplete((error, results) => {
                        if (error) {
                            return;
                        }

                        if (results?.length) {
                            for (let i = 0; i < results.length; i++) {
                                const result = results[i];
                                const index = i + this.viewer.currentIndex;
                                while (index >= thumbs.length) thumbs.push(null);

                                thumbs[index] = {
                                    fullImageUrl: result.url,
                                    isVideo: result.type === "video",
                                };
                            }

                            this.viewer.loadAndShowIndex(this.viewer.currentIndex);
                            this.viewer.refreshGrids();
                            embeddedGridView?.refreshAll();
                        }

                    });
                    this.mediaExtractorUi.render();
                    break;
                case "N":
                case "I":
                    console.log("Inserting a blank image at index", this.viewer.currentIndex + 1);
                    const item = {
                        fullImageUrl: null
                    };
                    insertAfterIndex(thumbs, this.viewer.currentIndex, item);
                    this.viewer.loadAndShowIndex(this.viewer.currentIndex + 1);
                    break;
            }
        }

        // Add these inside the ViewerInputHandler class or where appropriate

        // Regular expression to match common video file extensions
        _isVideoExtension(filename = '') {
            return /\.(mp4|webm|mov|ogg|avi|mkv)$/i.test(filename);
        }

        // Regular expression to match common image file extensions
        _isImageExtension(filename = '') {
            return /\.(jpe?g|png|gif|webp|svg|bmp|ico)$/i.test(filename);
        }

        // Function to parse MIME type from a Data URL
        _getMimeTypeFromDataUrl(dataUrl = '') {
            const match = dataUrl.match(/^data:([\w\/+-.]+)(?:;.*)?/);
            return match ? match[1].toLowerCase() : null; // e.g., 'image/png' or 'video/mp4'
        }

        /**
         * Tries to insert and load media at the given index.
         * Updates the viewer's state and associated grid views.
         *
         * @param {string} url - The blob:, data:, or http(s): URL to load.
         * @param {number} idx - The index in the thumbs array to update.
         * @param {boolean} isVideo - Whether the URL represents a video.
         * @returns {Promise<boolean>} - True if the media loaded successfully, false otherwise.
         */
        async insertImage(url, idx, isVideo) {
            const thumbItem = thumbs[idx];
            if (!thumbItem || thumbItem === 'deleted') {
                console.warn(`Cannot insert media at index ${idx}, item is invalid.`);
                return false; // Indicate failure
            }

            console.log(`Attempting to insert ${isVideo ? 'video' : 'image'} from URL: ${url.substring(0, 100)}... at index ${idx}`);

            // Update thumb data
            thumbItem.isVideo = isVideo;
            // For pasted content, we assume the pasted URL is both full and original for simplicity
            thumbItem.fullImageUrl = url;
            thumbItem.originalImageUrl = url; // Or null if we want to be strict? Let's keep it simple.
            thumbItem.imageUrlToLoad = null; // Clear any cached URL from loadImageUrlAtIndex
            thumbItem.imageElement = null;   // Clear any cached element

            // Force update in any visible grid views
            this.viewer.sidebar?.forceSetFullImage(idx, url); // Pass URL for immediate update if needed
            this.viewer.gridView?.forceSetFullImage(idx, url);
            embeddedGridView?.forceSetFullImage(idx, url); // Assuming embeddedGridView might exist

            // Reset viewer state before loading the new content
            this.viewer.userChangedZoom = false; // Allow fitMode to apply initially
            this.viewer.currentZoom = 1;
            this.viewer.currentRotation = 0;
            this.viewer.displayedRotation = 0;

            // --- Load and Verify ---
            try {
                document.body.style.cursor = "progress";

                // loadAndShowIndex will attempt to load the media using _show, which uses waitForMediaLoad
                await this.viewer.loadAndShowIndex(idx);

                // After loadAndShowIndex completes, check if the correct element loaded successfully
                const primaryElement = isVideo ? this.viewer.videoDisplay : this.viewer.imgDisplay;

                // Check if the primary display slot is showing the content we just set
                // Note: src might be blob: even if original was http: after fetchImageBlobUrl
                // So, checking dimensions is more reliable than src equality in some cases.
                const isPrimaryVisible = primaryElement.style.display === 'block';
                const hasValidDimensions = isVideo
                    ? primaryElement.videoWidth > 0
                    : primaryElement.naturalWidth > 0;

                // Check if the displayed src corresponds *roughly* to what we tried loading.
                // For data/blob URLs, currentSrc will be the same. For http, it might differ if redirected.
                // This check is less critical than dimensions.
                const srcMatches = primaryElement.currentSrc === url || primaryElement.src === url;

                if (isPrimaryVisible && hasValidDimensions) {
                    console.log(`Successfully loaded and displayed ${isVideo ? 'video' : 'image'} at index ${idx}.`);
                    // Optional: Force another transform update? Usually loadAndShowIndex handles it.
                    // this.viewer.updateTransforms();
                    return true; // Indicate success
                } else {
                    console.warn(`Failed to verify load for ${isVideo ? 'video' : 'image'} at index ${idx}. Visible: ${isPrimaryVisible}, Valid Dims: ${hasValidDimensions}, Src Matches: ${srcMatches}, currentSrc: ${primaryElement.currentSrc.substring(0, 100)}`);
                    return false; // Indicate failure
                }
            } catch (error) {
                console.error(`Error during loadAndShowIndex after paste for index ${idx}:`, error);
                return false; // Indicate failure
            } finally {
                document.body.style.cursor = "default";
            }
        }

        async _handlePasteEvent(e) {

            const targetElement = e.target;
            if (targetElement && (
                targetElement.tagName === 'INPUT' ||
                targetElement.tagName === 'TEXTAREA' ||
                targetElement.isContentEditable
            )) {
                return;
            }

            if (!this.viewer.isActive()) return;
            e.preventDefault();

            const currentIdx = this.viewer.currentIndex;
            if (!thumbs[currentIdx] || thumbs[currentIdx] === 'deleted') {
                console.warn("Cannot paste as target thumb item is invalid or deleted.");
                createNotification("Cannot paste here");
                return;
            }

            const items = (e.clipboardData || window.clipboardData).items;
            let foundMedia = false;
            let potentialTextItem = null;
            console.log(`Paste event detected with ${items.length} items.`);

            // --- Try Pasted Files First (Images or Videos) ---
            for (let i = 0; i < items.length; i++) {
                const item = items[i];
                // console.log(`Item ${i}: type = ${item.type}, kind = ${item.kind}`);
                if (item.kind === 'file' && (item.type.startsWith("image/") || item.type.startsWith("video/"))) {
                    const blob = item.getAsFile();
                    if (blob) {
                        const isVideo = blob.type.startsWith("video/");
                        console.log(`Found ${isVideo ? 'video' : 'image'} blob:`, blob);
                        const reader = new FileReader();
                        reader.onload = async (event) => { // Make onload async
                            const pastedDataUrl = event.target.result;
                            console.log(`Pasted ${isVideo ? 'video' : 'image'} data loaded (Data URL).`);
                            const success = await this.insertImage(pastedDataUrl, currentIdx, isVideo);
                            if (success) {
                                console.log(`Pasted ${isVideo ? 'video' : 'image'} data`);
                                foundMedia = true; // Set flag on successful insertion *here*
                            } else {
                                // Notification handled by insertImage/loadAndShowIndex
                            }
                        };
                        reader.onerror = (err) => {
                            console.error("FileReader error:", err);
                            createNotification(`Error reading pasted ${isVideo ? 'video' : 'image'}`);
                        }
                        reader.readAsDataURL(blob);
                        // Don't set foundMedia here, wait for reader.onload confirmation
                        // foundMedia = true; // Removed from here
                        break; // Stop after finding the first file
                    }
                } else if (item.kind === 'string' && item.type === 'text/plain' && !potentialTextItem) {
                    potentialTextItem = item;
                }
            }

            // Await potential file processing before checking text
            // (FileReader is async, but we broke the loop, so this is roughly okay)
            // A better approach might involve Promise.all if multiple files were processed.
            // For now, assuming we only process the first file:

            // --- If No File Found *or file processing failed*, Try Pasted Text (URL) ---
            if (!foundMedia && potentialTextItem) { // Check foundMedia status now
                console.log("No media file found or loaded, checking potential text item...");
                potentialTextItem.getAsString(async (pastedText) => { // Make callback async
                    pastedText = pastedText.trim();
                    if (!pastedText) {
                        console.log("Pasted text is empty.");
                        return;
                    }
                    console.log("Pasted text:", pastedText.substring(0, 100) + (pastedText.length > 100 ? '...' : ''));

                    let success = false;
                    let triedInsert = false;

                    // --- Handle data: URLs ---
                    if (pastedText.startsWith('data:')) {
                        console.log("Pasted text is a data: URL.");
                        triedInsert = true;
                        const mimeType = this._getMimeTypeFromDataUrl(pastedText);
                        if (mimeType) {
                            if (mimeType.startsWith('video/') || mimeType.startsWith('image/')) {
                                const isVideo = mimeType.startsWith('video/');
                                success = await this.insertImage(pastedText, currentIdx, isVideo);
                                if (success) {
                                    console.log(`Pasted ${isVideo ? 'video' : 'image'} from data URL`);
                                }
                            } else {
                                console.log("Pasted data is not an image or video");
                                createNotification("Paste is not image or video");
                            }
                        } else {
                            console.warn("Could not parse MIME type from data: URL.");
                            createNotification("Invalid data URL format");
                        }
                        // --- Handle http/https: URLs ---
                    } else if (pastedText.startsWith('http')) {
                        let isVideo = this._isVideoExtension(pastedText);
                        let isImage = this._isImageExtension(pastedText);
                        // Basic check if it looks like any http URL, even without extension
                        const isLikelyHttpUrl = /^https?:\/\/[^\s]+$/i.test(pastedText);

                        // If it looks like an HTTP URL but has no recognized media extension, assume image
                        if (isLikelyHttpUrl && !isVideo && !isImage) {
                            console.log("Pasted HTTP URL has no recognized media extension, fetching type..");
                            const type = await getMediaContentType(pastedText);
                            if (!type) {
                                console.warn("Could not retrieve type, assuming image");
                                isImage = true;
                            }

                            isImage = type.startsWith("image/");
                            isVideo = type.startsWith("video/");

                            if (!isImage && !isVideo) {
                                console.log("Pasted URL does not point to an image or video");
                                createNotification("Paste is not an image or video URL");
                                return;
                            }
                        } else if (isLikelyHttpUrl) {
                            console.log(`Pasted text looks like a ${isVideo ? 'video' : 'image'} HTTP URL.`);
                        }
                        if (isVideo) isImage = false; // Video takes precedence

                        if (isImage || isVideo) {
                            triedInsert = true;
                            // --- Attempt Direct Load ---
                            success = await this.insertImage(pastedText, currentIdx, isVideo);

                            if (success) {
                                console.log(`Pasted ${isVideo ? 'video' : 'image'} from URL`);
                            } else {
                                console.warn(`Direct load failed for URL: ${pastedText}`);
                                // Notification handled internally, but now try cross-origin fetch

                                // --- If Direct Load Failed, Try Cross-Origin Fetch ---
                                const isPotentiallyCrossOrigin = !pastedText.startsWith(window.location.origin);
                                if (isPotentiallyCrossOrigin) {
                                    console.log("Direct load failed, attempting cross-origin fetch...");
                                    try {
                                        document.body.style.cursor = "progress";
                                        // fetchImageBlobUrl returns { blobUrl, type }
                                        const result = await fetchMediaBlobUrl(pastedText);

                                        // Check if we got the expected result object and a blobUrl
                                        if (result && result.blobUrl) {
                                            // Determine media type PRIMARILY from the fetched Content-Type header
                                            let fetchedIsVideo = false;
                                            if (result.type) { // Check if type was successfully extracted
                                                fetchedIsVideo = result.type.startsWith('video/');
                                                console.log(`Cross-origin fetch successful. Actual Type: ${result.type}. Blob URL: ${result.blobUrl.substring(0, 100)}...`);
                                            } else {
                                                // Fallback to guessing from original URL if Content-Type was missing
                                                fetchedIsVideo = isVideo; // Use the 'isVideo' flag guessed before
                                                console.warn(`Cross-origin fetch successful but Content-Type header was missing/unparsable. Falling back to guessed type (${fetchedIsVideo ? 'video' : 'image'}) from original URL.`);
                                            }

                                            // Try inserting the Blob URL using the DETECTED (or guessed) type
                                            success = await this.insertImage(result.blobUrl, currentIdx, fetchedIsVideo);

                                            if (success) {
                                                console.log(`Pasted ${fetchedIsVideo ? 'video' : 'image'} via cross-origin fetch`);
                                            } else {
                                                // Load failed. Notification likely handled by insertImage/loadAndShowIndex.
                                                console.log("Revoking unused/failed blob URL after failed load:", result.blobUrl.substring(0, 100));
                                                URL.revokeObjectURL(result.blobUrl); // Clean up memory
                                            }
                                        } else {
                                            // This case handles if fetchImageBlobUrl resolved without a blobUrl (shouldn't happen with current logic, but good check)
                                            console.error("Cross-origin fetch did not return a blob URL in the result object.");
                                            createNotification("Cross-origin fetch failed (no blob URL)");
                                        }
                                    } catch (error) {
                                        // Handle potential rejections from fetchImageBlobUrl
                                        console.error(`Cross-origin fetch failed for ${pastedText}:`, error);
                                        let errorMsg = "Cross-origin fetch failed";
                                        if (error instanceof Error && error.message) {
                                            errorMsg += `: ${error.message}`;
                                        } else if (typeof error === 'string') {
                                            errorMsg += `: ${error}`;
                                        }
                                        createNotification(errorMsg);
                                        // No blobUrl to revoke if fetchImageBlobUrl rejects
                                    } finally {
                                        document.body.style.cursor = "default";
                                    }
                                } else {
                                    console.log("URL is not cross-origin or direct load failed for other reasons.");
                                }
                            }
                        } else if (isLikelyHttpUrl) {
                            console.log("Pasted text looks like an HTTP URL, but not a recognized media type.");
                            createNotification("URL does not point to known media type");
                        } else {
                            console.log("Pasted text is not a data: or http(s): URL.");
                            createNotification("Paste does not contain media URL");
                        }
                        // --- Handle other text ---
                    } else {
                        console.log("Pasted text is not a data: or http(s): URL.");
                        createNotification("Paste does not contain media URL");
                    }

                    // Update foundMedia *after* all attempts for the text item
                    if (success) {
                        foundMedia = true;
                    }
                });
                // Need to wait for getAsString callback if it was called
                // This requires more complex promise handling, or we accept that foundMedia
                // might not be updated immediately if text pasting occurs.
                // Let's simplify: the notification inside getAsString is the primary feedback.

            }

            // Final check after potential async operations (might be slightly delayed)
            // This check is less reliable due to async nature of getAsString
            /*
            if (!foundMedia && !potentialTextItem && items.length > 0) {
                 console.log("No compatible media file or text URL found or loaded successfully.");
            } else if (items.length === 0) {
                 console.log("Paste event had no items.");
                 createNotification("Nothing found in clipboard to paste");
            }
            */
        }

        async _handleCopyEvent(e) {
            const targetElement = e.target;
            if (targetElement && (
                targetElement.tagName === 'INPUT' ||
                targetElement.tagName === 'TEXTAREA' ||
                targetElement.isContentEditable
            )) {
                return;
            }

            if (!this.viewer.isActive()) return;

            const item = thumbs[this.viewer.currentIndex];

            if (!item) {
                console.warn("Cannot copy as thumbs[currentIndex] is null");
                return;
            }

            const url = this.config.useOriginalImages && item.originalImageUrl
                ? item.originalImageUrl
                : item.fullImageUrl;


            if (!url) {
                console.warn("Cannot copy, missing url");
                return;
            }

            if (e) e.preventDefault();

            // Check if the Clipboard API (writeText) is supported
            if (!navigator.clipboard || !navigator.clipboard.writeText) {
                console.error("Clipboard API (writeText) not supported.");
                // Fallback for older browsers (less reliable)
                try {
                    const textArea = document.createElement("textarea");
                    textArea.value = url;
                    textArea.style.position = "fixed"; // Avoid scrolling
                    document.body.appendChild(textArea);
                    textArea.focus();
                    textArea.select();
                    const successful = document.execCommand('copy');
                    document.body.removeChild(textArea);
                    if (successful) {
                        console.log("Image URL copied to clipboard (fallback method).");
                        createNotification("Copied image URL");
                    } else {
                        throw new Error('Fallback copy failed');
                    }
                } catch (err) {
                    console.error("Failed to copy URL (fallback failed):", err);
                    createNotification("Error copying URL");
                }
                return;
            }

            // Use modern Clipboard API
            try {
                await navigator.clipboard.writeText(url);
                console.log("Image URL copied to clipboard:", url);
                createNotification("Copied image URL");
            } catch (err) {
                console.error("Failed to copy URL:", err);
                createNotification("Error copying URL");
                if (err.name === 'NotAllowedError') {
                    createNotification("Error: Clipboard permission denied.");
                }
            }
        }

        // Method to detach listeners
        destroy() {
            document.removeEventListener("paste", this._boundHandlePasteEvent);
            document.removeEventListener("copy", this._boundHandleCopyEvent);
            document.removeEventListener("keydown", this._boundHandleKeyDown);
            this.viewer.parent.removeEventListener("wheel", this._boundHandleWheel);
        }
    }
    // ----------------------------------------------------------------------------------------------
    // gridView.js
    // ----------------------------------------------------------------------------------------------

    class GridView {
        // --- Internal State and Calculated Values ---
        gridContainer;
        aspectRatio = 3 / 4;
        disableOnImageSelect = false;
        observeViaViewport = false;
        minMargin = 10;
        placeholderColor = '#cccccc';
        observerRootMargin = '200px 0px';
        itemPaddingBottom;
        placeholderBorderStyle;
        gridIntersectionObserver = null;
        itemsToLoadQueue = [];
        gridViewIsLoading = false;
        showCalled = false;
        disabled = false;
        loadingEnabled = true;
        initialIndex = 0;
        loadingStatus = new Map(); // Tracks status: 'pending', 'queued', 'loading', 'loaded', 'failed'
        _cachedGridItemRect = null; // Cache for grid item dimensions
        fetchingThumbPages = new Set(); // Track pages currently fetching thumbnails
        config; // To store passed configuration

        /**
         * Initializes the GridView component.
         * @param {HTMLElement} parent The parent element to append the grid container to
         * @param {object} config Configuration object containing grid settings
         * @param {number} config.numColumns Number of columns in the grid
         * @param {boolean} config.showPageNumbers Whether to show index labels on grid items
         * @param {boolean} config.fetchFullImages Whether to actively fetch full images when items intersect.
         * @param {boolean} [observeViaViewport] Determines the root for the IntersectionObserver.
         *      - If `false` (default): The observer monitors item visibility relative to the `gridContainer` itself. Use this when the `gridContainer` has a fixed height and internal scrolling (`overflow: auto/scroll`).
         *      - If `true`: The observer monitors item visibility relative to the browser's viewport. Use this when the `gridContainer` displays all its items at once and visibility changes occur due to page scrolling.
         * @param {boolean} [disableOnImageSelect=false] Whether to disable grid view when an image is clicked.
         */
        constructor(parent, config, observeViaViewport, disableOnImageSelect = false) {
            if (!parent || !(parent instanceof Element)) {
                throw new Error("GridView requires a valid parent HTMLElement.");
            }
            if (!config) {
                throw new Error("GridView requires a config object.");
            }
            if (typeof config.numColumns !== 'number' || typeof config.showPageNumbers !== 'boolean' || typeof config.fetchFullImages !== 'boolean') {
                throw new Error("GridView requires config object with 'numColumns' (number), 'gridLabels' (bool) and 'fetchFullImages' (bool).");
            }
            if (typeof loadImageUrlAtIndex !== 'function') {
                throw new Error("GridView requires global 'loadImageUrlAtIndex' function.");
            }
            if (typeof getThumbOffset !== 'function') {
                throw new Error("GridView requires global 'getThumbOffset' function.");
            }
            if (typeof thumbs === 'undefined') {
                throw new Error("GridView requires global 'thumbs' collection.");
            }
            if (typeof getPageIndexByImageIndex !== 'function') {
                throw new Error("GridView requires global 'getPageIndexByImageIndex' function.");
            }
            if (typeof fetchAndPopulateThumbsOnPage !== 'function') {
                throw new Error("GridView requires global 'fetchAndPopulateThumbsOnPage' function.");
            }

            // --- Calculate derived values (Internal) ---
            this.parent = parent;
            this.config = config;
            this.disableOnImageSelect = disableOnImageSelect;
            this.observeViaViewport = observeViaViewport;
            this.itemPaddingBottom = `${(1 / this.aspectRatio) * 100}%`;
            this.placeholderBorderStyle = `1px solid ${this.placeholderColor}`;
            this.initialIndex = 0;

            // --- Create the main grid container element (Internal) ---
            this.gridContainer = document.createElement('div');

            // --- Initial setup using globals ---
            this._initializeDOM();
        }

        /**
        * Appends the gridContainer to the global parent.
        * @private
        */
        _initializeDOM() {
            this.parent.appendChild(this.gridContainer);
            this.gridContainer.style.display = 'none';
            this.gridContainer.style.position = 'relative';
            this.gridContainer.style.overflow = 'auto'; // Should be auto or scroll
            this.gridContainer.style.height = '100%'; // Take full overlay height
            this.gridContainer.style.width = '100%';
            this.gridContainer.style.gap = `${this.minMargin}px`;
            this.gridContainer.style.boxSizing = 'border-box'; // Include padding/border in size
            // this.gridContainer.style.padding = `${this.minMargin}px`; // Add padding
        }

        /**
         * Handles the click event on a grid item.
         * @param {number} index - The index of the clicked image.
         * @private
         */
        _handleGridItemClick(index) {
            if (!imageViewer) {
                console.error("Global 'imageViewer' does not exist.");
                return;
            }

            console.log(`GridView: Clicked index ${index}`);

            this.stopLoading(); // Stop background loading

            imageViewer.loadAndShowIndex(index) // Prioritize loading clicked item
                .catch(err => {
                    console.error(`Error loading image on click for index ${index}:`, err);
                })
                .finally(() => {
                    if (this.disableOnImageSelect) {
                        console.log(`GridView: Disabling grid view`);
                        this.disable();
                    }
                });
        }

        /**
         * Cleans up observers/queues.
         */
        stopLoading() {
            if (!this.loadingEnabled) return;

            console.log("GridView: Stopping loading and disconnecting observer");
            this.loadingEnabled = false;

            if (this.gridIntersectionObserver) {
                this.gridIntersectionObserver.disconnect();
                // Ensure all observed items are unobserved if observer is reused later
                this.gridContainer.querySelectorAll('[data-index]').forEach(item => {
                    try {
                        this.gridIntersectionObserver.unobserve(item);
                    } catch (e) { /* Ignore errors if already unobserved */ }
                });
            }
            this.itemsToLoadQueue.length = 0;
            this.gridViewIsLoading = false;
            // Optionally reset 'queued'/'loading' statuses back to 'pending'
            // this.loadingStatus.forEach((status, index) => {
            //     if (status === 'queued' || status === 'loading') {
            //         this.loadingStatus.set(index, 'pending');
            //     }
            // });
        }

        /**
         * Checks if a full image is available for a grid item and updates it if necessary.
         * @param {HTMLElement} gridItem The grid item element.
         * @param {number} index The index of the grid item.
         * @private
         */
        _checkAndApplyFullImage(gridItem, index) {
            if (!this.config.fetchFullImages) return; // Only proceed if configured to fetch full images

            const thumb = thumbs[index];
            const currentStatus = this.loadingStatus.get(index) || 'pending';

            // Check if full image URL exists and the item isn't already loaded/failed or currently loading one
            if (thumb && thumb.fullImageUrl && currentStatus !== 'loaded' && currentStatus !== 'failed' && currentStatus !== 'loading') {
                // Check if the gridItem does *not* already have an <img> element (the full image)
                if (!gridItem.querySelector('img')) {
                    console.log(`GridView [${index}]: Found available full image during enableLoading. Updating.`);
                    this._updateGridItemImage(gridItem, index); // This will handle showing the full image
                }
            }
        }

        /**
         * Checks if a grid item element is currently visible within the relevant scroll container.
         * @param {HTMLElement} element The grid item element to check.
         * @returns {boolean} True if the element is at least partially visible, false otherwise.
         * @private
         */
        _isElementVisible(element) {
            if (!element || !element.isConnected) {
                return false;
            }

            const rect = element.getBoundingClientRect();

            if (this.observeViaViewport) {
                // Check against viewport
                const viewHeight = window.innerHeight || document.documentElement.clientHeight;
                const viewWidth = window.innerWidth || document.documentElement.clientWidth;

                // Check if vertical bounds intersect
                const vertInView = (rect.top <= viewHeight) && ((rect.top + rect.height) >= 0);
                // Check if horizontal bounds intersect
                const horzInView = (rect.left <= viewWidth) && ((rect.left + rect.width) >= 0);

                return vertInView && horzInView;
            } else {
                // Check against gridContainer
                if (!this.gridContainer) return false; // Should not happen if element exists

                const containerRect = this.gridContainer.getBoundingClientRect();

                // Check if vertical bounds intersect (relative to container)
                const vertInView = (rect.bottom > containerRect.top) && (rect.top < containerRect.bottom);
                // Check if horizontal bounds intersect (relative to container)
                const horzInView = (rect.right > containerRect.left) && (rect.left < containerRect.right);

                return vertInView && horzInView;
            }
        }

        /**
         * Iterates through grid items, checks which are currently visible,
         * and updates those that are in 'pending' state and have thumbnail data available.
         * @private
         */
        _updateVisiblePendingItems() { // Currently unused. Kept around just in case.
            // console.log("GridView: Running _updateVisiblePendingItems check.");
            let totalUpdated = 0;
            this.gridContainer.querySelectorAll('[data-index]').forEach(gridItem => {
                const index = parseInt(gridItem.dataset.index, 10);
                if (isNaN(index)) return;

                const currentStatus = this.loadingStatus.get(index) || 'pending';
                const thumb = thumbs.get(index); // Use .get() for safety

                // Update only if: Pending status + Has thumb background + Currently visible
                if (currentStatus === 'pending' && thumb?.background && this._isElementVisible(gridItem)) {
                    // console.log(`GridView [_updateVisiblePendingItems]: Updating visible pending item ${index}.`);
                    this._updateGridItemImage(gridItem, index); // This will update visuals and potentially status/observer
                    totalUpdated++;
                }
            });
            console.log("GridView: _updateVisiblePendingItems: Checked", totalUpdated, "items.");
        }

        /**
        * Reenables image loading after stopLoading was called.
        */
        enableLoading() {
            if (this.loadingEnabled) return;
            console.log("GridView: Enabling loading and re-observing grid items");

            this._ensureObserverInitialized();
            this.loadingEnabled = true;

            this.gridContainer.querySelectorAll('[data-index]').forEach(gridItem => {
                const index = parseInt(gridItem.dataset.index, 10);
                if (isNaN(index)) return;

                const currentStatus = this.loadingStatus.get(index) || 'pending';
                const isFinalStatus = currentStatus === 'loaded' || currentStatus === 'failed';

                if (!isFinalStatus) {
                    if (this.gridIntersectionObserver) {
                        try {
                            this.gridIntersectionObserver.observe(gridItem);
                        } catch (e) {
                            console.warn(`GridView: Error observing item ${index} during enableLoading:`, e);
                        }
                    }
                } // No need to explicitly unobserve final items
            });

            // Update currently visible items that might have received thumb data
            // requestAnimationFrame(() => this._updateVisiblePendingItems());
        }

        /**
         * Hides the grid view and cleans up observers/queues.
         */
        disable() {
            if (this.disabled) return;
            console.log("GridView: Disabling");
            this.gridContainer.style.display = 'none';
            this.stopLoading();
            this.disabled = true;
        }

        /**
         * Gets whether the grid view is currently visible.
         */
        isShown() {
            return this.gridContainer.style.display !== 'none';
        }

        /**
         * Forces the grid item at the specified index to display its full-resolution image.
         * This will fetch the full image data if necessary, even if `config.fetchFullImages` is false.
         * It updates the item's visual state and loading status accordingly.
         *
         * @param {number} index The index of the image item to update.
         * @returns {Promise<void>} A promise that resolves when the update attempt is complete (either success or failure).
         * @public
         */
        async forceSetFullImage(index) {
            if (typeof index !== 'number' || index < 0 || index >= thumbs.length) {
                console.warn(`GridView: forceSetFullImage: Invalid index ${index}.`);
                return;
            }

            const gridItem = this.gridContainer.querySelector(`[data-index="${index}"]`);
            if (!gridItem) {
                console.warn(`GridView: forceSetFullImage: Could not find grid item element for index ${index}. Grid might not be visible or item not rendered.`);
                return;
            }

            let thumb = thumbs[index];
            const currentStatus = this.loadingStatus.get(index) || 'pending';

            // Check if the full image URL is already available
            if (!thumb || !thumb.fullImageUrl) {
                console.log(`GridView: forceSetFullImage [${index}]: Full image URL not found locally. Fetching data...`);
                try {
                    this.loadingStatus.set(index, 'loading');
                    await loadImageUrlAtIndex(index);
                    thumb = thumbs[index];
                    if (!thumb || !thumb.fullImageUrl) {
                        throw new Error(`Full image URL still missing after fetch attempt for index ${index}.`);
                    }
                    console.log(`GridView: forceSetFullImage [${index}]: Full image data fetched successfully.`);

                } catch (error) {
                    console.error(`GridView: forceSetFullImage [${index}]: Error fetching full image data:`, error);
                    this.loadingStatus.set(index, 'failed');
                    // Update visual to reflect failure (likely back to thumb/placeholder)
                    this._updateGridItemImage(gridItem, index);
                    if (this.gridIntersectionObserver) {
                        try { this.gridIntersectionObserver.unobserve(gridItem); } catch (e) { }
                    }
                    return; // Stop further processing
                }
            } else {
                // console.log(`GridView: forceSetFullImage [${index}]: Full image URL already available.`);
                // If URL exists but status isn't final, mark as loading before update
                if (currentStatus !== 'loaded' && currentStatus !== 'failed') {
                    this.loadingStatus.set(index, 'loading');
                }
            }

            // At this point, we expect thumb.fullImageUrl to exist.
            // Call _updateGridItemImage to handle the visual update.
            // This function will create/update the <img> element, set the src,
            // handle onload/onerror, update the status to 'loaded' or 'failed',
            // and unobserve the item via the observer.
            // console.log(`GridView: forceSetFullImage [${index}]: Triggering visual update with full image URL.`);
            this._updateGridItemImage(gridItem, index);
        }

        /**
         * Calculates the expected width of a single grid item based on the container's
         * current clientWidth, the number of columns, and the computed column gap.
         * @returns {number | null} The calculated width in pixels, or null if calculation is not possible
         *                          (e.g., container not ready).
         * @private
         */
        _calculateGridItemWidth() {
            if (!this.gridContainer || !this.gridContainer.isConnected) {
                console.warn("GridView: Cannot calculate item width, grid container not ready.");
                return null;
            }

            const container = this.gridContainer;
            const numColumns = this.config.numColumns;

            // Get the actual computed gap value
            const styles = window.getComputedStyle(container);
            const gapStyle = styles.columnGap; // Could be '10px', 'normal', etc.
            let gap = parseFloat(gapStyle);

            // Handle non-pixel gaps or 'normal' - fallback to configured minMargin
            // 'normal' usually computes to a specific value, but parseFloat fails.
            // If parseFloat returns NaN, use the class's minMargin as the intended gap.
            if (isNaN(gap)) {
                gap = this.minMargin;
            }

            const paddingLeft = parseFloat(styles.paddingLeft) || 0;
            const paddingRight = parseFloat(styles.paddingRight) || 0;

            // container.clientWidth includes horizontal padding
            const availableWidthForGrid = container.clientWidth - (paddingLeft + paddingRight);

            if (availableWidthForGrid <= 0) {
                console.warn(`GridView: Available width for grid (${availableWidthForGrid}) is <= 0 after accounting for padding. Item width will be 0.`);
                return 0;
            }

            // Calculate total gap space: (N-1) gaps for N columns
            // Use Math.max(0, ...) in case numColumns is 1 (no gaps)
            const totalGap = Math.max(0, numColumns - 1) * gap;

            // Calculate the width allocated to one '1fr' trac
            const calculatedWidth = (availableWidthForGrid - totalGap) / numColumns;

            if (calculatedWidth < 0) {
                return 0;
            }

            return calculatedWidth;
        }

        /**
         * Updates the grid item with the best available image: full image > thumbnail > placeholder.
         * Handles transitions more smoothly by showing thumb first if available.
         * @param {HTMLElement} gridItem - The grid item div.
         * @param {number} index - The index of the image.
         * @private
         */
        _updateGridItemImage(gridItem, index) {
            const thumb = thumbs[index];
            const currentStatus = this.loadingStatus.get(index) || 'pending';
            let thumbnailApplied = false;

            // --- Special Handling for Videos ---
            if (thumb && thumb.isVideo === true) {
                // console.log(`GridView [${index}]: Item identified as video. Applying placeholder.`);

                // Clear existing image/background
                const existingImg = gridItem.querySelector('img');
                if (existingImg) existingImg.remove();
                gridItem.style.background = 'none';

                // Apply placeholder styles
                gridItem.style.border = this.placeholderBorderStyle;
                gridItem.style.backgroundColor = this.placeholderColor;
                gridItem.style.opacity = '1'; // Ensure visible

                // Set final status and unobserve
                this.loadingStatus.set(index, 'loaded'); // Treat as loaded for observer purposes
                if (this.gridIntersectionObserver) {
                    try { this.gridIntersectionObserver.unobserve(gridItem); } catch (e) { /* Ignore */ }
                }
                return; // Stop further processing for video items
            }

            // --- Clear only the things we are definitely replacing ---
            const existingImg = gridItem.querySelector('img');
            if (existingImg) existingImg.remove();
            // Reset core styles that might interfere
            gridItem.style.background = 'none';
            gridItem.style.border = 'none';
            gridItem.style.backgroundColor = '';
            gridItem.style.opacity = '1'; // Ensure visible by default

            // --- Step 1: Try to Apply Thumbnail Background First ---
            if (thumb && typeof thumb === 'object' && thumb.background && typeof getThumbOffset === 'function') {
                try {
                    // console.log(`GridView [${index}]: Attempting to apply thumbnail background.`);
                    const currentWidth = this._calculateGridItemWidth();
                    const thumbOffset = getThumbOffset(thumb.background, currentWidth);
                    const scale = currentWidth / THUMB_WIDTH;
                    const currentPageIndex = imageIndexToPageIndex(index);
                    const thumbSpritesheetWidth = getSpritesheetWidthForPage(currentPageIndex);

                    // Apply background styles
                    gridItem.style.background = thumb.background;
                    gridItem.style.backgroundPosition = `${thumbOffset}px 0px`;
                    if (galleryUsesSpritesheets) {
                        gridItem.style.backgroundSize = `${thumbSpritesheetWidth * scale}px auto`;
                    } else {
                        gridItem.style.backgroundSize = `100% auto`;
                    }
                    thumbnailApplied = true;

                } catch (e) {
                    console.error(`GridView [${index}]: Error applying thumbnail background:`, e);
                    gridItem.style.background = 'none'; // Clear potentially broken background
                    thumbnailApplied = false; // Ensure flag reflects failure
                }
            }

            // --- Step 2: Try to Load Full Image (if URL exists and not failed) ---
            if (thumb && typeof thumb === 'object' && thumb.fullImageUrl && currentStatus !== 'failed') {
                // console.log(`GridView [${index}]: Found full image URL. Starting load overlaying thumbnail (if applied).`);

                const img = document.createElement('img');
                img.src = thumb.fullImageUrl;
                img.referrerPolicy = "no-referrer";
                img.alt = `Image ${index + 1}`;
                img.style.position = 'absolute';
                img.style.top = '0';
                img.style.left = '0';
                img.style.width = '100%';
                img.style.objectFit = 'cover';
                img.style.display = 'block';
                img.style.opacity = '0'; // Start hidden
                img.style.transition = 'opacity 0.3s ease-in-out';
                img.style.zIndex = '1'; // Ensure it's above background

                img.onload = () => {
                    // console.log(`GridView [${index}]: Full image loaded successfully.`);
                    gridItem.style.border = 'none'; // Ensure no placeholder border remains

                    // Start the fade-in transition for the full image
                    img.style.opacity = '1';

                    // Listener to remove background *after* fade completes, if a thumb was applied
                    if (thumbnailApplied) {
                        img.addEventListener('transitionend', () => {
                            // Check if opacity transition finished and it's fully opaque
                            if (img.style.opacity === '1') {
                                gridItem.style.background = 'none'; // Remove thumb background now
                                // console.log(`GridView [${index}]: Removed thumbnail background after full image fade-in.`);
                            }
                        }, { once: true }); // Ensure listener runs only once
                    }

                    // --- Status Update & Unobserve ---
                    this.loadingStatus.set(index, 'loaded');
                    if (this.gridIntersectionObserver) {
                        try { this.gridIntersectionObserver.unobserve(gridItem); } catch (e) { /* Ignore */ }
                    }
                };

                img.onerror = () => {
                    console.warn(`GridView [${index}]: Failed to load full image: ${thumb.fullImageUrl}`);
                    img.remove(); // Clean up the failed image element
                    this.loadingStatus.set(index, 'failed');

                    // If the thumbnail didn't apply OR failed earlier, we need the placeholder
                    if (!thumbnailApplied) {
                        // console.log(`GridView [${index}]: Full image failed, no thumbnail applied, falling back to placeholder.`);
                        gridItem.style.background = 'none';
                        gridItem.style.border = this.placeholderBorderStyle;
                        gridItem.style.backgroundColor = this.placeholderColor;
                    } else {
                        // console.log(`GridView [${index}]: Full image failed, leaving thumbnail background visible.`);
                        // Ensure border/bgcolor are not set if thumbnail is the fallback
                        gridItem.style.border = 'none';
                        gridItem.style.backgroundColor = '';
                    }

                    // Unobserve after failure
                    if (this.gridIntersectionObserver) {
                        try { this.gridIntersectionObserver.unobserve(gridItem); } catch (e) { /* Ignore */ }
                    }
                };

                // Append the image. It will sit transparently over the background (thumb or placeholder)
                // until it loads and fades in, or fails.
                gridItem.appendChild(img);

                // --- Step 3: Fallback to Placeholder if nothing else worked ---
            } else if (!thumbnailApplied) {
                // Apply placeholder only if no full image is being loaded AND no thumb was successfully applied
                // console.log(`GridView [${index}]: Applying placeholder (no full image or thumbnail applied).`);
                gridItem.style.background = 'none'; // Clear any previous background attempt
                gridItem.style.border = this.placeholderBorderStyle;
                gridItem.style.backgroundColor = this.placeholderColor;

                // Update status if it wasn't already set or finalized (and not 'failed' from img load)
                if (currentStatus !== 'failed' && currentStatus !== 'loading' && currentStatus !== 'queued' && currentStatus !== 'loaded') {
                    this.loadingStatus.set(index, 'pending');
                }

                // Keep observing if pending or potentially retryable failed state (unless full image just failed)
                // Note: Observing logic is primarily handled by the intersection handler now. We just ensure
                // the final state leads to unobserving elsewhere.
            }

            // --- Final Observer State Check ---
            // If the final state determined here is loaded or failed, ensure it's unobserved.
            // (This adds redundancy but ensures correctness if called outside intersection flow)
            const finalStatusCheck = this.loadingStatus.get(index);
            if ((finalStatusCheck === 'loaded' || finalStatusCheck === 'failed') && this.gridIntersectionObserver) {
                try { this.gridIntersectionObserver.unobserve(gridItem); } catch (e) { /* Ignore */ }
            }
        }

        /**
         * Efficiently removes an item from the queue.
         * @param {number} indexToRemove - The index to remove.
         * @private
         */
        _removeFromQueue(indexToRemove) {
            this.itemsToLoadQueue = this.itemsToLoadQueue.filter(index => index !== indexToRemove);
        }

        /**
         * After loading an image, checks neighbors for newly available thumbnails and updates them.
         * @param {number} centerIndex - The index of the image that was just loaded.
         * @private
         */
        /**
         * Updates grid items on a specific page, typically after thumbnails have been fetched.
         * Only updates items that are currently placeholders.
         * @param {number} pageIndex - The index of the page whose items should be updated.
         * @private
         */
        _updateThumbnailsForPage(pageIndex) {
            const startIndex = pageIndex * PAGINATION;
            const endIndex = Math.min(startIndex + PAGINATION, thumbs.length);
            // console.log(`GridView: Updating thumbnails visibility for page ${pageIndex} [${startIndex}-${endIndex-1}]`);

            for (let i = startIndex; i < endIndex; i++) {
                const thumb = thumbs[i];
                const gridItem = this.gridContainer.querySelector(`[data-index="${i}"]`);

                // Check if item exists, has a thumb now, and doesn't already have a full image or background image set
                if (gridItem && thumb && thumb.background) {
                    const hasFullImg = gridItem.querySelector('img');
                    const hasThumbBg = gridItem.style.backgroundImage && gridItem.style.backgroundImage !== 'none';
                    if (!hasFullImg && !hasThumbBg) {
                        // console.log(`GridView: Applying newly fetched thumbnail to item ${i}`);
                        this._updateGridItemImage(gridItem, i); // Will apply the thumbnail
                    }
                }
            }
        }

        _updateNeighborThumbnails(centerIndex) {
            let minUpdated = centerIndex;
            let maxUpdated = centerIndex;
            let updated = false;
            const maxIndex = thumbs.length - 1; // Cache length

            // Check backwards
            for (let i = centerIndex - 1; i >= 0; i--) {
                const thumb = thumbs[i];
                if (!thumb) break; // Reached edge of loaded thumb data for this page
                const status = this.loadingStatus.get(i);
                if (status === 'loaded' || status === 'failed' || (thumb && thumb.fullImageUrl)) {
                    break; // Already has full image or failed, stop checking further back
                }


                if (thumb.background) {
                    const gridItem = this.gridContainer.querySelector(`[data-index="${i}"]`);
                    // Only update if the item exists and doesn't already have an <img> (full image) or a background image (thumb)
                    if (gridItem && !gridItem.querySelector('img') && (!gridItem.style.backgroundImage || gridItem.style.backgroundImage === 'none')) {
                        // console.log(`GridView: Updating neighbor ${i} with thumbnail.`);
                        this._updateGridItemImage(gridItem, i); // Will apply the thumb
                        minUpdated = i;
                        updated = true;
                    }
                }
            }

            // Check forwards
            for (let i = centerIndex + 1; i <= maxIndex; i++) {
                const thumb = thumbs[i];
                if (!thumb) break; // Reached edge of loaded thumb data for this page
                const status = this.loadingStatus.get(i);
                if (status === 'loaded' || status === 'failed' || (thumb && thumb.fullImageUrl)) {
                    break; // Already has full image or failed, stop checking further forward
                }

                if (thumb.background) {
                    const gridItem = this.gridContainer.querySelector(`[data-index="${i}"]`);
                    if (gridItem && !gridItem.querySelector('img') && (!gridItem.style.backgroundImage || gridItem.style.backgroundImage === 'none')) {
                        // console.log(`GridView: Updating neighbor ${i} with thumbnail.`);
                        this._updateGridItemImage(gridItem, i); // Will apply the thumb
                        maxUpdated = i;
                        updated = true;
                    }
                }
            }

            if (updated && minUpdated <= maxUpdated && !(minUpdated === centerIndex && maxUpdated === centerIndex)) {
                // Log only if actual neighbors were updated
                console.log(`GridView: Found/updated thumbnails for neighbors in range [${minUpdated}, ${maxUpdated}] around loaded index ${centerIndex}.`);
            }
        }


        /**
         * Processes the queue of items needing their full image URL loaded.
         * @private
         */
        async _processLoadQueue() {
            // Check gridViewIsLoading BEFORE checking queue length for slightly faster exit
            if (this.gridViewIsLoading) return;
            if (this.itemsToLoadQueue.length === 0) return;
            if (!this.loadingEnabled) return;

            this.gridViewIsLoading = true;
            const indexToLoad = this.itemsToLoadQueue.shift(); // Get the next item

            // --- Crucial Check: Is it still relevant? ---
            const currentStatus = this.loadingStatus.get(indexToLoad);
            if (currentStatus !== 'queued') {
                // console.log(`GridView: Skipping load for index ${indexToLoad}, status is now ${currentStatus}.`);
                this.gridViewIsLoading = false;
                // Process next item if queue isn't empty
                if (this.itemsToLoadQueue.length > 0) requestAnimationFrame(() => this._processLoadQueue());
                return;
            }

            // Check if grid item exists *before* async call
            const gridItem = this.gridContainer.querySelector(`[data-index="${indexToLoad}"]`);
            if (!gridItem) {
                console.warn(`GridView: Could not find grid item DOM element for index ${indexToLoad} before starting load.`);
                this.loadingStatus.set(indexToLoad, 'failed'); // Mark as failed if DOM element is gone
                this.gridViewIsLoading = false;
                if (this.itemsToLoadQueue.length > 0) requestAnimationFrame(() => this._processLoadQueue());
                return;
            }

            try {
                // console.log(`GridView: Processing queue for index ${indexToLoad}`);
                this.loadingStatus.set(indexToLoad, 'loading'); // Mark as loading *before* async call

                const pageIndex = getPageIndexByImageIndex(indexToLoad);
                if (pageIndex === null || pageIndex === undefined) {
                    throw new Error(`Could not determine page index for image index ${indexToLoad}`);
                }

                if (this.config.fetchFullImages) {
                    // --- Fetch Full Image Data (and neighbor thumbs) ---
                    if (typeof loadImageUrlAtIndex !== 'function') {
                        throw new Error("Global function 'loadImageUrlAtIndex' is not defined.");
                    }

                    await loadImageUrlAtIndex(indexToLoad, false); // Fetch full image data

                    // Re-select item after await, it might have been removed/changed
                    const currentGridItem = this.gridContainer.querySelector(`[data-index="${indexToLoad}"]`);
                    if (currentGridItem) {
                        // This call will attempt to display the full image if available
                        this._updateGridItemImage(currentGridItem, indexToLoad);
                    } else {
                        console.warn(`GridView: Grid item for index ${indexToLoad} disappeared before image update after full load.`);
                        // Status was 'loading'. Check thumb data to finalize status if item vanished.
                        const thumb = thumbs[indexToLoad];
                        if (thumb && thumb.fullImageUrl) {
                            // Only update status if it didn't already fail during load/update
                            if (this.loadingStatus.get(indexToLoad) !== 'failed') this.loadingStatus.set(indexToLoad, 'loaded');
                        } else if (this.loadingStatus.get(indexToLoad) === 'loading') {
                            this.loadingStatus.set(indexToLoad, 'failed'); // Only mark failed if it wasn't already 'failed' or didn't become 'loaded'
                        }
                    }
                    // Update neighbors which might have got thumbs from loadImageUrlAtIndex
                    this._updateNeighborThumbnails(indexToLoad);

                } else {
                    // --- Fetch Only Thumbnail Data for the Page ---
                    if (typeof getPageIndexByImageIndex !== 'function' || typeof fetchAndPopulateThumbsOnPage !== 'function') {
                        throw new Error("Required global functions for thumbnail fetching are missing.");
                    }

                    console.log(`GridView: Fetching only thumbs for page ${pageIndex} (triggered by index ${indexToLoad})`);
                    // Note: fetchAndPopulateThumbsOnPage locks per page index, so it's okay to call it multiple times on the same page
                    await fetchAndPopulateThumbsOnPage(pageIndex);

                    // Re-select item after await
                    const currentGridItem = this.gridContainer.querySelector(`[data-index="${indexToLoad}"]`);
                    if (currentGridItem) {
                        // Update item - will now show thumbnail if available, or placeholder
                        this._updateGridItemImage(currentGridItem, indexToLoad);
                    } else {
                        console.warn(`GridView: Grid item for index ${indexToLoad} disappeared before image update after thumb fetch.`);
                        // Even if item is gone, the thumb data might be useful for others
                    }

                    // Update neighbors on the same page which might now have thumbs
                    this._updateNeighborThumbnails(indexToLoad);

                    // Mark as 'loaded' because the requested *thumbnail* fetch is complete.
                    // The observer should stop observing this item now.
                    // Only set to loaded if it didn't fail during the update process somehow
                    if (this.loadingStatus.get(indexToLoad) === 'loading') {
                        this.loadingStatus.set(indexToLoad, 'loaded');
                        // If we marked it loaded, ensure it's unobserved
                        if (currentGridItem && this.gridIntersectionObserver) {
                            try { this.gridIntersectionObserver.unobserve(currentGridItem); } catch (e) { }
                        }
                    }
                }

            } catch (error) {
                console.error(`GridView: Error calling loadImageUrlAtIndex for index ${indexToLoad}:`, error);
                this.loadingStatus.set(indexToLoad, 'failed');
                // Re-select item before trying to update its state after error
                const currentGridItem = this.gridContainer.querySelector(`[data-index="${indexToLoad}"]`);
                if (currentGridItem) {
                    // Update to show thumb/placeholder on error
                    this._updateGridItemImage(currentGridItem, indexToLoad);
                    // Unobserve on catch failure (if observer exists)
                    if (this.gridIntersectionObserver) {
                        try { this.gridIntersectionObserver.unobserve(currentGridItem); } catch (e) { }
                    }
                }
            } finally {
                this.gridViewIsLoading = false;
                if (this.itemsToLoadQueue.length > 0) {
                    requestAnimationFrame(() => this._processLoadQueue());
                }
            }
        }

        /**
         * The callback function for the IntersectionObserver.
         * Handles both entering and leaving the viewport.
         * @param {IntersectionObserverEntry[]} entries
         * @param {IntersectionObserver} observer
         * @private
         */
        _handleIntersection(entries, observer) {
            entries.forEach(entry => {
                const gridItem = entry.target;
                const index = parseInt(gridItem.dataset.index, 10);

                if (isNaN(index)) {
                    console.warn("GridView: Could not determine index for intersecting grid item.", gridItem);
                    try { observer.unobserve(gridItem); } catch (e) { } // Unobserve problematic items
                    return;
                }

                if (entry.isIntersecting) {
                    // --- Item is entering the viewport ---

                    // **Step 1: Update visual based on current data**
                    // This applies thumb/placeholder/existing full image immediately.
                    // It also updates status if full image loads/fails synchronously or is already known.
                    // It also handles unobserving if status becomes loaded/failed.
                    this._updateGridItemImage(gridItem, index);

                    // **Step 2: Check if further network loading is needed**
                    const updatedStatus = this.loadingStatus.get(index) || 'pending'; // Get status *after* update attempt
                    const thumb = thumbs[index];
                    const hasFullImage = thumb && thumb.fullImageUrl; // We might not need this specific check now
                    const isFinalStatus = updatedStatus === 'loaded' || updatedStatus === 'failed';

                    // Needs network action if: status isn't final AND (we need full images OR thumb data might be missing)
                    const needsNetworkAction = !isFinalStatus && (this.config.fetchFullImages || !thumb?.background);

                    if (needsNetworkAction) {
                        const pageIndex = getPageIndexByImageIndex(index);

                        // --- Independent Thumbnail Fetch Trigger ---
                        if (pageIndex !== null && pageIndex !== undefined) {
                            const firstThumbIndexOnPage = pageIndex * PAGINATION;
                            // Check if thumbs for this page are likely missing and not already being fetched.
                            // A simple check on the first item of the page is usually sufficient.
                            const thumbsLikelyMissing = !thumbs.get(firstThumbIndexOnPage); // Using .get() for robustness

                            if (thumbsLikelyMissing && !this.fetchingThumbPages.has(pageIndex)) {
                                console.log(`GridView: Item ${index} intersected. Triggering thumb fetch for page ${pageIndex}.`);
                                this.fetchingThumbPages.add(pageIndex);
                                fetchAndPopulateThumbsOnPage(pageIndex)
                                    .then(() => {
                                        console.log(`GridView: Thumb fetch for page ${pageIndex} completed.`);
                                        // Update visuals for items on this page that might now have thumbnails
                                        this._updateThumbnailsForPage(pageIndex);
                                    })
                                    .catch(err => {
                                        console.error(`GridView: Thumb fetch for page ${pageIndex} failed:`, err);
                                    })
                                    .finally(() => {
                                        this.fetchingThumbPages.delete(pageIndex);
                                    });
                            }
                        }
                        // --- End Independent Thumbnail Fetch ---

                        // --- Full Image Queueing (if applicable) ---
                        // Queue the specific item for full image loading if fetchFullImages is true.
                        // This happens regardless of the separate thumbnail fetch.
                        // console.log(`GridView: Intersecting item index ${index} (Status: ${currentStatus}). Adding to full image queue.`);
                        this.itemsToLoadQueue.push(index);
                        this.loadingStatus.set(index, 'queued');
                        this._processLoadQueue(); // Start processing queue if not already running
                    }
                    // Case: Item is intersecting, doesn't need a *network* load (full image URL exists),
                    // but hasn't reached a final visual state ('loaded'/'failed').
                    // This forces an update check if, e.g., the full image arrived via another mechanism (main viewer load)
                    // while this item was not visible or being processed.
                    else if (hasFullImage && !isFinalStatus) {
                        // console.log(`GridView [${index}]: Intersecting with existing full image data. Forcing update.`);
                        this._updateGridItemImage(gridItem, index);
                    }
                    // ---- END ADDED LOGIC ----
                } else {
                    // --- Item is leaving the viewport ---
                    const currentStatus = this.loadingStatus.get(index) || 'pending';
                    if (currentStatus === 'queued') {
                        // console.log(`GridView: Item index ${index} left viewport while queued. Dequeuing.`);
                        this._removeFromQueue(index);
                        this.loadingStatus.set(index, 'pending');
                    }
                    // Keep observing otherwise; it will be unobserved on load/fail.
                }
            });
        }


        /**
         * Creates a single grid item element, styles it, adds listeners.
         * @param {number} index - The index of the image.
         * @returns {HTMLElement} The configured grid item element.
         * @private
         */
        _createGridItem(index) {
            const gridItem = document.createElement('div');
            gridItem.style.position = 'relative';
            gridItem.style.overflow = 'hidden';
            gridItem.style.width = '100%';
            gridItem.style.height = '0';
            gridItem.style.paddingBottom = this.itemPaddingBottom;
            gridItem.style.cursor = 'pointer';
            gridItem.style.boxSizing = 'border-box';
            gridItem.dataset.index = index;

            // Set initial placeholder state directly
            gridItem.style.backgroundColor = this.placeholderColor;
            gridItem.style.border = this.placeholderBorderStyle;

            if (this.config.showPageNumbers) {
                const indexLabel = document.createElement('span');
                indexLabel.textContent = index + 1;
                indexLabel.style.position = 'absolute';
                indexLabel.style.top = '2px';
                indexLabel.style.left = '2px';
                indexLabel.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
                indexLabel.style.color = 'white';
                indexLabel.style.padding = '3px 6px';
                indexLabel.style.fontSize = '14px';
                indexLabel.style.borderRadius = '4px';
                indexLabel.style.fontWeight = 'bold';
                indexLabel.style.zIndex = '10'; // Above image/thumb background but below potential UI elements
                indexLabel.style.pointerEvents = 'none';
                gridItem.appendChild(indexLabel);
            }

            gridItem.addEventListener('click', () => this._handleGridItemClick(index));

            return gridItem;
        }


        /**
         * Prepares the grid container for display.
         * @private
         */
        _prepareGridContainer() {
            this.gridContainer.innerHTML = ''; // Clear previous items

            // Disconnect observer, clear queue, reset loading flag
            if (this.gridIntersectionObserver) {
                this.gridIntersectionObserver.disconnect();
                this.gridContainer.querySelectorAll('[data-index]').forEach(item => {
                    try {
                        this.gridIntersectionObserver.unobserve(item);
                    } catch (e) { /* Ignore errors if already unobserved */ }
                });
            }
            this.itemsToLoadQueue.length = 0;
            this.gridViewIsLoading = false;

            this.loadingStatus.clear(); // Clear status map for fresh build
            this._cachedGridItemRect = null; // Invalidate cached dimensions

            // Re-initialize observer if it was destroyed or null
            if (!this.gridIntersectionObserver) {
                this._ensureObserverInitialized();
            } else {
                // If observer exists, ensure it's disconnected before adding new items
                this.gridIntersectionObserver.disconnect();
            }

            this.gridContainer.style.display = 'grid';
            this.gridContainer.style.gridTemplateColumns = `repeat(${this.config.numColumns}, 1fr)`;
        }

        /**
         * Initializes intersection observer if needed.
         * @private
         */
        _ensureObserverInitialized() {
            if (!this.gridIntersectionObserver) {
                const observerRoot = this.observeViaViewport ? null : this.gridContainer;
                const observerOptions = {
                    root: observerRoot,
                    rootMargin: this.observerRootMargin,
                    threshold: 0
                };
                this.gridIntersectionObserver = new IntersectionObserver(
                    this._handleIntersection.bind(this),
                    observerOptions
                );
            }
        }

        /**
        * Scrolls the grid view to the specified image index.
        * @param {number} index - The index of the image to scroll to.
        * @param {boolean} [highlight=false] - Whether to temporarily highlight the scrolled-to item.
        */
        scrollToIndex(index, highlight = true) {
            if (index === null || index < 0 || index >= thumbs.length) {
                console.warn(`GridView: scrollToIndex: Invalid index ${index}.`);
                return;
            }
            // Ensure scrolling happens after the DOM might have updated
            requestAnimationFrame(() => {
                const targetItem = this.gridContainer.querySelector(`[data-index="${index}"]`);
                if (targetItem) {
                    console.log(`GridView: Scrolling to index ${index}`);
                    targetItem.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' });

                    // --- Add Highlighting Logic ---
                    if (highlight) {
                        const fadeInDuration = 200; // ms for fade-in
                        const holdDuration = 800; // ms to keep highlight fully visible
                        const fadeOutDuration = 800; // ms for fade-out
                        // Brighter white-yellow glow, more spread
                        const highlightBoxShadow = '0 0 30px 10px rgba(255, 255, 200, 1.0)';

                        // Function to clean up styles and timeouts for a specific item
                        const cleanupHighlight = (item) => {
                            if (item._highlightTimeout) clearTimeout(item._highlightTimeout);
                            if (item._cleanupTimeout) clearTimeout(item._cleanupTimeout);
                            item.style.removeProperty('outline');
                            item.style.removeProperty('box-shadow');
                            item.style.removeProperty('transition');
                            item.style.removeProperty('z-index'); // Clean up z-index
                            delete item._highlightTimeout; // Clean up custom property
                            delete item._cleanupTimeout; // Clean up custom property
                        };

                        // Clean up any previous highlight effect on this item immediately
                        cleanupHighlight(targetItem);

                        // Ensure item is visible above others if overlapping during scroll/highlight
                        targetItem.style.zIndex = '2';

                        // Set initial transition for fade-in
                        targetItem.style.transition = `box-shadow ${fadeInDuration}ms ease-in`;
                        // Ensure initial state (no shadow) is registered before applying the highlight
                        targetItem.style.boxShadow = '';

                        // Use requestAnimationFrame to apply the highlight *after* the initial styles are set
                        requestAnimationFrame(() => {
                            requestAnimationFrame(() => { // Double RAF for robustness in some browsers
                                targetItem.style.boxShadow = highlightBoxShadow; // Apply highlight, starts fade-in

                                // Set timeout to start the fade-out after fade-in + hold duration
                                targetItem._highlightTimeout = setTimeout(() => {
                                    // Change transition for fade-out
                                    targetItem.style.transition = `box-shadow ${fadeOutDuration}ms ease-out`;
                                    targetItem.style.boxShadow = ''; // Start fade-out

                                    // Set another timeout to clean up styles *after* the fade-out completes
                                    targetItem._cleanupTimeout = setTimeout(() => {
                                        cleanupHighlight(targetItem); // Use the cleanup function
                                    }, fadeOutDuration);
                                }, fadeInDuration + holdDuration); // Wait for fade-in and hold
                            });
                        });
                    }
                    // --- End Highlighting Logic ---
                } else {
                    console.warn(`GridView: scrollToIndex: Could not find grid item for index ${index} to scroll to.`);
                }
            });
        }

        /**
        * Displays the grid view, creating items and setting up observers.
        * @param {number | null} [initialIndex=null] - Optional index to scroll into view.
        */
        showGridView(initialIndex = null) {
            console.log(`GridView: showGridView called, initialIndex: ${initialIndex}`);
            this.showCalled = true;
            this.loadingEnabled = true;
            this.initialIndex = initialIndex;
            const currentTotalImages = thumbs.length;

            this._prepareGridContainer(); // Sets up container, observer, clears state

            if (currentTotalImages > 0) {
                this.gridContainer.style.display = 'grid'; // Ensure display is grid

                let itemsCreated = 0;
                for (let i = 0; i < currentTotalImages; i++) {
                    const thumb = thumbs[i];
                    // --- Skip deleted items ---
                    if (thumb === 'deleted') {
                        continue;
                    }

                    // Always set initial status to pending
                    this.loadingStatus.set(i, 'pending');

                    const gridItem = this._createGridItem(i); // Creates item and sets initial visual

                    this.gridContainer.appendChild(gridItem);
                    itemsCreated++;

                    // Always observe the item initially. The intersection handler will manage updates.
                    if (this.gridIntersectionObserver) {
                        this.gridIntersectionObserver.observe(gridItem);
                    } else {
                        console.warn("GridView: Observer not initialized when trying to observe item", i);
                    }
                }

                // Initial update for items already visible after grid creation
                requestAnimationFrame(() => {
                    // this._updateVisiblePendingItems();

                    // Scroll after the potential initial visible item update
                    if (initialIndex !== null && initialIndex >= 0 && initialIndex < currentTotalImages) {
                        this.scrollToIndex(initialIndex, false);
                    }
                });

            } else {
                this.gridContainer.innerHTML = '<p style="text-align: center; grid-column: 1 / -1; color: #555; padding: 20px;">No images to display.</p>';
                this.gridContainer.style.display = 'block'; // Use block for the message
            }

            this.disabled = false;
        }

        /**
         * Scrolls the grid container by the specified amount.
         * @param {number|ScrollToOptions} x The number of pixels to scroll horizontally, or a ScrollToOptions
                                             object
         * @param {number} [y] - The number of pixels to scroll vertically (if x is a number)
         */
        scrollBy(x, y) {
            if (typeof y === 'number') {
                this.gridContainer.scrollBy(x, y);
            } else {
                this.gridContainer.scrollBy(x);
            }
        }

        /**
         * Returns the grid container element.
         * @returns {HTMLElement} The grid container element
         */
        getContainer() {
            return this.gridContainer;
        }

        /**
         * Refreshes all currently displayed grid items.
         * It checks if any item in the `thumbs` collection has been marked as "deleted"
         * and removes the corresponding grid item if found.
         * For items that are not deleted, it updates their visual representation based
         * on the latest available data (thumbnail or full image) and configuration,
         * essentially calling `_updateGridItemImage` on them.
         * Note: This function currently only updates or removes existing items. It does
         * not add new grid items if the `thumbs` collection grew since the last show.
         * For adding new items, consider calling `showGridView` again.
         */
        refreshAll() {
            console.log("GridView: Refreshing all grid items.");
            const items = Array.from(this.gridContainer.querySelectorAll('[data-index]'));

            items.forEach(gridItem => {
                const index = parseInt(gridItem.dataset.index, 10);
                if (isNaN(index)) {
                    console.warn("GridView [refreshAll]: Found grid item with invalid index.", gridItem);
                    return; // Skip this item
                }

                const thumb = thumbs.get(index); // Use .get() for safety if thumbs is proxy/collection

                if (thumb === 'deleted') {
                    console.log(`GridView [refreshAll]: Item ${index} marked as deleted. Removing grid item.`);
                    // Unobserve before removing
                    if (this.gridIntersectionObserver) {
                        try { this.gridIntersectionObserver.unobserve(gridItem); } catch (e) { /* Ignore */ }
                    }
                    gridItem.remove();
                    this.loadingStatus.delete(index);
                } else {
                    // Item is not deleted, update its visual state
                    // console.log(`GridView [refreshAll]: Updating item ${index}.`);
                    // _updateGridItemImage handles checking thumb/fullImage and applying appropriate visual
                    // It also manages observer status internally.
                    this._updateGridItemImage(gridItem, index);
                }
            });
        }
    }
    /**
     * @typedef {object} MediaResult
     * @property {string} url - The final extracted media URL.
     * @property {string} type - The determined media type ('image', 'video', or a custom type from config).
     * @property {string} sourceUrl - The URL of the page where this media item was found.
     * @property {number} depth - The recursion depth at which this was found (0 is the initial page).
     * @property {Element} [element] - The specific element (img, video, etc.) if extracted directly (might be null/undefined if from deep recursion context).
     */

    /**
     * @typedef {object} CustomSelectorConfig
     * @property {string} selector - The CSS selector or XPath expression.
     * @property {'css' | 'xpath'} type - The type of the selector.
     * @property {string} attribute - Attribute for direct extraction OR attribute with the link URL (if following).
     * @property {string} [mediaType] - Optional: Explicitly define the type label (e.g., 'image', 'video', 'galleryThumb'). If omitted, the extractor attempts to auto-detect 'image' or 'video' based on element or URL; otherwise defaults to 'custom'. This label is applied *after* filtering.
     * @property {RegExp|null} [styleUrlRegex=null] - Optional regex for extracting URL from style attribute. Must contain one capturing group for the URL.
     * @property {boolean} [followLink=false] - If true, use 'attribute' value as URL and fetch recursively.
     * @property {Partial<ExtractorConfig>|null} [nextConfig=null] - Config for the page loaded *after* following the link. Required if followLink is true.
     * @property {Array<string>|null} [allowedMediaTypes=null] - Optional: If followLink is false, only extract if the determined media type (using _determineMediaType) is one of these (e.g., ['video']). If null, extract regardless of type.
     */

    /**
     * @typedef {object} ExtractorConfig
     * @property {boolean} [extractImages=true] - Whether to extract <img> elements on the current level.
     * @property {boolean} [extractVideos=true] - Whether to extract <video> elements on the current level.
     * @property {Array<CustomSelectorConfig>} [customSelectors=[]] - Array of custom selectors.
     * @property {number | null | undefined} [maxDepth] - Optional maximum recursion depth. If null/undefined/Infinity, depth is limited only by config structure and cycle detection. 0 means initial page only.
     * @property {number} [concurrency=5] - How many links to follow in parallel.
     */

    class MediaExtractor {

        /** @type {Required<ExtractorConfig>} */
        static DEFAULT_CONFIG = {
            extractImages: true,
            extractVideos: true,
            customSelectors: [],
            maxDepth: Infinity, // Default to no explicit depth limit
            concurrency: 5
        };

        // Common media file extensions for auto-detection
        static IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico', 'tif', 'tiff']);
        static VIDEO_EXTENSIONS = new Set(['mp4', 'webm', 'ogg', 'mov', 'avi', 'wmv', 'flv', 'mkv', 'm4v']);

        /**
         * Merges user config with defaults or parent config. Ensures 'maxDepth' is a non-negative number or Infinity.
         * Ignores 'maxDepth' properties within nested nextConfigs during resolution.
         * Inherits 'concurrency' from parentConfig if not specified in userConfig.
         * @param {Partial<ExtractorConfig>} [userConfig={}] The specific config for the current level (e.g., nextConfig).
         * @param {Required<ExtractorConfig>} [parentConfig=null] The resolved config of the parent level (null for the initial call).
         * @returns {Required<ExtractorConfig>} The fully resolved configuration for the current level.
         * @private
         */
        _resolveConfig(userConfig = {}, parentConfig = null) {
            // Start with defaults
            let resolved = { ...MediaExtractor.DEFAULT_CONFIG };

            // 1. Inherit concurrency from parent if userConfig doesn't specify it
            if (userConfig.concurrency === undefined || userConfig.concurrency === null || userConfig.concurrency <= 0) {
                if (parentConfig && parentConfig.concurrency > 0) {
                    resolved.concurrency = parentConfig.concurrency;
                }
                // Otherwise, it keeps the DEFAULT_CONFIG.concurrency
            } else {
                resolved.concurrency = userConfig.concurrency;
            }

            // 2. Merge top-level simple properties (excluding concurrency)
            if (userConfig.extractImages !== undefined) resolved.extractImages = userConfig.extractImages;
            if (userConfig.extractVideos !== undefined) resolved.extractVideos = userConfig.extractVideos;

            // 3. Handle maxDepth specifically (only initial top-level matters for limit)
            if (parentConfig === null) {
                if (userConfig.maxDepth !== undefined && userConfig.maxDepth !== null && userConfig.maxDepth >= 0) {
                    resolved.maxDepth = Number(userConfig.maxDepth);
                } else if (userConfig.hasOwnProperty('maxDepth')) {
                    console.warn(`MediaExtractor: Invalid or null top-level 'maxDepth' provided (${userConfig.maxDepth}). Defaulting to Infinity (no limit).`);
                    resolved.maxDepth = Infinity;
                }
                // Otherwise, it keeps default Infinity
            } else {
                // Inherit parent's resolved maxDepth for structural consistency
                resolved.maxDepth = parentConfig.maxDepth;
            }

            // 4. Merge customSelectors carefully
            resolved.customSelectors = (userConfig.customSelectors || []).map(cs => {
                // Basic structure with defaults
                const resolvedCs = {
                    // No default mediaType here; it's determined later or falls back to 'custom'
                    followLink: false,
                    nextConfig: null,
                    styleUrlRegex: null,
                    allowedMediaTypes: null, // <<< Renamed and default added
                    ...cs // User overrides
                };

                // Validate required fields
                if (!resolvedCs.selector || !resolvedCs.type || !resolvedCs.attribute) {
                    console.error("MediaExtractor: Custom selector is missing required fields (selector, type, attribute):", cs);
                    return null; // Mark for filtering
                }

                // Validate/prepare nextConfig if following link
                if (resolvedCs.followLink && !resolvedCs.nextConfig) {
                    console.warn(`MediaExtractor: Custom selector has followLink=true but no nextConfig. Recursion for this selector will use inherited/default config for the next level. Selector:`, resolvedCs.selector);
                    resolvedCs.nextConfig = {}; // Ensure object exists for next _resolveConfig call
                }

                // Validate styleUrlRegex if provided
                if (resolvedCs.styleUrlRegex && !(resolvedCs.styleUrlRegex instanceof RegExp)) {
                    console.error(`MediaExtractor: Invalid styleUrlRegex (must be a RegExp instance) for selector: ${resolvedCs.selector}`);
                    resolvedCs.styleUrlRegex = null; // Invalidate it
                }

                return resolvedCs;
            }).filter(cs => cs !== null); // Filter out invalid selectors

            return resolved;
        }


        /**
         * Fetches HTML content.
         * @param {string} url
         * @returns {Promise<string>}
         * @private
         */
        async _fetchHtml(url) {
            // In a real userscript, use GM_xmlhttpRequest or GM.xmlHttpRequest
            // For simulation/testing, you might use fetch if CORS allows or a proxy
            console.debug(`MediaExtractor: Fetching ${url}`);
            return new Promise((resolve, reject) => {
                // Assuming GM_xmlhttpRequest is available in the execution context
                if (typeof GM_xmlhttpRequest === 'undefined') {
                    console.error("MediaExtractor: GM_xmlhttpRequest is not available. Cannot fetch URL:", url);
                    return reject(new Error("GM_xmlhttpRequest is not defined. Ensure the script runs in a userscript manager environment (e.g., Tampermonkey, Greasemonkey) with appropriate @grant privileges."));
                }
                GM_xmlhttpRequest({
                    method: "GET",
                    url: url,
                    headers: { // Add some common headers to mimic a browser
                        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
                        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
                        "Accept-Language": "en-US,en;q=0.9",
                    },
                    timeout: 30000, // 30 second timeout
                    onload: function (response) {
                        if (response.status >= 200 && response.status < 400) { // Allow redirects (3xx) usually handled by browser/GM
                            resolve(response.responseText);
                        } else {
                            const errorDetails = response.responseText ? `: ${response.responseText.substring(0, 200)}` : '';
                            reject(new Error(`HTTP error! status: ${response.status} for ${url}${errorDetails}`));
                        }
                    },
                    onerror: function (response) {
                        reject(new Error(`Network error fetching ${url}: ${response.error || 'Unknown error'}`));
                    },
                    ontimeout: function () {
                        reject(new Error(`Request timed out for ${url}`));
                    },
                    onabort: function () {
                        reject(new Error(`Request aborted for ${url}`));
                    }
                });
            });
        }

        /**
         * Extracts a URL from a DOM element attribute or style.
         * @param {Element} element
         * @param {string} attributeName
         * @param {string} baseUrl
         * @param {RegExp|null} [styleUrlRegex=null] - Regex for style attribute extraction. Must have one capturing group.
         * @returns {string|null} The absolute URL or null.
         * @private
         */
        _extractUrlFromAttribute(element, attributeName, baseUrl, styleUrlRegex = null) {
            let rawUrl = null;

            if (attributeName.toLowerCase() === 'style' && styleUrlRegex instanceof RegExp) {
                const styleContent = element.getAttribute('style');
                if (styleContent) {
                    const match = styleContent.match(styleUrlRegex);
                    // Use the first capturing group (index 1)
                    if (match && match[1]) {
                        rawUrl = match[1].trim().replace(/&/g, '&'); // Decode entities just in case
                    }
                }
            } else {
                // Handle data-* attributes correctly
                if (attributeName.startsWith('data-')) {
                    // Convert kebab-case attribute name to camelCase dataset property name
                    const datasetName = attributeName.substring(5).replace(/-([a-z])/g, (g) => g[1].toUpperCase());
                    rawUrl = element.dataset[datasetName];
                    // Fallback to getAttribute if dataset property is undefined (less common but possible)
                    if (rawUrl === undefined) {
                        rawUrl = element.getAttribute(attributeName);
                    }
                } else {
                    rawUrl = element.getAttribute(attributeName);
                }
            }


            if (rawUrl) {
                rawUrl = rawUrl.trim();
                if (rawUrl) {
                    // Skip javascript:, mailto:, #fragment links specifically for 'href'
                    if (attributeName.toLowerCase() === 'href' && /^(javascript:|mailto:|#)/i.test(rawUrl)) {
                        // console.debug(`MediaExtractor: Skipping non-media link: ${rawUrl}`);
                        return null;
                    }
                    try {
                        // Resolve relative URLs against the base URL
                        return new URL(rawUrl, baseUrl).href;
                    } catch (e) {
                        // Ignore invalid URLs
                        console.warn(`MediaExtractor: Skipping invalid URL "${rawUrl}" found in attribute "${attributeName}" on element <${element.tagName.toLowerCase()}> on page ${baseUrl}`, e.message);
                        return null;
                    }
                }
            }
            return null;
        }

        /**
         * Helper to run async tasks with controlled concurrency.
         * @template T
         * @param {Array<() => Promise<T>>} taskFns - Array of functions returning promises.
         * @param {number} concurrency - Max tasks in parallel.
         * @returns {Promise<Array<T | {error: Error}>>} - Promise with results or error objects.
         * @private
         */
        async _runWithConcurrency(taskFns, concurrency) {
            const results = new Array(taskFns.length);
            let taskIndex = 0;
            const totalTasks = taskFns.length;
            const workers = [];
            const effectiveConcurrency = Math.max(1, Math.min(concurrency, totalTasks));

            for (let i = 0; i < effectiveConcurrency; i++) {
                workers.push((async () => {
                    while (taskIndex < totalTasks) {
                        const currentIndex = taskIndex++;
                        const taskFn = taskFns[currentIndex];

                        if (taskFn) {
                            try {
                                results[currentIndex] = await taskFn();
                            } catch (error) {
                                const err = (error instanceof Error) ? error : new Error(String(error || 'Unknown error in concurrent task'));
                                console.error(`MediaExtractor: Error in concurrent task index ${currentIndex}:`, err.message);
                                results[currentIndex] = { error: err }; // Store error marker
                            }
                        }
                    }
                })());
            }

            await Promise.all(workers);
            return results;
        }

        /**
         * Determines the media type based on element tag, URL extension, or falls back.
         * Respects the explicitly provided mediaType from the config.
         * @param {Element} element The source DOM element.
         * @param {string|null} url The extracted URL.
         * @param {string|undefined} [configMediaType] The mediaType specified in the CustomSelectorConfig.
         * @returns {string} The determined media type (e.g., 'image', 'video', 'custom', or configMediaType).
         * @private
         */
        _determineMediaType(element, url, configMediaType) {
            // 1. Explicit Override: If a mediaType is provided in the config, use it.
            if (configMediaType) {
                return configMediaType;
            }

            // 2. Element Type Check: Infer from common media tags.
            const tagName = element.tagName.toUpperCase();
            if (tagName === 'IMG') return 'image';
            if (tagName === 'VIDEO') return 'video';
            // Check <source> specifically if its parent is <video>
            if (tagName === 'SOURCE' && element.closest('video')) return 'video';
            // Could add checks for AUDIO, PICTURE > SOURCE etc. if needed

            // 3. URL Extension Check: Attempt to infer from the URL path.
            if (url) {
                try {
                    const parsedUrl = new URL(url);
                    // Get path without query string or hash
                    const pathname = parsedUrl.pathname;
                    // Extract the last part after '.'
                    const extensionMatch = pathname.match(/\.([^.?#]+)(?:[?#]|$)/);
                    if (extensionMatch && extensionMatch[1]) {
                        const ext = extensionMatch[1].toLowerCase();
                        if (MediaExtractor.IMAGE_EXTENSIONS.has(ext)) return 'image';
                        if (MediaExtractor.VIDEO_EXTENSIONS.has(ext)) return 'video';
                    }
                } catch (e) {
                    // Ignore URL parsing errors; cannot determine type from extension.
                    console.warn(`MediaExtractor: Could not parse URL for extension check: ${url}`, e.message);
                }
            }

            // 4. Fallback: If no type determined, default to 'custom'.
            return 'custom';
        }


        /**
         * Parses HTML, extracts direct media, and identifies tasks for recursive calls based on config structure.
         * @param {string} htmlString
         * @param {string} baseUrl
         * @param {Required<ExtractorConfig>} config - The configuration resolved for THIS level.
         * @param {number} currentDepth
         * @param {number} initialMaxDepth - The maxDepth value set at the START of the crawl (can be Infinity).
         * @param {Set<string>} visitedUrls - Set of URLs visited in the current crawl path.
         * @returns {Promise<{directResults: Array<MediaResult>, followTasks: Array<() => Promise<Array<MediaResult>>>}>}
         * @private
         */
        async _parseHtmlAndIdentifyTasks(htmlString, baseUrl, config, currentDepth, initialMaxDepth, visitedUrls) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(htmlString, 'text/html');
            const directResults = [];
            const followTasks = [];

            // Use a Map to associate elements with the rules that matched them
            // Key: Element, Value: Array<{source: 'img'|'video'|'custom', configIndex?: number}>
            const candidateElements = new Map();
            const addCandidate = (element, sourceInfo) => {
                if (!candidateElements.has(element)) {
                    candidateElements.set(element, []);
                }
                candidateElements.get(element).push(sourceInfo);
            };

            // 1. Standard image/video extraction
            if (config.extractImages) {
                doc.querySelectorAll('img[src]').forEach(el => addCandidate(el, { source: 'img' }));
            }
            if (config.extractVideos) {
                doc.querySelectorAll('video').forEach(el => {
                    // Check video[src] OR video > source[src]
                    if (el.hasAttribute('src') || el.querySelector('source[src]')) {
                        addCandidate(el, { source: 'video' });
                    }
                });
            }

            // 2. Custom selector processing
            config.customSelectors.forEach((customConfig, index) => {
                if (!customConfig.selector || !customConfig.type || !customConfig.attribute) return; // Already validated in _resolveConfig, but check again

                try {
                    let nodes = [];
                    if (customConfig.type === 'css') {
                        nodes = Array.from(doc.querySelectorAll(customConfig.selector));
                    } else if (customConfig.type === 'xpath') {
                        const xpathResult = doc.evaluate(customConfig.selector, doc, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
                        let node;
                        while ((node = xpathResult.iterateNext())) {
                            if (node instanceof Element) {
                                nodes.push(node);
                            }
                        }
                    }

                    nodes.forEach(el => {
                        if (el instanceof Element) { // Ensure it's an element
                            addCandidate(el, { source: 'custom', configIndex: index });
                        }
                    });
                } catch (e) {
                    console.error(`MediaExtractor: Error processing custom selector ${customConfig.type}: "${customConfig.selector}" on ${baseUrl}`, e);
                }
            });

            // 3. Sort elements by document order for predictable processing
            const sortedElements = Array.from(candidateElements.keys()).sort((a, b) => {
                const position = a.compareDocumentPosition(b);
                if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
                if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
                return 0;
            });


            // 4. Process sorted elements, prioritizing custom rules and avoiding duplicate processing per element
            for (const element of sortedElements) {
                const matchingSources = candidateElements.get(element);
                let processedForDirectMedia = false; // Found media URL directly from this element?
                let processedForLinkFollow = false; // Followed a link from this element?

                // --- Check Custom Rules First ---
                for (const sourceInfo of matchingSources) {
                    if (sourceInfo.source === 'custom') {
                        const customConfig = config.customSelectors[sourceInfo.configIndex];
                        if (!customConfig) continue; // Should not happen

                        // A. Handle Link Following
                        if (customConfig.followLink) {
                            if (processedForLinkFollow) continue; // Only follow one link per element

                            const linkUrl = this._extractUrlFromAttribute(element, customConfig.attribute, baseUrl);

                            if (linkUrl && !visitedUrls.has(linkUrl)) {
                                if (currentDepth + 1 >= initialMaxDepth) {
                                    // Depth limit reached, don't queue
                                } else {
                                    visitedUrls.add(linkUrl); // Mark visited *before* queueing task
                                    processedForLinkFollow = true;

                                    const taskFn = () => {
                                        const resolvedNextConfig = this._resolveConfig(customConfig.nextConfig || {}, config);
                                        return this._processUrlRecursive(
                                            linkUrl,
                                            resolvedNextConfig,
                                            currentDepth + 1,
                                            initialMaxDepth,
                                            new Set(visitedUrls) // Pass a copy for branch isolation
                                        );
                                    };
                                    followTasks.push(taskFn);
                                }
                            } else if (linkUrl && visitedUrls.has(linkUrl)) {
                                // Already visited on this path
                            }
                            // Link following typically takes precedence over direct extraction *from the same rule*.
                            // If a different rule extracts media directly, that's handled below.
                        }
                        // B. Handle Direct Media Extraction (Custom Rule)
                        else { // customConfig.followLink is false
                            if (processedForDirectMedia) continue; // Already got media from this element

                            const mediaUrl = this._extractUrlFromAttribute(element, customConfig.attribute, baseUrl, customConfig.styleUrlRegex);
                            if (mediaUrl) {
                                // Determine the potential media type *before* adding
                                const potentialType = this._determineMediaType(element, mediaUrl, customConfig.mediaType);

                                // <<< START: Apply the allowedMediaTypes filter >>>
                                let shouldExtract = true; // Assume yes by default
                                // Check if the filter is defined and is an array
                                if (customConfig.allowedMediaTypes && Array.isArray(customConfig.allowedMediaTypes)) {
                                    // Check if the determined type is NOT in the allowed list
                                    if (!customConfig.allowedMediaTypes.includes(potentialType)) {
                                        // console.debug(`MediaExtractor: Skipping ${mediaUrl} - type '${potentialType}' not in allowedMediaTypes [${customConfig.allowedMediaTypes.join(', ')}] for selector ${customConfig.selector}`);
                                        shouldExtract = false; // Type not allowed, skip it
                                    }
                                }
                                // <<< END: Apply the allowedMediaTypes filter >>>

                                if (shouldExtract) {
                                    directResults.push({
                                        url: mediaUrl,
                                        type: potentialType, // Use the type determined (respecting configMediaType override within _determineMediaType)
                                        sourceUrl: baseUrl,
                                        depth: currentDepth,
                                        element: element
                                    });
                                    processedForDirectMedia = true;
                                }
                            }
                        }
                    }
                } // End loop through custom sources for this element

                // --- Standard Element Processing (if not already handled by custom rule) ---
                if (!processedForDirectMedia) {
                    const isVideoTag = matchingSources.some(s => s.source === 'video');
                    const isImageTag = matchingSources.some(s => s.source === 'img');

                    // Standard Video: Check <video src> then <source src>
                    if (isVideoTag && config.extractVideos) { // Check config again
                        let videoUrl = this._extractUrlFromAttribute(element, 'src', baseUrl);
                        if (!videoUrl) {
                            const sourceElements = element.querySelectorAll('source[src]');
                            for (const sourceElement of sourceElements) {
                                videoUrl = this._extractUrlFromAttribute(sourceElement, 'src', baseUrl);
                                if (videoUrl) break; // Use first valid source
                            }
                        }
                        if (videoUrl) {
                            // Type is inherently 'video' here
                            directResults.push({ url: videoUrl, type: 'video', sourceUrl: baseUrl, depth: currentDepth, element: element });
                            processedForDirectMedia = true;
                        }
                    }

                    // Standard Image: Check <img src> (only if not already processed)
                    if (!processedForDirectMedia && isImageTag && config.extractImages) { // Check config again
                        const imgUrl = this._extractUrlFromAttribute(element, 'src', baseUrl);
                        if (imgUrl) {
                            // Type is inherently 'image' here
                            directResults.push({ url: imgUrl, type: 'image', sourceUrl: baseUrl, depth: currentDepth, element: element });
                            processedForDirectMedia = true;
                        }
                    }
                }
            } // End loop through sorted elements

            return { directResults, followTasks };
        }


        /**
         * Internal recursive method. Checks depth limit against initialMaxDepth.
         * Uses a copy of visitedUrls for each branch.
         * @param {string} url
         * @param {Required<ExtractorConfig>} config - Config resolved for THIS level.
         * @param {number} currentDepth
         * @param {number} initialMaxDepth - The maxDepth from the initial call (can be Infinity).
         * @param {Set<string>} visitedUrlsOnPath - Set of URLs visited on the *current specific branch* of the crawl.
         * @returns {Promise<Array<MediaResult>>}
         * @private
         */
        async _processUrlRecursive(url, config, currentDepth, initialMaxDepth, visitedUrlsOnPath) {
            // --- Depth Check ---
            if (currentDepth >= initialMaxDepth) {
                if (initialMaxDepth !== Infinity) {
                    console.debug(`MediaExtractor: Max depth (${initialMaxDepth}) reached at depth ${currentDepth}. Stopping branch at ${url}.`);
                }
                return [];
            }

            // --- Cycle Check --- (Handled before call via visitedUrls.add)

            try {
                console.log(`MediaExtractor: [Depth ${currentDepth}] Fetching and processing: ${url} (Concurrency: ${config.concurrency})`);
                const htmlContent = await this._fetchHtml(url);
                console.log(htmlContent);

                // Parse, extract direct media, identify next tasks
                const { directResults, followTasks } = await this._parseHtmlAndIdentifyTasks(
                    htmlContent,
                    url,
                    config,
                    currentDepth,
                    initialMaxDepth,
                    visitedUrlsOnPath // Pass the set for this path
                );

                // --- Execute recursive calls concurrently ---
                let nestedResults = [];
                if (followTasks.length > 0) {
                    const effectiveConcurrency = config.concurrency;
                    console.log(`MediaExtractor: [Depth ${currentDepth}] Following ${followTasks.length} links from ${url} (Concurrency: ${effectiveConcurrency})...`);
                    const taskResults = await this._runWithConcurrency(followTasks, effectiveConcurrency);
                    nestedResults = taskResults.filter(r => r && !r.error).flat(); // Flatten results, filter errors
                    console.log(`MediaExtractor: [Depth ${currentDepth}] Finished following ${followTasks.length} links from ${url}. Found ${nestedResults.length} nested items.`);
                }

                // Combine results from this page and nested calls
                return [...directResults, ...nestedResults];

            } catch (error) {
                console.error(`MediaExtractor: [Depth ${currentDepth}] Failed to process URL ${url}:`, error.message);
                // console.error(error); // Optional: Log full stack trace
                return []; // Return empty for this failed branch
            }
        }

        /**
         * PUBLIC METHOD: Initiates the recursive extraction.
         * @param {string} url - The initial URL.
         * @param {Partial<ExtractorConfig>} [userConfig={}] - User configuration.
         * @returns {Promise<Array<MediaResult>>} A promise resolving to an array of found media results.
         */
        async extractFromUrl(url, userConfig = {}) {
            console.log(`MediaExtractor: Starting extraction from ${url}`);

            // 1. Resolve initial config and determine the effective max depth limit
            const initialConfig = this._resolveConfig(userConfig, null);
            const effectiveMaxDepth = initialConfig.maxDepth; // Number or Infinity

            if (effectiveMaxDepth !== Infinity) {
                console.log(`MediaExtractor: Configured with maxDepth limit: ${effectiveMaxDepth}`);
            } else {
                console.log(`MediaExtractor: No explicit maxDepth limit. Depth controlled by config structure and cycle detection.`);
            }

            // 2. Initialize visited set for the starting path
            const initialVisitedUrls = new Set();
            try {
                // Normalize starting URL before adding
                const startUrl = new URL(url).href;
                initialVisitedUrls.add(startUrl);

                // 3. Start the recursive process
                const allMediaResults = await this._processUrlRecursive(
                    startUrl,
                    initialConfig,
                    0,             // Start depth
                    effectiveMaxDepth,
                    initialVisitedUrls
                );

                console.log(`MediaExtractor: Extraction complete. Found ${allMediaResults.length} total media items starting from ${url}.`);
                if (allMediaResults.length === 0 && effectiveMaxDepth !== 0) {
                    console.log("MediaExtractor: Hint - If 0 results, check config (selectors, attributes, followLink structure), network logs (F12) for fetch errors/redirects, or website structure changes.");
                }

                // Optional: Deduplicate results based on URL if needed
                // const uniqueResults = Array.from(new Map(allMediaResults.map(item => [item.url, item])).values());
                // return uniqueResults;
                return allMediaResults;

            } catch (error) {
                // Catch errors during initial setup (e.g., invalid initial URL) or the top-level process call
                console.error(`MediaExtractor: Critical failure during extraction starting from ${url}:`, error.message);
                console.error(error);
                return []; // Return empty on critical failure
            }
        }
    } const CONFIG_STORAGE_PREFIX = 'mediaExtractorConfig_';
    const CONFIG_LIST_KEY = 'mediaExtractorConfig_list';
    const CONFIG_LAST_USED_KEY = 'mediaExtractorConfig_lastUsed';

    class MediaExtractorUI {
        /**
         * @param {string} [containerId=null] - Optional ID of the HTML element to render into.
         *                                      If null, UI will be rendered in a centered modal overlay.
         */
        constructor(containerId = null) {
            this.targetElement = null;
            if (containerId) {
                this.targetElement = document.getElementById(containerId);
                if (!this.targetElement) {
                    throw new Error(`MediaExtractorUI: Container element with ID "${containerId}" not found.`);
                }
            }
            this.extractor = new MediaExtractor(); // Assuming MediaExtractor is available
            this.uniqueIdCounter = 0;
            this._isShown = false;
            this.uiRoot = null; // Will hold the main UI container element (inside modal or target)
            this.overlayElement = null; // For modal mode
            this.modalElement = null; // For modal mode
            this.boundKeyHandler = this._handleKeyPress.bind(this); // Bind escape handler
            this.extractionCompleteCallbacks = []; // Initialize callbacks array

            // --- Define the UI structure ---

            // Definition for top-level ExtractorConfig fields
            // NOTE: maxDepth was removed previously
            this.TOP_LEVEL_CONFIG_FIELDS = [
                { key: 'extractImages', label: 'Extract <img> tags', type: 'checkbox', defaultValue: true },
                { key: 'extractVideos', label: 'Extract <video> tags', type: 'checkbox', defaultValue: true },
                { key: 'concurrency', label: 'Concurrency:', type: 'number', defaultValue: 5, min: 1 },
                // The 'customSelectors' field is handled specially by _renderCustomSelectorList
            ];

            this.CUSTOM_SELECTOR_FIELDS = [
                { key: 'selector', label: 'Selector (CSS or XPath):', type: 'text', placeholder: 'e.g., .my-image a', required: true },
                { key: 'type', label: 'Selector Type:', type: 'select', options: { css: 'CSS', xpath: 'XPath' }, defaultValue: 'css', required: true },
                { key: 'attribute', label: 'Attribute (for URL/Link):', type: 'text', placeholder: 'e.g., href, src, data-src, style', required: true },
                {
                    key: 'styleUrlRegex', label: 'Style Regex (if attr=style):', type: 'text', placeholder: 'e.g., url\\(["\']?(.*?)["\']?\\)',
                    valueGetter: (input) => input.value.trim() ? new RegExp(input.value.trim()) : null,
                    valueSetter: (value) => value instanceof RegExp ? value.source : ''
                },
                // --- MOVED & MODIFIED ---
                {
                    key: 'allowedMediaTypes',
                    label: 'Extract Only If URL is..',
                    type: 'select',
                    options: { // Key: value sent to valueGetter/received from valueSetter, Value: Display text
                        'all': 'Any',
                        'image': 'Image',
                        'video': 'Video'
                    },
                    defaultValue: 'all', // UI default corresponds to 'all'
                    // helpText: '...', // <<< Removed helpText
                    valueGetter: (selectElement) => {
                        switch (selectElement.value) {
                            case 'image': return ['image'];
                            case 'video': return ['video'];
                            case 'all':
                            default: return null; // null means no filtering
                        }
                    },
                    valueSetter: (value) => {
                        if (Array.isArray(value)) {
                            if (value.includes('image') && value.length === 1) return 'image';
                            if (value.includes('video') && value.length === 1) return 'video';
                        }
                        return 'all'; // Default to 'all' if null or unrecognized array
                    }
                },
                // --- Follow Link now comes after Allowed Types ---
                {
                    key: 'followLink', label: 'Follow Link (Extract from linked page)', type: 'checkbox', defaultValue: false,
                    onChange: this._handleFollowLinkChange // Keep existing handler
                }
                // 'nextConfig' is handled implicitly when 'followLink' is true
            ];
        }

        // --- Core Methods (render, close, _addStyles) ---

        _getNextId() {
            return `me-ui-${this.uniqueIdCounter++}`;
        }

        _addStyles() {
            const styleId = 'media-extractor-ui-styles';
            if (document.getElementById(styleId)) return;

            const css = `
            /* Basic UI styles */
            .media-extractor-ui {
                font-family: sans-serif;
                border: 1px solid #444; /* Darker border */
                padding: 15px;
                border-radius: 5px;
                background-color: #333; /* Dark gray background */
                color: #eee; /* Light text color */
                margin: 0; /* Reset margin for modal context */
            }
            .media-extractor-ui fieldset { border: 1px solid #555; padding: 10px; margin-bottom: 15px; border-radius: 4px; }
            .media-extractor-ui legend { font-weight: bold; padding: 0 5px; color: #eee; } /* Light legend color */
            .media-extractor-ui label { display: block; margin-bottom: 3px; font-size: 0.9em; color: #ccc; } /* Lighter label color */
            .media-extractor-ui .field-wrapper { margin-bottom: 0.75em; }
            .media-extractor-ui .field-wrapper label { display: block; margin-bottom: 3px; font-size: 0.9em; color: #ccc;} /* Lighter label color */
            .media-extractor-ui .field-wrapper.checkbox-field label { display: inline-block; margin-left: 5px; vertical-align: middle;}
             /* Adjust alignment for checkbox */
            .media-extractor-ui .field-wrapper.checkbox-field input[type="checkbox"] {
                 vertical-align: middle;
                 margin-right: 0; /* Remove default margin if label handles spacing */
            }

            .media-extractor-ui input[type="text"],
            .media-extractor-ui input[type="number"],
            .media-extractor-ui select,
            .media-extractor-ui textarea {
                width: 95%; padding: 6px; margin-bottom: 2px; /* Smaller bottom margin */
                border: 1px solid #666; /* Slightly darker border */
                border-radius: 3px; box-sizing: border-box;
                background-color: #444; /* Darker input background */
                color: #eee; /* Light input text */
            }
             /* Reduce bottom margin specifically for text/number/select/textarea inside the wrapper */
            .media-extractor-ui .field-wrapper input[type="text"],
            .media-extractor-ui .field-wrapper input[type="number"],
            .media-extractor-ui .field-wrapper select,
            .media-extractor-ui .field-wrapper textarea {
                 margin-bottom: 2px;
            }

            .media-extractor-ui .config-section { padding: 10px; margin-top: 10px; background-color: #3a3a3a; border-radius: 3px; } /* Slightly lighter section bg */
            .media-extractor-ui .custom-selector-group { border: 1px solid #555; padding: 10px 10px 15px 10px; margin-bottom: 10px; background-color: #383838; position: relative; border-radius: 4px; } /* Slightly lighter group bg */
            .media-extractor-ui .custom-selector-group legend { font-size: 0.95em; font-weight: normal; color: #ddd; } /* Lighter legend */
            .media-extractor-ui .nested-config { margin-left: 20px; padding-left: 15px; border-left: 2px solid #007bff; margin-top: 10px; display: none; /* Hidden by default */ }
            .media-extractor-ui button { padding: 8px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; font-size: 1em; }
            .media-extractor-ui button:hover { background-color: #0056b3; }
            .media-extractor-ui .remove-button { background-color: #dc3545; font-size: 0.8em; padding: 3px 8px; position: absolute; top: 5px; right: 5px; }
            .media-extractor-ui .remove-button:hover { background-color: #c82333; }
            .media-extractor-ui .add-button { background-color: #28a745; font-size: 0.9em; padding: 5px 10px; margin-top: 10px; }
            .media-extractor-ui .add-button:hover { background-color: #218838; }
            .media-extractor-ui .output-area { margin-top: 15px; padding: 10px; background-color: #444; border: 1px solid #666; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; min-height: 50px; max-height: 200px; overflow-y: auto; color: #ccc; } /* Darker output, lighter text */
            .media-extractor-ui .output-area.extracting { color: #66aaff; } /* Lighter blue */
            .media-extractor-ui .output-area.error { color: #ff8080; font-weight: bold; } /* Lighter red */
            .media-extractor-ui .output-area.success { color: #80ff80; } /* Lighter green */
            .media-extractor-ui .config-options-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 10px; }
            .media-extractor-ui .custom-selector-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
            .media-extractor-ui .field-help-text { font-size: 0.8em; color: #aaa; margin-top: 2px; } /* Adjust help text color */

            /* Config Management styles */
            .media-extractor-ui .config-management {
                display: flex;
                flex-wrap: wrap; /* Allow wrapping on smaller screens */
                align-items: center; /* Align items vertically */
                gap: 10px; /* Spacing between elements */
                padding: 10px;
                background-color: #3a3a3a; /* Match config section */
                border-radius: 3px;
                margin-bottom: 15px; /* Space below this section */
            }
            .media-extractor-ui .config-management label {
                margin-bottom: 0; /* Remove default bottom margin */
                margin-right: 5px; /* Space after label */
                font-size: 0.9em;
                 color: #ccc;
            }
            .media-extractor-ui .config-management select,
            .media-extractor-ui .config-management input[type="text"] {
                flex-grow: 1; /* Allow select and input to take available space */
                width: auto; /* Override default width */
                min-width: 150px; /* Ensure minimum width */
                 padding: 5px; /* Slightly smaller padding */
                 margin-bottom: 0; /* Remove default bottom margin */
                 font-size: 0.9em;
            }
             .media-extractor-ui .config-management input[type="text"] {
                 /* Specific styling for save name input */
                 background-color: #444;
                 border: 1px solid #666;
                 color: #eee;
             }
            .media-extractor-ui .config-management button {
                padding: 5px 10px; /* Smaller button padding */
                font-size: 0.9em; /* Smaller font size */
                margin: 0 2px; /* Adjust margin */
                flex-shrink: 0; /* Prevent buttons from shrinking too much */
            }
             .media-extractor-ui .config-management .delete-config-button {
                 background-color: #dc3545;
             }
             .media-extractor-ui .config-management .delete-config-button:hover {
                 background-color: #c82333;
             }
            .media-extractor-ui .config-management .config-status {
                font-size: 0.85em;
                color: #aaa;
                margin-left: 10px; /* Space from buttons */
                flex-basis: 100%; /* Force status to new line if needed */
                text-align: left; /* Align left */
            }

            /* Modal specific styles */
            .media-extractor-overlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background-color: rgba(0, 0, 0, 0.7); /* Slightly darker overlay */
                display: flex;
                justify-content: center; align-items: center; z-index: 9999999;
                padding: 20px; box-sizing: border-box;
            }
            .media-extractor-modal {
                /* Background handled by inner .media-extractor-ui now */
                padding: 0; /* Padding applied to inner */
                border-radius: 8px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.4); /* Adjust shadow */
                max-width: 95%;
                width: 850px; max-height: 90vh; overflow-y: auto; position: relative;
                background-color: #333; /* Set modal background same as UI */
                border: 1px solid #444; /* Match border */
            }
            .media-extractor-modal-close {
                position: absolute; top: 8px; right: 12px; font-size: 1.8em;
                font-weight: bold; color: #aaa; background: none; border: none;
                cursor: pointer; line-height: 1; padding: 0 5px; z-index: 1; /* Ensure button is clickable */
            }
            .media-extractor-modal-close:hover { color: #fff; } /* Brighter hover */

            .media-extractor-ui .field-wrapper.field-disabled-by-follow label,
            .media-extractor-ui .field-wrapper.field-disabled-by-follow select:disabled {
                color: #777; /* Dim the label */
                cursor: not-allowed;
            }
            .media-extractor-ui .field-wrapper.field-disabled-by-follow select:disabled {
                background-color: #555; /* Darker background for disabled select */
                opacity: 0.7;
            }
        `;
            const styleElement = document.createElement('style');
            styleElement.id = styleId;
            styleElement.textContent = css;
            document.head.appendChild(styleElement);
        }

        isShown() {
            return this._isShown;
        }

        render() {
            this._addStyles();
            this.close(); // Clear previous modal if exists
            this._isShown = true;

            if (this.targetElement) {
                // ... (rendering into target element, no escape listener needed)
                this.targetElement.innerHTML = '';
                this.uiRoot = document.createElement('div');
                this.uiRoot.classList.add('media-extractor-ui');
                this._buildUIContent(this.uiRoot);
                this.targetElement.appendChild(this.uiRoot);
                this._loadLastUsedConfig(); // Load last used config after building

            } else { // Render into a modal
                this.overlayElement = document.createElement('div');
                this.overlayElement.classList.add('media-extractor-overlay');
                this.overlayElement.addEventListener('click', (e) => {
                    if (e.target === this.overlayElement) this.close();
                });

                this.modalElement = document.createElement('div');
                this.modalElement.classList.add('media-extractor-modal');

                // uiRoot container now inside modal
                this.uiRoot = document.createElement('div');
                this.uiRoot.classList.add('media-extractor-ui');

                const closeButton = document.createElement('button');
                closeButton.type = 'button';
                closeButton.classList.add('media-extractor-modal-close');
                closeButton.innerHTML = '×';
                closeButton.title = 'Close (Esc)'; // Hint for escape key
                closeButton.onclick = () => this.close();
                this.modalElement.appendChild(closeButton); // Append to modal, not uiRoot

                this._buildUIContent(this.uiRoot); // Build inside uiRoot

                this.modalElement.appendChild(this.uiRoot); // Add uiRoot content to modal
                this.overlayElement.appendChild(this.modalElement);
                document.body.appendChild(this.overlayElement);

                // Add Escape key listener for modal mode
                document.addEventListener('keydown', this.boundKeyHandler);

                this._loadLastUsedConfig(); // Load last used config after building
            }
        }

        close() {
            if (this.overlayElement && this.overlayElement.parentNode) {
                this.overlayElement.parentNode.removeChild(this.overlayElement);
                // Remove Escape key listener when closing
                document.removeEventListener('keydown', this.boundKeyHandler);
            }
            this.overlayElement = null;
            this.modalElement = null;
            this._isShown = false;
            // If rendering into target, don't clear uiRoot unless desired
            // If modal, clearing uiRoot isn't strictly necessary as it's removed with the modal
        }

        // --- UI Building ---

        /**
         * Builds the main form structure within the parent element.
         * @param {HTMLElement} parentElement - Usually this.uiRoot.
         * @private
         */
        _buildUIContent(parentElement) {
            parentElement.innerHTML = ''; // Clear previous content

            const form = document.createElement('form');
            form.addEventListener('submit', (e) => e.preventDefault());

            // --- Configuration Management Section ---
            const configMgmtFieldset = document.createElement('fieldset');
            configMgmtFieldset.innerHTML = '<legend>Configuration Management</legend>';
            const configMgmtDiv = document.createElement('div');
            configMgmtDiv.classList.add('config-management');
            configMgmtDiv.dataset.role = 'config-management-area';

            // Dropdown for saved configs
            const selectLabel = document.createElement('label');
            selectLabel.htmlFor = this._getNextId() + '-config-select';
            selectLabel.textContent = 'Saved Configs:';
            const configSelect = document.createElement('select');
            configSelect.id = selectLabel.htmlFor;
            configSelect.dataset.role = 'config-select';
            configSelect.innerHTML = '<option value="">--- Select Config ---</option>'; // Default option

            // Input for naming config
            const saveNameLabel = document.createElement('label');
            saveNameLabel.htmlFor = this._getNextId() + '-config-save-name';
            saveNameLabel.textContent = 'Save/Update As:';
            const configSaveName = document.createElement('input');
            configSaveName.type = 'text';
            configSaveName.id = saveNameLabel.htmlFor;
            configSaveName.dataset.role = 'config-save-name';
            configSaveName.placeholder = 'Enter config name';

            // Buttons
            // --- REMOVED Load Button ---
            // const loadButton = document.createElement('button');
            // loadButton.type = 'button';
            // loadButton.textContent = 'Load';
            // loadButton.dataset.role = 'load-config-btn';
            // loadButton.onclick = () => this._handleLoadConfigClick(); // Removed

            const saveButton = document.createElement('button');
            saveButton.type = 'button';
            saveButton.textContent = 'Save';
            saveButton.dataset.role = 'save-config-btn';
            saveButton.onclick = () => this._handleSaveConfigClick();

            const deleteButton = document.createElement('button');
            deleteButton.type = 'button';
            deleteButton.textContent = 'Delete';
            deleteButton.classList.add('delete-config-button');
            deleteButton.dataset.role = 'delete-config-btn';
            deleteButton.onclick = () => this._handleDeleteConfigClick();

            // Status message area
            const statusSpan = document.createElement('span');
            statusSpan.classList.add('config-status');
            statusSpan.dataset.role = 'config-status-message';
            statusSpan.textContent = ''; // Initially empty

            // --- MODIFIED: Add listener to load config instantly and update save name ---
            configSelect.addEventListener('change', this._handleConfigSelectChange.bind(this));


            configMgmtDiv.appendChild(selectLabel);
            configMgmtDiv.appendChild(configSelect);
            // configMgmtDiv.appendChild(loadButton); // Removed
            configMgmtDiv.appendChild(saveNameLabel);
            configMgmtDiv.appendChild(configSaveName);
            configMgmtDiv.appendChild(saveButton);
            configMgmtDiv.appendChild(deleteButton);
            configMgmtDiv.appendChild(statusSpan);
            configMgmtFieldset.appendChild(configMgmtDiv);
            form.appendChild(configMgmtFieldset);

            // --- Start URL Section (existing) ---
            const urlFieldset = document.createElement('fieldset');
            urlFieldset.innerHTML = '<legend>Start URL</legend>';
            const urlLabel = document.createElement('label');
            const urlInputId = this._getNextId() + '-start-url';
            urlLabel.htmlFor = urlInputId;
            urlLabel.textContent = 'Enter the initial URL to extract from:';
            const urlInput = document.createElement('input');
            urlInput.type = 'text';
            urlInput.id = urlInputId;
            urlInput.placeholder = 'https://example.com/page';
            urlInput.required = true;
            urlInput.dataset.role = 'start-url-input'; // Add role for easier selection
            urlFieldset.appendChild(urlLabel);
            urlFieldset.appendChild(urlInput);
            form.appendChild(urlFieldset);

            // --- Top Level Configuration Section (existing) ---
            const topLevelFieldset = document.createElement('fieldset');
            topLevelFieldset.innerHTML = '<legend>Extraction Configuration (Top Level)</legend>';
            topLevelFieldset.dataset.role = 'top-level-config-fieldset'; // Add role
            const topLevelConfigDiv = document.createElement('div');
            topLevelConfigDiv.classList.add('config-section', 'config-options-grid'); // Use grid layout
            topLevelConfigDiv.dataset.depth = '0'; // Mark depth
            this._renderConfigSection(
                topLevelConfigDiv,
                this.TOP_LEVEL_CONFIG_FIELDS,
                MediaExtractor.DEFAULT_CONFIG, // Initial defaults
                0
            );
            topLevelFieldset.appendChild(topLevelConfigDiv);
            form.appendChild(topLevelFieldset);

            // --- Custom Selectors Section (existing) ---
            const csFieldset = document.createElement('fieldset');
            csFieldset.innerHTML = `<legend>Custom Selectors</legend>`;
            csFieldset.dataset.role = 'custom-selectors-fieldset'; // Add role
            this._renderCustomSelectorList(csFieldset, MediaExtractor.DEFAULT_CONFIG.customSelectors || [], 0);
            form.appendChild(csFieldset);

            // --- Controls & Output (existing) ---
            const controlsFieldset = document.createElement('fieldset');
            controlsFieldset.innerHTML = '<legend>Control & Output</legend>';
            this._renderControls(controlsFieldset);
            form.appendChild(controlsFieldset);

            parentElement.appendChild(form);

            // Populate the config dropdown after the UI is built
            this._populateConfigDropdown();
            // NOTE: Loading last used config is now handled in render() after this function completes
        }

        /**
      * Renders a configuration section based on a field definition array.
      * @param {HTMLElement} parentElement - The container element for this section.
      * @param {Array<object>} fieldsDefinition - Array describing the fields.
      * @param {object} currentConfigData - The current data object for this config level.
      * @param {number} depth - The current nesting depth (0 for top level).
      * @param {object} [sectionDataAttributes={}] - Optional data attributes for the section.
      * @private
      */
        _renderConfigSection(parentElement, fieldsDefinition, currentConfigData, depth, sectionDataAttributes = {}) {
            // Add data attributes to the parent element itself
            for (const attr in sectionDataAttributes) {
                parentElement.dataset[attr] = sectionDataAttributes[attr];
            }

            fieldsDefinition.forEach(fieldDef => {
                // Skip fields handled elsewhere (like customSelectors list itself)
                if (fieldDef.key === 'customSelectors' || fieldDef.key === 'nextConfig') {
                    return;
                }
                // Skip concurrency field if rendering a nested config (depth > 0)
                if (fieldDef.key === 'concurrency' && depth > 0) {
                    return;
                }

                // Get the current value for this field
                const currentValue = currentConfigData?.[fieldDef.key] ?? fieldDef.defaultValue;

                // Create and append the field
                this._renderFormField(parentElement, fieldDef, currentValue, depth);
            });
        }

        /**
         * Renders a single form field based on its definition.
         * @param {HTMLElement} parentElement - The container to append the field to.
         * @param {object} fieldDef - The field definition object.
         * @param {*} currentValue - The current value for the field.
         * @param {number} depth - Current nesting depth.
         * @private
         */
        _renderFormField(parentElement, fieldDef, currentValue, depth) {
            const wrapper = document.createElement('div');
            wrapper.classList.add('field-wrapper');
            const fieldId = this._getNextId() + '-' + fieldDef.key;

            let inputElement;
            const label = document.createElement('label');
            label.htmlFor = fieldId;
            label.textContent = fieldDef.label;

            switch (fieldDef.type) {
                case 'checkbox':
                    wrapper.classList.add('checkbox-field'); // Add class for specific styling
                    inputElement = document.createElement('input');
                    inputElement.type = 'checkbox';
                    inputElement.checked = currentValue === true;
                    // Place checkbox before label for common layout
                    wrapper.appendChild(inputElement);
                    wrapper.appendChild(label); // Label now appears after checkbox
                    break;

                case 'select':
                    inputElement = document.createElement('select');
                    for (const value in fieldDef.options) {
                        const option = document.createElement('option');
                        option.value = value;
                        option.textContent = fieldDef.options[value];
                        if (value === currentValue) {
                            option.selected = true;
                        }
                        inputElement.appendChild(option);
                    }
                    wrapper.appendChild(label);
                    wrapper.appendChild(inputElement);
                    break;

                case 'number':
                    inputElement = document.createElement('input');
                    inputElement.type = 'number';
                    if (fieldDef.min !== undefined) inputElement.min = fieldDef.min;
                    if (fieldDef.max !== undefined) inputElement.max = fieldDef.max;
                    if (fieldDef.step !== undefined) inputElement.step = fieldDef.step;
                    inputElement.placeholder = fieldDef.placeholder || '';
                    // Use valueSetter if provided, otherwise default behavior
                    inputElement.value = fieldDef.valueSetter ? fieldDef.valueSetter(currentValue) : (currentValue ?? '');
                    wrapper.appendChild(label);
                    wrapper.appendChild(inputElement);
                    break;

                case 'textarea':
                    inputElement = document.createElement('textarea');
                    inputElement.rows = fieldDef.rows || 3;
                    inputElement.placeholder = fieldDef.placeholder || '';
                    inputElement.value = fieldDef.valueSetter ? fieldDef.valueSetter(currentValue) : (currentValue ?? '');
                    wrapper.appendChild(label);
                    wrapper.appendChild(inputElement);
                    break;

                case 'text':
                default:
                    inputElement = document.createElement('input');
                    inputElement.type = 'text';
                    inputElement.placeholder = fieldDef.placeholder || '';
                    inputElement.value = fieldDef.valueSetter ? fieldDef.valueSetter(currentValue) : (currentValue ?? '');
                    wrapper.appendChild(label);
                    wrapper.appendChild(inputElement);
                    break;
            }

            inputElement.id = fieldId;
            inputElement.dataset.key = fieldDef.key; // Crucial for data collection
            if (fieldDef.required) {
                inputElement.required = true;
            }

            // Add help text if defined
            if (fieldDef.helpText) {
                const helpText = document.createElement('p');
                helpText.classList.add('field-help-text');
                helpText.textContent = fieldDef.helpText;
                wrapper.appendChild(helpText);
            }

            // Attach change listener if defined
            if (fieldDef.onChange && typeof fieldDef.onChange === 'function') {
                inputElement.addEventListener('change', (event) => fieldDef.onChange(event, wrapper, depth));
            }

            // Append the whole wrapper to the parent
            parentElement.appendChild(wrapper);
        }

        /**
         * Renders the list of custom selectors and the "Add" button.
         * @param {HTMLElement} parentElement - The fieldset element to contain the list.
         * @param {Array<object>} currentSelectorsData - Array of custom selector config objects.
         * @param {number} depth - The depth level this list belongs to.
         * @private
         */
        _renderCustomSelectorList(parentElement, currentSelectorsData, depth) {
            const selectorsListDiv = document.createElement('div');
            selectorsListDiv.classList.add('custom-selectors-list');
            selectorsListDiv.dataset.depth = depth; // Store depth for context

            // Render existing selectors
            currentSelectorsData.forEach((csData, index) => {
                this._addCustomSelector(selectorsListDiv, csData, depth, index);
            });

            parentElement.appendChild(selectorsListDiv);

            // Add "Add Custom Selector" button
            const addButton = document.createElement('button');
            addButton.type = 'button';
            addButton.textContent = '+ Add Custom Selector';
            addButton.classList.add('add-button');
            addButton.onclick = () => {
                // Add a new selector with default values
                this._addCustomSelector(selectorsListDiv, {}, depth, selectorsListDiv.children.length);
            };
            parentElement.appendChild(addButton);
        }

        /**
     * Renders a single custom selector group UI.
     * @param {HTMLElement} listContainerElement - The div holding all selector groups.
     * @param {object} selectorData - The configuration data for this specific selector.
     * @param {number} listDepth - The depth of the list container itself.
     * @param {number} index - The index of this selector in the list.
     * @private
     */
        _addCustomSelector(listContainerElement, selectorData, listDepth, index) {
            const groupFieldset = document.createElement('fieldset');
            groupFieldset.classList.add('custom-selector-group');
            groupFieldset.dataset.isCustomSelector = 'true'; // Mark as a selector group
            groupFieldset.dataset.depth = listDepth; // Carry over depth
            groupFieldset.innerHTML = `<legend>Selector #${index + 1}</legend>`;

            const gridContainer = document.createElement('div');
            gridContainer.classList.add('custom-selector-grid'); // Apply grid layout

            // Render fields for this custom selector using its definition and loaded data
            this._renderConfigSection(
                gridContainer,
                this.CUSTOM_SELECTOR_FIELDS,
                selectorData, // Pass the specific data for this selector
                listDepth // Depth is the same as the list container
            );

            groupFieldset.appendChild(gridContainer);

            // --- Nested Configuration Handling ---
            const nestedConfigContainer = document.createElement('div');
            nestedConfigContainer.classList.add('nested-config');
            nestedConfigContainer.classList.add('config-options-grid');
            nestedConfigContainer.dataset.role = 'nested-config-container'; // For easy selection

            // Check the *loaded* data to determine initial state
            const shouldShowNested = selectorData?.followLink === true && selectorData?.nextConfig;

            if (shouldShowNested) {
                const nextConfigData = selectorData.nextConfig || {};
                const nextDepth = listDepth + 1;
                nestedConfigContainer.style.display = 'block'; // Show it

                // --- Recursive Update Logic integrated here for loading ---
                // We need helper functions similar to those in _updateUIFromConfig,
                // but adapted for rendering within this context. Let's reuse the main ones.

                // Helper function (could be refactored out, but placed here for clarity)
                const updateOrRenderSection = (parent, fieldsDef, configLevelData, depth, dataAttrs = {}) => {
                    // Clear existing content before rendering/updating
                    // parent.innerHTML = ''; // Don't clear here, _renderConfigSection appends

                    // Use _renderConfigSection to build the fields based on loaded data
                    this._renderConfigSection(parent, fieldsDef, configLevelData, depth, dataAttrs);
                };

                // Helper function
                const updateOrRenderCustomSelectors = (listParent, selectorsData, depth) => {
                    // Clear existing selectors first
                    const existingList = listParent.querySelector('.custom-selectors-list');
                    if (existingList) existingList.remove();
                    const existingAddButton = listParent.querySelector('.add-button');
                    if (existingAddButton) existingAddButton.remove(); // Also remove old add button

                    // Use _renderCustomSelectorList to rebuild the list based on loaded data
                    this._renderCustomSelectorList(listParent, selectorsData || [], depth);
                };
                // --- End Helpers ---

                // Render/Update the nested top-level fields
                updateOrRenderSection(
                    nestedConfigContainer,
                    this.TOP_LEVEL_CONFIG_FIELDS,
                    nextConfigData,
                    nextDepth,
                    { configType: 'nested-top-level' }
                );

                // Render/Update the nested custom selector list recursively
                // We need a container *within* nestedConfigContainer for the nested list+button
                const nestedCsListWrapper = document.createElement('div'); // Create a wrapper
                nestedCsListWrapper.dataset.role = 'nested-custom-selectors-wrapper';
                nestedConfigContainer.appendChild(nestedCsListWrapper); // Append wrapper
                updateOrRenderCustomSelectors(
                    nestedCsListWrapper, // Pass the wrapper as the parent
                    nextConfigData.customSelectors || [],
                    nextDepth
                );

            } else {
                // Ensure it's hidden if followLink is false or no nextConfig exists
                nestedConfigContainer.style.display = 'none';
                // Optionally render empty structure when hidden? Or leave empty?
                // Leaving it empty is simpler and _handleFollowLinkChange will populate if needed.
            }
            // Append the (potentially populated or empty/hidden) container
            groupFieldset.appendChild(nestedConfigContainer);


            // Add Remove Button
            const removeButton = document.createElement('button');
            removeButton.type = 'button';
            removeButton.textContent = 'Remove';
            removeButton.classList.add('remove-button');
            removeButton.onclick = () => {
                groupFieldset.remove();
                // Optional: Renumber legends after removal
                this._renumberSelectorLegends(listContainerElement);
            };
            groupFieldset.appendChild(removeButton);

            listContainerElement.appendChild(groupFieldset);
        }

        /** Helper to renumber selector legends after removal */
        _renumberSelectorLegends(listContainerElement) {
            const groups = listContainerElement.querySelectorAll('.custom-selector-group');
            groups.forEach((group, index) => {
                const legend = group.querySelector('legend');
                if (legend) {
                    legend.textContent = `Selector #${index + 1}`;
                }
            });
        }


        /**
         * Event handler for the 'followLink' checkbox change.
         * Shows/hides the nested config section AND enables/disables the allowedMediaTypes dropdown.
         * NOTE: Defined as an arrow function property to automatically bind 'this'.
         * @param {Event} event - The change event object.
         * @param {HTMLElement} fieldWrapper - The wrapper div of the checkbox field.
         * @param {number} depth - The depth where this checkbox resides.
         * @private
         */
        _handleFollowLinkChange = (event, fieldWrapper, depth) => { // Arrow function syntax
            const checkbox = event.target;
            const groupFieldset = fieldWrapper.closest('.custom-selector-group');
            if (!groupFieldset) return;

            const nestedConfigContainer = groupFieldset.querySelector('.nested-config[data-role="nested-config-container"]');
            // Find the allowedMediaTypes dropdown within the same group
            const allowedTypesSelect = groupFieldset.querySelector('select[data-key="allowedMediaTypes"]');

            if (!nestedConfigContainer) {
                console.warn("Nested config container not found during followLink change.");
                // Still proceed to enable/disable dropdown if found
            }

            if (checkbox.checked) {
                // --- Nested Config Handling (Show) ---
                if (nestedConfigContainer) {
                    nestedConfigContainer.style.display = 'block';
                    // Only render if it's empty (first time being checked)
                    if (nestedConfigContainer.children.length === 0) {
                        const nextDepth = depth + 1;
                        // 'this' here correctly refers to the MediaExtractorUI instance
                        this._renderConfigSection(
                            nestedConfigContainer,
                            this.TOP_LEVEL_CONFIG_FIELDS,
                            {}, // Start with defaults for nested config
                            nextDepth,
                            { configType: 'nested-top-level' }
                        );
                        // Create a wrapper for the nested list and button
                        const nestedCsListWrapper = document.createElement('div');
                        nestedCsListWrapper.dataset.role = 'nested-custom-selectors-wrapper';
                        nestedConfigContainer.appendChild(nestedCsListWrapper);
                        this._renderCustomSelectorList(
                            nestedCsListWrapper, // Pass the wrapper
                            [], // Start with empty list for nested
                            nextDepth
                        );
                    }
                }
                // --- Allowed Types Handling (Disable) ---
                if (allowedTypesSelect) {
                    allowedTypesSelect.disabled = true;
                    // Optional: Add a class for visual styling when disabled due to followLink
                    allowedTypesSelect.closest('.field-wrapper')?.classList.add('field-disabled-by-follow');
                }

            } else { // Checkbox is UNchecked
                // --- Nested Config Handling (Hide) ---
                if (nestedConfigContainer) {
                    nestedConfigContainer.style.display = 'none';
                    // Optional: Clear the content when hiding to reset state?
                    // nestedConfigContainer.innerHTML = '';
                }
                // --- Allowed Types Handling (Enable) ---
                if (allowedTypesSelect) {
                    allowedTypesSelect.disabled = false;
                    allowedTypesSelect.closest('.field-wrapper')?.classList.remove('field-disabled-by-follow');
                }
            }
        } // End of arrow function




        /**
         * Renders the control buttons and output area. (Largely unchanged)
         * @param {HTMLElement} parentElement - The DOM element to append controls to.
         * @private
         */
        _renderControls(parentElement) {
            const extractButton = document.createElement('button');
            extractButton.type = 'button';
            extractButton.textContent = 'Extract Media';
            extractButton.dataset.role = 'extract-btn';
            extractButton.onclick = () => this._handleExtractClick();

            const outputArea = document.createElement('div');
            outputArea.classList.add('output-area');
            outputArea.dataset.role = 'output-area';
            outputArea.textContent = 'Click "Extract Media" or press Enter to start.';

            parentElement.appendChild(extractButton);
            parentElement.appendChild(outputArea);
        }

        // --- Data Collection ---

        /**
         * Reads the configuration from the UI elements within a given section.
         * Handles nested configurations recursively.
         * @param {HTMLElement} sectionElement - The element containing the config section UI (e.g., config-section, custom-selector-group, nested-config).
         * @param {Array<object>} fieldsDefinition - The definition array used to build this section (or a dummy definition for structural reads like customSelectors).
         * @param {number} depth - The nesting depth of the section being read (0 for top-level).
         * @returns {object} The config object derived from the UI fields.
         * @private
         */
        _getConfigFromUI(sectionElement, fieldsDefinition, depth) {
            const config = {};

            // --- 1. Read Simple Fields Defined by fieldsDefinition ---
            fieldsDefinition.forEach(fieldDef => {
                const key = fieldDef.key;
                // Skip keys handled structurally (like the container for custom selectors or nested config itself)
                if (key === 'customSelectors' || key === 'nextConfig') {
                    return;
                }
                // Skip reading concurrency if reading a nested config (depth > 0)
                if (key === 'concurrency' && depth > 0) {
                    return;
                }

                // Find input element (checking direct children wrappers first)
                let inputElement = null;
                const wrapper = Array.from(sectionElement.querySelectorAll(':scope > .field-wrapper')).find(
                    wrap => wrap.querySelector(`[data-key="${key}"]`)
                );
                inputElement = wrapper ? wrapper.querySelector(`[data-key="${key}"]`) : null;

                // Fallback: Check direct children if not in a wrapper (less common now)
                if (!inputElement) {
                    inputElement = sectionElement.querySelector(`:scope > [data-key="${key}"]`);
                }
                // Fallback: Check any descendant (needed for custom selector fields within grid)
                if (!inputElement) {
                    inputElement = sectionElement.querySelector(`[data-key="${key}"]`);
                }


                if (!inputElement) {
                    // console.warn(`_getConfigFromUI: Could not find input element for key "${key}" at depth ${depth} in section:`, sectionElement);
                    // Assign default value only if the field is expected at this depth
                    if (!(key === 'concurrency' && depth > 0)) {
                        config[key] = fieldDef.defaultValue;
                    }
                    return; // Skip to next field definition
                }

                // Extract value using getter or default logic
                if (fieldDef.valueGetter) {
                    config[key] = fieldDef.valueGetter(inputElement);
                } else {
                    switch (inputElement.type) {
                        case 'checkbox':
                            config[key] = inputElement.checked;
                            break;
                        case 'number':
                            const numVal = parseFloat(inputElement.value);
                            config[key] = isNaN(numVal) && !(key === 'concurrency' && depth > 0) ? fieldDef.defaultValue : numVal;
                            break;
                        case 'select-one':
                            // Assign default only if value is empty AND the field is expected
                            config[key] = inputElement.value === '' && !(key === 'concurrency' && depth > 0) ? fieldDef.defaultValue : inputElement.value;
                            if (config[key] === undefined && inputElement.value === '') {
                                config[key] = fieldDef.defaultValue;
                            }
                            break;
                        case 'textarea':
                        case 'text':
                        default:
                            const trimmedValue = inputElement.value.trim();
                            // Assign default only if value is empty AND the field is expected
                            config[key] = trimmedValue === '' && !(key === 'concurrency' && depth > 0) ? fieldDef.defaultValue : trimmedValue;
                            if (config[key] === undefined && trimmedValue === '') {
                                config[key] = fieldDef.defaultValue;
                            }
                            break;
                    }
                }
            }); // End of simple field processing

            // --- 2. Handle Nested Structures (Custom Selectors List) ---
            // This part runs regardless of the fieldsDefinition passed, but only populates
            // config.customSelectors if a list is found within sectionElement.

            // --- MODIFICATION START: Find the list container more flexibly ---
            let selectorListDiv = sectionElement.querySelector(':scope > .custom-selectors-list'); // Check direct child
            if (!selectorListDiv) {
                // If not direct, check within the known nested wrapper structure
                const nestedWrapper = sectionElement.querySelector(':scope > div[data-role="nested-custom-selectors-wrapper"]');
                if (nestedWrapper) {
                    selectorListDiv = nestedWrapper.querySelector(':scope > .custom-selectors-list');
                    // console.log(`_getConfigFromUI (depth ${depth}): Found nested selector list inside wrapper.`, selectorListDiv);
                }
            }
            /* else {
                console.log(`_getConfigFromUI (depth ${depth}): Found direct selector list.`, selectorListDiv);
            }
   
            if (!selectorListDiv && sectionElement.dataset.role !== 'config-section') { // Don't log for sections not expected to have list
                // console.log(`_getConfigFromUI (depth ${depth}): No selector list found in section.`, sectionElement);
            } */
            // --- MODIFICATION END ---


            if (selectorListDiv) {
                config.customSelectors = []; // Initialize array for this level
                const selectorGroups = selectorListDiv.querySelectorAll(':scope > .custom-selector-group');

                selectorGroups.forEach(group => {
                    // Get config for this specific selector group using its fields definition
                    // The depth passed here is the depth of the *list* container (selectorListDiv.dataset.depth or fallback to current depth)
                    const groupDepth = parseInt(selectorListDiv.dataset.depth, 10) || depth;
                    const csConfig = this._getConfigFromUI(group, this.CUSTOM_SELECTOR_FIELDS, groupDepth);

                    // --- 3. Handle Nested Config *within* a Custom Selector Group (Recursive Part) ---
                    if (csConfig.followLink) {
                        const nestedContainer = group.querySelector(':scope > .nested-config[data-role="nested-config-container"]');
                        const nestedDepth = groupDepth + 1; // Calculate nested depth

                        if (nestedContainer && nestedContainer.style.display !== 'none' && nestedContainer.children.length > 0) {
                            // console.log(`_getConfigFromUI (depth ${depth}): Reading nested config for group at depth ${groupDepth}. Nested container found.`, nestedContainer);

                            // Recursively get the *entire* nested config object (top-level fields and its own selectors)
                            // by calling _getConfigFromUI on the nestedContainer.
                            // Pass both field definitions so it reads everything inside.
                            // We don't need two separate calls anymore. _getConfigFromUI will handle reading
                            // both simple fields and the custom selector list *within* the nestedContainer.
                            const fullNestedConfig = this._getConfigFromUI(
                                nestedContainer,
                                this.TOP_LEVEL_CONFIG_FIELDS, // Provide definitions for simple fields inside
                                nestedDepth // Pass the correct nested depth
                                // The function will *also* look for a custom selector list inside nestedContainer
                            );


                            // The result 'fullNestedConfig' will have keys from TOP_LEVEL_CONFIG_FIELDS
                            // AND potentially a 'customSelectors' key if a list was found inside.
                            csConfig.nextConfig = fullNestedConfig;


                            // console.log(`_getConfigFromUI (depth ${depth}): Finished reading nested config. Result:`, JSON.parse(JSON.stringify(csConfig.nextConfig)));

                        } else {
                            // console.log(`_getConfigFromUI (depth ${depth}): Nested container for group at depth ${groupDepth} is hidden or empty. Setting nextConfig to empty object.`);
                            // If followLink is checked but container is hidden/empty, provide empty config or null?
                            // Let's use null for consistency with the 'else' branch.
                            csConfig.nextConfig = null; // Changed from {} to null
                        }
                    } else {
                        csConfig.nextConfig = null; // Ensure it's null if not following
                    }

                    // Add the complete custom selector config (with potential nextConfig) if valid
                    if (csConfig.selector && csConfig.type && csConfig.attribute) {
                        config.customSelectors.push(csConfig);
                    } else {
                        // Avoid logging warning for empty selector groups the user hasn't filled yet
                        const isEmpty = !(csConfig.selector || csConfig.type || csConfig.attribute || csConfig.followLink);
                        if (!isEmpty) {
                            console.warn("MediaExtractorUI: Skipping custom selector due to missing required fields (selector, type, or attribute):", group, csConfig);
                        }
                    }
                }); // End loop through selector groups

                // If no valid custom selectors were added, remove the empty array
                if (config.customSelectors && config.customSelectors.length === 0) {
                    delete config.customSelectors;
                }
            } // End if (selectorListDiv)

            return config;
        } // End _getConfigFromUI


        // --- Event Handling & Extraction ---

        /**
     * Handles the click event of the "Extract" button.
     * @private
     */
        async _handleExtractClick() {
            if (!this.uiRoot) {
                console.error("MediaExtractorUI: UI Root not found. Cannot extract.");
                this._notifyExtractionComplete(new Error("UI Root not found"), null); // Notify listeners of failure
                return;
            }

            const startUrlInput = this.uiRoot.querySelector('input[data-role="start-url-input"]');
            const outputArea = this.uiRoot.querySelector('.output-area[data-role="output-area"]');
            const extractButton = this.uiRoot.querySelector('button[data-role="extract-btn"]');

            if (!startUrlInput || !outputArea || !extractButton) {
                console.error("MediaExtractorUI: Core UI elements not found.");
                if (outputArea) outputArea.textContent = "Error: UI elements missing.";
                this._notifyExtractionComplete(new Error("Core UI elements not found"), null); // Notify listeners of failure
                return;
            }

            const startUrl = startUrlInput.value.trim();
            let urlError = null;
            if (!startUrl) {
                urlError = new Error('Please enter a valid Start URL.');
            } else {
                try { new URL(startUrl); } catch (e) {
                    urlError = new Error(`Invalid Start URL format: ${e.message}`);
                }
            }

            if (urlError) {
                outputArea.textContent = `Error: ${urlError.message}`;
                outputArea.className = 'output-area error';
                startUrlInput.focus();
                this._notifyExtractionComplete(urlError, null); // Notify listeners of failure
                return;
            }


            // Get config from the top-level section
            const topLevelConfigDiv = this.uiRoot.querySelector('.config-section[data-depth="0"]');
            if (!topLevelConfigDiv) {
                const configError = new Error('Could not find top-level configuration section.');
                outputArea.textContent = `Error: ${configError.message}`;
                outputArea.className = 'output-area error';
                this._notifyExtractionComplete(configError, null); // Notify listeners of failure
                return;
            }

            // Read config using the new data-driven method
            let userConfig;
            try {
                // Find the top-level custom selectors list container (usually inside the fieldset)
                const topLevelCsListContainer = this.uiRoot.querySelector('fieldset > .custom-selectors-list[data-depth="0"]');

                // Read top-level fields (passing depth 0)
                const topLevelFields = this._getConfigFromUI(topLevelConfigDiv, this.TOP_LEVEL_CONFIG_FIELDS, 0);

                // Read custom selectors (passing depth 0 for the list container)
                // Pass the container's parent fieldset to find the list inside
                const customSelectorsData = topLevelCsListContainer ? this._getConfigFromUI(topLevelCsListContainer.parentNode, [{ key: 'customSelectors' }], 0) : {};

                userConfig = {
                    ...topLevelFields,
                    // Make sure customSelectors is an array even if none were found
                    customSelectors: customSelectorsData.customSelectors || []
                };

            } catch (configError) {
                console.error("MediaExtractorUI: Error reading configuration from UI.", configError);
                outputArea.textContent = `Error reading UI configuration: ${configError.message}`;
                outputArea.className = 'output-area error';
                this._notifyExtractionComplete(configError, null); // Notify listeners of failure
                return;
            }

            outputArea.scrollIntoView();
            outputArea.textContent = `Starting extraction from: ${startUrl}...\nConfiguration:\n${JSON.stringify(userConfig, (key, value) => value instanceof RegExp ? value.toString() : value, 2)}`;
            outputArea.className = 'output-area extracting';
            extractButton.disabled = true;
            extractButton.textContent = 'Extracting...';
            console.log("MediaExtractorUI: Starting extraction with config:", JSON.parse(JSON.stringify(userConfig))); // Deep copy for logging

            const startTime = performance.now();
            let results = null;
            let error = null;

            try {
                // Assuming this.extractor.extractFromUrl exists and works with the config structure
                results = await this.extractor.extractFromUrl(startUrl, userConfig);
                const endTime = performance.now();
                const duration = ((endTime - startTime) / 1000).toFixed(2);
                console.log(`MediaExtractorUI: Extraction finished in ${duration} seconds.`);
                console.log("MediaExtractorUI: Results:", results);
                if (results.length > 0) {
                    outputArea.textContent = `Extraction complete!\nFound ${results.length} media items in ${duration} seconds.\n\nResults logged to the browser console (Press F12).`;
                    outputArea.textContent += `\n\nFirst ${Math.min(results.length, 20)} results:\n` + results.slice(0, 20).map(r => `- [${r.type}@d${r.depth}] ${r.url}`).join('\n');
                    outputArea.className = 'output-area success';
                } else {
                    outputArea.textContent = "Extractor found no results. Check console for details.";
                    outputArea.className = 'output-area error';
                }
            } catch (extractionError) {
                const endTime = performance.now();
                const duration = ((endTime - startTime) / 1000).toFixed(2);
                console.error("MediaExtractorUI: Extraction failed.", extractionError);
                outputArea.textContent = `Error during extraction after ${duration} seconds: ${extractionError.message}\nSee console for details.`;
                outputArea.className = 'output-area error';
                error = extractionError; // Store error
            } finally {
                extractButton.disabled = false;
                extractButton.textContent = 'Extract Media';
                // Notify listeners after everything is done
                this._notifyExtractionComplete(error, results);
            }
        }

        /**
     * Handles key presses
     * @param {KeyboardEvent} event
     * @private
     */
        _handleKeyPress(event) {
            if (event.key === 'Escape') {
                this.close();
            } else if (event.key === 'Enter') {
                this._handleExtractClick();
            }
        }

        /**
     * Registers a callback function to be executed when media extraction completes.
     * @param {function(Error|null, Array<MediaResult>|null): void} callback - The function to call.
     *        It receives an error object (or null if successful) as the first argument,
     *        and the array of results (or null if an error occurred) as the second argument.
     */
        onExtractionComplete(callback) {
            if (typeof callback === 'function') {
                this.extractionCompleteCallbacks.push(callback);
            } else {
                console.warn("MediaExtractorUI: Attempted to register a non-function as an extraction complete callback.");
            }
        }

        /**
    * Notifies all registered extraction complete listeners.
    * @param {Error|null} error - The error object if extraction failed, otherwise null.
    * @param {Array<MediaResult>|null} results - The array of results if extraction succeeded, otherwise null.
    * @private
    */
        _notifyExtractionComplete(error, results) {
            this.extractionCompleteCallbacks.forEach((callback, index) => {
                try {
                    callback(error, results);
                } catch (e) {
                    console.error(`MediaExtractorUI: Error executing extraction complete callback at index ${index}:`, e);
                }
            });
        }

        /**
         * Populates the saved configuration dropdown from GM storage.
         * Handles potential errors during loading or parsing the list.
         * @private
         */
        _populateConfigDropdown() {
            if (!this.uiRoot) {
                console.error("MediaExtractorUI: Cannot populate dropdown, UI Root not found.");
                return;
            }
            const selectElement = this.uiRoot.querySelector('select[data-role="config-select"]');
            if (!selectElement) {
                console.error("MediaExtractorUI: Cannot populate dropdown, select element not found.");
                return;
            }

            // Clear existing options except the default placeholder
            selectElement.length = 1; // Keep only the "--- Select Config ---" option

            let configList = [];
            try {
                const configListJson = GM_getValue(CONFIG_LIST_KEY, '[]');
                console.log("MediaExtractorUI: Raw config list JSON from storage:", configListJson); // Log raw value

                if (configListJson) {
                    configList = JSON.parse(configListJson);
                    if (!Array.isArray(configList)) {
                        console.warn('MediaExtractorUI: Invalid format for config list in storage (not an array). Resetting.', configList);
                        configList = [];
                        GM_setValue(CONFIG_LIST_KEY, '[]'); // Reset if invalid format
                    }
                } else {
                    console.warn('MediaExtractorUI: Config list key not found or empty in storage. Initializing.');
                    configList = [];
                    GM_setValue(CONFIG_LIST_KEY, '[]'); // Initialize if missing
                }

                console.log("MediaExtractorUI: Parsed config list:", configList); // Log parsed list

                if (configList.length > 0) {
                    configList.sort(); // Sort names alphabetically
                    configList.forEach(name => {
                        if (typeof name === 'string' && name.trim() !== '') { // Ensure it's a non-empty string
                            const option = document.createElement('option');
                            option.value = name;
                            option.textContent = name;
                            selectElement.appendChild(option);
                            // console.log(`MediaExtractorUI: Added option: ${name}`); // Uncomment for verbose logging
                        } else {
                            console.warn("MediaExtractorUI: Skipping invalid entry in config list:", name);
                        }
                    });
                } else {
                    console.log("MediaExtractorUI: Config list is empty. No options added.");
                }

            } catch (e) {
                console.error('MediaExtractorUI: Error parsing config list from storage:', e);
                // Optionally reset the list if parsing fails critically
                try {
                    GM_setValue(CONFIG_LIST_KEY, '[]');
                    console.log("MediaExtractorUI: Reset config list due to parsing error.");
                } catch (resetError) {
                    console.error("MediaExtractorUI: Failed to reset config list after parsing error:", resetError);
                }
                // Ensure dropdown is empty (except placeholder) after error
                selectElement.length = 1;
            }
            console.log("MediaExtractorUI: Dropdown population complete. Current options:", selectElement.options.length);
        }

        /**
         * Handles the click event for the "Save Config" button.
         * Reads the current UI config, validates the name, and saves to GM storage.
         * Stores the saved name as the last used config.
         * @private
         */
        _handleSaveConfigClick() {
            if (!this.uiRoot) return;

            const saveNameInput = this.uiRoot.querySelector('input[data-role="config-save-name"]');
            const statusSpan = this.uiRoot.querySelector('.config-status[data-role="config-status-message"]');
            const selectElement = this.uiRoot.querySelector('select[data-role="config-select"]');
            const startUrlInput = this.uiRoot.querySelector('input[data-role="start-url-input"]'); // Get Start URL input

            if (!saveNameInput || !statusSpan || !selectElement || !startUrlInput) { // Check for startUrlInput too
                console.error("Config Management or Start URL UI elements not found for saving.");
                if (statusSpan) statusSpan.textContent = "Error: UI elements missing.";
                return;
            }

            const configName = saveNameInput.value.trim();
            if (!configName) {
                statusSpan.textContent = "Error: Please enter a name for the configuration.";
                saveNameInput.focus();
                return;
            }
            if (configName.includes('_') || configName.includes(' ')) {
                statusSpan.textContent = "Error: Config name cannot contain spaces or underscores.";
                saveNameInput.focus();
                return;
            }


            let currentConfig;
            try {
                // Reuse the logic from _handleExtractClick to get the full config object
                const topLevelConfigDiv = this.uiRoot.querySelector('.config-section[data-depth="0"]');
                const topLevelCsFieldset = this.uiRoot.querySelector('fieldset[data-role="custom-selectors-fieldset"]'); // Get the fieldset

                if (!topLevelConfigDiv || !topLevelCsFieldset) {
                    throw new Error('Could not find configuration sections.');
                }

                const topLevelFields = this._getConfigFromUI(topLevelConfigDiv, this.TOP_LEVEL_CONFIG_FIELDS, 0);
                // Pass the fieldset containing the list to _getConfigFromUI for custom selectors
                const customSelectorsData = this._getConfigFromUI(topLevelCsFieldset, [{ key: 'customSelectors' }], 0);

                // --- Get Start URL ---
                const startUrlValue = startUrlInput.value.trim();

                currentConfig = {
                    startUrl: startUrlValue, // Add start URL here
                    ...topLevelFields,
                    customSelectors: customSelectorsData.customSelectors || []
                };

                // Clean up potential empty nextConfig objects if followLink is false
                const cleanConfig = (config) => {
                    if (config.customSelectors) {
                        config.customSelectors.forEach(cs => {
                            if (!cs.followLink) {
                                delete cs.nextConfig; // Remove nextConfig if followLink is false
                            } else if (cs.nextConfig) {
                                cleanConfig(cs.nextConfig); // Recursively clean nested configs
                            }
                        });
                    }
                };
                cleanConfig(currentConfig);


            } catch (configError) {
                console.error("MediaExtractorUI: Error reading configuration for saving.", configError);
                statusSpan.textContent = `Error reading UI config: ${configError.message}`;
                return;
            }

            try {
                const configKey = CONFIG_STORAGE_PREFIX + configName;
                const configJson = JSON.stringify(currentConfig, (key, value) => {
                    // Special handling for RegExp during serialization
                    if (value instanceof RegExp) {
                        return { __type: 'RegExp', source: value.source, flags: value.flags };
                    }
                    return value;
                });


                // Update the list of names
                const configListJson = GM_getValue(CONFIG_LIST_KEY, '[]');
                let configList = JSON.parse(configListJson);
                if (!Array.isArray(configList)) configList = []; // Ensure it's an array

                if (!configList.includes(configName)) {
                    configList.push(configName);
                    GM_setValue(CONFIG_LIST_KEY, JSON.stringify(configList));
                }

                // Save the actual config
                GM_setValue(configKey, configJson);

                // Refresh the dropdown
                this._populateConfigDropdown();
                // Ensure the newly saved/updated config is selected
                selectElement.value = configName;

                statusSpan.textContent = `Configuration "${configName}" saved successfully.`;
                console.log(`MediaExtractorUI: Config "${configName}" saved.`, currentConfig);

                // --- ADDED: Store as last used config ---
                this._setLastUsedConfig(configName);
                // --- END ADDED ---

            } catch (e) {
                console.error('MediaExtractorUI: Error saving configuration to storage:', e);
                statusSpan.textContent = `Error saving config: ${e.message}`;
            }
        }

        /**
         * Handles the click event for the "Delete Config" button.
         * Removes the selected configuration from GM storage, updates the list,
         * refreshes the dropdown, and resets the selection.
         * @private
         */
        _handleDeleteConfigClick() {
            if (!this.uiRoot) return;

            const selectElement = this.uiRoot.querySelector('select[data-role="config-select"]');
            const statusSpan = this.uiRoot.querySelector('.config-status[data-role="config-status-message"]');
            const saveNameInput = this.uiRoot.querySelector('input[data-role="config-save-name"]');

            if (!selectElement || !statusSpan || !saveNameInput) {
                console.error("Config Management UI elements not found for deleting.");
                if (statusSpan) statusSpan.textContent = "Error: UI elements missing.";
                return;
            }

            const configName = selectElement.value;
            if (!configName) {
                statusSpan.textContent = "Please select a configuration to delete.";
                return;
            }

            // Use a more specific confirmation message
            if (!confirm(`Are you sure you want to permanently delete the configuration "${configName}"? This cannot be undone.`)) {
                statusSpan.textContent = "Deletion cancelled.";
                return;
            }

            try {
                const configKey = CONFIG_STORAGE_PREFIX + configName;

                // 1. Remove the config itself
                GM_deleteValue(configKey);
                console.log(`MediaExtractorUI: Deleted config data for key: ${configKey}`);

                // 2. Update the list of names
                const configListJson = GM_getValue(CONFIG_LIST_KEY, '[]');
                let configList = [];
                try {
                    configList = JSON.parse(configListJson);
                    if (!Array.isArray(configList)) { // Ensure it's an array
                        console.warn("MediaExtractorUI: Config list was not an array, resetting during delete.", configList);
                        configList = [];
                    }
                } catch (parseError) {
                    console.error("MediaExtractorUI: Error parsing config list during delete, resetting.", parseError);
                    configList = [];
                }

                const originalLength = configList.length;
                configList = configList.filter(name => name !== configName); // Filter out the deleted name

                if (configList.length < originalLength) {
                    GM_setValue(CONFIG_LIST_KEY, JSON.stringify(configList));
                    console.log(`MediaExtractorUI: Updated config list in storage after deleting "${configName}". New list:`, configList);
                } else {
                    console.warn(`MediaExtractorUI: Config name "${configName}" not found in the stored list during deletion.`);
                }


                // 3. Refresh the dropdown using the updated list from storage
                this._populateConfigDropdown();

                // --- IMPORTANT FIXES ---
                // 4. Explicitly reset the dropdown selection to the default placeholder
                selectElement.value = ''; // Set value to the placeholder's value

                // 5. Clear the save name input, regardless of whether it matched the deleted item
                saveNameInput.value = '';

                // 6. Clear the last used config if the deleted one was the last used
                const lastUsed = GM_getValue(CONFIG_LAST_USED_KEY);
                if (lastUsed === configName) {
                    this._setLastUsedConfig(null); // Clear it
                    console.log(`MediaExtractorUI: Cleared last used config setting as "${configName}" was deleted.`);
                }
                // --- END FIXES ---

                statusSpan.textContent = `Configuration "${configName}" deleted successfully.`;
                console.log(`MediaExtractorUI: Config "${configName}" deleted.`);
                // Optionally, trigger a UI reset to default state after deletion?
                // this._handleConfigSelectChange({ target: { value: '' } }); // Simulate selecting "--- Select ---"

            } catch (e) {
                console.error(`MediaExtractorUI: Error deleting configuration "${configName}":`, e);
                statusSpan.textContent = `Error deleting config: ${e.message}`;
            }
        }
        /**
         * Updates the UI elements to reflect the state of a loaded configuration object.
         * @param {object} configData - The configuration object to load into the UI.
         * @private
         */
        _updateUIFromConfig(configData) {
            if (!this.uiRoot || !configData) return;

            console.log("Updating UI from config:", JSON.parse(JSON.stringify(configData))); // Log deep copy

            // Helper function to update a single config section
            const updateSection = (parentElement, fieldsDefinition, currentConfigLevelData, depth) => {
                if (!currentConfigLevelData) {
                    console.warn(`_updateUIFromConfig: No config data provided for section at depth ${depth} in parent:`, parentElement);
                    return;
                }

                fieldsDefinition.forEach(fieldDef => {
                    const key = fieldDef.key;
                    if (key === 'customSelectors' || key === 'nextConfig') return;
                    if (key === 'concurrency' && depth > 0) return;

                    let inputElement = parentElement.querySelector(`[data-key="${key}"]`);
                    if (!inputElement) {
                        const wrapper = Array.from(parentElement.querySelectorAll(':scope > .field-wrapper')).find(
                            wrap => wrap.querySelector(`[data-key="${key}"]`)
                        );
                        inputElement = wrapper ? wrapper.querySelector(`[data-key="${key}"]`) : null;
                    }

                    if (inputElement) {
                        const valueToSet = currentConfigLevelData.hasOwnProperty(key) ? currentConfigLevelData[key] : fieldDef.defaultValue;
                        const displayValue = fieldDef.valueSetter ? fieldDef.valueSetter(valueToSet) : (valueToSet ?? '');

                        switch (inputElement.type) {
                            case 'checkbox':
                                inputElement.checked = valueToSet === true;
                                // --- MODIFICATION START ---
                                // ALWAYS call the onChange handler *after* setting the value
                                // if one is defined, to ensure dependent UI updates correctly on load.
                                if (fieldDef.onChange) {
                                    // We need to simulate the event object minimally and find the wrapper.
                                    const pseudoEvent = { target: inputElement };
                                    const fieldWrapper = inputElement.closest('.field-wrapper');
                                    if (fieldWrapper) {
                                        // Call the handler directly with the necessary context.
                                        // Assuming the handler is bound correctly (like _handleFollowLinkChange).
                                        try {
                                            fieldDef.onChange(pseudoEvent, fieldWrapper, depth);
                                        } catch (e) {
                                            console.error(`Error executing onChange handler for key "${key}" during config load:`, e);
                                        }
                                    } else {
                                        console.warn(`Could not find field wrapper for key "${key}" during onChange trigger on load.`);
                                    }
                                }
                                // --- MODIFICATION END ---
                                break;
                            case 'number':
                            case 'text':
                            case 'select-one':
                            case 'textarea':
                            default:
                                const previousValue = inputElement.value; // Store previous value
                                inputElement.value = displayValue;
                                // Trigger change event for select/text/etc. if value changed and handler exists
                                if (inputElement.value !== previousValue && fieldDef.onChange) {
                                    // For non-checkboxes, dispatching event might still be okay,
                                    // but manual call like above is also an option. Let's keep dispatch for now.
                                    const event = new Event('change', { bubbles: true });
                                    inputElement.dispatchEvent(event);
                                }
                                break;
                        }
                    } else if (!(key === 'concurrency' && depth > 0)) {
                        console.warn(`_updateUIFromConfig: Input element not found for key "${key}" at depth ${depth}`, parentElement);
                    }
                });
            }; // End of updateSection helper

            // --- Update Start URL (no change) ---
            const startUrlInput = this.uiRoot.querySelector('input[data-role="start-url-input"]');
            if (startUrlInput) {
                startUrlInput.value = configData.startUrl || '';
            } else {
                console.error("_updateUIFromConfig: Start URL input not found.");
            }

            // --- Update Top-Level Fields (no change) ---
            const topLevelConfigDiv = this.uiRoot.querySelector('.config-section[data-depth="0"]');
            if (topLevelConfigDiv) {
                updateSection(topLevelConfigDiv, this.TOP_LEVEL_CONFIG_FIELDS, configData, 0);
            } else {
                console.error("_updateUIFromConfig: Top-level config section not found.");
            }

            // --- Update Top-Level Custom Selectors (no change in this part) ---
            const topLevelCsFieldset = this.uiRoot.querySelector('fieldset[data-role="custom-selectors-fieldset"]');
            if (topLevelCsFieldset) {
                // Define updateCustomSelectors helper inside or ensure it's accessible
                const updateCustomSelectors = (listContainerParent, selectorsData, depth) => {
                    let listContainer = listContainerParent.querySelector('.custom-selectors-list');
                    if (listContainer) {
                        listContainer.innerHTML = ''; // Clear existing
                    } else {
                        console.warn("_updateUIFromConfig: custom-selectors-list container not found, creating.", listContainerParent);
                        listContainer = document.createElement('div');
                        listContainer.classList.add('custom-selectors-list');
                        listContainer.dataset.depth = depth;
                        const addButton = listContainerParent.querySelector('.add-button');
                        if (addButton) { listContainerParent.insertBefore(listContainer, addButton); }
                        else { listContainerParent.appendChild(listContainer); }
                    }
                    if (Array.isArray(selectorsData)) {
                        selectorsData.forEach((csData, index) => {
                            this._addCustomSelector(listContainer, csData, depth, index);
                        });
                    }
                };
                updateCustomSelectors(topLevelCsFieldset, configData.customSelectors || [], 0);
            } else {
                console.error("_updateUIFromConfig: Top-level custom selectors fieldset not found.");
            }

        } // End of _updateUIFromConfig

        /**
     * Stores the name of the last successfully loaded or saved configuration.
     * @param {string} configName - The name of the configuration.
     * @private
     */
        _setLastUsedConfig(configName) {
            try {
                if (configName) {
                    GM_setValue(CONFIG_LAST_USED_KEY, configName);
                } else {
                    // If configName is empty/null, remove the key
                    GM_deleteValue(CONFIG_LAST_USED_KEY);
                }
            } catch (e) {
                console.error("MediaExtractorUI: Error setting last used config:", e);
            }
        }

        /**
 * Loads and parses a configuration object from storage by its name.
 * Handles RegExp deserialization.
 * @param {string} configName - The name of the configuration to load.
 * @returns {object|null} The loaded configuration object, or null if not found or error occurs.
 * @private
 */
        _loadConfigByName(configName) {
            if (!configName) return null;

            const configKey = CONFIG_STORAGE_PREFIX + configName;
            try {
                const configJson = GM_getValue(configKey);
                if (!configJson) {
                    console.warn(`MediaExtractorUI: Configuration "${configName}" not found in storage.`);
                    return null;
                }

                const config = JSON.parse(configJson, (key, value) => {
                    // Special handling for RegExp during deserialization
                    if (value && typeof value === 'object' && value.__type === 'RegExp') {
                        return new RegExp(value.source, value.flags);
                    }
                    return value;
                });
                return config;

            } catch (e) {
                console.error(`MediaExtractorUI: Error loading or parsing configuration "${configName}":`, e);
                return null;
            }
        }

        /**
         * Handles the 'change' event on the saved configurations dropdown.
         * Loads the selected configuration instantly into the UI, or resets the UI
         * to defaults if the placeholder option is selected.
         * @param {Event} event - The change event object.
         * @private
         */
        _handleConfigSelectChange(event) {
            if (!this.uiRoot) return;

            const selectElement = event.target;
            const configName = selectElement.value;
            const statusSpan = this.uiRoot.querySelector('.config-status[data-role="config-status-message"]');
            const saveNameInput = this.uiRoot.querySelector('input[data-role="config-save-name"]');

            if (!statusSpan || !saveNameInput) {
                console.error("Config Management UI elements not found for handling select change.");
                return;
            }

            // Always update the save name input to match the selection
            saveNameInput.value = configName;
            statusSpan.textContent = ''; // Clear previous status

            if (!configName) {
                // "--- Select Config ---" chosen - Reset UI to defaults
                console.log("MediaExtractorUI: Resetting UI to defaults.");

                // --- Create a default config object ---
                const defaultConfig = {
                    startUrl: '', // Empty Start URL
                    customSelectors: [] // Empty custom selectors list
                };
                // Populate with defaults from TOP_LEVEL_CONFIG_FIELDS definition
                this.TOP_LEVEL_CONFIG_FIELDS.forEach(fieldDef => {
                    // Only add if it has a defined defaultValue, excluding structural keys
                    if (fieldDef.defaultValue !== undefined && fieldDef.key !== 'customSelectors') {
                        defaultConfig[fieldDef.key] = fieldDef.defaultValue;
                    }
                });
                // Also ensure nested defaults like concurrency are present if defined in MediaExtractor defaults
                if (typeof MediaExtractor !== 'undefined' && MediaExtractor.DEFAULT_CONFIG) {
                    Object.assign(defaultConfig, { // Merge, letting our specific defaults take precedence
                        concurrency: MediaExtractor.DEFAULT_CONFIG.concurrency ?? 5 // Example default
                        // Add other potential base defaults if needed
                    });
                }
                // --- End default config object creation ---


                try {
                    this._updateUIFromConfig(defaultConfig); // Apply defaults to UI
                    statusSpan.textContent = "UI reset to defaults.";
                    this._setLastUsedConfig(null); // Clear last used config name as none is selected
                } catch (e) {
                    console.error("MediaExtractorUI: Error resetting UI to defaults:", e);
                    statusSpan.textContent = "Error resetting UI.";
                }

                return; // Stop processing, UI is reset
            }

            // --- Proceed with loading the selected config ---
            const config = this._loadConfigByName(configName);

            if (config) {
                try {
                    this._updateUIFromConfig(config);
                    statusSpan.textContent = `Configuration "${configName}" loaded.`;
                    console.log(`MediaExtractorUI: Config "${configName}" loaded via dropdown.`, config);
                    this._setLastUsedConfig(configName); // Store as last used
                } catch (e) {
                    console.error(`MediaExtractorUI: Error applying configuration "${configName}" to UI:`, e);
                    statusSpan.textContent = `Error applying config: ${e.message}`;
                    // Failed to apply, clear last used?
                    this._setLastUsedConfig(null);
                }
            } else {
                // Config wasn't found or failed to parse (error logged in _loadConfigByName)
                statusSpan.textContent = `Error loading "${configName}". Check console.`;
                // Clear last used if loading failed
                this._setLastUsedConfig(null);
                // Optionally reset UI fully here too? Or leave the previous state?
                // Leaving previous state might be less confusing than a sudden reset on load failure.
                // this._handleConfigSelectChange({ target: { value: '' } }); // Simulate selecting "--- Select ---"
            }
        }

        /**
 * Attempts to load the last used configuration when the UI is initialized.
 * Updates the UI, dropdown selection, and save name input if successful.
 * @private
 */
        _loadLastUsedConfig() {
            if (!this.uiRoot) return;

            const lastUsedName = GM_getValue(CONFIG_LAST_USED_KEY);
            if (!lastUsedName) {
                console.log("MediaExtractorUI: No last used configuration found.");
                return; // Nothing to load
            }

            console.log(`MediaExtractorUI: Found last used config name: "${lastUsedName}". Attempting to load.`);
            const config = this._loadConfigByName(lastUsedName);

            if (config) {
                const selectElement = this.uiRoot.querySelector('select[data-role="config-select"]');
                const saveNameInput = this.uiRoot.querySelector('input[data-role="config-save-name"]');
                const statusSpan = this.uiRoot.querySelector('.config-status[data-role="config-status-message"]');

                try {
                    this._updateUIFromConfig(config);

                    // Update dropdown and save name input
                    if (selectElement) selectElement.value = lastUsedName;
                    if (saveNameInput) saveNameInput.value = lastUsedName;
                    if (statusSpan) statusSpan.textContent = `Loaded last used config: "${lastUsedName}".`;

                    console.log(`MediaExtractorUI: Successfully loaded and applied last used config "${lastUsedName}".`);
                } catch (e) {
                    console.error(`MediaExtractorUI: Error applying last used config "${lastUsedName}" to UI:`, e);
                    if (statusSpan) statusSpan.textContent = `Error applying last config: ${e.message}`;
                    // Clear last used if applying failed?
                    // this._setLastUsedConfig(null);
                }
            } else {
                // Config not found or failed parse (error logged in _loadConfigByName)
                // Remove the invalid last used key
                console.warn(`MediaExtractorUI: Could not load last used config "${lastUsedName}", removing stored key.`);
                this._setLastUsedConfig(null);
                const statusSpan = this.uiRoot.querySelector('.config-status[data-role="config-status-message"]');
                if (statusSpan) statusSpan.textContent = `Could not load last config: "${lastUsedName}".`;
            }
        }
    }// ----------------------------------------------------------------------------------------------
    // main.js
    // ----------------------------------------------------------------------------------------------

    // ----------------------------------------------------------------------------------------------
    // DEFAULT CONFIGURATION
    // ----------------------------------------------------------------------------------------------

    const defaultConfigValues = {
        viewer: {
            _tabName: "Viewer",
            _subgroup: false, // To access values with config.key instead of config.groupName.key
            useFullscreen: { default: true, label: "Open in Fullscreen", requiresReload: false },
            openInDualPageMode: { default: false, label: "Open in dual page mode", requiresReload: false },
            openInRightToLeftMode: { default: false, label: "Open in right to left mode", requiresReload: false },
            reverseNavigationInRtlMode: { default: false, label: "Reverse Navigation in Right to Left mode", requiresReload: false },
            exitToViewerPage: { default: false, label: "Highlight Current Page After Exiting", requiresReload: false },
            useOriginalImages: { default: false, label: "Use original images (slower, limited)", requiresReload: false },
            useFallbackImages: {
                default: true, label: "Use original images as fallback", requiresReload: false,
                condition: ["viewer.useOriginalImages", false], onConditionFail: { label: "Use webp images as fallback" }
            },
            enableFallbackStyling: { default: true, label: "Enable fallback styling for /s/ links", requiresReload: false },
            alwaysRestoreViewer: { default: false, label: "Always restore viewer after reload" },
            enableAnimations: { default: false, label: "Enable smooth animations", hidden: true },
            panFirst: { default: true, label: "Pan before changing page when zoomed", requiresReload: false },
            panStep: { default: 250, label: "Pan step", requiresReload: false, condition: ["viewer.panFirst", true] },
            fitMode: { default: "fit-window", label: "Default Fit Mode", choices: ["fit-window", "fit-width", "one-to-one"], requiresReload: false },
            dualLayout: { default: "selected-first", label: "Dual page layout", choices: ["selected-first", "odd-first", "even-first"], requiresReload: false },
            viewerLabels: {
                default: "always", label: "Page Number Visibility", requiresReload: false,
                choices: ["proximity", "always", "disabled"]
            },
            preloadCount: { default: 5, label: "Number of Images to Preload", requiresReload: false },
            zoomAnimation: { default: true, label: "Enable Zoom Animation", hidden: true, requiresReload: false },
            useFetchFallback: { default: true, label: "Use fetch fallback for videos", hidden: true, requiresReload: false },
        },
        toolbar: {
            _tabName: "Toolbar",
            _subgroup: false,
            helpAndSettingsButtons: {
                default: "always", label: "Toolbar Visibility", requiresReload: false,
                choices: ["proximity", "always", "disabled"]
            },
            buttonSize: { default: 38, label: "Button Size (in pixels)", requiresReload: false },
            showChaptersButton: { default: true, label: "Show Chapters Button (☰)", requiresReload: false },
            showSettingsButton: { default: true, label: "Show Settings Button (⚙)", requiresReload: false },
            showHelpButton: { default: true, label: "Show Help Button (?)", requiresReload: false },
            showExitButton: { default: true, label: "Show Exit Button (×)", requiresReload: false },
            showNavButtons: { default: false, label: "Show Navigation Buttons (← →)", requiresReload: false },
            showRotateButtons: { default: false, label: "Show Rotate Buttons (↶ ↷)", requiresReload: false },
            showZoomButtons: { default: false, label: "Show Zoom Buttons (+ -)", requiresReload: false },
            showGalleryViewButton: { default: true, label: "Show Gallery View Button", requiresReload: false },
            showDualPageButton: { default: true, label: "Show Toggle Dual Page Button", requiresReload: false },
            showFullscreenButton: { default: true, label: "Show Fullscreen Button (⛶)", requiresReload: false },
            showDownloadButton: { default: false, label: "Show Download Button", requiresReload: false },
            showFindGalleriesButton: { default: false, label: "Show 'Find Galleries' Button", requiresReload: false },
            showGotoPageButton: { default: true, label: "Show 'Go To Page' Button", requiresReload: false },
        },
        sidebar: {
            _tabName: "Sidebar",
            enableSidebar: { default: true, label: "Enable Sidebar" },
            pinSidebar: { default: false, label: "Pin Sidebar", requiresReload: false, condition: ["sidebar.enableSidebar", true] },
            sidebarGridConfig: {
                showPageNumbers: { default: true, label: "Show page numbers", condition: ["sidebar.enableSidebar", true] },
                numColumns: { default: 1, label: "Number of columns", hidden: true },
                fetchFullImages: { default: true, label: "Fetch full images", requiresReload: false, condition: ["sidebar.enableSidebar", true] },
            },
            sidebarPosition: { default: "left", label: "Sidebar Position", choices: ["left", "right"], condition: ["sidebar.enableSidebar", true] },
            sidebarWidth: { default: 400, label: "Sidebar Width", condition: ["sidebar.enableSidebar", true] },
            _subgroup: false,
        },
        gallery: {
            _tabName: "Gallery",
            gridViewConfig: {
                showPageNumbers: { default: false, label: "Show page numbers" },
                fetchFullImages: { default: true, label: "Fetch full images", requiresReload: false },
                numColumns: { default: 4, label: "Number of columns" },
            },
            _subgroup: false,
        },
        embeddedGallery: {
            _tabName: "Gallery (Embedded)",
            replaceDefaultGridView: { default: true, label: "Enable (replaces default grid view)" },
            embeddedViewFullWidth: {
                default: false, label: "Make embedded gallery view span the full width",
                condition: ["embeddedGallery.replaceDefaultGridView", true]
            },
            embeddedGridGotoOpensViewer: {
                default: false, label: "Goto box opens viewer instead of scrolling", requiresReload: false,
                condition: ["embeddedGallery.replaceDefaultGridView", true]
            },
            embeddedGridViewConfig: {
                showPageNumbers: { default: false, label: "Show page numbers", condition: ["embeddedGallery.replaceDefaultGridView", true] },
                fetchFullImages: { default: true, label: "Fetch full images", requiresReload: false, condition: ["embeddedGallery.replaceDefaultGridView", true] },
                numColumns: { default: 4, label: "Number of columns", condition: ["embeddedGallery.replaceDefaultGridView", true] },
            },
            _subgroup: false,
        },
        videoConfig: {
            _tabName: "Video",
            _hidden: true,
            autoplay: { default: true, label: "Autoplay Videos" },
            loop: { default: true, label: "Loop Playback" },
        },
    };


    // ----------------------------------------------------------------------------------------------
    // CONSTANTS & STATE
    // ----------------------------------------------------------------------------------------------

    const PAGINATION = 20;
    const THUMB_WIDTH = 200;
    const THUMB_HEIGHT = 284;

    const baseUrl = `${window.location.protocol}//${window.location.host}`;

    // initialized in init()
    let galleryId = null;
    let totalPages = null;
    let totalGalleryImages = null;
    let galleryUsesSpritesheets = null;

    let config = null; // Config
    let thumbs = null; // ThumbCollection
    let chapterList = null;

    let imageViewer = null; // ImageViewer
    let embeddedGridView = null; // GridView

    // ----------------------------------------------------------------------------------------------
    // INITIALIZATION
    // ----------------------------------------------------------------------------------------------

    function initializeGlobalShortcuts() {
        document.addEventListener("keydown", (e) => {
            if (e.ctrlKey || e.altKey || e.metaKey) return;

            const targetElement = e.target;
            if (targetElement && (
                targetElement.tagName === 'INPUT' ||
                targetElement.tagName === 'TEXTAREA' ||
                targetElement.isContentEditable
            )) {
                return;
            }

            if (e.key === 'g' || e.key === 'G') {
                e.preventDefault(); // Prevent 'g' from being typed
                showGotoPageInput();
                return;
            } else if (e.key === 'p' || e.key === 'P') {
                if (config.showingUI()) {
                    config.closeUI();
                } else {
                    config.showUI();
                }
            }
        });
    }

    function initializeThumbnailListeners() {
        thumbs.forEach((thumbObj, index) => {
            if (!thumbObj)
                return;
            thumbObj.link.addEventListener("click", (e) => {
                e.preventDefault();
                imageViewer.loadAndShowIndex(index);
            });
        });
    }

    function processComments() {
        const c6Divs = document.querySelectorAll('.c6');
        let bestResult = null;
        let maxSuitableLinks = 0;

        // First pass: try to find comments with chapter links.
        c6Divs.forEach(div => {
            const suitableLinks = [];
            const links = div.querySelectorAll('a');

            links.forEach(link => {
                const href = link.getAttribute('href');
                // Check if href exists and contains the gallery id with a hyphen.
                if (href && href.includes(`${galleryId}-`)) {
                    const match = href.match(/\d+$/);
                    if (match) {
                        const index = parseInt(match[0], 10) - 1;
                        // Add click event for imageViewer.
                        link.addEventListener('click', (e) => {
                            e.preventDefault();
                            imageViewer.loadAndShowIndex(index);
                        });

                        // Extract the text description immediately following the <a> element.
                        let description = "";
                        let sibling = link.nextSibling;
                        while (sibling) {
                            if (sibling.nodeType === Node.TEXT_NODE) {
                                if (sibling.textContent.includes('\n')) {
                                    description += sibling.textContent.split('\n')[0];
                                    break;
                                } else {
                                    description += sibling.textContent;
                                }
                            } else if (
                                sibling.nodeType === Node.ELEMENT_NODE &&
                                sibling.tagName.toUpperCase() === "BR"
                            ) {
                                break;
                            }
                            sibling = sibling.nextSibling;
                        }
                        description = description.trim();

                        suitableLinks.push({
                            href: href,
                            index: index,
                            linkText: link.innerText.trim(),
                            description: description
                        });
                    }
                }
            });

            // Record the comment if it has at least two suitable links and more than any previous comment.
            if (suitableLinks.length >= 2 && suitableLinks.length > maxSuitableLinks) {
                bestResult = suitableLinks;
                maxSuitableLinks = suitableLinks.length;
            }
        });

        if (bestResult) {
            bestResult.sort((a, b) => a.index - b.index);
            return bestResult;
        }

        /************
         * BONUS: If no chapter list with links was found,
         * fallback to scanning comments for chapter lines.
         *
         * We pre-process the innerHTML for each comment by replacing two or more <br>
         * (and variants like <br/> or <br />) with a single <br> then convert <br> to newline.
         * This helps in cases like your example where double <br> can disrupt the line splitting.
         ************/
        let bestTextResult = null;
        let maxMatchingLines = 0;

        // Regex matching: optional leading zeroes, digits, then one of colon, period, or space followed by optional spaces and text.
        const chapterLineRegex = /^0*(\d+)[\:\.\s]\s*(.+)$/;

        c6Divs.forEach(div => {
            // Use innerHTML to capture <br> tags.
            let html = div.innerHTML;
            // Replace two or more consecutive <br> variants with a single <br>.
            html = html.replace(/(<br\s*\/?>\s*){2,}/gi, '<br>');
            // Replace remaining <br> tags with newline.
            const text = html.replace(/<br\s*\/?>/gi, '\n');
            // Split into lines.
            const lines = text.split(/\r?\n/);
            let matchingLines = [];
            let consecutiveBlock = [];

            // Iterate through each line, tracking consecutive matching lines.
            lines.forEach(line => {
                const trimmedLine = line.trim();
                if (trimmedLine === '') {
                    // Reset block if blank, preserving the longest block found so far.
                    if (consecutiveBlock.length > matchingLines.length) {
                        matchingLines = consecutiveBlock.slice();
                    }
                    consecutiveBlock = [];
                    return;
                }
                const match = trimmedLine.match(chapterLineRegex);
                if (match) {
                    consecutiveBlock.push({
                        index: parseInt(match[1], 10) - 1,
                        description: match[2].trim(),
                        line: trimmedLine
                    });
                } else {
                    if (consecutiveBlock.length > matchingLines.length) {
                        matchingLines = consecutiveBlock.slice();
                    }
                    consecutiveBlock = [];
                }
            });
            // Final check after the loop.
            if (consecutiveBlock.length > matchingLines.length) {
                matchingLines = consecutiveBlock.slice();
            }
            // Save this comment's chapter block if it has at least 2 entries and outperforms previous ones.
            if (matchingLines.length >= 2 && matchingLines.length > maxMatchingLines) {
                bestTextResult = matchingLines;
                maxMatchingLines = matchingLines.length;
            }
        });

        if (bestTextResult) {
            bestTextResult.sort((a, b) => a.index - b.index);
            return bestTextResult;
        }

        return null;
    }

    function initializeEmbeddedGridView() {
        const gridParent = document.getElementById('gdt');
        gridParent.innerHTML = '';
        gridParent.className = 'gt200-nogrid'; // disables default grid css

        if (config.embeddedViewFullWidth) {
            gridParent.style.maxWidth = "none";
        }

        const headers = document.getElementsByClassName('gtb');
        const comments = document.getElementById('cdiv');

        for (let i = 0; i < headers.length; i++) {
            const element = headers[i];
            element.innerHTML = '';
            element.style.height = '10px';
        }

        // Create combined floating navigation button
        const navButton = document.createElement('div');
        navButton.style.position = 'fixed';
        navButton.style.bottom = '20px';
        navButton.style.right = '20px';
        navButton.style.zIndex = '1000';
        navButton.style.display = 'flex';
        navButton.style.flexDirection = 'column';
        navButton.style.gap = '4px';

        const upButton = document.createElement('button');
        upButton.textContent = '▲';
        upButton.style.padding = '12px 24px';
        upButton.style.fontSize = '18px';
        upButton.style.fontWeight = 'bold';
        upButton.style.backgroundColor = '#444';
        upButton.style.color = '#fff';
        upButton.style.border = 'none';
        upButton.style.borderRadius = '4px';
        upButton.style.cursor = 'pointer';
        upButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
        upButton.addEventListener('click', () => {
            window.scrollTo(0, 0);
        });

        const downButton = document.createElement('button');
        downButton.textContent = '▼';
        downButton.style.padding = '12px 24px';
        downButton.style.fontSize = '18px';
        downButton.style.fontWeight = 'bold';
        downButton.style.backgroundColor = '#444';
        downButton.style.color = '#fff';
        downButton.style.border = 'none';
        downButton.style.borderRadius = '4px';
        downButton.style.cursor = 'pointer';
        downButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
        downButton.addEventListener('click', () => {
            if (comments) {
                comments.scrollIntoView();
            }
        });

        navButton.appendChild(upButton);
        navButton.appendChild(downButton);
        document.body.appendChild(navButton);

        embeddedGridView = new GridView(gridParent, config.embeddedGridViewConfig, true);
        embeddedGridView.showGridView();
    }

    // Fallback logic for /s/ urls
    function doFallback() {
        const resizeAndCenterImage = () => {
            const img = document.getElementById("img");
            if (!img) {
                console.warn("No image element found.");
                return;
            }

            if (!img.complete) {
                console.warn("Image not loaded, ignoring.");
                return;
            }

            const naturalWidth = img.naturalWidth;
            const naturalHeight = img.naturalHeight;
            if (!naturalWidth || !naturalHeight) {
                console.warn("Image dimension values missing, ignoring.");
                return;
            }

            // Calculate dimensions and scaling factor to fit the image inside the window.
            const scale = Math.min(window.innerWidth / naturalWidth, window.innerHeight / naturalHeight);
            const newWidth = naturalWidth * scale;
            const newHeight = naturalHeight * scale;

            // Remove any width/height attributes and update inline styles.
            img.removeAttribute("width");
            img.removeAttribute("height");
            img.style.width = newWidth + "px";
            img.style.height = newHeight + "px";

            // Scroll vertically to center the image.
            const imgRectBefore = img.getBoundingClientRect();
            const imgCenterY = window.scrollY + imgRectBefore.top + newHeight / 2;
            const targetScrollY = imgCenterY - window.innerHeight / 2;
            window.scrollTo(window.scrollX, targetScrollY);

            // Remove any existing horizontal translate before recalculating.
            img.style.transform = '';
            const imgRect = img.getBoundingClientRect();
            const currentImgLeft = imgRect.left;
            const desiredImgLeft = (window.innerWidth - newWidth) / 2;
            const translationX = desiredImgLeft - currentImgLeft;
            img.style.transform = `translateX(${translationX}px)`;
        };

        const attachResizeOnImageLoad = () => {
            const img = document.getElementById("img");
            if (!img) return;

            if (img.complete) {
                resizeAndCenterImage();
                // Second pass after a short delay to override other scripts
                setTimeout(resizeAndCenterImage, 100);
            } else {
                img.removeEventListener('load', attachResizeOnImageLoad);
                img.addEventListener('load', attachResizeOnImageLoad);
            }
        };

        const checkAndApplyFallback = () => {
            if (window.location.pathname.startsWith('/s/')) {
                attachResizeOnImageLoad();
            } else {
                console.log("Current URL doesn't match '/s/'; fallback operations not needed.");
            }
        };

        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === 1) {
                        if (node.id === "img" || (node.querySelector && node.querySelector("#img"))) {
                            attachResizeOnImageLoad();
                        }
                    }
                });
            });
        });

        if (document.body) {
            observer.observe(document.body, { childList: true, subtree: true });
        } else {
            window.addEventListener('DOMContentLoaded', () => {
                observer.observe(document.body, { childList: true, subtree: true });
            });
        }

        window.addEventListener('locationchange', () => {
            checkAndApplyFallback();
        });

        checkAndApplyFallback();
    }

    async function init() {
        config = new Config("YAEV Preferences", defaultConfigValues, true, true);

        if (config.enableFallbackStyling && window.location.pathname.startsWith('/s/')) {
            console.warn("/s/ urls are not supported unless they refer to an image in the same gallery id. Please open the image from its index page.");
            console.log("Applying fallback styling and exiting.");
            doFallback();
            return;
        }

        galleryId = getGalleryId();
        totalPages = getTotalPages();
        totalGalleryImages = getTotalImages();
        const intialPageIndex = getPageIndexFromUrl(window.location.href);

        console.log(`Extracted values:\nGallery ID: ${galleryId}\nTotal Images: ${totalGalleryImages}\nTotal Pages: ${totalPages}\nInitial Page: ${intialPageIndex + 1}`);

        const saveIndex = () => {
            if (imageViewer?.isActive()) {
                const currentIndex = imageViewer.currentIndex;
                GM_setValue('_savedViewerIndex', [galleryId, currentIndex]);
                console.log(`Saved current index ${currentIndex} for gallery ${galleryId} for reload`);
            }
        };

        config.onReload(saveIndex);

        if (config.alwaysRestoreViewer) {
            window.addEventListener('beforeunload', saveIndex);
        }

        thumbs = createThumbCollection(new Array(totalGalleryImages).fill(null));

        const initialThumbs = extractThumbnailLinks(document, intialPageIndex);
        // console.log(initialThumbs);
        if (!initialThumbs?.length) {
            console.warn('Could not extract pages');
        }

        galleryUsesSpritesheets = determineIfSpritesheets(initialThumbs);
        console.log(`Gallery uses spritesheets: ${galleryUsesSpritesheets}`);
        populateThumbsOnPage(intialPageIndex, initialThumbs);

        initializeGlobalShortcuts();

        if (!config.replaceDefaultGridView) {
            initializeThumbnailListeners();
        } else {
            initializeEmbeddedGridView();
        }

        try {
            chapterList = processComments();
            if (chapterList) {
                console.log(`Found possible chapter list:`, chapterList);
            }
        } catch (e) {
            console.error(`Error processing comments: ${e}`);
        }

        imageViewer = new ImageViewer(config, (exitToPage) => {
            if (config.replaceDefaultGridView && embeddedGridView) {
                embeddedGridView.enableLoading();
            }
            if (exitToPage) {
                if (!config.replaceDefaultGridView) {
                    const page = thumbs[imageViewer.currentIndex].page;
                    const url = new URL(window.location.href);
                    url.searchParams.set("p", page);
                    console.log(`Loading page ${page}`);
                    window.location.href = url.href;
                } else if (embeddedGridView) {
                    embeddedGridView.scrollToIndex(imageViewer.currentIndex);
                }
            }
        });

        // Check for saved index from previous reload
        const savedData = GM_getValue('_savedViewerIndex', null);
        if (savedData !== null) {
            const [savedGalleryId, savedIndex] = savedData;
            if (savedGalleryId === galleryId) {
                console.log(`Restoring saved index ${savedIndex} for gallery ${galleryId}`);
                GM_deleteValue('_savedViewerIndex');
                imageViewer.loadAndShowIndex(savedIndex);
            } else {
                console.log(`Saved index ${savedIndex} is for different gallery ${savedGalleryId}, deleting`);
                GM_deleteValue('_savedViewerIndex');
            }
        }
    }

    init();

})();