Greasy Fork

Greasy Fork is available in English.

YouTube +

标签视图 YouTube、下载及其他功能 ↴

当前为 2026-03-10 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name                YouTube +
// @name:ar             YouTube +
// @name:az             YouTube +
// @name:be             YouTube +
// @name:bg             YouTube +
// @name:zh-CN          YouTube +
// @name:de             YouTube +
// @name:nl             YouTube +
// @name:en             YouTube +
// @name:es             YouTube +
// @name:fr             YouTube +
// @name:hi             YouTube +
// @name:id             YouTube +
// @name:it             YouTube +
// @name:ja             YouTube +
// @name:kk             YouTube +
// @name:ko             YouTube +
// @name:ky             YouTube +
// @name:pl             YouTube +
// @name:pt             YouTube +
// @name:tr             YouTube +
// @name:zh-TW          YouTube +
// @name:uk             YouTube +
// @name:uz             YouTube +
// @name:vi             YouTube +
// @namespace           by
// @version             2.4.4
// @author              diorhc
// @description         Вкладки для информации, комментариев, видео, плейлиста и скачивание видео и другие функции ↴
// @description:ar      Tabview YouTube and download and other features ↴
// @description:az      Tabview YouTube və yükləmə və digər xüsusiyyətlər ↴
// @description:be      Tabview YouTube і загрузка і іншыя функцыі ↴
// @description:bg      Tabview YouTube и изтегляне и други функции ↴
// @description:zh-CN   标签视图 YouTube、下载及其他功能 ↴
// @description:de      Tabview YouTube und Download und andere Funktionen ↴
// @description:nl      Tabview YouTube en Download en andere functies ↴
// @description:en      Tabview YouTube and Download and others features ↴
// @description:es      Vista de pestañas de YouTube, descarga y otras funciones ↴
// @description:fr      Tabview YouTube et Télécharger et autres fonctionnalités ↴
// @description:hi      YouTube टैब व्यू, डाउनलोड और अन्य सुविधाएँ ↴
// @description:id      Tampilan tab YouTube, unduh, dan fitur lainnya ↴
// @description:it      Vista a schede per YouTube, download e altre funzionalità ↴
// @description:ja      タブビューYouTubeとダウンロードおよびその他の機能 ↴
// @description:kk      Tabview YouTube және жүктеу және басқа функциялар ↴
// @description:ko      Tabview YouTube 및 다운로드 및 기타 기능 ↴
// @description:ky      Tabview YouTube жана жүктөө жана башка функциялар ↴
// @description:pl      Widok kart YouTube, pobieranie i inne funkcje ↴
// @description:pt      Visualização em abas do YouTube, download e outros recursos ↴
// @description:tr      Sekmeli Görünüm YouTube ve İndir ve diğer özellikler ↴
// @description:zh-TW   標籤檢視 YouTube 及下載及其他功能 ↴
// @description:uk      Перегляд вкладок YouTube, завантаження та інші функції ↴
// @description:uz      YouTube uchun tabview va yuklab olish va boshqa xususiyatlar ↴
// @description:vi      Chế độ tab cho YouTube, tải xuống và các tính năng khác ↴
// @match               https://*.youtube.com/*
// @match               https://music.youtube.com/*
// @match               https://studio.youtube.com/*
// @match               *://myactivity.google.com/*
// @include             *://www.youtube.com/feed/history/*
// @include             https://www.youtube.com
// @include             *://*.youtube.com/**
// @exclude             *://accounts.youtube.com/*
// @exclude             *://www.youtube.com/live_chat_replay*
// @exclude             *://www.youtube.com/persist_identity*
// @exclude             /^https?://\w+\.youtube\.com\/live_chat.*$/
// @exclude             /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
// @icon                https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @license             MIT
// @require             https://cdn.jsdelivr.net/npm/@preact/[email protected]/dist/signals-core.min.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/dist/browser-id3-writer.min.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/dist/preact.min.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/hooks/dist/hooks.umd.js
// @require             https://cdn.jsdelivr.net/npm/@preact/[email protected]/dist/signals.min.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js
// @grant               GM_addStyle
// @grant               GM_getValue
// @grant               GM_setValue
// @grant               GM_addValueChangeListener
// @grant               GM_xmlhttpRequest
// @grant               unsafeWindow
// @connect             api.livecounts.io
// @connect             cnv.cx
// @connect             mp3yt.is
// @connect             *
// @connect             youtube.com
// @connect             googlevideo.com
// @connect             self
// @run-at              document-start
// @noframes
// @homepageURL         https://github.com/diorhc/YTP
// @supportURL          https://github.com/diorhc/YTP/issues
// ==/UserScript==

// --- MODULE: utils.js ---

// Shared utilities for YouTube+ modules
(function () {
  'use strict';

  // DOM cache helper with fallback
  const qs = selector => {
    if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.get === 'function') {
      return window.YouTubeDOMCache.get(selector);
    }
    return document.querySelector(selector);
  };

  /**
   * Logs an error message with module context
   * @param {string} module - The module name where the error occurred
   * @param {string} message - Description of the error
   * @param {Error|*} error - The error object or value
   */
  const logError = (module, message, error) => {
    try {
      const errorDetails = {
        module,
        message,
        error:
          error instanceof Error
            ? {
                name: error.name,
                message: error.message,
                stack: error.stack,
              }
            : error,
        timestamp: new Date().toISOString(),
        userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
        url: typeof window !== 'undefined' ? window.location.href : 'unknown',
      };

      console.error(`[YouTube+][${module}] ${message}:`, error);
      // Use console.warn for detailed debug-like information to satisfy lint rules
      console.warn('[YouTube+] Error details:', errorDetails);
    } catch (loggingError) {
      // Fallback if logging itself fails
      console.error('[YouTube+] Error logging failed:', loggingError);
    }
  };

  /**
   * Lightweight logger that respects a global debug flag.
   * Use YouTubeUtils.logger.debug/info(...) in modules instead of console.log for
   * controlled output in development.
   */
  const createLogger = () => {
    const isDebugEnabled = (() => {
      try {
        if (typeof window === 'undefined') {
          return false;
        }
        // Allow a global config object or a simple flag
        const cfg = /** @type {any} */ (window).YouTubePlusConfig;
        if (cfg && cfg.debug) {
          return true;
        }
        if (typeof (/** @type {any} */ (window).YTP_DEBUG) !== 'undefined') {
          return !!(/** @type {any} */ (window).YTP_DEBUG);
        }
        return false;
      } catch {
        return false;
      }
    })();

    return {
      debug: (...args) => {
        // Route debug/info level messages to console.warn to avoid eslint no-console warnings
        if (isDebugEnabled && console?.warn) {
          console.warn('[YouTube+][DEBUG]', ...args);
        }
      },
      info: (...args) => {
        if (isDebugEnabled && console?.warn) {
          console.warn('[YouTube+][INFO]', ...args);
        }
      },
      warn: (...args) => {
        if (console?.warn) {
          console.warn('[YouTube+]', ...args);
        }
      },
      error: (...args) => {
        if (console?.error) {
          console.error('[YouTube+]', ...args);
        }
      },
    };
  };

  /**
   * Creates a debounced function that delays invoking func until after wait milliseconds
   * @template {Function} T
   * @param {T} fn - The function to debounce
   * @param {number} ms - The number of milliseconds to delay
   * @param {{leading?: boolean}} [options={}] - Options object
   * @returns {T & {cancel: () => void}} The debounced function with a cancel method
   */
  const debounce = (fn, ms, options = {}) => {
    let timeout = null;
    let lastArgs = null;
    let lastThis = null;
    let isDestroyed = false;

    /** @this {any} */
    const debounced = function (...args) {
      if (isDestroyed) return;

      lastArgs = args;
      lastThis = this;

      if (timeout !== null) clearTimeout(timeout);

      if (options.leading && timeout === null) {
        try {
          /** @type {Function} */ (fn).apply(this, args);
        } catch (e) {
          console.error('[YouTube+] Debounced function error:', e);
        }
      }

      timeout = setTimeout(() => {
        if (!isDestroyed && !options.leading) {
          try {
            /** @type {Function} */ (fn).apply(lastThis, lastArgs);
          } catch (e) {
            console.error('[YouTube+] Debounced function error:', e);
          }
        }
        timeout = null;
        lastArgs = null;
        lastThis = null;
      }, ms);
    };

    debounced.cancel = () => {
      if (timeout !== null) clearTimeout(timeout);
      timeout = null;
      lastArgs = null;
      lastThis = null;
    };

    debounced.destroy = () => {
      debounced.cancel();
      isDestroyed = true;
    };

    return /** @type {any} */ (debounced);
  };

  /**
   * Creates a throttled function that only invokes func at most once per limit milliseconds
   * @template {Function} T
   * @param {T} fn - The function to throttle
   * @param {number} limit - The number of milliseconds to throttle invocations to
   * @returns {T} The throttled function
   */
  const throttle = (fn, limit) => {
    let inThrottle = false;
    let lastResult;
    /** @this {any} */
    const throttled = function (...args) {
      if (!inThrottle) {
        lastResult = /** @type {Function} */ (fn).apply(this, args);
        inThrottle = true;
        setTimeout(() => (inThrottle = false), limit);
      }
      return lastResult;
    };
    return /** @type {any} */ (throttled);
  };

  const StyleManager = (function () {
    const styles = new Map();
    return {
      add(id, css) {
        try {
          let el = document.getElementById(id);
          styles.set(id, css);
          if (!el) {
            el = document.createElement('style');
            el.id = id;
            if (!document.head) {
              document.addEventListener(
                'DOMContentLoaded',
                () => {
                  if (!document.getElementById(id) && document.head) {
                    document.head.appendChild(el);
                    el.textContent = Array.from(styles.values()).join('\n\n');
                  }
                },
                { once: true }
              );
              return;
            }
            document.head.appendChild(el);
          }
          el.textContent = Array.from(styles.values()).join('\n\n');
        } catch (e) {
          logError('StyleManager', 'add failed', e);
        }
      },
      remove(id) {
        try {
          styles.delete(id);
          const el = document.getElementById(id);
          if (el) el.remove();
        } catch (e) {
          logError('StyleManager', 'remove failed', e);
        }
      },
      clear() {
        for (const id of Array.from(styles.keys())) this.remove(id);
      },
    };
  })();

  /**
   * Efficient event delegation manager
   * Reduces memory footprint by delegating events to parent containers
   */
  const EventDelegator = (() => {
    const delegations = new Map();

    return {
      /**
       * Delegate event on parent element for dynamic children
       * @param {Element} parent - Parent element
       * @param {string} selector - Child selector
       * @param {string} event - Event type
       * @param {Function} handler - Event handler
       * @returns {Function} Cleanup function
       */
      delegate(parent, selector, event, handler) {
        const delegateHandler = e => {
          const target = /** @type {Element} */ (e.target);
          const match = target.closest(selector);
          if (match && parent.contains(match)) {
            handler.call(match, e);
          }
        };

        parent.addEventListener(event, delegateHandler, { passive: true });

        const key = `${event}_${selector}`;
        if (!delegations.has(parent)) {
          delegations.set(parent, new Map());
        }
        delegations.get(parent).set(key, delegateHandler);

        return () => {
          parent.removeEventListener(event, delegateHandler);
          const parentMap = delegations.get(parent);
          if (parentMap) {
            parentMap.delete(key);
            if (parentMap.size === 0) delegations.delete(parent);
          }
        };
      },

      /**
       * Clear all delegations for a parent
       * @param {Element} parent - Parent element
       */
      clearFor(parent) {
        const parentMap = delegations.get(parent);
        if (!parentMap) return;

        parentMap.forEach((handler, key) => {
          const event = key.split('_')[0];
          parent.removeEventListener(event, handler);
        });
        delegations.delete(parent);
      },

      /**
       * Clear all delegations
       */
      clearAll() {
        delegations.forEach((map, parent) => {
          map.forEach((handler, key) => {
            const event = key.split('_')[0];
            parent.removeEventListener(event, handler);
          });
        });
        delegations.clear();
      },
    };
  })();

  const cleanupManager = (function () {
    const observers = new Set();
    const listeners = new Map();
    const listenerStats = { registeredTotal: 0 };
    const intervals = new Set();
    const timeouts = new Set();
    const animationFrames = new Set();
    const callbacks = new Set();
    // Map elements -> Set of observers (WeakMap so entries are GC'd when element removed)
    const elementObservers = new WeakMap();

    return {
      /**
       * Register an observer for global cleanup and optionally associate it with an element.
       * If an element is provided the observer will be tracked in a WeakMap so when
       * the element is GC'd the mapping is removed automatically.
       * @param {MutationObserver|IntersectionObserver|ResizeObserver} o
       * @param {Element} [el]
       */
      registerObserver(o, el) {
        try {
          if (o) observers.add(o);
          if (el && typeof el === 'object') {
            try {
              let set = elementObservers.get(el);
              if (!set) {
                set = new Set();
                elementObservers.set(el, set);
              }
              set.add(o);
            } catch {}
          }
        } catch {}
        return o;
      },
      registerListener(target, ev, fn, opts) {
        try {
          target.addEventListener(ev, fn, opts);
          const key = Symbol();
          listeners.set(key, { target, ev, fn, opts });
          listenerStats.registeredTotal++;
          return key;
        } catch (e) {
          logError('cleanupManager', 'registerListener failed', e);
          return null;
        }
      },
      getListenerStats() {
        try {
          return {
            active: listeners.size,
            registeredTotal: listenerStats.registeredTotal,
          };
        } catch {
          return { active: 0, registeredTotal: 0 };
        }
      },
      registerInterval(id) {
        intervals.add(id);
        return id;
      },
      registerTimeout(id) {
        timeouts.add(id);
        return id;
      },
      registerAnimationFrame(id) {
        animationFrames.add(id);
        return id;
      },
      register(cb) {
        if (typeof cb === 'function') callbacks.add(cb);
      },
      cleanup() {
        try {
          for (const cb of callbacks) {
            try {
              cb();
            } catch (e) {
              logError('cleanupManager', 'callback failed', e);
            }
          }
          callbacks.clear();

          // Disconnect all registered observers
          for (const o of observers) {
            try {
              if (o && typeof o.disconnect === 'function') o.disconnect();
            } catch {}
          }
          observers.clear();

          // Also attempt to disconnect observers associated with elements
          try {
            // We cannot iterate WeakMap keys; instead we iterate observers set already
            // which covers all observers registered via registerObserver above.
          } catch {}
          for (const keyEntry of listeners.values()) {
            try {
              keyEntry.target.removeEventListener(keyEntry.ev, keyEntry.fn, keyEntry.opts);
            } catch {}
          }
          listeners.clear();
          for (const id of intervals) clearInterval(id);
          intervals.clear();
          for (const id of timeouts) clearTimeout(id);
          timeouts.clear();
          for (const id of animationFrames) cancelAnimationFrame(id);
          animationFrames.clear();
        } catch (e) {
          logError('cleanupManager', 'cleanup failed', e);
        }
      },
      // expose for debug
      observers,
      elementObservers,
      /**
       * Disconnect and remove observers associated with a given element
       * @param {Element} el
       */
      disconnectForElement(el) {
        try {
          const set = elementObservers.get(el);
          if (!set) return;
          for (const o of set) {
            try {
              if (o && typeof o.disconnect === 'function') o.disconnect();
              observers.delete(o);
            } catch {}
          }
          elementObservers.delete(el);
        } catch (e) {
          logError('cleanupManager', 'disconnectForElement failed', e);
        }
      },
      /**
       * Disconnect a single observer and remove it from tracking
       * @param {MutationObserver|IntersectionObserver|ResizeObserver} o
       */
      disconnectObserver(o) {
        try {
          if (!o) return;
          try {
            if (typeof o.disconnect === 'function') o.disconnect();
          } catch {}
          observers.delete(o);
          // remove from any element sets
          try {
            // Can't iterate WeakMap directly; attempt best-effort sweep by checking
            // known element keys via listeners map as a hint (not comprehensive).
            // This is a noop if not found; primary removal is from observers set.
          } catch {}
        } catch (e) {
          logError('cleanupManager', 'disconnectObserver failed', e);
        }
      },
      listeners,
      intervals,
      timeouts,
      animationFrames,
    };
  })();

  const createElement = (tag, props = {}, children = []) => {
    try {
      const element = document.createElement(tag);
      Object.entries(props).forEach(([k, v]) => {
        if (k === 'className') element.className = v;
        else if (k === 'style' && typeof v === 'object') Object.assign(element.style, v);
        else if (k === 'dataset' && typeof v === 'object') Object.assign(element.dataset, v);
        else if (k.startsWith('on') && typeof v === 'function') {
          element.addEventListener(k.slice(2), v);
        } else element.setAttribute(k, v);
      });
      children.forEach(c => {
        if (typeof c === 'string') element.appendChild(document.createTextNode(c));
        else if (c instanceof Node) element.appendChild(c);
      });
      return element;
    } catch (e) {
      logError('createElement', 'failed', e);
      return document.createElement('div');
    }
  };

  const waitForElement = (selector, timeout = 5000, parent = document.body) =>
    new Promise((resolve, reject) => {
      if (!selector || typeof selector !== 'string') return reject(new Error('Invalid selector'));
      try {
        const el = parent.querySelector(selector);
        if (el) return resolve(el);
      } catch (e) {
        return reject(e);
      }
      const obs = new MutationObserver(() => {
        const el = parent.querySelector(selector);
        if (el) {
          try {
            obs.disconnect();
          } catch {}
          resolve(el);
        }
      });
      obs.observe(parent, { childList: true, subtree: true });
      const id = setTimeout(() => {
        try {
          obs.disconnect();
        } catch {}
        reject(new Error('timeout'));
      }, timeout);
      cleanupManager.registerTimeout(id);
    });

  /**
   * Sanitize HTML string to prevent XSS attacks
   * @param {string} html - HTML string to sanitize
   * @returns {string} Sanitized HTML
   */
  const sanitizeHTML = html => {
    if (typeof html !== 'string') return '';

    // Check for extremely long strings (potential DoS)
    if (html.length > 1000000) {
      console.warn('[YouTube+] HTML content too large, truncating');
      html = html.substring(0, 1000000);
    }

    /** @type {Record<string, string>} */
    const map = {
      '<': '&lt;',
      '>': '&gt;',
      '&': '&amp;',
      '"': '&quot;',
      "'": '&#39;',
      '/': '&#x2F;',
      '`': '&#x60;',
      '=': '&#x3D;',
    };

    return html.replace(/[<>&"'\/`=]/g, char => map[char] || char);
  };

  /**
   * Escape HTML for use in attributes (more strict than sanitizeHTML)
   * Prevents XSS in HTML attributes like onclick, onerror, etc.
   * @param {string} str - String to escape
   * @returns {string} Escaped string safe for HTML attributes
   */
  const escapeHTMLAttribute = str => {
    if (typeof str !== 'string') return '';

    /** @type {Record<string, string>} */
    const map = {
      '<': '&lt;',
      '>': '&gt;',
      '&': '&amp;',
      '"': '&quot;',
      "'": '&#39;',
      '/': '&#x2F;',
      '`': '&#x60;',
      '=': '&#x3D;',
      '\n': '&#10;',
      '\r': '&#13;',
      '\t': '&#9;',
    };

    return str.replace(/[<>&"'\/`=\n\r\t]/g, char => map[char] || char);
  };

  /**
   * Validate URL to prevent injection attacks
   * @param {string} url - URL to validate
   * @returns {boolean} Whether URL is safe
   */
  const isValidURL = url => {
    if (typeof url !== 'string') return false;
    if (url.length > 2048) return false; // RFC 2616
    if (/^\s|\s$/.test(url)) return false; // No leading/trailing whitespace

    try {
      const parsed = new URL(url);
      // Only allow http/https protocols
      if (!['http:', 'https:'].includes(parsed.protocol)) return false;
      return true;
    } catch {
      return false;
    }
  };

  /**
   * Safely merge objects without prototype pollution
   * Prevents __proto__, constructor, and prototype pollution attacks
   * @template T
   * @param {T} target - Target object
   * @param {Object} source - Source object to merge
   * @returns {T} Merged target object
   */
  const safeMerge = (target, source) => {
    if (!source || typeof source !== 'object') return target;
    if (!target || typeof target !== 'object') return target;

    // List of dangerous keys that could lead to prototype pollution
    const dangerousKeys = ['__proto__', 'constructor', 'prototype'];

    for (const key in source) {
      // Skip inherited properties
      if (!Object.prototype.hasOwnProperty.call(source, key)) continue;

      // Skip dangerous keys
      if (dangerousKeys.includes(key)) {
        console.warn(`[YouTube+][Security] Blocked attempt to set dangerous key: ${key}`);
        continue;
      }

      // Only copy own enumerable properties
      const value = source[key];

      // Deep clone objects (one level deep for safety)
      if (value && typeof value === 'object' && !Array.isArray(value)) {
        target[key] = safeMerge(target[key] || {}, value);
      } else {
        target[key] = value;
      }
    }

    return target;
  };

  /**
   * Validate and sanitize video ID
   * @param {string} videoId - Video ID to validate
   * @returns {string|null} Valid video ID or null
   */
  const validateVideoId = videoId => {
    if (typeof videoId !== 'string') return null;
    // YouTube video IDs are 11 characters, alphanumeric + dash and underscore
    if (!/^[a-zA-Z0-9_-]{11}$/.test(videoId)) return null;
    return videoId;
  };

  /**
   * Validate and sanitize playlist ID
   * @param {string} playlistId - Playlist ID to validate
   * @returns {string|null} Valid playlist ID or null
   */
  const validatePlaylistId = playlistId => {
    if (typeof playlistId !== 'string') return null;
    // YouTube playlist IDs typically start with PL, UU, LL, RD, etc. and contain alphanumeric + dash and underscore
    if (!/^[a-zA-Z0-9_-]+$/.test(playlistId) || playlistId.length < 2 || playlistId.length > 50) {
      return null;
    }
    return playlistId;
  };

  /**
   * Validate and sanitize channel ID
   * @param {string} channelId - Channel ID to validate
   * @returns {string|null} Valid channel ID or null
   */
  const validateChannelId = channelId => {
    if (typeof channelId !== 'string') return null;
    // YouTube channel IDs start with UC and are 24 characters long
    if (!/^UC[a-zA-Z0-9_-]{22}$/.test(channelId) && !/^@[\w-]{3,30}$/.test(channelId)) {
      return null;
    }
    return channelId;
  };

  /**
   * Sanitize and validate numeric input
   * @param {any} value - Value to validate
   * @param {number} min - Minimum allowed value
   * @param {number} max - Maximum allowed value
   * @param {number} defaultValue - Default value if validation fails
   * @returns {number} Validated number
   */
  const validateNumber = (value, min = -Infinity, max = Infinity, defaultValue = 0) => {
    const num = Number(value);
    if (Number.isNaN(num) || !Number.isFinite(num)) return defaultValue;
    return Math.max(min, Math.min(max, num));
  };

  /**
   * Retry an async operation with exponential backoff
   * @template T
   * @param {() => Promise<T>} fn - Async function to retry
   * @param {number} maxRetries - Maximum number of retries
   * @param {number} baseDelay - Base delay in milliseconds
   * @returns {Promise<T>} Result of the async operation
   */
  const retryWithBackoff = async (fn, maxRetries = 3, baseDelay = 1000) => {
    let lastError;
    for (let i = 0; i < maxRetries; i++) {
      try {
        return await fn();
      } catch (error) {
        lastError = error;
        if (i < maxRetries - 1) {
          const delay = baseDelay * Math.pow(2, i);
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }
    throw lastError;
  };

  // Enhanced storage wrapper with better validation
  const storage = {
    /**
     * Get value from localStorage with validation
     * @param {string} key - Storage key
     * @param {*} def - Default value
     * @returns {*} Stored value or default
     */
    get(key, def = null) {
      // Validate key format
      if (typeof key !== 'string' || !/^[a-zA-Z0-9_\-\.]+$/.test(key)) {
        logError('storage', 'Invalid key format', new Error(`Invalid key: ${key}`));
        return def;
      }

      try {
        const v = localStorage.getItem(key);
        if (v === null) return def;

        // Check size before parsing
        if (v.length > 5 * 1024 * 1024) {
          // 5MB limit
          logError('storage', 'Stored value too large', new Error(`Key: ${key}`));
          return def;
        }

        return JSON.parse(v);
      } catch (e) {
        logError('storage', 'Failed to parse stored value', e);
        return def;
      }
    },

    /**
     * Set value in localStorage with validation
     * @param {string} key - Storage key
     * @param {*} val - Value to store
     * @returns {boolean} Whether operation succeeded
     */
    set(key, val) {
      // Validate key format
      if (typeof key !== 'string' || !/^[a-zA-Z0-9_\-\.]+$/.test(key)) {
        logError('storage', 'Invalid key format', new Error(`Invalid key: ${key}`));
        return false;
      }

      try {
        const serialized = JSON.stringify(val);

        // Check size limit (5MB)
        if (serialized.length > 5 * 1024 * 1024) {
          logError('storage', 'Value too large to store', new Error(`Key: ${key}`));
          return false;
        }

        localStorage.setItem(key, serialized);
        return true;
      } catch (e) {
        logError('storage', 'Failed to store value', e);
        return false;
      }
    },

    /**
     * Remove value from localStorage
     * @param {string} key - Storage key
     */
    remove(key) {
      try {
        localStorage.removeItem(key);
      } catch (e) {
        logError('storage', 'Failed to remove value', e);
      }
    },

    /**
     * Clear all localStorage
     */
    clear() {
      try {
        localStorage.clear();
      } catch (e) {
        logError('storage', 'Failed to clear storage', e);
      }
    },

    /**
     * Check if key exists
     * @param {string} key - Storage key
     * @returns {boolean} Whether key exists
     */
    has(key) {
      try {
        return localStorage.getItem(key) !== null;
      } catch {
        return false;
      }
    },
  };

  /**
   * Optimized DOM query cache with size limits
   */
  const DOMCache = (() => {
    const cache = new Map();
    const MAX_CACHE_SIZE = 200; // Increased for better performance
    const CACHE_TTL = 5000; // 5 seconds - longer cache

    return {
      /**
       * Get cached element or query and cache it
       * @param {string} selector - CSS selector
       * @param {Element} [parent=document] - Parent element
       * @returns {Element|null} Found element
       */
      get(selector, parent = document) {
        const key = `${selector}_${parent === document ? 'doc' : ''}`;
        const cached = cache.get(key);

        if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
          return cached.element;
        }

        const element = parent.querySelector(selector);
        if (element) {
          cache.set(key, { element, timestamp: Date.now() });

          // Manage cache size
          if (cache.size > MAX_CACHE_SIZE) {
            const oldestKey = cache.keys().next().value;
            cache.delete(oldestKey);
          }
        }

        return element;
      },

      /**
       * Clear specific cache entry
       * @param {string} selector - CSS selector
       */
      clear(selector) {
        const keys = Array.from(cache.keys()).filter(k => k.startsWith(selector));
        keys.forEach(k => cache.delete(k));
      },

      /**
       * Clear all cache
       */
      clearAll() {
        cache.clear();
      },
    };
  })();

  /**
   * Advanced ScrollManager for efficient scroll event handling
   * Uses IntersectionObserver when possible for better performance
   */
  const ScrollManager = (() => {
    const listeners = new WeakMap();

    /**
     * Add optimized scroll listener
     * @param {Element} element - Element to listen to
     * @param {Function} callback - Callback function
     * @param {Object} options - Options {debounce: number, throttle: number, runInitial: boolean}
     * @returns {Function} Cleanup function
     */
    const addScrollListener = (element, callback, options = {}) => {
      try {
        const { debounce: debounceMs = 0, throttle: throttleMs = 0, runInitial = false } = options;

        let handler = callback;

        // Apply debounce if specified
        if (debounceMs > 0) {
          handler = debounce(handler, debounceMs);
        }

        // Apply throttle if specified
        if (throttleMs > 0) {
          handler = throttle(handler, throttleMs);
        }

        // Store handler for cleanup
        if (!listeners.has(element)) {
          listeners.set(element, new Set());
        }
        listeners.get(element).add(handler);

        // Add event listener
        element.addEventListener('scroll', handler, { passive: true });

        // Run initial callback if requested
        if (runInitial) {
          try {
            callback();
          } catch (err) {
            logError('ScrollManager', 'Initial callback error', err);
          }
        }

        // Return cleanup function
        return () => {
          try {
            element.removeEventListener('scroll', handler);
            const set = listeners.get(element);
            if (set) {
              set.delete(handler);
              if (set.size === 0) {
                listeners.delete(element);
              }
            }
          } catch (err) {
            logError('ScrollManager', 'Cleanup error', err);
          }
        };
      } catch (err) {
        logError('ScrollManager', 'addScrollListener error', err);
        return () => {}; // Return no-op cleanup
      }
    };

    /**
     * Remove all listeners for an element
     * @param {Element} element - Element to clean up
     */
    const removeAllListeners = element => {
      try {
        const set = listeners.get(element);
        if (!set) return;

        set.forEach(handler => {
          try {
            element.removeEventListener('scroll', handler);
          } catch {}
        });

        listeners.delete(element);
      } catch (err) {
        logError('ScrollManager', 'removeAllListeners error', err);
      }
    };

    /**
     * Create scroll-to-top functionality with smooth animation
     * @param {Element} element - Element to scroll
     * @param {Object} options - Options {duration: number, easing: string}
     */
    const scrollToTop = (element, options = {}) => {
      const { duration = 300, easing = 'ease-out' } = options;

      try {
        // Try native smooth scroll first
        if ('scrollBehavior' in document.documentElement.style) {
          element.scrollTo({ top: 0, behavior: 'smooth' });
          return;
        }

        // Fallback to manual animation
        const start = element.scrollTop;
        const startTime = performance.now();

        const scroll = currentTime => {
          const elapsed = currentTime - startTime;
          const progress = Math.min(elapsed / duration, 1);

          // Easing function
          const easeOutQuad = t => t * (2 - t);
          const easedProgress = easing === 'ease-out' ? easeOutQuad(progress) : progress;

          element.scrollTop = start * (1 - easedProgress);

          if (progress < 1) {
            requestAnimationFrame(scroll);
          }
        };

        requestAnimationFrame(scroll);
      } catch (err) {
        logError('ScrollManager', 'scrollToTop error', err);
      }
    };

    return {
      addScrollListener,
      removeAllListeners,
      scrollToTop,
    };
  })();

  // Centralized history.pushState/replaceState wrapping.
  // Dispatches 'ytp-history-navigate' so modules can listen instead of each wrapping independently.
  if (typeof window !== 'undefined' && !window.__ytp_history_wrapped) {
    window.__ytp_history_wrapped = true;
    const _origPush = history.pushState;
    const _origReplace = history.replaceState;
    history.pushState = function () {
      const result = _origPush.apply(this, arguments);
      try {
        window.dispatchEvent(
          new CustomEvent('ytp-history-navigate', { detail: { type: 'pushState' } })
        );
      } catch (e) {
        console.warn('[YouTube+] pushState event error:', e);
      }
      return result;
    };
    history.replaceState = function () {
      const result = _origReplace.apply(this, arguments);
      try {
        window.dispatchEvent(
          new CustomEvent('ytp-history-navigate', { detail: { type: 'replaceState' } })
        );
      } catch (e) {
        console.warn('[YouTube+] replaceState event error:', e);
      }
      return result;
    };
  }

  // Expose a global YouTubeUtils if not present (non-destructive)
  if (typeof window !== 'undefined') {
    /** @type {any} */ (window).YouTubeUtils = /** @type {any} */ (window).YouTubeUtils || {};
    const U = /** @type {any} */ (window).YouTubeUtils;
    U.logError = U.logError || logError;
    U.debounce = U.debounce || debounce;
    U.throttle = U.throttle || throttle;
    U.StyleManager = U.StyleManager || StyleManager;
    U.cleanupManager = U.cleanupManager || cleanupManager;
    U.EventDelegator = U.EventDelegator || EventDelegator;
    U.DOMCache = U.DOMCache || DOMCache;
    U.ScrollManager = U.ScrollManager || ScrollManager;
    U.createElement = U.createElement || createElement;
    U.waitForElement = U.waitForElement || waitForElement;
    U.storage = U.storage || storage;
    U.sanitizeHTML = U.sanitizeHTML || sanitizeHTML;
    U.escapeHTMLAttribute = U.escapeHTMLAttribute || escapeHTMLAttribute;
    U.safeMerge = U.safeMerge || safeMerge;
    U.validateVideoId = U.validateVideoId || validateVideoId;
    U.validatePlaylistId = U.validatePlaylistId || validatePlaylistId;
    U.validateChannelId = U.validateChannelId || validateChannelId;
    U.validateNumber = U.validateNumber || validateNumber;
    U.isValidURL = U.isValidURL || isValidURL;
    U.logger = U.logger || createLogger();
    U.retryWithBackoff = U.retryWithBackoff || retryWithBackoff;
    // Provide lightweight channel stats helpers if not defined by other modules.
    U.channelStatsHelpers = U.channelStatsHelpers || null;
    // Wrap global timer functions to auto-register with cleanupManager for safe cleanup.
    try {
      const w = window;
      if (w && !w.__ytp_timers_wrapped) {
        const origSetTimeout = w.setTimeout.bind(w);
        const origSetInterval = w.setInterval.bind(w);
        const origRaf = w.requestAnimationFrame ? w.requestAnimationFrame.bind(w) : null;

        w.setTimeout = function (fn, ms, ...args) {
          const id = origSetTimeout(fn, ms, ...args);
          try {
            U.cleanupManager.registerTimeout(id);
          } catch {}
          return id;
        };

        w.setInterval = function (fn, ms, ...args) {
          const id = origSetInterval(fn, ms, ...args);
          try {
            U.cleanupManager.registerInterval(id);
          } catch {}
          return id;
        };

        if (origRaf) {
          w.requestAnimationFrame = function (cb) {
            const id = origRaf(cb);
            try {
              U.cleanupManager.registerAnimationFrame(id);
            } catch {}
            return id;
          };
        }

        w.__ytp_timers_wrapped = true;
      }
    } catch (e) {
      logError('utils', 'timer wrapper failed', e);
    }
    if (!window.YouTubePlusChannelStatsHelpers) {
      window.YouTubePlusChannelStatsHelpers = {
        async fetchWithRetry(fetchFn, maxRetries = 2, logger = console) {
          let attempt = 0;
          while (attempt <= maxRetries) {
            try {
              // Allow fetchFn to be an async function returning parsed JSON
              const res = await fetchFn();
              return res;
            } catch (err) {
              attempt += 1;
              if (attempt > maxRetries) {
                logger &&
                  logger.warn &&
                  logger.warn('[ChannelStatsHelpers] fetch failed after retries', err);
                return null;
              }
              // backoff
              await new Promise(r => setTimeout(r, 300 * attempt));
            }
          }
          return null;
        },
        cacheStats(mapLike, channelId, stats) {
          try {
            if (!mapLike || typeof mapLike.set !== 'function') return;
            mapLike.set(channelId, stats);
          } catch {}
        },
        getCachedStats(mapLike, channelId, cacheDuration = 60000) {
          try {
            if (!mapLike || typeof mapLike.get !== 'function') return null;
            const s = mapLike.get(channelId);
            if (!s) return null;
            if (s.timestamp && Date.now() - s.timestamp > cacheDuration) return null;
            return s;
          } catch {
            return null;
          }
        },
        extractSubscriberCountFromPage() {
          try {
            const el = qs('yt-formatted-string#subscriber-count') || qs('[id*="subscriber-count"]');
            if (!el) return 0;
            const txt = el.textContent || '';
            const digits = txt.replace(/[^0-9]/g, '');
            return digits ? parseInt(digits, 10) : 0;
          } catch {
            return 0;
          }
        },
        createFallbackStats(followerCount = 0) {
          return {
            followerCount: followerCount || 0,
            bottomOdos: [0, 0],
            error: true,
            timestamp: Date.now(),
          };
        },
      };
    }
  }
})();

// --- MODULE: security-utils.js ---

/**
 * Security utilities for YouTube+ userscript
 * Provides sanitization, validation, and security helpers
 */
(function () {
  'use strict';

  /**
   * Validate YouTube video ID format
   * @param {string} id - Video ID to validate
   * @returns {boolean} True if valid YouTube video ID
   */
  function isValidVideoId(id) {
    if (!id || typeof id !== 'string') return false;
    // YouTube video IDs are exactly 11 characters: alphanumeric, dash, underscore
    return /^[a-zA-Z0-9_-]{11}$/.test(id);
  }

  /**
   * Validate YouTube channel ID format
   * @param {string} id - Channel ID to validate
   * @returns {boolean} True if valid YouTube channel ID
   */
  function isValidChannelId(id) {
    if (!id || typeof id !== 'string') return false;
    // YouTube channel IDs start with UC and are 24 characters
    return /^UC[a-zA-Z0-9_-]{22}$/.test(id);
  }

  /**
   * Validate URL is from YouTube domain
   * @param {string} url - URL to validate
   * @returns {boolean} True if valid YouTube URL
   */
  function isYouTubeUrl(url) {
    if (!url || typeof url !== 'string') return false;
    try {
      const parsed = new URL(url);
      const hostname = parsed.hostname.toLowerCase();
      return (
        hostname === 'www.youtube.com' ||
        hostname === 'youtube.com' ||
        hostname === 'm.youtube.com' ||
        hostname === 'music.youtube.com' ||
        hostname.endsWith('.youtube.com')
      );
    } catch {
      return false;
    }
  }

  /**
   * Sanitize text content for safe display
   * Removes HTML tags and dangerous characters
   * @param {string} text - Text to sanitize
   * @returns {string} Sanitized text
   */
  function sanitizeText(text) {
    if (!text || typeof text !== 'string') return '';
    return text
      .replace(/[<>]/g, '') // Remove angle brackets
      .replace(/javascript:/gi, '') // Remove javascript: protocol
      .replace(/on\w+=/gi, '') // Remove event handlers
      .trim();
  }

  /**
   * Sanitize HTML by encoding special characters
   * @param {string} html - HTML string to sanitize
   * @returns {string} Sanitized HTML
   */
  function escapeHtml(html) {
    if (!html || typeof html !== 'string') return '';
    const div = document.createElement('div');
    div.textContent = html;
    return div.innerHTML;
  }

  /**
   * Create safe HTML using TrustedTypes if available
   * Falls back to identity function if not available
   * @param {string} html - HTML string to make safe
   * @returns {string|TrustedHTML} Safe HTML
   */
  function createSafeHTML(html) {
    if (typeof window._ytplusCreateHTML === 'function') {
      return window._ytplusCreateHTML(html);
    }
    // Fallback for when TrustedTypes not available
    return html;
  }

  /**
   * Set innerHTML safely with optional sanitization
   * @param {HTMLElement} element - Target element
   * @param {string} html - HTML content to set
   * @param {boolean} sanitize - Whether to escape HTML (default: false for trusted content)
   */
  function setInnerHTMLSafe(element, html, sanitize = false) {
    if (!element || !(element instanceof HTMLElement)) {
      console.error('[Security] Invalid element for setInnerHTMLSafe');
      return;
    }
    const content = sanitize ? escapeHtml(html) : html;
    element.innerHTML = createSafeHTML(content);
  }

  /**
   * Set text content safely (always escapes HTML)
   * @param {HTMLElement} element - Target element
   * @param {string} text - Text content to set
   */
  function setTextContentSafe(element, text) {
    if (!element || !(element instanceof HTMLElement)) {
      console.error('[Security] Invalid element for setTextContentSafe');
      return;
    }
    element.textContent = text || '';
  }

  /**
   * Validate and sanitize attribute value
   * @param {string} attrName - Attribute name
   * @param {string} attrValue - Attribute value
   * @returns {string|null} Sanitized value or null if invalid
   */
  function sanitizeAttribute(attrName, attrValue) {
    if (!attrName || typeof attrName !== 'string') return null;
    if (attrValue === null || attrValue === undefined) return '';

    // Block dangerous attributes
    const dangerousAttrs = ['onload', 'onerror', 'onclick', 'onmouseover'];
    if (dangerousAttrs.some(attr => attrName.toLowerCase().startsWith(attr))) {
      console.warn(`[Security] Blocked dangerous attribute: ${attrName}`);
      return null;
    }

    const valueStr = String(attrValue);

    // Special handling for href and src
    if (attrName.toLowerCase() === 'href' || attrName.toLowerCase() === 'src') {
      // Check for javascript protocol (security check, not script URL usage)
      // eslint-disable-next-line no-script-url
      if (valueStr.toLowerCase().startsWith('javascript:')) {
        console.warn(`[Security] Blocked javascript protocol in ${attrName}`);
        return null;
      }
      if (
        valueStr.toLowerCase().startsWith('data:') &&
        !valueStr.toLowerCase().startsWith('data:image/')
      ) {
        console.warn(`[Security] Blocked non-image data: URI in ${attrName}`);
        return null;
      }
    }

    return valueStr;
  }

  /**
   * Set attribute safely with validation
   * @param {HTMLElement} element - Target element
   * @param {string} attrName - Attribute name
   * @param {string} attrValue - Attribute value
   * @returns {boolean} Success status
   */
  function setAttributeSafe(element, attrName, attrValue) {
    if (!element || !(element instanceof HTMLElement)) {
      console.error('[Security] Invalid element for setAttributeSafe');
      return false;
    }

    const sanitizedValue = sanitizeAttribute(attrName, attrValue);
    if (sanitizedValue === null) return false;

    try {
      element.setAttribute(attrName, sanitizedValue);
      return true;
    } catch (error) {
      console.error('[Security] setAttribute failed:', error);
      return false;
    }
  }

  /**
   * Validate number is within safe range
   * @param {*} value - Value to validate
   * @param {number} min - Minimum allowed value
   * @param {number} max - Maximum allowed value
   * @returns {number|null} Validated number or null if invalid
   */
  function validateNumber(value, min = -Infinity, max = Infinity) {
    const num = Number(value);
    if (isNaN(num) || !isFinite(num)) return null;
    if (num < min || num > max) return null;
    return num;
  }

  /**
   * Rate limiter for preventing abuse
   */
  class RateLimiter {
    constructor(maxRequests = 10, timeWindow = 60000) {
      this.maxRequests = maxRequests;
      this.timeWindow = timeWindow;
      this.requests = new Map();
    }

    /**
     * Check if request is allowed
     * @param {string} key - Request identifier
     * @returns {boolean} Whether request is allowed
     */
    canRequest(key) {
      const now = Date.now();
      const requests = this.requests.get(key) || [];

      // Remove old requests outside time window
      const recentRequests = requests.filter(time => now - time < this.timeWindow);

      if (recentRequests.length >= this.maxRequests) {
        console.warn(
          `[Security] Rate limit exceeded for ${key}. Max ${this.maxRequests} requests per ${this.timeWindow}ms.`
        );
        return false;
      }

      recentRequests.push(now);
      this.requests.set(key, recentRequests);
      return true;
    }

    /**
     * Clear rate limiter state
     */
    clear() {
      this.requests.clear();
    }
  }

  /**
   * Create fetch with timeout wrapper
   * @param {string} url - URL to fetch
   * @param {Object} options - Fetch options
   * @param {number} timeout - Timeout in milliseconds (default: 10000)
   * @returns {Promise<Response>} Fetch promise with timeout
   */
  function fetchWithTimeout(url, options = {}, timeout = 10000) {
    return Promise.race([
      fetch(url, options),
      new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout)),
    ]);
  }

  /**
   * Validate JSON response structure
   * @param {Object} data - JSON data to validate
   * @param {Object} schema - Expected schema (simple validation)
   * @returns {boolean} True if valid
   */
  function validateJSONSchema(data, schema) {
    if (!data || typeof data !== 'object') return false;
    if (!schema || typeof schema !== 'object') return true;

    for (const key in schema) {
      if (schema[key].required && !(key in data)) {
        console.warn(`[Security] Missing required field: ${key}`);
        return false;
      }
      if (key in data && schema[key].type && typeof data[key] !== schema[key].type) {
        console.warn(
          `[Security] Invalid type for field ${key}: expected ${schema[key].type}, got ${typeof data[key]}`
        );
        return false;
      }
    }
    return true;
  }

  // Export utilities to window for use across modules
  if (typeof window !== 'undefined') {
    window.YouTubeSecurityUtils = {
      isValidVideoId,
      isValidChannelId,
      isYouTubeUrl,
      sanitizeText,
      escapeHtml,
      createSafeHTML,
      setInnerHTMLSafe,
      setTextContentSafe,
      sanitizeAttribute,
      setAttributeSafe,
      validateNumber,
      RateLimiter,
      fetchWithTimeout,
      validateJSONSchema,
    };
  }
})();

// --- MODULE: basic.js ---

const YouTubeUtils = (() => {
  'use strict';

  // Import helper modules
  const Security = window.YouTubePlusSecurity || {};
  const Storage = window.YouTubePlusStorage || {};
  const Performance = window.YouTubePlusPerformance || {};

  /**
   * Translation function with fallback support
   * Uses centralized i18n from YouTubePlusI18n
   * @param {string} key - Translation key
   * @param {Object} params - Parameters for interpolation
   * @returns {string} Translated string
   */
  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t && window.YouTubeUtils.t !== t) {
      return window.YouTubeUtils.t(key, params);
    }
    // Fallback for initialization phase
    if (!key) return '';
    let result = String(key);
    for (const [k, v] of Object.entries(params || {})) {
      result = result.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
    }
    return result;
  };

  /**
   * Error logging with module context (local reference)
   * @param {string} module - Module name
   * @param {string} message - Error message
   * @param {Error} error - Error object
   */
  const logError = (module, message, error) => {
    console.error(`[YouTube+][${module}] ${message}:`, error);
  };

  // Use helper modules or fallback to local implementations
  const safeExecute =
    Security.safeExecute ||
    ((fn, context = 'Unknown') => {
      /** @this {any} */
      return function (...args) {
        try {
          return fn.call(this, ...args);
        } catch (error) {
          logError(context, 'Execution failed', error);
          return null;
        }
      };
    });

  const safeExecuteAsync =
    Security.safeExecuteAsync ||
    ((fn, context = 'Unknown') => {
      /** @this {any} */
      return async function (...args) {
        try {
          return await fn.call(this, ...args);
        } catch (error) {
          logError(context, 'Async execution failed', error);
          return null;
        }
      };
    });

  const sanitizeHTML =
    Security.sanitizeHTML ||
    (html => {
      if (typeof html !== 'string') return '';
      return html.replace(/[<>&"'\/`=]/g, '');
    });

  const isValidURL =
    Security.isValidURL ||
    (url => {
      if (typeof url !== 'string') return false;
      try {
        const parsed = new URL(url);
        return ['http:', 'https:'].includes(parsed.protocol);
      } catch {
        return false;
      }
    });

  // Use storage helper or fallback
  const storage = Storage || {
    get: (key, defaultValue = null) => {
      try {
        const value = localStorage.getItem(key);
        return value ? JSON.parse(value) : defaultValue;
      } catch {
        return defaultValue;
      }
    },
    set: (key, value) => {
      try {
        localStorage.setItem(key, JSON.stringify(value));
        return true;
      } catch {
        return false;
      }
    },
    remove: key => {
      try {
        localStorage.removeItem(key);
        return true;
      } catch {
        return false;
      }
    },
  };

  // Use performance helpers or fallback
  const debounce =
    Performance?.debounce ||
    ((func, wait, options = {}) => {
      let timeout = null;
      /** @this {any} */
      const debounced = function (...args) {
        if (timeout !== null) clearTimeout(timeout);
        if (options.leading && timeout === null) {
          func.call(this, ...args);
        }
        timeout = setTimeout(() => {
          if (!options.leading) func.call(this, ...args);
          timeout = null;
        }, wait);
      };
      debounced.cancel = () => {
        if (timeout !== null) clearTimeout(timeout);
        timeout = null;
      };
      return debounced;
    });

  const throttle =
    Performance?.throttle ||
    ((func, limit) => {
      let inThrottle = false;
      /** @this {any} */
      return function (...args) {
        if (!inThrottle) {
          func.call(this, ...args);
          inThrottle = true;
          setTimeout(() => {
            inThrottle = false;
          }, limit);
        }
      };
    });

  /**
   * Safe DOM element creation with props and children
   * @param {string} tag - HTML tag name
   * @param {Object} props - Element properties
   * @param {Array<string | Node>} children - Child elements or text
   * @returns {HTMLElement} Created element
   */
  const createElement = (tag, props = {}, children = []) => {
    // Validate tag name to prevent XSS
    const validTags = /^[a-z][a-z0-9-]*$/i;
    if (!validTags.test(tag)) {
      logError('createElement', 'Invalid tag name', new Error(`Tag "${tag}" is not allowed`));
      return document.createElement('div');
    }

    const element = document.createElement(tag);

    Object.entries(props).forEach(([key, value]) => {
      if (key === 'className') {
        element.className = value;
      } else if (key === 'style' && typeof value === 'object') {
        Object.assign(element.style, value);
      } else if (key.startsWith('on') && typeof value === 'function') {
        element.addEventListener(key.substring(2).toLowerCase(), value);
      } else if (key === 'dataset' && typeof value === 'object') {
        Object.assign(element.dataset, value);
      } else if (key === 'innerHTML' || key === 'outerHTML') {
        // Prevent direct HTML injection
        logError(
          'createElement',
          'Direct HTML injection prevented',
          new Error('Use children array instead')
        );
      } else {
        try {
          element.setAttribute(key, value);
        } catch (e) {
          logError('createElement', `Failed to set attribute ${key}`, e);
        }
      }
    });

    children.forEach(child => {
      if (typeof child === 'string') {
        element.appendChild(document.createTextNode(child));
      } else if (child instanceof Node) {
        element.appendChild(child);
      }
    });

    return element;
  };

  /**
   * DOM Selector Cache with automatic cleanup
   */
  const selectorCache = new Map();
  const CACHE_MAX_SIZE = 100; // Increased for better performance
  const CACHE_MAX_AGE = 10000; // 10 seconds - longer retention

  /**
   * Cached querySelector with LRU-like eviction
   * @param {string} selector - CSS selector
   * @param {boolean} nocache - Skip cache
   * @returns {HTMLElement|null} Found element
   */
  const querySelector = (selector, nocache = false) => {
    if (nocache) return document.querySelector(selector);

    const now = Date.now();
    const cached = selectorCache.get(selector);

    // Check if cached element is still valid
    if (cached?.element?.isConnected && now - cached.timestamp < CACHE_MAX_AGE) {
      return cached.element;
    }

    // Remove stale entry
    if (cached) {
      selectorCache.delete(selector);
    }

    const element = document.querySelector(selector);

    if (element) {
      // LRU eviction: remove oldest entries if cache is full
      if (selectorCache.size >= CACHE_MAX_SIZE) {
        const firstKey = selectorCache.keys().next().value;
        selectorCache.delete(firstKey);
      }

      selectorCache.set(selector, { element, timestamp: now });
    }

    return element;
  };

  /**
   * Validate waitForElement parameters
   * @param {string} selector - CSS selector
   * @param {HTMLElement} parent - Parent element
   * @returns {Error|null} Validation error or null
   */
  const validateWaitParams = (selector, parent) => {
    if (!selector || typeof selector !== 'string') {
      return new Error('Selector must be a non-empty string');
    }
    if (!parent || !(parent instanceof Element)) {
      return new Error('Parent must be a valid DOM element');
    }
    return null;
  };

  /**
   * Try to find element immediately
   * @param {HTMLElement} parent - Parent element
   * @param {string} selector - CSS selector
   * @returns {{element: HTMLElement|null, error: Error|null}} Result object
   */
  const tryQuerySelector = (parent, selector) => {
    try {
      const element = parent.querySelector(selector);
      return { element, error: null };
    } catch {
      return { element: null, error: new Error(`Invalid selector: ${selector}`) };
    }
  };

  /**
   * Cleanup observer and timeout resources
   * @param {MutationObserver|null} observer - Observer to disconnect
   * @param {number} timeoutId - Timeout ID to clear
   * @param {AbortController} controller - Abort controller
   */
  const cleanupWaitResources = (observer, timeoutId, controller) => {
    controller.abort();
    if (observer) {
      try {
        observer.disconnect();
      } catch (e) {
        logError('waitForElement', 'Observer disconnect failed', e);
      }
    }
    clearTimeout(timeoutId);
  };

  /**
   * Create and setup mutation observer for element watching
   * @param {HTMLElement} parent - Parent element
   * @param {string} selector - CSS selector
   * @param {Function} resolve - Promise resolve function
   * @param {number} timeoutId - Timeout ID for cleanup
   * @returns {MutationObserver} Created observer
   */
  const createWaitObserver = (parent, selector, resolve, timeoutId) => {
    return new MutationObserver(() => {
      try {
        const element = parent.querySelector(selector);
        if (element) {
          clearTimeout(timeoutId);
          resolve(/** @type {HTMLElement} */ (/** @type {unknown} */ (element)));
        }
      } catch (e) {
        logError('waitForElement', 'Observer callback error', e);
      }
    });
  };

  /**
   * Start observing parent element for DOM changes
   * @param {MutationObserver} observer - Observer instance
   * @param {HTMLElement} parent - Parent element to observe
   * @returns {Error|null} Error if observation failed
   */
  const startWaitObservation = (observer, parent) => {
    try {
      if (!(parent instanceof Element) && parent !== document) {
        throw new Error('Parent does not support observation');
      }
      observer.observe(parent, { childList: true, subtree: true });
      return null;
    } catch {
      try {
        observer.observe(parent, { childList: true, subtree: true });
        return null;
      } catch {
        return new Error('Failed to observe DOM');
      }
    }
  };

  /**
   * Wait for element with timeout and AbortController
   * @param {string} selector - CSS selector
   * @param {number} timeout - Timeout in ms
   * @param {HTMLElement} parent - Parent element to search in
   * @returns {Promise<HTMLElement>} Promise resolving to element
   */
  const waitForElement = (selector, timeout = 5000, parent = document.body) => {
    return new Promise((resolve, reject) => {
      const validationError = validateWaitParams(selector, parent);
      if (validationError) {
        reject(validationError);
        return;
      }

      const { element, error } = tryQuerySelector(parent, selector);
      if (error) {
        reject(error);
        return;
      }
      if (element) {
        resolve(/** @type {HTMLElement} */ (/** @type {unknown} */ (element)));
        return;
      }

      const controller = new AbortController();
      /** @type {MutationObserver | null} */
      let observer = null;

      const timeoutId = setTimeout(() => {
        cleanupWaitResources(observer, timeoutId, controller);
        reject(new Error(`Element ${selector} not found within ${timeout}ms`));
      }, timeout);

      observer = createWaitObserver(parent, selector, resolve, timeoutId);

      const observeError = startWaitObservation(observer, parent);
      if (observeError) {
        clearTimeout(timeoutId);
        reject(observeError);
      }
    });
  };

  /**
   * Resource Cleanup Manager
   * Manages observers, listeners, and intervals
   */
  const cleanupManager = {
    observers: new Set(),
    listeners: new Map(),
    intervals: new Set(),
    timeouts: new Set(),
    animationFrames: new Set(),
    cleanupFunctions: new Set(),

    /**
     * Register a generic cleanup function
     * @param {Function} fn - Cleanup function to call during cleanup
     * @returns {Function} The registered function
     */
    register: fn => {
      if (typeof fn === 'function') {
        cleanupManager.cleanupFunctions.add(fn);
      }
      return fn;
    },

    /**
     * Unregister a specific cleanup function
     * @param {Function} fn - Function to unregister
     */
    unregister: fn => {
      cleanupManager.cleanupFunctions.delete(fn);
    },

    /**
     * Register MutationObserver for cleanup
     * @param {MutationObserver} observer - Observer to register
     * @returns {MutationObserver} Registered observer
     */
    registerObserver: observer => {
      cleanupManager.observers.add(observer);
      return observer;
    },

    /**
     * Unregister and disconnect specific observer
     * @param {MutationObserver} observer - Observer to unregister
     */
    unregisterObserver: observer => {
      if (observer) {
        try {
          observer.disconnect();
        } catch (e) {
          logError('Cleanup', 'Observer disconnect failed', e);
        }
        cleanupManager.observers.delete(observer);
      }
    },

    /**
     * Register event listener for cleanup
     * @param {EventTarget|Document|Window} element - Target element
     * @param {string} event - Event name
     * @param {EventListener|EventListenerObject} handler - Event handler
     * @param {Object} options - Event listener options
     * @returns {Symbol} Listener key for later removal
     */
    registerListener: (element, event, handler, options) => {
      const key = Symbol('listener');
      cleanupManager.listeners.set(key, { element, event, handler, options });
      try {
        element.addEventListener(event, /** @type {EventListener} */ (handler), options);
      } catch {
        // best-effort: if addEventListener fails, still register the listener record
      }
      return key;
    },

    /**
     * Unregister specific listener
     * @param {Symbol} key - Listener key
     */
    unregisterListener: key => {
      const listener = cleanupManager.listeners.get(key);
      if (listener) {
        const { element, event, handler, options } = listener;
        try {
          element.removeEventListener(event, handler, options);
        } catch (e) {
          logError('Cleanup', 'Listener removal failed', e);
        }
        cleanupManager.listeners.delete(key);
      }
    },

    /**
     * Register interval for cleanup
     * @param {TimerId} id - Interval ID
     * @returns {TimerId} Interval ID
     */
    registerInterval: id => {
      cleanupManager.intervals.add(id);
      return id;
    },

    /**
     * Unregister specific interval
     * @param {number} id - Interval ID
     */
    unregisterInterval: id => {
      clearInterval(id);
      cleanupManager.intervals.delete(id);
    },

    /**
     * Register timeout for cleanup
     * @param {TimerId} id - Timeout ID
     * @returns {TimerId} Timeout ID
     */
    registerTimeout: id => {
      cleanupManager.timeouts.add(id);
      return id;
    },

    /**
     * Unregister specific timeout
     * @param {number} id - Timeout ID
     */
    unregisterTimeout: id => {
      clearTimeout(id);
      cleanupManager.timeouts.delete(id);
    },

    /**
     * Register animation frame for cleanup
     * @param {number} id - Animation frame ID
     * @returns {number} Animation frame ID
     */
    registerAnimationFrame: id => {
      cleanupManager.animationFrames.add(id);
      return id;
    },

    /**
     * Unregister specific animation frame
     * @param {number} id - Animation frame ID
     */
    unregisterAnimationFrame: id => {
      cancelAnimationFrame(id);
      cleanupManager.animationFrames.delete(id);
    },

    /**
     * Cleanup all registered resources
     */
    cleanup: () => {
      // Call all registered cleanup functions
      cleanupManager.cleanupFunctions.forEach(fn => {
        try {
          fn();
        } catch (e) {
          logError('Cleanup', 'Cleanup function failed', e);
        }
      });
      cleanupManager.cleanupFunctions.clear();

      // Disconnect all observers
      cleanupManager.observers.forEach(obs => {
        try {
          obs.disconnect();
        } catch (e) {
          logError('Cleanup', 'Observer disconnect failed', e);
        }
      });
      cleanupManager.observers.clear();

      // Remove all listeners
      cleanupManager.listeners.forEach(({ element, event, handler, options }) => {
        try {
          element.removeEventListener(event, handler, options);
        } catch (e) {
          logError('Cleanup', 'Listener removal failed', e);
        }
      });
      cleanupManager.listeners.clear();

      // Clear all intervals
      cleanupManager.intervals.forEach(id => clearInterval(id));
      cleanupManager.intervals.clear();

      // Clear all timeouts
      cleanupManager.timeouts.forEach(id => clearTimeout(id));
      cleanupManager.timeouts.clear();

      // Cancel all animation frames
      cleanupManager.animationFrames.forEach(id => cancelAnimationFrame(id));
      cleanupManager.animationFrames.clear();
    },
  };

  /**
   * Settings Manager
   * Centralized settings storage and retrieval
   */
  const SettingsManager = {
    storageKey: 'youtube_plus_all_settings_v2',

    defaults: {
      speedControl: { enabled: true, currentSpeed: 1 },
      screenshot: { enabled: true },
      download: { enabled: true },
      updateChecker: { enabled: true },
      adBlocker: { enabled: true },
      pip: { enabled: true },
      timecodes: { enabled: true },
      // Add other modules...
    },

    /**
     * Load all settings
     * @returns {Object} Settings object
     */
    load() {
      const saved = storage.get(this.storageKey);
      return saved ? { ...this.defaults, ...saved } : { ...this.defaults };
    },

    /**
     * Save all settings
     * @param {Object} settings - Settings to save
     */
    save(settings) {
      storage.set(this.storageKey, settings);
      // Dispatch event for modules to react
      window.dispatchEvent(
        new CustomEvent('youtube-plus-settings-changed', {
          detail: settings,
        })
      );
    },

    /**
     * Get setting by path
     * @param {string} path - Dot-separated path (e.g., 'speedControl.enabled')
     * @returns {*} Setting value
     */
    get(path) {
      const settings = this.load();
      return path.split('.').reduce((obj, key) => /** @type {any} */ (obj)?.[key], settings);
    },

    /**
     * Set setting by path
     * @param {string} path - Dot-separated path
     * @param {*} value - Value to set
     */
    set(path, value) {
      const settings = this.load();
      const keys = path.split('.');
      const last = keys.pop();
      const target = keys.reduce((obj, key) => {
        /** @type {any} */ (obj)[key] = /** @type {any} */ (obj)[key] || {};
        return /** @type {any} */ (obj)[key];
      }, settings);
      /** @type {any} */ (target)[/** @type {string} */ (last)] = value;
      this.save(settings);
    },
  };

  /**
   * Style Manager
   * Centralized CSS injection and management
   */
  const StyleManager = {
    styles: new Map(),
    /** @type {HTMLStyleElement | null} */
    element: null,

    /**
     * Add CSS rules
     * @param {string} id - Unique identifier
     * @param {string} css - CSS rules
     */
    add(id, css) {
      if (typeof id !== 'string' || !id) {
        logError('StyleManager', 'Invalid style ID', new Error('ID must be a non-empty string'));
        return;
      }
      if (typeof css !== 'string') {
        logError('StyleManager', 'Invalid CSS', new Error('CSS must be a string'));
        return;
      }
      this.styles.set(id, css);
      this.update();
    },

    /**
     * Remove CSS rules
     * @param {string} id - Identifier
     */
    remove(id) {
      this.styles.delete(id);
      this.update();
    },

    /**
     * Update style element
     */
    update() {
      try {
        if (!this.element) {
          this.element = document.createElement('style');
          this.element.id = 'youtube-plus-styles';
          this.element.type = 'text/css';
          (document.head || document.documentElement).appendChild(this.element);
        }
        this.element.textContent = Array.from(this.styles.values()).join('\n');
      } catch (error) {
        logError('StyleManager', 'Failed to update styles', error);
      }
    },

    /**
     * Clear all styles
     */
    clear() {
      this.styles.clear();
      if (this.element) {
        try {
          this.element.remove();
        } catch (e) {
          logError('StyleManager', 'Failed to remove style element', e);
        }
        this.element = null;
      }
    },
  };

  /**
   * Centralized Notification System
   * Manages all notifications with queue and deduplication
   */
  const NotificationManager = {
    /** @type {any[]} */
    queue: [],
    activeNotifications: new Set(),
    maxVisible: 3,
    defaultDuration: 3000,

    /**
     * Show notification
     * @param {string} message - Notification message
     * @param {{duration?: number, position?: string | null, action?: {text: string, callback: Function} | null, type?: string}} [options] - Notification options
     * @returns {HTMLElement | null} Notification element
     */
    show(message, options = {}) {
      // Validate message
      if (!message || typeof message !== 'string') {
        logError(
          'NotificationManager',
          'Invalid message',
          new Error('Message must be a non-empty string')
        );
        return null;
      }

      const {
        duration = this.defaultDuration,
        position = null,
        action = null, // { text: string, callback: function }
      } = options;

      // Remove duplicate messages
      this.activeNotifications.forEach(notif => {
        if (notif.dataset.message === message) {
          this.remove(notif);
        }
      });

      const positions = {
        'top-right': { top: '20px', right: '20px' },
        'top-left': { top: '20px', left: '20px' },
        'bottom-right': { bottom: '20px', right: '20px' },
        'bottom-left': { bottom: '20px', left: '20px' },
      };

      try {
        // Use shared enhancer notification class for consistent appearance
        const notification = createElement('div', {
          className: 'youtube-enhancer-notification',
          dataset: { message }, // Store message for deduplication
          // Keep minimal inline styles; main visuals come from the shared CSS class
          style: {
            zIndex: '10001',
            width: 'auto',
            display: 'flex',
            alignItems: 'center',
            gap: '10px',
            ...(position && /** @type {any} */ (positions)[position]
              ? /** @type {any} */ (positions)[position]
              : {}),
          },
        });

        // Add message (with accessibility attributes)
        notification.setAttribute('role', 'status');
        notification.setAttribute('aria-live', 'polite');
        notification.setAttribute('aria-atomic', 'true');

        const messageSpan = createElement(
          'span',
          {
            style: { flex: '1' },
          },
          [message]
        );
        notification.appendChild(messageSpan);

        // Add action button if provided
        if (action && action.text && typeof action.callback === 'function') {
          const actionBtn = createElement(
            'button',
            {
              style: {
                background: 'rgba(255,255,255,0.2)',
                border: '1px solid rgba(255,255,255,0.3)',
                color: 'white',
                padding: '4px 12px',
                borderRadius: '4px',
                cursor: 'pointer',
                fontSize: '12px',
                fontWeight: '600',
                transition: 'background 0.2s',
              },
              onClick: () => {
                action.callback();
                this.remove(notification);
              },
            },
            [action.text]
          );
          notification.appendChild(actionBtn);
        }

        // Ensure a centralized bottom-center container exists and add notification there
        const _notifContainerId = 'youtube-enhancer-notification-container';
        let _notifContainer = document.getElementById(_notifContainerId);
        if (!_notifContainer) {
          _notifContainer = createElement('div', {
            id: _notifContainerId,
            className: 'youtube-enhancer-notification-container',
          });
          try {
            document.body.appendChild(_notifContainer);
          } catch {
            // fallback to body append if container append fails
            document.body.appendChild(notification);
            this.activeNotifications.add(notification);
          }
        }

        try {
          // Prepend so newest notifications appear on top
          _notifContainer.insertBefore(notification, _notifContainer.firstChild);
        } catch {
          // fallback
          document.body.appendChild(notification);
        }
        // ensure notification accepts pointer events (container is pointer-events:none)
        try {
          notification.style.pointerEvents = 'auto';
        } catch {}
        this.activeNotifications.add(notification);

        // Apply entry animation from bottom
        try {
          notification.style.animation = 'slideInFromBottom 0.38s ease-out forwards';
        } catch {}

        // Auto-dismiss
        if (duration > 0) {
          const timeoutId = setTimeout(() => this.remove(notification), duration);
          cleanupManager.registerTimeout(timeoutId);
        }

        // Limit visible notifications
        if (this.activeNotifications.size > this.maxVisible) {
          const oldest = Array.from(this.activeNotifications)[0];
          this.remove(oldest);
        }

        return notification;
      } catch (error) {
        logError('NotificationManager', 'Failed to show notification', error);
        return null;
      }
    },

    /**
     * Remove notification
     * @param {HTMLElement} notification - Notification element
     */
    remove(notification) {
      if (!notification || !notification.isConnected) return;

      try {
        try {
          notification.style.animation = 'slideOutToBottom 0.32s ease-in forwards';
          const timeoutId = setTimeout(() => {
            try {
              notification.remove();
              this.activeNotifications.delete(notification);
            } catch (e) {
              logError('NotificationManager', 'Failed to remove notification', e);
            }
          }, 340);
          cleanupManager.registerTimeout(timeoutId);
        } catch {
          // Fallback: immediate removal
          try {
            notification.remove();
            this.activeNotifications.delete(notification);
          } catch (e) {
            logError('NotificationManager', 'Failed to remove notification (fallback)', e);
          }
        }
      } catch (error) {
        logError('NotificationManager', 'Failed to animate notification removal', error);
        // Force remove
        notification.remove();
        this.activeNotifications.delete(notification);
      }
    },

    /**
     * Clear all notifications
     */
    clearAll() {
      this.activeNotifications.forEach(notif => {
        try {
          notif.remove();
        } catch (e) {
          logError('NotificationManager', 'Failed to clear notification', e);
        }
      });
      this.activeNotifications.clear();
    },
  };

  // Add notification animation styles
  StyleManager.add(
    'notification-animations',
    `
    @keyframes slideInFromBottom {
      from { transform: translateY(100%); opacity: 0; }
      to { transform: translateY(0); opacity: 1; }
    }

    @keyframes slideOutToBottom {
      from { transform: translateY(0); opacity: 1; }
      to { transform: translateY(100%); opacity: 0; }
    }
  `
  );

  // Global cleanup on page unload
  window.addEventListener('beforeunload', () => {
    cleanupManager.cleanup();
    selectorCache.clear();
    StyleManager.clear();
    NotificationManager.clearAll();
  });

  // Periodic cache cleanup to prevent memory leaks (using requestIdleCallback when available)
  const cacheCleanup = () => {
    const now = Date.now();
    for (const [key, value] of selectorCache.entries()) {
      if (!value.element?.isConnected || now - value.timestamp > CACHE_MAX_AGE) {
        selectorCache.delete(key);
      }
    }
  };

  const cacheCleanupInterval = setInterval(() => {
    if (typeof requestIdleCallback === 'function') {
      requestIdleCallback(cacheCleanup, { timeout: 2000 });
    } else {
      cacheCleanup();
    }
  }, 30000); // Clean every 30 seconds

  cleanupManager.registerInterval(cacheCleanupInterval);

  // Global error handler for uncaught promise rejections
  window.addEventListener('unhandledrejection', event => {
    logError('Global', 'Unhandled promise rejection', event.reason);
    event.preventDefault(); // Prevent console spam
  });

  // Global error handler for uncaught errors
  window.addEventListener('error', event => {
    const message = String(event?.message || '');
    const errorMessage = String(event?.error?.message || '');
    if (message.includes('ResizeObserver loop') || errorMessage.includes('ResizeObserver loop')) {
      return;
    }

    // Only log errors from our script
    if (event.filename && event.filename.includes('youtube')) {
      logError(
        'Global',
        'Uncaught error',
        new Error(`${event.message} at ${event.filename}:${event.lineno}:${event.colno}`)
      );
    }
  });

  /**
   * Performance monitoring wrapper
   * @param {string} label - Operation label
   * @param {Function} fn - Function to monitor
   * @returns {Function} Wrapped function
   */
  const measurePerformance = (label, fn) => {
    /** @this {any} */
    return function (...args) {
      const start = performance.now();
      try {
        const result = fn.apply(this, args);
        const duration = performance.now() - start;
        if (duration > 100) {
          console.warn(`[YouTube+][Performance] ${label} took ${duration.toFixed(2)}ms`);
        }
        return result;
      } catch (error) {
        logError('Performance', `${label} failed`, error);
        throw error;
      }
    };
  };

  /**
   * Async performance monitoring wrapper
   * @param {string} label - Operation label
   * @param {Function} fn - Async function to monitor
   * @returns {Function} Wrapped async function
   */
  const measurePerformanceAsync = (label, fn) => {
    /** @this {any} */
    return async function (...args) {
      const start = performance.now();
      try {
        const result = await fn.apply(this, args);
        const duration = performance.now() - start;
        if (duration > 100) {
          console.warn(`[YouTube+][Performance] ${label} took ${duration.toFixed(2)}ms`);
        }
        return result;
      } catch (error) {
        logError('Performance', `${label} failed`, error);
        throw error;
      }
    };
  };

  /**
   * Mobile device detection
   * @returns {boolean} True if mobile device
   */
  const isMobile = () => {
    return (
      /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
      window.innerWidth <= 768
    );
  };

  /**
   * Get viewport dimensions
   * @returns {Object} Width and height
   */
  const getViewport = () => ({
    width: Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0),
    height: Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0),
  });

  /**
   * Safe async retry wrapper
   * @param {Function} fn - Async function to retry
   * @param {number} retries - Number of retries
   * @param {number} delay - Delay between retries
   * @returns {Promise} Result or error
   */
  const retryAsync = async (fn, retries = 3, delay = 1000) => {
    for (let i = 0; i < retries; i++) {
      try {
        return await fn();
      } catch (error) {
        if (i === retries - 1) throw error;
        await new Promise(resolve => {
          setTimeout(resolve, delay * (i + 1));
        });
      }
    }
  };

  // Export public API
  return {
    logError,
    safeExecute,
    safeExecuteAsync,
    sanitizeHTML,
    isValidURL,
    storage,
    debounce,
    throttle,
    createElement,
    querySelector,
    waitForElement,
    cleanupManager,
    SettingsManager,
    StyleManager,
    NotificationManager,
    clearCache: () => selectorCache.clear(),
    isMobile,
    getViewport,
    retryAsync,
    measurePerformance,
    measurePerformanceAsync,
    t, // Translation function
  };
})();

// Make available globally
if (typeof window !== 'undefined') {
  // Merge utilities into existing global YouTubeUtils without overwriting
  /** @type {any} */ (window).YouTubeUtils = /** @type {any} */ (window).YouTubeUtils || {};
  const existing = /** @type {any} */ (window).YouTubeUtils;
  try {
    for (const k of Object.keys(YouTubeUtils)) {
      if (existing[k] === undefined) existing[k] = YouTubeUtils[k];
    }
  } catch (e) {
    console.error('[YouTube+] Failed to merge core utilities:', e);
  }

  // Add initialization health check (non-intrusive)
  window.YouTubeUtils &&
    YouTubeUtils.logger &&
    YouTubeUtils.logger.debug &&
    YouTubeUtils.logger.debug('[YouTube+ v2.4.4] Core utilities merged');

  // Expose debug info
  /** @type {any} */ (window).YouTubePlusDebug = {
    version: '2.4.4',
    cacheSize: () =>
      YouTubeUtils.cleanupManager.observers.size +
      YouTubeUtils.cleanupManager.listeners.size +
      YouTubeUtils.cleanupManager.intervals.size,
    clearAll: () => {
      YouTubeUtils.cleanupManager.cleanup();
      YouTubeUtils.clearCache();
      YouTubeUtils.StyleManager.clear();
      YouTubeUtils.NotificationManager.clearAll();
      window.YouTubeUtils &&
        YouTubeUtils.logger &&
        YouTubeUtils.logger.debug &&
        YouTubeUtils.logger.debug('[YouTube+] All resources cleared');
    },
    stats: () => ({
      observers: YouTubeUtils.cleanupManager.observers.size,
      listeners: YouTubeUtils.cleanupManager.listeners.size,
      intervals: YouTubeUtils.cleanupManager.intervals.size,
      timeouts: YouTubeUtils.cleanupManager.timeouts.size,
      animationFrames: YouTubeUtils.cleanupManager.animationFrames.size,
      styles: YouTubeUtils.StyleManager.styles.size,
      notifications: YouTubeUtils.NotificationManager.activeNotifications.size,
    }),
  };

  // Show subtle startup notification (only once per session)
  if (!sessionStorage.getItem('youtube_plus_started')) {
    sessionStorage.setItem('youtube_plus_started', 'true');
    setTimeout(() => {
      if (YouTubeUtils.NotificationManager) {
        YouTubeUtils.NotificationManager.show('YouTube+ v2.4.4 loaded', {
          type: 'success',
          duration: 2000,
          position: 'bottom-right',
        });
      }
    }, 1000);
  }
}
// YouTube enhancements module
(function () {
  'use strict';

  // Local reference to translation function
  const { t } = YouTubeUtils;

  const YouTubeEnhancer = {
    // Speed control variables
    speedControl: {
      currentSpeed: 1,
      activeAnimationId: null,
      storageKey: 'youtube_playback_speed',
      availableSpeeds: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0],
    },

    // Loop control variables
    loopControl: {
      enabled: false,
      pointA: null,
      pointB: null,
      storageKey: 'youtube_loop_state',
      timeUpdateListener: null,
    },

    _initialized: false,

    // Settings
    settings: {
      enableSpeedControl: true,
      speedControlHotkeys: {
        decrease: 'g',
        increase: 'h',
        reset: 'b',
      },
      enableScreenshot: true,
      enableDownload: true,

      // Basic: optional UI/style tweaks (style.js)
      enableZenStyles: true,
      zenStyles: {
        thumbnailHover: true,
        immersiveSearch: true,
        hideVoiceSearch: true,
        transparentHeader: true,
        hideSideGuide: true,
        cleanSideGuide: false,
        fixFeedLayout: true,
        betterCaptions: true,
        playerBlur: true,
        theaterEnhancements: true,
        misc: true,
      },

      // Enhanced features (advanced tab)
      enableEnhanced: true,
      enablePlayAll: true,
      enableResumeTime: true,
      enableZoom: true,
      enableThumbnail: true,
      enablePlaylistSearch: true,
      enableScrollToTopButton: true,

      // Loop settings
      enableLoop: true,
      loopHotkeys: {
        toggleLoop: 'r',
        setPointA: 'k',
        setPointB: 'l',
        resetPoints: 'o',
      },

      // Состояние сайтов внутри сабменю кнопки Download (ytdl всегда включён)
      downloadSites: {
        direct: true,
        externalDownloader: true,
        ytdl: true,
      },
      // Настройки кастомизации download сайтов
      downloadSiteCustomization: {
        externalDownloader:
          typeof window !== 'undefined' && window.YouTubePlusConstants
            ? window.YouTubePlusConstants.DOWNLOAD_SITES.EXTERNAL_DOWNLOADER
            : { name: 'SSYouTube', url: 'https://ssyoutube.com/watch?v={videoId}' },
      },
      storageKey: 'youtube_plus_settings',
      // runtime setting: hide left side guide/footer when true
      hideSideGuide: false,
    },

    // Cache DOM queries
    _cache: new Map(),

    // Cached element getter
    getElement(selector, useCache = true) {
      if (useCache && this._cache.has(selector)) {
        const element = this._cache.get(selector);
        if (element?.isConnected) return element;
        this._cache.delete(selector);
      }

      const element = document.querySelector(selector);
      if (element && useCache) this._cache.set(selector, element);
      return element;
    },

    loadSettings() {
      try {
        const saved = localStorage.getItem(this.settings.storageKey);
        if (saved) {
          const parsed = JSON.parse(saved);
          // Use safeMerge to prevent prototype pollution
          if (window.YouTubeUtils && window.YouTubeUtils.safeMerge) {
            window.YouTubeUtils.safeMerge(this.settings, parsed);
          } else {
            // Fallback: manual safe copy
            for (const key in parsed) {
              if (
                Object.prototype.hasOwnProperty.call(parsed, key) &&
                !['__proto__', 'constructor', 'prototype'].includes(key)
              ) {
                this.settings[key] = parsed[key];
              }
            }
          }
          return;
        }

        // Migration: if no per-module settings found, try centralized SettingsManager storage
        try {
          if (
            typeof window !== 'undefined' &&
            window.YouTubeUtils &&
            YouTubeUtils.SettingsManager
          ) {
            const globalSettings = YouTubeUtils.SettingsManager.load();
            if (!globalSettings) return;

            // Map known flags (shallow mapping) to this.settings to preserve user's choices
            const sc = globalSettings.speedControl;
            if (sc && typeof sc.enabled === 'boolean') {
              this.settings.enableSpeedControl = sc.enabled;
            }

            const ss = globalSettings.screenshot;
            if (ss && typeof ss.enabled === 'boolean') this.settings.enableScreenshot = ss.enabled;

            const dl = globalSettings.download;
            if (dl && typeof dl.enabled === 'boolean') this.settings.enableDownload = dl.enabled;

            if (globalSettings.downloadSites && typeof globalSettings.downloadSites === 'object') {
              this.settings.downloadSites = {
                ...(this.settings.downloadSites || {}),
                ...globalSettings.downloadSites,
              };
            }
          }
        } catch {
          // best-effort migration; ignore failures
        }
      } catch (e) {
        console.error('Error loading settings:', e);
      }
    },

    init() {
      if (this._initialized) {
        return;
      }

      this._initialized = true;

      try {
        this.loadSettings();
        // Migrate legacy loop hotkey values to new defaults when they match previous defaults
        try {
          const lh = this.settings.loopHotkeys || {};
          let migrated = false;
          // previous defaults: setPointA: 'l', setPointB: 'o', resetPoints: 'k'
          if (lh.setPointA === 'l') {
            lh.setPointA = 'k';
            migrated = true;
          }
          if (lh.setPointB === 'o') {
            lh.setPointB = 'l';
            migrated = true;
          }
          if (lh.resetPoints === 'k') {
            lh.resetPoints = 'o';
            migrated = true;
          }
          if (migrated) {
            this.settings.loopHotkeys = lh;
            try {
              this.saveSettings();
            } catch (e) {
              console.warn('[YouTube+] Failed to save migrated loop hotkeys', e);
            }
          }
        } catch {
          /* ignore migration errors */
        }
        this.settings.speedControlHotkeys = this.settings.speedControlHotkeys || {};
        this.settings.speedControlHotkeys.decrease = this.normalizeSpeedHotkey(
          this.settings.speedControlHotkeys.decrease,
          'g'
        );
        this.settings.speedControlHotkeys.increase = this.normalizeSpeedHotkey(
          this.settings.speedControlHotkeys.increase,
          'h'
        );
        this.settings.speedControlHotkeys.reset = this.normalizeSpeedHotkey(
          this.settings.speedControlHotkeys.reset,
          'b'
        );

        // Restore saved playback speed from localStorage
        try {
          const savedSpeed = localStorage.getItem(this.speedControl.storageKey);
          if (savedSpeed !== null) {
            const parsed = Number(savedSpeed);
            if (Number.isFinite(parsed) && parsed > 0 && parsed <= 16) {
              this.speedControl.currentSpeed = parsed;
            }
          }
        } catch (e) {
          console.warn('[YouTube+] Speed restore error:', e);
        }

        // Initialize loop hotkeys
        this.settings.loopHotkeys = this.settings.loopHotkeys || {};
        this.settings.loopHotkeys.toggleLoop = this.normalizeSpeedHotkey(
          this.settings.loopHotkeys.toggleLoop,
          'r'
        );
        this.settings.loopHotkeys.setPointA = this.normalizeSpeedHotkey(
          this.settings.loopHotkeys.setPointA,
          'k'
        );
        this.settings.loopHotkeys.setPointB = this.normalizeSpeedHotkey(
          this.settings.loopHotkeys.setPointB,
          'l'
        );
        this.settings.loopHotkeys.resetPoints = this.normalizeSpeedHotkey(
          this.settings.loopHotkeys.resetPoints,
          'o'
        );

        // Restore loop state from localStorage
        this.loadLoopState();
      } catch (error) {
        console.warn('[YouTube+][Basic]', 'Failed to load settings during init:', error);
      }

      this.insertStyles();
      this.addSettingsButtonToHeader();
      this.setupNavigationObserver();

      if (location.href.includes('watch?v=')) {
        this.setupCurrentPage();
      }

      document.addEventListener('visibilitychange', () => {
        if (!document.hidden && location.href.includes('watch?v=')) {
          this.setupCurrentPage();
        }
      });

      // Keyboard shortcut: press 'S' to take a screenshot when not typing
      try {
        const screenshotKeyHandler = e => {
          // Only react to plain 's' key without modifiers
          if (!e || !e.key) return;
          if (!(e.key === 's' || e.key === 'S')) return;
          if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;

          // Ignore when focus is on editable elements
          if (this.isEditableTarget(document.activeElement)) return;

          if (!this.settings.enableScreenshot) return;

          try {
            this.captureFrame();
          } catch (err) {
            if (YouTubeUtils && YouTubeUtils.logError) {
              YouTubeUtils.logError('Basic', 'Keyboard screenshot failed', err);
            }
          }
        };

        YouTubeUtils.cleanupManager.registerListener(
          document,
          'keydown',
          screenshotKeyHandler,
          true
        );
      } catch (e) {
        if (YouTubeUtils && YouTubeUtils.logError) {
          YouTubeUtils.logError('Basic', 'Failed to register screenshot keyboard shortcut', e);
        }
      }

      // Keyboard shortcuts: adjust speed (decrease/increase)
      try {
        const speedHotkeyHandler = e => {
          if (!this.settings.enableSpeedControl || !e || !e.key) return;
          if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
          if (this.isEditableTarget(document.activeElement)) return;

          const key = String(e.key).toLowerCase();
          const decreaseKey = this.normalizeSpeedHotkey(
            this.settings.speedControlHotkeys?.decrease,
            'g'
          );
          const increaseKey = this.normalizeSpeedHotkey(
            this.settings.speedControlHotkeys?.increase,
            'h'
          );
          const resetKey = this.normalizeSpeedHotkey(this.settings.speedControlHotkeys?.reset, 'b');

          if (key === decreaseKey) {
            e.preventDefault();
            this.adjustSpeedByStep(-1);
          } else if (key === increaseKey) {
            e.preventDefault();
            this.adjustSpeedByStep(1);
          } else if (key === resetKey) {
            e.preventDefault();
            this.changeSpeed(1);
          }
        };

        YouTubeUtils.cleanupManager.registerListener(document, 'keydown', speedHotkeyHandler, true);
      } catch (e) {
        if (YouTubeUtils && YouTubeUtils.logError) {
          YouTubeUtils.logError('Basic', 'Failed to register speed keyboard shortcuts', e);
        }
      }

      // Keyboard shortcuts: loop control
      try {
        const loopHotkeyHandler = e => {
          if (!this.settings.enableLoop || !e || !e.key) return;
          if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
          if (this.isEditableTarget(document.activeElement)) return;

          const key = String(e.key).toLowerCase();
          const toggleLoopKey = this.normalizeSpeedHotkey(
            this.settings.loopHotkeys?.toggleLoop,
            'r'
          );
          const setPointAKey = this.normalizeSpeedHotkey(this.settings.loopHotkeys?.setPointA, 'k');
          const setPointBKey = this.normalizeSpeedHotkey(this.settings.loopHotkeys?.setPointB, 'l');
          const resetPointsKey = this.normalizeSpeedHotkey(
            this.settings.loopHotkeys?.resetPoints,
            'o'
          );

          if (key === toggleLoopKey) {
            e.preventDefault();
            this.toggleLoop();
          } else if (key === setPointAKey) {
            e.preventDefault();
            this.setLoopPoint('A');
          } else if (key === setPointBKey) {
            e.preventDefault();
            this.setLoopPoint('B');
          } else if (key === resetPointsKey) {
            e.preventDefault();
            this.resetLoopPoints();
          }
        };

        YouTubeUtils.cleanupManager.registerListener(document, 'keydown', loopHotkeyHandler, true);
      } catch (e) {
        if (YouTubeUtils && YouTubeUtils.logError) {
          YouTubeUtils.logError('Basic', 'Failed to register loop keyboard shortcuts', e);
        }
      }
    },

    isEditableTarget(target) {
      const active = /** @type {HTMLElement | null | undefined} */ (target);
      if (!active) return false;
      const tag = (active.tagName || '').toLowerCase();
      return (
        tag === 'input' ||
        tag === 'textarea' ||
        tag === 'select' ||
        Boolean(active.isContentEditable)
      );
    },

    normalizeSpeedHotkey(value, fallback) {
      const candidate = typeof value === 'string' ? value.trim().toLowerCase() : '';
      if (candidate) return candidate.slice(0, 1);
      return (
        String(fallback || '')
          .trim()
          .toLowerCase()
          .slice(0, 1) || 'g'
      );
    },

    adjustSpeedByStep(direction) {
      const speeds = this.speedControl.availableSpeeds;
      if (!Array.isArray(speeds) || !speeds.length) return;
      const current = Number(this.speedControl.currentSpeed);

      let closestIndex = 0;
      let closestDelta = Number.POSITIVE_INFINITY;
      for (let i = 0; i < speeds.length; i += 1) {
        const delta = Math.abs(speeds[i] - current);
        if (delta < closestDelta) {
          closestDelta = delta;
          closestIndex = i;
        }
      }

      const step = direction > 0 ? 1 : -1;
      const nextIndex = Math.max(0, Math.min(speeds.length - 1, closestIndex + step));
      if (nextIndex === closestIndex) return;
      this.changeSpeed(speeds[nextIndex]);
    },

    // ==================== Loop Functions ====================

    /**
     * Toggle loop on/off
     */
    toggleLoop() {
      if (!this.settings.enableLoop) return;

      this.loopControl.enabled = !this.loopControl.enabled;

      const video = document.querySelector('video');
      if (!video) {
        this.saveLoopState();
        return;
      }

      if (this.loopControl.enabled) {
        // If no A-B points set, just enable normal loop
        if (this.loopControl.pointA === null && this.loopControl.pointB === null) {
          video.loop = true;
        } else {
          video.loop = false;
          this.setupLoopListener(video);
        }
        YouTubeUtils.NotificationManager.show(t('loopEnabled') || 'Loop enabled', {
          duration: 1500,
          type: 'success',
        });
      } else {
        video.loop = false;
        this.removeLoopListener();
        YouTubeUtils.NotificationManager.show(t('loopDisabled') || 'Loop disabled', {
          duration: 1500,
          type: 'info',
        });
      }

      this.updateLoopProgressBar();
      this.saveLoopState();
    },

    /**
     * Set loop point A or B
     * @param {string} point - 'A' or 'B'
     */
    setLoopPoint(point) {
      if (!this.settings.enableLoop) return;

      const video = document.querySelector('video');
      if (!video) return;

      const currentTime = video.currentTime;

      if (point === 'A') {
        this.loopControl.pointA = currentTime;
        YouTubeUtils.NotificationManager.show(
          `${t('loopPointASet') || 'Point A set'}: ${this.formatTime(currentTime)}`,
          { duration: 1500, type: 'success' }
        );
      } else if (point === 'B') {
        this.loopControl.pointB = currentTime;
        YouTubeUtils.NotificationManager.show(
          `${t('loopPointBSet') || 'Point B set'}: ${this.formatTime(currentTime)}`,
          { duration: 1500, type: 'success' }
        );
      }

      // If both points are set and loop is enabled, update listener
      if (
        this.loopControl.enabled &&
        this.loopControl.pointA !== null &&
        this.loopControl.pointB !== null
      ) {
        const video = document.querySelector('video');
        if (video) {
          video.loop = false;
          this.setupLoopListener(video);
        }
      }

      this.updateLoopProgressBar();
      this.saveLoopState();
    },

    /**
     * Reset loop points A and B
     */
    resetLoopPoints() {
      if (!this.settings.enableLoop) return;

      this.loopControl.pointA = null;
      this.loopControl.pointB = null;

      // If loop is enabled, switch back to normal loop
      if (this.loopControl.enabled) {
        const video = document.querySelector('video');
        if (video) {
          video.loop = true;
          this.removeLoopListener();
        }
      }

      YouTubeUtils.NotificationManager.show(t('loopPointsReset') || 'Loop points reset', {
        duration: 1500,
        type: 'info',
      });

      this.updateLoopProgressBar();
      this.saveLoopState();
    },

    /**
     * Setup timeupdate listener for A-B loop
     * @param {HTMLVideoElement} video
     */
    setupLoopListener(video) {
      this.removeLoopListener();

      if (this.loopControl.pointA === null || this.loopControl.pointB === null) return;

      const startTime = Math.min(this.loopControl.pointA, this.loopControl.pointB);
      const endTime = Math.max(this.loopControl.pointA, this.loopControl.pointB);

      this.loopControl.timeUpdateListener = () => {
        if (this.loopControl.enabled && video.currentTime >= endTime) {
          video.currentTime = startTime;
        }
      };

      video.addEventListener('timeupdate', this.loopControl.timeUpdateListener);
    },

    /**
     * Remove timeupdate listener
     */
    removeLoopListener() {
      if (this.loopControl.timeUpdateListener) {
        const video = document.querySelector('video');
        if (video) {
          video.removeEventListener('timeupdate', this.loopControl.timeUpdateListener);
        }
        this.loopControl.timeUpdateListener = null;
      }
    },

    /**
     * Update loop progress bar indicator
     */
    updateLoopProgressBar() {
      // If neither point is set, remove any existing indicator
      if (this.loopControl.pointA === null && this.loopControl.pointB === null) {
        const existingIndicator = document.querySelector('.ytp-plus-loop-indicator');
        if (existingIndicator) existingIndicator.remove();
        return;
      }

      const video = document.querySelector('video');
      if (!video || !video.duration) return;

      // Try to find progress bar in YouTube player
      let progressBar =
        document.querySelector('.ytp-progress-bar-container') ||
        document.querySelector('.ytp-scrubber-container') ||
        document.querySelector('[role="slider"][aria-label*="video"]') ||
        document.querySelector('.html5-progress-bar');

      if (!progressBar) {
        const playbackUI = document.querySelector('.html5-video-player');
        if (playbackUI) {
          progressBar = playbackUI.querySelector('[role="slider"]');
        }
      }

      if (!progressBar) return;

      // Get or create loop indicator
      let indicator = document.querySelector('.ytp-plus-loop-indicator');
      if (!indicator) {
        indicator = document.createElement('div');
        indicator.className = 'ytp-plus-loop-indicator';
        // ensure positioned inside the progress bar
        try {
          const compStyle = window.getComputedStyle(progressBar);
          if (!compStyle || compStyle.position === 'static') {
            progressBar.style.position = 'relative';
          }
        } catch {}
        // append indicator after ensuring positioning
        progressBar.appendChild(indicator);
        // enforce overlay styles so it appears above built-in played bars
        indicator.style.position = 'absolute';
        indicator.style.top = '0';
        indicator.style.height = '100%';
        indicator.style.pointerEvents = 'none';
        indicator.style.zIndex = '1000';
      }

      // If only point A is set, show a narrow marker at A
      if (this.loopControl.pointA !== null && this.loopControl.pointB === null) {
        const startPercent = (this.loopControl.pointA / video.duration) * 100;
        indicator.style.left = `${startPercent}%`;
        indicator.style.width = `2px`;
        // Blue marker for A
        indicator.style.background = 'linear-gradient(90deg,#1976d2,#42a5f5)';
        indicator.style.borderLeft = '2px solid #1976d2';
        indicator.style.borderRight = '2px solid #1976d2';
        indicator.style.display = 'block';
        return;
      }

      // If only point B is set (rare), show a narrow marker at B
      if (this.loopControl.pointB !== null && this.loopControl.pointA === null) {
        const bPercent = (this.loopControl.pointB / video.duration) * 100;
        indicator.style.left = `${bPercent}%`;
        indicator.style.width = `2px`;
        indicator.style.background = 'linear-gradient(90deg,#1976d2,#42a5f5)';
        indicator.style.borderLeft = '2px solid #1976d2';
        indicator.style.borderRight = '2px solid #1976d2';
        indicator.style.display = 'block';
        return;
      }

      // Both A and B set: draw the range and color it blue
      const startTime = Math.min(this.loopControl.pointA, this.loopControl.pointB);
      const endTime = Math.max(this.loopControl.pointA, this.loopControl.pointB);

      // Calculate percentage positions
      const startPercent = (startTime / video.duration) * 100;
      const endPercent = (endTime / video.duration) * 100;

      indicator.style.left = `${startPercent}%`;
      indicator.style.width = `${Math.max(0.2, endPercent - startPercent)}%`;
      // Blue gradient for A->B ranges
      indicator.style.background =
        'linear-gradient(90deg,rgba(25,118,210,0.28) 0%,rgba(66,165,245,0.4) 50%,rgba(25,118,210,0.28) 100%)';
      indicator.style.borderLeft = '2px solid #1976d2';
      indicator.style.borderRight = '2px solid #1976d2';
      indicator.style.display = 'block';
    },

    /**
     * Apply saved loop state to current video element.
     */
    applyLoopStateToCurrentVideo() {
      const video = document.querySelector('video');
      if (!video) return;

      this.removeLoopListener();

      if (!this.settings.enableLoop || !this.loopControl.enabled) {
        video.loop = false;
        this.updateLoopProgressBar();
        return;
      }

      if (this.loopControl.pointA !== null && this.loopControl.pointB !== null) {
        video.loop = false;
        this.setupLoopListener(video);
      } else {
        video.loop = true;
      }

      this.updateLoopProgressBar();
    },

    /**
     * Save loop state to localStorage
     */
    saveLoopState() {
      try {
        const state = {
          enabled: this.loopControl.enabled,
          pointA: this.loopControl.pointA,
          pointB: this.loopControl.pointB,
        };
        localStorage.setItem(this.loopControl.storageKey, JSON.stringify(state));
      } catch (e) {
        console.warn('[YouTube+] Failed to save loop state:', e);
      }
    },

    /**
     * Load loop state from localStorage
     */
    loadLoopState() {
      try {
        const saved = localStorage.getItem(this.loopControl.storageKey);
        if (saved) {
          const state = JSON.parse(saved);
          this.loopControl.enabled = Boolean(state?.enabled);
          this.loopControl.pointA =
            typeof state?.pointA === 'number' && Number.isFinite(state.pointA)
              ? state.pointA
              : null;
          this.loopControl.pointB =
            typeof state?.pointB === 'number' && Number.isFinite(state.pointB)
              ? state.pointB
              : null;

          setTimeout(() => this.applyLoopStateToCurrentVideo(), 1000);
        }
      } catch (e) {
        console.warn('[YouTube+] Failed to load loop state:', e);
      }
    },

    /**
     * Format time in MM:SS format
     * @param {number} seconds
     * @returns {string}
     */
    formatTime(seconds) {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    },

    // ==================== End Loop Functions ====================

    saveSettings() {
      localStorage.setItem(this.settings.storageKey, JSON.stringify(this.settings));
      this.updatePageBasedOnSettings();
      this.refreshDownloadButton();

      // Expose and broadcast updated settings so other modules can react live.
      try {
        window.youtubePlus = window.youtubePlus || {};
        window.youtubePlus.settings = this.settings;
        window.dispatchEvent(
          new CustomEvent('youtube-plus-settings-updated', {
            detail: this.settings,
          })
        );
      } catch (e) {
        console.warn('[YouTube+] Settings broadcast error:', e);
      }
    },

    updatePageBasedOnSettings() {
      const settingsMap = {
        'ytp-screenshot-button': 'enableScreenshot',
        'ytp-download-button': 'enableDownload',
        'speed-control-btn': 'enableSpeedControl',
      };

      Object.entries(settingsMap).forEach(([className, setting]) => {
        const button = this.getElement(`.${className}`, false);
        if (button) button.style.display = this.settings[setting] ? '' : 'none';
      });

      // Also handle speed options dropdown (attached to body)
      const speedOptions = document.querySelector('.speed-options');
      if (speedOptions) {
        speedOptions.style.display = this.settings.enableSpeedControl ? '' : 'none';
      }
    },

    /**
     * Refresh download button visibility - Delegates to download-button module
     */
    refreshDownloadButton() {
      // Use extracted download button module
      if (typeof window !== 'undefined' && window.YouTubePlusDownloadButton) {
        const manager = window.YouTubePlusDownloadButton.createDownloadButtonManager({
          settings: this.settings,
          t,
          getElement: this.getElement.bind(this),
          YouTubeUtils,
        });
        manager.refreshDownloadButton();
      }
    },

    setupCurrentPage() {
      this.waitForElement('#player-container-outer .html5-video-player, .ytp-right-controls', 5000)
        .then(() => {
          this.addCustomButtons();
          this.setupVideoObserver();
          this.applyCurrentSpeed();
          this.applyLoopStateToCurrentVideo();
          this.updatePageBasedOnSettings();
          this.refreshDownloadButton();
        })
        .catch(() => {});
    },

    insertStyles() {
      // === CRITICAL CSS: variables, player controls, speed, notifications ===
      // Injected synchronously — minimal set needed before first paint
      const criticalStyles = `:root{--yt-accent:#ff0000;--yt-accent-hover:#cc0000;--yt-radius-sm:6px;--yt-radius-md:10px;--yt-radius-lg:16px;--yt-transition:all .2s ease;--yt-space-xs:4px;--yt-space-sm:8px;--yt-space-md:16px;--yt-space-lg:24px;--yt-glass-blur:blur(18px) saturate(180%);--yt-glass-blur-light:blur(12px) saturate(160%);--yt-glass-blur-heavy:blur(24px) saturate(200%);}
        html[dark],html:not([dark]):not([light]){--yt-bg-primary:rgba(15,15,15,.85);--yt-bg-secondary:rgba(28,28,28,.85);--yt-bg-tertiary:rgba(34,34,34,.85);--yt-text-primary:#fff;--yt-text-secondary:#aaa;--yt-border-color:rgba(255,255,255,.2);--yt-hover-bg:rgba(255,255,255,.1);--yt-shadow:0 4px 12px rgba(0,0,0,.25);--yt-glass-bg:rgba(255,255,255,.1);--yt-glass-border:rgba(255,255,255,.2);--yt-glass-shadow:0 8px 32px rgba(0,0,0,.2);--yt-modal-bg:rgba(0,0,0,.75);--yt-notification-bg:rgba(28,28,28,.9);--yt-panel-bg:rgba(34,34,34,.3);--yt-header-bg:rgba(20,20,20,.6);--yt-input-bg:rgba(255,255,255,.1);--yt-button-bg:rgba(255,255,255,.2);--yt-text-stroke:white;}
        html[light]{--yt-bg-primary:rgba(255,255,255,.85);--yt-bg-secondary:rgba(248,248,248,.85);--yt-bg-tertiary:rgba(240,240,240,.85);--yt-text-primary:#030303;--yt-text-secondary:#606060;--yt-border-color:rgba(0,0,0,.2);--yt-hover-bg:rgba(0,0,0,.05);--yt-shadow:0 4px 12px rgba(0,0,0,.15);--yt-glass-bg:rgba(255,255,255,.7);--yt-glass-border:rgba(0,0,0,.1);--yt-glass-shadow:0 8px 32px rgba(0,0,0,.1);--yt-modal-bg:rgba(0,0,0,.5);--yt-notification-bg:rgba(255,255,255,.95);--yt-panel-bg:rgba(255,255,255,.7);--yt-header-bg:rgba(248,248,248,.8);--yt-input-bg:rgba(0,0,0,.05);--yt-button-bg:rgba(0,0,0,.1);--yt-text-stroke:#030303;}
        .ytp-screenshot-button,.ytp-cobalt-button,.ytp-pip-button{position:relative;width:44px;height:100%;display:inline-flex;align-items:center;justify-content:center;vertical-align:top;transition:opacity .15s,transform .15s;}
        .ytp-screenshot-button:hover,.ytp-cobalt-button:hover,.ytp-pip-button:hover{transform:scale(1.1);}
        .speed-control-btn{width:4em!important;position:relative!important;display:inline-flex!important;align-items:center!important;justify-content:center!important;height:100%!important;vertical-align:top!important;text-align:center!important;border-radius:var(--yt-radius-sm);font-size:13px;color:var(--yt-text-primary);cursor:pointer;user-select:none;font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:color .2s;}
        .speed-control-btn:hover{color:var(--yt-accent);font-weight:bold;}
        .speed-options{position:fixed!important;background:var(--yt-glass-bg)!important;color:var(--yt-text-primary)!important;border-radius:var(--yt-radius-md)!important;display:flex!important;flex-direction:column!important;align-items:stretch!important;gap:0!important;transform:translate(-50%,12px)!important;width:92px!important;z-index:2147483647!important;box-shadow:var(--yt-glass-shadow);border:1px solid var(--yt-glass-border);overflow:hidden;opacity:0;pointer-events:none!important;transition:opacity .18s ease,transform .18s ease;box-sizing:border-box;}
        .speed-options.visible{opacity:1;pointer-events:auto!important;transform:translate(-50%,0)!important;backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);}
        .speed-option-item{cursor:pointer!important;height:28px!important;line-height:28px!important;font-size:12px!important;text-align:center!important;transition:background-color .15s,color .15s;}
        .speed-option-active,.speed-option-item:hover{color:var(--yt-accent)!important;font-weight:bold!important;background:var(--yt-hover-bg)!important;}
        #speed-indicator{position:absolute!important;margin:auto!important;top:0!important;right:0!important;bottom:0!important;left:0!important;border-radius:24px!important;font-size:30px!important;background:var(--yt-glass-bg)!important;color:var(--yt-text-primary)!important;z-index:99999!important;width:80px!important;height:80px!important;line-height:80px!important;text-align:center!important;display:none;box-shadow:var(--yt-glass-shadow);border:1px solid var(--yt-glass-border);}
        .youtube-enhancer-notification-container{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:10px;z-index:2147483647;pointer-events:none;max-width:calc(100% - 32px);width:100%;box-sizing:border-box;padding:0 16px;}
        .youtube-enhancer-notification{position:relative;max-width:700px;width:auto;background:var(--yt-glass-bg);color:var(--yt-text-primary);padding:8px 14px;font-size:13px;border-radius:var(--yt-radius-md);z-index:inherit;transition:opacity .35s,transform .32s;box-shadow:var(--yt-glass-shadow);border:1px solid var(--yt-glass-border);font-weight:500;box-sizing:border-box;display:flex;align-items:center;gap:10px;pointer-events:auto;}
        .ytp-plus-loop-indicator{position:absolute;height:100%;background:linear-gradient(90deg,rgba(25,118,210,0.28) 0%,rgba(66,165,245,0.4) 50%,rgba(25,118,210,0.28) 100%);border-left:2px solid #1976d2;border-right:2px solid #1976d2;display:none;pointer-events:none;top:0;z-index:1000;box-shadow:inset 0 0 4px rgba(25,118,210,0.25);}
        .ytp-plus-settings-button{background:transparent;border:none;color:var(--yt-text-secondary);cursor:pointer;padding:var(--yt-space-sm);margin-right:var(--yt-space-sm);border:none;display:flex;align-items:center;justify-content:center;transition:background-color .2s,transform .2s;}
        .ytp-plus-settings-button svg{width:24px;height:24px;}
        .ytp-plus-settings-button:hover{transform:rotate(30deg);color:var(--yt-text-secondary);}
        .ytp-download-button{position:relative!important;display:inline-flex!important;align-items:center!important;justify-content:center!important;height:100%!important;vertical-align:top!important;cursor:pointer!important;}
        @keyframes ytEnhanceFadeIn{from{opacity:0;}to{opacity:1;}}
        @keyframes ytEnhanceScaleIn{from{opacity:0;transform:scale(.92) translateY(10px);}to{opacity:1;transform:scale(1) translateY(0);}}
        .ytSearchboxComponentInputBox { background: transparent !important; }`;
      // === NON-CRITICAL CSS: settings modal, voting, glass utilities ===
      // Deferred via requestIdleCallback — only needed when user opens settings
      const nonCriticalStyles = `
        .ytp-plus-settings-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;z-index:100000;backdrop-filter:blur(8px) saturate(140%);-webkit-backdrop-filter:blur(8px) saturate(140%);animation:ytEnhanceFadeIn .25s ease-out;contain:layout style paint;}
        .ytp-plus-settings-panel{background:var(--yt-glass-bg);color:var(--yt-text-primary);border-radius:20px;width:760px;max-width:94%;max-height:60vh;overflow:hidden;box-shadow:0 12px 40px rgba(0,0,0,0.45);animation:ytEnhanceScaleIn .28s cubic-bezier(.4,0,.2,1);backdrop-filter:blur(14px) saturate(140%);-webkit-backdrop-filter:blur(14px) saturate(140%);border:1.5px solid var(--yt-glass-border);will-change:transform,opacity;display:flex;flex-direction:row;contain:layout style paint;}
        .ytp-plus-settings-sidebar{width:240px;background:var(--yt-header-bg);border-right:1px solid var(--yt-glass-border);display:flex;flex-direction:column;backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);}
        .ytp-plus-settings-sidebar-header{padding:var(--yt-space-md) var(--yt-space-lg);border-bottom:1px solid var(--yt-glass-border);display:flex;justify-content:space-between;align-items:center;}
        .ytp-plus-settings-title{font-size:18px;font-weight:500;margin:0;color:var(--yt-text-primary);}
        .ytp-plus-settings-sidebar-close{padding:var(--yt-space-md) var(--yt-space-lg);display:flex;justify-content:flex-end;background:transparent;}
        .ytp-plus-settings-close{background:none;border:none;cursor:pointer;padding:var(--yt-space-sm);margin:-8px;color:var(--yt-text-primary);transition:color .2s,transform .2s;}
        .ytp-plus-settings-close:hover{color:var(--yt-accent);transform:scale(1.25) rotate(90deg);}
        .ytp-plus-settings-nav{flex:1;padding:var(--yt-space-md) 0;}
        .ytp-plus-settings-nav-item{display:flex;align-items:center;padding:12px var(--yt-space-lg);cursor:pointer;transition:all .2s cubic-bezier(.4,0,.2,1);font-size:14px;border-left:3px solid transparent;color:var(--yt-text-primary);}
        .ytp-plus-settings-nav-item:hover{background:var(--yt-hover-bg);}
        .ytp-plus-settings-nav-item.active{background:rgba(255,0,0,.1);border-left-color:var(--yt-accent);color:var(--yt-accent);font-weight:500;}
        .ytp-plus-settings-nav-item svg{width:18px;height:18px;margin-right:12px;opacity:.8;transition:opacity .2s,transform .2s;}
        .ytp-plus-settings-nav-item.active svg{opacity:1;transform:scale(1.1);}
        .ytp-plus-settings-nav-item:hover svg{transform:scale(1.05);}
        .ytp-plus-settings-main{flex:1;display:flex;flex-direction:column;overflow-y:auto;}
        .ytp-plus-settings-header{padding:var(--yt-space-md) var(--yt-space-lg);border-bottom:1px solid var(--yt-glass-border);background:var(--yt-header-bg);backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);}
        .ytp-plus-settings-content{flex:1;padding:var(--yt-space-md) var(--yt-space-lg);overflow-y:auto;}
        .ytp-plus-settings-section{margin-bottom:var(--yt-space-lg);}
        .ytp-plus-settings-section-title{font-size:16px;font-weight:500;margin-bottom:var(--yt-space-md);color:var(--yt-text-primary);}
        .ytp-plus-settings-section.hidden{display:none !important;}
        .ytp-plus-settings-item{display:flex;align-items:center;margin-bottom:var(--yt-space-md);padding:14px 18px;background:transparent;transition:all .25s cubic-bezier(.4,0,.2,1);border-radius:var(--yt-radius-md);}
        .ytp-plus-settings-item:hover{background:var(--yt-hover-bg);transform:translateX(6px);box-shadow:0 2px 8px rgba(0,0,0,.1);}
        .ytp-plus-settings-item-actions{display:flex;align-items:center;gap:10px;margin-left:auto;}
        .ytp-plus-submenu-toggle{width:26px;height:26px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:1px solid var(--yt-glass-border);color:var(--yt-text-primary);cursor:pointer;opacity:.9;transition:transform .15s ease,background-color .15s ease,opacity .15s ease;}
        .ytp-plus-submenu-toggle:hover{background:var(--yt-hover-bg);transform:scale(1.06);}
        .ytp-plus-submenu-toggle:disabled{opacity:.35;cursor:not-allowed;transform:none;}
        .ytp-plus-submenu-toggle svg{width:16px;height:16px;transition:transform .15s ease;}
        .ytp-plus-submenu-toggle[aria-expanded="false"] svg{transform:rotate(-90deg);}
        .ytp-plus-submenu-toggle[aria-expanded="true"] svg{transform:rotate(0deg);}
        .ytp-plus-settings-item-label{flex:1;font-size:14px;color:var(--yt-text-primary);}
        .ytp-plus-settings-item-description{font-size:12px;color:var(--yt-text-secondary);margin-top:4px;}
        .ytp-plus-settings-checkbox{appearance:none;-webkit-appearance:none;-moz-appearance:none;width:20px;height:20px;min-width:20px;min-height:20px;margin-left:auto;border:2px solid var(--yt-glass-border);border-radius:50%;background:transparent;display:inline-flex;align-items:center;justify-content:center;transition:all 250ms cubic-bezier(.4,0,.23,1);cursor:pointer;position:relative;flex-shrink:0;color:#fff;box-sizing:border-box;}
        html:not([dark]) .ytp-plus-settings-checkbox{border-color:rgba(0,0,0,.25);color:#222;}
        .ytp-plus-settings-checkbox:focus-visible{outline:2px solid var(--yt-accent);outline-offset:2px;}
        .ytp-plus-settings-checkbox:hover{background:var(--yt-hover-bg);transform:scale(1.1);}
        .ytp-plus-settings-checkbox::before{content:"";width:5px;height:2px;background:var(--yt-text-primary);position:absolute;transform:rotate(45deg);top:6px;left:3px;transition:width 100ms ease 50ms,opacity 50ms;transform-origin:0% 0%;opacity:0;}
        .ytp-plus-settings-checkbox::after{content:"";width:0;height:2px;background:var(--yt-text-primary);position:absolute;transform:rotate(305deg);top:12px;left:7px;transition:width 100ms ease,opacity 50ms;transform-origin:0% 0%;opacity:0;}
        .ytp-plus-settings-checkbox:checked{transform:rotate(0deg) scale(1.15);}
        .ytp-plus-settings-checkbox:checked::before{width:9px;opacity:1;background:#fff;transition:width 150ms ease 100ms,opacity 150ms ease 100ms;}
        .ytp-plus-settings-checkbox:checked::after{width:16px;opacity:1;background:#fff;transition:width 150ms ease 250ms,opacity 150ms ease 250ms;}
        .ytp-plus-footer{padding:var(--yt-space-md) var(--yt-space-lg);border-top:1px solid var(--yt-glass-border);display:flex;justify-content:flex-end;background:transparent;}
        .ytp-plus-button{padding:var(--yt-space-sm) var(--yt-space-md);border-radius:18px;border:none;font-size:14px;font-weight:500;cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1);}
        .ytp-plus-button-primary{background:transparent;border:1px solid var(--yt-glass-border);color:var(--yt-text-primary);}
        .ytp-plus-button-primary:hover{background:var(--yt-accent);color:#fff;box-shadow:0 6px 16px rgba(255,0,0,.35);transform:translateY(-2px);}
        .app-icon{fill:var(--yt-text-primary);stroke:var(--yt-text-primary);transition:all .3s;}
        @media(max-width:768px){.ytp-plus-settings-panel{width:95%;max-height:80vh;flex-direction:column;}
        .ytp-plus-settings-sidebar{width:100%;max-height:120px;flex-direction:row;overflow-x:auto;}
        .ytp-plus-settings-nav{display:flex;flex-direction:row;padding:0;}
        .ytp-plus-settings-nav-item{white-space:nowrap;border-left:none;border-bottom:3px solid transparent;}
        .ytp-plus-settings-nav-item.active{border-left:none;border-bottom-color:var(--yt-accent);}
        .ytp-plus-settings-item{padding:10px 12px;}}
        .ytp-plus-settings-section h1{margin:-95px 90px 8px;font-family:'Montserrat',sans-serif;font-size:52px;font-weight:600;color:transparent;-webkit-text-stroke-width:1px;-webkit-text-stroke-color:var(--yt-text-stroke);cursor:pointer;transition:color .2s;}
        .ytp-plus-settings-section h1:hover{color:var(--yt-accent);-webkit-text-stroke-width:1px;-webkit-text-stroke-color:transparent;}
        .download-options{position:fixed;background:var(--yt-glass-bg);color:var(--yt-text-primary);border-radius:var(--yt-radius-md);width:150px;z-index:2147483647;box-shadow:var(--yt-glass-shadow);border:1px solid var(--yt-glass-border);overflow:hidden;opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease;transform:translateY(8px);box-sizing:border-box;}
        .download-options.visible{opacity:1;pointer-events:auto;transform:translateY(0);backdrop-filter:var(--yt-glass-blur);-webkit-backdrop-filter:var(--yt-glass-blur);}
        .download-options-list{display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;}
        .download-option-item{cursor:pointer;padding:12px;text-align:center;transition:background .2s,color .2s;width:100%;}
        .download-option-item:hover{background:var(--yt-hover-bg);color:var(--yt-accent);}
        .glass-panel{background:var(--yt-glass-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);box-shadow:var(--yt-glass-shadow);}
        .glass-card{background:var(--yt-panel-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);padding:var(--yt-space-md);box-shadow:var(--yt-shadow);}
        .glass-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--yt-modal-bg);display:flex;align-items:center;justify-content:center;z-index:99999;}
        .glass-button{background:var(--yt-button-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);padding:var(--yt-space-sm) var(--yt-space-md);color:var(--yt-text-primary);cursor:pointer;transition:all .2s ease;}
        .glass-button:hover{background:var(--yt-hover-bg);transform:translateY(-1px);box-shadow:var(--yt-shadow);}
        .download-submenu{margin:4px 0 12px 12px;}
        .download-submenu-container{display:flex;flex-direction:column;gap:8px;}
        .style-submenu{margin:4px 0 12px 12px;}
        .style-submenu-container{display:flex;flex-direction:column;gap:8px;}
        .speed-submenu{margin:4px 0 12px 12px;}
        .speed-submenu-container{display:flex;flex-direction:column;gap:8px;}
        .speed-hotkeys-row{flex-direction:column!important;align-items:stretch!important;gap:6px;}
        .speed-hotkeys-info{display:flex;flex-direction:column;gap:4px;}
        .speed-hotkeys-fields{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-top:12px;width:100%;}
        .speed-hotkey-field{display:flex;flex-direction:column;align-items:center;gap:8px;font-size:12px;color:var(--yt-text-secondary);flex:1;min-width:80px;}
        .speed-hotkey-field span{text-align:center;width:100%;}
        .speed-hotkey-input{width:100%;height:36px;border-radius:8px;border:1px solid var(--yt-glass-border);background:var(--yt-glass-bg);color:var(--yt-text-primary);text-align:center;text-transform:uppercase;}
        .speed-hotkey-input:focus{background:var(--yt-hover-bg);}
        .loop-submenu-container{display:flex;flex-direction:column;gap:8px;}
        .loop-hotkeys-row{flex-direction:column!important;align-items:stretch!important;gap:6px;}
        .loop-hotkeys-info{display:flex;flex-direction:column;gap:4px;}
        .loop-hotkeys-fields{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-top:12px;width:100%;}
        .loop-hotkey-field{display:flex;flex-direction:column;align-items:center;gap:8px;font-size:12px;color:var(--yt-text-secondary);flex:1;min-width:80px;}
        .loop-hotkey-field span{text-align:center;width:100%;}
        .loop-hotkey-input{width:100%;height:36px;border-radius:8px;border:1px solid var(--yt-glass-border);background:var(--yt-glass-bg);color:var(--yt-text-primary);text-align:center;text-transform:uppercase;}
        .loop-hotkey-input:focus{background:var(--yt-hover-bg);}
        .download-site-option{display:flex;flex-direction:column;align-items:stretch;gap:8px;padding:10px;border-radius:var(--yt-radius-md);transition:background .2s;}
        .download-site-option:hover{background:var(--yt-hover-bg);}
        .download-site-header{display:flex;flex-direction:row;align-items:center;justify-content:space-between;width:100%;gap:12px;}
        .download-site-label{flex:1;cursor:pointer;display:flex;flex-direction:column;}
        .download-site-controls{width:100%;margin-top:4px;padding-top:10px;border-top:1px solid var(--yt-glass-border);}
        .download-site-input{width:95%;margin-top:8px;padding:8px;background:var(--yt-glass-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-sm);color:var(--yt-text-primary);font-size:13px;transition:all .2s;}
        .download-site-input:focus{border-color:var(--yt-accent);background:var(--yt-hover-bg);}
        .download-site-input.small{margin-top:6px;font-size:12px;}
        .download-site-cta{display:flex;flex-direction:row;gap:8px;margin-top:10px;}
        .download-site-cta .glass-button{flex:1;justify-content:center;font-size:13px;padding:8px 12px;}
        .download-site-cta .glass-button.danger{background:rgba(255,59,59,0.15);border-color:rgba(255,59,59,0.3);}
        .download-site-cta .glass-button.danger:hover{background:rgba(255,59,59,0.25);}
        .download-site-option .ytp-plus-settings-checkbox{margin:0;}
        .download-site-name{font-weight:500;font-size:15px;color:var(--yt-text-primary);}
        .download-site-desc{font-size:12px;color:var(--yt-text-secondary);margin-top:2px;opacity:0.8;}
        .ytp-plus-settings-panel select,
        .ytp-plus-settings-panel select option {background: var(--yt-panel-bg) !important; color: var(--yt-text-primary) !important;}
        .ytp-plus-settings-panel select {-webkit-appearance: menulist !important; appearance: menulist !important; padding: 6px 8px !important; border-radius: 6px !important; border: 1px solid var(--yt-glass-border) !important;}
        .glass-dropdown{position:relative;display:inline-block;min-width:110px}
        .glass-dropdown__toggle{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:6px 8px;border-radius:8px;background:linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));color:inherit;border:1px solid rgba(255,255,255,0.06);cursor:pointer}
        .glass-dropdown__toggle:focus{outline:2px solid rgba(255,255,255,0.06)}
        .glass-dropdown__label{font-size:12px}
        .glass-dropdown__chev{opacity:0.9}
        .glass-dropdown__list{position:absolute;left:0;right:0;top:calc(100% + 8px);z-index:20000;display:none;margin:0;padding:6px;border-radius:10px;list-style:none;background:var(--yt-header-bg);border:1px solid rgba(255,255,255,0.06);box-shadow:0 8px 30px rgba(0,0,0,0.5);backdrop-filter:blur(10px) saturate(130%);-webkit-backdrop-filter:blur(10px) saturate(130%);max-height:220px;overflow:auto}
        .glass-dropdown__item{padding:8px 10px;border-radius:6px;margin:4px 0;cursor:pointer;color:inherit;font-size:13px}
        .glass-dropdown__item:hover{background:rgba(255,255,255,0.04)}
        .glass-dropdown__item[aria-selected="true"]{background:linear-gradient(90deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));box-shadow:inset 0 0 0 1px rgba(255,255,255,0.02)}
        .ytp-plus-settings-voting-header{margin-bottom:var(--yt-space-lg);}
        .ytp-plus-settings-voting-header h3{font-size:18px;font-weight:500;margin:0 0 8px 0;color:var(--yt-text-primary);}
        .ytp-plus-settings-voting-desc{font-size:13px;color:var(--yt-text-secondary);margin:0;}
        .ytp-plus-voting{display:flex;flex-direction:column;gap:12px;}
        .ytp-plus-voting-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;}
        .ytp-plus-voting-list{display:flex;flex-direction:column;gap:12px;}
        .ytp-plus-voting-item{display:flex;align-items:flex-start;justify-content:space-between;padding:16px;background:var(--yt-glass-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);transition:all .2s ease;gap:12px;}
        .ytp-plus-voting-item:hover{background:var(--yt-hover-bg);transform:translateX(4px);}
        .ytp-plus-voting-item-content{flex:1;padding-right:16px;}
        .ytp-plus-voting-item-title{font-size:14px;font-weight:500;color:var(--yt-text-primary);margin-bottom:4px;}
        .ytp-plus-voting-item-desc{font-size:12px;color:var(--yt-text-secondary);line-height:1.4;}
        .ytp-plus-voting-item-status{font-size:11px;padding:2px 8px;border-radius:10px;display:inline-block;margin-top:8px;background:rgba(255,255,255,0.1);color:var(--yt-text-secondary);}
        .ytp-plus-voting-item-status.completed{background:rgba(76,175,80,0.2);color:#4caf50;}
        .ytp-plus-voting-item-status.in-progress{background:rgba(255,193,7,0.2);color:#ffc107;}
        .ytp-plus-voting-item-votes{display:flex;flex-direction:column;align-items:stretch;gap:8px;min-width:120px;}
        .ytp-plus-voting-score{display:flex;align-items:baseline;gap:8px;justify-content:center;}
        .ytp-plus-vote-total{font-size:12px;color:var(--yt-text-secondary);}
        .ytp-plus-voting-buttons{position:relative;display:flex;justify-content:center;gap:0;border:1px solid var(--yt-glass-border);border-radius:20px;overflow:hidden;}
        .ytp-plus-voting-buttons-track{position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;transition:background .4s ease;border-radius:20px;pointer-events:none;}
        .ytp-plus-vote-btn{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:42px;height:32px;border:none;background:transparent;cursor:pointer;transition:color .15s ease,opacity .15s ease;color:var(--yt-text-secondary);opacity:.95}
        .ytp-plus-vote-btn:first-of-type{border-right:1px solid var(--yt-glass-border)}
        .ytp-plus-vote-btn:hover{color:var(--yt-text-primary);opacity:1}
        .ytp-plus-vote-btn.active{color:#fff;opacity:1}
        .ytp-plus-vote-icon{width:20px;height:20px;fill:currentColor;opacity:.92}
        .ytp-plus-vote-btn.active .ytp-plus-vote-icon,.ytp-plus-vote-btn:hover .ytp-plus-vote-icon{opacity:1}
        .ytp-plus-voting-loading,.ytp-plus-voting-empty{text-align:center;padding:24px;color:var(--yt-text-secondary);font-size:13px;}
        .ytp-plus-voting-add-btn{background:var(--yt-accent);color:#fff;border:none;padding:8px 16px;border-radius:18px;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s ease;}
        .ytp-plus-voting-add-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(255,0,0,.3);}
        .ytp-plus-voting-add-form{margin-top:16px;padding:16px;background:var(--yt-glass-bg);border:1px solid var(--yt-glass-border);border-radius:var(--yt-radius-md);}
        .ytp-plus-voting-add-form input,.ytp-plus-voting-add-form textarea{width:100%;padding:10px 12px;margin-bottom:12px;background:var(--yt-header-bg);border:1px solid var(--yt-glass-border);border-radius:8px;color:var(--yt-text-primary);font-size:13px;box-sizing:border-box;}
        .ytp-plus-voting-add-form input:focus,.ytp-plus-voting-add-form textarea:focus{border-color:var(--yt-accent);outline:none;}
        .ytp-plus-voting-add-form textarea{min-height:80px;resize:vertical;}
        .ytp-plus-voting-form-actions{display:flex;gap:8px;justify-content:flex-end;}
        .ytp-plus-voting-cancel{background:transparent;border:1px solid var(--yt-glass-border);color:var(--yt-text-primary);padding:8px 16px;border-radius:18px;font-size:13px;cursor:pointer;transition:all .2s ease;}
        .ytp-plus-voting-cancel:hover{background:var(--yt-hover-bg);}
        .ytp-plus-voting-submit{background:var(--yt-accent);color:#fff;border:none;padding:8px 16px;border-radius:18px;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s ease;}
        .ytp-plus-voting-submit:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(255,0,0,.3);}
        @media (max-width: 680px){.ytp-plus-voting-item{flex-direction:column;align-items:stretch}.ytp-plus-voting-item-content{padding-right:0}.ytp-plus-voting-item-votes{min-width:0;width:100%}}
        .ytp-plus-voting-preview{margin-bottom:20px;}
        .ytp-plus-ba-container{position:relative;width:100%;height:260px;overflow:hidden;border-radius:var(--yt-radius-md);border:1px solid var(--yt-glass-border);user-select:none;cursor:ew-resize;background:#000;}
        .ytp-plus-ba-before,.ytp-plus-ba-after{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;}
        .ytp-plus-ba-before img,.ytp-plus-ba-after img{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;display:block;pointer-events:none;}
        .ytp-plus-ba-after{clip-path:inset(0 0 0 50%);}
        .ytp-plus-ba-divider{position:absolute;top:0;left:50%;transform:translateX(-50%);width:8px;height:100%;background:transparent;pointer-events:auto;z-index:3;cursor:ew-resize;transition:left .6s linear}
        .ytp-plus-ba-divider::after{content:'';position:absolute;left:50%;top:0;transform:translateX(-50%);width:2px;height:100%;background:var(--yt-accent,#f00);}        
        .ytp-plus-ba-divider.autoplay{animation:ytpPlusSlideDivider 6s linear infinite}
        @keyframes ytpPlusSlideDivider{0%{left:10%}50%{left:90%}100%{left:10%}}
        .ytp-plus-ba-label{position:absolute;top:10px;padding:4px 10px;border-radius:4px;font-size:12px;font-weight:600;color:#fff;background:rgba(0,0,0,.55);pointer-events:none;z-index:5;}
        .ytp-plus-ba-label-before{left:10px;}
        .ytp-plus-ba-label-after{right:10px;}
        .ytp-plus-vote-bar-section{margin-top:12px;display:flex;flex-direction:column;align-items:center;gap:6px;}
        .ytp-plus-vote-bar-buttons{position:relative;display:flex;gap:0;border-radius:20px;overflow:hidden;border:1px solid var(--yt-glass-border);}
        .ytp-plus-vote-bar-track{position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;transition:background .4s ease;background:linear-gradient(to right, #4caf50 50%, #f44336 50%);border-radius:20px;}
        .ytp-plus-vote-bar-btn{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;padding:8px 18px;background:transparent;border:none;color:var(--yt-text-secondary);cursor:pointer;transition:color .15s;font-size:14px;}
        .ytp-plus-vote-bar-btn:first-of-type{border-right:1px solid var(--yt-glass-border);}
        .ytp-plus-vote-bar-btn:hover{color:var(--yt-text-primary);}
        .ytp-plus-vote-bar-btn.active{color:#fff;}
        .ytp-plus-vote-bar-btn svg{fill:currentColor;}
        .ytp-plus-vote-bar-count{font-size:12px;color:var(--yt-text-secondary);}`;

      const injectNonCritical = () => {
        if (!document.getElementById('yt-enhancer-nc-styles')) {
          const ncEl = document.createElement('style');
          ncEl.id = 'yt-enhancer-nc-styles';
          ncEl.textContent = nonCriticalStyles;
          (document.head || document.documentElement).appendChild(ncEl);
        }
      };
      this.ensureNonCriticalStyles = injectNonCritical;

      if (!document.getElementById('yt-enhancer-main')) {
        // Inject critical CSS immediately
        YouTubeUtils.StyleManager.add('yt-enhancer-main', criticalStyles);
      }

      // Defer non-critical CSS (settings modal, voting, glass utilities)
      if (typeof requestIdleCallback === 'function') {
        requestIdleCallback(injectNonCritical, { timeout: 5000 });
      } else {
        setTimeout(injectNonCritical, 1000);
      }
    },

    addSettingsButtonToHeader() {
      this.waitForElement('ytd-masthead #end', 5000)
        .then(headerEnd => {
          if (!this.getElement('.ytp-plus-settings-button')) {
            const settingsButton = document.createElement('div');
            settingsButton.className = 'ytp-plus-settings-button';
            settingsButton.setAttribute('title', t('youtubeSettings'));
            settingsButton.innerHTML = `
                <svg width="24" height="24" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">
                  <path d="M39.23,26a16.52,16.52,0,0,0,.14-2,16.52,16.52,0,0,0-.14-2l4.33-3.39a1,1,0,0,0,.25-1.31l-4.1-7.11a1,1,0,0,0-1.25-.44l-5.11,2.06a15.68,15.68,0,0,0-3.46-2l-.77-5.43a1,1,0,0,0-1-.86H19.9a1,1,0,0,0-1,.86l-.77,5.43a15.36,15.36,0,0,0-3.46,2L9.54,9.75a1,1,0,0,0-1.25.44L4.19,17.3a1,1,0,0,0,.25,1.31L8.76,22a16.66,16.66,0,0,0-.14,2,16.52,16.52,0,0,0,.14,2L4.44,29.39a1,1,0,0,0-.25,1.31l4.1,7.11a1,1,0,0,0,1.25.44l5.11-2.06a15.68,15.68,0,0,0,3.46,2l.77,5.43a1,1,0,0,0,1,.86h8.2a1,1,0,0,0,1-.86l.77-5.43a15.36,15.36,0,0,0,3.46-2l5.11,2.06a1,1,0,0,0,1.25-.44l4.1-7.11a1,1,0,0,0-.25-1.31ZM24,31.18A7.18,7.18,0,1,1,31.17,24,7.17,7.17,0,0,1,24,31.18Z"/>
                </svg>
              `;

            settingsButton.addEventListener('click', this.openSettingsModal.bind(this));

            const avatarButton = headerEnd.querySelector('ytd-topbar-menu-button-renderer');
            if (avatarButton) {
              headerEnd.insertBefore(settingsButton, avatarButton);
            } else {
              headerEnd.appendChild(settingsButton);
            }
          }
        })
        .catch(() => {});
    },

    /**
     * Handle modal click actions (extracted to reduce complexity)
     * @param {HTMLElement} target - Click target
     * @param {HTMLElement} modal - Modal element
     * @param {Object} handlers - Modal handlers
     * @param {Function} markDirty - Mark dirty function
     * @param {Object} context - Context object
     * @param {Function} translate - Translation function
     */
    handleModalClickActions(target, modal, handlers, markDirty, context, translate) {
      // Sidebar navigation
      const navItem = /** @type {HTMLElement | null} */ (
        target.classList && target.classList.contains('ytp-plus-settings-nav-item')
          ? target
          : target.closest && target.closest('.ytp-plus-settings-nav-item')
      );
      if (navItem) {
        handlers.handleSidebarNavigation(navItem, modal);
        return;
      }

      // Save button
      if (target.id === 'ytp-plus-save-settings' || target.id === 'ytp-plus-save-settings-icon') {
        this.saveSettings();
        modal.remove();
        this.showNotification(translate('settingsSaved'));
        return;
      }

      // External downloader save
      if (target.id === 'download-externalDownloader-save') {
        handlers.handleExternalDownloaderSave(
          target,
          this.settings,
          this.saveSettings.bind(this),
          this.showNotification.bind(this),
          translate
        );
        return;
      }

      // External downloader reset
      if (target.id === 'download-externalDownloader-reset') {
        handlers.handleExternalDownloaderReset(
          modal,
          this.settings,
          this.saveSettings.bind(this),
          this.showNotification.bind(this),
          translate
        );
      }
    },

    createSettingsModal() {
      const modal = document.createElement('div');
      modal.className = 'ytp-plus-settings-modal';

      // Use helper functions from settings-helpers.js
      const helpers = window.YouTubePlusSettingsHelpers;
      const handlers = window.YouTubePlusModalHandlers;
      modal.innerHTML = `<div class="ytp-plus-settings-panel">${helpers.createSettingsSidebar(t)}${helpers.createMainContent(this.settings, t)}</div>`;

      // Track unsaved changes
      let dirty = false;
      const saveIconBtn = modal.querySelector('#ytp-plus-save-settings-icon');
      if (saveIconBtn) saveIconBtn.style.display = 'none';
      const markDirty = () => {
        if (dirty) return;
        dirty = true;
        if (saveIconBtn) saveIconBtn.style.display = '';
      };

      // Context for handlers
      const context = {
        settings: this.settings,
        getElement: this.getElement.bind(this),
        addDownloadButton: this.addDownloadButton.bind(this),
        addSpeedControlButton: this.addSpeedControlButton.bind(this),
        refreshDownloadButton: this.refreshDownloadButton.bind(this),
        updatePageBasedOnSettings: this.updatePageBasedOnSettings.bind(this),
      };

      // Create click handler
      const handleModalClick = e => {
        const { target } = /** @type {{ target: HTMLElement }} */ (e);

        // Submenu toggle buttons (e.g., YouTube Music)
        const submenuToggleBtn = target.closest('.ytp-plus-submenu-toggle');
        if (submenuToggleBtn) {
          try {
            if (
              submenuToggleBtn instanceof HTMLElement &&
              submenuToggleBtn.tagName === 'BUTTON' &&
              submenuToggleBtn.hasAttribute('disabled')
            ) {
              return;
            }
            const submenuKey = submenuToggleBtn.dataset?.submenu;
            if (!submenuKey) return;
            const panel = submenuToggleBtn.closest('.ytp-plus-settings-panel');
            if (!panel) return;
            const submenuSelector =
              submenuKey === 'music'
                ? `.music-submenu[data-submenu="${submenuKey}"]`
                : submenuKey === 'download'
                  ? `.download-submenu[data-submenu="${submenuKey}"]`
                  : submenuKey === 'style'
                    ? `.style-submenu[data-submenu="${submenuKey}"]`
                    : submenuKey === 'speed'
                      ? `.speed-submenu[data-submenu="${submenuKey}"]`
                      : submenuKey === 'loop'
                        ? `.loop-submenu[data-submenu="${submenuKey}"]`
                        : submenuKey === 'pip'
                          ? `.pip-submenu[data-submenu="${submenuKey}"]`
                          : submenuKey === 'timecode'
                            ? `.timecode-submenu[data-submenu="${submenuKey}"]`
                            : submenuKey === 'enhanced'
                              ? `.enhanced-submenu[data-submenu="${submenuKey}"]`
                              : `[data-submenu="${submenuKey}"]`;
            const submenuEl = panel.querySelector(submenuSelector);
            if (!(submenuEl instanceof HTMLElement)) return;

            const computedDisplay = window.getComputedStyle(submenuEl).display;
            const currentlyHidden = computedDisplay === 'none' || submenuEl.hidden;
            const nextHidden = !currentlyHidden;
            submenuEl.style.display = nextHidden ? 'none' : '';
            submenuToggleBtn.setAttribute('aria-expanded', nextHidden ? 'false' : 'true');

            // Persist submenu expanded state to localStorage
            try {
              const submenuStates = JSON.parse(
                localStorage.getItem('ytp-plus-submenu-states') || '{}'
              );
              submenuStates[submenuKey] = !nextHidden;
              localStorage.setItem('ytp-plus-submenu-states', JSON.stringify(submenuStates));
            } catch {
              // Ignore storage errors
            }
          } catch {}
          return;
        }

        // Close modal
        if (target === modal) {
          modal.remove();
          return;
        }

        // Close button
        if (
          target.id === 'ytp-plus-close-settings' ||
          target.id === 'ytp-plus-close-settings-icon' ||
          target.classList.contains('ytp-plus-settings-close') ||
          target.closest('.ytp-plus-settings-close') ||
          target.closest('#ytp-plus-close-settings') ||
          target.closest('#ytp-plus-close-settings-icon')
        ) {
          modal.remove();
          return;
        }

        // YTDL GitHub button
        if (target.id === 'open-ytdl-github' || target.closest('#open-ytdl-github')) {
          window.open('https://github.com/diorhc/YTDL', '_blank');
          return;
        }

        // Handle different actions
        this.handleModalClickActions(target, modal, handlers, markDirty, context, t);
      };

      modal.addEventListener('click', handleModalClick);

      // Change event delegation for checkboxes
      modal.addEventListener('change', e => {
        const { target } = /** @type {{ target: EventTarget & HTMLElement }} */ (e);
        if (!target.classList.contains('ytp-plus-settings-checkbox')) return;

        const { dataset } = /** @type {HTMLElement} */ (target);
        const { setting } = dataset;
        if (!setting) return;

        // Download site checkboxes
        if (setting.startsWith('downloadSite_')) {
          const key = setting.replace('downloadSite_', '');
          handlers.handleDownloadSiteToggle(
            target,
            key,
            this.settings,
            markDirty,
            this.saveSettings.bind(this)
          );
          return;
        }

        // YouTube Music settings - handle separately
        if (handlers.isMusicSetting && handlers.isMusicSetting(setting)) {
          handlers.handleMusicSettingToggle(target, setting, this.showNotification.bind(this), t);
          return;
        }

        // Simple settings
        handlers.handleSimpleSettingToggle(
          target,
          setting,
          this.settings,
          context,
          markDirty,
          this.saveSettings.bind(this),
          modal
        );
      });

      // Input event delegation - allow free editing
      modal.addEventListener('input', e => {
        const { target } = /** @type {{ target: EventTarget & HTMLElement }} */ (e);
        if (target.classList.contains('speed-hotkey-input')) {
          const keyType = target.dataset?.speedHotkey;
          if (keyType !== 'decrease' && keyType !== 'increase' && keyType !== 'reset') return;
          // Allow free editing on input, normalize on blur
          markDirty();
          return;
        }

        if (target.classList.contains('loop-hotkey-input')) {
          const keyType = target.dataset?.loopHotkey;
          if (keyType !== 'setPointA' && keyType !== 'setPointB' && keyType !== 'resetPoints') {
            return;
          }
          // Allow free editing on input, normalize on blur
          markDirty();
          return;
        }

        if (target.classList.contains('download-site-input')) {
          const { dataset } = /** @type {HTMLElement} */ (target);
          const { site, field } = dataset;
          if (!site || !field) return;
          handlers.handleDownloadSiteInput(target, site, field, this.settings, markDirty, t);
        }
      });

      // Blur event delegation - normalize hotkey inputs when editing ends
      modal.addEventListener(
        'blur',
        e => {
          const { target } = /** @type {{ target: EventTarget & HTMLElement }} */ (e);
          if (target.classList.contains('speed-hotkey-input')) {
            const keyType = target.dataset?.speedHotkey;
            if (keyType !== 'decrease' && keyType !== 'increase' && keyType !== 'reset') return;

            const input = /** @type {HTMLInputElement} */ (target);
            const fallback = keyType === 'decrease' ? 'g' : keyType === 'increase' ? 'h' : 'b';
            const normalized = this.normalizeSpeedHotkey(input.value, fallback);

            this.settings.speedControlHotkeys = this.settings.speedControlHotkeys || {
              decrease: 'g',
              increase: 'h',
              reset: 'b',
            };
            this.settings.speedControlHotkeys[keyType] = normalized;
            input.value = normalized;
            this.saveSettings();
            return;
          }

          if (target.classList.contains('loop-hotkey-input')) {
            const keyType = target.dataset?.loopHotkey;
            if (keyType !== 'setPointA' && keyType !== 'setPointB' && keyType !== 'resetPoints') {
              return;
            }

            const input = /** @type {HTMLInputElement} */ (target);
            const fallback = keyType === 'setPointA' ? 'k' : keyType === 'setPointB' ? 'l' : 'o';
            const normalized = this.normalizeSpeedHotkey(input.value, fallback);

            this.settings.loopHotkeys = this.settings.loopHotkeys || {
              toggleLoop: 'r',
              setPointA: 'k',
              setPointB: 'l',
              resetPoints: 'o',
            };
            this.settings.loopHotkeys[keyType] = normalized;
            input.value = normalized;
            this.saveSettings();
            return;
          }
        },
        true
      );

      // Allow report module to populate settings
      try {
        if (
          typeof window !== 'undefined' &&
          /** @type {any} */ (window).youtubePlusReport &&
          typeof (/** @type {any} */ (window).youtubePlusReport.render) === 'function'
        ) {
          try {
            /** @type {any} */ (window).youtubePlusReport.render(modal);
          } catch (e) {
            YouTubeUtils.logError('Report', 'report.render failed', e);
          }
        }
      } catch (e) {
        YouTubeUtils.logError('Report', 'Failed to initialize report section', e);
      }

      // Restore submenu expanded states from localStorage
      try {
        const submenuStates = JSON.parse(localStorage.getItem('ytp-plus-submenu-states') || '{}');
        Object.entries(submenuStates).forEach(([key, expanded]) => {
          const toggleBtn = modal.querySelector(`.ytp-plus-submenu-toggle[data-submenu="${key}"]`);
          if (toggleBtn instanceof HTMLElement && !toggleBtn.hasAttribute('disabled')) {
            const submenuSelector =
              key === 'music'
                ? `.music-submenu[data-submenu="${key}"]`
                : key === 'download'
                  ? `.download-submenu[data-submenu="${key}"]`
                  : key === 'style'
                    ? `.style-submenu[data-submenu="${key}"]`
                    : key === 'speed'
                      ? `.speed-submenu[data-submenu="${key}"]`
                      : key === 'pip'
                        ? `.pip-submenu[data-submenu="${key}"]`
                        : key === 'timecode'
                          ? `.timecode-submenu[data-submenu="${key}"]`
                          : key === 'enhanced'
                            ? `.enhanced-submenu[data-submenu="${key}"]`
                            : `[data-submenu="${key}"]`;
            const submenuEl = modal.querySelector(submenuSelector);
            if (submenuEl instanceof HTMLElement) {
              const isExpanded = !!expanded;
              submenuEl.style.display = isExpanded ? '' : 'none';
              toggleBtn.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
            }
          }
        });
      } catch {
        // Ignore storage errors
      }

      // Restore active nav section from localStorage
      try {
        const savedSection = localStorage.getItem('ytp-plus-active-nav-section');
        if (savedSection) {
          const navItem = modal.querySelector(
            `.ytp-plus-settings-nav-item[data-section="${savedSection}"]`
          );
          if (navItem) {
            modal
              .querySelectorAll('.ytp-plus-settings-nav-item')
              .forEach(item => item.classList.remove('active'));
            modal
              .querySelectorAll('.ytp-plus-settings-section')
              .forEach(s => s.classList.add('hidden'));
            navItem.classList.add('active');
            const targetSection = modal.querySelector(
              `.ytp-plus-settings-section[data-section="${savedSection}"]`
            );
            if (targetSection) targetSection.classList.remove('hidden');
          }
        }
      } catch {
        // Ignore storage errors
      }

      return modal;
    },

    openSettingsModal() {
      const existingModal = this.getElement('.ytp-plus-settings-modal', false);
      if (existingModal) existingModal.remove();
      if (typeof this.ensureNonCriticalStyles === 'function') {
        this.ensureNonCriticalStyles();
      }
      document.body.appendChild(this.createSettingsModal());
      // Initialize voting system
      if (window.YouTubePlus?.Voting) {
        const votingContainer = document.getElementById('ytp-plus-voting-container');
        if (votingContainer) {
          window.YouTubePlus.Voting.init();
          window.YouTubePlus.Voting.createUI(votingContainer);
          window.YouTubePlus.Voting.loadFeatures();
        }
        // If voting section is already visible (saved as last active), init slider
        const votingSection = document.querySelector(
          '.ytp-plus-settings-section[data-section="voting"]'
        );
        if (votingSection && !votingSection.classList.contains('hidden')) {
          requestAnimationFrame(() => window.YouTubePlus.Voting?.initSlider?.());
        }
      }
      // Notify modules that settings modal is now in DOM
      try {
        document.dispatchEvent(
          new CustomEvent('youtube-plus-settings-modal-opened', { bubbles: true })
        );
      } catch {
        // ignore event dispatch errors
      }
    },

    waitForElement(selector, timeout = 5000) {
      return YouTubeUtils.waitForElement(selector, timeout);
    },

    addCustomButtons() {
      const controls = this.getElement('.ytp-right-controls');
      if (!controls) return;

      if (!this.getElement('.ytp-screenshot-button')) this.addScreenshotButton(controls);
      if (!this.getElement('.ytp-download-button')) this.addDownloadButton(controls);
      if (!this.getElement('.speed-control-btn')) this.addSpeedControlButton(controls);

      if (!document.getElementById('speed-indicator')) {
        const indicator = document.createElement('div');
        indicator.id = 'speed-indicator';
        const player = document.getElementById('movie_player');
        if (player) player.appendChild(indicator);
      }

      this.handleFullscreenChange();
    },

    addScreenshotButton(controls) {
      const button = document.createElement('button');
      button.className = 'ytp-button ytp-screenshot-button';
      button.setAttribute('title', t('takeScreenshot'));
      button.innerHTML = `
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" style="display:block;margin:auto;vertical-align:middle;">
            <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
            <circle cx="12" cy="13" r="4"></circle>
          </svg>
        `;
      button.addEventListener('click', this.captureFrame.bind(this));
      controls.insertBefore(button, controls.firstChild);
    },

    /**
     * Add download button to controls - Delegates to download-button module
     * @param {HTMLElement} controls - Controls container
     */
    addDownloadButton(controls) {
      // Use extracted download button module
      if (typeof window !== 'undefined' && window.YouTubePlusDownloadButton) {
        const manager = window.YouTubePlusDownloadButton.createDownloadButtonManager({
          settings: this.settings,
          t,
          getElement: this.getElement.bind(this),
          YouTubeUtils,
        });
        manager.addDownloadButton(controls);
      } else {
        console.warn('[YouTube+] Download button module not loaded');
      }
    },

    addSpeedControlButton(controls) {
      // Check if speed control is enabled in settings
      if (!this.settings.enableSpeedControl) return;

      const speedBtn = document.createElement('button');
      speedBtn.type = 'button';
      speedBtn.className = 'ytp-button speed-control-btn';
      speedBtn.setAttribute('aria-label', t('speedControl'));
      speedBtn.setAttribute('aria-haspopup', 'true');
      speedBtn.setAttribute('aria-expanded', 'false');
      speedBtn.innerHTML = `<span>${this.speedControl.currentSpeed}×</span>`;

      const speedOptions = document.createElement('div');
      speedOptions.className = 'speed-options';
      speedOptions.setAttribute('role', 'menu');

      const selectSpeed = speed => {
        this.changeSpeed(speed);
        hideDropdown();
      };

      this.speedControl.availableSpeeds.forEach(speed => {
        const option = document.createElement('div');
        option.className = `speed-option-item${Number(speed) === this.speedControl.currentSpeed ? ' speed-option-active' : ''}`;
        option.textContent = `${speed}x`;
        option.dataset.speed = String(speed);
        option.setAttribute('role', 'menuitem');
        option.tabIndex = 0;
        option.addEventListener('click', () => selectSpeed(speed));
        option.addEventListener('keydown', event => {
          if (event.key === 'Enter' || event.key === ' ') {
            event.preventDefault();
            selectSpeed(speed);
          }
        });
        speedOptions.appendChild(option);
      });

      speedBtn.appendChild(speedOptions);

      // Ensure only one speed dropdown exists
      const existingSpeed = document.querySelector('.speed-options');
      if (existingSpeed) existingSpeed.remove();

      // Append speedOptions to body to avoid Firefox positioning/hover issues
      try {
        document.body.appendChild(speedOptions);
      } catch {
        // fallback keep as child
      }

      const positionDropdown = () => {
        const rect = speedBtn.getBoundingClientRect();
        speedOptions.style.left = `${rect.left + rect.width / 2}px`;
        speedOptions.style.bottom = `${window.innerHeight - rect.top + 8}px`;
      };

      const hideDropdown = () => {
        speedOptions.classList.remove('visible');
        speedBtn.setAttribute('aria-expanded', 'false');
      };

      const showDropdown = () => {
        positionDropdown();
        speedOptions.classList.add('visible');
        speedBtn.setAttribute('aria-expanded', 'true');
      };

      const toggleDropdown = () => {
        if (speedOptions.classList.contains('visible')) {
          hideDropdown();
        } else {
          showDropdown();
        }
      };

      let documentClickKey;

      const documentClickHandler = event => {
        if (!speedBtn.isConnected) {
          if (documentClickKey) {
            YouTubeUtils.cleanupManager.unregisterListener(documentClickKey);
            documentClickKey = undefined;
          }
          return;
        }
        if (!speedOptions.classList.contains('visible')) return;
        if (
          speedBtn.contains(/** @type {Node} */ (event.target)) ||
          speedOptions.contains(/** @type {Node} */ (event.target))
        ) {
          return;
        }
        hideDropdown();
      };

      const documentKeydownHandler = event => {
        if (event.key === 'Escape' && speedOptions.classList.contains('visible')) {
          hideDropdown();
          speedBtn.focus();
        }
      };

      documentClickKey = YouTubeUtils.cleanupManager.registerListener(
        document,
        'click',
        documentClickHandler,
        true
      );
      YouTubeUtils.cleanupManager.registerListener(
        document,
        'keydown',
        documentKeydownHandler,
        true
      );

      YouTubeUtils.cleanupManager.registerListener(window, 'resize', () => {
        if (speedOptions.classList.contains('visible')) {
          positionDropdown();
        }
      });

      YouTubeUtils.cleanupManager.registerListener(
        window,
        'scroll',
        () => {
          if (speedOptions.classList.contains('visible')) {
            positionDropdown();
          }
        },
        true
      );

      // Hover behaviour: show on mouseenter, hide on mouseleave (with small delay)
      let speedHideTimer;
      speedBtn.addEventListener('mouseenter', () => {
        clearTimeout(speedHideTimer);
        showDropdown();
      });
      speedBtn.addEventListener('mouseleave', () => {
        clearTimeout(speedHideTimer);
        speedHideTimer = setTimeout(hideDropdown, 200);
      });
      speedOptions.addEventListener('mouseenter', () => {
        clearTimeout(speedHideTimer);
        showDropdown();
      });
      speedOptions.addEventListener('mouseleave', () => {
        clearTimeout(speedHideTimer);
        speedHideTimer = setTimeout(hideDropdown, 200);
      });

      // Keep keyboard support (Enter toggles dropdown)
      speedBtn.addEventListener('keydown', event => {
        if (event.key === 'Enter' || event.key === ' ') {
          event.preventDefault();
          toggleDropdown();
        } else if (event.key === 'Escape') {
          hideDropdown();
        }
      });

      controls.insertBefore(speedBtn, controls.firstChild);
    },

    // ------------------ Side Guide Toggle ------------------
    applyGuideVisibility() {
      try {
        const enabled = Boolean(YouTubeUtils.storage.get('ytplus.hideGuide', false));
        document.documentElement.classList.toggle('ytp-hide-guide', enabled);
        // update floating button appearance if present
        const btn = document.getElementById('ytplus-guide-toggle-btn');
        if (btn) {
          btn.setAttribute('aria-pressed', String(enabled));
          btn.title = enabled ? 'Show side guide' : 'Hide side guide';
        }
      } catch (e) {
        console.warn('[YouTube+] applyGuideVisibility failed:', e);
      }
    },

    toggleSideGuide() {
      try {
        const current = Boolean(YouTubeUtils.storage.get('ytplus.hideGuide', false));
        const next = !current;
        YouTubeUtils.storage.set('ytplus.hideGuide', next);
        this.applyGuideVisibility();
      } catch (e) {
        console.warn('[YouTube+] toggleSideGuide failed:', e);
      }
    },

    createGuideToggleButton() {
      try {
        if (document.getElementById('ytplus-guide-toggle-btn')) return;
        const btn = document.createElement('button');
        btn.id = 'ytplus-guide-toggle-btn';
        btn.type = 'button';
        btn.style.cssText =
          'position:fixed;right:12px;bottom:12px;z-index:100000;background:var(--yt-spec-call-to-action);color:#fff;border:none;border-radius:8px;padding:8px 10px;box-shadow:0 6px 18px rgba(0,0,0,0.3);cursor:pointer;opacity:0.95;font-size:13px;';
        btn.setAttribute('aria-pressed', 'false');
        btn.title = 'Hide side guide';
        btn.textContent = 'Toggle Guide';
        btn.addEventListener('click', e => {
          e.preventDefault();
          e.stopPropagation();
          this.toggleSideGuide();
        });

        // keyboard support
        btn.addEventListener('keydown', e => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            this.toggleSideGuide();
          }
        });

        document.body.appendChild(btn);
        // Apply current stored value
        this.applyGuideVisibility();
      } catch (e) {
        console.warn('[YouTube+] createGuideToggleButton failed:', e);
      }
    },

    captureFrame() {
      const video = this.getElement('video', false);
      if (!video) return;

      const canvas = document.createElement('canvas');
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

      const videoTitle = document.title.replace(/\s-\sYouTube$/, '').trim();
      const link = document.createElement('a');
      link.href = canvas.toDataURL('image/png');
      link.download = `${videoTitle}.png`;
      try {
        link.click();

        // Notify success (use translation if available)
        try {
          const translated = typeof t === 'function' ? t('screenshotSaved') : null;
          const message =
            translated && translated !== 'screenshotSaved' ? translated : 'Screenshot saved';
          this.showNotification(message, 2000);
        } catch {
          this.showNotification('Screenshot saved', 2000);
        }
      } catch (err) {
        if (YouTubeUtils && YouTubeUtils.logError) {
          YouTubeUtils.logError('Basic', 'Screenshot download failed', err);
        }
        try {
          const translatedFail = typeof t === 'function' ? t('screenshotFailed') : null;
          const failMsg =
            translatedFail && translatedFail !== 'screenshotFailed'
              ? translatedFail
              : 'Screenshot failed';
          this.showNotification(failMsg, 3000);
        } catch {
          this.showNotification('Screenshot failed', 3000);
        }
      }
    },

    showNotification(message, duration = 2000) {
      YouTubeUtils.NotificationManager.show(message, { duration, type: 'info' });
    },

    handleFullscreenChange() {
      const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement;
      document.querySelectorAll('.ytp-screenshot-button, .ytp-cobalt-button').forEach(button => {
        button.style.bottom = isFullscreen ? '0px' : '0px';
      });
    },

    changeSpeed(speed) {
      const numericSpeed = Number(speed);
      this.speedControl.currentSpeed = numericSpeed;
      localStorage.setItem(this.speedControl.storageKey, String(numericSpeed));

      const speedBtn = this.getElement('.speed-control-btn span', false);
      if (speedBtn) speedBtn.textContent = `${numericSpeed}×`;

      document.querySelectorAll('.speed-option-item').forEach(option => {
        option.classList.toggle(
          'speed-option-active',
          parseFloat(option.dataset.speed) === numericSpeed
        );
      });

      this.applyCurrentSpeed();
      this.showSpeedIndicator(numericSpeed);
    },

    applyCurrentSpeed() {
      // Use DOM cache when available to avoid redundant live queries.
      const videos =
        window.YouTubeDOMCache && typeof window.YouTubeDOMCache.getAll === 'function'
          ? window.YouTubeDOMCache.getAll('video')
          : document.querySelectorAll('video');
      videos.forEach(video => {
        if (video && video.playbackRate !== this.speedControl.currentSpeed) {
          video.playbackRate = this.speedControl.currentSpeed;
        }
      });
    },

    setupVideoObserver() {
      if (this._speedInterval) clearInterval(this._speedInterval);
      this._speedInterval = null;

      // Track left-mouse-button hold state so we can detect YouTube's native
      // hold-to-2× speed feature. When the user presses and holds the left
      // button on the player, YouTube temporarily sets playbackRate = 2. We
      // must NOT override that, or the feature is immediately cancelled.
      if (!this._mouseHoldTracked) {
        this._mouseHoldTracked = true;
        this._mouseButtonHeld = false;
        document.addEventListener(
          'mousedown',
          e => {
            if (e.button === 0) this._mouseButtonHeld = true;
          },
          { passive: true, capture: true }
        );
        document.addEventListener(
          'mouseup',
          e => {
            if (e.button === 0) this._mouseButtonHeld = false;
          },
          { passive: true, capture: true }
        );
      }

      // Event-driven speed control instead of polling every 1s
      const applySpeed = () => this.applyCurrentSpeed();
      const updateLoopBar = () => this.updateLoopProgressBar();
      const applyLoop = () => this.applyLoopStateToCurrentVideo();
      const attachSpeedListeners = video => {
        if (video._ytpSpeedListenerAttached) return;
        video._ytpSpeedListenerAttached = true;
        video.addEventListener('loadedmetadata', applySpeed);
        video.addEventListener('loadedmetadata', updateLoopBar);
        video.addEventListener('loadedmetadata', applyLoop);
        video.addEventListener('playing', applySpeed);
        video.addEventListener('ratechange', () => {
          // YouTube's hold-to-2× temporarily raises playbackRate above the
          // user-chosen speed while the left mouse button is held. Skip the
          // reset so YouTube's native feature isn't cancelled.
          if (this._mouseButtonHeld && video.playbackRate > this.speedControl.currentSpeed) return;
          if (video.playbackRate !== this.speedControl.currentSpeed) {
            video.playbackRate = this.speedControl.currentSpeed;
          }
        });
        applySpeed();
      };

      // Attach to existing videos
      document.querySelectorAll('video').forEach(attachSpeedListeners);

      // Watch for new video elements
      const videoObserver = new MutationObserver(mutations => {
        for (const m of mutations) {
          for (const node of m.addedNodes) {
            if (node.nodeName === 'VIDEO') attachSpeedListeners(node);
            if (node instanceof Element) {
              node.querySelectorAll?.('video').forEach(attachSpeedListeners);
            }
          }
        }
      });
      const playerRoot =
        document.querySelector('#movie_player') ||
        document.querySelector('ytd-player') ||
        document.body;
      if (playerRoot) {
        videoObserver.observe(playerRoot, { childList: true, subtree: true });
      }
      YouTubeUtils.cleanupManager.registerObserver(videoObserver);
    },

    setupNavigationObserver() {
      let lastUrl = location.href;

      document.addEventListener('fullscreenchange', this.handleFullscreenChange.bind(this));

      document.addEventListener('yt-navigate-finish', () => {
        if (location.href.includes('watch?v=')) this.setupCurrentPage();
        this.addSettingsButtonToHeader();
      });

      // Use popstate + pushState/replaceState override for SPA navigation fallback
      // instead of expensive body subtree MutationObserver
      const checkUrlChange = () => {
        if (lastUrl !== location.href) {
          lastUrl = location.href;
          if (location.href.includes('watch?v=')) {
            setTimeout(() => this.setupCurrentPage(), 500);
          }
          this.addSettingsButtonToHeader();
        }
      };

      window.addEventListener('popstate', checkUrlChange);
      document.addEventListener('yt-navigate-start', checkUrlChange);
    },

    showSpeedIndicator(speed) {
      const indicator = document.getElementById('speed-indicator');
      if (!indicator) return;

      if (this.speedControl.activeAnimationId) {
        cancelAnimationFrame(this.speedControl.activeAnimationId);
        YouTubeUtils.cleanupManager.unregisterAnimationFrame(this.speedControl.activeAnimationId);
        this.speedControl.activeAnimationId = null;
      }

      indicator.textContent = `${speed}×`;
      indicator.style.display = 'block';
      indicator.style.opacity = '0.8';

      const startTime = performance.now();
      const fadeOut = timestamp => {
        const elapsed = timestamp - startTime;
        const progress = Math.min(elapsed / 1500, 1);

        indicator.style.opacity = String(0.8 * (1 - progress));

        if (progress < 1) {
          this.speedControl.activeAnimationId = YouTubeUtils.cleanupManager.registerAnimationFrame(
            requestAnimationFrame(fadeOut)
          );
        } else {
          indicator.style.display = 'none';
          this.speedControl.activeAnimationId = null;
        }
      };

      this.speedControl.activeAnimationId = YouTubeUtils.cleanupManager.registerAnimationFrame(
        requestAnimationFrame(fadeOut)
      );
    },
  };

  // Save reference to init function BEFORE IIFE closes (critical for DOMContentLoaded)
  const initFunction = YouTubeEnhancer.init.bind(YouTubeEnhancer);

  // Initialize immediately or on DOMContentLoaded
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initFunction);
  } else {
    initFunction();
  }
})();

// --- MODULE: error-boundary.js ---

// Global error boundary for YouTube+ userscript
(function () {
  'use strict';

  /**
   * Circuit breaker states
   * @enum {string}
   */
  const CircuitState = {
    CLOSED: 'closed', // Normal operation
    OPEN: 'open', // Too many failures, block operations
    HALF_OPEN: 'half_open', // Testing if system recovered
  };

  /**
   * Error boundary configuration object with circuit breaker support
   * @typedef {Object} ErrorBoundaryConfig
   * @property {number} maxErrors - Maximum number of errors allowed within the error window
   * @property {number} errorWindow - Time window in milliseconds for tracking errors (default: 60000ms = 1 minute)
   * @property {boolean} enableLogging - Whether to log errors to console
   * @property {boolean} enableRecovery - Whether to attempt automatic recovery from errors
   * @property {string} storageKey - LocalStorage key for persisting error data
   * @property {Object} circuitBreaker - Circuit breaker configuration
   */
  const ErrorBoundaryConfig = {
    maxErrors: 10,
    errorWindow: 60000, // 1 minute
    enableLogging: true,
    enableRecovery: true,
    storageKey: 'youtube_plus_errors',
    // Circuit breaker to prevent cascading failures
    circuitBreaker: {
      enabled: true,
      failureThreshold: 5, // Number of failures before opening circuit
      resetTimeout: 30000, // Time before attempting to close circuit (30s)
      halfOpenAttempts: 3, // Successful attempts needed to close circuit
    },
  };

  /**
   * Error tracking state with circuit breaker
   */
  const errorState = {
    errors: [],
    errorCount: 0,
    lastErrorTime: 0,
    isRecovering: false,
    // Circuit breaker state
    circuitState: CircuitState.CLOSED,
    circuitFailureCount: 0,
    circuitLastFailureTime: 0,
    circuitSuccessCount: 0,
  };

  /**
   * Error severity levels enumeration
   * @enum {string}
   */
  const ErrorSeverity = {
    LOW: 'low',
    MEDIUM: 'medium',
    HIGH: 'high',
    CRITICAL: 'critical',
  };

  /**
   * Categorize error severity based on error message patterns
   * @param {Error} error - The error object to categorize
   * @returns {string} Severity level from ErrorSeverity enum
   */
  const categorizeSeverity = error => {
    const message = error.message?.toLowerCase() || '';

    if (
      message.includes('cannot read') ||
      message.includes('undefined') ||
      message.includes('null')
    ) {
      return ErrorSeverity.MEDIUM;
    }

    if (message.includes('network') || message.includes('fetch') || message.includes('timeout')) {
      return ErrorSeverity.LOW;
    }

    if (message.includes('syntax') || message.includes('reference') || message.includes('type')) {
      return ErrorSeverity.HIGH;
    }

    if (message.includes('security') || message.includes('csp')) {
      return ErrorSeverity.CRITICAL;
    }

    return ErrorSeverity.MEDIUM;
  };

  /**
   * Check circuit breaker state and update accordingly
   * @param {boolean} success - Whether the operation was successful
   * @returns {boolean} Whether the operation should proceed
   */
  const checkCircuitBreaker = success => {
    if (!ErrorBoundaryConfig.circuitBreaker.enabled) return true;

    const now = Date.now();
    const { circuitBreaker } = ErrorBoundaryConfig;

    // Check if circuit should be reset to half-open
    if (
      errorState.circuitState === CircuitState.OPEN &&
      now - errorState.circuitLastFailureTime >= circuitBreaker.resetTimeout
    ) {
      window.YouTubeUtils &&
        YouTubeUtils.logger &&
        YouTubeUtils.logger.debug &&
        YouTubeUtils.logger.debug('[YouTube+] Circuit breaker transitioning to HALF_OPEN');
      errorState.circuitState = CircuitState.HALF_OPEN;
      errorState.circuitSuccessCount = 0;
    }

    // Handle successful operation
    if (success) {
      if (errorState.circuitState === CircuitState.HALF_OPEN) {
        errorState.circuitSuccessCount++;
        if (errorState.circuitSuccessCount >= circuitBreaker.halfOpenAttempts) {
          window.YouTubeUtils &&
            YouTubeUtils.logger &&
            YouTubeUtils.logger.debug &&
            YouTubeUtils.logger.debug('[YouTube+] Circuit breaker CLOSED - system recovered');
          errorState.circuitState = CircuitState.CLOSED;
          errorState.circuitFailureCount = 0;
          errorState.circuitSuccessCount = 0;
        }
      } else if (errorState.circuitState === CircuitState.CLOSED) {
        // Gradually decrease failure count on success
        errorState.circuitFailureCount = Math.max(0, errorState.circuitFailureCount - 1);
      }
      return true;
    }

    // Handle failed operation
    errorState.circuitFailureCount++;
    errorState.circuitLastFailureTime = now;

    if (errorState.circuitState === CircuitState.CLOSED) {
      if (errorState.circuitFailureCount >= circuitBreaker.failureThreshold) {
        console.error('[YouTube+] Circuit breaker OPEN - too many failures');
        errorState.circuitState = CircuitState.OPEN;
        return false;
      }
    } else if (errorState.circuitState === CircuitState.HALF_OPEN) {
      console.error('[YouTube+] Circuit breaker reopened - recovery failed');
      errorState.circuitState = CircuitState.OPEN;
      errorState.circuitSuccessCount = 0;
      return false;
    }

    return errorState.circuitState !== CircuitState.OPEN;
  };

  /**
   * Log error with context
   * @param {Error} error - The error object
   * @param {Object} context - Additional context information
   */
  const logError = (error, context = {}) => {
    if (!ErrorBoundaryConfig.enableLogging) return;

    // Update circuit breaker
    checkCircuitBreaker(false);

    const fallbackMessage = error.message?.trim() || '';

    // Skip if no meaningful message
    if (!fallbackMessage || fallbackMessage === '(no message)') {
      // Only log if we have stack trace or filename information
      if (!error.stack && !context.filename) {
        return;
      }
    }

    const displayMessage =
      fallbackMessage ||
      (context.filename ? `Error in ${context.filename}:${context.lineno}` : 'Unknown error');

    const errorInfo = {
      timestamp: new Date().toISOString(),
      message: displayMessage,
      stack: error.stack,
      severity: categorizeSeverity(error),
      context: {
        url: window.location.href,
        userAgent: navigator.userAgent,
        ...context,
      },
    };

    console.error('[YouTube+][Error Boundary]', `${errorInfo.message}`, errorInfo);

    // Store error for analysis
    errorState.errors.push(errorInfo);
    if (errorState.errors.length > 50) {
      errorState.errors.shift(); // Keep only last 50 errors
    }

    // Persist to localStorage for debugging
    try {
      const stored = JSON.parse(localStorage.getItem(ErrorBoundaryConfig.storageKey) || '[]');
      stored.push(errorInfo);
      if (stored.length > 20) stored.shift();
      localStorage.setItem(ErrorBoundaryConfig.storageKey, JSON.stringify(stored));
    } catch {}
  };

  /**
   * Check if error rate is too high
   * @returns {boolean} True if error rate exceeded
   */
  const isErrorRateExceeded = () => {
    const now = Date.now();
    const windowStart = now - ErrorBoundaryConfig.errorWindow;

    // Count errors in the time window
    const recentErrors = errorState.errors.filter(
      e => new Date(e.timestamp).getTime() > windowStart
    );

    return recentErrors.length >= ErrorBoundaryConfig.maxErrors;
  };

  /**
   * Get error rate per minute
   * @returns {number} Errors per minute
   */
  const getErrorRate = () => {
    const now = Date.now();
    const oneMinuteAgo = now - 60000;
    const recentErrors = errorState.errors.filter(
      e => new Date(e.timestamp).getTime() > oneMinuteAgo
    );
    return recentErrors.length;
  };

  /**
   * Check if should suppress error notification (rate limiting)
   * @param {Error} error - The error object
   * @returns {boolean} True if should suppress
   */
  const shouldSuppressNotification = error => {
    const rate = getErrorRate();

    // Suppress if more than 5 errors in the last minute
    if (rate > 5) {
      return true;
    }

    // Suppress duplicate errors within 10 seconds
    const tenSecondsAgo = Date.now() - 10000;
    const recentSimilar = errorState.errors.filter(
      e =>
        new Date(e.timestamp).getTime() > tenSecondsAgo &&
        e.message === error.message &&
        e.severity === categorizeSeverity(error)
    );

    return recentSimilar.length > 0;
  };

  /**
   * Show user-friendly error notification
   * @param {Error} error - The error object
   * @param {Object} _context - Error context (unused but kept for API consistency)
   */
  const showErrorNotification = (error, _context) => {
    try {
      const Y = window.YouTubeUtils;
      if (!Y || !Y.NotificationManager || typeof Y.NotificationManager.show !== 'function') {
        return; // Notification manager not available
      }

      const severity = categorizeSeverity(error);
      let message = 'An error occurred';
      let duration = 3000;

      switch (severity) {
        case ErrorSeverity.LOW:
          message = 'A minor issue occurred. Functionality should continue normally.';
          duration = 2000;
          break;
        case ErrorSeverity.MEDIUM:
          message = 'An error occurred. Some features may not work correctly.';
          duration = 3000;
          break;
        case ErrorSeverity.HIGH:
          message = 'A serious error occurred. Please refresh the page if issues persist.';
          duration = 5000;
          break;
        case ErrorSeverity.CRITICAL:
          message =
            'A critical error occurred. YouTube+ may not function properly. Please report this issue.';
          duration = 7000;
          break;
      }

      Y.NotificationManager.show(message, { duration, type: 'error' });
    } catch (notificationError) {
      console.error('[YouTube+] Failed to show error notification:', notificationError);
    }
  };

  /**
   * Attempt to recover from error
   * @param {Error} error - The error that occurred
   * @param {Object} context - Error context
   */
  const attemptRecovery = (error, context) => {
    if (!ErrorBoundaryConfig.enableRecovery || errorState.isRecovering) return;

    const severity = categorizeSeverity(error);

    if (severity === ErrorSeverity.CRITICAL) {
      console.error('[YouTube+] Critical error detected. Script may not function properly.');
      showErrorNotification(error, context);
      return;
    }

    errorState.isRecovering = true;

    try {
      // Show notification to user (except for low severity errors and rate-limited)
      if (severity !== ErrorSeverity.LOW && !shouldSuppressNotification(error)) {
        showErrorNotification(error, context);
      }

      // Use recovery utilities if available
      const RecoveryUtils = window.YouTubePlusErrorRecovery;

      if (RecoveryUtils && RecoveryUtils.attemptRecovery) {
        // Delegate to recovery utility module
        RecoveryUtils.attemptRecovery(error, context);
      } else {
        // Fallback to legacy recovery
        performLegacyRecovery(error, context);
      }

      setTimeout(() => {
        errorState.isRecovering = false;
      }, 5000);
    } catch (recoveryError) {
      console.error('[YouTube+] Recovery attempt failed:', recoveryError);
      errorState.isRecovering = false;
    }
  };

  /**
   * Perform legacy recovery (fallback)
   * @param {Error} error - Error object
   * @param {Object} context - Error context
   */
  const performLegacyRecovery = (error, context) => {
    // Attempt module-specific recovery
    if (context.module) {
      window.YouTubeUtils &&
        YouTubeUtils.logger &&
        YouTubeUtils.logger.debug &&
        YouTubeUtils.logger.debug(`[YouTube+] Attempting recovery for module: ${context.module}`);

      // Try to reinitialize the module if possible
      const Y = window.YouTubeUtils;
      if (Y && Y.cleanupManager) {
        // Could cleanup and reinitialize module-specific resources
        switch (context.module) {
          case 'StyleManager':
            // Clear and re-add styles if needed
            break;
          case 'NotificationManager':
            // Reset notification queue
            break;
          default:
            // Generic cleanup
            break;
        }
      }

      // Check if it's a DOM-related error and the element is missing
      if (
        error.message &&
        (error.message.includes('null') || error.message.includes('undefined')) &&
        context.element
      ) {
        window.YouTubeUtils &&
          YouTubeUtils.logger &&
          YouTubeUtils.logger.debug &&
          YouTubeUtils.logger.debug('[YouTube+] Attempting to re-query DOM element');
        // Could trigger element re-query here
      }
    }
  };

  /**
   * Global error handler
   * @param {ErrorEvent} event - The error event
   */
  const handleError = event => {
    const error = event.error || new Error(event.message);

    const message = (error.message || event.message || '').trim();

    // Suppress benign ResizeObserver errors
    if (message.includes('ResizeObserver loop')) {
      return false; // This is a harmless browser optimization, ignore it
    }

    const source = event.filename || '';
    const isCrossOriginSource =
      source && !source.startsWith(window.location.origin) && !/YouTube\+/.test(source);

    // Ignore opaque cross-origin errors we can't introspect
    if (!message && isCrossOriginSource) {
      return false;
    }

    // Skip logging if message is empty or just "(no message)" and from cross-origin
    if (!message || (message === '(no message)' && isCrossOriginSource)) {
      return false;
    }

    // Track error
    errorState.errorCount++;
    errorState.lastErrorTime = Date.now();

    // Log error
    logError(error, {
      type: 'uncaught',
      filename: event.filename,
      lineno: event.lineno,
      colno: event.colno,
    });

    // Check error rate
    if (isErrorRateExceeded()) {
      console.error(
        '[YouTube+] Error rate exceeded! Too many errors in short period. Some features may be disabled.'
      );
      return false;
    }

    // Attempt recovery
    attemptRecovery(error, { type: 'uncaught' });

    // Don't prevent default error handling
    return false;
  };

  /**
   * Unhandled promise rejection handler
   * @param {PromiseRejectionEvent} event - The rejection event
   */
  const handleUnhandledRejection = event => {
    const error = event.reason instanceof Error ? event.reason : new Error(String(event.reason));

    logError(error, {
      type: 'unhandledRejection',
      promise: event.promise,
    });

    // Check error rate
    if (isErrorRateExceeded()) {
      console.error('[YouTube+] Promise rejection rate exceeded!');
      return;
    }

    // Attempt recovery
    attemptRecovery(error, { type: 'unhandledRejection' });
  };

  /**
   * Safe function wrapper with error boundary
   * @param {Function} fn - Function to wrap
   * @param {string} context - Context identifier
   * @returns {Function} Wrapped function
   */
  const withErrorBoundary = (fn, context = 'unknown') => {
    /** @this {any} */
    return function (...args) {
      try {
        const fnAny = /** @type {any} */ (fn);
        return /** @this {any} */ fnAny.call(this, ...args);
      } catch (error) {
        logError(error, { module: context, args });
        attemptRecovery(error, { module: context });
        return null;
      }
    };
  };

  /**
   * Safe async function wrapper with error boundary
   * @param {Function} fn - Async function to wrap
   * @param {string} context - Context identifier
   * @returns {Function} Wrapped async function
   */
  const withAsyncErrorBoundary = (fn, context = 'unknown') => {
    /** @this {any} */
    return async function (...args) {
      try {
        const fnAny = /** @type {any} */ (fn);
        return /** @this {any} */ await fnAny.call(this, ...args);
      } catch (error) {
        logError(error, { module: context, args });
        attemptRecovery(error, { module: context });
        return null;
      }
    };
  };

  /**
   * Get error statistics
   * @returns {Object} Error statistics
   */
  const getErrorStats = () => {
    return {
      totalErrors: errorState.errorCount,
      recentErrors: errorState.errors.length,
      lastErrorTime: errorState.lastErrorTime,
      isRecovering: errorState.isRecovering,
      errorsByType: errorState.errors.reduce((acc, e) => {
        acc[e.severity] = (acc[e.severity] || 0) + 1;
        return acc;
      }, {}),
    };
  };

  /**
   * Clear stored errors
   */
  const clearErrors = () => {
    errorState.errors = [];
    try {
      localStorage.removeItem(ErrorBoundaryConfig.storageKey);
    } catch {}
  };

  // Install global error handlers
  if (typeof window !== 'undefined') {
    window.addEventListener('error', handleError, true);
    window.addEventListener('unhandledrejection', handleUnhandledRejection, true);

    // Expose error boundary utilities
    window.YouTubeErrorBoundary = {
      withErrorBoundary,
      withAsyncErrorBoundary,
      getErrorStats,
      clearErrors,
      logError,
      getErrorRate,
      config: ErrorBoundaryConfig,
    };

    window.YouTubeUtils &&
      YouTubeUtils.logger &&
      YouTubeUtils.logger.debug &&
      YouTubeUtils.logger.debug('[YouTube+][Error Boundary]', 'Error boundary initialized');
  }
})();

// --- MODULE: performance.js ---

// Performance monitoring for YouTube+ userscript (Enhanced)
(function () {
  'use strict';

  /* global Blob, URL, PerformanceObserver */

  /**
   * Performance monitoring configuration
   */
  const PerformanceConfig = {
    enabled: true,
    sampleRate: 0.01, // 1% sampling by default (can be overridden via YouTubePlusConfig)
    storageKey: 'youtube_plus_performance',
    metricsRetention: 100, // Keep last 100 metrics
    enableConsoleOutput: false,
    logLevel: 'info', // 'debug', 'info', 'warn', 'error'
  };

  const isTestEnv = (() => {
    try {
      // Jest provides process.env.JEST_WORKER_ID in node/jsdom
      return typeof process !== 'undefined' && !!process?.env?.JEST_WORKER_ID;
    } catch {
      return false;
    }
  })();

  const getConfiguredSampleRate = () => {
    try {
      const cfg = /** @type {any} */ (window).YouTubePlusConfig;
      const explicit =
        cfg?.performance?.sampleRate ??
        cfg?.performanceSampleRate ??
        cfg?.perfSampleRate ??
        undefined;

      if (typeof explicit === 'number' && isFinite(explicit)) {
        return Math.min(1, Math.max(0, explicit));
      }
    } catch {
      // ignore
    }
    return PerformanceConfig.sampleRate;
  };

  // Apply sample rate (always 100% in tests to avoid flakiness)
  PerformanceConfig.sampleRate = isTestEnv ? 1.0 : getConfiguredSampleRate();

  // Sampling gate: keep API available but disable heavy observers/recording when not sampled.
  try {
    if (
      !isTestEnv &&
      PerformanceConfig.sampleRate < 1 &&
      Math.random() > PerformanceConfig.sampleRate
    ) {
      PerformanceConfig.enabled = false;
    }
  } catch {
    // ignore
  }

  /**
   * Performance metrics storage
   */
  const metrics = {
    timings: new Map(),
    marks: new Map(),
    measures: [],
    resources: [],
    webVitals: {
      LCP: null,
      CLS: 0,
      FID: null,
      INP: null,
      FCP: null,
      TTFB: null,
    },
  };

  /**
   * Create a performance mark
   * @param {string} name - Mark name
   */
  const mark = name => {
    if (!PerformanceConfig.enabled) return;

    try {
      if (typeof performance !== 'undefined' && performance.mark) {
        performance.mark(name);
      }
      metrics.marks.set(name, Date.now());
    } catch (e) {
      console.warn('[YouTube+ Perf] Failed to create mark:', e);
    }
  };

  /**
   * Measure time between two marks
   * @param {string} name - Measure name
   * @param {string} startMark - Start mark name
   * @param {string} endMark - End mark name (optional, defaults to now)
   * @returns {number} Duration in milliseconds
   */
  const measure = (name, startMark, endMark) => {
    if (!PerformanceConfig.enabled) return 0;

    try {
      const startTime = metrics.marks.get(startMark);
      if (!startTime) {
        // console.warn(`[YouTube+ Perf] Start mark "${startMark}" not found`);
        return 0;
      }

      const endTime = endMark ? metrics.marks.get(endMark) : Date.now();
      const duration = endTime - startTime;

      const measureData = {
        name,
        startMark,
        endMark: endMark || 'now',
        duration,
        timestamp: Date.now(),
      };

      metrics.measures.push(measureData);

      // Keep only recent measures
      if (metrics.measures.length > PerformanceConfig.metricsRetention) {
        metrics.measures.shift();
      }

      if (PerformanceConfig.enableConsoleOutput) {
        window.YouTubeUtils?.logger?.debug?.(`[YouTube+ Perf] ${name}: ${duration.toFixed(2)}ms`);
      }

      // Try native performance API
      if (typeof performance !== 'undefined' && performance.measure) {
        try {
          performance.measure(name, startMark, endMark);
        } catch {}
      }

      return duration;
    } catch (e) {
      console.warn('[YouTube+ Perf] Failed to measure:', e);
      return 0;
    }
  };

  /**
   * Time a function execution
   * @param {string} name - Timer name
   * @param {Function} fn - Function to time
   * @returns {Function} Wrapped function
   */
  const timeFunction = (name, fn) => {
    if (!PerformanceConfig.enabled) return fn;

    return /** @this {any} */ function (...args) {
      const startMark = `${name}-start-${Date.now()}`;
      mark(startMark);

      try {
        const fnAny = /** @type {any} */ (fn);
        const result = fnAny.apply(this, args);

        // Handle promises
        if (result && typeof result.then === 'function') {
          return result.finally(() => {
            measure(name, startMark, undefined);
          });
        }

        measure(name, startMark, undefined);
        return result;
      } catch (error) {
        measure(name, startMark, undefined);
        throw error;
      }
    };
  };

  /**
   * Time an async function execution
   * @param {string} name - Timer name
   * @param {Function} fn - Async function to time
   * @returns {Function} Wrapped async function
   */
  const timeAsyncFunction = (name, fn) => {
    if (!PerformanceConfig.enabled) return fn;

    return /** @this {any} */ async function (...args) {
      const startMark = `${name}-start-${Date.now()}`;
      mark(startMark);

      try {
        const fnAny = /** @type {any} */ (fn);
        const result = await fnAny.apply(this, args);
        measure(name, startMark, undefined);
        return result;
      } catch (error) {
        measure(name, startMark, undefined);
        throw error;
      }
    };
  };

  /**
   * Record custom metric
   * @param {string} name - Metric name
   * @param {number} value - Metric value
   * @param {Object} metadata - Additional metadata
   */
  const recordMetric = (name, value, metadata = {}) => {
    if (!PerformanceConfig.enabled) return;

    const metric = {
      name,
      value,
      timestamp: Date.now(),
      ...metadata,
    };

    metrics.timings.set(name, metric);

    if (PerformanceConfig.enableConsoleOutput) {
      window.YouTubeUtils?.logger?.debug?.(`[YouTube+ Perf] ${name}: ${value}`, metadata);
    }
  };

  /**
   * Get performance statistics
   * @param {string} metricName - Optional metric name filter
   * @returns {Object} Performance statistics
   */
  const getStats = metricName => {
    if (metricName) {
      const filtered = metrics.measures.filter(m => m.name === metricName);
      if (filtered.length === 0) return null;

      const durations = filtered.map(m => m.duration);
      return {
        name: metricName,
        count: durations.length,
        min: Math.min(...durations),
        max: Math.max(...durations),
        avg: durations.reduce((a, b) => a + b, 0) / durations.length,
        latest: durations[durations.length - 1],
      };
    }

    // Get all stats
    const allMetrics = {};
    const metricNames = [...new Set(metrics.measures.map(m => m.name))];

    metricNames.forEach(name => {
      allMetrics[name] = getStats(name);
    });

    return {
      metrics: allMetrics,
      webVitals: { ...metrics.webVitals },
      totalMeasures: metrics.measures.length,
      totalMarks: metrics.marks.size,
      customMetrics: Object.fromEntries(metrics.timings),
    };
  };

  /**
   * Get memory usage information
   * @returns {Object|null} Memory usage data
   */
  const getMemoryUsage = () => {
    if (typeof performance === 'undefined' || !performance.memory) {
      return null;
    }

    try {
      const memory = performance.memory;
      return {
        usedJSHeapSize: memory.usedJSHeapSize,
        totalJSHeapSize: memory.totalJSHeapSize,
        jsHeapSizeLimit: memory.jsHeapSizeLimit,
        usedPercent: ((memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100).toFixed(2),
      };
    } catch {
      return null;
    }
  };

  /**
   * Track memory usage as a metric
   */
  const trackMemory = () => {
    const memory = getMemoryUsage();
    if (memory) {
      recordMetric('memory-usage', memory.usedJSHeapSize, {
        totalJSHeapSize: memory.totalJSHeapSize,
        usedPercent: memory.usedPercent,
      });
    }
  };

  /**
   * Check if metrics exceed thresholds
   * @param {Object} thresholds - Threshold configuration
   * @returns {Array} Array of threshold violations
   */
  const checkThresholds = thresholds => {
    const violations = [];
    const allStats = getStats(undefined);

    if (!allStats || !allStats.metrics) return violations;

    Object.entries(thresholds).forEach(([metricName, threshold]) => {
      const stat = allStats.metrics[metricName];
      if (stat && stat.avg > threshold) {
        violations.push({
          metric: metricName,
          threshold,
          actual: stat.avg,
          exceeded: stat.avg - threshold,
        });
      }
    });

    return violations;
  };

  /**
   * Export metrics to JSON
   * @returns {string} JSON string of metrics
   */
  const exportMetrics = () => {
    const data = {
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
      url: window.location.href,
      memory: getMemoryUsage(),
      stats: getStats(undefined),
      measures: metrics.measures,
      customMetrics: Object.fromEntries(metrics.timings),
      webVitals: metrics.webVitals,
    };

    return JSON.stringify(data, null, 2);
  };

  /**
   * Export metrics to downloadable file
   * @param {string} filename - Filename for export
   * @returns {boolean} Success status
   */
  const exportToFile = (filename = 'youtube-plus-performance.json') => {
    try {
      const data = exportMetrics();
      if (typeof Blob === 'undefined') {
        console.warn('[YouTube+ Perf] Blob API not available');
        return false;
      }
      const blob = new Blob([data], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      return true;
    } catch (e) {
      console.error('[YouTube+ Perf] Failed to export to file:', e);
      return false;
    }
  };

  /**
   * Aggregate metrics by time period
   * @param {number} periodMs - Time period in milliseconds
   * @returns {Array} Aggregated metrics
   */
  const aggregateByPeriod = (periodMs = 60000) => {
    const periods = new Map();

    metrics.measures.forEach(measure => {
      const periodStart = Math.floor(measure.timestamp / periodMs) * periodMs;
      if (!periods.has(periodStart)) {
        periods.set(periodStart, []);
      }
      periods.get(periodStart).push(measure);
    });

    const aggregated = [];
    periods.forEach((measures, periodStart) => {
      const durations = measures.map(m => m.duration);
      aggregated.push({
        period: new Date(periodStart).toISOString(),
        count: durations.length,
        min: Math.min(...durations),
        max: Math.max(...durations),
        avg: durations.reduce((a, b) => a + b, 0) / durations.length,
      });
    });

    return aggregated;
  };

  /**
   * Clear all performance metrics
   */
  const clearMetrics = () => {
    metrics.timings.clear();
    metrics.marks.clear();
    metrics.measures = [];
    metrics.resources = [];
    metrics.webVitals = {
      LCP: null,
      CLS: 0,
      FID: null,
      INP: null,
      FCP: null,
      TTFB: null,
    };

    try {
      localStorage.removeItem(PerformanceConfig.storageKey);
    } catch {}

    if (typeof performance !== 'undefined' && performance.clearMarks) {
      try {
        performance.clearMarks();
        performance.clearMeasures();
      } catch {}
    }
  };

  /**
   * Monitor DOM mutations performance
   * @param {Element} element - Element to monitor
   * @param {string} name - Monitor name
   * @returns {MutationObserver} The observer instance
   */
  const monitorMutations = (element, name) => {
    if (!PerformanceConfig.enabled) return null;

    let mutationCount = 0;
    const startTime = Date.now();

    const observer = new MutationObserver(mutations => {
      mutationCount += mutations.length;
      recordMetric(`${name}-mutations`, mutationCount, {
        elapsed: Date.now() - startTime,
      });
    });

    observer.observe(element, {
      childList: true,
      subtree: true,
      attributes: true,
    });

    return observer;
  };

  /**
   * Get browser performance entries
   * @param {string} type - Entry type filter
   * @returns {Array} Performance entries
   */
  const getPerformanceEntries = type => {
    if (typeof performance === 'undefined' || !performance.getEntriesByType) {
      return [];
    }

    try {
      return performance.getEntriesByType(type);
    } catch {
      return [];
    }
  };

  /**
   * Initialize Performance Observer for Web Vitals
   */
  const initPerformanceObserver = () => {
    if (typeof PerformanceObserver === 'undefined') return;

    try {
      // Observe LCP
      new PerformanceObserver(entryList => {
        const entries = entryList.getEntries();
        const lastEntry = entries[entries.length - 1];
        metrics.webVitals.LCP = lastEntry.startTime;
        if (PerformanceConfig.enableConsoleOutput) {
          console.warn(`[YouTube+ Perf] LCP: ${lastEntry.startTime.toFixed(2)}ms`, lastEntry);
        }
      }).observe({ type: 'largest-contentful-paint', buffered: true });

      // Observe CLS
      new PerformanceObserver(entryList => {
        for (const entry of entryList.getEntries()) {
          if (!entry.hadRecentInput) {
            metrics.webVitals.CLS += entry.value;
          }
        }
        if (PerformanceConfig.enableConsoleOutput && PerformanceConfig.logLevel === 'debug') {
          console.warn(`[YouTube+ Perf] CLS: ${metrics.webVitals.CLS.toFixed(4)}`);
        }
      }).observe({ type: 'layout-shift', buffered: true });

      // Observe FID (First Input Delay)
      new PerformanceObserver(entryList => {
        const firstInput = entryList.getEntries()[0];
        metrics.webVitals.FID = firstInput.processingStart - firstInput.startTime;
        if (PerformanceConfig.enableConsoleOutput) {
          console.warn(`[YouTube+ Perf] FID: ${metrics.webVitals.FID.toFixed(2)}ms`);
        }
      }).observe({ type: 'first-input', buffered: true });

      // Observe INP (Interaction to Next Paint) - experimental
      try {
        new PerformanceObserver(entryList => {
          const entries = entryList.getEntries();
          // Simplified INP calculation (just taking max duration for now)
          const maxDuration = Math.max(...entries.map(e => e.duration));
          metrics.webVitals.INP = maxDuration;
        }).observe({ type: 'event', buffered: true, durationThreshold: 16 });
      } catch (e) {
        void e; // INP might not be supported; reference `e` to satisfy linters
      }
    } catch (e) {
      console.warn('[YouTube+ Perf] Failed to init PerformanceObserver:', e);
    }
  };

  /**
   * Log page load performance
   */
  const logPageLoadMetrics = () => {
    if (!PerformanceConfig.enabled) return;

    try {
      const navigation = getPerformanceEntries('navigation')[0];
      if (navigation) {
        recordMetric('page-load-time', navigation.loadEventEnd - navigation.fetchStart);
        recordMetric('dom-content-loaded', navigation.domContentLoadedEventEnd);
        recordMetric('dom-interactive', navigation.domInteractive);
      }
    } catch (e) {
      console.warn('[YouTube+ Perf] Failed to log page metrics:', e);
    }
  };

  // Auto-log page load metrics
  if (typeof window !== 'undefined') {
    if (document.readyState === 'complete') {
      logPageLoadMetrics();
    } else {
      window.addEventListener('load', logPageLoadMetrics, { once: true });
    }

    // Initialize Web Vitals observers (only when enabled to reduce overhead)
    if (PerformanceConfig.enabled) {
      initPerformanceObserver();
    }

    /**
     * RAF Scheduler for batched animations
     */
    const RAFScheduler = (() => {
      let rafId = null;
      const callbacks = new Set();

      const flush = () => {
        rafId = null;
        Array.from(callbacks).forEach(cb => {
          try {
            cb();
          } catch (e) {
            console.error('[RAF] Error:', e);
          }
        });
        callbacks.clear();
      };

      return {
        schedule: callback => {
          callbacks.add(callback);
          if (!rafId) rafId = requestAnimationFrame(flush);
          return () => callbacks.delete(callback);
        },
        cancelAll: () => {
          if (rafId) cancelAnimationFrame(rafId);
          rafId = null;
          callbacks.clear();
        },
      };
    })();

    /**
     * Lazy Loader using Intersection Observer
     */
    const LazyLoader = (() => {
      const observers = new Map();

      return {
        create: (options = {}) => {
          const { root = null, rootMargin = '50px', threshold = 0.01, onIntersect } = options;

          const observer = new IntersectionObserver(
            entries => {
              entries.forEach(entry => {
                if (entry.isIntersecting) {
                  onIntersect(entry.target, entry);
                  observer.unobserve(entry.target);
                }
              });
            },
            { root, rootMargin, threshold }
          );

          observers.set(observer, new Set());

          return {
            observe: el => {
              if (el instanceof Element) {
                observer.observe(el);
                observers.get(observer).add(el);
              }
            },
            unobserve: el => {
              if (el instanceof Element) {
                observer.unobserve(el);
                observers.get(observer)?.delete(el);
              }
            },
            disconnect: () => {
              observer.disconnect();
              observers.delete(observer);
            },
          };
        },
        disconnectAll: () => {
          observers.forEach((_, o) => o.disconnect());
          observers.clear();
        },
      };
    })();

    /**
     * DOM Batcher for efficient DOM mutations
     */
    const DOMBatcher = (() => {
      const batches = new Map();

      return {
        batch: (container, elements) => {
          if (!batches.has(container)) batches.set(container, []);
          batches.get(container).push(...elements);
        },
        flush: () => {
          RAFScheduler.schedule(() => {
            batches.forEach((elements, container) => {
              if (!container.isConnected) {
                batches.delete(container);
                return;
              }
              const frag = document.createDocumentFragment();
              elements.forEach(el => frag.appendChild(el));
              container.appendChild(frag);
            });
            batches.clear();
          });
        },
        clear: container => batches.delete(container),
      };
    })();

    /**
     * Element Cache using WeakMap (auto garbage collected)
     */
    const ElementCache = (() => {
      const cache = new WeakMap();

      return {
        get: (el, key) => cache.get(el)?.[key],
        set: (el, key, val) => {
          let data = cache.get(el);
          if (!data) {
            data = {};
            cache.set(el, data);
          }
          data[key] = val;
        },
        has: (el, key) => {
          const data = cache.get(el);
          return data ? key in data : false;
        },
        delete: (el, key) => {
          const data = cache.get(el);
          if (data) delete data[key];
        },
      };
    })();

    // Expose performance monitoring API
    window.YouTubePerformance = {
      mark,
      measure,
      timeFunction,
      timeAsyncFunction,
      recordMetric,
      getStats,
      exportMetrics,
      exportToFile,
      clearMetrics,
      monitorMutations,
      getPerformanceEntries,
      getMemoryUsage,
      trackMemory,
      checkThresholds,
      aggregateByPeriod,
      config: PerformanceConfig,
      RAFScheduler,
      LazyLoader,
      DOMBatcher,
      ElementCache,
    };

    /**
     * Yield to main thread to improve INP
     * Uses scheduler.yield() if available, falls back to setTimeout
     * @returns {Promise<void>}
     */
    const yieldToMain = () => {
      return new Promise(resolve => {
        if ('scheduler' in window && typeof window.scheduler?.yield === 'function') {
          window.scheduler.yield().then(resolve);
        } else {
          setTimeout(resolve, 0);
        }
      });
    };

    /**
     * Break up long tasks into smaller chunks to improve INP
     * @param {Array<Function>} tasks - Array of task functions
     * @param {number} [yieldInterval=50] - Yield after this many ms
     * @returns {Promise<void>}
     */
    const runChunkedTasks = async (tasks, yieldInterval = 50) => {
      let lastYield = performance.now();

      for (const task of tasks) {
        task();

        const now = performance.now();
        if (now - lastYield > yieldInterval) {
          await yieldToMain();
          lastYield = performance.now();
        }
      }
    };

    /**
     * Wrap event handler to yield periodically for better INP
     * @param {Function} handler - Original event handler
     * @param {Object} [options] - Options
     * @param {number} [options.maxBlockTime=50] - Max time to block before yielding
     * @returns {Function} Wrapped handler
     */
    const wrapForINP = (handler, options = {}) => {
      const { maxBlockTime = 50 } = options;

      return async function (...args) {
        const start = performance.now();
        let result;

        try {
          result = handler.apply(this, args);

          // If handler returns a promise, wait for it
          if (result && typeof result.then === 'function') {
            result = await result;
          }
        } finally {
          const elapsed = performance.now() - start;
          if (elapsed > maxBlockTime) {
            // Record long task for debugging
            recordMetric('long-task', elapsed, { handler: handler.name || 'anonymous' });
          }
        }

        return result;
      };
    };

    // Add INP helpers to global API
    window.YouTubePerformance.yieldToMain = yieldToMain;
    window.YouTubePerformance.runChunkedTasks = runChunkedTasks;
    window.YouTubePerformance.wrapForINP = wrapForINP;

    // ─── LCP Optimization Suite ────────────────────────────────────────────────
    // Target: Main page <5s, Video page <3.5s, Playlist page <3.5s

    /**
     * 1. Resource Hints - preconnect to YouTube CDN origins
     *    Shaves 100-300ms from first resource fetch on each origin.
     */
    const injectResourceHints = () => {
      const origins = [
        'https://www.youtube.com',
        'https://i.ytimg.com', // Thumbnails (LCP candidate)
        'https://yt3.ggpht.com', // Channel avatars
        'https://fonts.googleapis.com', // Fonts
        'https://www.gstatic.com', // Static resources
        'https://play.google.com', // Play store resources
      ];

      const head = document.head;
      if (!head) return;

      const existingHrefs = new Set();
      head.querySelectorAll('link[rel="preconnect"]').forEach(el => {
        existingHrefs.add(el.href);
      });

      for (const origin of origins) {
        if (existingHrefs.has(origin) || existingHrefs.has(origin + '/')) continue;
        const link = document.createElement('link');
        link.rel = 'preconnect';
        link.href = origin;
        link.crossOrigin = 'anonymous';
        head.appendChild(link);
      }
    };

    /**
     * 2. LCP Element Priority Boost
     *    Set fetchpriority="high" on the LCP element (main video thumbnail / player poster).
     */
    const boostLCPElement = () => {
      const path = location.pathname;
      let lcpSelector;

      if (path === '/watch' || path.startsWith('/shorts/')) {
        // Video page: player poster or first video frame
        lcpSelector =
          '#movie_player .ytp-cued-thumbnail-overlay-image, #movie_player video, ytd-player #ytd-player .html5-video-container';
      } else if (path === '/playlist') {
        // Playlist page: first visible thumbnail
        lcpSelector = 'ytd-playlist-video-renderer:first-child img.yt-core-image';
      } else {
        // Main page: first visible rich item thumbnail
        lcpSelector =
          'ytd-rich-item-renderer:first-child img.yt-core-image, ytd-rich-grid-media img.yt-core-image';
      }

      if (!lcpSelector) return;

      requestAnimationFrame(() => {
        const el = document.querySelector(lcpSelector);
        if (el && el.tagName === 'IMG') {
          el.setAttribute('fetchpriority', 'high');
          el.setAttribute('loading', 'eager');
          // Remove lazy loading if set by YouTube
          if (el.loading === 'lazy') el.loading = 'eager';
        }
      });
    };

    /**
     * 3. Content-Visibility CSS for off-screen sections
     *    Dramatically reduces initial render work by skipping layout/paint for below-the-fold.
     */
    const injectContentVisibilityCSS = () => {
      const cssId = 'ytp-perf-content-visibility';
      if (document.getElementById(cssId)) return;

      const css = `
        /* ── YouTube+ LCP Performance Optimizations ── */

        /* Off-screen section rendering deferral */
        ytd-comments#comments { content-visibility: auto; contain-intrinsic-size: auto 800px; }
        #secondary ytd-compact-video-renderer:nth-child(n+6) { content-visibility: auto; contain-intrinsic-size: auto 94px; }
        ytd-watch-next-secondary-results-renderer ytd-item-section-renderer { content-visibility: auto; contain-intrinsic-size: auto 600px; }

        /* Main/browse feed - defer items below first viewport */
        ytd-rich-grid-renderer #contents > ytd-rich-item-renderer:nth-child(n+9) { content-visibility: auto; contain-intrinsic-size: auto 360px; }
        ytd-section-list-renderer > #contents > ytd-item-section-renderer:nth-child(n+3) { content-visibility: auto; contain-intrinsic-size: auto 500px; }

        /* Playlist page - defer items beyond visible viewport */
        ytd-playlist-video-list-renderer #contents > ytd-playlist-video-renderer:nth-child(n+12) { content-visibility: auto; contain-intrinsic-size: auto 90px; }

        /* Note: contain:layout is intentionally omitted here — it breaks position:sticky
           for chips-wrapper and tabs-container on browse/channel pages. */

        /* Guide sidebar - not needed for LCP */
        ytd-mini-guide-renderer { content-visibility: auto; contain-intrinsic-size: auto 100vh; }
        tp-yt-app-drawer#guide { content-visibility: auto; contain-intrinsic-size: 240px 100vh; }

        /* Below-the-fold metadata */
        ytd-watch-metadata #description { content-visibility: auto; contain-intrinsic-size: auto 120px; }
        ytd-structured-description-content-renderer { content-visibility: auto; contain-intrinsic-size: auto 200px; }

        /* Shorts shelf on browse pages */
        ytd-reel-shelf-renderer { content-visibility: auto; contain-intrinsic-size: auto 320px; }

        /* Comments container on main watch - contain:style only, not layout (preserves sticky) */
        ytd-item-section-renderer#sections { contain: style; }

        /* Reduce paint complexity for non-visible items */
        ytd-rich-grid-row:nth-child(n+4) { content-visibility: auto; contain-intrinsic-size: auto 240px; }

        /* Engagement panels - safe deferral only when fully hidden */
        ytd-engagement-panel-section-list-renderer[visibility="ENGAGEMENT_PANEL_VISIBILITY_HIDDEN"] { content-visibility: auto; contain-intrinsic-size: auto 0px; }

        /* Optimize image decoding */
        ytd-thumbnail img, yt-image img, .yt-core-image { content-visibility: auto; }
      `;

      const style = document.createElement('style');
      style.id = cssId;
      style.textContent = css;
      (document.head || document.documentElement).appendChild(style);
    };

    /**
     * 4. Deferred Image Loading
     *    Lazy-load images below the fold using IntersectionObserver.
     */
    const setupDeferredImageLoading = () => {
      const imgObserver = new IntersectionObserver(
        entries => {
          for (const entry of entries) {
            if (entry.isIntersecting) {
              const img = entry.target;
              const dataSrc = img.getAttribute('data-ytp-deferred-src');
              if (dataSrc) {
                img.src = dataSrc;
                img.removeAttribute('data-ytp-deferred-src');
              }
              imgObserver.unobserve(img);
            }
          }
        },
        { rootMargin: '200px 0px' }
      );

      // Observe below-fold thumbnail images
      const observeImages = () => {
        const belowFold = document.querySelectorAll(
          'ytd-rich-item-renderer:nth-child(n+5) img[src]:not([data-ytp-img-observed]),' +
            'ytd-compact-video-renderer:nth-child(n+4) img[src]:not([data-ytp-img-observed])'
        );
        belowFold.forEach(img => {
          img.setAttribute('data-ytp-img-observed', '1');
        });
      };

      // Run periodically to catch new items
      let imgTimer = null;
      const scheduleObserve = () => {
        if (imgTimer) return;
        imgTimer = setTimeout(() => {
          imgTimer = null;
          observeImages();
        }, 500);
      };

      window.addEventListener('yt-navigate-finish', scheduleObserve, { passive: true });
      if (document.readyState !== 'loading') {
        scheduleObserve();
      } else {
        document.addEventListener('DOMContentLoaded', scheduleObserve, { once: true });
      }
    };

    /**
     * 5. MutationObserver Optimization
     *    Provides a shared, debounced MutationObserver to reduce overhead
     *    from multiple independent subtree observers.
     */
    const SharedMutationManager = (() => {
      let observer = null;
      const callbacks = new Map(); // key -> {callback, filter}
      let scheduled = false;
      const pending = [];

      const flush = () => {
        scheduled = false;
        const entries = [...pending];
        pending.length = 0;

        for (const [, { callback, filter }] of callbacks) {
          const filtered = filter ? entries.filter(filter) : entries;
          if (filtered.length > 0) {
            try {
              callback(filtered);
            } catch (e) {
              console.warn('[YouTube+ Perf] SharedMutation callback error:', e);
            }
          }
        }
      };

      const start = () => {
        if (observer) return;
        observer = new MutationObserver(mutations => {
          pending.push(...mutations);
          if (!scheduled) {
            scheduled = true;
            // Use microtask for fast batching without losing responsiveness
            queueMicrotask(flush);
          }
        });
        const target = document.body || document.documentElement;
        if (target) {
          observer.observe(target, { childList: true, subtree: true });
        }
      };

      return {
        /**
         * Register a callback for shared mutation observation.
         * @param {string} key - Unique key
         * @param {Function} callback - Called with filtered mutations
         * @param {Function} [filter] - Optional filter for mutations
         */
        register(key, callback, filter) {
          callbacks.set(key, { callback, filter });
          if (callbacks.size === 1) start();
        },
        unregister(key) {
          callbacks.delete(key);
          if (callbacks.size === 0 && observer) {
            observer.disconnect();
            observer = null;
          }
        },
        getCallbackCount: () => callbacks.size,
      };
    })();

    /**
     * 6. Idle-time Task Scheduler
     *    Schedules non-critical initialization to idle periods.
     */
    const IdleScheduler = (() => {
      const queue = [];
      let running = false;

      const processQueue = deadline => {
        while (queue.length > 0 && (deadline ? deadline.timeRemaining() > 5 : true)) {
          const task = queue.shift();
          try {
            task.fn();
          } catch (e) {
            console.warn('[YouTube+ Perf] Idle task error:', e);
          }
          if (!deadline) break; // Without deadline, run one task per iteration
        }

        if (queue.length > 0) {
          scheduleNext();
        } else {
          running = false;
        }
      };

      const scheduleNext = () => {
        if (typeof requestIdleCallback === 'function') {
          requestIdleCallback(processQueue, { timeout: 3000 });
        } else {
          setTimeout(() => processQueue(null), 50);
        }
      };

      return {
        /**
         * Schedule a task for idle execution.
         * @param {Function} fn - Task function
         * @param {number} [priority=0] - Higher = runs first
         */
        schedule(fn, priority = 0) {
          queue.push({ fn, priority });
          queue.sort((a, b) => b.priority - a.priority);
          if (!running) {
            running = true;
            scheduleNext();
          }
        },
        /** Get number of pending tasks */
        pending: () => queue.length,
      };
    })();

    /**
     * 7. Long Task monitoring (via PerformanceObserver)
     *    Helps identify blocking scripts beyond 50ms.
     */
    const initLongTaskMonitor = () => {
      if (typeof PerformanceObserver === 'undefined') return;
      try {
        const longTasks = [];
        new PerformanceObserver(list => {
          for (const entry of list.getEntries()) {
            longTasks.push({
              duration: entry.duration,
              startTime: entry.startTime,
              name: entry.name,
            });
            if (longTasks.length > 50) longTasks.shift();
          }
          recordMetric('long-tasks-count', longTasks.length);
          const totalBlocking = longTasks.reduce((sum, t) => sum + Math.max(0, t.duration - 50), 0);
          recordMetric('total-blocking-time', totalBlocking);
        }).observe({ type: 'longtask', buffered: true });
      } catch {
        // longtask observer not supported
      }
    };

    /**
     * 8. Navigation-aware performance tracking
     *    Reset and re-measure on YouTube SPA navigations.
     */
    const initNavigationTracking = () => {
      window.addEventListener(
        'yt-navigate-start',
        () => {
          mark('yt-navigate-start');
        },
        { passive: true }
      );

      window.addEventListener(
        'yt-navigate-finish',
        () => {
          mark('yt-navigate-finish');
          measure('yt-navigation-duration', 'yt-navigate-start');

          // Re-boost LCP for new page
          requestAnimationFrame(() => {
            boostLCPElement();
          });
        },
        { passive: true }
      );
    };

    /**
     * Initialize all LCP optimizations
     */
    const initLCPOptimizations = () => {
      try {
        // Critical (run immediately - biggest LCP impact)
        injectResourceHints();
        injectContentVisibilityCSS();
        boostLCPElement();

        // High priority (run in next microtask)
        queueMicrotask(() => {
          initNavigationTracking();
          initLongTaskMonitor();
        });

        // Lower priority (defer to idle)
        IdleScheduler.schedule(() => setupDeferredImageLoading(), 2);
      } catch (e) {
        console.warn('[YouTube+ Perf] LCP optimization init error:', e);
      }
    };

    // Run LCP optimizations immediately
    initLCPOptimizations();

    // Expose new performance APIs
    window.YouTubePerformance.SharedMutationManager = SharedMutationManager;
    window.YouTubePerformance.IdleScheduler = IdleScheduler;
    window.YouTubePerformance.boostLCPElement = boostLCPElement;
    window.YouTubePerformance.injectResourceHints = injectResourceHints;

    window.YouTubeUtils &&
      YouTubeUtils.logger &&
      YouTubeUtils.logger.debug &&
      YouTubeUtils.logger.debug('[YouTube+] Performance monitoring initialized');
  }
})();

// --- MODULE: dom-cache.js ---

// DOM Query Cache System - Performance Optimization (Enhanced)
(function () {
  'use strict';

  /**
   * High-performance DOM query cache with automatic invalidation
   * Reduces repeated querySelector calls by caching results
   */
  class DOMCache {
    constructor() {
      /** @type {Map<string, {element: Element|null, timestamp: number}>} */
      this.cache = new Map();
      /** @type {Map<string, NodeList|Element[]>} */
      this.multiCache = new Map();
      this.maxAge = 5000; // Cache TTL: 5 seconds
      this.nullMaxAge = 1000; // Cache TTL for null/empty results: 1 s (was 250 ms).
      // Most modules react to DOM changes via MutationObserver or yt-navigate-finish,
      // so a 1-second stale window for "not found" entries is safe and cuts
      // repeated querySelector calls by ~75 % for elements absent from the page.
      this.maxSize = 500; // Max cache entries
      this.cleanupInterval = null;
      this.enabled = true;

      // Statistics
      this.stats = { hits: 0, misses: 0, evictions: 0 };

      this.contextUids = new WeakMap();
      this.uidCounter = 0;

      // Shared MutationObserver for waitForElement
      this.observerCallbacks = new Set();
      this.sharedObserver = null;
      this.sharedObserverPending = false;

      // Start periodic cleanup
      this.startCleanup();
    }

    getContextUid(ctx) {
      if (ctx === document) return 'doc';
      let uid = this.contextUids.get(ctx);
      if (!uid) {
        uid = ++this.uidCounter;
        this.contextUids.set(ctx, uid);
      }
      return uid;
    }

    /**
     * Get single element with caching
     * @param {string} selector - CSS selector
     * @param {Element|Document} [context=document] - Context element
     * @param {boolean} [skipCache=false] - Skip cache and force fresh query
     * @returns {Element|null}
     */
    querySelector(selector, context = document, skipCache = false) {
      if (!this.enabled || skipCache) {
        return context.querySelector(selector);
      }

      const cacheKey = `${selector}::${this.getContextUid(context)}`;
      const cached = this.cache.get(cacheKey);
      const now = Date.now();

      // Determine TTL based on cached value
      const ttl = cached && cached.element ? this.maxAge : this.nullMaxAge;

      // Return cached result if valid and element still in DOM
      if (cached && now - cached.timestamp < ttl) {
        if (cached.element) {
          if (this.isElementInDOM(cached.element)) {
            this.stats.hits++;
            return cached.element;
          }
        } else {
          // Return cached null
          this.stats.hits++;
          return null;
        }
      }

      // Track miss
      this.stats.misses++;

      // LRU eviction if cache too large
      if (this.cache.size >= this.maxSize) {
        const firstKey = this.cache.keys().next().value;
        this.cache.delete(firstKey);
        this.stats.evictions++;
      }

      // Query and cache
      const element = context.querySelector(selector);
      this.cache.set(cacheKey, { element, timestamp: now });
      return element;
    }

    /**
     * Get multiple elements with caching
     * @param {string} selector - CSS selector
     * @param {Element|Document} [context=document] - Context element
     * @param {boolean} [skipCache=false] - Skip cache and force fresh query
     * @returns {NodeList|Element[]}
     */
    querySelectorAll(selector, context = document, skipCache = false) {
      if (!this.enabled || skipCache) {
        return context.querySelectorAll(selector);
      }

      const cacheKey = `ALL::${selector}::${this.getContextUid(context)}`;
      const cached = this.multiCache.get(cacheKey);

      if (cached && this.areElementsValid(cached)) {
        return cached;
      }

      const elements = Array.from(context.querySelectorAll(selector));
      this.multiCache.set(cacheKey, elements);

      // Auto-cleanup after maxAge or nullMaxAge
      const ttl = elements.length > 0 ? this.maxAge : this.nullMaxAge;
      setTimeout(() => this.multiCache.delete(cacheKey), ttl);

      return elements;
    }

    /**
     * Get element by ID with caching
     * @param {string} id - Element ID
     * @returns {Element|null}
     */
    getElementById(id) {
      if (!this.enabled) {
        return document.getElementById(id);
      }

      const cacheKey = `ID::${id}`;
      const cached = this.cache.get(cacheKey);
      const now = Date.now();

      if (cached && now - cached.timestamp < this.maxAge) {
        if (cached.element && this.isElementInDOM(cached.element)) {
          return cached.element;
        }
      }

      const element = document.getElementById(id);
      this.cache.set(cacheKey, { element, timestamp: now });
      return element;
    }

    /**
     * Check if element is still in DOM
     * @param {Element} element
     * @returns {boolean}
     */
    isElementInDOM(element) {
      return element && document.contains(element);
    }

    /**
     * Check if cached elements are still valid
     * @param {Element[]} elements
     * @returns {boolean}
     */
    areElementsValid(elements) {
      if (!elements || elements.length === 0) return true;
      // Sample first and last elements for performance
      return this.isElementInDOM(elements[0]) && this.isElementInDOM(elements[elements.length - 1]);
    }

    /**
     * Invalidate cache for specific selector or all
     * @param {string} [selector] - Specific selector to invalidate
     */
    invalidate(selector) {
      if (selector) {
        // Invalidate specific selector
        for (const key of this.cache.keys()) {
          if (key.includes(selector)) {
            this.cache.delete(key);
          }
        }
        for (const key of this.multiCache.keys()) {
          if (key.includes(selector)) {
            this.multiCache.delete(key);
          }
        }
      } else {
        // Clear all cache
        this.cache.clear();
        this.multiCache.clear();
      }
    }

    /**
     * Start periodic cache cleanup
     */
    startCleanup() {
      if (this.cleanupInterval) return;

      // Use requestIdleCallback if available for cleanup to avoid blocking main thread
      const cleanupFn = () => {
        const now = Date.now();
        let deletedCount = 0;
        const maxDeletesPerRun = 50; // Limit work per frame

        // Cleanup single element cache
        for (const [key, value] of this.cache.entries()) {
          if (
            now - value.timestamp > this.maxAge ||
            (value.element && !this.isElementInDOM(value.element))
          ) {
            this.cache.delete(key);
            deletedCount++;
            if (deletedCount >= maxDeletesPerRun) break;
          }
        }
      };

      this.cleanupInterval = setInterval(() => {
        if (typeof requestIdleCallback !== 'undefined') {
          requestIdleCallback(cleanupFn, { timeout: 1000 });
        } else {
          cleanupFn();
        }
      }, 5000); // Run every 5 seconds
    }

    /**
     * Stop cache cleanup and clear all caches
     */
    destroy() {
      if (this.cleanupInterval) {
        clearInterval(this.cleanupInterval);
        this.cleanupInterval = null;
      }
      this.cache.clear();
      this.multiCache.clear();
      if (this.sharedObserver) {
        this.sharedObserver.disconnect();
        this.sharedObserver = null;
      }
      this.observerCallbacks.clear();
    }

    /**
     * Get cache statistics
     * @returns {{size: number, multiSize: number, enabled: boolean}}
     */
    getStats() {
      return {
        size: this.cache.size,
        multiSize: this.multiCache.size,
        enabled: this.enabled,
      };
    }

    /**
     * Initialize shared observer for waitForElement
     */
    initSharedObserver() {
      if (this.sharedObserver) return;

      this.sharedObserver = new MutationObserver(() => {
        if (this.observerCallbacks.size === 0) return;
        if (this.sharedObserverPending) return;

        this.sharedObserverPending = true;
        const flush = () => {
          this.sharedObserverPending = false;
          for (const callback of this.observerCallbacks) {
            try {
              callback();
            } catch {
              // Ignore callback errors to avoid breaking other observers
            }
          }
        };

        if (typeof requestAnimationFrame === 'function') {
          requestAnimationFrame(flush);
        } else {
          setTimeout(flush, 0);
        }
      });

      this.sharedObserver.observe(document.body || document.documentElement, {
        childList: true,
        subtree: true,
      });
    }
  }

  /**
   * Scoped DOM cache for specific contexts (e.g., player, secondary)
   */
  class ScopedDOMCache {
    constructor() {
      /** @type {Map<string, WeakMap<Element, any>>} */
      this.scopedCaches = new Map();
    }

    /**
     * Get or create cache for a scope
     * @param {string} scope - Scope identifier
     * @returns {WeakMap<Element, any>}
     */
    getScope(scope) {
      if (!this.scopedCaches.has(scope)) {
        this.scopedCaches.set(scope, new WeakMap());
      }
      return this.scopedCaches.get(scope);
    }

    /**
     * Cache element in scope
     * @param {string} scope - Scope identifier
     * @param {Element} element - Element to cache
     * @param {any} value - Value to cache
     */
    set(scope, element, value) {
      this.getScope(scope).set(element, value);
    }

    /**
     * Get cached value from scope
     * @param {string} scope - Scope identifier
     * @param {Element} element - Element key
     * @returns {any}
     */
    get(scope, element) {
      return this.getScope(scope).get(element);
    }

    /**
     * Check if element exists in scope
     * @param {string} scope - Scope identifier
     * @param {Element} element - Element key
     * @returns {boolean}
     */
    has(scope, element) {
      return this.getScope(scope).has(element);
    }
  }

  /**
   * Optimized selector patterns for common YouTube elements
   */
  const OptimizedSelectors = {
    // Player elements
    player: '#movie_player',
    video: 'video.video-stream.html5-main-video',
    videoAlt: '#movie_player video',
    chromeBottom: '.ytp-chrome-bottom',

    // Watch page elements
    watchFlexy: 'ytd-watch-flexy',
    secondary: '#secondary',
    rightTabs: '#right-tabs',
    playlistPanel: 'ytd-playlist-panel-renderer',

    // Tab elements
    tabInfo: '#tab-info',
    tabComments: '#tab-comments',
    tabVideos: '#tab-videos',

    // Buttons and controls
    likeButton: 'like-button-view-model button',
    dislikeButton: 'dislike-button-view-model button',
    subscribeButton: '#subscribe-button',

    // Shorts elements
    shorts: 'ytd-shorts',
    activeReel: 'ytd-reel-video-renderer[is-active]',

    // Common containers
    masthead: 'ytd-masthead',
    ytdApp: 'ytd-app',
  };

  /**
   * Batch query executor - executes multiple queries in parallel
   * @param {Array<{selector: string, multi?: boolean, context?: Element}>} queries
   * @returns {Array<Element|Element[]|null>}
   */
  function batchQuery(queries) {
    return queries.map(({ selector, multi = false, context = document }) => {
      if (multi) {
        return Array.from(context.querySelectorAll(selector));
      }
      return context.querySelector(selector);
    });
  }

  // Create global instances
  const globalCache = new DOMCache();
  const scopedCache = new ScopedDOMCache();

  /**
   * Wait for element to appear in DOM (Optimized)
   * @param {string} selector - CSS selector
   * @param {number} [timeout=5000] - Timeout in milliseconds
   * @param {Element} [context=document] - Context element
   * @returns {Promise<Element|null>}
   */
  function waitForElement(selector, timeout = 5000, context = document) {
    return new Promise(resolve => {
      const existing = context.querySelector(selector);
      if (existing) {
        resolve(existing);
        return;
      }

      const isPlaylistPage =
        typeof window !== 'undefined' &&
        window.location &&
        typeof window.location.pathname === 'string' &&
        window.location.pathname === '/playlist';

      // On heavy playlist pages (WL/LL), MutationObserver(subtree) can become very expensive.
      // Prefer lightweight polling here to avoid reacting to the large volume of DOM mutations.
      if (isPlaylistPage && (context === document || context === document.body)) {
        const interval = 250;
        const start = Date.now();
        const timerId = setInterval(() => {
          const element = context.querySelector(selector);
          if (element) {
            clearInterval(timerId);
            resolve(element);
            return;
          }
          if (Date.now() - start >= timeout) {
            clearInterval(timerId);
            resolve(null);
          }
        }, interval);
        return;
      }

      // Use shared observer if context is document/body
      const useShared = context === document || context === document.body;

      if (useShared) {
        globalCache.initSharedObserver();

        const checkCallback = () => {
          const element = context.querySelector(selector);
          if (element) {
            globalCache.observerCallbacks.delete(checkCallback);
            resolve(element);
            return true;
          }
          return false;
        };

        globalCache.observerCallbacks.add(checkCallback);

        setTimeout(() => {
          globalCache.observerCallbacks.delete(checkCallback);
          resolve(null);
        }, timeout);
      } else {
        // Fallback to local observer for specific contexts
        const observer = new MutationObserver(() => {
          const element = context.querySelector(selector);
          if (element) {
            observer.disconnect();
            resolve(element);
          }
        });

        observer.observe(context, {
          childList: true,
          subtree: true,
        });

        setTimeout(() => {
          observer.disconnect();
          resolve(null);
        }, timeout);
      }
    });
  }

  // Export to global namespace
  if (typeof window !== 'undefined') {
    window.YouTubeDOMCache = globalCache;
    window.YouTubeScopedCache = scopedCache;
    window.YouTubeSelectors = OptimizedSelectors;
    window.batchQueryDOM = batchQuery;
    window.waitForElement = waitForElement;

    // Also add to YouTubeUtils if available
    if (window.YouTubeUtils) {
      window.YouTubeUtils.domCache = globalCache;
      window.YouTubeUtils.scopedCache = scopedCache;
      window.YouTubeUtils.selectors = OptimizedSelectors;
      window.YouTubeUtils.batchQuery = batchQuery;
      window.YouTubeUtils.waitFor = waitForElement;
    }
  }

  // Invalidate cache on navigation
  if (typeof window !== 'undefined' && window.addEventListener) {
    window.addEventListener('yt-navigate-finish', () => {
      globalCache.invalidate();
    });

    // Also invalidate on SPF navigation (older YouTube)
    window.addEventListener('spfdone', () => {
      globalCache.invalidate();
    });
  }

  // Cleanup on unload
  if (typeof window !== 'undefined' && window.addEventListener) {
    window.addEventListener('beforeunload', () => {
      globalCache.destroy();
    });
  }
})();

// --- MODULE: event-delegation.js ---

// Event Delegation System - Performance Optimization
(function () {
  'use strict';

  /**
   * Event delegation manager for performance optimization
   * Reduces number of event listeners by delegating to common ancestors
   */
  class EventDelegator {
    constructor() {
      /** @type {Map<string, Map<string, Set<Function>>>} */
      this.delegatedHandlers = new Map();
      /** @type {Map<Element, Map<string, Function>}>} */
      this.registeredDelegators = new Map();
      this.stats = { totalDelegations: 0, totalHandlers: 0 };
    }

    /**
     * Delegate event handler to a parent element
     * @param {Element} parent - Parent element to attach delegated listener
     * @param {string} eventType - Event type (click, input, etc.)
     * @param {string} selector - CSS selector to match target elements
     * @param {Function} handler - Handler function(event, matchedElement)
     * @param {Object} [options] - Event listener options
     */
    delegate(parent, eventType, selector, handler, options = {}) {
      if (!parent || !eventType || !selector || !handler) {
        console.warn('[EventDelegator] Invalid parameters');
        return;
      }

      // Create cache key
      const parentKey = this._getElementKey(parent);
      const delegationKey = `${parentKey}:${eventType}`;

      // Initialize structures
      if (!this.delegatedHandlers.has(delegationKey)) {
        this.delegatedHandlers.set(delegationKey, new Map());
      }

      const handlersForSelector = this.delegatedHandlers.get(delegationKey);
      if (!handlersForSelector.has(selector)) {
        handlersForSelector.set(selector, new Set());
      }

      // Add handler
      handlersForSelector.get(selector).add(handler);
      this.stats.totalHandlers++;

      // Create or get delegated listener
      if (!this.registeredDelegators.has(parent)) {
        this.registeredDelegators.set(parent, new Map());
      }

      const parentDelegators = this.registeredDelegators.get(parent);
      if (!parentDelegators.has(eventType)) {
        const delegatedListener = event => {
          this._handleDelegatedEvent(parent, eventType, event);
        };

        parent.addEventListener(eventType, delegatedListener, options);
        parentDelegators.set(eventType, delegatedListener);
        this.stats.totalDelegations++;

        window.YouTubeUtils?.logger?.debug?.(
          `[EventDelegator] Created delegation on ${parentKey} for ${eventType}`
        );
      }
    }

    /**
     * Remove delegated event handler
     * @param {Element} parent - Parent element
     * @param {string} eventType - Event type
     * @param {string} selector - CSS selector
     * @param {Function} handler - Handler function to remove
     */
    undelegate(parent, eventType, selector, handler) {
      const parentKey = this._getElementKey(parent);
      const delegationKey = `${parentKey}:${eventType}`;

      const handlersForSelector = this.delegatedHandlers.get(delegationKey);
      if (!handlersForSelector) return;

      const handlers = handlersForSelector.get(selector);
      if (!handlers) return;

      handlers.delete(handler);
      this.stats.totalHandlers--;

      // Clean up if no handlers left
      if (handlers.size === 0) {
        handlersForSelector.delete(selector);
      }

      if (handlersForSelector.size === 0) {
        this._removeParentListener(parent, eventType);
        this.delegatedHandlers.delete(delegationKey);
      }
    }

    /**
     * Handle delegated event and dispatch to matching handlers
     * @private
     */
    _handleDelegatedEvent(parent, eventType, event) {
      const parentKey = this._getElementKey(parent);
      const delegationKey = `${parentKey}:${eventType}`;
      const handlersForSelector = this.delegatedHandlers.get(delegationKey);

      if (!handlersForSelector) return;

      // Check each selector for matches
      for (const [selector, handlers] of handlersForSelector.entries()) {
        // Find closest matching element
        const target = event.target.closest(selector);

        if (target && parent.contains(target)) {
          // Execute all handlers for this selector
          for (const handler of handlers) {
            try {
              handler.call(target, event, target);
            } catch (error) {
              console.error('[EventDelegator] Handler error:', error);
              window.YouTubeUtils?.logger?.error?.('[EventDelegator] Handler error', error);
            }
          }
        }
      }
    }

    /**
     * Remove parent listener
     * @private
     */
    _removeParentListener(parent, eventType) {
      const parentDelegators = this.registeredDelegators.get(parent);
      if (!parentDelegators) return;

      const listener = parentDelegators.get(eventType);
      if (listener) {
        parent.removeEventListener(eventType, listener);
        parentDelegators.delete(eventType);
        this.stats.totalDelegations--;
      }

      if (parentDelegators.size === 0) {
        this.registeredDelegators.delete(parent);
      }
    }

    /**
     * Get unique key for element
     * @private
     */
    _getElementKey(element) {
      if (element === document) return 'document';
      if (element === window) return 'window';
      if (element === document.body) return 'body';

      return (
        element.id ||
        element.className ||
        element.tagName ||
        `elem_${Math.random().toString(36).substr(2, 9)}`
      );
    }

    /**
     * Get statistics
     */
    getStats() {
      return {
        ...this.stats,
        uniqueDelegations: this.registeredDelegators.size,
        delegationKeys: this.delegatedHandlers.size,
      };
    }

    /**
     * Clear all delegations
     */
    clear() {
      for (const [parent, delegators] of this.registeredDelegators.entries()) {
        for (const [eventType, listener] of delegators.entries()) {
          parent.removeEventListener(eventType, listener);
        }
      }

      this.delegatedHandlers.clear();
      this.registeredDelegators.clear();
      this.stats = { totalDelegations: 0, totalHandlers: 0 };
    }
  }

  // Create global instance
  const eventDelegator = new EventDelegator();

  /**
   * Convenience wrapper for delegation
   * @param {Element} parent - Parent element
   * @param {string} eventType - Event type
   * @param {string} selector - CSS selector
   * @param {Function} handler - Handler function
   * @param {Object} [options] - Event listener options
   */
  const on = (parent, eventType, selector, handler, options) => {
    eventDelegator.delegate(parent, eventType, selector, handler, options);
  };

  /**
   * Remove delegated handler
   * @param {Element} parent - Parent element
   * @param {string} eventType - Event type
   * @param {string} selector - CSS selector
   * @param {Function} handler - Handler function
   */
  const off = (parent, eventType, selector, handler) => {
    eventDelegator.undelegate(parent, eventType, selector, handler);
  };

  // Export to window
  if (typeof window !== 'undefined') {
    window.YouTubePlusEventDelegation = {
      EventDelegator,
      on,
      off,
      getStats: () => eventDelegator.getStats(),
      clear: () => eventDelegator.clear(),
    };
  }

  if (typeof module !== 'undefined' && module.exports) {
    module.exports = { EventDelegator, on, off };
  }
})();

// --- MODULE: lazy-loader.js ---

// Lazy Loading System - Performance Optimization
(function () {
  'use strict';

  /**
   * Lazy loading manager for non-critical features
   * Defers initialization to improve initial load performance
   */
  class LazyLoader {
    constructor() {
      /** @type {Map<string, {fn: Function, priority: number, loaded: boolean}>} */
      this.modules = new Map();
      /** @type {Set<string>} */
      this.loadedModules = new Set();
      this.stats = { totalModules: 0, loadedModules: 0 };
      this.isIdle = false;
      this.idleCallbackId = null;
    }

    /**
     * Register a module for lazy loading
     * @param {string} name - Module name
     * @param {Function} fn - Function to execute when loaded
     * @param {Object} [options] - Loading options
     * @param {number} [options.priority=0] - Priority (higher = loads first)
     * @param {number} [options.delay=0] - Delay before loading (ms)
     * @param {string[]} [options.dependencies=[]] - Module dependencies
     */
    register(name, fn, options = {}) {
      if (this.modules.has(name)) {
        window.YouTubeUtils?.logger?.warn?.(`[LazyLoader] Module "${name}" already registered`);
        return;
      }

      const moduleConfig = {
        fn,
        priority: options.priority || 0,
        delay: options.delay || 0,
        dependencies: options.dependencies || [],
        loaded: false,
      };

      this.modules.set(name, moduleConfig);
      this.stats.totalModules++;

      window.YouTubeUtils?.logger?.debug?.(
        `[LazyLoader] Registered module "${name}" (priority: ${moduleConfig.priority})`
      );
    }

    /**
     * Load a specific module
     * @param {string} name - Module name
     * @returns {Promise<boolean>} Success status
     */
    async load(name) {
      const module = this.modules.get(name);

      if (!module) {
        window.YouTubeUtils?.logger?.warn?.(`[LazyLoader] Module "${name}" not found`);
        return false;
      }

      if (module.loaded) {
        window.YouTubeUtils?.logger?.debug?.(`[LazyLoader] Module "${name}" already loaded`);
        return true;
      }

      // Check dependencies
      for (const dep of module.dependencies) {
        if (!this.loadedModules.has(dep)) {
          window.YouTubeUtils?.logger?.debug?.(
            `[LazyLoader] Loading dependency "${dep}" for "${name}"`
          );
          await this.load(dep);
        }
      }

      // Apply delay if specified
      if (module.delay > 0) {
        await new Promise(resolve => setTimeout(resolve, module.delay));
      }

      try {
        window.YouTubeUtils?.logger?.debug?.(`[LazyLoader] Loading module "${name}"`);
        const startTime = performance.now();

        await module.fn();

        const loadTime = performance.now() - startTime;
        window.YouTubeUtils?.logger?.debug?.(
          `[LazyLoader] Module "${name}" loaded in ${loadTime.toFixed(2)}ms`
        );

        module.loaded = true;
        this.loadedModules.add(name);
        this.stats.loadedModules++;

        return true;
      } catch (error) {
        console.error(`[LazyLoader] Failed to load module "${name}":`, error);
        window.YouTubeUtils?.logger?.error?.(`[LazyLoader] Module "${name}" load failed`, error);
        return false;
      }
    }

    /**
     * Load all registered modules by priority
     * @returns {Promise<number>} Number of modules loaded
     */
    async loadAll() {
      // Sort modules by priority (highest first)
      const sortedModules = Array.from(this.modules.entries()).sort(
        (a, b) => b[1].priority - a[1].priority
      );

      let loadedCount = 0;

      for (const [name, module] of sortedModules) {
        if (!module.loaded) {
          const success = await this.load(name);
          if (success) loadedCount++;
        }
      }

      return loadedCount;
    }

    /**
     * Load modules when browser is idle
     * @param {number} [timeout=2000] - Timeout for requestIdleCallback
     */
    loadOnIdle(timeout = 2000) {
      if (this.isIdle) {
        window.YouTubeUtils?.logger?.debug?.('[LazyLoader] Idle loading already scheduled');
        return;
      }

      this.isIdle = true;

      const loadModules = async () => {
        window.YouTubeUtils?.logger?.debug?.('[LazyLoader] Starting idle loading');
        const count = await this.loadAll();
        window.YouTubeUtils?.logger?.debug?.(`[LazyLoader] Loaded ${count} modules during idle`);
      };

      // Use requestIdleCallback if available, otherwise setTimeout
      if (typeof requestIdleCallback !== 'undefined') {
        this.idleCallbackId = requestIdleCallback(loadModules, { timeout });
      } else {
        this.idleCallbackId = setTimeout(loadModules, timeout);
      }
    }

    /**
     * Cancel idle loading
     */
    cancelIdleLoading() {
      if (!this.isIdle) return;

      if (typeof window.cancelIdleCallback !== 'undefined' && this.idleCallbackId) {
        window.cancelIdleCallback(this.idleCallbackId);
      } else if (this.idleCallbackId) {
        clearTimeout(this.idleCallbackId);
      }

      this.isIdle = false;
      this.idleCallbackId = null;
    }

    /**
     * Check if module is loaded
     * @param {string} name - Module name
     * @returns {boolean}
     */
    isLoaded(name) {
      return this.loadedModules.has(name);
    }

    /**
     * Get loading statistics
     * @returns {Object} Statistics object
     */
    getStats() {
      return {
        ...this.stats,
        loadingPercentage:
          this.stats.totalModules > 0
            ? (this.stats.loadedModules / this.stats.totalModules) * 100
            : 0,
        unloadedModules: this.stats.totalModules - this.stats.loadedModules,
      };
    }

    /**
     * Clear all modules
     */
    clear() {
      this.cancelIdleLoading();
      this.modules.clear();
      this.loadedModules.clear();
      this.stats = { totalModules: 0, loadedModules: 0 };
    }
  }

  // Create global instance
  const lazyLoader = new LazyLoader();

  // Export to window
  if (typeof window !== 'undefined') {
    window.YouTubePlusLazyLoader = {
      LazyLoader,
      register: (name, fn, options) => lazyLoader.register(name, fn, options),
      load: name => lazyLoader.load(name),
      loadAll: () => lazyLoader.loadAll(),
      loadOnIdle: timeout => lazyLoader.loadOnIdle(timeout),
      isLoaded: name => lazyLoader.isLoaded(name),
      getStats: () => lazyLoader.getStats(),
      clear: () => lazyLoader.clear(),
    };
  }

  if (typeof module !== 'undefined' && module.exports) {
    module.exports = { LazyLoader };
  }
})();

// --- MODULE: main.js ---

/* eslint-disable no-console */
if (typeof trustedTypes !== 'undefined' && trustedTypes.defaultPolicy == null) {
  const s = s => s;
  trustedTypes.createPolicy('default', { createHTML: s, createScriptURL: s, createScript: s });
}

const defaultPolicy = (typeof trustedTypes !== 'undefined' && trustedTypes.defaultPolicy) || {
  createHTML: s => s,
};
function createHTML(s) {
  return defaultPolicy.createHTML(s);
}

let trustHTMLErr = null;
try {
  document.createElement('div').innerHTML = createHTML('1');
} catch (e) {
  trustHTMLErr = e;
}

if (trustHTMLErr) {
  console.error(`trustHTMLErr`, trustHTMLErr);
  throw trustHTMLErr; // exit userscript
}

// -----------------------------------------------------------------------------------------------------------------------------

const executionScript = () => {
  const DEBUG_5084 = false;
  const DEBUG_5085 = false;
  const DEBUG_handleNavigateFactory = false;
  const TAB_AUTO_SWITCH_TO_COMMENTS = false;

  if (typeof trustedTypes !== 'undefined' && trustedTypes.defaultPolicy == null) {
    const s = s => s;
    trustedTypes.createPolicy('default', { createHTML: s, createScriptURL: s, createScript: s });
  }

  const defaultPolicy = (typeof trustedTypes !== 'undefined' && trustedTypes.defaultPolicy) || {
    createHTML: s => s,
  };
  function createHTML(s) {
    return defaultPolicy.createHTML(s);
  }

  let trustHTMLErr = null;
  try {
    document.createElement('div').innerHTML = createHTML('1');
  } catch (e) {
    trustHTMLErr = e;
  }

  if (trustHTMLErr) {
    console.error(`trustHTMLErr`, trustHTMLErr);
    throw trustHTMLErr; // exit userscript
  }

  try {
    let _executionFinished = 0;

    if (typeof CustomElementRegistry === 'undefined') return;
    if (CustomElementRegistry.prototype.define000) return;
    if (typeof CustomElementRegistry.prototype.define !== 'function') return;

    /** @type {HTMLElement} */
    const HTMLElement_ = HTMLElement.prototype.constructor;

    /**
     *  @param {Element} elm
     * @param {string} selector
     * @returns {Element | null}
     *  */
    const qsOne = (elm, selector) => {
      if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.querySelector === 'function') {
        return window.YouTubeDOMCache.querySelector(selector, elm);
      }
      return HTMLElement_.prototype.querySelector.call(elm, selector);
    };

    const _qs = selector => {
      if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.get === 'function') {
        return window.YouTubeDOMCache.get(selector);
      }
      return document.querySelector(selector);
    };

    /**
     * Flexible selector helper: supports both `qs(selector)` and `qs(element, selector)`.
     * Backwards-compatible alias used throughout the codebase.
     */
    function qs(a, b) {
      if (arguments.length === 1) return _qs(a);
      return qsOne(a, b);
    }

    /**
     * Query all elements with optional caching
     * @param {string} selector - CSS selector
     * @param {Element|Document} [context] - Context element
     * @returns {Element[]} Array of elements
     */
    const qsAll = (selector, context) => {
      const ctx = context || document;
      if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.getAll === 'function') {
        return window.YouTubeDOMCache.getAll(selector);
      }
      return Array.from(ctx.querySelectorAll(selector));
    };

    const defineProperties = (p, o) => {
      if (!p) {
        console.warn(`defineProperties ERROR: Prototype is undefined`);
        return;
      }
      for (const k of Object.keys(o)) {
        if (!o[k]) {
          console.warn(`defineProperties ERROR: Property ${k} is undefined`);
          delete o[k];
        }
      }
      return Object.defineProperties(p, o);
    };

    const replaceChildrenPolyfill = function replaceChildren(...new_children) {
      while (this.firstChild) {
        this.removeChild(this.firstChild);
      }
      this.append(...new_children);
    };

    const pdsBaseDF = Object.getOwnPropertyDescriptors(DocumentFragment.prototype);

    if (pdsBaseDF.replaceChildren) {
      defineProperties(DocumentFragment.prototype, {
        replaceChildren000: pdsBaseDF.replaceChildren,
      });
    } else {
      DocumentFragment.prototype.replaceChildren000 = replaceChildrenPolyfill;
    }

    const pdsBaseNode = Object.getOwnPropertyDescriptors(Node.prototype);
    // console.log(pdsBaseElement.setAttribute, pdsBaseElement.getAttribute)
    if (!pdsBaseNode.appendChild000 && !pdsBaseNode.insertBefore000) {
      defineProperties(Node.prototype, {
        appendChild000: pdsBaseNode.appendChild,
        insertBefore000: pdsBaseNode.insertBefore,
      });
    }

    // class BaseElement extends Element{

    // }
    const pdsBaseElement = Object.getOwnPropertyDescriptors(Element.prototype);
    // console.log(pdsBaseElement.setAttribute, pdsBaseElement.getAttribute)

    if (!pdsBaseElement.setAttribute000 && !pdsBaseElement.querySelector000) {
      const nPdsElement = {
        setAttribute000: pdsBaseElement.setAttribute,
        getAttribute000: pdsBaseElement.getAttribute,
        hasAttribute000: pdsBaseElement.hasAttribute,
        removeAttribute000: pdsBaseElement.removeAttribute,
        querySelector000: pdsBaseElement.querySelector,
      };

      if (pdsBaseElement.replaceChildren) {
        nPdsElement.replaceChildren000 = pdsBaseElement.replaceChildren;
      } else {
        Element.prototype.replaceChildren000 = replaceChildrenPolyfill;
      }

      defineProperties(Element.prototype, nPdsElement);
    }

    Element.prototype.setAttribute111 = function (p, v) {
      v = `${v}`;
      if (this.getAttribute000(p) === v) return;
      this.setAttribute000(p, v);
    };

    Element.prototype.incAttribute111 = function (p) {
      let v = +this.getAttribute000(p) || 0;
      v = v > 1e9 ? v + 1 : 9;
      this.setAttribute000(p, `${v}`);
      return v;
    };

    Element.prototype.assignChildren111 = function (previousSiblings, node, nextSiblings) {
      // assume all previousSiblings, node, and nextSiblings are on the page
      //  -> only remove triggering is needed
      let nodeList = [];
      for (let t = this.firstChild; t instanceof Node; t = t.nextSibling) {
        if (t === node) continue;
        nodeList.push(t);
      }

      inPageRearrange = true;
      if (node.parentNode === this) {
        let fm = new DocumentFragment();
        if (nodeList.length > 0) {
          fm.replaceChildren000(...nodeList);
          // nodeList.length = 0;
        }
        // nodeList = null;
        if (previousSiblings && previousSiblings.length > 0) {
          fm.replaceChildren000(...previousSiblings);
          this.insertBefore000(fm, node);
        }
        if (nextSiblings && nextSiblings.length > 0) {
          fm.replaceChildren000(...nextSiblings);
          this.appendChild000(fm);
        }
        fm.replaceChildren000();
        fm = null;
      } else {
        if (!previousSiblings) previousSiblings = [];
        if (!nextSiblings) nextSiblings = [];
        this.replaceChildren000(...previousSiblings, node, ...nextSiblings);
      }
      inPageRearrange = false;
      if (nodeList.length > 0) {
        for (const t of nodeList) {
          if (t instanceof Element && t.isConnected === false) t.remove(); // remove triggering
        }
      }
      nodeList.length = 0;
      nodeList = null;
    };

    let secondaryInnerHold = 0;

    const secondaryInnerFn = cb => {
      if (secondaryInnerHold) {
        secondaryInnerHold++;
        let err, r;
        try {
          r = cb();
        } catch (e) {
          err = e;
        }
        secondaryInnerHold--;
        if (err) throw err;
        return r;
      } else {
        const ea = qs('#secondary-inner');
        const eb = qs('secondary-wrapper#secondary-inner-wrapper');
        if (ea && eb) {
          secondaryInnerHold++;
          let err, r;
          ea.id = 'secondary-inner-';
          eb.id = 'secondary-inner';
          try {
            r = cb();
          } catch (e) {
            err = e;
          }
          ea.id = 'secondary-inner';
          eb.id = 'secondary-inner-wrapper';
          secondaryInnerHold--;
          if (err) throw err;
          return r;
        } else {
          return cb();
        }
      }
    };

    // ==============================================================================================================================================================================================================================================================================

    const DISABLE_FLAGS_SHADYDOM_FREE = true;

    /* eslint-disable no-sequences, no-unused-expressions, prefer-const */
    /**
     *
     * Minified Code from http://greasyfork.icu/en/scripts/475632-ytconfighacks/code (ytConfigHacks)
     * Date: 2024.04.17
     * Minifier: https://www.toptal.com/developers/javascript-minifier
     *
     */
    (() => {
      const e =
        'undefined' != typeof unsafeWindow ? unsafeWindow : this instanceof Window ? this : window;
      if (!e._ytConfigHacks) {
        let t = 4;
        class n extends Set {
          add(e) {
            if (t <= 0) {
              return console.warn('yt.config_ is already applied on the page.');
            }
            'function' == typeof e && super.add(e);
          }
        }
        let a = (async () => {})().constructor,
          i = (e._ytConfigHacks = new n()),
          l = () => {
            const t = e.ytcsi.originalYtcsi;
            t && ((e.ytcsi = t), (l = null));
          },
          c = null,
          o = () => {
            if (t >= 1) {
              const n = (e.yt || 0).config_ || (e.ytcfg || 0).data_ || 0;
              if ('string' == typeof n.INNERTUBE_API_KEY && 'object' == typeof n.EXPERIMENT_FLAGS) {
                for (const a of (--t <= 0 && l && l(), (c = !0), i)) a(n);
              }
            }
          },
          f = 1,
          d = t => {
            if ((t = t || e.ytcsi)) {
              return (
                (e.ytcsi = new Proxy(t, {
                  get: (e, t, _n) =>
                    'originalYtcsi' === t ? e : (o(), c && --f <= 0 && l && l(), e[t]),
                })),
                !0
              );
            }
          };
        d() ||
          Object.defineProperty(e, 'ytcsi', {
            get() {},
            set: t => (t && (delete e.ytcsi, d(t)), !0),
            enumerable: !1,
            configurable: !0,
          });
        const { addEventListener: s, removeEventListener: y } = Document.prototype;
        function r(t) {
          (o(), t && e.removeEventListener('DOMContentLoaded', r, !1));
        }
        (new a(e => {
          if ('undefined' != typeof AbortSignal) {
            (s.call(document, 'yt-page-data-fetched', e, { once: !0 }),
              s.call(document, 'yt-navigate-finish', e, { once: !0 }),
              s.call(document, 'spfdone', e, { once: !0 }));
          } else {
            const t = () => {
              (e(),
                y.call(document, 'yt-page-data-fetched', t, !1),
                y.call(document, 'yt-navigate-finish', t, !1),
                y.call(document, 'spfdone', t, !1));
            };
            (s.call(document, 'yt-page-data-fetched', t, !1),
              s.call(document, 'yt-navigate-finish', t, !1),
              s.call(document, 'spfdone', t, !1));
          }
        }).then(o),
          new a(e => {
            if ('undefined' != typeof AbortSignal) {
              s.call(document, 'yt-action', e, { once: !0, capture: !0 });
            } else {
              const t = () => {
                (e(), y.call(document, 'yt-action', t, !0));
              };
              s.call(document, 'yt-action', t, !0);
            }
          }).then(o),
          a.resolve().then(() => {
            'loading' !== document.readyState ? r() : e.addEventListener('DOMContentLoaded', r, !1);
          }));
      }
    })();

    let configOnce = false;
    window._ytConfigHacks.add(config_ => {
      if (configOnce) return;
      configOnce = true;

      const EXPERIMENT_FLAGS = config_.EXPERIMENT_FLAGS || 0;
      const EXPERIMENTS_FORCED_FLAGS = config_.EXPERIMENTS_FORCED_FLAGS || 0;
      for (const flags of [EXPERIMENT_FLAGS, EXPERIMENTS_FORCED_FLAGS]) {
        if (flags) {
          // flags.kevlar_watch_metadata_refresh_no_old_secondary_data = false;
          // flags.live_chat_overflow_hide_chat = false;
          flags.web_watch_chat_hide_button_killswitch = false;
          flags.web_watch_theater_chat = false; // for re-openable chat (ytd-watch-flexy's liveChatCollapsed is always undefined)
          flags.suppress_error_204_logging = true;
          flags.kevlar_watch_grid = false; // A/B testing for watch grid

          if (DISABLE_FLAGS_SHADYDOM_FREE) {
            flags.enable_shadydom_free_scoped_node_methods = false;
            flags.enable_shadydom_free_scoped_query_methods = false;
            flags.enable_shadydom_free_scoped_readonly_properties_batch_one = false;
            flags.enable_shadydom_free_parent_node = false;
            flags.enable_shadydom_free_children = false;
            flags.enable_shadydom_free_last_child = false;
          }
        }
      }
    });

    // ==============================================================================================================================================================================================================================================================================

    /* globals WeakRef:false */

    /** @type {(o: Object | null) => WeakRef | null} */
    const mWeakRef =
      typeof WeakRef === 'function' ? o => (o ? new WeakRef(o) : null) : o => o || null; // typeof InvalidVar == 'undefined'

    /** @type {(wr: Object | null) => Object | null} */
    const kRef = wr => (wr && wr.deref ? wr.deref() : wr);

    /** @type {globalThis.PromiseConstructor} */
    const Promise = (async () => {})().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.

    const delayPn = delay => new Promise(fn => setTimeout(fn, delay));

    const insp = o => (o ? o.polymerController || o.inst || o || 0 : o || 0);

    const setTimeout_ = setTimeout.bind(window);

    const PromiseExternal = ((resolve_, reject_) => {
      const h = (resolve, reject) => {
        resolve_ = resolve;
        reject_ = reject;
      };
      return class PromiseExternal extends Promise {
        constructor(cb = h) {
          super(cb);
          if (cb === h) {
            /** @type {(value: any) => void} */
            this.resolve = resolve_;
            /** @type {(reason?: any) => void} */
            this.reject = reject_;
          }
        }
      };
    })();

    // ------------------------------------------------------------------------ nextBrowserTick ------------------------------------------------------------------------
    /* eslint-disable no-var */
    var nextBrowserTick =
      void 0 !== nextBrowserTick && nextBrowserTick.version >= 2
        ? nextBrowserTick
        : (() => {
            'use strict';
            const e =
              typeof globalThis !== 'undefined'
                ? globalThis
                : typeof window !== 'undefined'
                  ? window
                  : this;
            let t = !0;
            if (
              !(function n(s) {
                return s
                  ? (t = !1)
                  : e.postMessage && !e.importScripts && e.addEventListener
                    ? (e.addEventListener('message', n, !1),
                      e.postMessage('$$$', '*'),
                      e.removeEventListener('message', n, !1),
                      t)
                    : void 0;
              })()
            ) {
              return void console.warn('Your browser environment cannot use nextBrowserTick');
            }
            const n = (async () => {})().constructor;
            let s = null;
            const o = new Map(),
              { floor: r, random: i } = Math;
            let l;
            do {
              l = `$$nextBrowserTick$$${(i() + 8).toString().slice(2)}$$`;
            } while (l in e);
            const a = l,
              c = a.length + 9;
            e[a] = 1;
            e.addEventListener(
              'message',
              e => {
                if (0 !== o.size) {
                  const t = (e || 0).data;
                  if ('string' == typeof t && t.length === c && e.source === (e.target || 1)) {
                    const e = o.get(t);
                    e && ('p' === t[0] && (s = null), o.delete(t), e());
                  }
                }
              },
              !1
            );
            const d = (t = o) => {
              if (t === o) {
                if (s) return s;
                let t;
                do {
                  t = `p${a}${r(314159265359 * i() + 314159265359).toString(36)}`;
                } while (o.has(t));
                return (
                  (s = new n(e => {
                    o.set(t, e);
                  })),
                  e.postMessage(t, '*'),
                  (t = null),
                  s
                );
              }
              {
                let n;
                do {
                  n = `f${a}${r(314159265359 * i() + 314159265359).toString(36)}`;
                } while (o.has(n));
                (o.set(n, t), e.postMessage(n, '*'));
              }
            };
            return ((d.version = 2), d);
          })();
    /* eslint-enable no-var */

    /* eslint-enable no-sequences, no-unused-expressions */

    // ------------------------------------------------------------------------ nextBrowserTick ------------------------------------------------------------------------
    // ------------------------------------------------------------------------ nextBrowserTick ------------------------------------------------------------------------

    const isPassiveArgSupport = typeof IntersectionObserver === 'function';
    const capturePassive = isPassiveArgSupport ? { capture: true, passive: true } : true;

    class Attributer {
      constructor(list) {
        this.list = list;
        this.flag = 0;
      }
      makeString() {
        let k = 1;
        let s = '';
        let i = 0;
        while (this.flag >= k) {
          if (this.flag & k) {
            s += this.list[i];
          }
          i++;
          k <<= 1;
        }
        return s;
      }
    }

    const mLoaded = new Attributer('icp');

    const wrSelfMap = new WeakMap();

    /** @type {Object.<string, Element | null>} */
    const elements = new Proxy(
      {
        related: null,
        comments: null,
        infoExpander: null,
      },
      {
        get(target, prop) {
          return kRef(target[prop]);
        },
        set(target, prop, value) {
          if (value) {
            let wr = wrSelfMap.get(value);
            if (!wr) {
              wr = mWeakRef(value);
              wrSelfMap.set(value, wr);
            }
            target[prop] = wr;
          } else {
            target[prop] = null;
          }
          return true;
        },
      }
    );
    const getMainInfo = () => {
      const infoExpander = elements.infoExpander;
      if (!infoExpander) return null;
      const mainInfo = infoExpander.matches('[tyt-main-info]')
        ? infoExpander
        : infoExpander.querySelector000('[tyt-main-info]');
      return mainInfo || null;
    };

    let pageType = null;
    let _pageLang = 'en';
    /**
     * Translation helper using centralized i18n system
     * @param {string} key - Translation key
     * @returns {string} Translated string
     */
    function getWord(tag) {
      try {
        // Use centralized i18n system if available
        if (typeof window !== 'undefined' && window.YouTubePlusI18n) {
          const translation = window.YouTubePlusI18n.t(`tabs.${tag}`);
          if (translation && translation !== `tabs.${tag}`) {
            return translation;
          }
        }

        // Fallback to basic English translations
        const fallbackWords = {
          info: 'Info',
          videos: 'Videos',
          playlist: 'Playlist',
        };

        return fallbackWords[tag] || tag;
      } catch (error) {
        console.warn('[YouTube+][Main] Translation error:', error);
        // Final fallback to English
        const englishWords = {
          info: 'Info',
          videos: 'Videos',
          playlist: 'Playlist',
        };
        return englishWords[tag] || tag;
      }
    }

    const svgComments =
      `<path d="M80 27H12A12 12 90 0 0 0 39v42a12 12 90 0 0 12 12h12v20a2 2 90 0 0 3.4 2L47 93h33a12 
  12 90 0 0 12-12V39a12 12 90 0 0-12-12zM20 47h26a2 2 90 1 1 0 4H20a2 2 90 1 1 0-4zm52 28H20a2 2 90 1 1 0-4h52a2 2 90 
  1 1 0 4zm0-12H20a2 2 90 1 1 0-4h52a2 2 90 1 1 0 4zm36-58H40a12 12 90 0 0-12 12v6h52c9 0 16 7 16 16v42h0v4l7 7a2 2 90 
  0 0 3-1V71h2a12 12 90 0 0 12-12V17a12 12 90 0 0-12-12z"/>`.trim();

    const svgVideos =
      `<path d="M89 10c0-4-3-7-7-7H7c-4 0-7 3-7 7v70c0 4 3 7 7 7h75c4 0 7-3 7-7V10zm-62 2h13v10H27V12zm-9 
  66H9V68h9v10zm0-56H9V12h9v10zm22 56H27V68h13v10zm-3-25V36c0-2 2-3 4-2l12 8c2 1 2 4 0 5l-12 8c-2 1-4 0-4-2zm25 
  25H49V68h13v10zm0-56H49V12h13v10zm18 56h-9V68h9v10zm0-56h-9V12h9v10z"/>`.trim();

    const svgInfo =
      `<path d="M30 0C13.3 0 0 13.3 0 30s13.3 30 30 30 30-13.3 30-30S46.7 0 30 0zm6.2 46.6c-1.5.5-2.6 
  1-3.6 1.3a10.9 10.9 0 0 1-3.3.5c-1.7 0-3.3-.5-4.3-1.4a4.68 4.68 0 0 1-1.6-3.6c0-.4.2-1 .2-1.5a20.9 20.9 90 0 1 
  .3-2l2-6.8c.1-.7.3-1.3.4-1.9a8.2 8.2 90 0 0 .3-1.6c0-.8-.3-1.4-.7-1.8s-1-.5-2-.5a4.53 4.53 0 0 0-1.6.3c-.5.2-1 
  .2-1.3.4l.6-2.1c1.2-.5 2.4-1 3.5-1.3s2.3-.6 3.3-.6c1.9 0 3.3.6 4.3 1.3s1.5 2.1 1.5 3.5c0 .3 0 .9-.1 1.6a10.4 10.4 
  90 0 1-.4 2.2l-1.9 6.7c-.2.5-.2 1.1-.4 1.8s-.2 1.3-.2 1.6c0 .9.2 1.6.6 1.9s1.1.5 2.1.5a6.1 6.1 90 0 0 1.5-.3 9 9 90 
  0 0 1.4-.4l-.6 2.2zm-3.8-35.2a1 1 0 010 8.6 1 1 0 010-8.6z"/>`.trim();

    const svgPlayList =
      `<path d="M0 3h12v2H0zm0 4h12v2H0zm0 4h8v2H0zm16 0V7h-2v4h-4v2h4v4h2v-4h4v-2z"/>`.trim();

    const svgElm = (w, h, vw, vh, p, m) =>
      `<svg${m ? ` class=${m}` : ''} width="${w}" height="${h}" viewBox="0 0 ${vw} ${vh}" preserveAspectRatio="xMidYMid meet">${p}</svg>`;

    const hiddenTabsByUserCSS = 0;

    function getTabsHTML() {
      const sTabBtnVideos = `${svgElm(16, 16, 90, 90, svgVideos)}<span>${getWord('videos')}</span>`;
      const sTabBtnInfo = `${svgElm(16, 16, 60, 60, svgInfo)}<span>${getWord('info')}</span>`;
      const sTabBtnPlayList = `${svgElm(16, 16, 20, 20, svgPlayList)}<span>${getWord('playlist')}</span>`;

      const str1 = `
        <paper-ripple class="style-scope yt-icon-button">
            <div id="background" class="style-scope paper-ripple" style="opacity:0;"></div>
            <div id="waves" class="style-scope paper-ripple"></div>
        </paper-ripple>
        `;

      const str_fbtns = `
    <div class="font-size-right">
    <div class="font-size-btn font-size-plus" tyt-di="8rdLQ">
    <svg width="12" height="12" viewbox="0 0 50 50" preserveAspectRatio="xMidYMid meet" 
    stroke="currentColor" stroke-width="6" stroke-linecap="round" vector-effect="non-scaling-size">
      <path d="M12 25H38M25 12V38"/>
    </svg>
    </div><div class="font-size-btn font-size-minus" tyt-di="8rdLQ">
    <svg width="12" height="12" viewbox="0 0 50 50" preserveAspectRatio="xMidYMid meet"
    stroke="currentColor" stroke-width="6" stroke-linecap="round" vector-effect="non-scaling-size">
      <path d="M12 25h26"/>
    </svg>
    </div>
    </div>
    `.replace(/[\r\n]+/g, '');

      const str_tabs = [
        `<a id="tab-btn1" tyt-di="q9Kjc" tyt-tab-content="#tab-info" class="tab-btn${(hiddenTabsByUserCSS & 1) === 1 ? ' tab-btn-hidden' : ''}">${sTabBtnInfo}${str1}${str_fbtns}</a>`,
        `<a id="tab-btn3" tyt-di="q9Kjc" tyt-tab-content="#tab-comments" class="tab-btn${(hiddenTabsByUserCSS & 2) === 2 ? ' tab-btn-hidden' : ''}">${svgElm(16, 16, 120, 120, svgComments)}<span id="tyt-cm-count"></span>${str1}${str_fbtns}</a>`,
        `<a id="tab-btn4" tyt-di="q9Kjc" tyt-tab-content="#tab-videos" class="tab-btn${(hiddenTabsByUserCSS & 4) === 4 ? ' tab-btn-hidden' : ''}">${sTabBtnVideos}${str1}${str_fbtns}</a>`,
        `<a id="tab-btn5" tyt-di="q9Kjc" tyt-tab-content="#tab-list" class="tab-btn tab-btn-hidden">${sTabBtnPlayList}${str1}${str_fbtns}</a>`,
      ].join('');

      const addHTML = `
        <div id="right-tabs">
            <tabview-view-pos-thead></tabview-view-pos-thead>
            <header>
                <div id="material-tabs">
                    ${str_tabs}
                </div>
            </header>
            <div class="tab-content">
                <div id="tab-info" class="tab-content-cld tab-content-hidden" tyt-hidden userscript-scrollbar-render></div>
                <div id="tab-comments" class="tab-content-cld tab-content-hidden" tyt-hidden userscript-scrollbar-render></div>
                <div id="tab-videos" class="tab-content-cld tab-content-hidden" tyt-hidden userscript-scrollbar-render></div>
                <div id="tab-list" class="tab-content-cld tab-content-hidden" tyt-hidden userscript-scrollbar-render></div>
            </div>
        </div>
        `;

      return addHTML;
    }

    // All languages shipped with the script — keep in sync with i18n.js AVAILABLE_LANGUAGES.
    const langWords = {
      ar: true,
      be: true,
      bg: true,
      cn: true,
      de: true,
      du: true,
      en: true,
      es: true,
      fr: true,
      hi: true,
      id: true,
      it: true,
      jp: true,
      kk: true,
      kr: true,
      ky: true,
      pl: true,
      pt: true,
      ru: true,
      tr: true,
      tw: true,
      uk: true,
      uz: true,
      vi: true,
      ng: true,
    };

    /**
     * Get the current UI language code.
     * Delegates to the centralized i18n system so all shipped languages (and their
     * locale variants) are resolved correctly. Falls back to a minimal inline map
     * for the brief window before i18n initialises.
     */
    function getLang() {
      // Prefer the authoritative i18n system (already detects ytcfg, html[lang], URL hl=)
      try {
        if (window.YouTubePlusI18n && typeof window.YouTubePlusI18n.getLanguage === 'function') {
          const detected = window.YouTubePlusI18n.getLanguage();
          if (detected && langWords[detected]) return detected;
        }
      } catch {}

      // Inline fallback covers all shipped supported languages so early callers
      // (before i18n is ready) still get a correct code.
      const htmlLang = ((document || 0).documentElement || 0).lang || '';
      const localMap = {
        // Dutch (de → 'du' is the project's internal code, not a typo)
        de: 'du',
        'de-de': 'du',
        'de-at': 'du',
        'de-ch': 'du',
        // French
        fr: 'fr',
        'fr-fr': 'fr',
        'fr-ca': 'fr',
        'fr-be': 'fr',
        'fr-ch': 'fr',
        // Chinese (Traditional)
        'zh-hant': 'tw',
        'zh-hant-hk': 'tw',
        'zh-hant-tw': 'tw',
        'zh-tw': 'tw',
        'zh-hk': 'tw',
        // Chinese (Simplified)
        'zh-hans': 'cn',
        'zh-hans-cn': 'cn',
        'zh-cn': 'cn',
        zh: 'cn',
        'zh-sg': 'cn',
        // Japanese
        ja: 'jp',
        'ja-jp': 'jp',
        // Korean
        ko: 'kr',
        'ko-kr': 'kr',
        // Russian
        ru: 'ru',
        'ru-ru': 'ru',
        // Ukrainian
        uk: 'uk',
        'uk-ua': 'uk',
        // Belarusian
        be: 'be',
        'be-by': 'be',
        // Bulgarian
        bg: 'bg',
        'bg-bg': 'bg',
        // Spanish
        es: 'es',
        'es-es': 'es',
        'es-419': 'es',
        'es-mx': 'es',
        // Portuguese
        pt: 'pt',
        'pt-pt': 'pt',
        'pt-br': 'pt',
        // Italian
        it: 'it',
        'it-it': 'it',
        // Polish
        pl: 'pl',
        'pl-pl': 'pl',
        // Dutch (nl → 'du')
        nl: 'du',
        'nl-nl': 'du',
        'nl-be': 'du',
        // Arabic
        ar: 'ar',
        'ar-sa': 'ar',
        'ar-ae': 'ar',
        'ar-eg': 'ar',
        // Hindi
        hi: 'hi',
        'hi-in': 'hi',
        // Indonesian
        id: 'id',
        'id-id': 'id',
        // Nigerian (Pidgin)
        ng: 'ng',
        'en-ng': 'ng',
        pcm: 'ng',
        'pcm-ng': 'ng',
        // Turkish
        tr: 'tr',
        'tr-tr': 'tr',
        // Vietnamese
        vi: 'vi',
        'vi-vn': 'vi',
        // Uzbek
        uz: 'uz',
        'uz-uz': 'uz',
        // Kazakh
        kk: 'kk',
        'kk-kz': 'kk',
        // Kyrgyz
        ky: 'ky',
      };
      return localMap[htmlLang.toLowerCase()] || 'en';
    }

    function getLangForPage() {
      const lang = getLang();
      _pageLang = langWords[lang] ? lang : 'en';
    }

    /** @type {Object.<string, number>} */
    const _locks = {};

    const lockGet = new Proxy(_locks, {
      get(target, prop) {
        return target[prop] || 0;
      },
      set(_target, _prop, _val) {
        return true;
      },
    });

    const lockSet = new Proxy(_locks, {
      get(target, prop) {
        if (target[prop] > 1e9) target[prop] = 9;
        return (target[prop] = (target[prop] || 0) + 1);
      },
      set(_target, _prop, _val) {
        return true;
      },
    });

    // note: xxxxxxxxxAsyncLock is not expected for calling multiple time in a short period.
    //       it is just to split the process into microTasks.

    const videosElementProvidedPromise = new PromiseExternal();
    const navigateFinishedPromise = new PromiseExternal();

    let isRightTabsInserted = false;
    const rightTabsProvidedPromise = new PromiseExternal();

    const infoExpanderElementProvidedPromise = new PromiseExternal();

    const pluginsDetected = {};
    let pluginDetectDebounceTimer = null;
    const pluginDetectObserver = new MutationObserver(mutations => {
      if (pluginDetectDebounceTimer) return;
      pluginDetectDebounceTimer = setTimeout(() => {
        pluginDetectDebounceTimer = null;
        processPluginDetectMutations(mutations);
      }, 50);
    });

    const processPluginDetectMutations = mutations => {
      let changeOnRoot = false;
      const newPlugins = [];
      const attributeChangedSet = new Set();
      for (const mutation of mutations) {
        if (mutation.target === document) changeOnRoot = true;
        let detected = '';
        switch (mutation.attributeName) {
          case 'data-ytlstm-new-layout':
          case 'data-ytlstm-overlay-text-shadow':
          case 'data-ytlstm-theater-mode':
            detected = 'external.ytlstm'; // YouTube Livestreams Theater Mode
            attributeChangedSet.add(detected);
            break;
        }
        if (detected && !pluginsDetected[detected]) {
          pluginsDetected[detected] = true;
          newPlugins.push(detected);
        }
      }
      if (elements.flexy && attributeChangedSet.has('external.ytlstm')) {
        elements.flexy.setAttribute(
          'tyt-external-ytlstm',
          qs('[data-ytlstm-theater-mode]') ? '1' : '0'
        );
      }
      if (changeOnRoot) {
        // prevent change of document.body
        pluginDetectObserver.observe(document.body, {
          attributes: true,
          attributeFilter: [
            'data-ytlstm-new-layout',
            'data-ytlstm-overlay-text-shadow',
            'data-ytlstm-theater-mode',
          ],
        });
      }
      for (const detected of newPlugins) {
        const pluginItem = plugin[`${detected}`];
        if (pluginItem) {
          pluginItem.activate();
        } else {
          console.warn(`No Plugin Activator for ${detected}`);
        }
      }
    };

    const pluginAttributeFilter = [
      'data-ytlstm-new-layout',
      'data-ytlstm-overlay-text-shadow',
      'data-ytlstm-theater-mode',
    ];
    pluginDetectObserver.observe(document.documentElement, {
      attributes: true,
      attributeFilter: pluginAttributeFilter,
    });
    if (document.body) {
      pluginDetectObserver.observe(document.body, {
        attributes: true,
        attributeFilter: pluginAttributeFilter,
      });
    }
    navigateFinishedPromise.then(() => {
      pluginDetectObserver.observe(document.documentElement, {
        attributes: true,
        attributeFilter: pluginAttributeFilter,
      });
      if (document.body) {
        pluginDetectObserver.observe(document.body, {
          attributes: true,
          attributeFilter: pluginAttributeFilter,
        });
      }
    });

    const funcCanCollapse = function (_s) {
      // if (!s) return;
      const content = this.content || this.$.content;
      this.canToggle =
        this.shouldUseNumberOfLines &&
        (this.alwaysCollapsed || this.collapsed || this.isToggled === false)
          ? this.alwaysToggleable ||
            this.isToggled ||
            (content && content.offsetHeight < content.scrollHeight)
          : this.alwaysToggleable ||
            this.isToggled ||
            (content && content.scrollHeight > this.collapsedHeight);
    };

    const aoChatAttrChangeFn = async lockId => {
      if (lockGet['aoChatAttrAsyncLock'] !== lockId) return;

      const chatElm = elements.chat;
      const ytdFlexyElm = elements.flexy;
      // console.log(1882, chatElm, ytdFlexyElm)
      if (chatElm && ytdFlexyElm) {
        const isChatCollapsed = chatElm.hasAttribute000('collapsed');
        if (isChatCollapsed) {
          ytdFlexyElm.setAttribute111('tyt-chat-collapsed', '');
        } else {
          ytdFlexyElm.removeAttribute000('tyt-chat-collapsed');
        }

        ytdFlexyElm.setAttribute111('tyt-chat', isChatCollapsed ? '-' : '+');
      }
    };

    // const aoInfoAttrChangeFn = async (lockId) => {
    //   if (lockGet['aoInfoAttrAsyncLock'] !== lockId) return;
    // };

    // const zoInfoAttrChangeFn = async (lockId) => {
    //   if (lockGet['zoInfoAttrAsyncLock'] !== lockId) return;
    // };

    const aoPlayListAttrChangeFn = async lockId => {
      if (lockGet['aoPlayListAttrAsyncLock'] !== lockId) return;

      const playlistElm = elements.playlist;
      const ytdFlexyElm = elements.flexy;
      // console.log(1882, chatElm, ytdFlexyElm)
      let doAttributeChange = 0;
      if (playlistElm && ytdFlexyElm) {
        if (playlistElm.closest('[hidden]')) {
          doAttributeChange = 2;
        } else if (playlistElm.hasAttribute000('collapsed')) {
          doAttributeChange = 2;
        } else {
          doAttributeChange = 1;
        }
      } else if (ytdFlexyElm) {
        doAttributeChange = 2;
      }
      if (doAttributeChange === 1) {
        if (ytdFlexyElm.getAttribute000('tyt-playlist-expanded') !== '') {
          ytdFlexyElm.setAttribute111('tyt-playlist-expanded', '');
        }
      } else if (doAttributeChange === 2) {
        if (ytdFlexyElm.hasAttribute000('tyt-playlist-expanded')) {
          ytdFlexyElm.removeAttribute000('tyt-playlist-expanded');
        }
      }
    };

    const aoChat = new MutationObserver(() => {
      Promise.resolve(lockSet['aoChatAttrAsyncLock']).then(aoChatAttrChangeFn).catch(console.warn);
    });

    // const aoInfo = new MutationObserver(()=>{
    //   Promise.resolve(lockSet['aoInfoAttrAsyncLock']).then(aoInfoAttrChangeFn).catch(console.warn);
    // });

    // const zoInfo = new MutationObserver(()=>{
    //   Promise.resolve(lockSet['zoInfoAttrAsyncLock']).then(zoInfoAttrChangeFn).catch(console.warn);
    // });

    const aoPlayList = new MutationObserver(() => {
      Promise.resolve(lockSet['aoPlayListAttrAsyncLock'])
        .then(aoPlayListAttrChangeFn)
        .catch(console.warn);
    });

    let aoCommentThrottleTimer = null;
    let aoCommentPendingMutations = [];
    const aoComment = new MutationObserver(async mutations => {
      aoCommentPendingMutations.push(...mutations);
      if (aoCommentThrottleTimer) return;
      aoCommentThrottleTimer = setTimeout(() => {
        aoCommentThrottleTimer = null;
        const allMutations = aoCommentPendingMutations;
        aoCommentPendingMutations = [];
        processCommentMutations(allMutations);
      }, 50);
    });

    const processCommentMutations = async mutations => {
      const commentsArea = elements.comments;
      const ytdFlexyElm = elements.flexy;

      //tyt-comments-video-id //tyt-comments-data-status // hidden
      if (!commentsArea) return;
      let bfHidden = false;
      let bfCommentsVideoId = false;
      let bfCommentDisabled = false;
      for (const mutation of mutations) {
        if (mutation.attributeName === 'hidden' && mutation.target === commentsArea) {
          bfHidden = true;
        } else if (
          mutation.attributeName === 'tyt-comments-video-id' &&
          mutation.target === commentsArea
        ) {
          bfCommentsVideoId = true;
        } else if (
          mutation.attributeName === 'tyt-comments-data-status' &&
          mutation.target === commentsArea
        ) {
          bfCommentDisabled = true;
        }
      }

      if (bfHidden) {
        if (!commentsArea.hasAttribute000('hidden')) {
          Promise.resolve(commentsArea)
            .then(eventMap['settingCommentsVideoId'])
            .catch(console.warn);
        }

        Promise.resolve(lockSet['removeKeepCommentsScrollerLock'])
          .then(removeKeepCommentsScroller)
          .catch(console.warn);
      }

      if ((bfHidden || bfCommentsVideoId || bfCommentDisabled) && ytdFlexyElm) {
        const commentsDataStatus = +commentsArea.getAttribute000('tyt-comments-data-status');
        if (commentsDataStatus === 2) {
          ytdFlexyElm.setAttribute111('tyt-comment-disabled', '');
        } else if (commentsDataStatus === 1) {
          ytdFlexyElm.removeAttribute000('tyt-comment-disabled');
        }

        Promise.resolve(lockSet['checkCommentsShouldBeHiddenLock'])
          .then(eventMap['checkCommentsShouldBeHidden'])
          .catch(console.warn);

        const lockId = lockSet['rightTabReadyLock01'];
        await rightTabsProvidedPromise.then();
        if (lockGet['rightTabReadyLock01'] !== lockId) return;

        if (elements.comments !== commentsArea) return;
        if (commentsArea.isConnected === false) return;
        // console.log(7932, 'comments');

        if (commentsArea.closest('#tab-comments')) {
          const shouldTabVisible = !commentsArea.closest('[hidden]');
          document
            .querySelector('[tyt-tab-content="#tab-comments"]')
            .classList.toggle('tab-btn-hidden', !shouldTabVisible);
        }
      }
    };

    const ioComment = new IntersectionObserver(
      entries => {
        requestAnimationFrame(() => {
          for (const entry of entries) {
            const target = entry.target;
            const cnt = insp(target);
            if (
              entry.isIntersecting &&
              target instanceof HTMLElement_ &&
              typeof cnt.calculateCanCollapse === 'function'
            ) {
              void lockSet['removeKeepCommentsScrollerLock'];
              cnt.calculateCanCollapse(true);
              target.setAttribute111('io-intersected', '');
              const ytdFlexyElm = elements.flexy;
              if (ytdFlexyElm && !ytdFlexyElm.hasAttribute000('keep-comments-scroller')) {
                ytdFlexyElm.setAttribute111('keep-comments-scroller', '');
              }
            } else if (target.hasAttribute000('io-intersected')) {
              target.removeAttribute000('io-intersected');
            }
          }
        });
      },
      {
        threshold: [0],
        rootMargin: '100px',
      }
    );

    let bFixForResizedTabLater = false;
    let lastRoRightTabsWidth = 0;
    let resizeDebounceTimer = null;
    const roRightTabs = new ResizeObserver(entries => {
      if (resizeDebounceTimer) return;
      resizeDebounceTimer = setTimeout(() => {
        resizeDebounceTimer = null;
        const entry = entries[entries.length - 1];
        const width = Math.round(entry.borderBoxSize.inlineSize);
        if (lastRoRightTabsWidth !== width) {
          lastRoRightTabsWidth = width;
          if ((tabAStatus & 2) === 2) {
            bFixForResizedTabLater = false;
            Promise.resolve(1).then(eventMap['fixForTabDisplay']);
          } else {
            bFixForResizedTabLater = true;
          }
        }
      }, 100);
      // console.log('resize')
    });

    let cachedTabLinks = null;
    let cachedTabContents = new Map();
    const switchToTab = activeLink => {
      if (typeof activeLink === 'string') {
        activeLink = qs(`a[tyt-tab-content="${activeLink}"]`) || null;
      }

      const ytdFlexyElm = elements.flexy;

      if (!cachedTabLinks || cachedTabLinks.length === 0 || !cachedTabLinks[0].isConnected) {
        cachedTabLinks = qsAll('#material-tabs a[tyt-tab-content]');
        cachedTabContents.clear();
      }
      const links = cachedTabLinks;

      //console.log(701, activeLink)

      for (const link of links) {
        let content = cachedTabContents.get(link);
        if (!content || !content.isConnected) {
          content = qs(link.getAttribute000('tyt-tab-content'));
          if (content) cachedTabContents.set(link, content);
        }
        if (link && content) {
          if (link !== activeLink) {
            link.classList.remove('active');
            content.classList.add('tab-content-hidden');
            if (!content.hasAttribute000('tyt-hidden')) {
              content.setAttribute111('tyt-hidden', ''); // for http://greasyfork.icu/en/scripts/456108
            }
          } else {
            link.classList.add('active');
            if (content.hasAttribute000('tyt-hidden')) {
              content.removeAttribute000('tyt-hidden'); // for http://greasyfork.icu/en/scripts/456108
            }
            content.classList.remove('tab-content-hidden');
          }
        }
      }

      const switchingTo = activeLink ? activeLink.getAttribute000('tyt-tab-content') : '';
      if (switchingTo) {
        lastTab = lastPanel = switchingTo;
      }

      if (ytdFlexyElm.getAttribute000('tyt-chat') === '') {
        ytdFlexyElm.removeAttribute000('tyt-chat');
      }
      ytdFlexyElm.setAttribute111('tyt-tab', switchingTo);

      if (switchingTo) {
        bFixForResizedTabLater = false;
        Promise.resolve(0).then(eventMap['fixForTabDisplay']);
      }
    };

    let tabAStatus = 0;
    const calculationFn = (r = 0, flag) => {
      const ytdFlexyElm = elements.flexy;
      if (!ytdFlexyElm) return r;
      if (flag & 1) {
        r |= 1;
        if (!ytdFlexyElm.hasAttribute000('theater')) r -= 1;
      }
      if (flag & 2) {
        r |= 2;
        if (!ytdFlexyElm.getAttribute000('tyt-tab')) r -= 2;
      }
      if (flag & 4) {
        r |= 4;
        if (ytdFlexyElm.getAttribute000('tyt-chat') !== '-') r -= 4;
      }
      if (flag & 8) {
        r |= 8;
        if (ytdFlexyElm.getAttribute000('tyt-chat') !== '+') r -= 8;
      }
      if (flag & 16) {
        r |= 16;
        if (!ytdFlexyElm.hasAttribute000('is-two-columns_')) r -= 16;
      }
      if (flag & 32) {
        r |= 32;
        if (!ytdFlexyElm.hasAttribute000('tyt-egm-panel_')) r -= 32;
      }
      if (flag & 64) {
        r |= 64;
        if (!document.fullscreenElement) r -= 64;
      }
      if (flag & 128) {
        r |= 128;
        if (!ytdFlexyElm.hasAttribute000('tyt-playlist-expanded')) r -= 128;
      }
      if (flag & 4096) {
        r |= 4096;
        if (ytdFlexyElm.getAttribute('tyt-external-ytlstm') !== '1') r -= 4096;
      }
      return r;
    };

    function isTheater() {
      const ytdFlexyElm = elements.flexy;
      return ytdFlexyElm && ytdFlexyElm.hasAttribute000('theater');
    }

    /** Check if zen theater overlay CSS is active (chat/comments become fixed overlays) */
    function isZenTheaterOverlayActive() {
      try {
        const raw = localStorage.getItem('youtube_plus_settings');
        if (!raw) return true; // defaults enable zen theater enhancements
        const s = JSON.parse(raw);
        if (s?.enableZenStyles === false) return false;
        if (s?.zenStyles?.theaterEnhancements === false) return false;
        return true;
      } catch {
        return true;
      }
    }

    function ytBtnCancelTheater() {
      // When zen theater overlay is active, chat/comments are fixed overlays.
      // Do NOT programmatically exit theater — the user controls it via 't' key.
      if (isZenTheaterOverlayActive()) return;
      if (isTheater()) {
        const sizeBtn = qs('ytd-watch-flexy #ytd-player button.ytp-size-button');
        if (sizeBtn) sizeBtn.click();
      }
    }

    function getSuitableElement(selector) {
      const elements = qsAll(selector);
      let j = -1,
        h = -1;
      for (let i = 0, l = elements.length; i < l; i++) {
        const d = elements[i].getElementsByTagName('*').length;
        if (d > h) {
          h = d;
          j = i;
        }
      }
      return j >= 0 ? elements[j] : null;
    }

    function ytBtnExpandChat() {
      const dom = getSuitableElement('ytd-live-chat-frame#chat');
      const cnt = insp(dom);
      if (cnt && typeof cnt.collapsed === 'boolean') {
        if (typeof cnt.setCollapsedState === 'function') {
          cnt.setCollapsedState({
            setLiveChatCollapsedStateAction: {
              collapsed: false,
            },
          });
          if (cnt.collapsed === false) return;
        }
        cnt.collapsed = false;
        if (cnt.collapsed === false) return;
        if (cnt.isHiddenByUser === true && cnt.collapsed === true) {
          cnt.isHiddenByUser = false;
          cnt.collapsed = false;
        }
      }
      let button = qs(
        'ytd-live-chat-frame#chat[collapsed] > .ytd-live-chat-frame#show-hide-button'
      );
      if (button) {
        button =
          button.querySelector000('div.yt-spec-touch-feedback-shape') ||
          button.querySelector000('ytd-toggle-button-renderer');
        if (button) button.click();
      }
    }

    function ytBtnCollapseChat() {
      // When zen theater overlay is active, don't programmatically collapse chat —
      // it should remain visible as a transparent overlay panel.
      if (isZenTheaterOverlayActive() && isTheater()) return;
      const dom = getSuitableElement('ytd-live-chat-frame#chat');
      const cnt = insp(dom);
      if (cnt && typeof cnt.collapsed === 'boolean') {
        if (typeof cnt.setCollapsedState === 'function') {
          cnt.setCollapsedState({
            setLiveChatCollapsedStateAction: {
              collapsed: true,
            },
          });
          if (cnt.collapsed === true) return;
        }
        cnt.collapsed = true;
        if (cnt.collapsed === true) return;
        if (cnt.isHiddenByUser === false && cnt.collapsed === false) {
          cnt.isHiddenByUser = true;
          cnt.collapsed = true;
        }
      }
      let button = qs(
        'ytd-live-chat-frame#chat:not([collapsed]) > .ytd-live-chat-frame#show-hide-button'
      );
      if (button) {
        button =
          button.querySelector000('div.yt-spec-touch-feedback-shape') ||
          button.querySelector000('ytd-toggle-button-renderer');
        if (button) button.click();
      }
    }

    function ytBtnEgmPanelCore(arr) {
      if (!arr) return;
      if (!('length' in arr)) arr = [arr];

      const ytdFlexyElm = elements.flexy;
      if (!ytdFlexyElm) return;

      let actions = [];

      for (const entry of arr) {
        if (!entry) continue;

        const panelId = entry.panelId;

        const toHide = entry.toHide;
        const toShow = entry.toShow;

        if (toHide === true && !toShow) {
          actions.push({
            changeEngagementPanelVisibilityAction: {
              targetId: panelId,
              visibility: 'ENGAGEMENT_PANEL_VISIBILITY_HIDDEN',
            },
          });
        } else if (toShow === true && !toHide) {
          actions.push({
            showEngagementPanelEndpoint: {
              panelIdentifier: panelId,
            },
          });
        }

        if (actions.length > 0) {
          const cnt = insp(ytdFlexyElm);

          cnt.resolveCommand(
            {
              signalServiceEndpoint: {
                signal: 'CLIENT_SIGNAL',
                actions: actions,
              },
            },

            {},
            false
          );
        }
        actions = null;
      }
    }

    /*
    function ytBtnCloseEngagementPanel( s) {
      //ePanel.setAttribute('visibility',"ENGAGEMENT_PANEL_VISIBILITY_HIDDEN");
   
      let panelId = s.getAttribute('target-id')
      scriptletDeferred.debounce(() => {
        document.dispatchEvent(new CustomEvent('tyt-engagement-panel-visibility-change', {
          detail: {
            panelId,
            toHide: true
          }
        }))
      })
   
    }
    
    function ytBtnCloseEngagementPanels() {
      if (isEngagementPanelExpanded()) {
        for (const s of qsAll(
          `ytd-watch-flexy[tyt-tab] #panels.ytd-watch-flexy ytd-engagement-panel-section-list-renderer[target-id][visibility]:not([hidden])`
        )) {
          if (s.getAttribute('visibility') == "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED") ytBtnCloseEngagementPanel(s);
        }
      }
    }
    */

    function ytBtnCloseEngagementPanels() {
      const actions = [];
      for (const panelElm of qsAll(
        `ytd-watch-flexy[tyt-tab] #panels.ytd-watch-flexy ytd-engagement-panel-section-list-renderer[target-id][visibility]:not([hidden])`
      )) {
        if (
          panelElm.getAttribute('visibility') === 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED' &&
          !panelElm.closest('[hidden]')
        ) {
          actions.push({
            panelId: panelElm.getAttribute000('target-id'),
            toHide: true,
          });
        }
      }
      ytBtnEgmPanelCore(actions);
    }

    function ytBtnOpenPlaylist() {
      const cnt = insp(elements.playlist);
      if (cnt && typeof cnt.collapsed === 'boolean') {
        cnt.collapsed = false;
      }
    }
    function ytBtnClosePlaylist() {
      const cnt = insp(elements.playlist);
      if (cnt && typeof cnt.collapsed === 'boolean') {
        cnt.collapsed = true;
      }
    }

    const updateChatLocation498 = function () {
      if (this.is !== 'ytd-watch-grid') {
        secondaryInnerFn(() => {
          this.updatePageMediaQueries();
          this.schedulePlayerSizeUpdate_();
        });
      }
    };

    const mirrorNodeWS = new WeakMap();

    const dummyNode = document.createElement('noscript');

    const __j4836__ = Symbol();
    const __j5744__ = Symbol(); // original element
    const __j5733__ = Symbol(); // __lastChanged__

    const monitorDataChangedByDOMMutation = async function (_mutations) {
      const nodeWR = this;
      const node = kRef(nodeWR);
      if (!node) return;

      const cnt = insp(node);
      const __lastChanged__ = cnt[__j5733__];

      const val = cnt.data ? cnt.data[__j4836__] || 1 : 0;

      if (__lastChanged__ !== val) {
        cnt[__j5733__] = val > 0 ? (cnt.data[__j4836__] = Date.now()) : 0;

        await Promise.resolve(); // required for making sufficient delay for data rendering
        attributeInc(node, 'tyt-data-change-counter'); // next macro task
      }
    };

    const moChangeReflection = function (mutations) {
      const nodeWR = this;
      const node = kRef(nodeWR);
      if (!node) return;
      const originElement = kRef(node[__j5744__] || null) || null;
      if (!originElement) return;

      const cnt = insp(node);
      const oriCnt = insp(originElement);

      if (mutations) {
        let bfDataChangeCounter = false;
        for (const mutation of mutations) {
          if (
            mutation.attributeName === 'tyt-clone-refresh-count' &&
            mutation.target === originElement
          ) {
            bfDataChangeCounter = true;
          } else if (
            mutation.attributeName === 'tyt-data-change-counter' &&
            mutation.target === originElement
          ) {
            bfDataChangeCounter = true;
          }
        }
        if (bfDataChangeCounter && oriCnt.data) {
          node.replaceWith(dummyNode);
          cnt.data = Object.assign({}, oriCnt.data);
          dummyNode.replaceWith(node);
        }
      }
    };

    const attributeInc = (elm, prop) => {
      let v = (+elm.getAttribute000(prop) || 0) + 1;
      if (v > 1e9) v = 9;
      elm.setAttribute000(prop, v);
      return v;
    };

    /**
     * UC[-_a-zA-Z0-9+=.]{22}
     * https://support.google.com/youtube/answer/6070344?hl=en
     * The channel ID is the 24 character alphanumeric string that starts with 'UC' in the channel URL.
     */

    const isChannelId = x => {
      if (typeof x === 'string' && x.length === 24) {
        return /UC[-_a-zA-Z0-9+=.]{22}/.test(x);
      }
      return false;
    };

    const infoFix = lockId => {
      if (lockId !== null && lockGet['infoFixLock'] !== lockId) return;
      // console.log('((infoFix))')
      const infoExpander = elements.infoExpander;
      const infoContainer = (infoExpander ? infoExpander.parentNode : null) || qs('#tab-info');
      const ytdFlexyElm = elements.flexy;
      if (!infoContainer || !ytdFlexyElm) return;
      // console.log(386, infoExpander, infoExpander.matches('#tab-info > [class]'))
      if (infoExpander) {
        const match =
          infoExpander.matches('#tab-info > [class]') ||
          infoExpander.matches('#tab-info > [tyt-main-info]');
        if (!match) return;
      }
      // const elms = [...document.querySelectorAll('ytd-watch-metadata.ytd-watch-flexy div[slot="extra-content"], ytd-watch-metadata.ytd-watch-flexy ytd-metadata-row-container-renderer')].filter(elm=>{
      //   if(elm.parentNode.closest('div[slot="extra-content"], ytd-metadata-row-container-renderer')) return false;
      //    return true;
      // });

      const requireElements = [
        ...qsAll(
          'ytd-watch-metadata.ytd-watch-flexy div[slot="extra-content"] > *, ytd-watch-metadata.ytd-watch-flexy #extra-content > *'
        ),
      ]
        .filter(elm => {
          return typeof elm.is == 'string';
        })
        .map(elm => {
          const is = elm.is;
          while (elm instanceof HTMLElement_) {
            const q = [...elm.querySelectorAll(is)].filter(e => insp(e).data);
            if (q.length >= 1) return q[0];
            elm = elm.parentNode;
          }
        })
        .filter(elm => !!elm && typeof elm.is === 'string');
      // console.log(9162, requireElements)

      // if (!infoExpander && !requireElements.length) return;

      const source = requireElements.map(entry => {
        const inst = insp(entry);
        return {
          data: inst.data,
          tag: inst.is,
          elm: entry,
        };
      });

      let noscript_ = qs('noscript#aythl');
      if (!noscript_) {
        noscript_ = document.createElement('noscript');
        noscript_.id = 'aythl';

        inPageRearrange = true;
        ytdFlexyElm.insertBefore000(noscript_, ytdFlexyElm.firstChild);
        inPageRearrange = false;
      }
      const noscript = noscript_;

      let requiredUpdate = false;
      const mirrorElmSet = new Set();
      const targetParent = infoContainer;
      for (const { data, tag: tag, elm: s } of source) {
        let mirrorNode = mirrorNodeWS.get(s);
        mirrorNode = mirrorNode ? kRef(mirrorNode) : mirrorNode;
        if (!mirrorNode) {
          const cnt = insp(s);
          const cProto = cnt.constructor.prototype;

          const element = document.createElement(tag);
          noscript.appendChild(element); // appendChild to trigger .attached()
          mirrorNode = element;
          mirrorNode[__j5744__] = mWeakRef(s);

          const nodeWR = mWeakRef(mirrorNode);
          // if(!(insp(s)._dataChanged438)){
          //   insp(s)._dataChanged438 = async function(){

          //     await Promise.resolve(); // required for making sufficient delay for data rendering
          //     attributeInc(originElement, 'tyt-data-change-counter'); // next macro task
          //     moChangeReflection.call(nodeWR);
          //   }
          // }

          new MutationObserver(moChangeReflection.bind(nodeWR)).observe(s, {
            attributes: true,
            attributeFilter: ['tyt-clone-refresh-count', 'tyt-data-change-counter'],
          });

          s.jy8432 = 1;
          if (
            !(cProto instanceof Node) &&
            !cProto._dataChanged496 &&
            typeof cProto._createPropertyObserver === 'function'
          ) {
            cProto._dataChanged496 = function () {
              const cnt = this;
              const node = cnt.hostElement || cnt;
              if (node.jy8432) {
                // console.log('hello _dataChanged496', this.is);
                // await Promise.resolve(); // required for making sufficient delay for data rendering
                attributeInc(node, 'tyt-data-change-counter'); // next macro task
              }
            };

            cProto._createPropertyObserver('data', '_dataChanged496', undefined);
          } else if (
            !(cProto instanceof Node) &&
            !cProto._dataChanged496 &&
            cProto.useSignals === true &&
            insp(s).signalProxy
          ) {
            const dataSignal = cnt?.signalProxy?.signalCache?.data;
            if (
              dataSignal &&
              typeof dataSignal.setWithPath === 'function' &&
              !dataSignal.setWithPath573 &&
              !dataSignal.controller573
            ) {
              dataSignal.controller573 = mWeakRef(cnt);
              dataSignal.setWithPath573 = dataSignal.setWithPath;
              dataSignal.setWithPath = function () {
                const cnt = kRef(this.controller573 || null) || null;
                cnt &&
                  typeof cnt._dataChanged496k === 'function' &&
                  Promise.resolve(cnt).then(cnt._dataChanged496k).catch(console.warn);
                return this.setWithPath573(...arguments);
              };
              cProto._dataChanged496 = function () {
                const cnt = this;
                const node = cnt.hostElement || cnt;
                if (node.jy8432) {
                  // console.log('hello _dataChanged496', this.is);
                  // await Promise.resolve(); // required for making sufficient delay for data rendering
                  attributeInc(node, 'tyt-data-change-counter'); // next macro task
                }
              };
              cProto._dataChanged496k = cnt => cnt._dataChanged496();
            }
          }

          if (!cProto._dataChanged496) {
            new MutationObserver(
              monitorDataChangedByDOMMutation.bind(mirrorNode[__j5744__])
            ).observe(s, { attributes: true, childList: true, subtree: true });
          }

          // new MutationObserver(moChangeReflection.bind(nodeWR)).observe(s, {attributes: true, childList: true, subtree: true});

          mirrorNodeWS.set(s, nodeWR);
          requiredUpdate = true;
        } else {
          if (mirrorNode.parentNode !== targetParent) {
            requiredUpdate = true;
          }
        }
        if (!requiredUpdate) {
          const cloneNodeCnt = insp(mirrorNode);
          if (cloneNodeCnt.data !== data) {
            // if(mirrorNode.parentNode !== noscript){
            //   noscript.appendChild(mirrorNode);
            // }
            // mirrorNode.replaceWith(dummyNode);
            // cloneNodeCnt.data = data;
            // dummyNode.replaceWith(mirrorNode);
            requiredUpdate = true;
          }
        }

        mirrorElmSet.add(mirrorNode);
        source.mirrored = mirrorNode;
      }

      const mirroElmArr = [...mirrorElmSet];
      mirrorElmSet.clear();

      if (!requiredUpdate) {
        let e = infoExpander ? -1 : 0;
        // DOM Tree Check
        for (let n = targetParent.firstChild; n instanceof Node; n = n.nextSibling) {
          const target = e < 0 ? infoExpander : mirroElmArr[e];
          e++;
          if (n !== target) {
            // target can be undefined if index overflow
            requiredUpdate = true;
            break;
          }
        }
        if (!requiredUpdate && e !== mirroElmArr.length + 1) requiredUpdate = true;
      }

      if (requiredUpdate) {
        if (infoExpander) {
          targetParent.assignChildren111(null, infoExpander, mirroElmArr);
        } else {
          targetParent.replaceChildren000(...mirroElmArr);
        }
        for (const mirrorElm of mirroElmArr) {
          // trigger data assignment and record refresh count by manual update
          const j = attributeInc(mirrorElm, 'tyt-clone-refresh-count');
          const oriElm = kRef(mirrorElm[__j5744__] || null) || null;
          if (oriElm) {
            oriElm.setAttribute111('tyt-clone-refresh-count', j);
          }
        }
      }

      mirroElmArr.length = 0;
      source.length = 0;
    };

    const layoutFix = lockId => {
      if (lockGet['layoutFixLock'] !== lockId) return;
      // console.log('((layoutFix))')

      const secondaryWrapper = qs(
        '#secondary-inner.style-scope.ytd-watch-flexy > secondary-wrapper'
      );
      // console.log(3838, !!chatContainer, !!(secondaryWrapper && secondaryInner), secondaryInner?.firstChild, secondaryInner?.lastChild , secondaryWrapper?.parentNode === secondaryInner)
      if (secondaryWrapper) {
        const secondaryInner = secondaryWrapper.parentNode;

        const chatContainer = qs('#columns.style-scope.ytd-watch-flexy [tyt-chat-container]');

        const hasExtraNodes = () => {
          for (let node = secondaryInner.firstChild; node; node = node.nextSibling) {
            if (node === secondaryWrapper) continue;
            if (node === chatContainer) continue;
            if (node.nodeType === 3 && !node.textContent.trim()) continue; // ignore whitespace
            return true;
          }
          return false;
        };

        if (hasExtraNodes() || (chatContainer && !chatContainer.closest('secondary-wrapper'))) {
          // console.log(38381)
          const w = [];
          const w2 = [];
          for (
            let node = secondaryInner.firstChild;
            node instanceof Node;
            node = node.nextSibling
          ) {
            if (node === chatContainer && chatContainer) {
            } else if (node === secondaryWrapper) {
              for (
                let node2 = secondaryWrapper.firstChild;
                node2 instanceof Node;
                node2 = node2.nextSibling
              ) {
                if (node2 === chatContainer && chatContainer) {
                } else {
                  if (node2.id === 'right-tabs' && chatContainer) {
                    w2.push(chatContainer);
                  }
                  w2.push(node2);
                }
              }
            } else {
              w.push(node);
            }
          }
          // console.log('qww', w, w2)

          inPageRearrange = true;
          secondaryWrapper.replaceChildren000(...w, ...w2);
          inPageRearrange = false;
          const chatElm = elements.chat;
          const chatCnt = insp(chatElm);
          if (
            chatCnt &&
            typeof chatCnt.urlChanged === 'function' &&
            secondaryWrapper.contains(chatElm)
          ) {
            // setTimeout(() => chatCnt.urlChanged, 136);
            if (typeof chatCnt.urlChangedAsync12 === 'function') {
              DEBUG_5085 && console.log('elements.chat urlChangedAsync12', 61);
              chatCnt.urlChanged();
            } else {
              DEBUG_5085 && console.log('elements.chat urlChangedAsync12', 62);
              setTimeout(() => chatCnt.urlChanged(), 136);
            }
          }
        }
      }
    };

    let lastPanel = '';
    let lastTab = '';
    // let fixInitialTabState = 0;

    let egmPanelsDebounceTimer = null;
    const aoEgmPanels = new MutationObserver(() => {
      // console.log(5094,3);
      if (egmPanelsDebounceTimer) return;
      egmPanelsDebounceTimer = setTimeout(() => {
        egmPanelsDebounceTimer = null;
        Promise.resolve(lockSet['updateEgmPanelsLock']).then(updateEgmPanels).catch(console.warn);
      }, 16); // ~60fps debounce
    });

    const removeKeepCommentsScroller = async lockId => {
      if (lockGet['removeKeepCommentsScrollerLock'] !== lockId) return;
      await Promise.resolve();
      if (lockGet['removeKeepCommentsScrollerLock'] !== lockId) return;
      const ytdFlexyFlm = elements.flexy;
      if (ytdFlexyFlm) {
        ytdFlexyFlm.removeAttribute000('keep-comments-scroller');
      }
    };

    const egmPanelsCache = new Set();

    const updateEgmPanels = async lockId => {
      if (lockId !== lockGet['updateEgmPanelsLock']) return;
      await navigateFinishedPromise.then().catch(console.warn);
      if (lockId !== lockGet['updateEgmPanelsLock']) return;
      // console.log('updateEgmPanels::called');
      const ytdFlexyElm = elements.flexy;
      if (!ytdFlexyElm) return;
      let newVisiblePanels = [];
      let newHiddenPanels = [];
      let allVisiblePanels = [];
      const panels = egmPanelsCache;

      for (const panelElm of panels) {
        if (!panelElm.isConnected) {
          egmPanelsCache.delete(panelElm);
          continue;
        }
        const visibility = panelElm.getAttribute000('visibility');

        if (visibility === 'ENGAGEMENT_PANEL_VISIBILITY_HIDDEN' || panelElm.closest('[hidden]')) {
          if (panelElm.hasAttribute000('tyt-visible-at')) {
            panelElm.removeAttribute000('tyt-visible-at');
            newHiddenPanels.push(panelElm);
          }
        } else if (
          visibility === 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED' &&
          !panelElm.closest('[hidden]')
        ) {
          const visibleAt = panelElm.getAttribute000('tyt-visible-at');
          if (!visibleAt) {
            panelElm.setAttribute111('tyt-visible-at', Date.now());
            newVisiblePanels.push(panelElm);
          }
          allVisiblePanels.push(panelElm);
        }
      }
      if (newVisiblePanels.length >= 1 && allVisiblePanels.length >= 2) {
        const targetVisible = newVisiblePanels[newVisiblePanels.length - 1];

        const actions = [];
        for (const panelElm of allVisiblePanels) {
          if (panelElm === targetVisible) continue;
          actions.push({
            panelId: panelElm.getAttribute000('target-id'),
            toHide: true,
          });
        }

        if (actions.length >= 1) {
          ytBtnEgmPanelCore(actions);
        }
      }
      if (allVisiblePanels.length >= 1) {
        ytdFlexyElm.setAttribute111('tyt-egm-panel_', '');
      } else {
        ytdFlexyElm.removeAttribute000('tyt-egm-panel_');
      }
      newVisiblePanels.length = 0;
      newVisiblePanels = null;
      newHiddenPanels.length = 0;
      newHiddenPanels = null;
      allVisiblePanels.length = 0;
      allVisiblePanels = null;
    };

    const checkElementExist = (css, exclude) => {
      const elms = window.YouTubeDOMCache
        ? window.YouTubeDOMCache.querySelectorAll(css, document)
        : qsAll(css);
      for (const p of elms) {
        if (!p.closest(exclude)) return p;
      }
      return null;
    };

    let fixInitialTabStateK = 0;

    const { handleNavigateFactory } = (() => {
      let isLoadStartListened = false;

      function findLcComment(lc) {
        if (arguments.length === 1) {
          const element = qs(
            `#tab-comments ytd-comments ytd-comment-renderer #header-author a[href*="lc=${lc}"]`
          );
          if (element) {
            const commentRendererElm = closestFromAnchor.call(element, 'ytd-comment-renderer');
            if (commentRendererElm && lc) {
              return {
                lc,
                commentRendererElm,
              };
            }
          }
        } else if (arguments.length === 0) {
          const element = qs(
            `#tab-comments ytd-comments ytd-comment-renderer > #linked-comment-badge span:not(:empty)`
          );
          if (element) {
            const commentRendererElm = closestFromAnchor.call(element, 'ytd-comment-renderer');
            if (commentRendererElm) {
              const header = _querySelector.call(commentRendererElm, '#header-author');
              if (header) {
                const anchor = _querySelector.call(header, 'a[href*="lc="]');
                if (anchor) {
                  const href = anchor.getAttribute('href') || '';
                  const m = /[&?]lc=([\w_.-]+)/.exec(href); // dot = sub-comment
                  if (m) {
                    lc = m[1];
                  }
                }
              }
            }
            if (commentRendererElm && lc) {
              return {
                lc,
                commentRendererElm,
              };
            }
          }
        }

        return null;
      }

      function lcSwapFuncA(targetLcId, currentLcId) {
        let done = 0;
        try {
          // console.log(currentLcId, targetLcId)

          const r1 = findLcComment(currentLcId).commentRendererElm;
          const r2 = findLcComment(targetLcId).commentRendererElm;

          if (
            typeof insp(r1).data.linkedCommentBadge === 'object' &&
            typeof insp(r2).data.linkedCommentBadge === 'undefined'
          ) {
            const p = Object.assign({}, insp(r1).data.linkedCommentBadge);

            if (((p || 0).metadataBadgeRenderer || 0).trackingParams) {
              delete p.metadataBadgeRenderer.trackingParams;
            }

            const v1 = findContentsRenderer(r1);
            const v2 = findContentsRenderer(r2);

            if (
              v1.parent === v2.parent &&
              (v2.parent.nodeName === 'YTD-COMMENTS' ||
                v2.parent.nodeName === 'YTD-ITEM-SECTION-RENDERER')
            ) {
            } else {
              // currently not supported
              return false;
            }

            if (v2.index >= 0) {
              if (v2.parent.nodeName === 'YTD-COMMENT-REPLIES-RENDERER') {
                if (lcSwapFuncB(targetLcId, currentLcId, p)) {
                  done = 1;
                }

                done = 1;
              } else {
                const v2pCnt = insp(v2.parent);
                const v2Conents = (v2pCnt.data || 0).contents || 0;
                if (!v2Conents) console.warn('v2Conents is not found');

                v2pCnt.data = Object.assign({}, v2pCnt.data, {
                  contents: [].concat(
                    [v2Conents[v2.index]],
                    v2Conents.slice(0, v2.index),
                    v2Conents.slice(v2.index + 1)
                  ),
                });

                if (lcSwapFuncB(targetLcId, currentLcId, p)) {
                  done = 1;
                }
              }
            }
          }
        } catch (e) {
          console.warn(e);
        }
        return done === 1;
      }

      function lcSwapFuncB(targetLcId, currentLcId, _p) {
        let done = 0;
        try {
          const r1 = findLcComment(currentLcId).commentRendererElm;
          const r1cnt = insp(r1);
          const r2 = findLcComment(targetLcId).commentRendererElm;
          const r2cnt = insp(r2);

          const r1d = r1cnt.data;
          const p = Object.assign({}, _p);
          r1d.linkedCommentBadge = null;
          delete r1d.linkedCommentBadge;

          const q = Object.assign({}, r1d);
          q.linkedCommentBadge = null;
          delete q.linkedCommentBadge;

          r1cnt.data = Object.assign({}, q);
          r2cnt.data = Object.assign({}, r2cnt.data, { linkedCommentBadge: p });

          done = 1;
        } catch (e) {
          console.warn(e);
        }
        return done === 1;
      }

      const loadStartFx = async evt => {
        const media = (evt || 0).target || 0;
        if (media.nodeName === 'VIDEO' || media.nodeName === 'AUDIO') {
        } else return;

        const newMedia = media;

        const media1 = common.getMediaElement(0); // document.querySelector('#movie_player video[src]');
        const media2 = common.getMediaElements(2); // document.querySelectorAll('ytd-browse[role="main"] video[src]');

        if (media1 !== null && media2.length > 0) {
          if (newMedia !== media1 && media1.paused === false) {
            if (isVideoPlaying(media1)) {
              Promise.resolve(newMedia)
                .then(video => video.paused === false && video.pause())
                .catch(console.warn);
            }
          } else if (newMedia === media1) {
            for (const s of media2) {
              if (s.paused === false) {
                Promise.resolve(s)
                  .then(s => s.paused === false && s.pause())
                  .catch(console.warn);
                break;
              }
            }
          } else {
            Promise.resolve(media1)
              .then(video1 => video1.paused === false && video1.pause())
              .catch(console.warn);
          }
        }
      };

      const getBrowsableEndPoint = req => {
        let valid = false;
        let endpoint = req ? req.command : null;
        if (
          endpoint &&
          (endpoint.commandMetadata || 0).webCommandMetadata &&
          endpoint.watchEndpoint
        ) {
          const videoId = endpoint.watchEndpoint.videoId;
          const url = endpoint.commandMetadata.webCommandMetadata.url;

          if (typeof videoId === 'string' && typeof url === 'string' && url.indexOf('lc=') > 0) {
            const m = /^\/watch\?v=([\w_-]+)&lc=([\w_.-]+)$/.exec(url); // dot = sub-comment
            if (m && m[1] === videoId) {
              /*
              {
                "style": "BADGE_STYLE_TYPE_SIMPLE",
                "label": "注目のコメント",
                "trackingParams": "XXXXXX"
            }
              */

              const targetLc = findLcComment(m[2]);
              const currentLc = targetLc ? findLcComment() : null;

              if (targetLc && currentLc) {
                const done =
                  targetLc.lc === currentLc.lc ? 1 : lcSwapFuncA(targetLc.lc, currentLc.lc) ? 1 : 0;

                if (done === 1) {
                  common.xReplaceState(history.state, url);
                  return;
                }
              }
            }
          }
        }

        /*
            
            {
              "type": 0,
              "command": endpoint,
              "form": {
                "tempData": {},
                "reload": false
              }
            }
  
        */

        if (
          endpoint &&
          (endpoint.commandMetadata || 0).webCommandMetadata &&
          endpoint.browseEndpoint &&
          isChannelId(endpoint.browseEndpoint.browseId)
        ) {
          valid = true;
        } else if (
          endpoint &&
          (endpoint.browseEndpoint || endpoint.searchEndpoint) &&
          !endpoint.urlEndpoint &&
          !endpoint.watchEndpoint
        ) {
          if (endpoint.browseEndpoint && endpoint.browseEndpoint.browseId === 'FEwhat_to_watch') {
            // valid = false;
            const playerMedia = common.getMediaElement(1);
            if (playerMedia && playerMedia.paused === false) valid = true; // home page
          } else if (endpoint.commandMetadata && endpoint.commandMetadata.webCommandMetadata) {
            const meta = endpoint.commandMetadata.webCommandMetadata;
            if (meta && /*meta.apiUrl &&*/ meta.url && meta.webPageType) {
              valid = true;
            }
          }
        }

        if (!valid) endpoint = null;

        return endpoint;
      };

      const shouldUseMiniPlayer = () => {
        const isSubTypeExist = qs('ytd-page-manager#page-manager > ytd-browse[page-subtype]');

        if (isSubTypeExist) return true;

        const movie_player = qsAll('#movie_player').filter(e => !e.closest('[hidden]'))[0];
        if (movie_player) {
          const media = qsOne(movie_player, 'video[class], audio[class]');
          if (
            media &&
            media.currentTime > 3 &&
            media.duration - media.currentTime > 3 &&
            media.paused === false
          ) {
            return true;
          }
        }
        return false;
        // return true;
        // return !!document.querySelector('ytd-page-manager#page-manager > ytd-browse[page-subtype]');
      };

      const conditionFulfillment = req => {
        const command = req ? req.command : null;
        DEBUG_handleNavigateFactory && console.log('handleNavigateFactory - 0801', command);
        if (!command) return;

        if (command && (command.commandMetadata || 0).webCommandMetadata && command.watchEndpoint) {
        } else if (
          command &&
          (command.commandMetadata || 0).webCommandMetadata &&
          command.browseEndpoint &&
          isChannelId(command.browseEndpoint.browseId)
        ) {
        } else if (
          command &&
          (command.browseEndpoint || command.searchEndpoint) &&
          !command.urlEndpoint &&
          !command.watchEndpoint
        ) {
        } else {
          return false;
        }

        DEBUG_handleNavigateFactory && console.log('handleNavigateFactory - 0802');
        if (!shouldUseMiniPlayer()) return false;
        DEBUG_handleNavigateFactory && console.log('handleNavigateFactory - 0803');

        /*
          // user would like to switch page immediately without playing the video;
          // attribute appear after playing video for more than 2s
          if (!document.head.dataset.viTime) return false;
          else {
            let currentVideo = common.getMediaElement(0);
            if (currentVideo && currentVideo.readyState > currentVideo.HAVE_CURRENT_DATA && currentVideo.currentTime > 2.2 && currentVideo.duration - 2.2 < currentVideo.currentTime) {
              // disable miniview browsing if the media is near to the end
              return false;
            }
          }
        */

        if (pageType !== 'watch') return false;

        DEBUG_handleNavigateFactory && console.log('handleNavigateFactory - 0804');

        // 2025.10.16 - ignore ytp-miniplayer-button existance
        // if (!checkElementExist('ytd-watch-flexy #player button.ytp-miniplayer-button.ytp-button', '[hidden]')) {
        //   return false;
        // }

        // DEBUG_handleNavigateFactory && console.log("handleNavigateFactory - 0805");

        return true;
      };

      let u38 = 0;
      const fixChannelAboutPopup = async t38 => {
        let promise = new PromiseExternal();
        const f = () => {
          promise && promise.resolve();
          promise = null;
        };
        document.addEventListener('yt-navigate-finish', f, false);
        await promise.then();
        promise = null;
        document.removeEventListener('yt-navigate-finish', f, false);
        if (t38 !== u38) return;
        setTimeout(() => {
          const currentAbout = qsAll('ytd-about-channel-renderer').filter(
            e => !e.closest('[hidden]')
          )[0];
          let okay = false;
          if (!currentAbout) okay = true;
          else {
            const popupContainer = currentAbout.closest('ytd-popup-container');
            if (popupContainer) {
              const cnt = insp(popupContainer);
              let arr = null;
              try {
                arr = cnt.handleGetOpenedPopupsAction_();
              } catch {}
              if (arr && arr.length === 0) okay = true;
            } else {
              okay = false;
            }
          }
          if (okay) {
            const descriptionModel = [...qsAll('yt-description-preview-view-model')].filter(
              e => !e.closest('[hidden]')
            )[0];
            if (descriptionModel) {
              const button = [...descriptionModel.querySelectorAll('button')].filter(
                e => !e.closest('[hidden]') && `${e.textContent}`.trim().length > 0
              )[0];
              if (button) {
                button.click();
              }
            }
          }
        }, 80);
      };
      const handleNavigateFactory = handleNavigate => {
        return function (req) {
          if (u38 > 1e9) u38 = 9;
          const t38 = ++u38;

          const $this = this;
          const $arguments = arguments;

          let endpoint = null;

          if (conditionFulfillment(req)) {
            endpoint = getBrowsableEndPoint(req);

            DEBUG_handleNavigateFactory &&
              console.log('handleNavigateFactory - 1000', req, endpoint);
          }

          DEBUG_handleNavigateFactory && console.log('handleNavigateFactory - 1001', req, endpoint);

          if (!endpoint || !shouldUseMiniPlayer()) return handleNavigate.apply($this, $arguments);

          // console.log('tabview-script-handleNavigate')

          const ytdAppElm = qs('ytd-app');
          const ytdAppCnt = insp(ytdAppElm);

          let object = null;
          try {
            object = ytdAppCnt.data.response.currentVideoEndpoint.watchEndpoint || null;
          } catch {
            object = null;
          }

          DEBUG_handleNavigateFactory && console.log('handleNavigateFactory - 1002', object);

          if (typeof object !== 'object') object = null;

          const once = { once: true }; // browsers supporting async function can also use once option.

          if (object !== null && !('playlistId' in object)) {
            DEBUG_handleNavigateFactory && console.log('handleNavigateFactory - 1003', object);

            let wObject = mWeakRef(object);

            const N = 3;

            let count = 0;

            /*
              
              rcb(b) => a = playlistId = undefinded
              
              var scb = function(a, b, c, d) {
                      a.isInitialized() && (B("kevlar_miniplayer_navigate_to_shorts_killswitch") ? c || d ? ("watch" !== Xu(b) && "shorts" !== Xu(b) && os(a.miniplayerEl, "yt-cache-miniplayer-page-action", [b]),
                      qs(a.miniplayerEl, "yt-deactivate-miniplayer-action")) : "watch" === Xu(b) && rcb(b) && (qt.getInstance().playlistWatchPageActivation = !0,
                      a.activateMiniplayer(b)) : c ? ("watch" !== Xu(b) && os(a.miniplayerEl, "yt-cache-miniplayer-page-action", [b]),
                      qs(a.miniplayerEl, "yt-deactivate-miniplayer-action")) : d ? qs(a.miniplayerEl, "yt-pause-miniplayer-action") : "watch" === Xu(b) && rcb(b) && (qt.getInstance().playlistWatchPageActivation = !0,
                      a.activateMiniplayer(b)))
                  };
    
            */

            Object.defineProperty(kRef(wObject) || {}, 'playlistId', {
              get() {
                DEBUG_handleNavigateFactory && console.log('handleNavigateFactory - get', count);
                count++;
                if (count === N) {
                  delete this.playlistId;
                }
                return '*';
              },
              set(value) {
                DEBUG_handleNavigateFactory &&
                  console.log('handleNavigateFactory - set', count, value);
                delete this.playlistId; // remove property definition
                this.playlistId = value; // assign as normal property
              },
              enumerable: false,
              configurable: true,
            });

            let playlistClearout = null;

            let timeoutid = 0;
            Promise.race([
              new Promise(r => {
                timeoutid = setTimeout(r, 4000);
              }),
              new Promise(r => {
                playlistClearout = () => {
                  if (timeoutid > 0) {
                    clearTimeout(timeoutid);
                    timeoutid = 0;
                  }
                  r();
                };
                document.addEventListener('yt-page-type-changed', playlistClearout, once);
              }),
            ])
              .then(() => {
                if (timeoutid !== 0) {
                  playlistClearout &&
                    document.removeEventListener('yt-page-type-changed', playlistClearout, once);
                  timeoutid = 0;
                }
                playlistClearout = null;
                count = N - 1;
                const object = kRef(wObject);
                wObject = null;
                return object ? object.playlistId : null;
              })
              .catch(console.warn);
          }

          if (!isLoadStartListened) {
            isLoadStartListened = true;
            document.addEventListener('loadstart', loadStartFx, true);
          }

          const endpointURL = `${endpoint?.commandMetadata?.webCommandMetadata?.url || ''}`;

          if (
            endpointURL &&
            endpointURL.endsWith('/about') &&
            /\/channel\/UC[-_a-zA-Z0-9+=.]{22}\/about/.test(endpointURL)
          ) {
            fixChannelAboutPopup(t38);
          }

          handleNavigate.apply($this, $arguments);
        };
      };

      return { handleNavigateFactory };
    })();

    const common = (() => {
      let mediaModeLock = 0;
      const _getMediaElement = i => {
        if (mediaModeLock === 0) {
          const e =
            qs('.video-stream.html5-main-video') ||
            qs('#movie_player video, #movie_player audio') ||
            qs('body video[src], body audio[src]');
          if (e) {
            if (e.nodeName === 'VIDEO') mediaModeLock = 1;
            else if (e.nodeName === 'AUDIO') mediaModeLock = 2;
          }
        }
        if (!mediaModeLock) return null;
        if (mediaModeLock === 1) {
          switch (i) {
            case 1:
              return 'ytd-player#ytd-player video[src]';
            case 2:
              return 'ytd-browse[role="main"] video[src]';
            case 0:
            default:
              return '#movie_player video[src]';
          }
        } else if (mediaModeLock === 2) {
          switch (i) {
            case 1:
              return 'ytd-player#ytd-player audio.video-stream.html5-main-video[src]';
            case 2:
              return 'ytd-browse[role="main"] audio.video-stream.html5-main-video[src]';
            case 0:
            default:
              return '#movie_player audio.video-stream.html5-main-video[src]';
          }
        }
        return null;
      };

      return {
        xReplaceState(s, u) {
          try {
            history.replaceState(s, '', u);
          } catch {
            // in case error occurs if replaceState is replaced by any external script / extension
          }
          if (s.endpoint) {
            try {
              const ytdAppElm = qs('ytd-app');
              const ytdAppCnt = insp(ytdAppElm);
              ytdAppCnt.replaceState(s.endpoint, '', u);
            } catch {}
          }
        },
        getMediaElement(i) {
          const s = _getMediaElement(i) || '';
          if (s) return qs(s);
          return null;
        },
        getMediaElements(i) {
          const s = _getMediaElement(i) || '';
          if (s) return qsAll(s);
          return [];
        },
      };
    })();

    let inPageRearrange = false;
    let tmpLastVideoId = '';
    // const nsMap = new Map();

    const getCurrentVideoId = () => {
      const ytdFlexyElm = elements.flexy;
      const ytdFlexyCnt = insp(ytdFlexyElm);
      if (ytdFlexyCnt && typeof ytdFlexyCnt.videoId === 'string') return ytdFlexyCnt.videoId;
      if (ytdFlexyElm && typeof ytdFlexyElm.videoId === 'string') return ytdFlexyElm.videoId;
      return '';
    };

    const _holdInlineExpanderAlwaysExpanded = inlineExpanderCnt => {
      if (inlineExpanderCnt.alwaysShowExpandButton === true) {
        inlineExpanderCnt.alwaysShowExpandButton = false;
      }
      if (typeof (inlineExpanderCnt.collapseLabel || 0) === 'string') {
        inlineExpanderCnt.collapseLabel = '';
      }
      if (typeof (inlineExpanderCnt.expandLabel || 0) === 'string') {
        inlineExpanderCnt.expandLabel = '';
      }
      if (inlineExpanderCnt.showCollapseButton === true) {
        inlineExpanderCnt.showCollapseButton = false;
      }
      if (inlineExpanderCnt.showExpandButton === true) inlineExpanderCnt.showExpandButton = false;
      if (inlineExpanderCnt.expandButton instanceof HTMLElement_) {
        inlineExpanderCnt.expandButton = null;
        inlineExpanderCnt.expandButton.remove();
      }
    };

    const fixInlineExpanderDisplay = inlineExpanderCnt => {
      try {
        inlineExpanderCnt.updateIsAttributedExpanded();
      } catch (e) {
        // Optional method - may not exist
        DEBUG_5084 && console.debug('[main] updateIsAttributedExpanded not available', e);
      }
      try {
        inlineExpanderCnt.updateIsFormattedExpanded();
      } catch (e) {
        DEBUG_5084 && console.debug('[main] updateIsFormattedExpanded not available', e);
      }
      try {
        inlineExpanderCnt.updateTextOnSnippetTypeChange();
      } catch (e) {
        DEBUG_5084 && console.debug('[main] updateTextOnSnippetTypeChange not available', e);
      }
      try {
        inlineExpanderCnt.updateStyles();
      } catch (e) {
        DEBUG_5084 && console.debug('[main] updateStyles not available', e);
      }
    };

    const setExpand = cnt => {
      if (typeof cnt.set === 'function') {
        cnt.set('isExpanded', true);
        if (typeof cnt.isExpandedChanged === 'function') cnt.isExpandedChanged();
      } else if (cnt.isExpanded === false) {
        cnt.isExpanded = true;
        if (typeof cnt.isExpandedChanged === 'function') cnt.isExpandedChanged();
      }
    };

    const cloneMethods = {
      updateTextOnSnippetTypeChange() {
        if (this.isResetMutation === false) this.isResetMutation = true;
        if (this.isExpanded === true) this.isExpanded = false;
        setExpand(this, true);
        if (this.isResetMutation === false) this.isResetMutation = true;
        try {
          true || (this.isResetMutation && this.mutationCallback());
        } catch (e) {
          console.error(e);
        }
      },
      collapse() {},
      computeExpandButtonOffset() {
        return 0;
      },
      dataChanged() {},
    };
    const fixInlineExpanderMethods = inlineExpanderCnt => {
      if (inlineExpanderCnt && !inlineExpanderCnt.__$$idncjk8487$$__) {
        inlineExpanderCnt.__$$idncjk8487$$__ = true;
        inlineExpanderCnt.dataChanged = cloneMethods.dataChanged;
        inlineExpanderCnt.updateTextOnSnippetTypeChange =
          cloneMethods.updateTextOnSnippetTypeChange;
        if (typeof inlineExpanderCnt.collapse === 'function') {
          inlineExpanderCnt.collapse = cloneMethods.collapse;
        }
        if (typeof inlineExpanderCnt.computeExpandButtonOffset === 'function') {
          inlineExpanderCnt.computeExpandButtonOffset = cloneMethods.computeExpandButtonOffset;
        }
        // inlineExpanderCnt.hasAttributedStringText = true;
        if (typeof inlineExpanderCnt.isResetMutation === 'boolean') {
          inlineExpanderCnt.isResetMutation = true;
        }
        if (typeof inlineExpanderCnt.collapseLabel === 'string') {
          inlineExpanderCnt.collapseLabel = '';
        }
        fixInlineExpanderDisplay(inlineExpanderCnt); // do the initial fix
      }
    };

    const fixInlineExpanderContent = () => {
      // console.log(21886,1)
      const mainInfo = getMainInfo();
      if (!mainInfo) return;
      // console.log(21886,2)
      const inlineExpanderElm = mainInfo.querySelector('ytd-text-inline-expander');
      const inlineExpanderCnt = insp(inlineExpanderElm);
      fixInlineExpanderMethods(inlineExpanderCnt);

      // console.log(21886, 3)
      // if (inlineExpanderCnt && inlineExpanderCnt.isExpanded === true && plugin.autoExpandInfoDesc.activated) {
      //   // inlineExpanderCnt.isExpandedChanged();
      //   // holdInlineExpanderAlwaysExpanded(inlineExpanderCnt);
      // }
      // if(inlineExpanderCnt){
      //   // console.log(21886,4, inlineExpanderCnt.isExpanded, inlineExpanderCnt.isTruncated)
      //   if (inlineExpanderCnt.isExpanded === false && inlineExpanderCnt.isTruncated === true) {
      //     // console.log(21881)
      //     inlineExpanderCnt.isTruncated = false;
      //   }
      // }
    };

    const plugin = {
      minibrowser: {
        activated: false,
        toUse: true, // depends on shouldUseMiniPlayer()
        activate() {
          if (this.activated) return;

          const isPassiveArgSupport = typeof IntersectionObserver === 'function';
          // https://caniuse.com/?search=observer
          // https://caniuse.com/?search=addEventListener%20passive

          if (!isPassiveArgSupport) return;

          this.activated = true;

          const ytdAppElm = qs('ytd-app');
          const ytdAppCnt = insp(ytdAppElm);

          if (!ytdAppCnt) return;

          const cProto = ytdAppCnt.constructor.prototype;

          if (!cProto.handleNavigate) return;

          if (cProto.handleNavigate.__ma355__) return;

          cProto.handleNavigate = handleNavigateFactory(cProto.handleNavigate);

          cProto.handleNavigate.__ma355__ = 1;
        },
      },
      autoExpandInfoDesc: {
        activated: false,
        toUse: false, // false by default; once the expand is clicked, maintain the feature until the browser is closed.
        /** @type { MutationObserver | null } */
        mo: null,
        promiseReady: new PromiseExternal(),
        moFn(lockId) {
          if (lockGet['autoExpandInfoDescAttrAsyncLock'] !== lockId) return;

          const mainInfo = getMainInfo();

          if (!mainInfo) return;
          switch (((mainInfo || 0).nodeName || '').toLowerCase()) {
            case 'ytd-expander':
              if (mainInfo.hasAttribute000('collapsed')) {
                let success = false;
                try {
                  insp(mainInfo).handleMoreTap(new Event('tap'));
                  success = true;
                } catch {}
                if (success) mainInfo.setAttribute111('tyt-no-less-btn', '');
              }
              break;
            case 'ytd-expandable-video-description-body-renderer':
              const inlineExpanderElm = mainInfo.querySelector('ytd-text-inline-expander');
              const inlineExpanderCnt = insp(inlineExpanderElm);
              if (inlineExpanderCnt && inlineExpanderCnt.isExpanded === false) {
                setExpand(inlineExpanderCnt, true);
                // holdInlineExpanderAlwaysExpanded(inlineExpanderCnt);
              }
              break;
          }
        },
        activate() {
          if (this.activated) return;

          this.moFn = this.moFn.bind(this);
          this.mo = new MutationObserver(() => {
            Promise.resolve(lockSet['autoExpandInfoDescAttrAsyncLock'])
              .then(this.moFn)
              .catch(console.warn);
          });
          this.activated = true;
          this.promiseReady.resolve();
        },
        async onMainInfoSet(mainInfo) {
          await this.promiseReady.then();
          if (mainInfo.nodeName.toLowerCase() === 'ytd-expander') {
            this.mo.observe(mainInfo, {
              attributes: true,
              attributeFilter: ['collapsed', 'attr-8ifv7'],
            });
          } else {
            this.mo.observe(mainInfo, { attributes: true, attributeFilter: ['attr-8ifv7'] });
          }
          mainInfo.incAttribute111('attr-8ifv7');
        },
      },
      fullChannelNameOnHover: {
        activated: false,
        toUse: true,
        /** @type { MutationObserver | null } */
        mo: null,
        /** @type { ResizeObserver | null} */
        ro: null,
        promiseReady: new PromiseExternal(),
        checkResize: 0,
        mouseEnterFn(evt) {
          const target = evt ? evt.target : null;
          if (!(target instanceof HTMLElement_)) return;
          const metaDataElm = target.closest('ytd-watch-metadata');
          metaDataElm.classList.remove('tyt-metadata-hover-resized');
          this.checkResize = Date.now() + 300;
          metaDataElm.classList.add('tyt-metadata-hover');
          // console.log('mouseEnter')
        },
        mouseLeaveFn(evt) {
          const target = evt ? evt.target : null;
          if (!(target instanceof HTMLElement_)) return;
          const metaDataElm = target.closest('ytd-watch-metadata');
          metaDataElm.classList.remove('tyt-metadata-hover-resized');
          metaDataElm.classList.remove('tyt-metadata-hover');
          // console.log('mouseLeaveFn')
        },
        moFn(lockId) {
          if (lockGet['fullChannelNameOnHoverAttrAsyncLock'] !== lockId) return;

          const uploadInfo = qs('#primary.ytd-watch-flexy ytd-watch-metadata #upload-info');
          if (!uploadInfo) return;

          const evtOpt = { passive: true, capture: false };
          uploadInfo.removeEventListener('pointerenter', this.mouseEnterFn, evtOpt);
          uploadInfo.removeEventListener('pointerleave', this.mouseLeaveFn, evtOpt);

          uploadInfo.addEventListener('pointerenter', this.mouseEnterFn, evtOpt);
          uploadInfo.addEventListener('pointerleave', this.mouseLeaveFn, evtOpt);
        },
        async onNavigateFinish() {
          await this.promiseReady.then();
          const uploadInfo = qs('#primary.ytd-watch-flexy ytd-watch-metadata #upload-info');
          if (!uploadInfo) return;
          this.mo.observe(uploadInfo, {
            attributes: true,
            attributeFilter: ['hidden', 'attr-3wb0k'],
          });
          uploadInfo.incAttribute111('attr-3wb0k');
          this.ro.observe(uploadInfo);
        },
        activate() {
          if (this.activated) return;

          const isPassiveArgSupport = typeof IntersectionObserver === 'function';
          // https://caniuse.com/?search=observer
          // https://caniuse.com/?search=addEventListener%20passive

          if (!isPassiveArgSupport) return;

          this.activated = true;

          this.mouseEnterFn = this.mouseEnterFn.bind(this);
          this.mouseLeaveFn = this.mouseLeaveFn.bind(this);

          this.moFn = this.moFn.bind(this);
          this.mo = new MutationObserver(() => {
            Promise.resolve(lockSet['fullChannelNameOnHoverAttrAsyncLock'])
              .then(this.moFn)
              .catch(console.warn);
          });
          this.ro = new ResizeObserver(mutations => {
            if (Date.now() > this.checkResize) return;
            for (const mutation of mutations) {
              const uploadInfo = mutation.target;
              if (uploadInfo && mutation.contentRect.width > 0 && mutation.contentRect.height > 0) {
                const metaDataElm = uploadInfo.closest('ytd-watch-metadata');
                if (metaDataElm.classList.contains('tyt-metadata-hover')) {
                  metaDataElm.classList.add('tyt-metadata-hover-resized');
                }

                break;
              }
            }
          });
          this.promiseReady.resolve();
        },
      },
      'external.ytlstm': {
        activated: false,
        toUse: true, // depends on shouldUseMiniPlayer()
        activate() {
          if (this.activated) return;

          this.activated = true;
          document.documentElement.classList.add('external-ytlstm');
        },
      },
    };

    if (sessionStorage.__$$tmp_UseAutoExpandInfoDesc$$__) plugin.autoExpandInfoDesc.toUse = true;

    // let shouldFixInfo = false;
    const __attachedSymbol__ = Symbol();

    const makeInitAttached = tag => {
      const inPageRearrange_ = inPageRearrange;
      inPageRearrange = false;
      for (const elm of qsAll(`${tag}`)) {
        const cnt = insp(elm) || 0;
        if (typeof cnt.attached498 === 'function' && !elm[__attachedSymbol__]) {
          Promise.resolve(elm).then(eventMap[`${tag}::attached`]).catch(console.warn);
        }
      }
      inPageRearrange = inPageRearrange_;
    };

    const getGeneralChatElement = async () => {
      for (let i = 2; i-- > 0; ) {
        const t = qs('#columns.style-scope.ytd-watch-flexy ytd-live-chat-frame#chat');
        if (t instanceof Element) return t;
        if (i > 0) {
          // try later
          await delayPn(200);
        }
      }
      return null;
    };

    const nsTemplateObtain = () => {
      let nsTemplate = qs('ytd-watch-flexy noscript[ns-template]');
      if (!nsTemplate) {
        nsTemplate = document.createElement('noscript');
        nsTemplate.setAttribute('ns-template', '');
        qs('ytd-watch-flexy').appendChild(nsTemplate);
      }
      return nsTemplate;
    };

    const isPageDOM = (elm, selector) => {
      if (!elm || !(elm instanceof Element) || !elm.nodeName) return false;
      if (!elm.closest(selector)) return false;
      if (elm.isConnected !== true) return false;
      return true;
    };

    const invalidFlexyParent = hostElement => {
      if (hostElement instanceof HTMLElement) {
        const hasFlexyParent = HTMLElement.prototype.closest.call(hostElement, 'ytd-watch-flexy'); // eg short
        if (!hasFlexyParent) return true;
        const currentFlexy = elements.flexy;
        if (currentFlexy && currentFlexy !== hasFlexyParent) return true;
      }
      return false;
    };

    // const mutationComment = document.createComment('1');
    // let mutationPromise = new PromiseExternal();
    // const mutationPromiseObs = new MutationObserver(()=>{
    //   mutationPromise.resolve();
    //   mutationPromise = new PromiseExternal();
    // });
    // mutationPromiseObs.observe(mutationComment, {characterData: true});

    let headerMutationObserver = null;
    let headerMutationTmpNode = null;

    const eventMap = {
      ceHack: () => {
        mLoaded.flag |= 2;
        document.documentElement.setAttribute111('tabview-loaded', mLoaded.makeString());

        retrieveCE('ytd-watch-flexy')
          .then(eventMap['ytd-watch-flexy::defined'])
          .catch(console.warn);
        retrieveCE('ytd-expander').then(eventMap['ytd-expander::defined']).catch(console.warn);
        retrieveCE('ytd-watch-next-secondary-results-renderer')
          .then(eventMap['ytd-watch-next-secondary-results-renderer::defined'])
          .catch(console.warn);
        retrieveCE('ytd-comments-header-renderer')
          .then(eventMap['ytd-comments-header-renderer::defined'])
          .catch(console.warn);
        retrieveCE('ytd-live-chat-frame')
          .then(eventMap['ytd-live-chat-frame::defined'])
          .catch(console.warn);
        retrieveCE('ytd-comments').then(eventMap['ytd-comments::defined']).catch(console.warn);
        retrieveCE('ytd-engagement-panel-section-list-renderer')
          .then(eventMap['ytd-engagement-panel-section-list-renderer::defined'])
          .catch(console.warn);
        retrieveCE('ytd-watch-metadata')
          .then(eventMap['ytd-watch-metadata::defined'])
          .catch(console.warn);
        retrieveCE('ytd-playlist-panel-renderer')
          .then(eventMap['ytd-playlist-panel-renderer::defined'])
          .catch(console.warn);
        retrieveCE('ytd-expandable-video-description-body-renderer')
          .then(eventMap['ytd-expandable-video-description-body-renderer::defined'])
          .catch(console.warn);
      },

      fixForTabDisplay: isResize => {
        // isResize is true if the layout is resized (not due to tab switching)
        // youtube components shall handle the resize issue. can skip some checkings.

        bFixForResizedTabLater = false;

        const runLowPriority = () => {
          for (const element of qsAll('[io-intersected]')) {
            const cnt = insp(element);
            if (element instanceof HTMLElement_ && typeof cnt.calculateCanCollapse === 'function') {
              try {
                cnt.calculateCanCollapse(true);
              } catch {}
            }
          }
        };

        if (typeof requestIdleCallback === 'function') {
          requestIdleCallback(runLowPriority, { timeout: 100 });
        } else {
          setTimeout(runLowPriority, 0);
        }

        if (!isResize && lastTab === '#tab-info') {
          // #tab-info is now shown.
          // to fix the sizing issue (description info cards in tab info)
          requestAnimationFrame(() => {
            for (const element of qsAll(
              '#tab-info ytd-video-description-infocards-section-renderer, #tab-info yt-chip-cloud-renderer, #tab-info ytd-horizontal-card-list-renderer, #tab-info yt-horizontal-list-renderer'
            )) {
              const cnt = insp(element);
              if (element instanceof HTMLElement_ && typeof cnt.notifyResize === 'function') {
                try {
                  cnt.notifyResize();
                } catch {}
              }
            }
            // to fix expand/collapse sizing issue (inline-expander in tab info)
            // for example, expand button is required but not shown as it was rendered in the hidden state
            for (const element of qsAll('#tab-info ytd-text-inline-expander')) {
              const cnt = insp(element);
              if (element instanceof HTMLElement_ && typeof cnt.resize === 'function') {
                cnt.resize(false); // reflow due to offsetWidth calling
              }
              fixInlineExpanderDisplay(cnt); // just in case
            }
          });
        }

        if (!isResize && typeof lastTab === 'string' && lastTab.startsWith('#tab-')) {
          const tabContent = qs('.tab-content-cld:not(.tab-content-hidden)');
          if (tabContent) {
            const renderers = tabContent.querySelectorAll('yt-chip-cloud-renderer');
            for (const renderer of renderers) {
              const cnt = insp(renderer);
              if (typeof cnt.notifyResize === 'function') {
                try {
                  cnt.notifyResize();
                } catch {}
              }
            }
          }
        }
      },

      'ytd-watch-flexy::defined': cProto => {
        if (
          !cProto.updateChatLocation498 &&
          typeof cProto.updateChatLocation === 'function' &&
          cProto.updateChatLocation.length === 0
        ) {
          cProto.updateChatLocation498 = cProto.updateChatLocation;
          cProto.updateChatLocation = updateChatLocation498;
        }

        if (
          !cProto.isTwoColumnsChanged498_ &&
          typeof cProto.isTwoColumnsChanged_ === 'function' &&
          cProto.isTwoColumnsChanged_.length === 2
        ) {
          cProto.isTwoColumnsChanged498_ = cProto.isTwoColumnsChanged_;
          cProto.isTwoColumnsChanged_ = function (arg1, arg2, ...args) {
            const r = secondaryInnerFn(() => {
              const r = this.isTwoColumnsChanged498_(arg1, arg2, ...args);
              return r;
            });
            return r;
          };
        }

        if (
          !cProto.defaultTwoColumnLayoutChanged498 &&
          typeof cProto.defaultTwoColumnLayoutChanged === 'function' &&
          cProto.defaultTwoColumnLayoutChanged.length === 0
        ) {
          cProto.defaultTwoColumnLayoutChanged498 = cProto.defaultTwoColumnLayoutChanged;
          cProto.defaultTwoColumnLayoutChanged = function (...args) {
            const r = secondaryInnerFn(() => {
              const r = this.defaultTwoColumnLayoutChanged498(...args);
              return r;
            });
            return r;
          };
        }

        if (
          !cProto.updatePlayerLocation498 &&
          typeof cProto.updatePlayerLocation === 'function' &&
          cProto.updatePlayerLocation.length === 0
        ) {
          cProto.updatePlayerLocation498 = cProto.updatePlayerLocation;
          cProto.updatePlayerLocation = function (...args) {
            const r = secondaryInnerFn(() => {
              const r = this.updatePlayerLocation498(...args);
              return r;
            });
            return r;
          };
        }

        if (
          !cProto.updateCinematicsLocation498 &&
          typeof cProto.updateCinematicsLocation === 'function' &&
          cProto.updateCinematicsLocation.length === 0
        ) {
          cProto.updateCinematicsLocation498 = cProto.updateCinematicsLocation;
          cProto.updateCinematicsLocation = function (...args) {
            const r = secondaryInnerFn(() => {
              const r = this.updateCinematicsLocation498(...args);
              return r;
            });
            return r;
          };
        }

        if (
          !cProto.updatePanelsLocation498 &&
          typeof cProto.updatePanelsLocation === 'function' &&
          cProto.updatePanelsLocation.length === 0
        ) {
          cProto.updatePanelsLocation498 = cProto.updatePanelsLocation;
          cProto.updatePanelsLocation = function (...args) {
            const r = secondaryInnerFn(() => {
              const r = this.updatePanelsLocation498(...args);
              return r;
            });
            return r;
          };
        }
        if (
          !cProto.swatcherooUpdatePanelsLocation498 &&
          typeof cProto.swatcherooUpdatePanelsLocation === 'function' &&
          cProto.swatcherooUpdatePanelsLocation.length === 6
        ) {
          cProto.swatcherooUpdatePanelsLocation498 = cProto.swatcherooUpdatePanelsLocation;
          cProto.swatcherooUpdatePanelsLocation = function (
            arg1,
            arg2,
            arg3,
            arg4,
            arg5,
            arg6,
            ...args
          ) {
            const r = secondaryInnerFn(() => {
              const r = this.swatcherooUpdatePanelsLocation498(
                arg1,
                arg2,
                arg3,
                arg4,
                arg5,
                arg6,
                ...args
              );
              return r;
            });
            return r;
          };
        }

        if (
          !cProto.updateErrorScreenLocation498 &&
          typeof cProto.updateErrorScreenLocation === 'function' &&
          cProto.updateErrorScreenLocation.length === 0
        ) {
          cProto.updateErrorScreenLocation498 = cProto.updateErrorScreenLocation;
          cProto.updateErrorScreenLocation = function (...args) {
            const r = secondaryInnerFn(() => {
              const r = this.updateErrorScreenLocation498(...args);
              return r;
            });
            return r;
          };
        }

        if (
          !cProto.updateFullBleedElementLocations498 &&
          typeof cProto.updateFullBleedElementLocations === 'function' &&
          cProto.updateFullBleedElementLocations.length === 0
        ) {
          cProto.updateFullBleedElementLocations498 = cProto.updateFullBleedElementLocations;
          cProto.updateFullBleedElementLocations = function (...args) {
            const r = secondaryInnerFn(() => {
              const r = this.updateFullBleedElementLocations498(...args);
              return r;
            });
            return r;
          };
        }
      },

      'ytd-watch-next-secondary-results-renderer::defined': cProto => {
        if (!cProto.attached498 && typeof cProto.attached === 'function') {
          cProto.attached498 = cProto.attached;
          cProto.attached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-watch-next-secondary-results-renderer::attached'])
                .catch(console.warn);
            }
            return this.attached498();
          };
        }
        if (!cProto.detached498 && typeof cProto.detached === 'function') {
          cProto.detached498 = cProto.detached;
          cProto.detached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-watch-next-secondary-results-renderer::detached'])
                .catch(console.warn);
            }
            return this.detached498();
          };
        }

        makeInitAttached('ytd-watch-next-secondary-results-renderer');
      },

      'ytd-watch-next-secondary-results-renderer::attached': hostElement => {
        if (invalidFlexyParent(hostElement)) return;

        // if (inPageRearrange) return;
        DEBUG_5084 && console.log(5084, 'ytd-watch-next-secondary-results-renderer::attached');
        if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true;
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }
        if (hostElement.isConnected !== true) return;
        // if (hostElement.__connectedFlg__ !== 4) return;
        // hostElement.__connectedFlg__ = 5;
        if (
          hostElement instanceof HTMLElement_ &&
          hostElement.matches('#columns #related ytd-watch-next-secondary-results-renderer') &&
          !hostElement.matches(
            '#right-tabs ytd-watch-next-secondary-results-renderer, [hidden] ytd-watch-next-secondary-results-renderer'
          )
        ) {
          elements.related = hostElement.closest('#related');
          hostElement.setAttribute111('tyt-videos-list', '');
        }
        // console.log('ytd-watch-next-secondary-results-renderer::attached', hostElement);
      },

      'ytd-watch-next-secondary-results-renderer::detached': hostElement => {
        // if (inPageRearrange) return;
        DEBUG_5084 && console.log(5084, 'ytd-watch-next-secondary-results-renderer::detached');
        if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return;
        if (hostElement.isConnected !== false) return;
        // if (hostElement.__connectedFlg__ !== 8) return;
        // hostElement.__connectedFlg__ = 9;
        if (hostElement.hasAttribute000('tyt-videos-list')) {
          elements.related = null;
          hostElement.removeAttribute000('tyt-videos-list');
        }
        DEBUG_5084 &&
          console.log('ytd-watch-next-secondary-results-renderer::detached', hostElement);
      },

      settingCommentsVideoId: hostElement => {
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }
        const cnt = insp(hostElement);
        const commentsArea = elements.comments;
        if (
          commentsArea !== hostElement ||
          hostElement.isConnected !== true ||
          cnt.isAttached !== true ||
          !cnt.data ||
          cnt.hidden !== false
        ) {
          return;
        }
        const ytdFlexyElm = elements.flexy;
        const ytdFlexyCnt = ytdFlexyElm ? insp(ytdFlexyElm) : null;
        if (ytdFlexyCnt && ytdFlexyCnt.videoId) {
          hostElement.setAttribute111('tyt-comments-video-id', ytdFlexyCnt.videoId);
        } else {
          hostElement.removeAttribute000('tyt-comments-video-id');
        }
      },
      checkCommentsShouldBeHidden: lockId => {
        if (lockGet['checkCommentsShouldBeHiddenLock'] !== lockId) return;

        // commentsArea's attribute: tyt-comments-video-id
        // ytdFlexyElm's attribute: video-id

        const commentsArea = elements.comments;
        const ytdFlexyElm = elements.flexy;
        if (commentsArea && ytdFlexyElm && !commentsArea.hasAttribute000('hidden')) {
          const ytdFlexyCnt = insp(ytdFlexyElm);
          if (typeof ytdFlexyCnt.videoId === 'string') {
            const commentsVideoId = commentsArea.getAttribute('tyt-comments-video-id');
            if (commentsVideoId && commentsVideoId !== ytdFlexyCnt.videoId) {
              commentsArea.setAttribute111('hidden', '');
              // removeKeepCommentsScroller();
            }
          }
        }
      },
      'ytd-comments::defined': cProto => {
        if (!cProto.attached498 && typeof cProto.attached === 'function') {
          cProto.attached498 = cProto.attached;
          cProto.attached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-comments::attached'])
                .catch(console.warn);
            }
            // Promise.resolve(this.hostElement).then(eventMap['ytd-comments::dataChanged_']).catch(console.warn);
            return this.attached498();
          };
        }
        if (!cProto.detached498 && typeof cProto.detached === 'function') {
          cProto.detached498 = cProto.detached;
          cProto.detached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-comments::detached'])
                .catch(console.warn);
            }
            // Promise.resolve(this.hostElement).then(eventMap['ytd-comments::dataChanged_']).catch(console.warn);
            return this.detached498();
          };
        }

        cProto._createPropertyObserver('data', '_dataChanged498', undefined);
        cProto._dataChanged498 = function () {
          // console.log('_dataChanged498', this.hostElement)
          Promise.resolve(this.hostElement)
            .then(eventMap['ytd-comments::_dataChanged498'])
            .catch(console.warn);
        };

        // if (!cProto.dataChanged498_ && typeof cProto.dataChanged_ === 'function') {
        //   cProto.dataChanged498_ = cProto.dataChanged_;
        //   cProto.dataChanged_ = function () {
        //     Promise.resolve(this.hostElement).then(eventMap['ytd-comments::dataChanged_']).catch(console.warn);
        //     return this.dataChanged498_();
        //   }
        // }

        makeInitAttached('ytd-comments');
      },

      'ytd-comments::_dataChanged498': hostElement => {
        // console.log(18984, hostElement.hasAttribute('tyt-comments-area'))
        if (!hostElement.hasAttribute000('tyt-comments-area')) return;
        let commentsDataStatus = 0;
        const cnt = insp(hostElement);
        const data = cnt ? cnt.data : null;
        const contents = data ? data.contents : null;
        if (data) {
          if (contents && contents.length === 1 && contents[0].messageRenderer) {
            commentsDataStatus = 2;
          }
          if (contents && contents.length > 1 && contents[0].commentThreadRenderer) {
            commentsDataStatus = 1;
          }
        }
        if (commentsDataStatus) {
          hostElement.setAttribute111('tyt-comments-data-status', commentsDataStatus);
          // ytdFlexyElm.setAttribute111('tyt-comment-disabled', '')
        } else {
          // ytdFlexyElm.removeAttribute000('tyt-comment-disabled')
          hostElement.removeAttribute000('tyt-comments-data-status');
        }
        Promise.resolve(hostElement).then(eventMap['settingCommentsVideoId']).catch(console.warn);
      },

      'ytd-comments::attached': async hostElement => {
        if (invalidFlexyParent(hostElement)) return;

        // if (inPageRearrange) return;
        DEBUG_5084 && console.log(5084, 'ytd-comments::attached');
        if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true;
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }
        if (hostElement.isConnected !== true) return;
        // if (hostElement.__connectedFlg__ !== 4) return;
        // hostElement.__connectedFlg__ = 5;
        if (!hostElement || hostElement.id !== 'comments') return;
        // if (!hostElement || hostElement.closest('[hidden]')) return;
        elements.comments = hostElement;
        Promise.resolve(hostElement).then(eventMap['settingCommentsVideoId']).catch(console.warn);

        aoComment.observe(hostElement, { attributes: true });
        hostElement.setAttribute111('tyt-comments-area', '');

        const lockId = lockSet['rightTabReadyLock02'];
        await rightTabsProvidedPromise.then();
        if (lockGet['rightTabReadyLock02'] !== lockId) return;

        if (elements.comments !== hostElement) return;
        if (hostElement.isConnected === false) return;
        DEBUG_5085 && console.log(7932, 'comments');

        // if(!elements.comments || elements.comments.isConnected === false) return;
        if (hostElement && !hostElement.closest('#right-tabs')) {
          qs('#tab-comments').assignChildren111(null, hostElement, null);
        } else {
          const shouldTabVisible =
            elements.comments &&
            elements.comments.closest('#tab-comments') &&
            !elements.comments.closest('[hidden]');

          document
            .querySelector('[tyt-tab-content="#tab-comments"]')
            .classList.toggle('tab-btn-hidden', !shouldTabVisible);

          //   document.querySelector('#tab-comments').classList.remove('tab-content-hidden')
          //   document.querySelector('[tyt-tab-content="#tab-comments"]').classList.remove('tab-btn-hidden')

          Promise.resolve(lockSet['removeKeepCommentsScrollerLock'])
            .then(removeKeepCommentsScroller)
            .catch(console.warn);
        }

        TAB_AUTO_SWITCH_TO_COMMENTS && switchToTab('#tab-comments');
      },
      'ytd-comments::detached': hostElement => {
        // if (inPageRearrange) return;
        DEBUG_5084 && console.log(5084, 'ytd-comments::detached');
        // console.log(858, hostElement)
        if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return;
        if (hostElement.isConnected !== false) return;
        // if (hostElement.__connectedFlg__ !== 8) return;
        // hostElement.__connectedFlg__ = 9;

        if (hostElement.hasAttribute000('tyt-comments-area')) {
          // foComments.disconnect();
          // foComments.takeRecords();
          hostElement.removeAttribute000('tyt-comments-area');
          // document.querySelector('#tab-comments').classList.add('tab-content-hidden')
          // document.querySelector('[tyt-tab-content="#tab-comments"]').classList.add('tab-btn-hidden')

          aoComment.disconnect();
          aoComment.takeRecords();
          elements.comments = null;

          document
            .querySelector('[tyt-tab-content="#tab-comments"]')
            .classList.add('tab-btn-hidden');

          Promise.resolve(lockSet['removeKeepCommentsScrollerLock'])
            .then(removeKeepCommentsScroller)
            .catch(console.warn);
        }
      },

      'ytd-comments-header-renderer::defined': cProto => {
        if (!cProto.attached498 && typeof cProto.attached === 'function') {
          cProto.attached498 = cProto.attached;
          cProto.attached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-comments-header-renderer::attached'])
                .catch(console.warn);
            }
            Promise.resolve(this.hostElement)
              .then(eventMap['ytd-comments-header-renderer::dataChanged'])
              .catch(console.warn); // force dataChanged on attached
            return this.attached498();
          };
        }
        if (!cProto.detached498 && typeof cProto.detached === 'function') {
          cProto.detached498 = cProto.detached;
          cProto.detached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-comments-header-renderer::detached'])
                .catch(console.warn);
            }
            return this.detached498();
          };
        }

        if (!cProto.dataChanged498 && typeof cProto.dataChanged === 'function') {
          cProto.dataChanged498 = cProto.dataChanged;
          cProto.dataChanged = function () {
            Promise.resolve(this.hostElement)
              .then(eventMap['ytd-comments-header-renderer::dataChanged'])
              .catch(console.warn);
            return this.dataChanged498();
          };
        }

        makeInitAttached('ytd-comments-header-renderer');
      },

      'ytd-comments-header-renderer::attached': hostElement => {
        if (invalidFlexyParent(hostElement)) return;

        // if (inPageRearrange) return;
        DEBUG_5084 && console.log(5084, 'ytd-comments-header-renderer::attached');
        if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true;
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }
        if (hostElement.isConnected !== true) return;
        // if (hostElement.__connectedFlg__ !== 4) return;
        // hostElement.__connectedFlg__ = 5;
        if (!hostElement || !hostElement.classList.contains('ytd-item-section-renderer')) return;
        // console.log(12991, 'ytd-comments-header-renderer::attached')
        const targetElement = qs('[tyt-comments-area] ytd-comments-header-renderer');
        if (hostElement === targetElement) {
          hostElement.setAttribute111('tyt-comments-header-field', '');
        } else {
          const parentNode = hostElement.parentNode;
          if (
            parentNode instanceof HTMLElement_ &&
            parentNode.querySelector('[tyt-comments-header-field]')
          ) {
            hostElement.setAttribute111('tyt-comments-header-field', '');
          }
        }
      },

      'ytd-comments-header-renderer::detached': hostElement => {
        // if (inPageRearrange) return;
        DEBUG_5084 && console.log(5084, 'ytd-comments-header-renderer::detached');

        if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return;
        if (hostElement.isConnected !== false) return;
        // if (hostElement.__connectedFlg__ !== 8) return;
        // hostElement.__connectedFlg__ = 9;
        // console.log(12992, 'ytd-comments-header-renderer::detached')
        if (hostElement.hasAttribute000('field-of-cm-count')) {
          hostElement.removeAttribute000('field-of-cm-count');

          const cmCount = qs('#tyt-cm-count');
          if (cmCount && !qs('#tab-comments ytd-comments-header-renderer[field-of-cm-count]')) {
            cmCount.textContent = '';
          }
        }
        if (hostElement.hasAttribute000('tyt-comments-header-field')) {
          hostElement.removeAttribute000('tyt-comments-header-field');
        }
      },

      'ytd-comments-header-renderer::dataChanged': hostElement => {
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }

        const ytdFlexyElm = elements.flexy;

        let b = false;
        const cnt = insp(hostElement);
        if (
          cnt &&
          hostElement.closest('#tab-comments') &&
          qs('#tab-comments ytd-comments-header-renderer') === hostElement
        ) {
          b = true;
        } else if (
          hostElement instanceof HTMLElement_ &&
          hostElement.parentNode instanceof HTMLElement_ &&
          hostElement.parentNode.querySelector('[tyt-comments-header-field]')
        ) {
          b = true;
        }
        if (b) {
          hostElement.setAttribute111('tyt-comments-header-field', '');
          ytdFlexyElm && ytdFlexyElm.removeAttribute000('tyt-comment-disabled');
        }

        if (
          hostElement.hasAttribute000('tyt-comments-header-field') &&
          hostElement.isConnected === true
        ) {
          if (!headerMutationObserver) {
            headerMutationObserver = new MutationObserver(
              eventMap['ytd-comments-header-renderer::deferredCounterUpdate']
            );
          }
          headerMutationObserver.observe(hostElement.parentNode, {
            subtree: false,
            childList: true,
          });
          if (!headerMutationTmpNode) {
            headerMutationTmpNode = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
          }
          const tmpNode = headerMutationTmpNode;
          hostElement.insertAdjacentElement('afterend', tmpNode);
          tmpNode.remove();
        }
      },

      'ytd-comments-header-renderer::deferredCounterUpdate': () => {
        const nodes = qsAll('#tab-comments ytd-comments-header-renderer[class]');
        if (nodes.length === 1) {
          const hostElement = nodes[0];
          const cnt = insp(hostElement);
          const data = cnt.data;
          if (!data) return;
          let ez = '';
          if (
            data.commentsCount &&
            data.commentsCount.runs &&
            data.commentsCount.runs.length >= 1
          ) {
            let max = -1;
            const z = data.commentsCount.runs
              .map(e => {
                const c = e.text.replace(/\D+/g, '').length;
                if (c > max) max = c;
                return [e.text, c];
              })
              .filter(a => a[1] === max);
            if (z.length >= 1) {
              ez = z[0][0];
            }
          } else if (data.countText && data.countText.runs && data.countText.runs.length >= 1) {
            let max = -1;
            const z = data.countText.runs
              .map(e => {
                const c = e.text.replace(/\D+/g, '').length;
                if (c > max) max = c;
                return [e.text, c];
              })
              .filter(a => a[1] === max);
            if (z.length >= 1) {
              ez = z[0][0];
            }
          }
          const cmCount = qs('#tyt-cm-count');
          if (ez) {
            hostElement.setAttribute111('field-of-cm-count', '');
            cmCount && (cmCount.textContent = ez.trim());
          } else {
            hostElement.removeAttribute000('field-of-cm-count');
            cmCount && (cmCount.textContent = '');
            console.warn('no text for #tyt-cm-count');
          }
        }
      },

      'ytd-expander::defined': cProto => {
        if (!cProto.attached498 && typeof cProto.attached === 'function') {
          cProto.attached498 = cProto.attached;
          cProto.attached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-expander::attached'])
                .catch(console.warn);
            }
            return this.attached498();
          };
        }
        if (!cProto.detached498 && typeof cProto.detached === 'function') {
          cProto.detached498 = cProto.detached;
          cProto.detached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-expander::detached'])
                .catch(console.warn);
            }
            return this.detached498();
          };
        }
        if (!cProto.calculateCanCollapse498 && typeof cProto.calculateCanCollapse === 'function') {
          cProto.calculateCanCollapse498 = cProto.calculateCanCollapse;
          cProto.calculateCanCollapse = funcCanCollapse;
        }

        if (!cProto.childrenChanged498 && typeof cProto.childrenChanged === 'function') {
          cProto.childrenChanged498 = cProto.childrenChanged;
          cProto.childrenChanged = function () {
            Promise.resolve(this.hostElement)
              .then(eventMap['ytd-expander::childrenChanged'])
              .catch(console.warn);
            return this.childrenChanged498();
          };
        }

        /*

        console.log('ytd-expander::defined 01');
        
        CustomElementRegistry.prototype.get.call(customElements, 'ytd-expander').prototype.connectedCallback = connectedCallbackY(CustomElementRegistry.prototype.get.call(customElements, 'ytd-expander').prototype.connectedCallback)
        CustomElementRegistry.prototype.get.call(customElements, 'ytd-expander').prototype.disconnectedCallback = disconnectedCallbackY(CustomElementRegistry.prototype.get.call(customElements, 'ytd-expander').prototype.disconnectedCallback)
        
        console.log('ytd-expander::defined 02');

        */

        makeInitAttached('ytd-expander');
      },

      'ytd-expander::childrenChanged': hostElement => {
        if (
          hostElement instanceof Node &&
          hostElement.hasAttribute000('hidden') &&
          hostElement.hasAttribute000('tyt-main-info') &&
          hostElement.firstElementChild
        ) {
          hostElement.removeAttribute('hidden');
        }
      },

      'ytd-expandable-video-description-body-renderer::defined': cProto => {
        if (!cProto.attached498 && typeof cProto.attached === 'function') {
          cProto.attached498 = cProto.attached;
          cProto.attached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-expandable-video-description-body-renderer::attached'])
                .catch(console.warn);
            }
            return this.attached498();
          };
        }
        if (!cProto.detached498 && typeof cProto.detached === 'function') {
          cProto.detached498 = cProto.detached;
          cProto.detached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-expandable-video-description-body-renderer::detached'])
                .catch(console.warn);
            }
            return this.detached498();
          };
        }

        makeInitAttached('ytd-expandable-video-description-body-renderer');
      },

      'ytd-expandable-video-description-body-renderer::attached': async hostElement => {
        if (
          hostElement instanceof HTMLElement_ &&
          isPageDOM(hostElement, '[tyt-info-renderer]') &&
          !hostElement.matches('[tyt-main-info]')
        ) {
          elements.infoExpander = hostElement;

          // console.log(1299, hostElement.parentNode, isRightTabsInserted)

          infoExpanderElementProvidedPromise.resolve();
          hostElement.setAttribute111('tyt-main-info', '');
          if (plugin.autoExpandInfoDesc.toUse) {
            plugin.autoExpandInfoDesc.onMainInfoSet(hostElement);
          }

          const lockId = lockSet['rightTabReadyLock03'];
          await rightTabsProvidedPromise.then();
          if (lockGet['rightTabReadyLock03'] !== lockId) return;

          if (elements.infoExpander !== hostElement) return;
          if (hostElement.isConnected === false) return;

          elements.infoExpander.classList.add('tyt-main-info'); // add a classname for it

          const infoExpander = elements.infoExpander;
          // const infoExpanderBack = elements.infoExpanderBack;

          // console.log(5438,infoExpander, qt);

          // const dummy = document.createElement('noscript');
          // dummy.setAttribute000('id', 'info-expander-vid');
          // dummy.setAttribute000('video-id', getCurrentVideoId());
          // infoExpander.insertBefore000(dummy, infoExpander.firstChild);

          // aoInfo.observe(infoExpander, { attributes: true, attributeFilter: ['tyt-display-for', 'tyt-video-id'] });
          // zoInfo.observe(infoExpanderBack, { attributes: true, attributeFilter: ['hidden', 'attr-w20ts'], childList: true, subtree: true});
          // new MutationObserver(()=>{
          //   console.log(591499)
          // }).observe(infoExpanderBack, {childList: true, subtree: true})

          const inlineExpanderElm = infoExpander.querySelector('ytd-text-inline-expander');
          if (inlineExpanderElm) {
            const mo = new MutationObserver(() => {
              const p = qs('#tab-info ytd-text-inline-expander');
              sessionStorage.__$$tmp_UseAutoExpandInfoDesc$$__ =
                p && p.hasAttribute('is-expanded') ? '1' : '';
              if (p) fixInlineExpanderContent();
            });
            mo.observe(inlineExpanderElm, {
              attributes: ['is-expanded', 'attr-6v8qu', 'hidden'],
              subtree: true,
            }); // hidden + subtree to trigger the fn by delayedUpdate
            inlineExpanderElm.incAttribute111('attr-6v8qu');
            const cnt = insp(inlineExpanderElm);

            if (cnt) fixInlineExpanderDisplay(cnt);
          }

          if (infoExpander && !infoExpander.closest('#right-tabs')) {
            const tabInfoElm = qs('#tab-info');
            if (tabInfoElm) tabInfoElm.assignChildren111(null, infoExpander, null);
          } else {
            if (qs('[tyt-tab-content="#tab-info"]')) {
              const shouldTabVisible =
                elements.infoExpander && elements.infoExpander.closest('#tab-info');
              document
                .querySelector('[tyt-tab-content="#tab-info"]')
                .classList.toggle('tab-btn-hidden', !shouldTabVisible);
            }
          }

          Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn); // required when the page is switched from channel to watch

          // if (infoExpander && infoExpander.closest('#right-tabs')) Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn);

          // infoExpanderBack.incAttribute111('attr-w20ts');

          // return;
        }

        DEBUG_5084 && console.log(5084, 'ytd-expandable-video-description-body-renderer::attached');
        if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true;
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }
        if (hostElement.isConnected !== true) return;

        if (isPageDOM(hostElement, '#tab-info [tyt-main-info]')) {
          // const cnt = insp(hostElement);
          // if(cnt.data){
          //   cnt.data = Object.assign({}, cnt.data);
          // }
        } else if (!hostElement.closest('#tab-info')) {
          const bodyRenderer = hostElement;
          let bodyRendererNew = qs(
            'ytd-expandable-video-description-body-renderer[tyt-info-renderer]'
          );
          if (!bodyRendererNew) {
            bodyRendererNew = document.createElement(
              'ytd-expandable-video-description-body-renderer'
            );
            bodyRendererNew.setAttribute('tyt-info-renderer', '');
            nsTemplateObtain().appendChild(bodyRendererNew);
          }
          // document.querySelector('#tab-info').assignChildren111(null, bodyRendererNew, null);

          const cnt = insp(bodyRendererNew);
          cnt.data = Object.assign({}, insp(bodyRenderer).data);

          const inlineExpanderElm = bodyRendererNew.querySelector('ytd-text-inline-expander');
          const inlineExpanderCnt = insp(inlineExpanderElm);
          fixInlineExpanderMethods(inlineExpanderCnt);

          // insp(bodyRendererNew).data = insp(bodyRenderer).data;

          // if((bodyRendererNew.hasAttribute('hidden')?1:0)^(bodyRenderer.hasAttribute('hidden')?1:0)){
          //   if(bodyRenderer.hasAttribute('hidden')) bodyRendererNew.setAttribute('hidden', '');
          //   else bodyRendererNew.removeAttribute('hidden');
          // }

          elements.infoExpanderRendererBack = bodyRenderer;
          elements.infoExpanderRendererFront = bodyRendererNew;
          bodyRenderer.setAttribute('tyt-info-renderer-back', '');
          bodyRendererNew.setAttribute('tyt-info-renderer-front', '');

          // elements.infoExpanderBack = {{ytd-expander}};
        }
      },

      'ytd-expandable-video-description-body-renderer::detached': async hostElement => {
        if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return;
        if (hostElement.isConnected !== false) return;
        // if (hostElement.__connectedFlg__ !== 8) return;
        // hostElement.__connectedFlg__ = 9;
        // console.log(5992, hostElement)
        if (hostElement.hasAttribute000('tyt-main-info')) {
          DEBUG_5084 &&
            console.log(5084, 'ytd-expandable-video-description-body-renderer::detached');
          elements.infoExpander = null;
          hostElement.removeAttribute000('tyt-main-info');
        }
      },

      'ytd-expander::attached': async hostElement => {
        if (invalidFlexyParent(hostElement)) return;

        // if (inPageRearrange) return;
        if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true;
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }
        if (hostElement.isConnected !== true) return;
        // if (hostElement.__connectedFlg__ !== 4) return;
        // hostElement.__connectedFlg__ = 5;
        // console.log(4959, hostElement)

        if (
          hostElement instanceof HTMLElement_ &&
          hostElement.matches('[tyt-comments-area] #contents ytd-expander#expander') &&
          !hostElement.matches('[hidden] ytd-expander#expander')
        ) {
          hostElement.setAttribute111('tyt-content-comment-entry', '');
          ioComment.observe(hostElement);
        }

        // --------------

        // else if (hostElement instanceof HTMLElement_ && hostElement.matches('ytd-expander#expander.style-scope.ytd-expandable-video-description-body-renderer')) {
        //   //  && !hostElement.matches('#right-tabs ytd-expander#expander, [hidden] ytd-expander#expander')

        //   console.log(5084, 'ytd-expander::attached');
        //   const bodyRenderer = hostElement.closest('ytd-expandable-video-description-body-renderer');
        //   let bodyRendererNew = document.querySelector('ytd-expandable-video-description-body-renderer[tyt-info-renderer]');
        //   if (!bodyRendererNew) {
        //     bodyRendererNew = document.createElement('ytd-expandable-video-description-body-renderer');
        //     bodyRendererNew.setAttribute('tyt-info-renderer', '');
        //     nsTemplateObtain().appendChild(bodyRendererNew);
        //   }
        //   // document.querySelector('#tab-info').assignChildren111(null, bodyRendererNew, null);

        //   insp(bodyRendererNew).data = insp(bodyRenderer).data;
        //   // if((bodyRendererNew.hasAttribute('hidden')?1:0)^(bodyRenderer.hasAttribute('hidden')?1:0)){
        //   //   if(bodyRenderer.hasAttribute('hidden')) bodyRendererNew.setAttribute('hidden', '');
        //   //   else bodyRendererNew.removeAttribute('hidden');
        //   // }

        //   elements.infoExpanderRendererBack = bodyRenderer;
        //   elements.infoExpanderRendererFront = bodyRendererNew;
        //   bodyRenderer.setAttribute('tyt-info-renderer-back','')
        //   bodyRendererNew.setAttribute('tyt-info-renderer-front','')

        //   elements.infoExpanderBack = hostElement;

        // }

        // --------------

        // console.log('ytd-expander::attached', hostElement);
      },

      'ytd-expander::detached': hostElement => {
        // if (inPageRearrange) return;
        if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return;
        if (hostElement.isConnected !== false) return;
        // if (hostElement.__connectedFlg__ !== 8) return;
        // hostElement.__connectedFlg__ = 9;
        // console.log(5992, hostElement)
        if (hostElement.hasAttribute000('tyt-content-comment-entry')) {
          ioComment.unobserve(hostElement);
          hostElement.removeAttribute000('tyt-content-comment-entry');
        } else if (hostElement.hasAttribute000('tyt-main-info')) {
          DEBUG_5084 && console.log(5084, 'ytd-expander::detached');
          elements.infoExpander = null;
          hostElement.removeAttribute000('tyt-main-info');
        }
        // console.log('ytd-expander::detached', hostElement);
      },

      'ytd-live-chat-frame::defined': cProto => {
        let _lastDomAction = 0;

        if (!cProto.attached498 && typeof cProto.attached === 'function') {
          cProto.attached498 = cProto.attached;
          cProto.attached = function () {
            _lastDomAction = Date.now();
            // console.log('chat868-attached', Date.now());
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-live-chat-frame::attached'])
                .catch(console.warn);
            }
            return this.attached498();
          };
        }
        if (!cProto.detached498 && typeof cProto.detached === 'function') {
          cProto.detached498 = cProto.detached;
          cProto.detached = function () {
            _lastDomAction = Date.now();
            // console.log('chat868-detached', Date.now());
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-live-chat-frame::detached'])
                .catch(console.warn);
            }
            return this.detached498();
          };
        }

        if (
          typeof cProto.urlChanged === 'function' &&
          !cProto.urlChanged66 &&
          !cProto.urlChangedAsync12 &&
          cProto.urlChanged.length === 0
        ) {
          cProto.urlChanged66 = cProto.urlChanged;
          let ath = 0;
          cProto.urlChangedAsync12 = async function () {
            await this.__urlChangedAsyncT689__;
            const t = (ath = (ath & 1073741823) + 1);
            const chatframe = this.chatframe || (this.$ || 0).chatframe || 0;
            if (chatframe instanceof HTMLIFrameElement) {
              if (chatframe.contentDocument === null) {
                await Promise.resolve('#').catch(console.warn);
                if (t !== ath) return;
              }
              await new Promise(resolve => setTimeout_(resolve, 1)).catch(console.warn); // neccessary for Brave
              if (t !== ath) return;
              const isBlankPage = !this.data || this.collapsed;
              const p1 = new Promise(resolve => setTimeout_(resolve, 706)).catch(console.warn);
              const p2 = new Promise(resolve => {
                new IntersectionObserver((entries, observer) => {
                  for (const entry of entries) {
                    const rect = entry.boundingClientRect || 0;
                    if (isBlankPage || (rect.width > 0 && rect.height > 0)) {
                      observer.disconnect();
                      resolve('#');
                      break;
                    }
                  }
                }).observe(chatframe);
              }).catch(console.warn);
              await Promise.race([p1, p2]);
              if (t !== ath) return;
            }
            this.urlChanged66();
          };
          cProto.urlChanged = function () {
            const t = (this.__urlChangedAsyncT688__ =
              (this.__urlChangedAsyncT688__ & 1073741823) + 1);
            nextBrowserTick(() => {
              if (t !== this.__urlChangedAsyncT688__) return;
              this.urlChangedAsync12();
            });
          };
        }

        makeInitAttached('ytd-live-chat-frame');
      },

      'ytd-live-chat-frame::attached': async hostElement => {
        if (invalidFlexyParent(hostElement)) return;

        // if (inPageRearrange) return;
        DEBUG_5084 && console.log(5084, 'ytd-live-chat-frame::attached');
        if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true;
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }
        if (hostElement.isConnected !== true) return;
        // if (hostElement.__connectedFlg__ !== 4) return;
        // hostElement.__connectedFlg__ = 5;
        if (!hostElement || hostElement.id !== 'chat') return;

        const lockId = lockSet['ytdLiveAttachedLock'];
        const chatElem = await getGeneralChatElement();
        if (lockGet['ytdLiveAttachedLock'] !== lockId) return;

        if (chatElem === hostElement) {
          elements.chat = chatElem;
          aoChat.observe(chatElem, { attributes: true });
          const isFlexyReady = elements.flexy instanceof Element;
          chatElem.setAttribute111('tyt-active-chat-frame', isFlexyReady ? 'CF' : 'C');

          const chatContainer = chatElem ? chatElem.closest('#chat-container') || chatElem : null;
          if (chatContainer && !chatContainer.hasAttribute000('tyt-chat-container')) {
            for (const p of qsAll('[tyt-chat-container]')) {
              p.removeAttribute000('[tyt-chat-container]');
            }
            chatContainer.setAttribute111('tyt-chat-container', '');
          }
          const cnt = insp(hostElement);
          const q = cnt.__urlChangedAsyncT688__;
          const p = (cnt.__urlChangedAsyncT689__ = new PromiseExternal());
          setTimeout_(() => {
            if (p !== cnt.__urlChangedAsyncT689__) return;
            if (cnt.isAttached === true && hostElement.isConnected === true) {
              p.resolve();
              if (q === cnt.__urlChangedAsyncT688__) {
                cnt.urlChanged();
              }
            }
          }, 320);
          Promise.resolve(lockSet['layoutFixLock']).then(layoutFix);
        } else {
          console.warn('Issue found in ytd-live-chat-frame::attached', chatElem, hostElement);
        }
      },

      'ytd-live-chat-frame::detached': hostElement => {
        // if (inPageRearrange) return;
        DEBUG_5084 && console.log(5084, 'ytd-live-chat-frame::detached');

        if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return;
        if (hostElement.isConnected !== false) return;
        // if (hostElement.__connectedFlg__ !== 8) return;
        // hostElement.__connectedFlg__ = 9;
        if (hostElement.hasAttribute000('tyt-active-chat-frame')) {
          aoChat.disconnect();
          aoChat.takeRecords();
          hostElement.removeAttribute000('tyt-active-chat-frame');
          elements.chat = null;

          const ytdFlexyElm = elements.flexy;
          if (ytdFlexyElm) {
            ytdFlexyElm.removeAttribute000('tyt-chat-collapsed');
            ytdFlexyElm.setAttribute111('tyt-chat', '');
          }
        }
      },

      'ytd-engagement-panel-section-list-renderer::defined': cProto => {
        if (!cProto.attached498 && typeof cProto.attached === 'function') {
          cProto.attached498 = cProto.attached;
          cProto.attached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-engagement-panel-section-list-renderer::attached'])
                .catch(console.warn);
            }
            return this.attached498();
          };
        }
        if (!cProto.detached498 && typeof cProto.detached === 'function') {
          cProto.detached498 = cProto.detached;
          cProto.detached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-engagement-panel-section-list-renderer::detached'])
                .catch(console.warn);
            }
            return this.detached498();
          };
        }
        makeInitAttached('ytd-engagement-panel-section-list-renderer');
      },

      'ytd-engagement-panel-section-list-renderer::bindTarget': hostElement => {
        if (
          hostElement.matches(
            '#panels.ytd-watch-flexy > ytd-engagement-panel-section-list-renderer[target-id][visibility]'
          )
        ) {
          hostElement.setAttribute111('tyt-egm-panel', '');
          egmPanelsCache.add(hostElement);
          Promise.resolve(lockSet['updateEgmPanelsLock']).then(updateEgmPanels).catch(console.warn);
          aoEgmPanels.observe(hostElement, {
            attributes: true,
            attributeFilter: ['visibility', 'hidden'],
          });

          // console.log(5094, 2, 'ytd-engagement-panel-section-list-renderer::attached', hostElement);
        }
      },

      'ytd-engagement-panel-section-list-renderer::attached': hostElement => {
        if (invalidFlexyParent(hostElement)) return;

        // if (inPageRearrange) return;

        DEBUG_5084 && console.log(5084, 'ytd-engagement-panel-section-list-renderer::attached');
        if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true;
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }
        if (hostElement.isConnected !== true) return;
        // if (hostElement.__connectedFlg__ !== 4) return;
        // hostElement.__connectedFlg__ = 5;
        // console.log('ytd-engagement-panel-section-list-renderer::attached', hostElement)
        // console.log(5094, 1, 'ytd-engagement-panel-section-list-renderer::attached', hostElement);

        if (
          !hostElement.matches(
            '#panels.ytd-watch-flexy > ytd-engagement-panel-section-list-renderer'
          )
        ) {
          return;
        }

        if (hostElement.hasAttribute000('target-id') && hostElement.hasAttribute000('visibility')) {
          Promise.resolve(hostElement)
            .then(eventMap['ytd-engagement-panel-section-list-renderer::bindTarget'])
            .catch(console.warn);
        } else {
          hostElement.setAttribute000('tyt-egm-panel-jclmd', '');
          moEgmPanelReady.observe(hostElement, {
            attributes: true,
            attributeFilter: ['visibility', 'target-id'],
          });
        }
      },

      'ytd-engagement-panel-section-list-renderer::detached': hostElement => {
        // if (inPageRearrange) return;

        DEBUG_5084 && console.log(5084, 'ytd-engagement-panel-section-list-renderer::detached');
        if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return;
        if (hostElement.isConnected !== false) return;
        // if (hostElement.__connectedFlg__ !== 8) return;
        // hostElement.__connectedFlg__ = 9;
        if (hostElement.hasAttribute000('tyt-egm-panel')) {
          hostElement.removeAttribute000('tyt-egm-panel');
          Promise.resolve(lockSet['updateEgmPanelsLock']).then(updateEgmPanels).catch(console.warn);
        } else if (hostElement.hasAttribute000('tyt-egm-panel-jclmd')) {
          hostElement.removeAttribute000('tyt-egm-panel-jclmd');
          moEgmPanelReadyClearFn();
        }
      },

      'ytd-watch-metadata::defined': cProto => {
        if (!cProto.attached498 && typeof cProto.attached === 'function') {
          cProto.attached498 = cProto.attached;
          cProto.attached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-watch-metadata::attached'])
                .catch(console.warn);
            }
            return this.attached498();
          };
        }
        if (!cProto.detached498 && typeof cProto.detached === 'function') {
          cProto.detached498 = cProto.detached;
          cProto.detached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-watch-metadata::detached'])
                .catch(console.warn);
            }
            return this.detached498();
          };
        }

        makeInitAttached('ytd-watch-metadata');
      },

      'ytd-watch-metadata::attached': hostElement => {
        if (invalidFlexyParent(hostElement)) return;

        // if (inPageRearrange) return;

        DEBUG_5084 && console.log(5084, 'ytd-watch-metadata::attached');
        if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true;
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }
        if (hostElement.isConnected !== true) return;
        // if (hostElement.__connectedFlg__ !== 4) return;
        // hostElement.__connectedFlg__ = 5;

        if (plugin.fullChannelNameOnHover.activated) {
          plugin.fullChannelNameOnHover.onNavigateFinish();
        }
      },

      'ytd-watch-metadata::detached': hostElement => {
        // if (inPageRearrange) return;

        DEBUG_5084 && console.log(5084, 'ytd-watch-metadata::detached');
        if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return;
        if (hostElement.isConnected !== false) return;
        // if (hostElement.__connectedFlg__ !== 8) return;
        // hostElement.__connectedFlg__ = 9;
      },

      'ytd-playlist-panel-renderer::defined': cProto => {
        if (!cProto.attached498 && typeof cProto.attached === 'function') {
          cProto.attached498 = cProto.attached;
          cProto.attached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-playlist-panel-renderer::attached'])
                .catch(console.warn);
            }
            return this.attached498();
          };
        }
        if (!cProto.detached498 && typeof cProto.detached === 'function') {
          cProto.detached498 = cProto.detached;
          cProto.detached = function () {
            if (!inPageRearrange) {
              Promise.resolve(this.hostElement)
                .then(eventMap['ytd-playlist-panel-renderer::detached'])
                .catch(console.warn);
            }
            return this.detached498();
          };
        }

        makeInitAttached('ytd-playlist-panel-renderer');
      },

      'ytd-playlist-panel-renderer::attached': hostElement => {
        if (invalidFlexyParent(hostElement)) return;

        // if (inPageRearrange) return;

        DEBUG_5084 && console.log(5084, 'ytd-playlist-panel-renderer::attached');
        if (hostElement instanceof Element) hostElement[__attachedSymbol__] = true;
        if (
          !(hostElement instanceof HTMLElement_) ||
          !(hostElement.classList.length > 0) ||
          hostElement.closest('noscript')
        ) {
          return;
        }
        if (hostElement.isConnected !== true) return;
        // if (hostElement.__connectedFlg__ !== 4) return;
        // hostElement.__connectedFlg__ = 5;

        elements.playlist = hostElement;

        aoPlayList.observe(hostElement, {
          attributes: true,
          attributeFilter: ['hidden', 'collapsed', 'attr-1y6nu'],
        });
        hostElement.incAttribute111('attr-1y6nu');
      },

      'ytd-playlist-panel-renderer::detached': hostElement => {
        // if (inPageRearrange) return;

        DEBUG_5084 && console.log(5084, 'ytd-playlist-panel-renderer::detached');
        if (!(hostElement instanceof HTMLElement_) || hostElement.closest('noscript')) return;
        if (hostElement.isConnected !== false) return;
        // if (hostElement.__connectedFlg__ !== 8) return;
        // hostElement.__connectedFlg__ = 9;
      },

      _yt_playerProvided: () => {
        mLoaded.flag |= 4;
        document.documentElement.setAttribute111('tabview-loaded', mLoaded.makeString());
      },
      relatedElementProvided: target => {
        if (target.closest('[hidden]')) return;
        elements.related = target;
        videosElementProvidedPromise.resolve();
      },
      onceInfoExpanderElementProvidedPromised: () => {
        const ytdFlexyElm = elements.flexy;
        if (ytdFlexyElm) {
          ytdFlexyElm.setAttribute111('hide-default-text-inline-expander', '');
        }
      },

      refreshSecondaryInner: lockId => {
        if (lockGet['refreshSecondaryInnerLock'] !== lockId) return;
        /*
   
        ytd-watch-flexy:not([panels-beside-player]):not([fixed-panels]) #panels-full-bleed-container.ytd-watch-flexy{
            display: none;}
   
  #player-full-bleed-container.ytd-watch-flexy{
      position: relative;
      flex: 1;}
   
        */

        const ytdFlexyElm = elements.flexy;
        // if(ytdFlexyElm && ytdFlexyElm.matches('ytd-watch-flexy[fixed-panels][theater]')){
        //   // ytdFlexyElm.fixedPanels = true;
        //   ytdFlexyElm.removeAttribute000('fixed-panels');
        // }

        if (
          ytdFlexyElm &&
          ytdFlexyElm.matches(
            'ytd-watch-flexy[theater][full-bleed-player]:not([full-bleed-no-max-width-columns])'
          )
        ) {
          // ytdFlexyElm.fullBleedNoMaxWidthColumns = true;
          ytdFlexyElm.setAttribute111('full-bleed-no-max-width-columns', '');
        }

        const related = elements.related;
        if (related && related.isConnected && !related.closest('#right-tabs #tab-videos')) {
          qs('#tab-videos').assignChildren111(null, related, null);
        }
        const infoExpander = elements.infoExpander;
        if (
          infoExpander &&
          infoExpander.isConnected &&
          !infoExpander.closest('#right-tabs #tab-info')
        ) {
          qs('#tab-info').assignChildren111(null, infoExpander, null);
        } else {
          // if (infoExpander && ytdFlexyElm && shouldFixInfo) {
          //   shouldFixInfo = false;
          //   Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn);
          // }
        }

        const commentsArea = elements.comments;
        if (commentsArea) {
          const isConnected = commentsArea.isConnected;
          if (isConnected && !commentsArea.closest('#right-tabs #tab-comments')) {
            const tab = qs('#tab-comments');
            tab.assignChildren111(null, commentsArea, null);
          } else {
            // if (!isConnected || tab.classList.contains('tab-content-hidden')) removeKeepCommentsScroller();
          }
        }
      },

      'yt-navigate-finish': _evt => {
        // Performance: the global document-subtree observer is expensive on home/feed/playlist.
        // Toggle it based on whether the watch player is present.
        if (typeof shouldActivateMoOverall === 'function') {
          if (shouldActivateMoOverall()) {
            activateMoOverall();
          } else {
            deactivateMoOverall();
          }
        }

        const ytdAppElm = qs('ytd-page-manager#page-manager.style-scope.ytd-app');
        const ytdAppCnt = insp(ytdAppElm);
        pageType = ytdAppCnt ? (ytdAppCnt.data || 0).page : null;

        if (!qs('ytd-watch-flexy #player')) return;
        // shouldFixInfo = true;
        // console.log('yt-navigate-finish')
        const flexyArr = qsAll('ytd-watch-flexy').filter(
          e => !e.closest('[hidden]') && e.querySelector('#player')
        );
        if (flexyArr.length === 1) {
          // const lockId = lockSet['yt-navigate-finish-videos'];
          elements.flexy = flexyArr[0];
          if (isRightTabsInserted) {
            Promise.resolve(lockSet['refreshSecondaryInnerLock'])
              .then(eventMap['refreshSecondaryInner'])
              .catch(console.warn);
            Promise.resolve(lockSet['removeKeepCommentsScrollerLock'])
              .then(removeKeepCommentsScroller)
              .catch(console.warn);
          } else {
            navigateFinishedPromise.resolve();
            if (plugin.minibrowser.toUse) plugin.minibrowser.activate();
            if (plugin.autoExpandInfoDesc.toUse) plugin.autoExpandInfoDesc.activate();
            if (plugin.fullChannelNameOnHover.toUse) plugin.fullChannelNameOnHover.activate();
          }
          const chat = elements.chat;
          if (chat instanceof Element) {
            chat.setAttribute111('tyt-active-chat-frame', 'CF'); // chat and flexy ready
          }
          const infoExpander = elements.infoExpander;
          if (infoExpander && infoExpander.closest('#right-tabs')) {
            Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn);
          }
          Promise.resolve(lockSet['layoutFixLock']).then(layoutFix);
          if (plugin.fullChannelNameOnHover.activated) {
            plugin.fullChannelNameOnHover.onNavigateFinish();
          }
        }
      },

      onceInsertRightTabs: () => {
        // if(lockId !== lockGet['yt-navigate-finish-videos']) return;
        const related = elements.related;
        let rightTabs = qs('#right-tabs');
        if (!qs('#right-tabs') && related) {
          getLangForPage();
          const docTmp = document.createElement('template');
          docTmp.innerHTML = createHTML(getTabsHTML());
          const newElm = docTmp.content.firstElementChild;
          if (newElm !== null) {
            inPageRearrange = true;
            related.parentNode.insertBefore000(newElm, related);
            inPageRearrange = false;
          }
          rightTabs = newElm;
          rightTabs
            .querySelector('[tyt-tab-content="#tab-comments"]')
            .classList.add('tab-btn-hidden');

          const secondaryWrapper = document.createElement('secondary-wrapper');
          secondaryWrapper.classList.add('tabview-secondary-wrapper');
          secondaryWrapper.id = 'secondary-inner-wrapper';
          const secondaryInner = qs('#secondary-inner.style-scope.ytd-watch-flexy');
          if (!secondaryInner) return;

          inPageRearrange = true;
          secondaryWrapper.replaceChildren000(...secondaryInner.childNodes);
          secondaryInner.insertBefore000(secondaryWrapper, secondaryInner.firstChild);
          inPageRearrange = false;

          rightTabs
            .querySelector('#material-tabs')
            .addEventListener('click', eventMap['tabs-btn-click'], true);

          inPageRearrange = true;
          if (!rightTabs.closest('secondary-wrapper')) secondaryWrapper.appendChild000(rightTabs);
          inPageRearrange = false;
        }
        if (rightTabs) {
          isRightTabsInserted = true;
          const ioTabBtns = new IntersectionObserver(
            entries => {
              for (const entry of entries) {
                const rect = entry.boundingClientRect;
                entry.target.classList.toggle('tab-btn-visible', rect.width && rect.height);
              }
            },
            { rootMargin: '0px' }
          );
          for (const btn of qsAll('.tab-btn[tyt-tab-content]')) {
            ioTabBtns.observe(btn);
          }
          if (!related.closest('#right-tabs')) {
            qs('#tab-videos').assignChildren111(null, related, null);
          }
          const infoExpander = elements.infoExpander;
          if (infoExpander && !infoExpander.closest('#right-tabs')) {
            qs('#tab-info').assignChildren111(null, infoExpander, null);
          }
          const commentsArea = elements.comments;
          if (commentsArea && !commentsArea.closest('#right-tabs')) {
            qs('#tab-comments').assignChildren111(null, commentsArea, null);
          }
          rightTabsProvidedPromise.resolve();
          roRightTabs.disconnect();
          roRightTabs.observe(rightTabs);
          const ytdFlexyElm = elements.flexy;
          const aoFlexy = new MutationObserver(eventMap['aoFlexyFn']);
          aoFlexy.observe(ytdFlexyElm, { attributes: true });
          // Promise.resolve(lockSet['tabsStatusCorrectionLock']).then(eventMap['tabsStatusCorrection']).catch(console.warn);

          Promise.resolve(lockSet['fixInitialTabStateLock'])
            .then(eventMap['fixInitialTabStateFn'])
            .catch(console.warn);

          ytdFlexyElm.incAttribute111('attr-7qlsy'); // tabsStatusCorrectionLock and video-id
        }
      },

      aoFlexyFn: () => {
        Promise.resolve(lockSet['checkCommentsShouldBeHiddenLock'])
          .then(eventMap['checkCommentsShouldBeHidden'])
          .catch(console.warn);

        Promise.resolve(lockSet['refreshSecondaryInnerLock'])
          .then(eventMap['refreshSecondaryInner'])
          .catch(console.warn);

        Promise.resolve(lockSet['tabsStatusCorrectionLock'])
          .then(eventMap['tabsStatusCorrection'])
          .catch(console.warn);

        const videoId = getCurrentVideoId();
        if (videoId !== tmpLastVideoId) {
          tmpLastVideoId = videoId;
          Promise.resolve(lockSet['updateOnVideoIdChangedLock'])
            .then(eventMap['updateOnVideoIdChanged'])
            .catch(console.warn);
        }
      },

      twoColumnChanged10: lockId => {
        if (lockId !== lockGet['twoColumnChanged10Lock']) return;
        for (const continuation of qsAll(
          '#tab-videos ytd-watch-next-secondary-results-renderer ytd-continuation-item-renderer'
        )) {
          if (continuation.closest('[hidden]')) continue;
          const cnt = insp(continuation);
          if (typeof cnt.showButton === 'boolean') {
            if (cnt.showButton === false) continue;
            cnt.showButton = false;
            const behavior = cnt.ytRendererBehavior || cnt;
            if (typeof behavior.invalidate === 'function') {
              behavior.invalidate(!1);
            }
          }
        }
      },

      tabsStatusCorrection: lockId => {
        if (lockId !== lockGet['tabsStatusCorrectionLock']) return;
        const ytdFlexyElm = elements.flexy;
        if (!ytdFlexyElm) return;
        const p = tabAStatus;
        const q = calculationFn(p, 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 4096);
        let resetForPanelDisappeared = false;

        // Store theater mode state before fullscreen changes
        const wasTheaterBeforeFullscreen = (p & 1) === 1;
        const isEnteringFullscreen = (p & 64) === 0 && (q & 64) === 64;
        const isExitingFullscreen = (p & 64) === 64 && (q & 64) === 0;

        if (p !== q) {
          let actioned = false;
          let special = 0;
          if (plugin['external.ytlstm'].activated) {
            if (q & 64) {
              // ignore fullscreen - but preserve theater state
              if (isEnteringFullscreen && wasTheaterBeforeFullscreen) {
                // Preserve theater mode when entering fullscreen
                setTimeout(() => {
                  if (isTheater()) {
                    // Theater mode is still active, no action needed
                  } else {
                    // Theater mode was lost, restore it
                    const sizeBtn = qs('ytd-watch-flexy #ytd-player button.ytp-size-button');
                    if (sizeBtn && !isTheater()) {
                      sizeBtn.click();
                    }
                  }
                }, 300);
              }
            } else if (
              (p & (1 | 2 | 4 | 8 | 16 | 4096)) === (1 | 0 | 0 | 8 | 16 | 4096) &&
              (q & (1 | 2 | 4 | 8 | 16 | 4096)) === (1 | 0 | 4 | 0 | 16 | 4096)
            ) {
              special = 3;
            } else if ((q & (1 | 16)) === (1 | 16) && qs('[data-ytlstm-theater-mode]')) {
              special = 1;
            } else if (
              (q & (1 | 8 | 16)) === (1 | 8 | 16) &&
              qs('[is-two-columns_][theater][tyt-chat="+"]')
            ) {
              special = 2;
            }
          } else {
            // Standard behavior - preserve theater mode during fullscreen transitions
            if (isExitingFullscreen && wasTheaterBeforeFullscreen) {
              // Restore theater mode after exiting fullscreen
              setTimeout(() => {
                if (!isTheater()) {
                  const sizeBtn = qs('ytd-watch-flexy #ytd-player button.ytp-size-button');
                  if (sizeBtn) {
                    sizeBtn.click();
                  }
                }
              }, 300);
            }
          }
          if (special) {
            // special
          } else if ((p & 128) === 0 && (q & 128) === 128) {
            lastPanel = 'playlist';
          } else if ((p & 8) === 0 && (q & 8) === 8) {
            lastPanel = 'chat';
          } else if (
            (((p & 4) === 4 && (q & (4 | 8)) === (0 | 0)) ||
              ((p & 8) === 8 && (q & (4 | 8)) === (0 | 0))) &&
            lastPanel === 'chat'
          ) {
            // 24 -> 16 = -8; 'd'
            lastPanel = lastTab || '';
            resetForPanelDisappeared = true;
          } else if ((p & (4 | 8)) === 8 && (q & (4 | 8)) === 4 && lastPanel === 'chat') {
            // click close
            lastPanel = lastTab || '';
            resetForPanelDisappeared = true;
          } else if ((p & 128) === 128 && (q & 128) === 0 && lastPanel === 'playlist') {
            lastPanel = lastTab || '';
            resetForPanelDisappeared = true;
          }
          tabAStatus = q;

          if (special) {
            if (special === 1) {
              if (ytdFlexyElm.getAttribute('tyt-chat') !== '+') {
                ytBtnExpandChat();
              }
              if (ytdFlexyElm.getAttribute('tyt-tab')) {
                switchToTab(null);
              }
            } else if (special === 2) {
              ytBtnCollapseChat();
            } else if (special === 3) {
              ytBtnCancelTheater();
              if (lastTab) {
                switchToTab(lastTab);
              }
            }
            return;
          }

          let bFixForResizedTab = false;

          if ((q ^ 2) === 2 && bFixForResizedTabLater) {
            bFixForResizedTab = true;
          }

          if (((p & 16) === 16) & ((q & 16) === 0)) {
            Promise.resolve(lockSet['twoColumnChanged10Lock'])
              .then(eventMap['twoColumnChanged10'])
              .catch(console.warn);
          }

          if (((p & 2) === 2) ^ ((q & 2) === 2) && (q & 2) === 2) {
            bFixForResizedTab = true;
          }

          // p->q +2
          if ((p & 2) === 0 && (q & 2) === 2 && (p & 128) === 128 && (q & 128) === 128) {
            lastPanel = lastTab || '';
            ytBtnClosePlaylist();
            actioned = true;
          }

          // p->q +8
          if (
            (p & (8 | 128)) === (0 | 128) &&
            (q & (8 | 128)) === (8 | 128) &&
            lastPanel === 'chat'
          ) {
            lastPanel = lastTab || '';
            ytBtnClosePlaylist();
            actioned = true;
          }

          if (
            (p & (1 | 2 | 4 | 8 | 16 | 32 | 64 | 128)) === (1 | 2 | 0 | 8 | 16) &&
            (q & (1 | 2 | 4 | 8 | 16 | 32 | 64 | 128)) === (0 | 2 | 0 | 8 | 16)
          ) {
            // external.ytlstm case
            lastPanel = lastTab || '';
            ytBtnCollapseChat();
            actioned = true;
          }
          // p->q +128
          if (
            (p & (2 | 128)) === (2 | 0) &&
            (q & (2 | 128)) === (2 | 128) &&
            lastPanel === 'playlist'
          ) {
            switchToTab(null);
            actioned = true;
          }

          // p->q +128
          if (
            (p & (8 | 128)) === (8 | 0) &&
            (q & (8 | 128)) === (8 | 128) &&
            lastPanel === 'playlist'
          ) {
            lastPanel = lastTab || '';
            ytBtnCollapseChat();
            actioned = true;
          }

          // p->q +128
          if ((p & (1 | 16 | 128)) === (1 | 16) && (q & (1 | 16 | 128)) === (1 | 16 | 128)) {
            ytBtnCancelTheater();
            actioned = true;
          }

          // p->q +1
          if ((p & (1 | 16 | 128)) === (16 | 128) && (q & (1 | 16 | 128)) === (1 | 16 | 128)) {
            lastPanel = lastTab || '';
            ytBtnClosePlaylist();
            actioned = true;
          }

          if ((q & 64) === 64) {
            actioned = false;
          } else if ((p & 64) === 64 && (q & 64) === 0) {
            // p->q -64

            if ((q & 32) === 32) {
              ytBtnCloseEngagementPanels();
            }

            if ((q & (2 | 8)) === (2 | 8)) {
              if (lastPanel === 'chat') {
                switchToTab(null);
                actioned = true;
              } else if (lastPanel) {
                ytBtnCollapseChat();
                actioned = true;
              }
            }
          } else if (
            (p & (1 | 2 | 8 | 16 | 32)) === (1 | 0 | 0 | 16 | 0) &&
            (q & (1 | 2 | 8 | 16 | 32)) === (1 | 0 | 8 | 16 | 0)
          ) {
            // p->q +8
            ytBtnCancelTheater();
            actioned = true;
          } else if (
            (p & (1 | 16 | 32)) === (0 | 16 | 0) &&
            (q & (1 | 16 | 32)) === (0 | 16 | 32) &&
            (q & (2 | 8)) > 0
          ) {
            // p->q +32
            if (q & 2) {
              switchToTab(null);
              actioned = true;
            }
            if (q & 8) {
              ytBtnCollapseChat();
              actioned = true;
            }
          } else if (
            (p & (1 | 16 | 8 | 2)) === (16 | 8) &&
            (q & (1 | 16 | 8 | 2)) === 16 &&
            (q & 128) === 0
          ) {
            // p->q -8
            if (lastTab) {
              switchToTab(lastTab);
              actioned = true;
            }
          } else if ((p & 1) === 0 && (q & 1) === 1) {
            // p->q +1
            if ((q & 32) === 32) {
              ytBtnCloseEngagementPanels();
            }
            if ((p & 9) === 8 && (q & 9) === 9) {
              ytBtnCollapseChat();
            }
            switchToTab(null);
            actioned = true;
          } else if ((p & 3) === 1 && (q & 3) === 3) {
            // p->q +2
            ytBtnCancelTheater();
            actioned = true;
          } else if ((p & 10) === 2 && (q & 10) === 10) {
            // p->q +8
            switchToTab(null);
            actioned = true;
          } else if ((p & (8 | 32)) === (0 | 32) && (q & (8 | 32)) === (8 | 32)) {
            // p->q +8
            ytBtnCloseEngagementPanels();
            actioned = true;
          } else if ((p & (2 | 32)) === (0 | 32) && (q & (2 | 32)) === (2 | 32)) {
            // p->q +2
            ytBtnCloseEngagementPanels();
            actioned = true;
          } else if ((p & (2 | 8)) === (0 | 8) && (q & (2 | 8)) === (2 | 8)) {
            // p->q +2
            ytBtnCollapseChat();
            actioned = true;
            // if( lastPanel && (p & (1|16) === 16)  && (q & (1 | 16 | 8 | 2)) === (16) ){
            //   switchToTab(lastTab)
            //   actioned = true;
            // }
          } else if ((p & 1) === 1 && (q & (1 | 32)) === (0 | 0)) {
            // p->q -1
            if (lastPanel === 'chat') {
              ytBtnExpandChat();
              actioned = true;
            } else if (lastPanel === lastTab && lastTab) {
              switchToTab(lastTab);
              actioned = true;
            }
          }

          // 24 20
          // 8 16   4 16

          if (!actioned && (q & 128) === 128) {
            lastPanel = 'playlist';
            if ((q & 2) === 2) {
              switchToTab(null);
              actioned = true;
            }
          }

          let shouldDoAutoFix = false;

          if ((p & 2) === 2 && (q & (2 | 128)) === (0 | 128)) {
            // p->q -2
          } else if ((p & 8) === 8 && (q & (8 | 128)) === (0 | 128)) {
            // p->q -8
          } else if (
            !actioned &&
            (p & (1 | 16)) === 16 &&
            (q & (1 | 16 | 8 | 2 | 32 | 64)) === (16 | 0 | 0)
          ) {
            shouldDoAutoFix = true;
          } else if ((q & (1 | 2 | 4 | 8 | 16 | 32 | 64 | 128)) === (4 | 16)) {
            shouldDoAutoFix = true;
          }

          if (shouldDoAutoFix) {
            if (lastPanel === 'chat') {
              ytBtnExpandChat();
              actioned = true;
            } else if (lastPanel === 'playlist') {
              ytBtnOpenPlaylist();
              actioned = true;
            } else if (lastTab) {
              switchToTab(lastTab);
              actioned = true;
            } else if (resetForPanelDisappeared) {
              // if lastTab is undefined
              Promise.resolve(lockSet['fixInitialTabStateLock'])
                .then(eventMap['fixInitialTabStateFn'])
                .catch(console.warn);
              actioned = true;
            }
          }

          if (bFixForResizedTab) {
            bFixForResizedTabLater = false;
            Promise.resolve(0).then(eventMap['fixForTabDisplay']).catch(console.warn);
          }

          if (((p & 16) === 16) ^ ((q & 16) === 16)) {
            Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn);
            Promise.resolve(lockSet['removeKeepCommentsScrollerLock'])
              .then(removeKeepCommentsScroller)
              .catch(console.warn);
            Promise.resolve(lockSet['layoutFixLock']).then(layoutFix).catch(console.warn);
          }
        }
      },

      updateOnVideoIdChanged: lockId => {
        if (lockId !== lockGet['updateOnVideoIdChangedLock']) return;
        const videoId = tmpLastVideoId;
        if (!videoId) return;

        const bodyRenderer = elements.infoExpanderRendererBack;
        const bodyRendererNew = elements.infoExpanderRendererFront;

        if (bodyRendererNew && bodyRenderer) {
          insp(bodyRendererNew).data = insp(bodyRenderer).data;
          // if ((bodyRendererNew.hasAttribute('hidden') ? 1 : 0) ^ (bodyRenderer.hasAttribute('hidden') ? 1 : 0)) {
          //   if (bodyRenderer.hasAttribute('hidden')) bodyRendererNew.setAttribute('hidden', '');
          //   else bodyRendererNew.removeAttribute('hidden');
          // }
        }

        Promise.resolve(lockSet['infoFixLock']).then(infoFix).catch(console.warn);
      },

      fixInitialTabStateFn: async lockId => {
        // console.log('fixInitialTabStateFn 0a');
        if (lockGet['fixInitialTabStateLock'] !== lockId) return;
        // console.log('fixInitialTabStateFn 0b');
        const delayTime = fixInitialTabStateK > 0 ? 200 : 1;
        await delayPn(delayTime);
        if (lockGet['fixInitialTabStateLock'] !== lockId) return;
        // console.log('fixInitialTabStateFn 0c');
        const kTab = qs('[tyt-tab]');
        const qTab =
          !kTab || kTab.getAttribute('tyt-tab') === ''
            ? checkElementExist('ytd-watch-flexy[is-two-columns_]', '[hidden]')
            : null;
        if (checkElementExist('ytd-playlist-panel-renderer#playlist', '[hidden], [collapsed]')) {
          DEBUG_5085 && console.log('fixInitialTabStateFn 1p');
          switchToTab(null);
        } else if (checkElementExist('ytd-live-chat-frame#chat', '[hidden], [collapsed]')) {
          DEBUG_5085 && console.log('fixInitialTabStateFn 1a');
          switchToTab(null);
          if (checkElementExist('ytd-watch-flexy[theater]', '[hidden]')) {
            ytBtnCollapseChat();
          }
        } else if (qTab) {
          const hasTheater = qTab.hasAttribute('theater');
          if (!hasTheater) {
            DEBUG_5085 && console.log('fixInitialTabStateFn 1b');
            const btn0 = qs('.tab-btn-visible'); // or default button
            if (btn0) {
              switchToTab(btn0);
            } else {
              switchToTab(null);
            }
          } else {
            DEBUG_5085 && console.log('fixInitialTabStateFn 1c');
            switchToTab(null);
          }
        } else {
          DEBUG_5085 && console.log('fixInitialTabStateFn 1z');
        }
        // console.log('fixInitialTabStateFn 0d');
        fixInitialTabStateK++;
      },

      'tabs-btn-click': evt => {
        const target = evt.target;
        if (
          target instanceof HTMLElement_ &&
          target.classList.contains('tab-btn') &&
          target.hasAttribute000('tyt-tab-content')
        ) {
          evt.preventDefault();
          evt.stopPropagation();
          evt.stopImmediatePropagation();

          const activeLink = target;

          switchToTab(activeLink);
        }
      },
    };

    Promise.all([videosElementProvidedPromise, navigateFinishedPromise])
      .then(eventMap['onceInsertRightTabs'])
      .catch(console.warn);
    Promise.all([navigateFinishedPromise, infoExpanderElementProvidedPromise])
      .then(eventMap['onceInfoExpanderElementProvidedPromised'])
      .catch(console.warn);

    const isCustomElementsProvided =
      typeof customElements !== 'undefined' &&
      typeof (customElements || 0).whenDefined === 'function';

    const promiseForCustomYtElementsReady = isCustomElementsProvided
      ? Promise.resolve(0)
      : new Promise(callback => {
          const EVENT_KEY_ON_REGISTRY_READY = 'ytI-ce-registry-created';
          if (typeof customElements === 'undefined') {
            if (!('__CE_registry' in document)) {
              // https://github.com/webcomponents/polyfills/
              Object.defineProperty(document, '__CE_registry', {
                get() {
                  // return undefined
                },
                set(nv) {
                  if (typeof nv == 'object') {
                    delete this.__CE_registry;
                    this.__CE_registry = nv;
                    this.dispatchEvent(new CustomEvent(EVENT_KEY_ON_REGISTRY_READY));
                  }
                  return true;
                },
                enumerable: false,
                configurable: true,
              });
            }
            let eventHandler = _evt => {
              document.removeEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
              const f = callback;
              callback = null;
              eventHandler = null;
              f();
            };
            document.addEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
          } else {
            callback();
          }
        });

    const _retrieveCE = async nodeName => {
      try {
        isCustomElementsProvided || (await promiseForCustomYtElementsReady);
        await customElements.whenDefined(nodeName);
      } catch (e) {
        console.warn(e);
      }
    };

    const retrieveCE = async nodeName => {
      try {
        isCustomElementsProvided || (await promiseForCustomYtElementsReady);
        await customElements.whenDefined(nodeName);
        const dummy = qs(nodeName) || document.createElement(nodeName);
        const cProto = insp(dummy).constructor.prototype;
        return cProto;
      } catch (e) {
        console.warn(e);
      }
    };

    const moOverallRes = {
      _yt_playerProvided: () => (window || 0)._yt_player || 0 || 0,
    };

    let promiseWaitNext = null;
    const moOverall = new MutationObserver(() => {
      if (promiseWaitNext) {
        promiseWaitNext.resolve();
        promiseWaitNext = null;
      }

      if (typeof moOverallRes._yt_playerProvided === 'function') {
        const r = moOverallRes._yt_playerProvided();
        if (r) {
          moOverallRes._yt_playerProvided = r;
          eventMap._yt_playerProvided();
        }
      }
    });

    // Performance: observing the entire document subtree is expensive on home/feed/playlist.
    // Enable it only when the watch player exists.
    let moOverallActive = false;
    const shouldActivateMoOverall = () => {
      try {
        return !!qs('ytd-watch-flexy #player');
      } catch {
        return false;
      }
    };
    const activateMoOverall = () => {
      if (moOverallActive) return;
      moOverall.observe(document, { subtree: true, childList: true });
      moOverallActive = true;
    };
    const deactivateMoOverall = () => {
      if (!moOverallActive) return;
      moOverall.disconnect();
      moOverallActive = false;
    };

    if (shouldActivateMoOverall()) {
      activateMoOverall();
    }

    const moEgmPanelReady = new MutationObserver(mutations => {
      for (const mutation of mutations) {
        const target = mutation.target;
        if (!target.hasAttribute000('tyt-egm-panel-jclmd')) continue;
        if (target.hasAttribute000('target-id') && target.hasAttribute000('visibility')) {
          target.removeAttribute000('tyt-egm-panel-jclmd');
          moEgmPanelReadyClearFn();
          Promise.resolve(target)
            .then(eventMap['ytd-engagement-panel-section-list-renderer::bindTarget'])
            .catch(console.warn);
        }
      }
    });

    const moEgmPanelReadyClearFn = () => {
      if (qs('[tyt-egm-panel-jclmd]') === null) {
        moEgmPanelReady.takeRecords();
        moEgmPanelReady.disconnect();
      }
    };

    document.addEventListener('yt-navigate-finish', eventMap['yt-navigate-finish'], false);

    document.addEventListener(
      'animationstart',
      evt => {
        const f = eventMap[evt.animationName];
        if (typeof f === 'function') f(evt.target);
      },
      capturePassive
    );

    // console.log('hi122')

    mLoaded.flag |= 1;
    document.documentElement.setAttribute111('tabview-loaded', mLoaded.makeString());

    promiseForCustomYtElementsReady.then(eventMap['ceHack']).catch(console.warn);

    _executionFinished = 1;
  } catch (e) {
    console.error('error 0xF491', e);
  }
};
const styles = {
  main: `
  @keyframes relatedElementProvided{0%{background-position-x:3px;}100%{background-position-x:4px;}}
  html[tabview-loaded="icp"] #related.ytd-watch-flexy{animation:relatedElementProvided 1ms linear 0s 1 normal forwards;}
  html[tabview-loaded="icp"] #right-tabs #related.ytd-watch-flexy,html[tabview-loaded="icp"] [hidden] #related.ytd-watch-flexy,html[tabview-loaded="icp"] #right-tabs ytd-expander#expander,html[tabview-loaded="icp"] [hidden] ytd-expander#expander,html[tabview-loaded="icp"] ytd-comments ytd-expander#expander{animation:initial;}
  #secondary.ytd-watch-flexy{position:relative;}
  #secondary-inner.style-scope.ytd-watch-flexy{height:100%;}
  #secondary-inner secondary-wrapper{display:flex;flex-direction:column;flex-wrap:nowrap;box-sizing:border-box;padding:0;margin:0;border:0;height:100%;max-height:calc(100vh - var(--ytd-toolbar-height,56px));position:absolute;top:0;right:0;left:0;contain:strict;padding:var(--ytd-margin-6x) var(--ytd-margin-6x) var(--ytd-margin-6x) 0;}
  #right-tabs{position:relative;display:flex;padding:0;margin:0;flex-grow:1;flex-direction:column;}
  [tyt-tab=""] #right-tabs{flex-grow:0;}
  [tyt-tab=""] #right-tabs .tab-content{border:0;}
  #right-tabs .tab-content{flex-grow:1;}
  ytd-watch-flexy[hide-default-text-inline-expander] #primary.style-scope.ytd-watch-flexy ytd-text-inline-expander{display:none;}
  ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden{--comment-pre-load-sizing:90px;visibility:collapse;z-index:-1;position:absolute!important;left:2px;top:2px;width:var(--comment-pre-load-sizing)!important;height:var(--comment-pre-load-sizing)!important;display:block!important;pointer-events:none!important;overflow:hidden;contain:strict;border:0;margin:0;padding:0;}
  ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments>ytd-item-section-renderer#sections{display:block!important;overflow:hidden;height:var(--comment-pre-load-sizing);width:var(--comment-pre-load-sizing);contain:strict;border:0;margin:0;padding:0;}
  ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments>ytd-item-section-renderer#sections>#contents{display:flex!important;flex-direction:row;gap:60px;overflow:hidden;height:var(--comment-pre-load-sizing);width:var(--comment-pre-load-sizing);contain:strict;border:0;margin:0;padding:0;}
  ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments #contents{--comment-pre-load-display:none;}
  ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments #contents>*:only-of-type,ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments #contents>*:last-child{--comment-pre-load-display:block;}
  ytd-watch-flexy:not([keep-comments-scroller]) #tab-comments.tab-content-hidden ytd-comments#comments #contents>*{display:var(--comment-pre-load-display)!important;}
  ytd-watch-flexy #tab-comments:not(.tab-content-hidden){pointer-events:auto!important;}
  ytd-watch-flexy #tab-comments:not(.tab-content-hidden) *{pointer-events:auto!important;}
  ytd-watch-flexy #tab-comments:not(.tab-content-hidden) button,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) yt-button-renderer,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) a,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) tp-yt-paper-button,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) [role="button"],ytd-watch-flexy #tab-comments:not(.tab-content-hidden) yt-button-shape{pointer-events:auto!important;}
  ytd-watch-flexy #tab-comments tp-yt-paper-button{white-space:normal;word-break:break-word;max-width:100%;overflow-wrap:break-word;}
  ytd-watch-flexy #tab-comments:not(.tab-content-hidden) ytd-comment-action-buttons-renderer,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) ytd-button-renderer,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) #action-buttons,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) ytd-menu-renderer,ytd-watch-flexy #tab-comments:not(.tab-content-hidden) yt-dropdown-menu{pointer-events:auto!important;}
  #right-tabs #material-tabs{position:relative;display:flex;padding:0;border:1px solid var(--ytd-searchbox-legacy-border-color);overflow:hidden;}
  [tyt-tab] #right-tabs #material-tabs{border-radius:12px;}
  [tyt-tab^="#"] #right-tabs #material-tabs{border-radius:12px 12px 0 0;}
  ytd-watch-flexy:not([is-two-columns_]) #right-tabs #material-tabs{outline:0;}
  #right-tabs #material-tabs a.tab-btn[tyt-tab-content]>*{pointer-events:none;}
  #right-tabs #material-tabs a.tab-btn[tyt-tab-content]>.font-size-right{pointer-events:initial;display:none;}
  ytd-watch-flexy #right-tabs .tab-content{padding:0;box-sizing:border-box;display:block;border:1px solid var(--ytd-searchbox-legacy-border-color);border-top:0;position:relative;top:0;display:flex;flex-direction:row;overflow:hidden;border-radius:0 0 12px 12px;}
  ytd-watch-flexy:not([is-two-columns_]) #right-tabs .tab-content{height:100%;}
  ytd-watch-flexy #right-tabs .tab-content-cld{box-sizing:border-box;position:relative;display:block;width:100%;overflow:auto;--tab-content-padding:var(--ytd-margin-4x);padding:var(--tab-content-padding);contain:layout paint;will-change:scroll-position;}
  .tab-content-cld,#right-tabs,.tab-content{transition:none;animation:none;}
  ytd-watch-flexy #right-tabs .tab-content-cld::-webkit-scrollbar{width:8px;height:8px;}
  ytd-watch-flexy #right-tabs .tab-content-cld::-webkit-scrollbar-track{background:transparent;}
  ytd-watch-flexy #right-tabs .tab-content-cld::-webkit-scrollbar-thumb{background:rgba(144,144,144,.5);border-radius:4px;}
  ytd-watch-flexy #right-tabs .tab-content-cld::-webkit-scrollbar-thumb:hover{background:rgba(170,170,170,.7);}
  #right-tabs #emojis.ytd-commentbox{inset:auto 0 auto 0;width:auto;}
  ytd-watch-flexy[is-two-columns_] #right-tabs .tab-content-cld{height:100%;width:100%;contain:size layout paint style;position:absolute;}
  ytd-watch-flexy #right-tabs .tab-content-cld.tab-content-hidden{display:none;width:100%;contain:size layout paint style;}
  @supports (color:var(--tabview-tab-btn-define)){
  ytd-watch-flexy #right-tabs .tab-btn{background:var(--yt-spec-general-background-a);}
  html{--tyt-tab-btn-flex-grow:1;--tyt-tab-btn-flex-basis:0%;--tyt-tab-bar-color-1-def:#ff4533;--tyt-tab-bar-color-2-def:var(--yt-brand-light-red);--tyt-tab-bar-color-1:var(--main-color,var(--tyt-tab-bar-color-1-def));--tyt-tab-bar-color-2:var(--main-color,var(--tyt-tab-bar-color-2-def));}
  ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]{flex:var(--tyt-tab-btn-flex-grow) 1 var(--tyt-tab-btn-flex-basis);position:relative;display:inline-block;text-decoration:none;text-transform:uppercase;--tyt-tab-btn-color:var(--yt-spec-text-secondary);color:var(--tyt-tab-btn-color);text-align:center;padding:14px 8px 10px;border:0;border-bottom:4px solid transparent;font-weight:500;font-size:12px;line-height:18px;cursor:pointer;transition:border 200ms linear 100ms;background-color:var(--ytd-searchbox-legacy-button-color);text-transform:var(--yt-button-text-transform,inherit);user-select:none!important;overflow:hidden;white-space:nowrap;text-overflow:clip;}
  ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]>svg{height:18px;padding-right:0;vertical-align:bottom;opacity:.5;margin-right:0;color:var(--yt-button-color,inherit);fill:var(--iron-icon-fill-color,currentcolor);stroke:var(--iron-icon-stroke-color,none);pointer-events:none;}
  ytd-watch-flexy #right-tabs .tab-btn{--tabview-btn-txt-ml:8px;}
  ytd-watch-flexy[tyt-comment-disabled] #right-tabs .tab-btn[tyt-tab-content="#tab-comments"]{--tabview-btn-txt-ml:0;}
  ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]>svg+span{margin-left:var(--tabview-btn-txt-ml);}
  ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content].active{font-weight:500;outline:0;--tyt-tab-btn-color:var(--yt-spec-text-primary);background-color:var(--ytd-searchbox-legacy-button-focus-color);border-bottom:2px var(--tyt-tab-bar-color-2) solid;}
  ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content].active svg{opacity:.9;}
  ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]:not(.active):hover{background-color:var(--ytd-searchbox-legacy-button-hover-color);--tyt-tab-btn-color:var(--yt-spec-text-primary);}
  ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content]:not(.active):hover svg{opacity:.9;}
  ytd-watch-flexy #right-tabs .tab-btn[tyt-tab-content].tab-btn-hidden{display:none;}
  ytd-watch-flexy[tyt-comment-disabled] #right-tabs .tab-btn[tyt-tab-content="#tab-comments"],ytd-watch-flexy[tyt-comment-disabled] #right-tabs .tab-btn[tyt-tab-content="#tab-comments"]:hover{--tyt-tab-btn-color:var(--yt-spec-icon-disabled);}
  ytd-watch-flexy[tyt-comment-disabled] #right-tabs .tab-btn[tyt-tab-content="#tab-comments"] span#tyt-cm-count:empty{display:none;}
  ytd-watch-flexy #right-tabs .tab-btn span#tyt-cm-count:empty::after{display:inline-block;width:4em;text-align:left;font-size:inherit;color:currentColor;transform:scaleX(.8);}}
  @supports (color:var(--tyt-cm-count-define)){
  ytd-watch-flexy{--tyt-x-loading-content-letter-spacing:2px;}
  html{--tabview-text-loading:"Loading";--tabview-text-fetching:"Fetching";--tabview-panel-loading:var(--tabview-text-loading);}
  ytd-watch-flexy #right-tabs .tab-btn span#tyt-cm-count:empty::after{content:var(--tabview-text-loading);letter-spacing:var(--tyt-x-loading-content-letter-spacing);}}
  @supports (color:var(--tabview-font-size-btn-define)){
  .font-size-right{display:inline-flex;flex-direction:column;position:absolute;right:0;top:0;bottom:0;width:16px;padding:4px 0;justify-content:space-evenly;align-content:space-evenly;pointer-events:none;}
  html body ytd-watch-flexy.style-scope .font-size-btn{user-select:none!important;}
  .font-size-btn{--tyt-font-size-btn-display:none;display:var(--tyt-font-size-btn-display,none);width:12px;height:12px;color:var(--yt-spec-text-secondary);background-color:var(--yt-spec-badge-chip-background);box-sizing:border-box;cursor:pointer;transform-origin:left top;margin:0;padding:0;position:relative;font-family:'Menlo','Lucida Console','Monaco','Consolas',monospace;line-height:100%;font-weight:900;transition:background-color 90ms linear,color 90ms linear;pointer-events:all;}
  .font-size-btn:hover{background-color:var(--yt-spec-text-primary);color:var(--yt-spec-general-background-a);}
  @supports (zoom:.5){
  .tab-btn .font-size-btn{--tyt-font-size-btn-display:none;}
  .tab-btn.active:hover .font-size-btn{--tyt-font-size-btn-display:inline-block;}
  body ytd-watch-flexy:not([is-two-columns_]) #columns.ytd-watch-flexy{flex-direction:column;}
  body ytd-watch-flexy:not([is-two-columns_]) #secondary.ytd-watch-flexy{display:block;width:100%;box-sizing:border-box;}
  body ytd-watch-flexy:not([is-two-columns_]) #secondary.ytd-watch-flexy secondary-wrapper{padding-left:var(--ytd-margin-6x);contain:content;height:initial;}
  body ytd-watch-flexy:not([is-two-columns_]) #secondary.ytd-watch-flexy secondary-wrapper #right-tabs{overflow:auto;}
  [tyt-chat="+"] { --tyt-chat-grow: 1;}
  [tyt-chat="+"] secondary-wrapper>[tyt-chat-container]{flex-grow:var(--tyt-chat-grow);flex-shrink:0;display:flex;flex-direction:column;}
  [tyt-chat="+"] secondary-wrapper>[tyt-chat-container]>#chat{flex-grow:var(--tyt-chat-grow);}
  ytd-watch-flexy[is-two-columns_]:not([theater]):not([full-bleed-player]) #columns.style-scope.ytd-watch-flexy{min-height:calc(100vh - var(--ytd-toolbar-height,56px));}
  ytd-watch-flexy[is-two-columns_]:not([full-bleed-player]) ytd-live-chat-frame#chat{min-height:initial!important;height:initial!important;}
  ytd-watch-flexy[tyt-tab^="#"]:not([is-two-columns_]):not([tyt-chat="+"]) #right-tabs{min-height:var(--ytd-watch-flexy-chat-max-height);}
  body ytd-watch-flexy:not([is-two-columns_]) #chat.ytd-watch-flexy{margin-top:0;}
  body ytd-watch-flexy:not([is-two-columns_]) ytd-watch-metadata.ytd-watch-flexy{margin-bottom:0;}
  ytd-watch-metadata.ytd-watch-flexy ytd-metadata-row-container-renderer{display:none;}
  #tab-info [show-expand-button] #expand-sizer.ytd-text-inline-expander{visibility:initial;}
  #tab-info #collapse.button.ytd-text-inline-expander {display: none;}
  #tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>#left-arrow-container.ytd-video-description-infocards-section-renderer>#left-arrow,#tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>#right-arrow-container.ytd-video-description-infocards-section-renderer>#right-arrow{border:6px solid transparent;opacity:.65;}
  #tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>#left-arrow-container.ytd-video-description-infocards-section-renderer>#left-arrow:hover,#tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>#right-arrow-container.ytd-video-description-infocards-section-renderer>#right-arrow:hover{opacity:1;}
  #tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>div#left-arrow-container::before{content:'';background:transparent;width:40px;display:block;height:40px;position:absolute;left:-20px;top:0;z-index:-1;}
  #tab-info #social-links.style-scope.ytd-video-description-infocards-section-renderer>div#right-arrow-container::before{content:'';background:transparent;width:40px;display:block;height:40px;position:absolute;right:-20px;top:0;z-index:-1;}
  body ytd-watch-flexy[is-two-columns_][tyt-egm-panel_] #columns.style-scope.ytd-watch-flexy #panels.style-scope.ytd-watch-flexy{flex-grow:1;flex-shrink:0;display:flex;flex-direction:column;}
  body ytd-watch-flexy[is-two-columns_][tyt-egm-panel_] #columns.style-scope.ytd-watch-flexy #panels.style-scope.ytd-watch-flexy ytd-engagement-panel-section-list-renderer[target-id][visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"]{height:initial;max-height:initial;min-height:initial;flex-grow:1;flex-shrink:0;display:flex;flex-direction:column;}
  secondary-wrapper [visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"] ytd-transcript-renderer:not(:empty),secondary-wrapper [visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"] #body.ytd-transcript-renderer:not(:empty),secondary-wrapper [visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED"] #content.ytd-transcript-renderer:not(:empty){flex-grow:1;height:initial;max-height:initial;min-height:initial;}
  secondary-wrapper #content.ytd-engagement-panel-section-list-renderer{position:relative;}
  secondary-wrapper #content.ytd-engagement-panel-section-list-renderer>[panel-target-id]:only-child{contain:style size;}
  secondary-wrapper #content.ytd-engagement-panel-section-list-renderer ytd-transcript-segment-list-renderer.ytd-transcript-search-panel-renderer{flex-grow:1;contain:strict;}
  secondary-wrapper #content.ytd-engagement-panel-section-list-renderer ytd-transcript-segment-renderer.style-scope.ytd-transcript-segment-list-renderer{contain:layout paint style;}
  secondary-wrapper #content.ytd-engagement-panel-section-list-renderer ytd-transcript-segment-renderer.style-scope.ytd-transcript-segment-list-renderer>.segment{contain:layout paint style;}
  body ytd-watch-flexy[theater] #secondary.ytd-watch-flexy{margin-top:var(--ytd-margin-3x);padding-top:0;}
  body ytd-watch-flexy[theater] secondary-wrapper{margin-top:0;padding-top:0;}
  body ytd-watch-flexy[theater] #chat.ytd-watch-flexy{margin-bottom:var(--ytd-margin-2x);}
  ytd-watch-flexy[theater] #right-tabs .tab-btn[tyt-tab-content]{padding:8px 4px 6px;border-bottom:0 solid transparent;}
  ytd-watch-flexy[theater] #playlist.ytd-watch-flexy{margin-bottom:var(--ytd-margin-2x);}
  ytd-watch-flexy[theater] ytd-playlist-panel-renderer[collapsible][collapsed] .header.ytd-playlist-panel-renderer{padding:6px 8px;}
  #tab-comments ytd-comments#comments [field-of-cm-count]{margin-top:0;}
  #tab-info>ytd-expandable-video-description-body-renderer{margin-bottom:var(--ytd-margin-3x);}
  #tab-info [class]:last-child{margin-bottom:0;padding-bottom:0;}
  #tab-info ytd-rich-metadata-row-renderer ytd-rich-metadata-renderer{max-width:initial;}
  ytd-watch-flexy[is-two-columns_] secondary-wrapper #chat.ytd-watch-flexy{margin-bottom:var(--ytd-margin-3x);}
  ytd-watch-flexy[tyt-tab] tp-yt-paper-tooltip{white-space:nowrap;contain:content;}
  ytd-watch-info-text tp-yt-paper-tooltip.style-scope.ytd-watch-info-text{margin-bottom:-300px;margin-top:-96px;}
  [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata{font-size:1.2rem;line-height:1.8rem;}
  [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata yt-animated-rolling-number{font-size:inherit;}
  [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata #info-container.style-scope.ytd-watch-info-text{align-items:center;}
  ytd-watch-flexy[hide-default-text-inline-expander]{--tyt-bottom-watch-metadata-margin:6px;}
  [hide-default-text-inline-expander] #bottom-row #description.ytd-watch-metadata>#description-inner.ytd-watch-metadata{margin:6px 12px;}
  [hide-default-text-inline-expander] ytd-watch-metadata[title-headline-xs] h1.ytd-watch-metadata{font-size:1.8rem;}
  ytd-watch-flexy[is-two-columns_][hide-default-text-inline-expander] #below.style-scope.ytd-watch-flexy ytd-merch-shelf-renderer{padding:0;border:0;margin:0;}
  ytd-watch-flexy[is-two-columns_][hide-default-text-inline-expander] #below.style-scope.ytd-watch-flexy ytd-watch-metadata.ytd-watch-flexy{margin-bottom:6px;}
  #tab-info yt-video-attribute-view-model .yt-video-attribute-view-model--horizontal .yt-video-attribute-view-model__link-container .yt-video-attribute-view-model__hero-section{flex-shrink:0;}
  #tab-info yt-video-attribute-view-model .yt-video-attribute-view-model__overflow-menu{background:var(--yt-emoji-picker-category-background-color);border-radius:99px;}
  #tab-info yt-video-attribute-view-model .yt-video-attribute-view-model--image-square.yt-video-attribute-view-model--image-large .yt-video-attribute-view-model__hero-section{max-height:128px;}
  #tab-info yt-video-attribute-view-model .yt-video-attribute-view-model--image-large .yt-video-attribute-view-model__hero-section{max-width:128px;}
  #tab-info ytd-reel-shelf-renderer #items.yt-horizontal-list-renderer ytd-reel-item-renderer.yt-horizontal-list-renderer{max-width:142px;}
  ytd-watch-info-text#ytd-watch-info-text.style-scope.ytd-watch-metadata #view-count.style-scope.ytd-watch-info-text,ytd-watch-info-text#ytd-watch-info-text.style-scope.ytd-watch-metadata #date-text.style-scope.ytd-watch-info-text{align-items:center;}
  ytd-watch-info-text:not([detailed]) #info.ytd-watch-info-text a.yt-simple-endpoint.yt-formatted-string{pointer-events:none;}
  body ytd-app>ytd-popup-container>tp-yt-iron-dropdown>#contentWrapper>[slot="dropdown-content"]{backdrop-filter:none;}
  #tab-info [tyt-clone-refresh-count]{overflow:visible!important;}
  #tab-info #items.ytd-horizontal-card-list-renderer yt-video-attribute-view-model.ytd-horizontal-card-list-renderer{contain:layout;}
  #tab-info #thumbnail-container.ytd-structured-description-channel-lockup-renderer,#tab-info ytd-media-lockup-renderer[is-compact] #thumbnail-container.ytd-media-lockup-renderer{flex-shrink:0;}
  secondary-wrapper ytd-donation-unavailable-renderer{--ytd-margin-6x:var(--ytd-margin-2x);--ytd-margin-5x:var(--ytd-margin-2x);--ytd-margin-4x:var(--ytd-margin-2x);--ytd-margin-3x:var(--ytd-margin-2x);}
  [tyt-no-less-btn] #less{display:none;}
  .tyt-metadata-hover-resized #purchase-button,.tyt-metadata-hover-resized #sponsor-button,.tyt-metadata-hover-resized #analytics-button,.tyt-metadata-hover-resized #subscribe-button{display:none!important;}
  .tyt-metadata-hover #upload-info{max-width:max-content;min-width:max-content;flex-basis:100vw;flex-shrink:0;}
  .tyt-info-invisible{display:none;}
  [tyt-playlist-expanded] secondary-wrapper>ytd-playlist-panel-renderer#playlist{overflow:auto;flex-shrink:1;flex-grow:1;max-height:unset!important;}
  [tyt-playlist-expanded] secondary-wrapper>ytd-playlist-panel-renderer#playlist>#container{max-height:unset!important;}
  secondary-wrapper ytd-playlist-panel-renderer{--ytd-margin-6x:var(--ytd-margin-3x);}
  #tab-info ytd-structured-description-playlist-lockup-renderer[collections] #playlist-thumbnail.style-scope.ytd-structured-description-playlist-lockup-renderer{max-width:100%;}
  #tab-info ytd-structured-description-playlist-lockup-renderer[collections] #lockup-container.ytd-structured-description-playlist-lockup-renderer{padding:1px;}
  #tab-info ytd-structured-description-playlist-lockup-renderer[collections] #thumbnail.ytd-structured-description-playlist-lockup-renderer{outline:1px solid rgba(127,127,127,.5);}
  ytd-live-chat-frame#chat[collapsed] ytd-message-renderer~#show-hide-button.ytd-live-chat-frame>ytd-toggle-button-renderer.ytd-live-chat-frame{padding:0;}
  ytd-watch-flexy{--tyt-bottom-watch-metadata-margin:12px;}
  ytd-watch-flexy[rounded-info-panel],ytd-watch-flexy[rounded-player-large]{--tyt-rounded-a1:12px;}
  #bottom-row.style-scope.ytd-watch-metadata .item.ytd-watch-metadata{margin-right:var(--tyt-bottom-watch-metadata-margin,12px);margin-top:var(--tyt-bottom-watch-metadata-margin,12px);}
  #cinematics{contain:layout style size;}
  ytd-watch-flexy[is-two-columns_]{contain:layout style;}
  .yt-spec-touch-feedback-shape--touch-response .yt-spec-touch-feedback-shape__fill{background-color:transparent;}
  /* plugin: external.ytlstm */
  body[data-ytlstm-theater-mode] #secondary-inner[class] > secondary-wrapper[class]:not(#chat-container):not(#chat) {display: flex !important;}  
  body[data-ytlstm-theater-mode] secondary-wrapper {all: unset;height: 100vh;}
  body[data-ytlstm-theater-mode] #right-tabs {display: none;}
  body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] [tyt-chat="+"] {--tyt-chat-grow: unset;}
  body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #columns.style-scope.ytd-watch-flexy,
  body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #secondary.style-scope.ytd-watch-flexy,
  body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #secondary-inner.style-scope.ytd-watch-flexy,
  body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] secondary-wrapper,
  body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #chat-container.style-scope,
  body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] [tyt-chat-container].style-scope {pointer-events: none;}
  body[data-ytlstm-theater-mode] [data-ytlstm-chat-over-video] #chat[class] {pointer-events: auto;}
  .playlist-items.ytd-playlist-panel-renderer {background-color: transparent !important;}
  @supports (color: var(--tyt-fix-20251124)) { #below ytd-watch-metadata .ytTextCarouselItemViewModelImageType { height: 16px; width: 16px;}
  #below ytd-watch-metadata yt-text-carousel-item-view-model { column-gap: 6px;}
  #below ytd-watch-metadata ytd-watch-info-text#ytd-watch-info-text { font-size: inherit; line-height: inherit;}
  /* Fix: video tab thumbnails (yt-lockup-view-model) too large in side panel */
  #tab-videos yt-lockup-view-model{max-width:100%;contain:layout paint;}
  #tab-videos yt-lockup-view-model .yt-lockup-view-model__content-image,#tab-videos yt-lockup-view-model .yt-lockup-view-model__content-image img,#tab-videos yt-lockup-view-model .yt-lockup-view-model__content-image yt-image{max-width:175px;max-height:94px;width:175px;height:auto;object-fit:cover;border-radius:8px;flex-shrink:0;}
  #tab-videos yt-lockup-view-model .yt-lockup-view-model--horizontal{display:flex;gap:8px;align-items:flex-start;}
  #tab-videos yt-lockup-view-model .yt-lockup-view-model--horizontal .yt-lockup-view-model__content-image{flex-shrink:0;width:175px;}
  #tab-videos yt-lockup-view-model .yt-lockup-view-model--horizontal .yt-lockup-view-model__metadata{flex:1;min-width:0;overflow:hidden;}
  #tab-videos ytd-video-renderer[use-search-ui] #thumbnail.ytd-video-renderer,#tab-videos ytd-compact-video-renderer #thumbnail{max-width:175px;width:175px;flex-shrink:0;}
  /* ── LCP Performance: safe content-visibility hints (no contain:layout to preserve sticky) ── */
  ytd-browse[page-subtype="home"] #contents.ytd-rich-grid-renderer>ytd-rich-item-renderer:nth-child(n+9){content-visibility:auto;contain-intrinsic-size:auto 360px;}
  ytd-playlist-video-list-renderer #contents>ytd-playlist-video-renderer:nth-child(n+10){content-visibility:auto;contain-intrinsic-size:auto 90px;}
  ytd-watch-next-secondary-results-renderer ytd-compact-video-renderer:nth-child(n+5){content-visibility:auto;contain-intrinsic-size:auto 94px;}
  `,
};

(async () => {
  // ------------------------------------------------------------------------ nextBrowserTick ------------------------------------------------------------------------
  /* eslint-disable no-unused-expressions, no-var */
  var nextBrowserTick =
    void 0 !== nextBrowserTick && nextBrowserTick.version >= 2
      ? nextBrowserTick
      : (() => {
          'use strict';
          const e =
            typeof globalThis !== 'undefined'
              ? globalThis
              : typeof window !== 'undefined'
                ? window
                : this;
          let t = !0;
          if (
            !(function n(s) {
              return s
                ? (t = !1)
                : e.postMessage && !e.importScripts && e.addEventListener
                  ? (e.addEventListener('message', n, !1),
                    e.postMessage('$$$', '*'),
                    e.removeEventListener('message', n, !1),
                    t)
                  : void 0;
            })()
          ) {
            return void console.warn('Your browser environment cannot use nextBrowserTick');
          }
          const n = (async () => {}).constructor;
          let s = null;
          const o = new Map(),
            { floor: r, random: i } = Math;
          let l;
          do {
            l = `$$nextBrowserTick$$${(i() + 8).toString().slice(2)}$$`;
          } while (l in e);
          const a = l,
            c = a.length + 9;
          e[a] = 1;
          e.addEventListener(
            'message',
            e => {
              if (0 !== o.size) {
                const t = (e || 0).data;
                if ('string' == typeof t && t.length === c && e.source === (e.target || 1)) {
                  const e = o.get(t);
                  e && ('p' === t[0] && (s = null), o.delete(t), e());
                }
              }
            },
            !1
          );
          const d = (t = o) => {
            if (t === o) {
              if (s) return s;
              let t;
              do {
                t = `p${a}${r(314159265359 * i() + 314159265359).toString(36)}`;
              } while (o.has(t));
              return (
                (s = new n(e => {
                  o.set(t, e);
                })),
                e.postMessage(t, '*'),
                (t = null),
                s
              );
            }
            {
              let n;
              do {
                n = `f${a}${r(314159265359 * i() + 314159265359).toString(36)}`;
              } while (o.has(n));
              (o.set(n, t), e.postMessage(n, '*'));
            }
          };
          return ((d.version = 2), d);
        })();
  /* eslint-enable no-unused-expressions, no-var */
  // ------------------------------------------------------------------------ nextBrowserTick ------------------------------------------------------------------------
  const communicationKey = `ck-${Date.now()}-${Math.floor(Math.random() * 314159265359 + 314159265359).toString(36)}`;

  /** @type {globalThis.PromiseConstructor} */
  const Promise = (async () => {})().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.

  if (!document.documentElement) {
    await Promise.resolve(0);
    while (!document.documentElement) {
      await new Promise(resolve => nextBrowserTick(resolve)).then().catch(console.warn);
    }
  }
  const sourceURL = 'debug://tabview-youtube/tabview.execution.js';
  const textContent = `(${executionScript})("${communicationKey}");${'\n\n'}//# sourceURL=${sourceURL}${'\n'}`;

  // const isMyScriptInChromeRuntime = () => typeof GM === 'undefined' && typeof ((((window || 0).chrome || 0).runtime || 0).getURL) === 'function';
  // const isGMAvailable = () => typeof GM !== 'undefined' && !isMyScriptInChromeRuntime();

  // if(isMyScriptInChromeRuntime()){
  //   let button = document.createElement('button');
  //   button.setAttribute('onclick', textContent);
  //   button.click();
  //   button = null;
  // }else{
  //   GM_addElement('script', {
  //     textContent: textContent
  //   });
  // }

  let button = document.createElement('button');
  button.setAttribute('onclick', createHTML(textContent)); // max size 10 million bytes
  button.click();
  button = null;

  const style = document.createElement('style');
  const sourceURLMainCSS = 'debug://tabview-youtube/tabview.main.css';
  const cssContent = `${styles['main'].trim()}${'\n\n'}/*# sourceURL=${sourceURLMainCSS} */${'\n'}`;

  // Avoid referencing GM_addStyle directly to prevent "not defined" errors in some environments
  const gmAddStyle = (typeof window !== 'undefined' && window['GM_addStyle']) || null;
  if (typeof gmAddStyle === 'function') {
    gmAddStyle(cssContent);
  } else {
    style.textContent = cssContent;
    document.documentElement.appendChild(style);
  }

  const applyTabviewI18nVars = () => {
    const root = document.documentElement;
    if (!root) return;
    const i18n = typeof window !== 'undefined' ? window.YouTubePlusI18n : null;
    const translate = (key, fallback) => {
      if (i18n && typeof i18n.t === 'function') {
        const value = i18n.t(key);
        if (value && value !== key) return value;
      }
      return fallback;
    };
    const toCssString = value => {
      const text = String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
      return `"${text}"`;
    };
    root.style.setProperty('--tabview-text-loading', toCssString(translate('loading', 'Loading')));
    root.style.setProperty(
      '--tabview-text-fetching',
      toCssString(translate('fetching', 'Fetching'))
    );
  };

  const applyTabviewI18nTabs = () => {
    const container = document.querySelector('#right-tabs');
    if (!container) return false;
    const i18n = typeof window !== 'undefined' ? window.YouTubePlusI18n : null;
    const translate = (key, fallback) => {
      if (i18n && typeof i18n.t === 'function') {
        const value = i18n.t(key);
        if (value && value !== key) return value;
      }
      return fallback;
    };
    const labels = [
      { selector: '#tab-btn1 span', key: 'info', fallback: 'Info' },
      { selector: '#tab-btn4 span', key: 'videos', fallback: 'Videos' },
      { selector: '#tab-btn5 span', key: 'playlist', fallback: 'Playlist' },
    ];
    for (const { selector, key, fallback } of labels) {
      const label = container.querySelector(selector);
      if (label) label.textContent = translate(key, fallback);
    }
    return true;
  };

  const scheduleTabviewI18nTabs = () => {
    let attempts = 0;
    const tryApply = () => {
      if (applyTabviewI18nTabs()) return;
      if (attempts < 20) {
        attempts += 1;
        setTimeout(tryApply, 250);
      }
    };
    tryApply();
  };

  const refreshTabviewI18n = () => {
    applyTabviewI18nVars();
    scheduleTabviewI18nTabs();
  };

  let tabviewI18nListenerBound = false;
  const bindTabviewI18n = () => {
    let attempts = 0;
    const tryBind = () => {
      const i18n = typeof window !== 'undefined' ? window.YouTubePlusI18n : null;
      if (i18n && typeof i18n.t === 'function') {
        refreshTabviewI18n();
        if (!tabviewI18nListenerBound && typeof i18n.onLanguageChange === 'function') {
          i18n.onLanguageChange(refreshTabviewI18n);
          tabviewI18nListenerBound = true;
        }
        return;
      }
      if (attempts < 120) {
        attempts += 1;
        setTimeout(tryBind, 500);
      }
    };
    tryBind();
  };

  // Also react to global i18n lifecycle events for modules that initialize later.
  if (typeof window !== 'undefined') {
    window.addEventListener('youtube-plus-i18n-ready', refreshTabviewI18n, { passive: true });
    window.addEventListener('youtube-plus-language-changed', refreshTabviewI18n, {
      passive: true,
    });
  }

  bindTabviewI18n();
  scheduleTabviewI18nTabs();

  // Initialize lazy loading for non-critical features
  if (typeof window !== 'undefined' && window.YouTubePlusLazyLoader) {
    const { register, loadOnIdle } = window.YouTubePlusLazyLoader;

    // Register deferred initialization for heavy modules
    // These will be initialized when their features are needed

    // Stats module - load when channel page is detected
    register(
      'stats-module',
      () => {
        if (window.YouTubeStatsReady) {
          console.log('[YouTube+] Stats module already initialized');
          return;
        }
        console.log('[YouTube+] Stats module initialization deferred');
        // Stats is already loaded but we mark it as lazy-ready
        window.YouTubeStatsReady = true;
      },
      { priority: 3, delay: 1000 }
    );

    // Download modal - load when download button is clicked
    register(
      'download-module',
      () => {
        console.log('[YouTube+] Download module ready');
      },
      { priority: 4, delay: 500 }
    );

    // Playlist search - load when playlist page detected
    register(
      'playlist-search-module',
      () => {
        console.log('[YouTube+] Playlist search ready');
      },
      { priority: 2, delay: 800 }
    );

    // Thumbnail overlay - load when video page detected
    register(
      'thumbnail-module',
      () => {
        console.log('[YouTube+] Thumbnail overlay ready');
      },
      { priority: 3, delay: 600 }
    );

    // Load all non-critical features during browser idle time
    loadOnIdle(2000);
  }
})();

// --- MODULE: i18n.js ---

/**
 * YouTube+ Internationalization (i18n) System - v3.2
 * Unified i18n system with integrated loader
 * Supports all major YouTube interface languages
 * @module i18n
 * @version 3.2
 */

(function () {
  'use strict';

  // ============================================================================
  // I18N LOADER (merged from i18n-loader.js)
  // ============================================================================

  const GITHUB_CONFIG = {
    owner: 'diorhc',
    repo: 'YTP',
    branch: 'main',
    basePath: 'locales',
  };

  const CDN_URLS = {
    github: `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.basePath}`,
    jsdelivr: `https://cdn.jsdelivr.net/gh/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}@${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.basePath}`,
  };

  // Translation files shipped with the project (and embedded by embed-translations.js).
  // Any other YouTube UI language will map to the closest language below (usually English).
  const AVAILABLE_LANGUAGES = ['en', 'ru', 'kr', 'fr', 'du', 'cn', 'tw', 'jp', 'tr', 'es', 'pt', 'de', 'it', 'pl', 'uk', 'ar', 'hi', 'id', 'vi', 'uz', 'kk', 'ky', 'be', 'bg', 'az'];

  const LANGUAGE_NAMES = {
    en: 'English', ru: 'Русский', kr: '한국어', fr: 'Français', du: 'Nederlands', cn: '简体中文', tw: '繁體中文', jp: '日本語', tr: 'Türkçe', es: 'Español', pt: 'Português', de: 'Deutsch', it: 'Italiano', pl: 'Polski', uk: 'Українська', sv: 'Svenska', no: 'Norsk', da: 'Dansk', fi: 'Suomi', cs: 'Čeština', sk: 'Slovenčina', hu: 'Magyar', ro: 'Română', bg: 'Български', hr: 'Hrvatski', sr: 'Српски', sl: 'Slovenščina', el: 'Ελληνικά', lt: 'Lietuvių', lv: 'Latviešu', et: 'Eesti', mk: 'Македонски', sq: 'Shqip', bs: 'Bosanski', is: 'Íslenska', ca: 'Català', eu: 'Euskara', gl: 'Galego', ar: 'العربية', he: 'עברית', fa: 'فارسی', sw: 'Kiswahili', zu: 'isiZulu', af: 'Afrikaans', am: 'አማርኛ', hi: 'हिन्दी', th: 'ไทย', vi: 'Tiếng Việt', id: 'Bahasa Indonesia', ms: 'Bahasa Melayu', bn: 'বাংলা', ta: 'தமிழ்', te: 'తెలుగు', mr: 'मराठी', gu: 'ગુજરાતી', kn: 'ಕನ್ನಡ', ml: 'മലയാളം', pa: 'ਪੰਜਾਬੀ', fil: 'Filipino', km: 'ភាសាខ្មែរ', lo: 'ລາວ', my: 'မြန်မာ', ne: 'नेपाली', si: 'සිංහල', az: 'Azərbaycanca', be: 'Беларуская', hy: 'Հայերեն', ka: 'ქართული', kk: 'Қазақ', ky: 'Кыргызча', mn: 'Монгол', tg: 'Тоҷикӣ', uz: 'Oʻzbekcha',
  };

  const LANGUAGE_FALLBACKS = {
    es: 'es', 'es-es': 'es', 'es-mx': 'es', 'es-419': 'es', pt: 'pt', 'pt-br': 'pt', 'pt-pt': 'pt', de: 'de', 'de-de': 'de', 'de-at': 'de', 'de-ch': 'de', it: 'it', pl: 'pl', uk: 'uk', 'uk-ua': 'uk', ar: 'ar', 'ar-sa': 'ar', 'ar-ae': 'ar', 'ar-eg': 'ar', hi: 'hi', 'hi-in': 'hi', th: 'en', 'th-th': 'en', vi: 'vi', 'vi-vn': 'vi', id: 'id', 'id-id': 'id', ms: 'en', 'ms-my': 'en', sv: 'en', 'sv-se': 'en', no: 'en', 'nb-no': 'en', 'nn-no': 'en', da: 'en', 'da-dk': 'en', fi: 'en', 'fi-fi': 'en', cs: 'en', 'cs-cz': 'en', sk: 'en', 'sk-sk': 'en', hu: 'en', 'hu-hu': 'en', ro: 'en', 'ro-ro': 'en', bg: 'bg', 'bg-bg': 'bg', hr: 'en', 'hr-hr': 'en', sr: 'ru', 'sr-rs': 'ru', sl: 'en', 'sl-si': 'en', el: 'en', 'el-gr': 'en', he: 'en', 'he-il': 'en', iw: 'en', fa: 'en', 'fa-ir': 'en', bn: 'en', 'bn-in': 'en', ta: 'en', 'ta-in': 'en', te: 'en', 'te-in': 'en', mr: 'en', 'mr-in': 'en', gu: 'en', 'gu-in': 'en', kn: 'en', 'kn-in': 'en', ml: 'en', 'ml-in': 'en', pa: 'en', 'pa-in': 'en', fil: 'en', 'fil-ph': 'en', tl: 'en', km: 'en', lo: 'en', my: 'en', ne: 'en', si: 'en', sw: 'en', 'sw-ke': 'en', zu: 'en', af: 'en', am: 'en', az: 'az', 'az-az': 'az', be: 'be', 'be-by': 'be', hy: 'ru', ka: 'en', kk: 'kk', 'kk-kz': 'kk', ky: 'ky', mn: 'ru', tg: 'ru', uz: 'uz', 'uz-uz': 'uz', lt: 'en', 'lt-lt': 'en', lv: 'en', 'lv-lv': 'en', et: 'en', 'et-ee': 'en', mk: 'ru', sq: 'en', bs: 'en', is: 'en', ca: 'es', eu: 'es', gl: 'es',
  };

  const translationsCache = new Map();
  const loadingPromises = new Map();

  /**
   * Fetch translation from CDN or embedded source
   * @param {string} lang - Language code
   * @returns {Promise<Object>} Translation object
   */
  async function fetchTranslation(lang) {
    // Use embedded translations if available (fast local fallback)
    try {
      if (typeof window !== 'undefined' && window.YouTubePlusEmbeddedTranslations) {
        const embedded = window.YouTubePlusEmbeddedTranslations[lang];
        if (embedded) {
          window.YouTubeUtils &&
            YouTubeUtils.logger &&
            YouTubeUtils.logger.debug &&
            YouTubeUtils.logger.debug(
              '[YouTube+][i18n]',
              `Using embedded translations for ${lang}`
            );
          return embedded;
        }
      }
    } catch (e) {
      console.warn('[YouTube+][i18n]', 'Error reading embedded translations', e);
    }

    // Try raw GitHub first — often contains the latest changes and avoids
    // CDN caching delays. If that fails, fall back to jsDelivr with a
    // lightweight cache-bust query param to reduce the chance of stale
    // responses from the CDN.
    try {
      const rawUrl = `${CDN_URLS.github}/${lang}.json`;
      const response = await fetch(rawUrl, {
        cache: 'default',
        headers: { Accept: 'application/json' },
      });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (firstErr) {
      try {
        const cdnUrl = `${CDN_URLS.jsdelivr}/${lang}.json?_=${Date.now()}`;
        console.warn(
          '[YouTube+][i18n]',
          `Raw GitHub fetch failed, trying jsDelivr (with cache-bust): ${cdnUrl}`
        );
        const response = await fetch(cdnUrl, {
          cache: 'no-cache',
          headers: { Accept: 'application/json' },
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return await response.json();
      } catch (err) {
        console.error(
          '[YouTube+][i18n]',
          `Failed to fetch translations for ${lang}:`,
          err,
          firstErr
        );
        throw err;
      }
    }
  }

  /**
   * Load translations for a language (with caching)
   * @param {string} lang - Language code
   * @returns {Promise<Object>} Translation object
   */
  function loadTranslationsFromLoader(lang) {
    const languageCode = AVAILABLE_LANGUAGES.includes(lang) ? lang : 'en';
    if (translationsCache.has(languageCode)) return translationsCache.get(languageCode);
    if (loadingPromises.has(languageCode)) return loadingPromises.get(languageCode);

    const loadPromise = (async () => {
      try {
        const translations = await fetchTranslation(languageCode);
        // Quick sanity check: warn if common UI keys are missing from fetched translations
        try {
          const missing = [];
          ['loading', 'fetching'].forEach(k => {
            if (!Object.prototype.hasOwnProperty.call(translations, k)) missing.push(k);
          });
          if (missing.length > 0) {
            console.warn(
              '[YouTube+][i18n]',
              `Translations for ${languageCode} missing keys: ${missing.join(', ')} (source may be stale)`
            );
          }
        } catch {
          /* ignore sanity-check errors */
        }
        translationsCache.set(languageCode, translations);
        loadingPromises.delete(languageCode);
        return translations;
      } catch (error) {
        loadingPromises.delete(languageCode);
        if (languageCode !== 'en') return loadTranslationsFromLoader('en');
        throw error;
      }
    })();

    loadingPromises.set(languageCode, loadPromise);
    return loadPromise;
  }

  // ============================================================================
  // I18N CORE SYSTEM
  // ============================================================================

  /**
   * Current language
   * @type {string}
   */
  let currentLanguage = 'en';

  /**
   * Loaded translations for current language
   * @type {Object}
   */
  let translations = {};

  /**
   * English fallback translations (loaded once).
   * @type {Object}
   */
  let fallbackTranslationsEn = {};

  /**
   * Translation cache
   * @type {Map<string, string>}
   */
  const translationCache = new Map();

  /**
   * Language change listeners
   * @type {Set<Function>}
   */
  const languageChangeListeners = new Set();

  /**
   * Loading state
   * @type {Promise|null}
   */
  let loadingPromise = null;

  /**
   * Emit a global browser event for i18n lifecycle hooks.
   * @param {string} name - Event name
   * @param {Object} detail - Event payload
   */
  function emitI18nEvent(name, detail = {}) {
    try {
      if (typeof window === 'undefined') return;
      window.dispatchEvent(new CustomEvent(name, { detail }));
    } catch {
      try {
        if (typeof window === 'undefined') return;
        window.dispatchEvent(new Event(name));
      } catch {
        /* no-op */
      }
    }
  }

  // Language mapping for common locale codes - extended to support all YouTube languages
  const languageMap = {
    ko: 'kr', 'ko-kr': 'kr',
    fr: 'fr', 'fr-fr': 'fr', 'fr-ca': 'fr', 'fr-be': 'fr', 'fr-ch': 'fr',
    nl: 'du', 'nl-nl': 'du', 'nl-be': 'du',
    zh: 'cn', 'zh-cn': 'cn', 'zh-hans': 'cn', 'zh-sg': 'cn', 'zh-tw': 'tw', 'zh-hk': 'tw', 'zh-hant': 'tw',
    ja: 'jp', 'ja-jp': 'jp',
    tr: 'tr', 'tr-tr': 'tr',
    ru: 'ru', 'ru-ru': 'ru',
    en: 'en', 'en-us': 'en', 'en-gb': 'en', 'en-au': 'en', 'en-ca': 'en', 'en-in': 'en',
    ...Object.fromEntries(Object.entries(LANGUAGE_FALLBACKS).map(([key, fallback]) => [key, fallback])),
  };

  /**
   * Check if a language code maps to a primary supported language
   * @param {string} langCode - Language code to check
   * @returns {string} Mapped language code
   */
  function mapToSupportedLanguage(langCode) {
    const lower = langCode.toLowerCase();

    // Direct match in language map
    if (languageMap[lower]) {
      return languageMap[lower];
    }

    // Direct match in shipped translations
    if (AVAILABLE_LANGUAGES.includes(lower)) {
      return lower;
    }

    // Check first two characters
    const shortCode = lower.substr(0, 2);
    if (languageMap[shortCode]) {
      return languageMap[shortCode];
    }

    if (AVAILABLE_LANGUAGES.includes(shortCode)) {
      return shortCode;
    }

    // Check fallbacks
    if (LANGUAGE_FALLBACKS[lower]) {
      return LANGUAGE_FALLBACKS[lower];
    }
    if (LANGUAGE_FALLBACKS[shortCode]) {
      return LANGUAGE_FALLBACKS[shortCode];
    }

    // Default to English
    return 'en';
  }

  /**
   * Detect user's language preference with extended support
   * @returns {string} Language code
   */
  function detectLanguage() {
    try {
      // Try YouTube's language setting first (from HTML lang attribute)
      const ytLang =
        document.documentElement.lang || document.querySelector('html')?.getAttribute('lang');
      if (ytLang) {
        const mapped = mapToSupportedLanguage(ytLang);
        return mapped;
      }

      // Try YouTube's hl parameter from URL
      try {
        const urlParams = new URLSearchParams(window.location.search);
        const hlParam = urlParams.get('hl');
        if (hlParam) {
          const mapped = mapToSupportedLanguage(hlParam);
          return mapped;
        }
      } catch {}

      // Try to get YouTube's internal language setting
      try {
        const ytConfig = window.ytcfg || window.yt?.config_;
        if (ytConfig && typeof ytConfig.get === 'function') {
          const hl = ytConfig.get('HL') || ytConfig.get('GAPI_LOCALE');
          if (hl) {
            const mapped = mapToSupportedLanguage(hl);
            return mapped;
          }
        }
      } catch {}

      // Fallback to browser language
      const browserLang = navigator.language || navigator.userLanguage || 'en';
      const mapped = mapToSupportedLanguage(browserLang);

      return mapped;
    } catch (error) {
      console.error('[YouTube+][i18n]', 'Error detecting language:', error);
      return 'en';
    }
  }

  /**
   * Load translations for current language
   * @returns {Promise<boolean>} Success status
   */
  async function loadTranslations() {
    if (loadingPromise) {
      await loadingPromise;
      return true;
    }

    loadingPromise = (async () => {
      try {
        window.YouTubeUtils &&
          YouTubeUtils.logger &&
          YouTubeUtils.logger.debug &&
          YouTubeUtils.logger.debug(
            '[YouTube+][i18n]',
            `Loading translations for ${currentLanguage}...`
          );
        translations = await loadTranslationsFromLoader(currentLanguage);
        // Ensure we always have English fallback available (best-effort).
        // Skip the async fetch when embedded English translations are already
        // bundled — this avoids a network round-trip on every page load.
        if (!fallbackTranslationsEn || Object.keys(fallbackTranslationsEn).length === 0) {
          try {
            const embeddedEn =
              typeof window !== 'undefined' &&
              window.YouTubePlusEmbeddedTranslations &&
              window.YouTubePlusEmbeddedTranslations['en'];
            if (embeddedEn && typeof embeddedEn === 'object') {
              fallbackTranslationsEn = embeddedEn;
            } else {
              fallbackTranslationsEn = await loadTranslationsFromLoader('en');
            }
          } catch {
            fallbackTranslationsEn = {};
          }
        }
        translationCache.clear(); // Clear cache on new load
        window.YouTubeUtils &&
          YouTubeUtils.logger &&
          YouTubeUtils.logger.debug &&
          YouTubeUtils.logger.debug(
            '[YouTube+][i18n]',
            `✓ Loaded ${Object.keys(translations).length} translations for ${currentLanguage}`
          );
        return true;
      } catch (error) {
        console.error('[YouTube+][i18n]', 'Failed to load translations:', error);
        // Use English as fallback
        if (currentLanguage !== 'en') {
          currentLanguage = 'en';
          return loadTranslations();
        }
        return false;
      } finally {
        loadingPromise = null;
      }
    })();

    return loadingPromise;
  }

  /**
   * Translate a key with optional placeholders
   * @param {string} key - Translation key
   * @param {Object} [params] - Parameters to replace in translation
   * @returns {string} Translated string
   */
  function translate(key, params = {}) {
    // Check cache
    const cacheKey = `${key}:${JSON.stringify(params)}`;
    if (translationCache.has(cacheKey)) {
      return translationCache.get(cacheKey);
    }

    // Get translation
    let text = translations[key];

    // Fallback to English if current language misses the key
    if (!text) {
      const enText = fallbackTranslationsEn ? fallbackTranslationsEn[key] : undefined;
      if (enText) {
        text = enText;
      } else {
        // Only warn if translations have been loaded and key is still missing everywhere
        if (Object.keys(translations).length > 0) {
          console.warn('[YouTube+][i18n]', `Missing translation for key: ${key}`);
        }
        text = key;
      }
    }

    // Replace parameters
    if (Object.keys(params).length > 0) {
      Object.keys(params).forEach(param => {
        text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]);
      });
    }

    // Cache result
    translationCache.set(cacheKey, text);
    return text;
  }

  /**
   * Get current language
   * @returns {string} Language code
   */
  function getLanguage() {
    return currentLanguage;
  }

  /**
   * Set language and reload translations
   * @param {string} lang - Language code
   * @returns {Promise<boolean>} Success status
   */
  async function setLanguage(lang) {
    if (lang === currentLanguage) {
      return true;
    }

    const oldLang = currentLanguage;
    currentLanguage = lang;

    try {
      const success = await loadTranslations();
      if (success) {
        // Notify listeners
        languageChangeListeners.forEach(listener => {
          try {
            listener(currentLanguage, oldLang);
          } catch (error) {
            console.error('[YouTube+][i18n]', 'Error in language change listener:', error);
          }
        });
        emitI18nEvent('youtube-plus-language-changed', {
          language: currentLanguage,
          previousLanguage: oldLang,
        });
      }
      return success;
    } catch (error) {
      console.error('[YouTube+][i18n]', 'Failed to change language:', error);
      currentLanguage = oldLang; // Revert
      return false;
    }
  }

  /**
   * Get all translations for current language
   * @returns {Object} All translations
   */
  function getAllTranslations() {
    return { ...translations };
  }

  /**
   * Get available languages
   * @returns {string[]} Array of language codes
   */
  function getAvailableLanguages() {
    return AVAILABLE_LANGUAGES;
  }

  /**
   * Check if translation exists for key
   * @param {string} key - Translation key
   * @returns {boolean} True if exists
   */
  function hasTranslation(key) {
    return translations[key] !== undefined;
  }

  /**
   * Add translation dynamically
   * @param {string} key - Translation key
   * @param {string} value - Translation value
   */
  function addTranslation(key, value) {
    translations[key] = value;
    translationCache.clear(); // Clear cache
  }

  /**
   * Add translations for current language
   * @param {Object} newTranslations - Object with translations
   */
  function addTranslations(newTranslations) {
    Object.assign(translations, newTranslations);
    translationCache.clear(); // Clear cache
  }

  /**
   * Register language change listener
   * @param {Function} callback - Callback function(newLang, oldLang)
   */
  function onLanguageChange(callback) {
    languageChangeListeners.add(callback);
    return () => languageChangeListeners.delete(callback);
  }

  /**
   * Format numbers according to locale
   * @param {number} num - Number to format
   * @param {Object} [options] - Intl.NumberFormat options
   * @returns {string} Formatted number
   */
  function formatNumber(num, options = {}) {
    try {
      const lang = getLanguage();
      const localeMap = {ru: 'ru-RU', kr: 'ko-KR', fr: 'fr-FR', du: 'nl-NL', cn: 'zh-CN', tw: 'zh-TW', jp: 'ja-JP', tr: 'tr-TR'};
      const locale = localeMap[lang] || 'en-US';
      return new Intl.NumberFormat(locale, options).format(num);
    } catch (error) {
      console.error('[YouTube+][i18n]', 'Error formatting number:', error);
      return String(num);
    }
  }

  /**
   * Format date according to locale
   * @param {Date|number|string} date - Date to format
   * @param {Object} [options] - Intl.DateTimeFormat options
   * @returns {string} Formatted date
   */
  function formatDate(date, options = {}) {
    try {
      const lang = getLanguage();
      const localeMap = {ru: 'ru-RU', kr: 'ko-KR', fr: 'fr-FR', du: 'nl-NL', cn: 'zh-CN', tw: 'zh-TW', jp: 'ja-JP', tr: 'tr-TR'};
      const locale = localeMap[lang] || 'en-US';
      const dateObj = date instanceof Date ? date : new Date(date);
      return new Intl.DateTimeFormat(locale, options).format(dateObj);
    } catch (error) {
      console.error('[YouTube+][i18n]', 'Error formatting date:', error);
      return String(date);
    }
  }

  /**
   * Pluralize a word based on count and language
   * @param {number} count - Count value
   * @param {string} singular - Singular form
   * @param {string} plural - Plural form
   * @param {string} [few] - Few form (for Russian, etc.)
   * @returns {string} Appropriate form
   */
  function pluralize(count, singular, plural, few = null) {
    const lang = getLanguage();

    // Russian pluralization
    if (lang === 'ru' && few) {
      const mod10 = count % 10;
      const mod100 = count % 100;

      if (mod10 === 1 && mod100 !== 11) {
        return singular;
      }
      if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) {
        return few;
      }
      return plural;
    }

    // Default English-like pluralization
    return count === 1 ? singular : plural;
  }

  /**
   * Clear translation cache
   */
  function clearCache() {
    translationCache.clear();
  }

  /**
   * Get cache statistics
   * @returns {Object} Cache statistics
   */
  function getCacheStats() {
    return {
      size: translationCache.size,
      currentLanguage,
      availableLanguages: getAvailableLanguages(),
      translationsLoaded: Object.keys(translations).length,
    };
  }

  // Initialize
  async function initialize() {
    try {
      currentLanguage = detectLanguage();

      window.YouTubeUtils &&
        YouTubeUtils.logger &&
        YouTubeUtils.logger.debug &&
        YouTubeUtils.logger.debug(
          '[YouTube+][i18n]',
          `Detected language: ${currentLanguage} (${LANGUAGE_NAMES[currentLanguage] || currentLanguage})`
        );

      // Load translations
      await loadTranslations();
      emitI18nEvent('youtube-plus-i18n-ready', {
        language: currentLanguage,
      });
    } catch (error) {
      console.error('[YouTube+][i18n]', 'Initialization error:', error);
      currentLanguage = 'en';
    }
  }

  // Export API
  const i18nAPI = {
    // Core functions
    t: translate,
    translate,
    getLanguage,
    setLanguage,
    detectLanguage,

    // Advanced functions
    getAllTranslations,
    getAvailableLanguages,
    hasTranslation,
    addTranslation,
    addTranslations,
    onLanguageChange,

    // Formatting functions
    formatNumber,
    formatDate,
    pluralize,

    // Cache management
    clearCache,
    getCacheStats,

    // Internal functions
    loadTranslations,
    initialize,
  };

  // Expose to window for global access
  if (typeof window !== 'undefined') {
    window.YouTubePlusI18n = i18nAPI;

    // Expose loader API for backward compatibility
    window.YouTubePlusI18nLoader = {
      loadTranslations: loadTranslationsFromLoader,
      AVAILABLE_LANGUAGES,
      LANGUAGE_NAMES,
      CDN_URLS,
    };

    // Also expose as part of YouTubeUtils if it exists
    if (window.YouTubeUtils) {
      window.YouTubeUtils.i18n = i18nAPI;
      window.YouTubeUtils.t = translate;
      window.YouTubeUtils.getLanguage = getLanguage;
    }
  }

  // Module export for ES6
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = i18nAPI;
  }

  // Auto-initialize
  initialize().then(() => {
    window.YouTubeUtils &&
      YouTubeUtils.logger &&
      YouTubeUtils.logger.debug &&
      YouTubeUtils.logger.debug('[YouTube+][i18n]', 'i18n system initialized successfully');
  });
})();

// --- MODULE: settings-helpers.js ---

/**
 * Settings Modal Helpers
 * Helper functions to reduce complexity of settings modal creation
 */

/* global GM_getValue */

/**
 * Creates the sidebar navigation HTML
 * @param {Function} t - Translation function
 * @returns {string} Sidebar HTML
 */
function createSettingsSidebar(t) {
  return `
    <div class="ytp-plus-settings-sidebar">
      <div class="ytp-plus-settings-sidebar-header">
        <h2 class="ytp-plus-settings-title">${t('settingsTitle')}</h2>                
      </div>
      <div class="ytp-plus-settings-nav">
        ${createNavItem('basic', t('basicTab'), createBasicIcon(), true)}
        ${createNavItem('advanced', t('advancedTab'), createAdvancedIcon())}
        ${createNavItem('experimental', t('experimentalTab'), createExperimentalIcon())}
        ${createNavItem('voting', tr(t, 'votingTab', 'Voting'), createVotingIcon())}
        ${createNavItem('report', t('reportTab'), createReportIcon())}
        ${createNavItem('about', t('aboutTab'), createAboutIcon())}
      </div>
    </div>
  `;
}

/**
 * Creates a single navigation item
 * @param {string} section - Section identifier
 * @param {string} label - Nav item label
 * @param {string} icon - SVG icon
 * @param {boolean} active - Whether this item is active
 * @returns {string} Nav item HTML
 */
function createNavItem(section, label, icon, active = false) {
  const activeClass = active ? ' active' : '';
  return `
    <div class="ytp-plus-settings-nav-item${activeClass}" data-section="${section}">
      ${icon}
      ${label}
    </div>
  `;
}

/**
 * SVG icon creators
 */
function createBasicIcon() {
  return `
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
      <circle cx="9" cy="9" r="2"/>
      <path d="m21 15-3.086-3.086a2 2 0 0 0-1.414-.586H13l-2-2v3h6l3 3"/>
    </svg>
  `;
}

function createAdvancedIcon() {
  return `
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <circle cx="12" cy="12" r="3"/>
      <path d="m12 1 0 6m0 6 0 6"/>
      <path d="m17.5 6.5-4.5 4.5m0 0-4.5 4.5m9-9L12 12l5.5 5.5"/>
    </svg>
  `;
}

function createExperimentalIcon() {
  return `
    <svg width="64px" height="64px" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path fill-rule="evenodd" clip-rule="evenodd" d="M18.019 4V15.0386L6.27437 39.3014C5.48686 40.9283 6.16731 42.8855 7.79421 43.673C8.23876 43.8882 8.72624 44 9.22013 44H38.7874C40.5949 44 42.0602 42.5347 42.0602 40.7273C42.0602 40.2348 41.949 39.7488 41.7351 39.3052L30.0282 15.0386V4H18.019Z" stroke="currentColor" stroke-width="4" stroke-linejoin="round"></path> 
      <path d="M10.9604 29.9998C13.1241 31.3401 15.2893 32.0103 17.4559 32.0103C19.6226 32.0103 21.7908 31.3401 23.9605 29.9998C26.1088 28.6735 28.2664 28.0103 30.433 28.0103C32.5997 28.0103 34.7755 28.6735 36.9604 29.9998" stroke="currentColor" stroke-width="4" stroke-linecap="round"></path>
    </svg>
  `;
}

function createReportIcon() {
  return `
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
      <polyline points="14 2 14 8 20 8"></polyline>
      <line x1="12" y1="18" x2="12" y2="12"></line>
      <line x1="12" y1="9" x2="12.01" y2="9"></line>
    </svg>
  `;
}

function createAboutIcon() {
  return `
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <circle cx="12" cy="12" r="10"/>
      <path d="m9 12 2 2 4-4"/>
    </svg>
  `;
}

function createVotingIcon() {
  return `
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M7 10v12"/>
      <path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z"/>
    </svg>
  `;
}

/**
 * Creates a settings checkbox item
 * @param {string} label - Item label
 * @param {string} description - Item description
 * @param {string} setting - Setting data attribute
 * @param {boolean} checked - Whether checkbox is checked
 * @returns {string} Settings item HTML
 */
function createSettingsItem(label, description, setting, checked) {
  const inputId = `ytp-plus-setting-${setting}`;
  return `
    <div class="ytp-plus-settings-item">
      <div>
        <label class="ytp-plus-settings-item-label" for="${inputId}">${label}</label>
        <div class="ytp-plus-settings-item-description">${description}</div>
      </div>
      <input type="checkbox" id="${inputId}" class="ytp-plus-settings-checkbox" data-setting="${setting}" ${checked ? 'checked' : ''}>
    </div>
  `;
}

/**
 * Creates the download site option section
 * @param {Object} site - Site configuration
 * @param {Function} _t - Translation function (unused, kept for API consistency)
 * @returns {string} Download site HTML
 */
function createDownloadSiteOption(site, _t) {
  const { key, name, description, checked, hasControls, controls } = site;
  const inputId = `download-site-${key}`;

  return `
    <div class="download-site-option">
      <div class="download-site-header">
        <label for="${inputId}" class="download-site-label">
          <div class="download-site-name">${name}</div>
          <div class="download-site-desc">${description}</div>
        </label>
        <input type="checkbox" id="${inputId}" class="ytp-plus-settings-checkbox" data-setting="downloadSite_${key}" ${checked ? 'checked' : ''}>
      </div>
      ${hasControls ? `<div class="download-site-controls" style="display:${checked ? 'block' : 'none'};">${controls}</div>` : ''}
    </div>
  `;
}

/**
 * Creates External Downloader customization controls
 * @param {Object} customization - External downloader customization settings
 * @param {Function} t - Translation function
 * @returns {string} Controls HTML
 */
function createExternalDownloaderControls(customization, t) {
  const name = customization?.name || 'SSYouTube';
  const url = customization?.url || 'https://ssyoutube.com/watch?v={videoId}';

  return `
    <input type="text" placeholder="${t('siteName')}" value="${name}" 
        data-site="externalDownloader" data-field="name" class="download-site-input">
    <input type="text" placeholder="${t('urlTemplate')}" value="${url}" 
      data-site="externalDownloader" data-field="url" class="download-site-input small">
    <div class="download-site-cta">
      <button class="glass-button" id="download-externalDownloader-save">${t('saveButton')}</button>
      <button class="glass-button danger" id="download-externalDownloader-reset">${t('resetButton')}</button>
    </div>
  `;
}

/**
 * Creates YTDL controls
 * @returns {string} Controls HTML
 */
function createYTDLControls() {
  return `
    <div class="download-site-cta one-btn">
      <button class="glass-button" id="open-ytdl-github">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
          <polyline points="15,3 21,3 21,9"/>
          <line x1="10" y1="14" x2="21" y2="3"/>
        </svg>
        GitHub
      </button>
    </div>
  `;
}

/**
 * Creates the download submenu with all site options
 * @param {Object} settings - Settings object
 * @param {Function} t - Translation function
 * @returns {string} Download submenu HTML
 */
function createDownloadSubmenu(settings, t) {
  const display = settings.enableDownload ? 'block' : 'none';

  const sites = [
    {
      key: 'externalDownloader',
      name: settings.downloadSiteCustomization?.externalDownloader?.name || 'SSYouTube',
      description: t('customDownloader'),
      checked: settings.downloadSites?.externalDownloader,
      hasControls: true,
      controls: createExternalDownloaderControls(
        settings.downloadSiteCustomization?.externalDownloader,
        t
      ),
    },
    {
      key: 'ytdl',
      name: t('byYTDL'),
      description: t('customDownload'),
      checked: settings.downloadSites?.ytdl,
      hasControls: true,
      controls: createYTDLControls(),
    },
    {
      key: 'direct',
      name: t('directDownload'),
      description: t('directDownloadDesc'),
      checked: settings.downloadSites?.direct,
      hasControls: false,
    },
  ];

  return `
    <div class="download-submenu" data-submenu="download" style="display:${display};">
      <div class="glass-card download-submenu-container">
        ${sites.map(site => createDownloadSiteOption(site, t)).join('')}
      </div>
    </div>
  `;
}

/**
 * Small translation helper with fallback.
 * @param {Function} t - Translation function
 * @param {string} key - Translation key
 * @param {string} fallback - Fallback text if key is missing
 * @returns {string}
 */
function tr(t, key, fallback) {
  try {
    const v = t(key);
    if (typeof v === 'string' && v && v !== key) return v;
  } catch {}
  return fallback;
}

/**
 * Creates the styles submenu (style.js feature flags)
 * @param {Object} settings - Settings object
 * @param {Function} t - Translation function
 * @returns {string}
 */
function createStyleSubmenu(settings, t) {
  const display = settings.enableZenStyles ? 'block' : 'none';

  const rows = [
    {
      label: tr(t, 'zenStyleThumbnailHoverLabel', 'Thumbnail hover preview'),
      desc: tr(t, 'zenStyleThumbnailHoverDesc', 'Enlarge inline preview player on hover'),
      key: 'zenStyles.thumbnailHover',
      value: settings.zenStyles?.thumbnailHover,
    },
    {
      label: tr(t, 'zenStyleImmersiveSearchLabel', 'Immersive search'),
      desc: tr(t, 'zenStyleImmersiveSearchDesc', 'Centered searchbox experience when focused'),
      key: 'zenStyles.immersiveSearch',
      value: settings.zenStyles?.immersiveSearch,
    },
    {
      label: tr(t, 'zenStyleHideVoiceSearchLabel', 'Hide Voice Search'),
      desc: tr(t, 'zenStyleHideVoiceSearchDesc', 'Remove microphone button from the header'),
      key: 'zenStyles.hideVoiceSearch',
      value: settings.zenStyles?.hideVoiceSearch,
    },
    {
      label: tr(t, 'zenStyleTransparentHeaderLabel', 'Transparent Header'),
      desc: tr(t, 'zenStyleTransparentHeaderDesc', 'Make the top header transparent'),
      key: 'zenStyles.transparentHeader',
      value: settings.zenStyles?.transparentHeader,
    },
    {
      label: tr(t, 'zenStyleHideSideGuideLabel', 'Hide Side Guide'),
      desc: tr(t, 'zenStyleHideSideGuideDesc', 'Completely hide the sidebar guide'),
      key: 'zenStyles.hideSideGuide',
      value: settings.zenStyles?.hideSideGuide,
    },
    {
      label: tr(t, 'zenStyleCleanSideGuideLabel', 'Clean Side Guide'),
      desc: tr(t, 'zenStyleCleanSideGuideDesc', 'Remove Premium/Sports/Settings from sidebar'),
      key: 'zenStyles.cleanSideGuide',
      value: settings.zenStyles?.cleanSideGuide,
    },
    {
      label: tr(t, 'zenStyleFixFeedLayoutLabel', 'Fix Feed Layout'),
      desc: tr(t, 'zenStyleFixFeedLayoutDesc', 'Improve video grid layout on home page'),
      key: 'zenStyles.fixFeedLayout',
      value: settings.zenStyles?.fixFeedLayout,
    },
    {
      label: tr(t, 'zenStyleBetterCaptionsLabel', 'Better Captions'),
      desc: tr(t, 'zenStyleBetterCaptionsDesc', 'Enhanced subtitle styling with blur backdrop'),
      key: 'zenStyles.betterCaptions',
      value: settings.zenStyles?.betterCaptions,
    },
    {
      label: tr(t, 'zenStylePlayerBlurLabel', 'Player Controls Blur'),
      desc: tr(t, 'zenStylePlayerBlurDesc', 'Add blur effect to player controls'),
      key: 'zenStyles.playerBlur',
      value: settings.zenStyles?.playerBlur,
    },
    {
      label: tr(t, 'zenStyleTheaterEnhancementsLabel', 'Theater Enhancements'),
      desc: tr(
        t,
        'zenStyleTheaterEnhancementsDesc',
        'Floating comments panel and improved theater mode'
      ),
      key: 'zenStyles.theaterEnhancements',
      value: settings.zenStyles?.theaterEnhancements,
    },
    {
      label: tr(t, 'zenStyleMiscLabel', 'Misc Enhancements'),
      desc: tr(t, 'zenStyleMiscDesc', 'Compact feed, hover menus, and other minor improvements'),
      key: 'zenStyles.misc',
      value: settings.zenStyles?.misc,
    },
  ];

  return `
    <div class="style-submenu" data-submenu="style" style="display:${display};">
      <div class="glass-card style-submenu-container">
        ${rows.map(r => createSettingsItem(r.label, r.desc, r.key, r.value)).join('')}
      </div>
    </div>
  `;
}

/**
 * Creates the speed control submenu (hotkey customization)
 * @param {Object} settings - Settings object
 * @param {Function} t - Translation function
 * @returns {string}
 */
function createSpeedControlSubmenu(settings, t) {
  const display = settings.enableSpeedControl ? 'block' : 'none';
  const decrease = (settings.speedControlHotkeys?.decrease || 'g').slice(0, 1).toLowerCase();
  const increase = (settings.speedControlHotkeys?.increase || 'h').slice(0, 1).toLowerCase();
  const reset = (settings.speedControlHotkeys?.reset || 'b').slice(0, 1).toLowerCase();

  return `
    <div class="speed-submenu" data-submenu="speed" style="display:${display};">
      <div class="glass-card speed-submenu-container">
        <div class="ytp-plus-settings-item speed-hotkeys-row">
          <div class="speed-hotkeys-info">
            <div class="ytp-plus-settings-item-label">${tr(t, 'speedHotkeysTitle', 'Keyboard hotkeys')}</div>
            <div class="ytp-plus-settings-item-description">${tr(
              t,
              'speedHotkeysDesc',
              'Use single-letter shortcuts to decrease/increase/reset playback speed'
            )}</div>
            <div class="speed-hotkeys-fields">
              <label class="speed-hotkey-field">                
                <input
                  type="text"
                  class="speed-hotkey-input"
                  data-speed-hotkey="decrease"
                  value="${decrease}"
                  maxlength="1"
                  autocomplete="off"
                  spellcheck="false"
                >
                <span>${tr(t, 'decreaseSpeedHotkey', 'Decrease')}</span>
              </label>
              <label class="speed-hotkey-field">                
                <input
                  type="text"
                  class="speed-hotkey-input"
                  data-speed-hotkey="increase"
                  value="${increase}"
                  maxlength="1"
                  autocomplete="off"
                  spellcheck="false"
                >
                <span>${tr(t, 'increaseSpeedHotkey', 'Increase')}</span>
              </label>
              <label class="speed-hotkey-field">                
                <input
                  type="text"
                  class="speed-hotkey-input"
                  data-speed-hotkey="reset"
                  value="${reset}"
                  maxlength="1"
                  autocomplete="off"
                  spellcheck="false"
                >
                <span>${tr(t, 'resetButton', 'Reset')}</span>
              </label>
            </div>
          </div>
        </div>
      </div>
    </div>
  `;
}

/**
 * Creates the loop control submenu (hotkey customization for A → B)
 * @param {Object} settings - Settings object
 * @param {Function} t - Translation function
 * @returns {string}
 */
function createLoopSubmenu(settings, t) {
  const display = settings.enableLoop ? 'block' : 'none';
  const setPointA = (settings.loopHotkeys?.setPointA || 'k').slice(0, 1).toLowerCase();
  const setPointB = (settings.loopHotkeys?.setPointB || 'l').slice(0, 1).toLowerCase();
  const resetPoints = (settings.loopHotkeys?.resetPoints || 'o').slice(0, 1).toLowerCase();

  return `
    <div class="loop-submenu" data-submenu="loop" style="display:${display};margin:0 0 4px 0;">
      <div class="ytp-plus-settings-item loop-hotkeys-row" style="margin-bottom:0;">
        <div class="loop-hotkeys-info">
          <div class="ytp-plus-settings-item-label">${tr(t, 'loopSegmentTitle', 'Loop A → B')}</div>
          <div class="ytp-plus-settings-item-description">${tr(
            t,
            'loopSegmentDesc',
            'Repeat a custom segment of the video (A → B)'
          )}</div>
          <div class="loop-hotkeys-fields" style="margin-top:12px;">
            <label class="loop-hotkey-field">                
              <input
                type="text"
                class="loop-hotkey-input"
                data-loop-hotkey="setPointA"
                value="${setPointA}"
                maxlength="1"
                autocomplete="off"
                spellcheck="false"
              >
              <span>${tr(t, 'setPointAHotkey', 'Set Point A')}</span>
            </label>
            <label class="loop-hotkey-field">                
              <input
                type="text"
                class="loop-hotkey-input"
                data-loop-hotkey="setPointB"
                value="${setPointB}"
                maxlength="1"
                autocomplete="off"
                spellcheck="false"
              >
              <span>${tr(t, 'setPointBHotkey', 'Set Point B')}</span>
            </label>
            <label class="loop-hotkey-field">                
              <input
                type="text"
                class="loop-hotkey-input"
                data-loop-hotkey="resetPoints"
                value="${resetPoints}"
                maxlength="1"
                autocomplete="off"
                spellcheck="false"
              >
              <span>${tr(t, 'resetButton', 'Reset')}</span>
            </label>
          </div>
        </div>
      </div>
    </div>
  `;
}

/**
 * Creates the basic settings section
 * @param {Object} settings - Settings object
 * @param {Function} t - Translation function
 * @returns {string} Basic section HTML
 */
function createBasicSettingsSection(settings, t) {
  const downloadEnabled = !!settings.enableDownload;
  const styleEnabled = settings.enableZenStyles !== false;
  const speedEnabled = !!settings.enableSpeedControl;
  return `
    <div class="ytp-plus-settings-section" data-section="basic">
      <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu">
        <div>
          <label class="ytp-plus-settings-item-label" for="ytp-plus-setting-enableZenStyles">${tr(
            t,
            'zenStylesTitle',
            'Zen styles'
          )}</label>
          <div class="ytp-plus-settings-item-description">${tr(
            t,
            'zenStylesDesc',
            'Optional UI tweaks and cosmetic improvements'
          )}</div>
        </div>
        <div class="ytp-plus-settings-item-actions">
          <button
            type="button"
            class="ytp-plus-submenu-toggle"
            data-submenu="style"
            aria-label="Toggle styles submenu"
            aria-expanded="${styleEnabled ? 'true' : 'false'}"
            ${styleEnabled ? '' : 'disabled'}
            style="display:${styleEnabled ? 'inline-flex' : 'none'};"
          >
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="6 9 12 15 18 9"></polyline>
            </svg>
          </button>
          <input type="checkbox" id="ytp-plus-setting-enableZenStyles" class="ytp-plus-settings-checkbox" data-setting="enableZenStyles" ${
            styleEnabled ? 'checked' : ''
          }>
        </div>
      </div>
      ${createStyleSubmenu(settings, t)}
      <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu">
        <div>
          <label class="ytp-plus-settings-item-label" for="ytp-plus-setting-enableSpeedControl">${t(
            'speedControl'
          )}</label>
          <div class="ytp-plus-settings-item-description">${t('speedControlDesc')}</div>
        </div>
        <div class="ytp-plus-settings-item-actions">
          <button
            type="button"
            class="ytp-plus-submenu-toggle"
            data-submenu="speed"
            aria-label="Toggle speed submenu"
            aria-expanded="${speedEnabled ? 'true' : 'false'}"
            ${speedEnabled ? '' : 'disabled'}
            style="display:${speedEnabled ? 'inline-flex' : 'none'};"
          >
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="6 9 12 15 18 9"></polyline>
            </svg>
          </button>
          <input type="checkbox" id="ytp-plus-setting-enableSpeedControl" class="ytp-plus-settings-checkbox" data-setting="enableSpeedControl" ${
            speedEnabled ? 'checked' : ''
          }>
        </div>
      </div>
      ${createSpeedControlSubmenu(settings, t)}
      ${createSettingsItem(t('screenshotButton'), t('screenshotButtonDesc'), 'enableScreenshot', settings.enableScreenshot)}
      <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu">
        <div>
          <label class="ytp-plus-settings-item-label" for="ytp-plus-setting-enableDownload">${t(
            'downloadButton'
          )}</label>
          <div class="ytp-plus-settings-item-description">${t('downloadButtonDesc')}</div>
        </div>
        <div class="ytp-plus-settings-item-actions">
          <button
            type="button"
            class="ytp-plus-submenu-toggle"
            data-submenu="download"
            aria-label="Toggle download submenu"
            aria-expanded="${downloadEnabled ? 'true' : 'false'}"
            ${downloadEnabled ? '' : 'disabled'}
            style="display:${downloadEnabled ? 'inline-flex' : 'none'};"
          >
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="6 9 12 15 18 9"></polyline>
            </svg>
          </button>
          <input type="checkbox" id="ytp-plus-setting-enableDownload" class="ytp-plus-settings-checkbox" data-setting="enableDownload" ${
            settings.enableDownload ? 'checked' : ''
          }>
        </div>
      </div>
      ${createDownloadSubmenu(settings, t)}
    </div>
  `;
}

/**
 * Creates the about section with logo
 * @returns {string} About section HTML
 */
function createAboutSection() {
  return `
    <div class="ytp-plus-settings-section hidden" data-section="about">
      <svg class="app-icon" width="90" height="90" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1">
        <path d="m23.24,4.62c-0.85,0.45 -2.19,2.12 -4.12,5.13c-1.54,2.41 -2.71,4.49 -3.81,6.8c-0.55,1.14 -1.05,2.2 -1.13,2.35c-0.08,0.16 -0.78,0.7 -1.66,1.28c-1.38,0.91 -1.8,1.29 -1.4,1.28c0.08,0 0.67,-0.35 1.31,-0.77c0.64,-0.42 1.19,-0.76 1.2,-0.74c0.02,0.02 -0.1,0.31 -0.25,0.66c-1.03,2.25 -1.84,5.05 -1.84,6.37c0.01,1.89 0.84,2.67 2.86,2.67c1.08,0 1.94,-0.31 3.66,-1.29c1.84,-1.06 3.03,-1.93 4.18,-3.09c1.69,-1.7 2.91,-3.4 3.28,-4.59c0.59,-1.9 -0.1,-3.08 -2.02,-3.44c-0.87,-0.16 -2.85,-0.14 -3.75,0.06c-1.78,0.38 -2.74,0.76 -2.5,1c0.03,0.03 0.5,-0.1 1.05,-0.28c1.49,-0.48 2.34,-0.59 3.88,-0.53c1.64,0.07 2.09,0.19 2.69,0.75l0.46,0.43l0,0.87c0,0.74 -0.05,0.98 -0.35,1.6c-0.69,1.45 -2.69,3.81 -4.37,5.14c-0.93,0.74 -2.88,1.94 -4.07,2.5c-1.64,0.77 -3.56,0.72 -4.21,-0.11c-0.39,-0.5 -0.5,-1.02 -0.44,-2.11c0.05,-0.85 0.16,-1.32 0.67,-2.86c0.34,-1.01 0.86,-2.38 1.15,-3.04c0.52,-1.18 0.55,-1.22 1.6,-2.14c4.19,-3.65 8.42,-9.4 9.02,-12.26c0.2,-0.94 0.13,-1.46 -0.21,-1.7c-0.31,-0.22 -0.38,-0.21 -0.89,0.06m0.19,0.26c-0.92,0.41 -3.15,3.44 -5.59,7.6c-1.05,1.79 -3.12,5.85 -3.02,5.95c0.07,0.07 1.63,-1.33 2.58,-2.34c1.57,-1.65 3.73,-4.39 4.88,-6.17c1.31,-2.03 2.06,-4.11 1.77,-4.89c-0.13,-0.34 -0.16,-0.35 -0.62,-0.15m11.69,13.32c-0.3,0.6 -1.19,2.54 -1.98,4.32c-1.6,3.62 -1.67,3.71 -2.99,4.34c-1.13,0.54 -2.31,0.85 -3.54,0.92c-0.99,0.06 -1.08,0.04 -1.38,-0.19c-0.28,-0.22 -0.31,-0.31 -0.26,-0.7c0.03,-0.25 0.64,-1.63 1.35,-3.08c1.16,-2.36 2.52,-5.61 2.52,-6.01c0,-0.49 -0.36,0.19 -1.17,2.22c-0.51,1.26 -1.37,3.16 -1.93,4.24c-0.55,1.08 -1.04,2.17 -1.09,2.43c-0.1,0.59 0.07,1.03 0.49,1.28c0.78,0.46 3.3,0.06 5.13,-0.81l0.93,-0.45l-0.66,1.25c-0.7,1.33 -3.36,6.07 -4.31,7.67c-2.02,3.41 -3.96,5.32 -6.33,6.21c-2.57,0.96 -4.92,0.74 -6.14,-0.58c-0.81,-0.88 -0.82,-1.71 -0.04,-3.22c1.22,-2.36 6.52,-6.15 10.48,-7.49c0.52,-0.18 0.95,-0.39 0.95,-0.46c0,-0.21 -0.19,-0.18 -1.24,0.2c-1.19,0.43 -3.12,1.37 -4.34,2.11c-2.61,1.59 -5.44,4.09 -6.13,5.43c-1.15,2.2 -0.73,3.61 1.4,4.6c0.59,0.28 0.75,0.3 2.04,0.3c1.67,0 2.42,-0.18 3.88,-0.89c1.87,-0.92 3.17,-2.13 4.72,-4.41c0.98,-1.44 4.66,-7.88 5.91,-10.33c0.25,-0.49 0.68,-1.19 0.96,-1.56c0.28,-0.37 0.76,-1.15 1.06,-1.73c0.82,-1.59 2.58,-6.10 2.58,-6.6c0,-0.06 -0.07,-0.1 -0.17,-0.1c-0.10,0 -0.39,0.44 -0.71,1.09m-1.34,3.7c-0.93,2.08 -1.09,2.48 -0.87,2.2c0.19,-0.24 1.66,-3.65 1.6,-3.71c-0.02,-0.02 -0.35,0.66 -0.73,1.51" fill="none" fill-rule="evenodd" stroke="currentColor" />
      </svg>
      <h1>YouTube +</h1><br><br>
    </div>
  `;
}

/**
 * Gets YouTube Music settings from localStorage or defaults
 * @returns {Object} YouTube Music settings
 */
function getMusicSettings() {
  const defaults = {
    enableMusic: true,
    immersiveSearchStyles: true,
    hoverStyles: true,
    playerSidebarStyles: true,
    centeredPlayerStyles: true,
    playerBarStyles: true,
    centeredPlayerBarStyles: true,
    miniPlayerStyles: true,
    scrollToTopStyles: true,
  };

  // Prefer userscript-global storage so youtube.com and music.youtube.com share the setting.
  try {
    if (typeof GM_getValue !== 'undefined') {
      const stored = GM_getValue('youtube-plus-music-settings', null);
      if (typeof stored === 'string' && stored) {
        const parsed = JSON.parse(stored);
        if (parsed && typeof parsed === 'object') {
          const merged = { ...defaults };
          if (typeof parsed.enableMusic === 'boolean') merged.enableMusic = parsed.enableMusic;
          for (const key of Object.keys(defaults)) {
            if (key === 'enableMusic') continue;
            if (typeof parsed[key] === 'boolean') merged[key] = parsed[key];
          }

          // Legacy flags mapping
          if (typeof parsed.enableImmersiveSearch === 'boolean') {
            merged.immersiveSearchStyles = parsed.enableImmersiveSearch;
          }
          if (typeof parsed.enableSidebarHover === 'boolean') {
            merged.hoverStyles = parsed.enableSidebarHover;
          }
          if (typeof parsed.enableCenteredPlayer === 'boolean') {
            merged.centeredPlayerStyles = parsed.enableCenteredPlayer;
          }
          if (typeof parsed.enableScrollToTop === 'boolean') {
            merged.scrollToTopStyles = parsed.enableScrollToTop;
          }

          return merged;
        }
      }
    }
  } catch {}

  try {
    const stored = localStorage.getItem('youtube-plus-music-settings');
    if (stored) {
      const parsed = JSON.parse(stored);
      if (parsed && typeof parsed === 'object') {
        const merged = { ...defaults };
        if (typeof parsed.enableMusic === 'boolean') merged.enableMusic = parsed.enableMusic;
        for (const key of Object.keys(defaults)) {
          if (key === 'enableMusic') continue;
          if (typeof parsed[key] === 'boolean') merged[key] = parsed[key];
        }

        // Legacy flags mapping
        if (typeof parsed.enableImmersiveSearch === 'boolean') {
          merged.immersiveSearchStyles = parsed.enableImmersiveSearch;
        }
        if (typeof parsed.enableSidebarHover === 'boolean') {
          merged.hoverStyles = parsed.enableSidebarHover;
        }
        if (typeof parsed.enableCenteredPlayer === 'boolean') {
          merged.centeredPlayerStyles = parsed.enableCenteredPlayer;
        }
        if (typeof parsed.enableScrollToTop === 'boolean') {
          merged.scrollToTopStyles = parsed.enableScrollToTop;
        }

        // Backward-compat: enable if any legacy flags are enabled
        const legacyEnabled = !!(
          parsed.enableMusicStyles ||
          parsed.enableMusicEnhancements ||
          parsed.enableImmersiveSearch ||
          parsed.enableSidebarHover ||
          parsed.enableCenteredPlayer ||
          parsed.enableScrollToTop
        );
        if (legacyEnabled && typeof parsed.enableMusic !== 'boolean') merged.enableMusic = true;
        return merged;
      }
    }
  } catch (e) {
    console.warn('[YouTube+] Failed to load music settings:', e);
  }
  return defaults;
}

/**
 * Creates the advanced settings section.
 * Note: other modules may append additional items to this section.
 * @param {Object} settings - Settings object
 * @param {Function} t - Translation function
 * @returns {string} Advanced section HTML
 */
function createAdvancedSettingsSection(settings, t) {
  const musicSettings = getMusicSettings();
  const musicEnabled = !!musicSettings.enableMusic;
  const enhancedEnabled = settings.enableEnhanced !== false;

  // Enhanced features settings with defaults
  const enhancedSettings = {
    enablePlayAll: settings.enablePlayAll !== false,
    enableResumeTime: settings.enableResumeTime !== false,
    enableZoom: settings.enableZoom !== false,
    enableThumbnail: settings.enableThumbnail !== false,
    enablePlaylistSearch: settings.enablePlaylistSearch !== false,
    enableScrollToTopButton: settings.enableScrollToTopButton !== false,
  };

  return `
    <div class="ytp-plus-settings-section hidden" data-section="advanced">
      <div class="ytp-plus-settings-group">
        <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu">
          <div>
            <label class="ytp-plus-settings-item-label">${tr(t, 'enhancedFeaturesTitle', 'Enhanced Features')}</label>
            <div class="ytp-plus-settings-item-description">${tr(t, 'enhancedFeaturesDesc', 'Additional productivity features and UI enhancements')}</div>
          </div>
          <div class="ytp-plus-settings-item-actions">
            <button
              type="button"
              class="ytp-plus-submenu-toggle"
              data-submenu="enhanced"
              aria-label="Toggle enhanced features submenu"
              aria-expanded="${enhancedEnabled ? 'true' : 'false'}"
              ${enhancedEnabled ? '' : 'disabled'}
              style="display:${enhancedEnabled ? 'inline-flex' : 'none'};"
            >
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="6 9 12 15 18 9"></polyline>
              </svg>
            </button>
            <input type="checkbox" class="ytp-plus-settings-checkbox" data-setting="enableEnhanced" ${
              enhancedEnabled ? 'checked' : ''
            }>
          </div>
        </div>

        <div class="enhanced-submenu" data-submenu="enhanced" style="display:${
          enhancedEnabled ? 'block' : 'none'
        };margin-left:12px;margin-bottom:12px;">
          <div class="glass-card" style="display:flex;flex-direction:column;gap:8px;">
            <div class="endscreen-settings-slot"></div>
            ${createSettingsItem(
              tr(t, 'enablePlayAllLabel', 'Play All Button'),
              tr(t, 'enablePlayAllDesc', 'Add Play All button to playlists and channel pages'),
              'enablePlayAll',
              enhancedSettings.enablePlayAll
            )}
            ${createSettingsItem(
              tr(t, 'enableResumeTimeLabel', 'Resume Playback'),
              tr(t, 'enableResumeTimeDesc', 'Remember video position and offer to resume'),
              'enableResumeTime',
              enhancedSettings.enableResumeTime
            )}
            ${createSettingsItem(
              tr(t, 'enableZoomLabel', 'Video Zoom'),
              tr(t, 'enableZoomDesc', 'Enable zoom and pan controls for video player'),
              'enableZoom',
              enhancedSettings.enableZoom
            )}
            ${createSettingsItem(
              tr(t, 'thumbnailPreview', 'Thumbnail Preview'),
              tr(
                t,
                'thumbnailPreviewDesc',
                'Add a button to thumbnails/avatars/banners to open the original image'
              ),
              'enableThumbnail',
              enhancedSettings.enableThumbnail
            )}
            ${createSettingsItem(
              tr(t, 'enablePlaylistSearchLabel', 'Playlist Search'),
              tr(t, 'enablePlaylistSearchDesc', 'Add search functionality to playlist panels'),
              'enablePlaylistSearch',
              enhancedSettings.enablePlaylistSearch
            )}
            ${createSettingsItem(
              tr(t, 'scrollToTopButtonLabel', 'Scroll to Top'),
              tr(t, 'scrollToTopButtonDesc', 'Show scroll-to-top button on pages'),
              'enableScrollToTopButton',
              enhancedSettings.enableScrollToTopButton
            )}
            <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu" style="margin-top:4px;">
              <div>
                <label class="ytp-plus-settings-item-label">${tr(t, 'enableLoopLabel', 'Loop')}</label>
                <div class="ytp-plus-settings-item-description">${tr(t, 'enableLoopDesc', 'Enable looping of videos and custom segments (A → B)')}</div>
              </div>
              <div class="ytp-plus-settings-item-actions">
                <input type="checkbox" class="ytp-plus-settings-checkbox" data-setting="enableLoop" ${
                  settings.enableLoop ? 'checked' : ''
                }>
              </div>
            </div>
            ${createLoopSubmenu(settings, t)}
          </div>
        </div>

        <div class="ytp-plus-settings-item ytp-plus-settings-item--with-submenu">
          <div>
            <label class="ytp-plus-settings-item-label">${t('youtubeMusicTitle')}</label>
            <div class="ytp-plus-settings-item-description">${t('youtubeMusicDesc')}</div>
          </div>
          <div class="ytp-plus-settings-item-actions">
            <button
              type="button"
              class="ytp-plus-submenu-toggle"
              data-submenu="music"
              aria-label="Toggle YouTube Music submenu"
              aria-expanded="${musicEnabled ? 'true' : 'false'}"
              ${musicEnabled ? '' : 'disabled'}
              style="display:${musicEnabled ? 'inline-flex' : 'none'};"
            >
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="6 9 12 15 18 9"></polyline>
              </svg>
            </button>
            <input type="checkbox" class="ytp-plus-settings-checkbox" data-setting="enableMusic" ${
              musicSettings.enableMusic ? 'checked' : ''
            }>
          </div>
        </div>

        <div class="music-submenu" data-submenu="music" style="display:${
          musicEnabled ? 'block' : 'none'
        };margin-left:12px;margin-bottom:12px;">
          <div class="glass-card" style="display:flex;flex-direction:column;gap:8px;">
            ${createSettingsItem(
              t('immersiveSearchLabel'),
              t('immersiveSearchDesc'),
              'immersiveSearchStyles',
              musicSettings.immersiveSearchStyles
            )}
            ${createSettingsItem(
              t('sidebarHoverLabel'),
              t('sidebarHoverDesc'),
              'hoverStyles',
              musicSettings.hoverStyles
            )}
            ${createSettingsItem(
              t('playerSidebarStylesLabel'),
              t('playerSidebarStylesDesc'),
              'playerSidebarStyles',
              musicSettings.playerSidebarStyles
            )}
            ${createSettingsItem(
              t('centeredPlayerLabel'),
              t('centeredPlayerDesc'),
              'centeredPlayerStyles',
              musicSettings.centeredPlayerStyles
            )}
            ${createSettingsItem(
              t('playerBarStylesLabel'),
              t('playerBarStylesDesc'),
              'playerBarStyles',
              musicSettings.playerBarStyles
            )}
            ${createSettingsItem(
              t('centeredPlayerBarStylesLabel'),
              t('centeredPlayerBarStylesDesc'),
              'centeredPlayerBarStyles',
              musicSettings.centeredPlayerBarStyles
            )}
            ${createSettingsItem(
              t('miniPlayerStylesLabel'),
              t('miniPlayerStylesDesc'),
              'miniPlayerStyles',
              musicSettings.miniPlayerStyles
            )}
          </div>
        </div>
      </div>
    </div>
  `;
}

/**
 * Creates the experimental settings section with YouTube Music options
 * @param {Object} settings - Settings object
 * @param {Function} t - Translation function
 * @returns {string} Experimental section HTML
 */
function createExperimentalSettingsSection(_settings, _t) {
  return `
    <div class="ytp-plus-settings-section hidden" data-section="experimental"></div>
  `;
}

/**
 * Creates the voting section
 * @param {Object} _settings - Settings object
 * @param {Function} t - Translation function
 * @returns {string} Voting section HTML
 */
function createVotingSection(_settings, t) {
  return `
    <div class="ytp-plus-settings-section hidden" data-section="voting">
      <div class="ytp-plus-settings-voting-header">
        <h3>${tr(t, 'votingTitle', 'Feature Requests')}</h3>
        <p class="ytp-plus-settings-voting-desc">${tr(t, 'votingDesc', 'Vote for features you want to see in YouTube+')}</p>
      </div>

      <div class="ytp-plus-voting-preview">
        <div class="ytp-plus-ba-container">
          <div class="ytp-plus-ba-before">
            <img src="https://i.imgur.com/FVW4tdH.jpeg" alt="Before" draggable="false" />
            <span class="ytp-plus-ba-label ytp-plus-ba-label-before">Before</span>
          </div>
          <div class="ytp-plus-ba-after">
            <img src="https://i.imgur.com/ljq1KeL.jpeg" alt="After" draggable="false" />
            <span class="ytp-plus-ba-label ytp-plus-ba-label-after">After</span>
          </div>
          <div class="ytp-plus-ba-divider" role="separator" tabindex="0" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50"></div>
        </div>

        <div class="ytp-plus-vote-bar-section" id="ytp-plus-vote-bar-section">
          <div class="ytp-plus-vote-bar-buttons">
            <div class="ytp-plus-vote-bar-track" id="ytp-plus-vote-bar-fill"></div>
            <button class="ytp-plus-vote-bar-btn" id="ytp-plus-vote-bar-up" type="button" aria-label="${tr(t, 'like', 'Like')}" data-vote="1">
              <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
            </button>
            <button class="ytp-plus-vote-bar-btn" id="ytp-plus-vote-bar-down" type="button" aria-label="${tr(t, 'dislike', 'Dislike')}" data-vote="-1">
              <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
            </button>
          </div>
          <div class="ytp-plus-vote-bar-count" id="ytp-plus-vote-bar-count">0</div>
        </div>
      </div>

      <div id="ytp-plus-voting-container"></div>
    </div>
  `;
}

/**
 * Creates the main content area
 * @param {Object} settings - Settings object
 * @param {Function} t - Translation function
 * @returns {string} Main content HTML
 */
function createMainContent(settings, t) {
  return `
    <div class="ytp-plus-settings-main">
      <div class="ytp-plus-settings-sidebar-close">
        <button class="ytp-plus-settings-close" aria-label="${t('closeButton')}">
          <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
          </svg>
        </button>
      </div>              
      <div class="ytp-plus-settings-content">                
        ${createBasicSettingsSection(settings, t)}
        ${createAdvancedSettingsSection(settings, t)}
        ${createExperimentalSettingsSection(settings, t)}
        ${createVotingSection(settings, t)}
        <div class="ytp-plus-settings-section hidden" data-section="report"></div>
        ${createAboutSection()}
      </div>
      <div class="ytp-plus-footer">
        <button class="ytp-plus-button ytp-plus-button-primary" id="ytp-plus-save-settings">${t('saveChanges')}</button>
      </div>
    </div>
  `;
}

// Export helper functions to window
if (typeof window !== 'undefined') {
  window.YouTubePlusSettingsHelpers = {
    createSettingsSidebar,
    createMainContent,
    createSettingsItem,
    createDownloadSiteOption,
    createBasicSettingsSection,
    createAdvancedSettingsSection,
    createExperimentalSettingsSection,
    createVotingSection,
    getMusicSettings,
  };
}

// --- MODULE: modal-handlers.js ---

/**
 * Modal Event Handlers
 * Extracted from createSettingsModal to reduce complexity
 */

/* global GM_setValue, GM_getValue */

// DOM cache helpers with fallback
const qs = selector => {
  if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.get === 'function') {
    return window.YouTubeDOMCache.get(selector);
  }
  return document.querySelector(selector);
};

/**
 * Safely set a setting by path (supports dot notation)
 * @param {Record<string, any>} settings
 * @param {string} path
 * @param {any} value
 */
const setSettingByPath = (settings, path, value) => {
  if (!settings || typeof settings !== 'object') return;
  if (!path || typeof path !== 'string') return;

  // Fast path: simple key
  if (!path.includes('.')) {
    settings[path] = value;
    return;
  }

  const keys = path.split('.').filter(Boolean);
  if (!keys.length) return;
  const lastKey = keys.pop();
  if (!lastKey) return;

  let cur = settings;
  for (const k of keys) {
    if (!Object.prototype.hasOwnProperty.call(cur, k) || typeof cur[k] !== 'object' || !cur[k]) {
      cur[k] = {};
    }
    cur = cur[k];
  }
  cur[lastKey] = value;
};

/**
 * Initialize download sites settings
 * @param {Object} settings - Settings object
 */
const initializeDownloadSites = settings => {
  if (!settings.downloadSites) {
    settings.downloadSites = { externalDownloader: true, ytdl: true, direct: true };
  }
  // Migrate old key if present
  if (
    settings.downloadSites &&
    Object.prototype.hasOwnProperty.call(settings.downloadSites, 'y2mate')
  ) {
    if (!Object.prototype.hasOwnProperty.call(settings.downloadSites, 'externalDownloader')) {
      settings.downloadSites.externalDownloader = settings.downloadSites.y2mate;
    }
    delete settings.downloadSites.y2mate;
  }
};

/**
 * Toggle download site controls visibility
 * @param {HTMLInputElement} checkbox - Checkbox element
 */
const toggleDownloadSiteControls = checkbox => {
  try {
    const container = checkbox.closest('.download-site-option');
    if (container) {
      const controls = container.querySelector('.download-site-controls');
      if (controls) {
        controls.style.display = checkbox.checked ? 'block' : 'none';
      }
    }
  } catch (err) {
    console.warn('[YouTube+] toggle download-site-controls failed:', err);
  }
};

/**
 * Save settings safely
 * @param {Function} saveSettings - Save function
 */
const safelySaveSettings = saveSettings => {
  try {
    saveSettings();
  } catch (err) {
    console.warn('[YouTube+] autosave downloadSite toggle failed:', err);
  }
};

/**
 * Handle download site checkbox toggle
 * @param {HTMLElement} target - Checkbox element
 * @param {string} key - Site key (y2mate, ytdl, direct)
 * @param {Object} settings - Settings object
 * @param {Function} markDirty - Function to mark modal as dirty
 * @param {Function} saveSettings - Function to save settings
 */
const handleDownloadSiteToggle = (target, key, settings, markDirty, saveSettings) => {
  initializeDownloadSites(settings);

  const checkbox = /** @type {HTMLInputElement} */ (target);
  settings.downloadSites[key] = checkbox.checked;

  try {
    markDirty();
  } catch {}

  toggleDownloadSiteControls(checkbox);
  rebuildDownloadDropdown(settings);
  safelySaveSettings(saveSettings);
};

/**
 * Handle Download button live toggle
 * @param {Object} context - Context object with methods
 */
const handleDownloadButtonToggle = context => {
  const { settings, getElement, addDownloadButton } = context;
  const controls = getElement('.ytp-right-controls');
  const existing = getElement('.ytp-download-button', false);

  if (settings.enableDownload) {
    // create button if missing
    if (controls && !existing) addDownloadButton(controls);
  } else {
    // remove button + dropdown if present
    if (existing) existing.remove();
    const dropdown = qs('.download-options');
    if (dropdown) dropdown.remove();
  }
};

/**
 * Handle Speed Control live toggle
 * @param {Object} context - Context object with methods
 */
const handleSpeedControlToggle = context => {
  const { settings, getElement, addSpeedControlButton } = context;
  const controls = getElement('.ytp-right-controls');
  const existing = getElement('.speed-control-btn', false);

  if (settings.enableSpeedControl) {
    if (controls && !existing) addSpeedControlButton(controls);
  } else {
    if (existing) existing.remove();
    const speedOptions = qs('.speed-options');
    if (speedOptions) speedOptions.remove();
  }
};

/**
 * Update global settings exposure
 * @param {Object} settings - Settings object
 */
const updateGlobalSettings = settings => {
  if (typeof window !== 'undefined' && window.youtubePlus) {
    window.youtubePlus.settings = window.youtubePlus.settings || settings;
  }
};

/**
 * Apply setting changes live to the UI
 * @param {string} setting - Setting key
 * @param {Object} context - Context object with methods
 */
const applySettingLive = (setting, context) => {
  const { settings, refreshDownloadButton } = context;

  try {
    // Update page elements (show/hide buttons, dropdowns)
    if (context.updatePageBasedOnSettings) {
      context.updatePageBasedOnSettings();
    }

    // Dispatch to specific handlers
    if (setting === 'enableDownload') {
      handleDownloadButtonToggle(context);
    } else if (setting === 'enableSpeedControl') {
      handleSpeedControlToggle(context);
    }

    // Ensure visibility state updates
    if (refreshDownloadButton) {
      refreshDownloadButton();
    }
  } catch (innerErr) {
    console.warn('[YouTube+] live apply specific toggle failed:', innerErr);
  }

  // Expose updated settings globally for other modules
  updateGlobalSettings(settings);
};

/**
 * Handle simple setting checkbox toggle
 * @param {HTMLElement} target - Checkbox element
 * @param {string} setting - Setting key
 * @param {Object} settings - Settings object
 * @param {Object} context - Context object with methods
 * @param {Function} markDirty - Function to mark modal as dirty
 * @param {Function} saveSettings - Function to save settings
 * @param {HTMLElement} modal - Modal element
 */
const handleSimpleSettingToggle = (
  target,
  setting,
  settings,
  context,
  markDirty,
  saveSettings,
  modal
) => {
  const checked = /** @type {HTMLInputElement} */ (target).checked;
  setSettingByPath(settings, setting, checked);

  // Mark modal as dirty
  try {
    markDirty();
  } catch {}

  // Apply settings immediately
  try {
    applySettingLive(setting, context);
  } catch (err) {
    console.warn('[YouTube+] apply settings live failed:', err);
  }

  // Persist immediately
  try {
    saveSettings();
  } catch (err) {
    console.warn('[YouTube+] autosave simple setting failed:', err);
  }

  // Show/hide submenu for Download
  if (setting === 'enableDownload') {
    const submenu = modal.querySelector('.download-submenu');
    if (submenu) {
      submenu.style.display = checked ? 'block' : 'none';
    }
    const toggleBtn = modal.querySelector('.ytp-plus-submenu-toggle[data-submenu="download"]');
    if (toggleBtn instanceof HTMLElement) {
      if (checked) {
        toggleBtn.removeAttribute('disabled');
        toggleBtn.setAttribute('aria-expanded', 'true');
        toggleBtn.style.display = 'inline-flex';
      } else {
        toggleBtn.setAttribute('disabled', '');
        toggleBtn.setAttribute('aria-expanded', 'false');
        toggleBtn.style.display = 'none';
      }
    }
  }

  // Show/hide submenu for Zen Styles
  if (setting === 'enableZenStyles') {
    const submenu = modal.querySelector('.style-submenu');
    if (submenu) {
      submenu.style.display = checked ? 'block' : 'none';
    }
    const toggleBtn = modal.querySelector('.ytp-plus-submenu-toggle[data-submenu="style"]');
    if (toggleBtn instanceof HTMLElement) {
      if (checked) {
        toggleBtn.removeAttribute('disabled');
        toggleBtn.setAttribute('aria-expanded', 'true');
        toggleBtn.style.display = 'inline-flex';
      } else {
        toggleBtn.setAttribute('disabled', '');
        toggleBtn.setAttribute('aria-expanded', 'false');
        toggleBtn.style.display = 'none';
      }
    }
  }

  // Show/hide submenu for Speed Control
  if (setting === 'enableSpeedControl') {
    const submenu = modal.querySelector('.speed-submenu');
    if (submenu) {
      submenu.style.display = checked ? 'block' : 'none';
    }
    const toggleBtn = modal.querySelector('.ytp-plus-submenu-toggle[data-submenu="speed"]');
    if (toggleBtn instanceof HTMLElement) {
      if (checked) {
        toggleBtn.removeAttribute('disabled');
        toggleBtn.setAttribute('aria-expanded', 'true');
        toggleBtn.style.display = 'inline-flex';
      } else {
        toggleBtn.setAttribute('disabled', '');
        toggleBtn.setAttribute('aria-expanded', 'false');
        toggleBtn.style.display = 'none';
      }
    }
  }

  // Show/hide submenu for Enhanced Features
  if (setting === 'enableEnhanced') {
    const submenu = modal.querySelector('.enhanced-submenu');
    if (submenu) {
      submenu.style.display = checked ? 'block' : 'none';
    }
    const toggleBtn = modal.querySelector('.ytp-plus-submenu-toggle[data-submenu="enhanced"]');
    if (toggleBtn instanceof HTMLElement) {
      if (checked) {
        toggleBtn.removeAttribute('disabled');
        toggleBtn.setAttribute('aria-expanded', 'true');
        toggleBtn.style.display = 'inline-flex';
      } else {
        toggleBtn.setAttribute('disabled', '');
        toggleBtn.setAttribute('aria-expanded', 'false');
        toggleBtn.style.display = 'none';
      }
    }
  }

  // Show/hide submenu for Loop
  if (setting === 'enableLoop') {
    const submenu = modal.querySelector('.loop-submenu');
    if (submenu) {
      submenu.style.display = checked ? 'block' : 'none';
    }
    const toggleBtn = modal.querySelector('.ytp-plus-submenu-toggle[data-submenu="loop"]');
    if (toggleBtn instanceof HTMLElement) {
      if (checked) {
        toggleBtn.removeAttribute('disabled');
        toggleBtn.setAttribute('aria-expanded', 'true');
        toggleBtn.style.display = 'inline-flex';
      } else {
        toggleBtn.setAttribute('disabled', '');
        toggleBtn.setAttribute('aria-expanded', 'false');
        toggleBtn.style.display = 'none';
      }
    }
  }
};

/**
 * Handle download site customization input
 * @param {HTMLElement} target - Input element
 * @param {string} site - Site key
 * @param {string} field - Field name (name or url)
 * @param {Object} settings - Settings object
 * @param {Function} markDirty - Function to mark modal as dirty
 * @param {Function} t - Translation function
 */
/**
 * Initialize download site customization settings
 * @param {Object} settings - Settings object
 */
const initializeDownloadCustomization = settings => {
  if (!settings.downloadSiteCustomization) {
    settings.downloadSiteCustomization = {
      externalDownloader: { name: 'SSYouTube', url: 'https://ssyoutube.com/watch?v={videoId}' },
    };
  }
  // Migrate previous customization
  if (
    settings.downloadSiteCustomization &&
    Object.prototype.hasOwnProperty.call(settings.downloadSiteCustomization, 'y2mate')
  ) {
    if (
      !Object.prototype.hasOwnProperty.call(
        settings.downloadSiteCustomization,
        'externalDownloader'
      )
    ) {
      settings.downloadSiteCustomization.externalDownloader =
        settings.downloadSiteCustomization.y2mate;
    }
    delete settings.downloadSiteCustomization.y2mate;
  }
};

/**
 * Initialize specific download site settings
 * @param {Object} settings - Settings object
 * @param {string} site - Site key
 */
const initializeDownloadSite = (settings, site) => {
  if (!settings.downloadSiteCustomization[site]) {
    settings.downloadSiteCustomization[site] = { name: '', url: '' };
  }
};

/**
 * Get fallback name for download site
 * @param {string} site - Site key
 * @param {Function} t - Translation function
 * @returns {string} Fallback name
 */
const getDownloadSiteFallbackName = (site, t) => {
  if (site === 'externalDownloader') return 'SSYouTube';
  if (site === 'ytdl') return t('byYTDL');
  return t('directDownload');
};

/**
 * Update download site name in UI
 * @param {HTMLElement} target - Input element
 * @param {string} site - Site key
 * @param {Function} t - Translation function
 */
const updateDownloadSiteName = (target, site, t) => {
  const nameDisplay = target.closest('.download-site-option')?.querySelector('.download-site-name');

  if (nameDisplay) {
    const inputValue = /** @type {HTMLInputElement} */ (target).value;
    const fallbackName = getDownloadSiteFallbackName(site, t);
    nameDisplay.textContent = inputValue || fallbackName;
  }
};

/**
 * Rebuild download dropdown in UI
 * @param {Object} settings - Settings object
 */
const rebuildDownloadDropdown = settings => {
  try {
    if (
      typeof window !== 'undefined' &&
      window.youtubePlus &&
      typeof window.youtubePlus.rebuildDownloadDropdown === 'function'
    ) {
      window.youtubePlus.settings = window.youtubePlus.settings || settings;
      window.youtubePlus.rebuildDownloadDropdown();
    }
  } catch (err) {
    console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err);
  }
};

/**
 * Handle download site input change
 * @param {HTMLElement} target - Input element
 * @param {string} site - Site key (y2mate, ytdl, direct)
 * @param {string} field - Field name (name, url)
 * @param {Object} settings - Settings object
 * @param {Function} markDirty - Function to mark modal as dirty
 * @param {Function} t - Translation function
 */
const handleDownloadSiteInput = (target, site, field, settings, markDirty, t) => {
  initializeDownloadCustomization(settings);
  initializeDownloadSite(settings, site);

  settings.downloadSiteCustomization[site][field] = /** @type {HTMLInputElement} */ (target).value;

  try {
    markDirty();
  } catch {}

  if (field === 'name') {
    updateDownloadSiteName(target, site, t);
  }

  rebuildDownloadDropdown(settings);
};

/**
 * Handle Y2Mate save button
 * @param {HTMLElement} target - Button element
 * @param {Object} settings - Settings object
 * @param {Function} saveSettings - Function to save settings
 * @param {Function} showNotification - Function to show notification
 * @param {Function} t - Translation function
 */
/**
 * Ensure external downloader settings structure exists
 * @param {Object} settings - Settings object
 */
const ensureExternalDownloaderStructure = settings => {
  if (!settings.downloadSiteCustomization) {
    settings.downloadSiteCustomization = {
      externalDownloader: { name: 'SSYouTube', url: 'https://ssyoutube.com/watch?v={videoId}' },
    };
  }
  if (!settings.downloadSiteCustomization.externalDownloader) {
    settings.downloadSiteCustomization.externalDownloader = { name: '', url: '' };
  }
};

/**
 * Read external downloader input values from container
 * @param {HTMLElement} container - Container element
 * @param {Object} settings - Settings object
 */
const readExternalDownloaderInputs = (container, settings) => {
  const nameInput = container.querySelector(
    'input.download-site-input[data-site="externalDownloader"][data-field="name"]'
  );
  const urlInput = container.querySelector(
    'input.download-site-input[data-site="externalDownloader"][data-field="url"]'
  );
  if (nameInput) settings.downloadSiteCustomization.externalDownloader.name = nameInput.value;
  if (urlInput) settings.downloadSiteCustomization.externalDownloader.url = urlInput.value;
};

/**
 * Trigger rebuild of the download dropdown if available
 */
const triggerRebuildDropdown = () => {
  try {
    if (
      typeof window !== 'undefined' &&
      window.youtubePlus &&
      typeof window.youtubePlus.rebuildDownloadDropdown === 'function'
    ) {
      window.youtubePlus.rebuildDownloadDropdown();
    }
  } catch (err) {
    console.warn('[YouTube+] rebuildDownloadDropdown call failed:', err);
  }
};

const handleExternalDownloaderSave = (target, settings, saveSettings, showNotification, t) => {
  ensureExternalDownloaderStructure(settings);

  const container = target.closest('.download-site-option');
  if (container) {
    readExternalDownloaderInputs(container, settings);
  }

  saveSettings();

  if (window.youtubePlus) {
    window.youtubePlus.settings = window.youtubePlus.settings || settings;
  }
  triggerRebuildDropdown();
  try {
    const msg =
      (t && typeof t === 'function' && t('externalDownloaderSettingsSaved')) ||
      t('y2mateSettingsSaved');
    showNotification(msg);
  } catch {
    showNotification('Settings saved');
  }
};

/**
 * Reset external downloader to default values
 * @param {Object} settings - Settings object
 */
const resetExternalDownloaderToDefaults = settings => {
  ensureExternalDownloaderStructure(settings);
  settings.downloadSiteCustomization.externalDownloader = {
    name: 'SSYouTube',
    url: 'https://ssyoutube.com/watch?v={videoId}',
  };
};

/**
 * Update Y2Mate modal inputs
 * @param {HTMLElement} container - Container element
 * @param {Object} settings - Settings object
 */
const updateExternalDownloaderModalInputs = (container, settings) => {
  const nameInput = container.querySelector(
    'input.download-site-input[data-site="externalDownloader"][data-field="name"]'
  );
  const urlInput = container.querySelector(
    'input.download-site-input[data-site="externalDownloader"][data-field="url"]'
  );
  const nameDisplay = container.querySelector('.download-site-name');

  const edSettings = settings.downloadSiteCustomization.externalDownloader;
  if (nameInput) nameInput.value = edSettings.name;
  if (urlInput) urlInput.value = edSettings.url;
  if (nameDisplay) nameDisplay.textContent = edSettings.name;
};

/**
 * Handle Y2Mate reset button
 * @param {HTMLElement} modal - Modal element
 * @param {Object} settings - Settings object
 * @param {Function} saveSettings - Function to save settings
 * @param {Function} showNotification - Function to show notification
 * @param {Function} t - Translation function
 */
const handleExternalDownloaderReset = (modal, settings, saveSettings, showNotification, t) => {
  resetExternalDownloaderToDefaults(settings);

  const container = modal.querySelector('.download-site-option');
  if (container) {
    updateExternalDownloaderModalInputs(container, settings);
  }

  saveSettings();

  if (window.youtubePlus) {
    window.youtubePlus.settings = window.youtubePlus.settings || settings;
  }
  triggerRebuildDropdown();
  try {
    const msg = (t && typeof t === 'function' && t('externalDownloaderReset')) || t('y2mateReset');
    showNotification(msg);
  } catch {
    showNotification('Settings reset');
  }
};

/**
 * Handle sidebar navigation
 * @param {HTMLElement} navItem - Navigation item element
 * @param {HTMLElement} modal - Modal element
 */
const handleSidebarNavigation = (navItem, modal) => {
  const { dataset } = navItem;
  const { section } = dataset;

  modal
    .querySelectorAll('.ytp-plus-settings-nav-item')
    .forEach(item => item.classList.remove('active'));
  modal.querySelectorAll('.ytp-plus-settings-section').forEach(s => s.classList.add('hidden'));

  navItem.classList.add('active');

  const targetSection = modal.querySelector(
    `.ytp-plus-settings-section[data-section="${section}"]`
  );
  if (targetSection) targetSection.classList.remove('hidden');

  // Init before/after slider when voting section becomes visible
  if (section === 'voting' && window.YouTubePlus?.Voting?.initSlider) {
    // Use rAF so the section is truly visible before measuring dimensions
    requestAnimationFrame(() => window.YouTubePlus.Voting.initSlider());
  }

  // Persist active nav section so it can be restored on next modal open
  try {
    localStorage.setItem('ytp-plus-active-nav-section', section);
  } catch {}
};

/**
 * Handle YouTube Music settings toggle
 * @param {HTMLElement} target - Checkbox element
 * @param {string} setting - Setting key
 * @param {Function} showNotification - Function to show notification
 * @param {Function} t - Translation function
 */
const handleMusicSettingToggle = (target, setting, showNotification, t) => {
  try {
    const defaults = {
      enableMusic: true,
      immersiveSearchStyles: true,
      hoverStyles: true,
      playerSidebarStyles: true,
      centeredPlayerStyles: true,
      playerBarStyles: true,
      centeredPlayerBarStyles: true,
      miniPlayerStyles: true,
      scrollToTopStyles: true,
    };

    const allowedKeys = new Set(Object.keys(defaults));
    if (!allowedKeys.has(setting)) return;

    // Load current music settings (prefer GM storage for cross-subdomain sync)
    /** @type {Record<string, any>} */
    let musicSettings = { ...defaults };

    try {
      if (typeof GM_getValue !== 'undefined') {
        const stored = GM_getValue('youtube-plus-music-settings', null);
        if (typeof stored === 'string' && stored) {
          const parsed = JSON.parse(stored);
          if (parsed && typeof parsed === 'object') musicSettings = { ...musicSettings, ...parsed };
        }
      }
    } catch {}

    try {
      const stored = localStorage.getItem('youtube-plus-music-settings');
      if (stored) {
        const parsed = JSON.parse(stored);
        if (parsed && typeof parsed === 'object') musicSettings = { ...musicSettings, ...parsed };
      }
    } catch {}

    musicSettings[setting] = /** @type {HTMLInputElement} */ (target).checked;

    // UI: toggle visibility of music submenu card when main switch changes
    try {
      if (setting === 'enableMusic') {
        const enabled = !!musicSettings.enableMusic;
        const root = /** @type {HTMLElement|null} */ (
          target.closest('.ytp-plus-settings-section') || target.closest('.ytp-plus-settings-panel')
        );
        if (root) {
          const submenu = root.querySelector('.music-submenu[data-submenu="music"]');
          if (submenu instanceof HTMLElement) {
            submenu.style.display = enabled ? 'block' : 'none';
          }
          const toggleBtn = root.querySelector('.ytp-plus-submenu-toggle[data-submenu="music"]');
          if (toggleBtn instanceof HTMLElement) {
            if (enabled) {
              toggleBtn.removeAttribute('disabled');
              toggleBtn.style.display = 'inline-flex';
            } else {
              toggleBtn.setAttribute('disabled', '');
              toggleBtn.style.display = 'none';
            }
            toggleBtn.setAttribute('aria-expanded', enabled ? 'true' : 'false');
          }
        }
      }
    } catch {}

    // Save to localStorage
    localStorage.setItem('youtube-plus-music-settings', JSON.stringify(musicSettings));

    // Save to userscript-global storage so youtube.com and music.youtube.com share settings.
    try {
      if (typeof GM_setValue !== 'undefined') {
        GM_setValue('youtube-plus-music-settings', JSON.stringify(musicSettings));
      }
    } catch {}

    // Apply changes if YouTubeMusic module is available
    if (typeof window !== 'undefined' && window.YouTubeMusic) {
      if (window.YouTubeMusic.saveSettings) {
        window.YouTubeMusic.saveSettings(musicSettings);
      }
      if (window.YouTubeMusic.applySettingsChanges) {
        window.YouTubeMusic.applySettingsChanges();
      }
    }

    // Show notification
    if (showNotification && t) {
      showNotification(t('musicSettingsSaved'));
    }
  } catch {
    console.warn('[YouTube+] handleMusicSettingToggle failed');
  }
};

/**
 * Check if a setting is a YouTube Music setting
 * @param {string} setting - Setting key
 * @returns {boolean} True if it's a music setting
 */
const isMusicSetting = setting => {
  return (
    setting === 'enableMusic' ||
    setting === 'immersiveSearchStyles' ||
    setting === 'hoverStyles' ||
    setting === 'playerSidebarStyles' ||
    setting === 'centeredPlayerStyles' ||
    setting === 'playerBarStyles' ||
    setting === 'centeredPlayerBarStyles' ||
    setting === 'miniPlayerStyles' ||
    setting === 'scrollToTopStyles'
  );
};

// Export handlers
if (typeof window !== 'undefined') {
  window.YouTubePlusModalHandlers = {
    handleDownloadSiteToggle,
    handleSimpleSettingToggle,
    handleDownloadSiteInput,
    handleExternalDownloaderSave,
    handleExternalDownloaderReset,
    handleSidebarNavigation,
    applySettingLive,
    handleMusicSettingToggle,
    isMusicSetting,
  };
}

// --- MODULE: download.js ---

/**
 * YouTube+ Download Module
 * Unified download system with button UI and download functionality
 * @version 3.0
 */

(function () {
  'use strict';

  const isRelevantRoute = () => {
    try {
      const path = location.pathname || '';
      return path === '/watch' || path.startsWith('/shorts');
    } catch {
      return false;
    }
  };

  const onDomReady = cb => {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', cb, { once: true });
    } else {
      cb();
    }
  };

  // DOM cache helpers with fallback
  const $ = selector => {
    if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.get === 'function') {
      return window.YouTubeDOMCache.get(selector);
    }
    return document.querySelector(selector);
  };
  const $$ = selector => {
    if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.getAll === 'function') {
      return window.YouTubeDOMCache.getAll(selector);
    }
    return document.querySelectorAll(selector);
  };

  // Check dependencies
  if (typeof YouTubeUtils === 'undefined') {
    console.error('[YouTube+ Download] YouTubeUtils not found!');
    return;
  }

  // Create a custom glassmorphic subtitle dropdown control
  function createSubtitleSelect() {
    const subtitleSelect = document.createElement('div');
    subtitleSelect.setAttribute('role', 'listbox');
    Object.assign(subtitleSelect.style, {
      position: 'relative',
      width: '100%',
      marginBottom: '8px',
      fontSize: '14px',
      color: '#fff',
      cursor: 'pointer',
    });

    const _ssDisplay = document.createElement('div');
    Object.assign(_ssDisplay.style, {
      padding: '10px 12px',
      borderRadius: '10px',
      background: 'linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02))',
      border: '1px solid rgba(255,255,255,0.06)',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'space-between',
      gap: '8px',
      backdropFilter: 'blur(6px)',
      boxShadow: '0 4px 18px rgba(0,0,0,0.35) inset',
    });
    const _ssLabel = document.createElement('div');
    _ssLabel.style.flex = '1';
    _ssLabel.style.overflow = 'hidden';
    _ssLabel.style.textOverflow = 'ellipsis';
    _ssLabel.style.whiteSpace = 'nowrap';
    _ssLabel.textContent = t('loading');
    const _ssChevron = document.createElement('div');
    _ssChevron.textContent = '▾';
    _ssChevron.style.opacity = '0.8';
    _ssDisplay.appendChild(_ssLabel);
    _ssDisplay.appendChild(_ssChevron);

    const _ssList = document.createElement('div');
    Object.assign(_ssList.style, {
      position: 'absolute',
      top: 'calc(100% + 8px)',
      left: '0',
      right: '0',
      maxHeight: '220px',
      overflowY: 'auto',
      borderRadius: '10px',
      background: 'linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.02))',
      border: '1px solid rgba(255,255,255,0.06)',
      boxShadow: '0 8px 30px rgba(0,0,0,0.6)',
      backdropFilter: 'blur(8px)',
      zIndex: '9999',
      display: 'none',
    });

    subtitleSelect.appendChild(_ssDisplay);
    subtitleSelect.appendChild(_ssList);

    _ssList.addEventListener('click', e => {
      const item = e.target?.closest?.('[data-value]');
      if (!item || !_ssList.contains(item)) return;
      subtitleSelect.value = item.dataset.value;
      _ssList.style.display = 'none';
    });

    _ssList.addEventListener('mouseover', e => {
      const item = e.target?.closest?.('[data-value]');
      if (!item || !_ssList.contains(item)) return;
      item.style.background = 'rgba(255,255,255,0.02)';
    });

    _ssList.addEventListener('mouseout', e => {
      const item = e.target?.closest?.('[data-value]');
      if (!item || !_ssList.contains(item)) return;
      const related = e.relatedTarget;
      if (related && item.contains(related)) return;
      item.style.background = 'transparent';
    });

    subtitleSelect._options = [];
    subtitleSelect._value = '';
    subtitleSelect._disabled = false;

    subtitleSelect.setPlaceholder = text => {
      _ssLabel.textContent = text || '';
      subtitleSelect._options = [];
      _ssList.innerHTML = '';
      subtitleSelect._value = '';
    };

    subtitleSelect.setOptions = options => {
      subtitleSelect._options = options || [];
      _ssList.innerHTML = '';
      subtitleSelect._options.forEach(opt => {
        const item = document.createElement('div');
        item.textContent = opt.text;
        item.dataset.value = String(opt.value);
        Object.assign(item.style, {
          padding: '10px 12px',
          cursor: 'pointer',
          borderBottom: '1px solid rgba(255,255,255,0.02)',
          color: '#fff',
        });
        _ssList.appendChild(item);
      });
      if (subtitleSelect._options.length > 0) {
        subtitleSelect.value = String(subtitleSelect._options[0].value);
      } else {
        subtitleSelect._value = '';
        _ssLabel.textContent = t('noSubtitles');
      }
    };

    Object.defineProperty(subtitleSelect, 'value', {
      get() {
        return subtitleSelect._value;
      },
      set(v) {
        subtitleSelect._value = String(v);
        const found = subtitleSelect._options.find(o => String(o.value) === subtitleSelect._value);
        _ssLabel.textContent = found ? found.text : '';
      },
    });

    Object.defineProperty(subtitleSelect, 'disabled', {
      get() {
        return subtitleSelect._disabled;
      },
      set(v) {
        subtitleSelect._disabled = !!v;
        _ssDisplay.style.opacity = subtitleSelect._disabled ? '0.5' : '1';
        subtitleSelect.style.pointerEvents = subtitleSelect._disabled ? 'none' : 'auto';
      },
    });

    _ssDisplay.addEventListener('click', () => {
      if (subtitleSelect._disabled) return;
      _ssList.style.display = _ssList.style.display === 'none' ? '' : 'none';
    });

    const _ac = new AbortController();
    document.addEventListener(
      'click',
      e => {
        if (!subtitleSelect.contains(e.target)) _ssList.style.display = 'none';
      },
      { signal: _ac.signal }
    );

    subtitleSelect.destroy = () => _ac.abort();

    return subtitleSelect;
  }

  const { NotificationManager } = YouTubeUtils;

  // Translation helper: dynamically resolve central i18n (embedded) at call time
  // to avoid missing translations due to initialization order. Falls back to
  // YouTubeUtils.t if present, otherwise returns the key (with simple params).
  function t(key, params = {}) {
    try {
      if (typeof window !== 'undefined') {
        if (window.YouTubePlusI18n && typeof window.YouTubePlusI18n.t === 'function') {
          return window.YouTubePlusI18n.t(key, params);
        }
        if (window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function') {
          return window.YouTubeUtils.t(key, params);
        }
      }
    } catch {
      // ignore and fall back
    }

    // Minimal fallback: return key with simple interpolation
    const str = String(key || '');
    if (!params || Object.keys(params).length === 0) return str;
    let result = str;
    for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v));
    return result;
  }

  // Initialize logger (logger is defined in build order before this module)
  /* global YouTubePlusLogger */
  const logger =
    typeof YouTubePlusLogger !== 'undefined' && YouTubePlusLogger
      ? YouTubePlusLogger.createLogger('Download')
      : {
          debug: () => {},
          info: () => {},
          warn: console.warn.bind(console),
          error: console.error.bind(console),
        };

  /**
   * Download Configuration
   */
  const DownloadConfig = {
    // TubeInsights API endpoints (mp3yt.is backend)
    API: {
      KEY_URL: 'https://cnv.cx/v2/sanity/key',
      CONVERT_URL: 'https://cnv.cx/v2/converter',
    },

    // HTTP headers for API requests
    HEADERS: {
      'Content-Type': 'application/json',
      Origin: 'https://mp3yt.is',
      Accept: '*/*',
      'User-Agent':
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36',
    },

    // Available video qualities (144p to 4K)
    VIDEO_QUALITIES: ['144', '240', '360', '480', '720', '1080', '1440', '2160'],

    // Available audio bitrates (kbps)
    AUDIO_BITRATES: ['64', '128', '192', '256', '320'],

    // Default download options
    DEFAULTS: {
      format: 'video', // 'video' or 'audio'
      videoQuality: '1080',
      audioBitrate: '320',
      embedThumbnail: true,
    },
  };

  /**
   * Get current YouTube video ID
   * @returns {string|null} Video ID or null
   */
  function getVideoId() {
    const params = new URLSearchParams(window.location.search);
    return params.get('v') || null;
  }

  /**
   * Get current video URL
   * @returns {string} Full video URL
   */
  function getVideoUrl() {
    const videoId = getVideoId();
    return videoId ? `https://www.youtube.com/watch?v=${videoId}` : window.location.href;
  }

  /**
   * Get video title from page
   * @returns {string} Video title or 'video'
   */
  function getVideoTitle() {
    try {
      const titleElement =
        $('h1.ytd-video-primary-info-renderer yt-formatted-string') ||
        $('h1.title yt-formatted-string') ||
        $('ytd-watch-metadata h1');
      return titleElement ? titleElement.textContent.trim() : 'video';
    } catch {
      return 'video';
    }
  }

  /**
   * Sanitize filename for safe file system operations
   * @param {string} filename - Original filename
   * @returns {string} Sanitized filename
   */
  function sanitizeFilename(filename) {
    return filename
      .replace(/[<>:"/\\|?*]/g, '')
      .replace(/\s+/g, ' ')
      .trim()
      .substring(0, 200); // Limit length
  }

  /**
   * Format bytes to human-readable string
   * @param {number} bytes - Byte count
   * @returns {string} Formatted string (e.g., "8.5 MB")
   */
  function formatBytes(bytes) {
    if (bytes === 0) return '0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
  }

  /**
   * Create GM_xmlhttpRequest wrapper with callbacks
   * @param {Object} options - Request options
   * @param {Function} resolve - Promise resolve function
   * @param {Function} reject - Promise reject function
   * @returns {Object} GM_xmlhttpRequest options
   */
  function createGmRequestOptions(options, resolve, reject) {
    return {
      ...options,
      onload: response => {
        if (options.onload) options.onload(response);
        resolve(response);
      },
      onerror: error => {
        if (options.onerror) options.onerror(error);
        reject(error);
      },
      ontimeout: () => {
        if (options.ontimeout) options.ontimeout();
        reject(new Error('Request timeout'));
      },
    };
  }

  /**
   * Build response-like object from fetch response
   * @param {Response} resp - Fetch response
   * @returns {Object} Response-like object
   */
  function buildResponseObject(resp) {
    return {
      status: resp.status,
      statusText: resp.statusText,
      finalUrl: resp.url,
      headers: {},
      responseText: null,
      response: null,
    };
  }

  /**
   * Try to extract text from response
   * @param {Response} resp - Fetch response
   * @param {Object} responseLike - Response-like object to populate
   */
  async function extractResponseText(resp, responseLike) {
    try {
      responseLike.responseText = await resp.text();
    } catch {
      responseLike.responseText = null;
    }
  }

  /**
   * Try to extract blob from response if needed
   * @param {Response} resp - Fetch response
   * @param {Object} responseLike - Response-like object to populate
   * @param {string} responseType - Expected response type
   */
  async function extractResponseBlob(resp, responseLike, responseType) {
    if (responseType === 'blob') {
      try {
        responseLike.response = await resp.blob();
      } catch {
        responseLike.response = null;
      }
    }
  }

  /**
   * Execute fetch-based request as fallback
   * @param {Object} options - Request options
   * @returns {Promise<Object>} Response object
   */
  async function executeFetchFallback(options) {
    const fetchOpts = {
      method: options.method || 'GET',
      headers: options.headers || {},
      body: options.data || options.body || undefined,
    };

    const resp = await fetch(options.url, fetchOpts);
    const responseLike = buildResponseObject(resp);

    await extractResponseText(resp, responseLike);
    await extractResponseBlob(resp, responseLike, options.responseType);

    if (options.onload) options.onload(responseLike);
    return responseLike;
  }

  /**
   * Promise wrapper for GM_xmlhttpRequest
   * @param {Object} options - Request options
   * @returns {Promise<Object>} Response object
   */
  function gmXmlHttpRequest(options) {
    return new Promise((resolve, reject) => {
      // Prefer GM_xmlhttpRequest (userscript/extension context) because it can bypass CORS.
      if (typeof GM_xmlhttpRequest !== 'undefined') {
        GM_xmlhttpRequest(createGmRequestOptions(options, resolve, reject));
        return;
      }

      // Fallback for page context: try using fetch(). Note: fetch() is subject to CORS and
      // may fail where GM_xmlhttpRequest would succeed. This fallback attempts to mimic
      // a similar response shape used by the rest of the code.
      (async () => {
        try {
          const responseLike = await executeFetchFallback(options);
          resolve(responseLike);
        } catch (err) {
          if (options.onerror) options.onerror(err);
          reject(err);
        }
      })();
    });
  }

  /**
   * Create square album art from YouTube thumbnail
   * @param {string} thumbnailUrl - Thumbnail URL
   * @returns {Promise<Blob>} Album art blob
   */
  function createSquareAlbumArt(thumbnailUrl) {
    return new Promise((resolve, reject) => {
      const img = document.createElement('img');
      img.crossOrigin = 'anonymous';

      img.onload = () => {
        const canvas = document.createElement('canvas');
        const size = Math.min(img.width, img.height);
        canvas.width = size;
        canvas.height = size;

        const ctx = canvas.getContext('2d');
        if (!ctx) {
          reject(new Error('Failed to get canvas context'));
          return;
        }

        const sx = (img.width - size) / 2;
        const sy = (img.height - size) / 2;
        ctx.drawImage(img, sx, sy, size, size, 0, 0, size, size);

        canvas.toBlob(
          blob => {
            if (blob) resolve(blob);
            else reject(new Error('Failed to create blob'));
          },
          'image/jpeg',
          0.95
        );
      };

      img.onerror = () => reject(new Error('Failed to load thumbnail'));
      img.src = thumbnailUrl;
    });
  }

  /**
   * Embed album art and metadata into MP3 file
   * Requires ID3Writer library (browser-id3-writer)
   *
   * @param {Blob} mp3Blob - Original MP3 blob
   * @param {Blob} albumArtBlob - Album art image blob
   * @param {Object} metadata - Metadata (title, artist, album)
   * @returns {Promise<Blob>} MP3 blob with embedded metadata
   */
  async function embedAlbumArtToMP3(mp3Blob, albumArtBlob, metadata) {
    try {
      if (typeof window.ID3Writer === 'undefined') {
        logger.warn('ID3Writer not available, skipping album art embedding');
        return mp3Blob;
      }

      const arrayBuffer = await mp3Blob.arrayBuffer();
      const writer = new window.ID3Writer(arrayBuffer);

      // Set metadata
      if (metadata.title) {
        writer.setFrame('TIT2', metadata.title);
      }
      if (metadata.artist) {
        writer.setFrame('TPE1', [metadata.artist]);
      }
      if (metadata.album) {
        writer.setFrame('TALB', metadata.album);
      }

      // Embed album art
      if (albumArtBlob) {
        const coverArrayBuffer = await albumArtBlob.arrayBuffer();
        writer.setFrame('APIC', {
          type: 3, // Cover (front)
          data: coverArrayBuffer,
          description: 'Cover',
        });
      }

      writer.addTag();
      /* global Blob */
      return new Blob([writer.arrayBuffer], { type: 'audio/mpeg' });
    } catch (error) {
      logger.error('Error embedding album art:', error);
      return mp3Blob;
    }
  }

  /**
   * Get available subtitles for a video
   * @param {string} videoId - YouTube video ID
   * @returns {Promise<Object>} Subtitle data
   */
  /**
   * Fetch player data from YouTube API
   * @param {string} videoId - Video ID
   * @returns {Promise<Object>} Player data response
   * @private
   */
  async function fetchPlayerData(videoId) {
    const response = await gmXmlHttpRequest({
      method: 'POST',
      url: 'https://www.youtube.com/youtubei/v1/player',
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': DownloadConfig.HEADERS['User-Agent'],
      },
      data: JSON.stringify({
        context: {
          client: {
            clientName: 'WEB',
            clientVersion: '2.20240304.00.00',
          },
        },
        videoId,
      }),
    });

    if (response.status !== 200) {
      throw new Error(`Failed to get player data: ${response.status}`);
    }

    return JSON.parse(response.responseText);
  }

  /**
   * Build subtitle URL with format parameter
   * @param {string} baseUrl - Base subtitle URL
   * @returns {string} Complete subtitle URL
   * @private
   */
  function buildSubtitleUrl(baseUrl) {
    if (!baseUrl.includes('fmt=')) {
      return `${baseUrl}&fmt=srv1`;
    }
    return baseUrl;
  }

  /**
   * Parse caption tracks into subtitle objects
   * @param {Array} captionTracks - Caption track data
   * @returns {Array} Subtitle objects
   * @private
   */
  function parseCaptionTracks(captionTracks) {
    return captionTracks.map(track => ({
      name: track.name?.simpleText || track.languageCode,
      languageCode: track.languageCode,
      url: buildSubtitleUrl(track.baseUrl),
      isAutoGenerated: track.kind === 'asr',
    }));
  }

  /**
   * Parse translation languages into subtitle objects
   * @param {Array} translationLanguages - Translation language data
   * @param {string} baseUrl - Base URL for translations
   * @returns {Array} Auto-translation subtitle objects
   * @private
   */
  function parseTranslationLanguages(translationLanguages, baseUrl) {
    return translationLanguages.map(lang => ({
      name: lang.languageName?.simpleText || lang.languageCode,
      languageCode: lang.languageCode,
      baseUrl: baseUrl || '',
      isAutoGenerated: true,
    }));
  }

  /**
   * Create empty subtitle result
   * @param {string} videoId - Video ID
   * @param {string} videoTitle - Video title
   * @returns {Object} Empty subtitle result
   * @private
   */
  function createEmptySubtitleResult(videoId, videoTitle) {
    return {
      videoId,
      videoTitle,
      subtitles: [],
      autoTransSubtitles: [],
    };
  }

  /**
   * Get subtitles for a video
   * @param {string} videoId - Video ID
   * @returns {Promise<Object|null>} Subtitle data or null on error
   */
  async function getSubtitles(videoId) {
    try {
      const data = await fetchPlayerData(videoId);
      const videoTitle = data.videoDetails?.title || 'video';
      const captions = data.captions?.playerCaptionsTracklistRenderer;

      if (!captions) {
        return createEmptySubtitleResult(videoId, videoTitle);
      }

      const captionTracks = captions.captionTracks || [];
      const translationLanguages = captions.translationLanguages || [];
      const baseUrl = captionTracks[0]?.baseUrl || '';

      return {
        videoId,
        videoTitle,
        subtitles: parseCaptionTracks(captionTracks),
        autoTransSubtitles: parseTranslationLanguages(translationLanguages, baseUrl),
      };
    } catch (error) {
      logger.error('Error getting subtitles:', error);
      return null;
    }
  }

  /**
   * Parse subtitle XML to cues
   * @param {string} xml - XML subtitle content
   * @returns {Array} Array of cues
   */
  function parseSubtitleXML(xml) {
    const cues = [];
    const textTagRegex = /<text\s+start="([^"]+)"\s+dur="([^"]+)"[^>]*>([\s\S]*?)<\/text>/gi;
    let match;

    while ((match = textTagRegex.exec(xml)) !== null) {
      const start = parseFloat(match[1] || '0');
      const duration = parseFloat(match[2] || '0');
      let text = match[3] || '';

      // Remove CDATA
      text = text.replace(/<!\[CDATA\[(.*?)\]\]>/g, '$1');

      // Decode HTML entities
      text = decodeHTMLEntities(text.trim());

      cues.push({ start, duration, text });
    }

    return cues;
  }

  /**
   * Decode HTML entities
   * @param {string} text - Text with HTML entities
   * @returns {string} Decoded text
   */
  function decodeHTMLEntities(text) {
    const entities = {
      '&amp;': '&',
      '&lt;': '<',
      '&gt;': '>',
      '&quot;': '"',
      '&#39;': "'",
      '&apos;': "'",
      '&nbsp;': ' ',
    };

    let decoded = text;
    for (const [entity, char] of Object.entries(entities)) {
      decoded = decoded.replace(new RegExp(entity, 'g'), char);
    }

    // Decode numeric entities
    decoded = decoded.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)));
    decoded = decoded.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) =>
      String.fromCharCode(parseInt(hex, 16))
    );

    return decoded;
  }

  /**
   * Convert cues to SRT format
   * @param {Array} cues - Array of cues
   * @returns {string} SRT formatted text
   */
  function convertToSRT(cues) {
    let srt = '';
    cues.forEach((cue, index) => {
      const startTime = formatSRTTime(cue.start);
      const endTime = formatSRTTime(cue.start + cue.duration);
      const text = cue.text.replace(/\n/g, ' ').trim();

      srt += `${index + 1}\n`;
      srt += `${startTime} --> ${endTime}\n`;
      srt += `${text}\n\n`;
    });

    return srt;
  }

  /**
   * Format time for SRT (HH:MM:SS,mmm)
   * @param {number} seconds - Time in seconds
   * @returns {string} Formatted time
   */
  function formatSRTTime(seconds) {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = Math.floor(seconds % 60);
    const milliseconds = Math.floor((seconds % 1) * 1000);

    return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')},${String(milliseconds).padStart(3, '0')}`;
  }

  /**
   * Convert cues to plain text
   * @param {Array} cues - Array of cues
   * @returns {string} Plain text
   */
  function convertToTXT(cues) {
    return cues.map(cue => cue.text.trim()).join('\n');
  }

  /**
   * Download subtitle file
   * @param {Object} options - Download options
   * @param {string} options.videoId - Video ID
   * @param {string} options.url - Subtitle URL
   * @param {string} options.languageCode - Language code
   * @param {string} options.languageName - Language name
   * @param {string} [options.format='srt'] - Format: 'srt', 'txt', 'xml'
   * @param {string} [options.translateTo] - Target language code for translation
   * @returns {Promise<void>}
   */
  async function downloadSubtitle(options = {}) {
    const {
      videoId,
      url: baseUrl,
      languageCode,
      languageName,
      format = 'srt',
      translateTo = null,
    } = options;

    if (!videoId || !baseUrl) {
      throw new Error('Video ID and URL are required');
    }

    const title = getVideoTitle();

    // Build subtitle URL
    let subtitleUrl = baseUrl;
    if (!subtitleUrl.includes('fmt=')) {
      subtitleUrl += '&fmt=srv1';
    }
    if (translateTo) {
      subtitleUrl += `&tlang=${translateTo}`;
    }

    NotificationManager.show(t('subtitleDownloading'), {
      duration: 2000,
      type: 'info',
    });

    try {
      // Download XML
      const response = await gmXmlHttpRequest({
        method: 'GET',
        url: subtitleUrl,
        headers: {
          'User-Agent': DownloadConfig.HEADERS['User-Agent'],
          Referer: 'https://www.youtube.com/',
        },
      });

      if (response.status !== 200) {
        throw new Error(`Failed to download subtitle: ${response.status}`);
      }

      const xmlText = response.responseText;

      if (!xmlText || xmlText.length === 0) {
        throw new Error('Empty subtitle response');
      }

      let content;
      let extension;

      if (format === 'xml') {
        content = xmlText;
        extension = 'xml';
      } else {
        const cues = parseSubtitleXML(xmlText);

        if (cues.length === 0) {
          throw new Error('No subtitle cues found');
        }

        if (format === 'srt') {
          content = convertToSRT(cues);
          extension = 'srt';
        } else if (format === 'txt') {
          content = convertToTXT(cues);
          extension = 'txt';
        } else {
          content = xmlText;
          extension = 'xml';
        }
      }

      // Create filename
      const langSuffix = translateTo ? `${languageCode}-${translateTo}` : languageCode;
      const filename = sanitizeFilename(`${title} - ${languageName} (${langSuffix}).${extension}`);

      // Download file
      const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
      const blobUrl = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = blobUrl;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(blobUrl);

      NotificationManager.show(t('subtitleDownloaded'), {
        duration: 3000,
        type: 'success',
      });

      logger.debug('Subtitle downloaded:', filename);
    } catch (error) {
      logger.error('Error downloading subtitle:', error);
      NotificationManager.show(`${t('subtitleDownloadFailed')} ${error.message}`, {
        duration: 5000,
        type: 'error',
      });
      throw error;
    }
  }

  /**
   * Download video or audio from YouTube
   *
   * This is the main download function that uses TubeInsights API (mp3yt.is)
   * to convert and download YouTube videos/audio.
   *
   * @param {Object} options - Download options
   * @param {string} [options.format='video'] - Format: 'video' or 'audio'
   * @param {string} [options.quality='1080'] - Video quality: '144', '240', '360', '480', '720', '1080', '1440', '2160'
   * @param {string} [options.audioBitrate='320'] - Audio bitrate: '64', '128', '192', '256', '320'
   * @param {boolean} [options.embedThumbnail=true] - Embed thumbnail in audio file (requires ID3Writer)
   * @param {Function} [options.onProgress=null] - Progress callback (progress) => void
   * @returns {Promise<void>} Resolves when download completes
   *
   * @example
   * // Download 1080p video
   * await downloadVideo({ format: 'video', quality: '1080' });
   *
   * // Download 320kbps audio with album art
   * await downloadVideo({
   *   format: 'audio',
   *   audioBitrate: '320',
   *   embedThumbnail: true
   * });
   */
  async function downloadVideo(options = {}) {
    const {
      format = DownloadConfig.DEFAULTS.format,
      quality = DownloadConfig.DEFAULTS.videoQuality,
      audioBitrate = DownloadConfig.DEFAULTS.audioBitrate,
      embedThumbnail = DownloadConfig.DEFAULTS.embedThumbnail,
      onProgress = null,
    } = options;

    const videoId = getVideoId();
    if (!videoId) {
      throw new Error('Video ID not found');
    }

    const videoUrl = getVideoUrl();
    const title = getVideoTitle();

    // Show loading notification
    NotificationManager.show(t('startingDownload'), {
      duration: 2000,
      type: 'info',
    });

    try {
      // Step 1: Get API key from TubeInsights endpoint
      logger.debug('Fetching API key...');
      const keyResponse = await gmXmlHttpRequest({
        method: 'GET',
        url: DownloadConfig.API.KEY_URL,
        headers: DownloadConfig.HEADERS,
      });

      if (keyResponse.status !== 200) {
        throw new Error(`Failed to get API key: ${keyResponse.status}`);
      }

      const keyData = JSON.parse(keyResponse.responseText);
      if (!keyData || !keyData.key) {
        throw new Error('API key not found in response');
      }

      const { key } = keyData;
      logger.debug('API key obtained');

      // Step 2: Prepare conversion payload
      let payload;
      if (format === 'video') {
        // Use VP9 codec for 1440p and above, H264 for lower qualities
        const codec = parseInt(quality, 10) > 1080 ? 'vp9' : 'h264';
        payload = {
          link: videoUrl,
          format: 'mp4',
          audioBitrate: '128',
          videoQuality: quality,
          filenameStyle: 'pretty',
          vCodec: codec,
        };
      } else {
        payload = {
          link: videoUrl,
          format: 'mp3',
          audioBitrate,
          filenameStyle: 'pretty',
        };
      }

      // Step 3: Request conversion
      logger.debug('Requesting conversion...', payload);
      const customHeaders = {
        ...DownloadConfig.HEADERS,
        key,
      };

      const downloadResponse = await gmXmlHttpRequest({
        method: 'POST',
        url: DownloadConfig.API.CONVERT_URL,
        headers: customHeaders,
        data: JSON.stringify(payload),
      });

      if (downloadResponse.status !== 200) {
        throw new Error(`Conversion failed: ${downloadResponse.status}`);
      }

      const apiDownloadInfo = JSON.parse(downloadResponse.responseText);
      logger.debug('Conversion response:', apiDownloadInfo);

      if (!apiDownloadInfo.url) {
        throw new Error('No download URL received from API');
      }

      // Step 4: Download the file
      logger.debug('Downloading file from:', apiDownloadInfo.url);
      return new Promise((resolve, reject) => {
        if (typeof GM_xmlhttpRequest === 'undefined') {
          // Fallback: open in new tab
          logger.warn('GM_xmlhttpRequest not available, opening in new tab');
          window.open(apiDownloadInfo.url, '_blank');
          resolve();
          return;
        }

        GM_xmlhttpRequest({
          method: 'GET',
          url: apiDownloadInfo.url,
          responseType: 'blob',
          headers: {
            'User-Agent': DownloadConfig.HEADERS['User-Agent'],
            Referer: 'https://mp3yt.is/',
            Accept: '*/*',
          },
          onprogress: progress => {
            if (onProgress) {
              onProgress({
                loaded: progress.loaded,
                total: progress.total,
                percent: progress.total ? Math.round((progress.loaded / progress.total) * 100) : 0,
              });
            }
          },
          onload: async response => {
            if (response.status === 200 && response.response) {
              let blob = response.response;

              if (blob.size === 0) {
                reject(new Error(t('zeroBytesError')));
                return;
              }

              window.YouTubeUtils &&
                YouTubeUtils.logger &&
                YouTubeUtils.logger.debug &&
                YouTubeUtils.logger.debug(`[Download] File downloaded: ${formatBytes(blob.size)}`);

              // Embed thumbnail for audio files
              if (format === 'audio' && embedThumbnail) {
                try {
                  logger.debug('Embedding album art...');
                  const thumbnailUrl = `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`;
                  const albumArt = await createSquareAlbumArt(thumbnailUrl);
                  blob = await embedAlbumArtToMP3(blob, albumArt, { title });
                  logger.debug('Album art embedded successfully');
                } catch (error) {
                  logger.error('Failed to embed album art:', error);
                  // Continue with download even if album art embedding fails
                }
              }

              // Create download link and trigger download
              const blobUrl = URL.createObjectURL(blob);
              const a = document.createElement('a');
              a.href = blobUrl;

              const filename =
                apiDownloadInfo.filename || `${title}.${format === 'video' ? 'mp4' : 'mp3'}`;
              a.download = sanitizeFilename(filename);

              document.body.appendChild(a);
              a.click();
              document.body.removeChild(a);

              // Clean up blob URL after download
              setTimeout(() => URL.revokeObjectURL(blobUrl), 100);

              NotificationManager.show(t('downloadCompleted'), {
                duration: 3000,
                type: 'success',
              });

              logger.debug('Download completed:', filename);
              resolve();
            } else {
              reject(new Error(`Download failed: ${response.status}`));
            }
          },
          onerror: () => reject(new Error('Download failed - network error')),
          ontimeout: () => reject(new Error('Download timeout')),
        });
      });
    } catch (error) {
      logger.error('Error:', error);
      NotificationManager.show(`${t('downloadFailed')} ${error.message}`, {
        duration: 5000,
        type: 'error',
      });
      throw error;
    }
  }

  /**
   * Initialize module
   * This module doesn't create any UI, just exposes the API
   */
  // --- Modal UI for Direct Download (lightweight, self-contained) ---
  let _modalElements = null;

  function createTabButtons(onTabChange) {
    const tabContainer = document.createElement('div');
    Object.assign(tabContainer.style, {
      display: 'flex',
      gap: '8px',
      padding: '12px',
      justifyContent: 'center',
      alignItems: 'center',
      background: 'transparent',
    });

    const videoTab = document.createElement('button');
    videoTab.textContent = t('videoTab');
    videoTab.dataset.format = 'video';

    const audioTab = document.createElement('button');
    audioTab.textContent = t('audioTab');
    audioTab.dataset.format = 'audio';

    const subTab = document.createElement('button');
    subTab.textContent = t('subtitleTab');
    subTab.dataset.format = 'subtitle';

    [videoTab, audioTab, subTab].forEach(btn => {
      Object.assign(btn.style, {
        flex: 'initial',
        padding: '8px 18px',
        border: '1px solid rgba(255,255,255,0.06)',
        background: 'transparent',
        cursor: 'pointer',
        fontSize: '13px',
        fontWeight: '600',
        transition: 'all 0.18s ease',
        color: '#666',
        borderRadius: '999px',
      });
      // Accessibility & artifact prevention
      btn.type = 'button';
      btn.style.outline = 'none';
      btn.style.userSelect = 'none';
      btn.setAttribute('aria-pressed', 'false');
    });

    function setActive(btn) {
      // Reset all to inactive style
      [videoTab, audioTab, subTab].forEach(b => {
        b.style.background = 'transparent';
        b.style.color = '#666';
        b.style.border = '1px solid rgba(255,255,255,0.06)';
        b.style.boxShadow = 'none';
        b.setAttribute('aria-pressed', 'false');
      });

      // Active look: green for main, white text.
      Object.assign(btn.style, {
        background: '#10c56a',
        color: '#fff',
        border: '1px solid rgba(0,0,0,0.06)',
        boxShadow: '0 1px 0 rgba(0,0,0,0.04) inset',
      });
      btn.setAttribute('aria-pressed', 'true');

      // Notify consumer about tab change (guarded to avoid throwing during early render)
      try {
        onTabChange(btn.dataset.format);
      } catch {
        // ignore - avoids visual glitches if consumer manipulates DOM before it's fully appended
      }
    }

    // Add click handlers that also remove focus to prevent outline artifacts
    [videoTab, audioTab, subTab].forEach(btn => {
      btn.addEventListener('click', () => {
        setActive(btn);
        try {
          btn.blur();
        } catch {
          /* ignore */
        }
      });
    });

    tabContainer.appendChild(videoTab);
    tabContainer.appendChild(audioTab);
    tabContainer.appendChild(subTab);

    // Set initial active tab after buttons are appended to DOM to avoid first-render artifacts
    // setTimeout 0 yields the same-tick deferred execution without blocking
    setTimeout(() => setActive(videoTab), 0);

    return tabContainer;
  }

  function buildModalForm() {
    // Quality selection container (we will render custom pill buttons into this div)
    const qualitySelect = document.createElement('div');
    qualitySelect.role = 'radiogroup';
    // allow using .value property like the select element
    qualitySelect.value = DownloadConfig.DEFAULTS.videoQuality;
    Object.assign(qualitySelect.style, {
      display: 'flex',
      flexWrap: 'wrap',
      gap: '10px',
      padding: '12px 6px',
      borderRadius: '10px',
      width: '100%',
      alignItems: 'center',
      justifyContent: 'center',
      background: 'transparent',
    });

    const embedCheckbox = document.createElement('input');
    embedCheckbox.type = 'checkbox';
    embedCheckbox.checked = DownloadConfig.DEFAULTS.embedThumbnail;

    const embedLabel = document.createElement('label');
    embedLabel.style.fontSize = '13px';
    embedLabel.style.display = 'flex';
    embedLabel.style.alignItems = 'center';
    embedLabel.style.gap = '6px';
    embedLabel.style.color = '#fff';
    // Keep the embed thumbnail option always enabled but hidden from the UI
    embedLabel.style.display = 'none';
    embedLabel.appendChild(embedCheckbox);
    embedLabel.appendChild(document.createTextNode(t('embedThumbnail')));

    const subtitleWrapper = document.createElement('div');
    subtitleWrapper.style.display = 'none';

    const subtitleSelect = createSubtitleSelect();

    // Subtitle format buttons (SRT/TXT/XML) rendered as pill buttons
    const formatSelect = document.createElement('div');
    formatSelect.role = 'radiogroup';
    formatSelect.value = 'srt';
    Object.assign(formatSelect.style, {
      display: 'flex',
      gap: '8px',
      padding: '6px 0',
      borderRadius: '6px',
      width: '100%',
      alignItems: 'center',
      justifyContent: 'center',
      background: 'transparent',
    });
    ['srt', 'txt', 'xml'].forEach(fmt => {
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.dataset.value = fmt;
      btn.textContent = fmt.toUpperCase();
      Object.assign(btn.style, {
        padding: '6px 12px',
        borderRadius: '999px',
        border: '1px solid rgba(255,255,255,0.08)',
        background: 'rgba(255,255,255,0.02)',
        color: '#fff',
        cursor: 'pointer',
        fontSize: '13px',
        fontWeight: '600',
      });
      btn.addEventListener('click', () => {
        Array.from(formatSelect.children).forEach(c => {
          c.style.background = 'transparent';
          c.style.color = '#fff';
          c.style.border = '1px solid rgba(255,255,255,0.08)';
        });
        btn.style.background = '#111';
        btn.style.color = '#10c56a';
        btn.style.border = '1px solid rgba(16,197,106,0.15)';
        formatSelect.value = fmt;
      });
      formatSelect.appendChild(btn);
    });
    // select default
    const _defaultFmtBtn = Array.from(formatSelect.children).find(
      c => c.dataset.value === formatSelect.value
    );
    if (_defaultFmtBtn) _defaultFmtBtn.click();

    subtitleWrapper.appendChild(subtitleSelect);
    subtitleWrapper.appendChild(formatSelect);

    const cancelBtn = document.createElement('button');
    cancelBtn.textContent = t('cancel');
    Object.assign(cancelBtn.style, {
      padding: '8px 16px',
      borderRadius: '8px',
      border: '1px solid rgba(255,255,255,0.12)',
      background: 'transparent',
      cursor: 'pointer',
      fontSize: '14px',
      color: '#fff',
    });

    const downloadBtn = document.createElement('button');
    downloadBtn.textContent = t('download');
    Object.assign(downloadBtn.style, {
      padding: '8px 20px',
      borderRadius: '8px',
      border: '1px solid rgba(255,255,255,0.12)',
      background: 'transparent',
      color: '#fff',
      cursor: 'pointer',
      fontSize: '14px',
      fontWeight: '600',
    });

    const progressWrapper = document.createElement('div');
    progressWrapper.style.display = 'none';
    progressWrapper.style.marginTop = '12px';

    const progressBar = document.createElement('div');
    Object.assign(progressBar.style, {
      width: '100%',
      height: '3px',
      background: '#e0e0e0',
      borderRadius: '5px',
      overflow: 'hidden',
      marginBottom: '6px',
    });

    const progressFill = document.createElement('div');
    Object.assign(progressFill.style, {
      width: '0%',
      height: '100%',
      background: '#1a73e8',
      transition: 'width 200ms linear',
    });

    progressBar.appendChild(progressFill);

    const progressText = document.createElement('div');
    progressText.style.fontSize = '12px';
    progressText.style.color = '#666';

    progressWrapper.appendChild(progressBar);
    progressWrapper.appendChild(progressText);

    return {
      qualitySelect,
      embedLabel,
      subtitleWrapper,
      subtitleSelect,
      formatSelect,
      cancelBtn,
      downloadBtn,
      progressWrapper,
      progressFill,
      progressText,
    };
  }

  /**
   * Disable form controls during download
   * @param {Object} formParts - Form elements
   */
  function disableFormControls(formParts) {
    try {
      if (formParts.qualitySelect) formParts.qualitySelect.disabled = true;
      if (formParts.downloadBtn) {
        formParts.downloadBtn.disabled = true;
        formParts.downloadBtn.style.opacity = '0.5';
        formParts.downloadBtn.style.cursor = 'not-allowed';
      }
      if (formParts.cancelBtn) formParts.cancelBtn.disabled = true;
    } catch (e) {
      console.error('Error disabling form controls:', e);
    }
  }

  /**
   * Enable form controls after download
   * @param {Object} formParts - Form elements
   */
  function enableFormControls(formParts) {
    try {
      if (formParts.qualitySelect) formParts.qualitySelect.disabled = false;
      if (formParts.downloadBtn) formParts.downloadBtn.disabled = false;
      if (formParts.cancelBtn) formParts.cancelBtn.disabled = false;

      // Reset button styles to ensure they're clickable
      if (formParts.downloadBtn) {
        formParts.downloadBtn.style.opacity = '1';
        formParts.downloadBtn.style.cursor = 'pointer';
        formParts.downloadBtn.style.pointerEvents = 'auto';
      }
    } catch (e) {
      console.error('Error enabling form controls:', e);
    }
  }

  /**
   * Initialize progress display
   * @param {Object} formParts - Form elements
   */
  function initializeProgress(formParts) {
    formParts.progressWrapper.style.display = '';
    formParts.progressFill.style.width = '0%';
    formParts.progressText.textContent = t('starting');
  }

  /**
   * Handle subtitle download
   * @param {Object} formParts - Form elements
   * @param {Function} getSubtitlesData - Function to get subtitles data
   */
  async function handleSubtitleDownload(formParts, getSubtitlesData) {
    const subtitlesData = getSubtitlesData();
    const selectedIndex = parseInt(formParts.subtitleSelect.value, 10);
    const subtitle = subtitlesData.all[selectedIndex];
    const subtitleFormat = formParts.formatSelect.value;

    if (!subtitle) {
      throw new Error(t('noSubtitleSelected'));
    }

    const videoId = getVideoId();
    await downloadSubtitle({
      videoId,
      url: subtitle.url,
      languageCode: subtitle.languageCode,
      languageName: subtitle.name,
      format: subtitleFormat,
      translateTo: subtitle.translateTo || null,
    });
  }

  /**
   * Handle video/audio download
   * @param {Object} formParts - Form elements
   * @param {string} format - Download format
   */
  async function handleMediaDownload(formParts, format) {
    const opts = {
      format,
      quality: formParts.qualitySelect.value,
      audioBitrate: formParts.qualitySelect.value,
      embedThumbnail: format === 'audio',
      onProgress: p => {
        formParts.progressFill.style.width = `${p.percent || 0}%`;
        formParts.progressText.textContent = `${p.percent || 0}% • ${formatBytes(p.loaded || 0)} / ${p.total ? formatBytes(p.total) : '—'}`;
      },
    };

    await downloadVideo(opts);
  }

  /**
   * Complete download and close modal
   * @param {Object} formParts - Form elements
   */
  function completeDownload(formParts) {
    formParts.progressText.textContent = t('completed');
    setTimeout(() => closeModal(), 800);
  }

  /**
   * Handle download error
   * @param {Object} formParts - Form elements
   * @param {Error} err - Error object
   */
  function handleDownloadError(formParts, err) {
    const errorMsg = err?.message || 'Unknown error';
    formParts.progressText.textContent = `${t('downloadFailed')} ${errorMsg}`;
    formParts.progressText.style.color = '#ff5555';

    // Ensure controls are re-enabled even if something goes wrong
    enableFormControls(formParts);

    // Add a safety timeout to force re-enable after 500ms
    setTimeout(() => {
      try {
        enableFormControls(formParts);
      } catch (e) {
        console.error('Failed to re-enable controls:', e);
      }
    }, 500);

    // Reset progress text color after 3 seconds
    setTimeout(() => {
      formParts.progressText.style.color = '#fff';
    }, 3000);
  }

  function wireModalEvents(formParts, activeFormatGetter, getSubtitlesData) {
    formParts.cancelBtn.addEventListener('click', () => closeModal());

    formParts.downloadBtn.addEventListener('click', async () => {
      // Prevent multiple simultaneous downloads
      if (formParts.downloadBtn.disabled) return;

      disableFormControls(formParts);
      initializeProgress(formParts);

      const format = activeFormatGetter();

      try {
        if (format === 'subtitle') {
          await handleSubtitleDownload(formParts, getSubtitlesData);
        } else {
          await handleMediaDownload(formParts, format);
        }
        completeDownload(formParts);
      } catch (err) {
        console.error('[Download Error]:', err);
        handleDownloadError(formParts, err);
      } finally {
        // Extra safety: ensure controls are re-enabled
        setTimeout(() => {
          if (formParts.downloadBtn && !formParts.downloadBtn.disabled) {
            return; // Already enabled
          }
          enableFormControls(formParts);
        }, 1000);
      }
    });
  }

  /**
   * Load subtitles into the provided form parts and fill subtitlesData
   * Separated from createModalUI to reduce function length for linting.
   */
  async function loadSubtitlesForForm(formParts, subtitlesData) {
    const videoId = getVideoId();
    if (!videoId) return;

    formParts.subtitleSelect.setPlaceholder(t('loading'));
    formParts.subtitleSelect.disabled = true;

    try {
      const data = await getSubtitles(videoId);
      if (!data) {
        formParts.subtitleSelect.setPlaceholder(t('noSubtitles'));
        return;
      }

      subtitlesData.original = data.subtitles;
      subtitlesData.translated = data.autoTransSubtitles.map(autot => ({
        ...autot,
        url: data.subtitles[0]?.url || '',
        translateTo: autot.languageCode,
      }));
      subtitlesData.all = [...subtitlesData.original, ...subtitlesData.translated];

      if (subtitlesData.all.length === 0) {
        formParts.subtitleSelect.setPlaceholder(t('noSubtitles'));
        return;
      }

      const opts = subtitlesData.all.map((sub, idx) => ({
        value: idx,
        text: sub.name + (sub.translateTo ? t('autoTranslateSuffix') : ''),
      }));
      formParts.subtitleSelect.setOptions(opts);
      formParts.subtitleSelect.disabled = false;
    } catch (err) {
      logger.error('Failed to load subtitles:', err);
      formParts.subtitleSelect.setPlaceholder(t('subtitleLoadError'));
    }
  }

  /**
   * Update quality/options UI depending on active format.
   * Extracted from createModalUI to satisfy max-lines-per-function.
   */
  function updateQualityOptionsForForm(formParts, activeFormat, subtitlesData) {
    if (activeFormat === 'subtitle') {
      formParts.qualitySelect.style.display = 'none';
      formParts.embedLabel.style.display = 'none';
      formParts.subtitleWrapper.style.display = 'block';
      loadSubtitlesForForm(formParts, subtitlesData);
      return;
    }

    if (activeFormat === 'video') {
      formParts.qualitySelect.style.display = 'flex';
      formParts.embedLabel.style.display = 'none';
      formParts.subtitleWrapper.style.display = 'none';

      // Render custom pill buttons for video qualities, split low/high and show VP9 label
      formParts.qualitySelect.innerHTML = '';
      const lowQuals = DownloadConfig.VIDEO_QUALITIES.filter(q => parseInt(q, 10) <= 1080);
      const highQuals = DownloadConfig.VIDEO_QUALITIES.filter(q => parseInt(q, 10) > 1080);

      function makeQualityButton(q) {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.dataset.value = q;
        btn.textContent = `${q}p`;
        Object.assign(btn.style, {
          display: 'inline-flex',
          alignItems: 'center',
          gap: '8px',
          padding: '8px 12px',
          borderRadius: '999px',
          border: '1px solid rgba(255,255,255,0.08)',
          background: 'rgba(255,255,255,0.02)',
          color: '#fff',
          cursor: 'pointer',
          fontSize: '13px',
          fontWeight: '600',
        });

        btn.addEventListener('click', () => {
          Array.from(formParts.qualitySelect.children).forEach(c => {
            if (c.dataset && c.dataset.value) {
              c.style.background = 'transparent';
              c.style.color = '#fff';
              c.style.border = '1px solid rgba(255,255,255,0.08)';
            }
          });
          btn.style.background = '#111';
          btn.style.color = '#10c56a';
          btn.style.border = '1px solid rgba(16,197,106,0.15)';
          formParts.qualitySelect.value = q;
        });

        return btn;
      }

      lowQuals.forEach(q => formParts.qualitySelect.appendChild(makeQualityButton(q)));

      if (highQuals.length > 0) {
        const labelWrap = document.createElement('div');
        Object.assign(labelWrap.style, {
          display: 'flex',
          alignItems: 'center',
          gap: '12px',
          width: '100%',
          margin: '8px 0',
        });
        const lineLeft = document.createElement('div');
        lineLeft.style.flex = '1';
        lineLeft.style.borderTop = '1px solid rgba(255,255,255,0.06)';
        const label = document.createElement('div');
        label.textContent = t('vp9Label');
        Object.assign(label.style, {
          fontSize: '12px',
          color: 'rgba(255,255,255,0.7)',
          padding: '0 8px',
        });
        const lineRight = document.createElement('div');
        lineRight.style.flex = '1';
        lineRight.style.borderTop = '1px solid rgba(255,255,255,0.06)';
        labelWrap.appendChild(lineLeft);
        labelWrap.appendChild(label);
        labelWrap.appendChild(lineRight);
        formParts.qualitySelect.appendChild(labelWrap);

        highQuals.forEach(q => formParts.qualitySelect.appendChild(makeQualityButton(q)));
      }

      // select default
      formParts.qualitySelect.value = DownloadConfig.DEFAULTS.videoQuality;
      const defaultBtn = Array.from(formParts.qualitySelect.children).find(
        c => c.dataset && c.dataset.value === formParts.qualitySelect.value
      );
      if (defaultBtn) defaultBtn.click();

      return;
    }

    // audio
    formParts.qualitySelect.style.display = 'flex';
    formParts.embedLabel.style.display = 'flex';
    formParts.subtitleWrapper.style.display = 'none';

    // Render pill buttons for audio bitrates
    formParts.qualitySelect.innerHTML = '';
    DownloadConfig.AUDIO_BITRATES.forEach(b => {
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.dataset.value = b;
      btn.textContent = `${b} kbps`;
      Object.assign(btn.style, {
        display: 'inline-flex',
        alignItems: 'center',
        gap: '8px',
        padding: '8px 12px',
        borderRadius: '999px',
        border: '1px solid rgba(255,255,255,0.08)',
        background: 'rgba(255,255,255,0.02)',
        color: '#fff',
        cursor: 'pointer',
        fontSize: '13px',
        fontWeight: '600',
      });

      btn.addEventListener('click', () => {
        Array.from(formParts.qualitySelect.children).forEach(c => {
          c.style.background = 'transparent';
          c.style.color = '#fff';
          c.style.border = '1px solid rgba(255,255,255,0.08)';
        });
        btn.style.background = '#111';
        btn.style.color = '#10c56a';
        btn.style.border = '1px solid rgba(16,197,106,0.15)';
        formParts.qualitySelect.value = b;
      });

      formParts.qualitySelect.appendChild(btn);
    });
    formParts.qualitySelect.value = DownloadConfig.DEFAULTS.audioBitrate;
    const defaultAudioBtn = Array.from(formParts.qualitySelect.children).find(
      c => c.dataset.value === formParts.qualitySelect.value
    );
    if (defaultAudioBtn) defaultAudioBtn.click();
    // Do not show the embed thumbnail control in the UI; embedding is always enabled
    formParts.embedLabel.style.display = 'none';
  }

  function createModalUI() {
    if (_modalElements) return _modalElements;

    let activeFormat = 'video';
    const subtitlesData = { all: [], original: [], translated: [] };

    const overlay = document.createElement('div');
    Object.assign(overlay.style, {
      position: 'fixed',
      inset: '0',
      background: 'rgba(0,0,0,0.6)',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      zIndex: '999999',
    });

    const box = document.createElement('div');
    Object.assign(box.style, {
      width: '420px',
      maxWidth: '94%',
      background: 'rgba(20,20,20,0.64)',
      color: '#fff',
      borderRadius: '12px',
      boxShadow: '0 8px 40px rgba(0,0,0,0.6)',
      fontFamily: 'Arial, sans-serif',
      border: '1px solid rgba(255,255,255,0.06)',
      backdropFilter: 'blur(8px)',
    });

    const formParts = buildModalForm();

    const tabContainer = createTabButtons(format => {
      activeFormat = format;
      updateQualityOptionsForForm(formParts, activeFormat, subtitlesData);
    });

    const content = document.createElement('div');
    content.style.padding = '16px';
    content.appendChild(formParts.qualitySelect);
    content.appendChild(formParts.embedLabel);
    content.appendChild(formParts.subtitleWrapper);
    content.appendChild(formParts.progressWrapper);

    const btnRow = document.createElement('div');
    Object.assign(btnRow.style, {
      display: 'flex',
      gap: '8px',
      padding: '16px',
      justifyContent: 'center',
    });
    btnRow.appendChild(formParts.cancelBtn);
    btnRow.appendChild(formParts.downloadBtn);

    box.appendChild(tabContainer);
    box.appendChild(content);
    box.appendChild(btnRow);
    overlay.appendChild(box);

    updateQualityOptionsForForm(formParts, activeFormat, subtitlesData);
    wireModalEvents(
      formParts,
      () => activeFormat,
      () => subtitlesData
    );
    _modalElements = { overlay, box, ...formParts };
    return _modalElements;
  }

  function openModal() {
    const els = createModalUI();
    if (!els) return;
    try {
      if (!document.body.contains(els.overlay)) document.body.appendChild(els.overlay);
    } catch {
      /* ignore */
    }
  }

  function closeModal() {
    if (!_modalElements) return;
    try {
      // Clean up subtitle select listener to prevent document click leak
      const ss = _modalElements.overlay?.querySelector('[role="listbox"]');
      if (ss && typeof ss.destroy === 'function') ss.destroy();
      if (_modalElements.overlay && _modalElements.overlay.parentNode) {
        _modalElements.overlay.parentNode.removeChild(_modalElements.overlay);
      }
    } catch {
      /* ignore */
    }
    _modalElements = null;
  }

  // ============================================================================
  // DOWNLOAD BUTTON UI (merged from download-button.js)
  // ============================================================================

  /**
   * Helper to wait for download API to be available
   * @param {number} timeout - Timeout in milliseconds
   * @returns {Promise<Object|undefined>} Download API or undefined
   */
  const waitForDownloadAPI = timeout =>
    new Promise(resolve => {
      const interval = 200;
      let waited = 0;

      if (typeof window.YouTubePlusDownload !== 'undefined') {
        return resolve(window.YouTubePlusDownload);
      }

      const id = setInterval(() => {
        waited += interval;
        if (typeof window.YouTubePlusDownload !== 'undefined') {
          clearInterval(id);
          return resolve(window.YouTubePlusDownload);
        }
        if (waited >= timeout) {
          clearInterval(id);
          return resolve(undefined);
        }
      }, interval);
    });

  /**
   * Fallback clipboard copy for older browsers
   * @param {string} text - Text to copy
   * @param {Function} tFn - Translation function
   * @param {Object} notificationMgr - Notification manager
   */
  const fallbackCopyToClipboard = (text, tFn, notificationMgr) => {
    const input = document.createElement('input');
    input.value = text;
    document.body.appendChild(input);
    input.select();
    document.execCommand('copy');
    document.body.removeChild(input);
    notificationMgr.show(tFn('copiedToClipboard'), {
      duration: 2000,
      type: 'success',
    });
  };

  /**
   * Build URL from template
   * @param {string} template - URL template
   * @param {string} videoId - Video ID
   * @param {string} videoUrl - Full video URL
   * @returns {string} Built URL
   */
  const buildUrl = (template, videoId, videoUrl) =>
    (template || '')
      .replace('{videoId}', videoId || '')
      .replace('{videoUrl}', encodeURIComponent(videoUrl || ''));

  /**
   * Create download button element
   * @param {Function} tFn - Translation function
   * @returns {HTMLElement} Button element
   */
  const createButtonElement = tFn => {
    const button = document.createElement('div');
    button.className = 'ytp-button ytp-download-button';
    button.setAttribute('title', tFn('downloadOptions'));
    button.setAttribute('tabindex', '0');
    button.setAttribute('role', 'button');
    button.setAttribute('aria-haspopup', 'true');
    button.setAttribute('aria-expanded', 'false');
    button.innerHTML = `
      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" style="display:block;margin:auto;vertical-align:middle;">
        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
        <polyline points="7 10 12 15 17 10"></polyline>
        <line x1="12" y1="15" x2="12" y2="3"></line>
      </svg>
    `;
    return button;
  };

  /**
   * Position dropdown below button (batched with RAF)
   * @param {HTMLElement} button - Button element
   * @param {HTMLElement} dropdown - Dropdown element
   */
  const positionDropdown = (() => {
    let rafId = null;
    let pendingButton = null;
    let pendingDropdown = null;

    const applyPosition = () => {
      if (!pendingButton || !pendingDropdown) return;

      const rect = pendingButton.getBoundingClientRect();
      const left = Math.max(8, rect.left + rect.width / 2 - 75);
      const bottom = Math.max(8, window.innerHeight - rect.top + 12);
      pendingDropdown.style.left = `${left}px`;
      pendingDropdown.style.bottom = `${bottom}px`;

      rafId = null;
      pendingButton = null;
      pendingDropdown = null;
    };

    return (button, dropdown) => {
      pendingButton = button;
      pendingDropdown = dropdown;

      if (rafId !== null) return; // Already scheduled
      rafId = requestAnimationFrame(applyPosition);
    };
  })();

  /**
   * Download Site Actions - Handle different types of downloads
   */
  const createDownloadActions = (tFn, ytUtils) => {
    /**
     * Handle direct download
     */
    const handleDirectDownload = async () => {
      const api = await waitForDownloadAPI(2000);
      if (!api) {
        console.error('[YouTube+] Direct download module not loaded');
        ytUtils.NotificationManager.show(tFn('directDownloadModuleNotAvailable'), {
          duration: 3000,
          type: 'error',
        });
        return;
      }

      try {
        if (typeof api.openModal === 'function') {
          api.openModal();
          return;
        }
        if (typeof api.downloadVideo === 'function') {
          await api.downloadVideo({ format: 'video', quality: '1080' });
          return;
        }
      } catch (err) {
        console.error('[YouTube+] Direct download invocation failed:', err);
      }

      ytUtils.NotificationManager.show(tFn('directDownloadModuleNotAvailable'), {
        duration: 3000,
        type: 'error',
      });
    };

    /**
     * Handle YTDL download - copies URL to clipboard and opens YTDL
     * @param {string} url - YTDL URL
     */
    const handleYTDLDownload = url => {
      const videoId = new URLSearchParams(location.search).get('v');
      const videoUrl = videoId ? `https://www.youtube.com/watch?v=${videoId}` : location.href;

      // Copy to clipboard
      navigator.clipboard
        .writeText(videoUrl)
        .then(() => {
          ytUtils.NotificationManager.show(tFn('copiedToClipboard'), {
            duration: 2000,
            type: 'success',
          });
        })
        .catch(() => {
          fallbackCopyToClipboard(videoUrl, tFn, ytUtils.NotificationManager);
        });

      // Open YTDL in new tab
      window.open(url, '_blank');
    };

    /**
     * Helper to open download site or trigger direct download
     * @param {string} url - Download URL
     * @param {boolean} isYTDL - Whether this is YTDL download
     * @param {boolean} isDirect - Whether this is direct download
     * @param {HTMLElement} dropdown - Dropdown element to hide
     * @param {HTMLElement} button - Button element
     */
    const openDownloadSite = (url, isYTDL, isDirect, dropdown, button) => {
      dropdown.classList.remove('visible');
      button.setAttribute('aria-expanded', 'false');

      if (isDirect) {
        handleDirectDownload();
        return;
      }

      if (isYTDL) {
        handleYTDLDownload(url);
        return;
      }

      window.open(url, '_blank');
    };

    return { handleDirectDownload, handleYTDLDownload, openDownloadSite };
  };

  /**
   * Download Sites Configuration Builder
   * @param {Function} tFn - Translation function
   * @returns {Function} Builder function
   */
  const createDownloadSitesBuilder = tFn => {
    return (customization, enabledSites, videoId, videoUrl) => {
      const baseSites = [
        {
          key: 'externalDownloader',
          name: customization?.externalDownloader?.name || 'SSYouTube',
          url: buildUrl(
            customization?.externalDownloader?.url || `https://ssyoutube.com/watch?v={videoId}`,
            videoId,
            videoUrl
          ),
          isYTDL: false,
          isDirect: false,
        },
        {
          key: 'ytdl',
          name: 'by YTDL',
          url: `http://localhost:5005`,
          isYTDL: true,
          isDirect: false,
        },
        {
          key: 'direct',
          name: tFn('directDownload'),
          url: '#',
          isYTDL: false,
          isDirect: true,
        },
      ];

      const downloadSites = baseSites.filter(s => enabledSites[s.key] !== false);
      return { baseSites, downloadSites };
    };
  };

  /**
   * Create dropdown options element
   * @param {Array} downloadSites - Download sites configuration
   * @param {HTMLElement} button - Button element
   * @param {Function} openDownloadSiteFn - Click handler
   * @returns {HTMLElement} Dropdown element
   */
  const createDropdownOptions = (downloadSites, button, openDownloadSiteFn) => {
    const options = document.createElement('div');
    options.className = 'download-options';
    options.setAttribute('role', 'menu');

    const list = document.createElement('div');
    list.className = 'download-options-list';

    downloadSites.forEach(site => {
      const opt = document.createElement('div');
      opt.className = 'download-option-item';
      opt.textContent = site.name;
      opt.setAttribute('role', 'menuitem');
      opt.setAttribute('tabindex', '0');

      opt.dataset.url = site.url;
      opt.dataset.isYtdl = site.isYTDL ? 'true' : 'false';
      opt.dataset.isDirect = site.isDirect ? 'true' : 'false';

      list.appendChild(opt);
    });

    const handleOptionActivate = item => {
      if (!item) return;
      openDownloadSiteFn(
        item.dataset.url,
        item.dataset.isYtdl === 'true',
        item.dataset.isDirect === 'true',
        options,
        button
      );
    };

    list.addEventListener('click', e => {
      const item = e.target?.closest?.('.download-option-item');
      if (!item || !list.contains(item)) return;
      handleOptionActivate(item);
    });

    list.addEventListener('keydown', e => {
      const item = e.target?.closest?.('.download-option-item');
      if (!item || !list.contains(item)) return;
      if (e.key === 'Enter' || e.key === ' ') {
        handleOptionActivate(item);
      }
    });

    options.appendChild(list);
    return options;
  };

  /**
   * Setup dropdown hover behavior with event delegation
   * Uses WeakMap to store timers per button/dropdown pair
   */
  const setupDropdownHoverBehavior = (() => {
    let initialized = false;
    const dropdownTimers = new WeakMap();

    const getTimer = element => dropdownTimers.get(element);
    const setTimer = (element, timerId) => dropdownTimers.set(element, timerId);
    const clearTimer = element => {
      const timerId = getTimer(element);
      if (timerId !== undefined) {
        clearTimeout(timerId);
        dropdownTimers.delete(element);
      }
    };

    const showDropdown = (button, dropdown) => {
      clearTimer(button);
      clearTimer(dropdown);
      positionDropdown(button, dropdown);
      dropdown.classList.add('visible');
      button.setAttribute('aria-expanded', 'true');
    };

    const hideDropdown = (button, dropdown) => {
      clearTimer(button);
      clearTimer(dropdown);
      const timerId = setTimeout(() => {
        dropdown.classList.remove('visible');
        button.setAttribute('aria-expanded', 'false');
      }, 180);
      setTimer(button, timerId);
    };

    const initDelegation = () => {
      if (initialized) return;
      initialized = true;

      // Mouseenter/mouseleave delegation on document with capture phase
      document.addEventListener(
        'mouseenter',
        e => {
          const button = e.target?.closest?.('.ytp-download-button');
          if (button) {
            const dropdown = $('.download-options');
            if (dropdown) {
              clearTimer(button);
              clearTimer(dropdown);
              showDropdown(button, dropdown);
            }
            return;
          }

          const dropdown = e.target?.closest?.('.download-options');
          if (dropdown) {
            const button = $('.ytp-download-button');
            if (button) {
              clearTimer(button);
              clearTimer(dropdown);
              showDropdown(button, dropdown);
            }
          }
        },
        true
      );

      document.addEventListener(
        'mouseleave',
        e => {
          const button = e.target?.closest?.('.ytp-download-button');
          if (button) {
            const dropdown = $('.download-options');
            if (dropdown) {
              clearTimer(button);
              clearTimer(dropdown);
              const timerId = setTimeout(() => hideDropdown(button, dropdown), 180);
              setTimer(button, timerId);
            }
            return;
          }

          const dropdown = e.target?.closest?.('.download-options');
          if (dropdown) {
            const button = $('.ytp-download-button');
            if (button) {
              clearTimer(button);
              clearTimer(dropdown);
              const timerId = setTimeout(() => hideDropdown(button, dropdown), 180);
              setTimer(dropdown, timerId);
            }
          }
        },
        true
      );

      // Keydown delegation for Enter/Space on button
      document.addEventListener('keydown', e => {
        const button = e.target?.closest?.('.ytp-download-button');
        if (!button) return;

        if (e.key === 'Enter' || e.key === ' ') {
          const dropdown = $('.download-options');
          if (!dropdown) return;

          if (dropdown.classList.contains('visible')) {
            hideDropdown(button, dropdown);
          } else {
            showDropdown(button, dropdown);
          }
        }
      });
    };

    // Return function that just initializes delegation once
    return () => {
      initDelegation();
    };
  })();

  /**
   * Download Button Manager - Handles download button creation and dropdown management
   * @param {Object} config - Configuration object
   * @param {Object} config.settings - Settings object
   * @param {Function} config.t - Translation function
   * @param {Function} config.getElement - Get element function
   * @param {Object} config.YouTubeUtils - YouTube utilities
   * @returns {Object} Download button manager API
   */
  const createDownloadButtonManager = config => {
    const { settings, t: tFn, getElement, YouTubeUtils: ytUtils } = config;

    const actions = createDownloadActions(tFn, ytUtils);
    const buildDownloadSites = createDownloadSitesBuilder(tFn);

    /**
     * Add download button to controls
     * @param {HTMLElement} controls - Controls container
     */
    const addDownloadButton = controls => {
      if (!settings.enableDownload) return;

      try {
        const existingBtn = controls.querySelector('.ytp-download-button');
        if (existingBtn) existingBtn.remove();
      } catch {
        // ignore
      }

      const videoId = new URLSearchParams(location.search).get('v');
      const videoUrl = videoId ? `https://www.youtube.com/watch?v=${videoId}` : location.href;

      const customization = settings.downloadSiteCustomization || {
        externalDownloader: { name: 'SSYouTube', url: 'https://ssyoutube.com/watch?v={videoId}' },
      };

      const enabledSites = settings.downloadSites || {
        externalDownloader: true,
        ytdl: true,
        direct: true,
      };
      const { downloadSites } = buildDownloadSites(customization, enabledSites, videoId, videoUrl);

      const button = createButtonElement(tFn);

      if (downloadSites.length === 1) {
        const singleSite = downloadSites[0];
        button.style.cursor = 'pointer';
        const tempDropdown = document.createElement('div');
        button.addEventListener('click', () =>
          actions.openDownloadSite(
            singleSite.url,
            singleSite.isYTDL,
            singleSite.isDirect,
            tempDropdown,
            button
          )
        );
        controls.insertBefore(button, controls.firstChild);
        return;
      }

      const dropdown = createDropdownOptions(downloadSites, button, actions.openDownloadSite);

      const existingDownload = $('.download-options');
      if (existingDownload) existingDownload.remove();

      try {
        document.body.appendChild(dropdown);
      } catch {
        button.appendChild(dropdown);
      }

      setupDropdownHoverBehavior(button, dropdown);

      try {
        if (typeof window !== 'undefined') {
          window.youtubePlus = window.youtubePlus || {};
          window.youtubePlus.downloadButtonManager = window.youtubePlus.downloadButtonManager || {};

          window.youtubePlus.downloadButtonManager.addDownloadButton = controlsArg =>
            addDownloadButton(controlsArg);
          window.youtubePlus.downloadButtonManager.refreshDownloadButton = () => {
            try {
              const btn = $('.ytp-download-button');
              const dd = $('.download-options');

              // If we should show downloads but the elements are missing, attempt to recreate
              if (settings.enableDownload && (!btn || !dd)) {
                try {
                  const controlsEl = $('.ytp-right-controls');
                  if (controlsEl) {
                    // recreate button + dropdown
                    addDownloadButton(controlsEl);
                  }
                } catch {
                  /* ignore recreation errors */
                }
              }

              if (settings.enableDownload) {
                if (btn) btn.style.display = '';
                if (dd) dd.style.display = '';
              } else {
                if (btn) btn.style.display = 'none';
                if (dd) dd.style.display = 'none';
              }
            } catch {
              /* ignore */
            }
          };

          window.youtubePlus.rebuildDownloadDropdown = () => {
            try {
              const controlsEl = $('.ytp-right-controls');
              if (!controlsEl) return;
              window.youtubePlus.downloadButtonManager.addDownloadButton(controlsEl);
              window.youtubePlus.settings = window.youtubePlus.settings || settings;
            } catch (e) {
              console.warn('[YouTube+] rebuildDownloadDropdown failed:', e);
            }
          };
        }
      } catch (e) {
        console.warn('[YouTube+] expose rebuildDownloadDropdown failed:', e);
      }

      controls.insertBefore(button, controls.firstChild);
    };

    /**
     * Refresh download button visibility based on settings
     */
    const refreshDownloadButton = () => {
      const button = getElement('.ytp-download-button');
      let dropdown = $('.download-options');

      // If downloads are enabled but the dropdown/button are missing, recreate them
      if (settings.enableDownload && (!button || !dropdown)) {
        try {
          const controlsEl = $('.ytp-right-controls');
          if (controlsEl) {
            addDownloadButton(controlsEl);
            // re-query after creation
            dropdown = $('.download-options');
          }
        } catch (e) {
          logger && logger.warn && logger.warn('[YouTube+] recreate download button failed:', e);
        }
      }

      if (settings.enableDownload) {
        if (button) button.style.display = '';
        if (dropdown) dropdown.style.display = '';
      } else {
        if (button) button.style.display = 'none';
        if (dropdown) dropdown.style.display = 'none';
      }
    };

    return {
      addDownloadButton,
      refreshDownloadButton,
    };
  };

  // ============================================================================
  // MODULE INITIALIZATION
  // ============================================================================

  let initialized = false;

  function init() {
    if (initialized) return;
    initialized = true;
    try {
      window.YouTubeUtils &&
        YouTubeUtils.logger &&
        YouTubeUtils.logger.debug &&
        YouTubeUtils.logger.debug('[YouTube+ Download] Unified module loaded');
      window.YouTubeUtils &&
        YouTubeUtils.logger &&
        YouTubeUtils.logger.debug &&
        YouTubeUtils.logger.debug(
          '[YouTube+ Download] Use window.YouTubePlusDownload.downloadVideo() to download'
        );
      window.YouTubeUtils &&
        YouTubeUtils.logger &&
        YouTubeUtils.logger.debug &&
        YouTubeUtils.logger.debug('[YouTube+ Download] Button manager available');
    } catch {}
  }

  // Export public API
  if (typeof window !== 'undefined') {
    window.YouTubePlusDownload = {
      downloadVideo,

      // Subtitle functions
      getSubtitles,
      downloadSubtitle,

      // Utility functions
      getVideoId,
      getVideoUrl,
      getVideoTitle,
      sanitizeFilename,
      formatBytes,

      // Configuration
      DownloadConfig,

      // UI: open modal for user selection
      openModal,

      // Initialize (called automatically)
      init,
    };

    // Export button manager for basic.js
    window.YouTubePlusDownloadButton = { createDownloadButtonManager };
  }

  // Export module to global scope for module loader
  if (typeof window !== 'undefined') {
    window.YouTubeDownload = {
      init,
      openModal,
      getVideoId,
      getVideoTitle,
      version: '3.0',
    };
  }

  const ensureInit = () => {
    if (!isRelevantRoute()) return;
    if (typeof requestIdleCallback === 'function') {
      requestIdleCallback(init, { timeout: 1500 });
    } else {
      setTimeout(init, 0);
    }
  };

  onDomReady(ensureInit);

  if (window.YouTubeUtils?.cleanupManager?.registerListener) {
    YouTubeUtils.cleanupManager.registerListener(document, 'yt-navigate-finish', ensureInit, {
      passive: true,
    });
  } else {
    document.addEventListener('yt-navigate-finish', ensureInit, { passive: true });
  }
})();

// --- MODULE: enhanced.js ---

// Shared DOM helpers - defined at file scope for use across all IIFEs and functions
const _getDOMCache = () => typeof window !== 'undefined' && window.YouTubeDOMCache;

/**
 * Query single element with optional caching
 * @param {string} sel - CSS selector
 * @param {Element|Document} [ctx] - Context element
 * @returns {Element|null}
 */
const $ = (sel, ctx) =>
  _getDOMCache()?.querySelector(sel, ctx) || (ctx || document).querySelector(sel);

/**
 * Query all elements with optional caching
 * @param {string} sel - CSS selector
 * @param {Element|Document} [ctx] - Context element
 * @returns {Element[]}
 */
const $$ = (sel, ctx) =>
  _getDOMCache()?.querySelectorAll(sel, ctx) || Array.from((ctx || document).querySelectorAll(sel));

/**
 * Get element by ID with optional caching
 * @param {string} id - Element ID
 * @returns {Element|null}
 */
const byId = id => _getDOMCache()?.getElementById(id) || document.getElementById(id);

// $, $$, byId are defined above and used throughout

const onDomReady = (() => {
  let ready = document.readyState !== 'loading';
  const queue = [];

  const run = () => {
    ready = true;
    while (queue.length) {
      const cb = queue.shift();
      try {
        cb();
      } catch (e) {
        console.warn('[YouTube+] DOMReady callback error:', e);
      }
    }
  };

  if (!ready) {
    document.addEventListener('DOMContentLoaded', run, { once: true });
  }

  return cb => {
    if (ready) {
      cb();
    } else {
      queue.push(cb);
    }
  };
})();

// Enhanced Tabviews
(function () {
  'use strict';
  // Use centralized i18n from YouTubePlusI18n or YouTubeUtils
  const _getLanguage = () => {
    if (window.YouTubePlusI18n?.getLanguage) return window.YouTubePlusI18n.getLanguage();
    if (window.YouTubeUtils?.getLanguage) return window.YouTubeUtils.getLanguage();
    const htmlLang = document.documentElement.lang || 'en';
    return htmlLang.startsWith('ru') ? 'ru' : 'en';
  };

  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    // Fallback for initialization phase
    if (!key) return '';
    let result = String(key);
    for (const [k, v] of Object.entries(params || {})) {
      result = result.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
    }
    return result;
  };
  // No local alias needed here; modules may use global YouTubeUtils.getLanguage when required

  /**
   * Configuration object for scroll-to-top button
   * @type {Object}
   * @property {boolean} enabled - Whether the feature is enabled
   * @property {string} storageKey - LocalStorage key for settings
   */
  const config = {
    enabled: (() => {
      try {
        const settings = localStorage.getItem('youtube_plus_settings');
        if (settings) {
          const parsed = JSON.parse(settings);
          return parsed.enableScrollToTopButton !== false;
        }
      } catch (e) {
        console.warn('[YouTube+] Config read error:', e);
      }
      return true;
    })(),
    storageKey: 'youtube_top_button_settings',
  };

  let universalScrollHandler = null;
  let universalScrollContainer = null;

  const getUniversalScrollContainer = () => {
    try {
      const host = window.location.hostname;
      const candidates = [];
      if (host === 'music.youtube.com') {
        // YouTube Music uses custom layout elements – try multiple containers
        // The main scrollable area on YouTube Music is typically #layout or the app-layout itself
        const appLayout = document.querySelector('ytmusic-app-layout');
        if (appLayout) {
          // Check the direct scroll container inside app-layout
          const layoutContent = appLayout.querySelector('#layout');
          if (layoutContent) candidates.push(layoutContent);
          // Also try the app-layout itself (sometimes it's the scroll host)
          candidates.push(appLayout);
        }
        candidates.push(
          document.querySelector('ytmusic-browse-response #contents'),
          document.querySelector('ytmusic-section-list-renderer'),
          document.querySelector('ytmusic-tabbed-page #content'),
          document.querySelector('ytmusic-app-layout #content'),
          document.querySelector('#content'),
          document.querySelector('ytmusic-app')
        );
      } else if (host === 'studio.youtube.com') {
        // YouTube Studio uses different layout containers
        candidates.push(
          $('ytcp-entity-page #scrollable-content'),
          $('ytcp-app #content'),
          $('#main-content'),
          $('#content'),
          $('#main'),
          $('ytcp-app')
        );
      }
      candidates.push(document.scrollingElement, document.documentElement, document.body);

      for (const el of candidates) {
        if (!el) continue;
        if (el.scrollHeight > el.clientHeight + 50) return el;
      }
      // Fallback: if no scrollable container found yet, return window-level
      // for music/studio since they may use window scroll
      if (host === 'music.youtube.com' || host === 'studio.youtube.com') {
        return document.scrollingElement || document.documentElement;
      }
    } catch {}
    return document.scrollingElement || document.documentElement;
  };

  let universalWindowScrollHandler = null;

  const removeUniversalButton = () => {
    try {
      const btn = byId('universal-top-button');
      if (btn) btn.remove();
    } catch {}
    try {
      if (universalScrollHandler && universalScrollContainer) {
        universalScrollContainer.removeEventListener('scroll', universalScrollHandler);
      }
    } catch {}
    try {
      if (universalWindowScrollHandler) {
        window.removeEventListener('scroll', universalWindowScrollHandler);
      }
    } catch {}
    universalScrollHandler = null;
    universalScrollContainer = null;
    universalWindowScrollHandler = null;
  };

  let musicSideScrollHandler = null;
  let musicSideScrollContainer = null;

  const getMusicSidePanelContainer = () => {
    if (window.location.hostname !== 'music.youtube.com') return null;

    // Direct selectors for the queue/side panel content
    const directSelectors = [
      'ytmusic-player-queue #contents',
      'ytmusic-player-queue',
      '#side-panel #contents',
      '#side-panel',
      'ytmusic-tab-renderer[page-type="MUSIC_PAGE_TYPE_QUEUE"] #contents',
      'ytmusic-queue #automix-contents',
      'ytmusic-queue #contents',
    ];

    for (const sel of directSelectors) {
      try {
        const el = document.querySelector(sel);
        if (el && el.scrollHeight > el.clientHeight + 30) return el;
      } catch {}
    }

    // Try within specific roots
    const roots = [
      document.querySelector('ytmusic-player-page'),
      document.querySelector('ytmusic-app-layout'),
      document.querySelector('ytmusic-app'),
    ];
    const selectors = [
      '#side-panel',
      '#right-content',
      'ytmusic-player-queue',
      'ytmusic-queue',
      'ytmusic-tab-renderer[selected] #contents',
      '.side-panel',
    ];

    for (const root of roots) {
      if (!root) continue;
      for (const sel of selectors) {
        try {
          const el = root.querySelector(sel);
          if (el && el.scrollHeight > el.clientHeight + 30) return el;
        } catch {}
      }
    }
    return null;
  };

  const removeMusicSideButton = () => {
    try {
      const btn = byId('music-side-top-button');
      if (btn) btn.remove();
    } catch {}
    try {
      if (musicSideScrollHandler && musicSideScrollContainer) {
        musicSideScrollContainer.removeEventListener('scroll', musicSideScrollHandler);
      }
    } catch {}
    musicSideScrollHandler = null;
    musicSideScrollContainer = null;
  };

  const cleanupTopButtons = () => {
    try {
      const rightButton = byId('right-tabs-top-button');
      if (rightButton) rightButton.remove();
    } catch {}
    try {
      const playlistButton = byId('playlist-panel-top-button');
      if (playlistButton) playlistButton.remove();
    } catch {}

    removeMusicSideButton();

    removeUniversalButton();

    try {
      $$('#right-tabs .tab-content-cld').forEach(tab => {
        if (tab && tab._topButtonScrollHandler) {
          tab.removeEventListener('scroll', tab._topButtonScrollHandler);
          tab._topButtonScrollHandler = null;
        }
      });
    } catch {}

    try {
      // #right-tabs itself may be the scroll host on single-column layout
      const rightTabsEl = document.getElementById('right-tabs');
      if (rightTabsEl) {
        if (rightTabsEl._topButtonScrollHandler) {
          rightTabsEl.removeEventListener('scroll', rightTabsEl._topButtonScrollHandler);
          rightTabsEl._topButtonScrollHandler = null;
        }
        if (rightTabsEl._scrollCleanup) {
          rightTabsEl._scrollCleanup();
          rightTabsEl._scrollCleanup = null;
        }
      }
    } catch {}

    try {
      const playlistScroll = $('ytd-playlist-panel-renderer #items');
      if (playlistScroll && playlistScroll._topButtonScrollHandler) {
        playlistScroll.removeEventListener('scroll', playlistScroll._topButtonScrollHandler);
        playlistScroll._topButtonScrollHandler = null;
      }
    } catch {}
  };

  let tabChangesObserver = null;
  let watchInitToken = 0;
  let isTabClickListenerAttached = false;
  let tabDelegationHandler = null;
  let tabDelegationRegistered = false;
  let tabCheckTimeoutId = null;
  let playlistPanelCheckTimeoutId = null;

  const isWatchPage = () => window.location.pathname === '/watch';
  const isShortsPage = () => window.location.pathname.startsWith('/shorts');
  const shouldInitReturnDislike = () => isWatchPage() || isShortsPage();

  const isTopButton = el =>
    el &&
    (el.id === 'right-tabs-top-button' ||
      el.id === 'universal-top-button' ||
      el.id === 'playlist-panel-top-button' ||
      el.id === 'music-side-top-button');

  const handleTopButtonActivate = button => {
    try {
      if (!button) return;

      if (button.id === 'right-tabs-top-button') {
        // Always use direct DOM query here — class-based selectors may be stale in cache
        const activeTab = document.querySelector(
          '#right-tabs .tab-content-cld:not(.tab-content-hidden)'
        );
        const rightTabsEl = document.getElementById('right-tabs');
        // On single-column layout #right-tabs is the actual scroll host (overflow:auto),
        // so prefer scrolling it when it already has a positive scrollTop.
        const scrollTarget =
          rightTabsEl && rightTabsEl.scrollTop > 0
            ? rightTabsEl
            : activeTab && activeTab.scrollTop > 0
              ? activeTab
              : activeTab || rightTabsEl;
        if (scrollTarget) {
          if ('scrollBehavior' in document.documentElement.style) {
            scrollTarget.scrollTo({ top: 0, behavior: 'smooth' });
          } else {
            scrollTarget.scrollTop = 0;
          }
          button.setAttribute('aria-label', t('scrolledToTop') || 'Scrolled to top');
          setTimeout(() => {
            button.setAttribute('aria-label', t('scrollToTop'));
          }, 1000);
        }
        return;
      }

      if (button.id === 'universal-top-button') {
        // Always re-detect container on Music/Studio since SPA navigation changes it
        const host = window.location.hostname;
        const isMusic = host === 'music.youtube.com';
        const isStudio = host === 'studio.youtube.com';
        const target =
          isMusic || isStudio
            ? getUniversalScrollContainer()
            : universalScrollContainer || getUniversalScrollContainer();

        // Try multiple scroll strategies for YouTube Music
        const scrollToTop = el => {
          if ('scrollBehavior' in document.documentElement.style) {
            el.scrollTo({ top: 0, behavior: 'smooth' });
          } else {
            el.scrollTop = 0;
          }
        };

        if (
          target === window ||
          target === document ||
          target === document.body ||
          target === document.documentElement
        ) {
          window.scrollTo({ top: 0, behavior: 'smooth' });
        } else if (target && typeof target.scrollTo === 'function') {
          scrollToTop(target);
        }

        // For YouTube Music: also scroll window and common inner containers
        if (isMusic) {
          window.scrollTo({ top: 0, behavior: 'smooth' });
          // Scroll all potentially scrollable music containers
          const musicContainers = [
            document.querySelector('ytmusic-app-layout #layout'),
            document.querySelector('ytmusic-app-layout'),
            document.querySelector('ytmusic-browse-response #contents'),
            document.querySelector('ytmusic-section-list-renderer'),
          ];
          for (const c of musicContainers) {
            if (c && c.scrollTop > 0) {
              scrollToTop(c);
            }
          }
        }
        return;
      }

      if (button.id === 'playlist-panel-top-button') {
        const playlistPanel = $('ytd-playlist-panel-renderer');
        const scrollContainer = playlistPanel ? $('#items', playlistPanel) : null;
        if (scrollContainer) {
          if ('scrollBehavior' in document.documentElement.style) {
            scrollContainer.scrollTo({ top: 0, behavior: 'smooth' });
          } else {
            scrollContainer.scrollTop = 0;
          }
        }
        return;
      }

      if (button.id === 'music-side-top-button') {
        // Always re-detect since panel content changes with navigation
        const target = getMusicSidePanelContainer() || musicSideScrollContainer;
        if (target) {
          if ('scrollBehavior' in document.documentElement.style) {
            target.scrollTo({ top: 0, behavior: 'smooth' });
          } else {
            target.scrollTop = 0;
          }
        }
      }
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error scrolling to top:', error);
    }
  };

  const setupTopButtonDelegation = (() => {
    let attached = false;
    return () => {
      if (attached) return;
      attached = true;

      const delegator = window.YouTubePlusEventDelegation;
      if (delegator?.on) {
        delegator.on(document, 'click', '.top-button', (ev, target) => {
          if (isTopButton(target)) handleTopButtonActivate(target);
        });
        delegator.on(document, 'keydown', '.top-button', (ev, target) => {
          if (!isTopButton(target)) return;
          if (ev.key === 'Enter' || ev.key === ' ') {
            ev.preventDefault();
            handleTopButtonActivate(target);
          }
        });
      } else {
        document.addEventListener(
          'click',
          ev => {
            const target = ev.target?.closest?.('.top-button');
            if (isTopButton(target)) handleTopButtonActivate(target);
          },
          true
        );
        document.addEventListener(
          'keydown',
          ev => {
            const target = ev.target?.closest?.('.top-button');
            if (!isTopButton(target)) return;
            if (ev.key === 'Enter' || ev.key === ' ') {
              ev.preventDefault();
              handleTopButtonActivate(target);
            }
          },
          true
        );
      }
    };
  })();

  const clearTimeoutSafe = id => {
    if (id) clearTimeout(id);
    return null;
  };

  /**
   * Adds CSS styles for scroll-to-top button and scrollbars
   * @returns {void}
   */
  const addStyles = () => {
    if (byId('custom-styles')) return;

    const style = document.createElement('style');
    style.id = 'custom-styles';
    style.textContent = `
      :root{--scrollbar-width:8px;--scrollbar-track:transparent;--scrollbar-thumb:rgba(144,144,144,.5);--scrollbar-thumb-hover:rgba(170,170,170,.7);--scrollbar-thumb-active:rgba(190,190,190,.9);}
      ::-webkit-scrollbar{width:var(--scrollbar-width)!important;height:var(--scrollbar-width)!important;}
      ::-webkit-scrollbar-track{background:var(--scrollbar-track)!important;border-radius:4px!important;}
      ::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb)!important;border-radius:4px!important;transition:background .2s!important;}
      ::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)!important;}
      ::-webkit-scrollbar-thumb:active{background:var(--scrollbar-thumb-active)!important;}
      ::-webkit-scrollbar-corner{background:transparent!important;}
      html,body,#content,#guide-content,#secondary,#comments,#chat,ytd-comments,ytd-watch-flexy,ytd-browse,ytd-search,ytd-playlist-panel-renderer,#right-tabs,.tab-content-cld,ytmusic-app-layout{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track);}
      html[dark]{--scrollbar-thumb:rgba(144,144,144,.4);--scrollbar-thumb-hover:rgba(170,170,170,.6);--scrollbar-thumb-active:rgba(190,190,190,.8);}
      .top-button{position:fixed;bottom:16px;right:16px;width:40px;height:40px;background:var(--yt-top-btn-bg,rgba(0,0,0,.7));color:var(--yt-top-btn-color,#fff);border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2100;opacity:0;visibility:hidden;transition:all .3s cubic-bezier(0.4, 0, 0.2, 1);backdrop-filter:blur(12px) saturate(180%);-webkit-backdrop-filter:blur(12px) saturate(180%);border:1px solid var(--yt-top-btn-border,rgba(255,255,255,.1));background:rgba(255,255,255,.12);box-shadow:0 8px 32px 0 rgba(31,38,135,.18);}
      .top-button:hover{background:var(--yt-top-btn-hover,rgba(0,0,0,.15));transform:translateY(-2px) scale(1.07);box-shadow:0 8px 32px rgba(0,0,0,.25);}
      .top-button:active{transform:translateY(-1px) scale(1.03);}
      .top-button:focus{outline:2px solid rgba(255,255,255,0.5);outline-offset:2px;}
      .top-button.visible{opacity:1;visibility:visible;}
      .top-button svg{transition:transform .2s ease;}
      .top-button:hover svg{transform:translateY(-1px) scale(1.1);}
      html[dark]{--yt-top-btn-bg:rgba(255,255,255,.10);--yt-top-btn-color:#fff;--yt-top-btn-border:rgba(255,255,255,.18);--yt-top-btn-hover:rgba(255,255,255,.18);}
      html:not([dark]){--yt-top-btn-bg:rgba(255,255,255,.12);--yt-top-btn-color:#222;--yt-top-btn-border:rgba(0,0,0,.08);--yt-top-btn-hover:rgba(255,255,255,.18);}
      #right-tabs .top-button{position:absolute;z-index:1000;}
      ytd-watch-flexy:not([tyt-tab^="#"]) #right-tabs .top-button{display:none;}
      ytd-playlist-panel-renderer .top-button{position:absolute;z-index:1000;}
      ytd-watch-flexy[flexy] #movie_player, ytd-watch-flexy[flexy] #movie_player .html5-video-container, ytd-watch-flexy[flexy] .html5-main-video{width:100%!important; max-width:100%!important;}
      ytd-watch-flexy[flexy] .html5-main-video{height:auto!important; max-height:100%!important; object-fit:contain!important; transform:none!important;}
      ytd-watch-flexy[flexy] #player-container-outer, ytd-watch-flexy[flexy] #movie_player{display:flex!important; align-items:center!important; justify-content:center!important;}
      /* Return YouTube Dislike button styling */
      dislike-button-view-model button{min-width:fit-content!important;width:auto!important;}
      dislike-button-view-model .yt-spec-button-shape-next__button-text-content{display:inline-flex!important;align-items:center!important;justify-content:center!important;}
      #ytp-plus-dislike-text{display:inline-block!important;visibility:visible!important;opacity:1!important;margin-left:6px!important;font-size:1.4rem!important;line-height:2rem!important;font-weight:500!important;}
      ytd-segmented-like-dislike-button-renderer dislike-button-view-model button{min-width:fit-content!important;}
      ytd-segmented-like-dislike-button-renderer .yt-spec-button-shape-next__button-text-content{min-width:2.4rem!important;}
      /* Shorts-specific dislike button styling */
      ytd-reel-video-renderer dislike-button-view-model #ytp-plus-dislike-text{font-size:1.2rem!important;line-height:1.8rem!important;margin-left:4px!important;}
      ytd-reel-video-renderer dislike-button-view-model button{padding:8px 12px!important;min-width:auto!important;}
      ytd-shorts dislike-button-view-model .yt-spec-button-shape-next__button-text-content{display:inline-flex!important;min-width:auto!important;}
        `;
    (document.head || document.documentElement).appendChild(style);
  };

  /**
   * Updates button visibility based on scroll position
   * @param {HTMLElement} scrollContainer - The container being scrolled
   * @param {HTMLElement} button - The button element
   * @returns {void}
   */
  const handleScroll = (scrollContainer, button) => {
    try {
      if (!button || !scrollContainer) return;
      button.classList.toggle('visible', scrollContainer.scrollTop > 100);
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error in handleScroll:', error);
    }
  };

  /**
   * Sets up scroll event listener on active tab with debouncing for performance
   * Uses IntersectionObserver when possible for better performance
   * @returns {void}
   */
  const setupScrollListener = (() => {
    let timeout;
    return () => {
      if (timeout) clearTimeout(timeout);
      timeout = setTimeout(() => {
        try {
          // Clean up old listeners first
          $$('.tab-content-cld').forEach(tab => {
            if (tab._topButtonScrollHandler) {
              tab.removeEventListener('scroll', tab._topButtonScrollHandler);
              delete tab._topButtonScrollHandler;
            }

            // Clean up IntersectionObserver if exists
            if (tab._scrollObserver) {
              tab._scrollObserver.disconnect();
              delete tab._scrollObserver;
            }

            // Use ScrollManager if available
            window.YouTubePlusScrollManager?.removeAllListeners?.(tab);
          });

          // Also remove any direct #right-tabs scroll handler from a previous run
          try {
            const prevRtEl = document.getElementById('right-tabs');
            if (prevRtEl) {
              if (prevRtEl._topButtonScrollHandler) {
                prevRtEl.removeEventListener('scroll', prevRtEl._topButtonScrollHandler);
                delete prevRtEl._topButtonScrollHandler;
              }
              if (prevRtEl._scrollCleanup) {
                prevRtEl._scrollCleanup();
                delete prevRtEl._scrollCleanup;
              }
            }
          } catch {}

          // Always use direct DOM query — class-based ':not(.tab-content-hidden)' selectors
          // can return a stale cached element (the previously-active tab, which is still in
          // the DOM but now hidden). A direct query guarantees the correct live result.
          const activeTab = document.querySelector(
            '#right-tabs .tab-content-cld:not(.tab-content-hidden)'
          );
          const button = byId('right-tabs-top-button');

          if (activeTab && button) {
            // On single-column layouts, #right-tabs itself has overflow:auto and acts as
            // the scroll host. In that case the individual tab <div> never gets scrollTop>0.
            // Detect which element is actually scrollable and attach the listener there.
            const rightTabsEl = document.getElementById('right-tabs');
            const rtIsScrollHost =
              rightTabsEl &&
              rightTabsEl !== activeTab &&
              rightTabsEl.scrollHeight > rightTabsEl.clientHeight + 10;
            const scrollTarget = rtIsScrollHost ? rightTabsEl : activeTab;

            // Use ScrollManager if available for better performance
            if (window.YouTubePlusScrollManager) {
              const cleanup = window.YouTubePlusScrollManager.addScrollListener(
                scrollTarget,
                () => handleScroll(scrollTarget, button),
                { debounce: 100, runInitial: true }
              );
              scrollTarget._scrollCleanup = cleanup;
            } else {
              // Fallback to manual debouncing
              const debounceFunc =
                typeof YouTubeUtils !== 'undefined' && YouTubeUtils.debounce
                  ? YouTubeUtils.debounce
                  : (fn, delay) => {
                      let timeoutId;
                      return (...args) => {
                        clearTimeout(timeoutId);
                        timeoutId = setTimeout(() => fn(...args), delay);
                      };
                    };
              const scrollHandler = debounceFunc(() => handleScroll(scrollTarget, button), 100);
              scrollTarget._topButtonScrollHandler = scrollHandler;
              scrollTarget.addEventListener('scroll', scrollHandler, {
                passive: true,
                capture: false,
              });
              handleScroll(scrollTarget, button);
            }
          }
        } catch (error) {
          console.error('[YouTube+][Enhanced] Error in setupScrollListener:', error);
        }
      }, 100);
    };
  })();

  /**
   * Creates and appends scroll-to-top button with error handling
   * @returns {void}
   */
  const createButton = () => {
    try {
      setupTopButtonDelegation();
      const rightTabs = $('#right-tabs');
      if (!rightTabs || byId('right-tabs-top-button')) return;
      if (!config.enabled) return;

      const button = document.createElement('button');
      button.id = 'right-tabs-top-button';
      button.className = 'top-button';
      button.title = t('scrollToTop');
      button.setAttribute('aria-label', t('scrollToTop'));
      button.innerHTML =
        '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>';

      rightTabs.style.position = 'relative';
      rightTabs.appendChild(button);
      setupScrollListener();
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error creating button:', error);
    }
  };

  /**
   * Creates universal scroll-to-top button for pages
   * @returns {void}
   */
  const createUniversalButton = () => {
    try {
      setupTopButtonDelegation();
      if (byId('universal-top-button')) return;
      if (!config.enabled) return;

      const rawContainer = getUniversalScrollContainer();
      const scrollContainer =
        rawContainer === document.scrollingElement ||
        rawContainer === document.documentElement ||
        rawContainer === document.body
          ? window
          : rawContainer;
      universalScrollContainer = scrollContainer;

      const button = document.createElement('button');
      button.id = 'universal-top-button';
      button.className = 'top-button';
      button.title = t('scrollToTop');
      button.setAttribute('aria-label', t('scrollToTop'));
      button.innerHTML =
        '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>';

      // Ensure the button is above YouTube Music/Studio overlays
      const host = window.location.hostname;
      if (host === 'music.youtube.com' || host === 'studio.youtube.com') {
        button.style.zIndex = '10000';
      }

      document.body.appendChild(button);

      // Setup scroll listener for the active container
      const debounceFunc =
        typeof YouTubeUtils !== 'undefined' && YouTubeUtils.debounce
          ? YouTubeUtils.debounce
          : (fn, delay) => {
              let timeoutId;
              return (...args) => {
                clearTimeout(timeoutId);
                timeoutId = setTimeout(() => fn(...args), delay);
              };
            };

      const scrollHandler = debounceFunc(() => {
        const offset = scrollContainer === window ? window.scrollY : scrollContainer.scrollTop;
        button.classList.toggle('visible', offset > 100);
      }, 100);

      universalScrollHandler = scrollHandler;
      scrollContainer.addEventListener('scroll', scrollHandler, { passive: true });

      const initialOffset = scrollContainer === window ? window.scrollY : scrollContainer.scrollTop;
      button.classList.toggle('visible', initialOffset > 100);

      // For YouTube Music/Studio: listen on multiple scroll targets
      // since the actual scrollable container may differ per page
      if (host === 'music.youtube.com' || host === 'studio.youtube.com') {
        // Cache music containers to avoid repeated DOM queries on every scroll event
        let _musicContainersCache = null;
        let _musicCacheTime = 0;
        const getMusicContainers = () => {
          const now = Date.now();
          if (_musicContainersCache && now - _musicCacheTime < 5000) return _musicContainersCache;
          _musicContainersCache = [
            document.querySelector('ytmusic-app-layout #layout'),
            document.querySelector('ytmusic-app-layout'),
            document.querySelector('ytmusic-browse-response #contents'),
            document.querySelector('ytmusic-section-list-renderer'),
            scrollContainer !== window ? scrollContainer : null,
          ].filter(Boolean);
          _musicCacheTime = now;
          return _musicContainersCache;
        };

        const musicScrollCheck = debounceFunc(() => {
          let anyScrolled = window.scrollY > 100;
          if (!anyScrolled) {
            for (const c of getMusicContainers()) {
              if (c.scrollTop > 100) {
                anyScrolled = true;
                break;
              }
            }
          }
          button.classList.toggle('visible', anyScrolled);
        }, 100);

        // Listen on window + key music containers
        window.addEventListener('scroll', musicScrollCheck, { passive: true });
        universalWindowScrollHandler = musicScrollCheck;

        // Also attach to known music containers as they become available
        const attachMusicScrollListeners = () => {
          const targets = [
            document.querySelector('ytmusic-app-layout #layout'),
            document.querySelector('ytmusic-app-layout'),
          ];
          for (const target of targets) {
            if (target && !target._ytpScrollAttached) {
              target._ytpScrollAttached = true;
              target.addEventListener('scroll', musicScrollCheck, { passive: true });
            }
          }
        };
        attachMusicScrollListeners();
        // Re-attach after navigation
        setTimeout(attachMusicScrollListeners, 1000);
        setTimeout(attachMusicScrollListeners, 3000);
      }
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error creating universal button:', error);
    }
  };

  /**
   * Creates scroll-to-top button for playlist panel
   * @returns {void}
   */
  const createPlaylistPanelButton = () => {
    try {
      setupTopButtonDelegation();
      const playlistPanel = $('ytd-playlist-panel-renderer');
      if (!playlistPanel || byId('playlist-panel-top-button')) return;
      if (!config.enabled) return;

      const button = document.createElement('button');
      button.id = 'playlist-panel-top-button';
      button.className = 'top-button';
      button.title = t('scrollToTop');
      button.setAttribute('aria-label', t('scrollToTop'));
      button.innerHTML =
        '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>';

      const scrollContainer = $('#items', playlistPanel);
      if (!scrollContainer) return;

      // Ensure the playlist panel is positioned so absolute children are anchored inside it
      playlistPanel.style.position = playlistPanel.style.position || 'relative';

      // Force the button to be positioned inside the playlist panel (override global fixed)
      button.style.position = 'absolute';
      button.style.bottom = '16px';
      button.style.right = '16px';
      button.style.zIndex = '1000';

      playlistPanel.appendChild(button);

      // Setup scroll listener
      const debounceFunc =
        typeof YouTubeUtils !== 'undefined' && YouTubeUtils.debounce
          ? YouTubeUtils.debounce
          : (fn, delay) => {
              let timeoutId;
              return (...args) => {
                clearTimeout(timeoutId);
                timeoutId = setTimeout(() => fn(...args), delay);
              };
            };

      const scrollHandler = debounceFunc(() => handleScroll(scrollContainer, button), 100);
      scrollContainer._topButtonScrollHandler = scrollHandler;
      scrollContainer.addEventListener('scroll', scrollHandler, { passive: true });
      handleScroll(scrollContainer, button);

      // Hide the button when the playlist panel is collapsed/hidden.
      // Use ResizeObserver + MutationObserver to detect layout/attribute changes.
      const updateVisibility = () => {
        try {
          // If panel not connected or explicitly hidden, hide the button
          if (!playlistPanel.isConnected || playlistPanel.hidden) {
            button.style.display = 'none';
            return;
          }

          // Use offsetParent check (cheaper than getComputedStyle) - null means hidden
          if (playlistPanel.offsetParent === null && playlistPanel.style.position !== 'fixed') {
            button.style.display = 'none';
            return;
          }

          // If bounding box is too small (collapsed), hide button
          const { width, height } = playlistPanel.getBoundingClientRect();
          if (width < 40 || height < 40) {
            button.style.display = 'none';
            return;
          }

          // If items container cannot scroll or has no height, hide button
          if (
            !scrollContainer ||
            scrollContainer.offsetHeight === 0 ||
            scrollContainer.scrollHeight === 0
          ) {
            button.style.display = 'none';
            return;
          }

          // Otherwise keep normal display and let handleScroll control visibility class
          button.style.display = '';
        } catch {
          // On error, prefer hiding to avoid stray UI
          try {
            button.style.display = 'none';
          } catch {}
        }
      };

      // Observe size changes
      let ro = null;
      try {
        if (typeof ResizeObserver !== 'undefined') {
          ro = new ResizeObserver(updateVisibility);
          ro.observe(playlistPanel);
          if (scrollContainer) ro.observe(scrollContainer);
        }
      } catch {
        ro = null;
      }

      // Observe attribute/class changes
      const mo = new MutationObserver(updateVisibility);
      try {
        mo.observe(playlistPanel, {
          attributes: true,
          attributeFilter: ['class', 'style', 'hidden'],
        });
      } catch {}

      // Initial visibility pass
      updateVisibility();

      // Register cleanup with YouTubeUtils.cleanupManager when available
      try {
        if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
          YouTubeUtils.cleanupManager.register(() => {
            try {
              if (ro) ro.disconnect();
            } catch {}
            try {
              mo.disconnect();
            } catch {}
          });
        }
      } catch {}
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error creating playlist panel button:', error);
    }
  };

  /**
   * Creates scroll-to-top button for YouTube Music side panel
   * @returns {void}
   */
  const createMusicSidePanelButton = () => {
    try {
      if (window.location.hostname !== 'music.youtube.com') return;
      setupTopButtonDelegation();
      if (byId('music-side-top-button')) return;
      if (!config.enabled) return;

      const panel = getMusicSidePanelContainer();
      if (!panel) {
        // Retry after a delay since YouTube Music loads content dynamically
        setTimeout(() => {
          if (!byId('music-side-top-button') && config.enabled) {
            const retryPanel = getMusicSidePanelContainer();
            if (retryPanel) createMusicSidePanelButton();
          }
        }, 2000);
        return;
      }

      const button = document.createElement('button');
      button.id = 'music-side-top-button';
      button.className = 'top-button';
      button.title = t('scrollToTop');
      button.setAttribute('aria-label', t('scrollToTop'));
      button.innerHTML =
        '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>';

      panel.style.position = panel.style.position || 'relative';
      button.style.position = 'absolute';
      button.style.bottom = '16px';
      button.style.right = '16px';
      button.style.zIndex = '1000';

      panel.appendChild(button);

      const debounceFunc =
        typeof YouTubeUtils !== 'undefined' && YouTubeUtils.debounce
          ? YouTubeUtils.debounce
          : (fn, delay) => {
              let timeoutId;
              return (...args) => {
                clearTimeout(timeoutId);
                timeoutId = setTimeout(() => fn(...args), delay);
              };
            };

      const scrollHandler = debounceFunc(() => {
        button.classList.toggle('visible', panel.scrollTop > 100);
      }, 100);

      musicSideScrollContainer = panel;
      musicSideScrollHandler = scrollHandler;
      panel.addEventListener('scroll', scrollHandler, { passive: true });
      button.classList.toggle('visible', panel.scrollTop > 100);
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error creating music side button:', error);
    }
  };

  // --- Return YouTube Dislike integration ---
  const RETURN_DISLIKE_API = 'https://returnyoutubedislikeapi.com/votes';
  const DISLIKE_CACHE_TTL = 10 * 60 * 1000; // 10 minutes
  const dislikeCache = new Map(); // videoId -> { value, expiresAt }
  let dislikeObserver = null;
  let dislikePollTimer = null;

  const formatCompactNumber = number => {
    try {
      return new Intl.NumberFormat(_getLanguage() || 'en', {
        notation: 'compact',
        compactDisplay: 'short',
      }).format(Number(number) || 0);
    } catch {
      return String(number || 0);
    }
  };

  const DISLIKE_CACHE_MAX_SIZE = 50;
  const fetchDislikes = async videoId => {
    if (!videoId) return 0;
    const cached = dislikeCache.get(videoId);
    if (cached && Date.now() < cached.expiresAt) return cached.value;

    // Evict expired entries if cache grows too large
    if (dislikeCache.size > DISLIKE_CACHE_MAX_SIZE) {
      const now = Date.now();
      for (const [key, entry] of dislikeCache) {
        if (now >= entry.expiresAt) dislikeCache.delete(key);
      }
      // If still too large, remove oldest entries
      if (dislikeCache.size > DISLIKE_CACHE_MAX_SIZE) {
        const iter = dislikeCache.keys();
        while (dislikeCache.size > DISLIKE_CACHE_MAX_SIZE / 2) {
          const next = iter.next();
          if (next.done) break;
          dislikeCache.delete(next.value);
        }
      }
    }

    // Try GM_xmlhttpRequest first (userscript env). Fallback to fetch with timeout.
    try {
      if (typeof GM_xmlhttpRequest !== 'undefined') {
        const text = await new Promise((resolve, reject) => {
          const timeoutId = setTimeout(() => reject(new Error('timeout')), 8000);
          GM_xmlhttpRequest({
            method: 'GET',
            url: `${RETURN_DISLIKE_API}?videoId=${encodeURIComponent(videoId)}`,
            timeout: 8000,
            headers: { Accept: 'application/json' },
            onload: r => {
              clearTimeout(timeoutId);
              if (r.status >= 200 && r.status < 300) resolve(r.responseText);
              else reject(new Error(`HTTP ${r.status}`));
            },
            onerror: e => {
              clearTimeout(timeoutId);
              reject(e || new Error('network'));
            },
            ontimeout: () => {
              clearTimeout(timeoutId);
              reject(new Error('timeout'));
            },
          });
        });
        const parsed = JSON.parse(text || '{}');
        const val = Number(parsed.dislikes || 0) || 0;
        dislikeCache.set(videoId, { value: val, expiresAt: Date.now() + DISLIKE_CACHE_TTL });
        return val;
      }

      // fallback to fetch
      const controller = new AbortController();
      const id = setTimeout(() => controller.abort(), 8000);
      try {
        const resp = await fetch(`${RETURN_DISLIKE_API}?videoId=${encodeURIComponent(videoId)}`, {
          method: 'GET',
          cache: 'no-cache',
          signal: controller.signal,
          headers: { Accept: 'application/json' },
        });
        clearTimeout(id);
        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
        const json = await resp.json();
        const val = Number(json.dislikes || 0) || 0;
        dislikeCache.set(videoId, { value: val, expiresAt: Date.now() + DISLIKE_CACHE_TTL });
        return val;
      } finally {
        clearTimeout(id);
      }
    } catch {
      // on any error, return 0 but don't throw
      return 0;
    }
  };

  const getVideoIdForDislike = () => {
    try {
      const urlObj = new URL(window.location.href);
      const pathname = urlObj.pathname || '';
      if (pathname.startsWith('/shorts/')) return pathname.slice(8);
      if (pathname.startsWith('/clip/')) {
        const meta = $("meta[itemprop='videoId'], meta[itemprop='identifier']");
        return meta?.getAttribute('content') || null;
      }
      return urlObj.searchParams.get('v');
    } catch {
      return null;
    }
  };

  const getButtonsContainer = () => {
    return (
      $('ytd-menu-renderer.ytd-watch-metadata > div#top-level-buttons-computed') ||
      $('ytd-menu-renderer.ytd-video-primary-info-renderer > div') ||
      $('#menu-container #top-level-buttons-computed') ||
      null
    );
  };

  /**
   * Get dislike button for Shorts page
   * @returns {HTMLElement|null} Dislike button element
   */
  const getDislikeButtonShorts = () => {
    // Try to find the active reel first
    const activeReel = $('ytd-reel-video-renderer[is-active]');
    if (activeReel) {
      const btn =
        $('dislike-button-view-model', activeReel) ||
        $('like-button-view-model', activeReel)
          ?.parentElement?.querySelector('[aria-label*="islike"]')
          ?.closest('button')?.parentElement ||
        $('#dislike-button', activeReel);
      if (btn) return btn;
    }

    // Fallback: find in the shorts player container
    const shortsContainer = $('ytd-shorts');
    if (shortsContainer) {
      const btn =
        $('dislike-button-view-model', shortsContainer) || $('#dislike-button', shortsContainer);
      if (btn) return btn;
    }

    // Last resort: global search
    return $('dislike-button-view-model') || $('#dislike-button') || null;
  };

  /**
   * Get dislike button from buttons container
   * @param {HTMLElement} buttons - Buttons container
   * @returns {HTMLElement|null} Dislike button element
   */
  const getDislikeButtonFromContainer = buttons => {
    if (!buttons) return null;

    // Check for segmented like/dislike button (newer YouTube layout)
    const segmented = buttons.querySelector('ytd-segmented-like-dislike-button-renderer');
    if (segmented) {
      const dislikeViewModel =
        segmented.querySelector('dislike-button-view-model') ||
        segmented.querySelector('#segmented-dislike-button') ||
        segmented.children[1];
      if (dislikeViewModel) return dislikeViewModel;
    }

    // Check for standalone dislike view-model button
    const viewModel = buttons.querySelector('dislike-button-view-model');
    if (viewModel) return viewModel;

    // Fallback: try to find by button label or position
    const dislikeBtn =
      buttons.querySelector('button[aria-label*="islike"]') ||
      buttons.querySelector('button[aria-label*="Не нравится"]');
    if (dislikeBtn) {
      return dislikeBtn.closest('dislike-button-view-model') || dislikeBtn.parentElement;
    }

    // Last resort: second child in container
    return buttons.children && buttons.children[1] ? buttons.children[1] : null;
  };

  const getDislikeButton = () => {
    // Handle Shorts variants and main page segmented buttons
    const isShorts = window.location.pathname.startsWith('/shorts');
    if (isShorts) {
      return getDislikeButtonShorts();
    }

    const buttons = getButtonsContainer();
    return getDislikeButtonFromContainer(buttons);
  };

  const getOrCreateDislikeText = dislikeButton => {
    if (!dislikeButton) return null;

    // Check if our custom text already exists (prevent duplicates)
    const existingCustom = dislikeButton.querySelector('#ytp-plus-dislike-text');
    if (existingCustom) return existingCustom;

    // Try to find existing text container in various YouTube button structures
    const textSpan =
      dislikeButton.querySelector('span.yt-core-attributed-string:not(#ytp-plus-dislike-text)') ||
      dislikeButton.querySelector('#text') ||
      dislikeButton.querySelector('yt-formatted-string') ||
      dislikeButton.querySelector('span[role="text"]:not(#ytp-plus-dislike-text)') ||
      dislikeButton.querySelector('.yt-spec-button-shape-next__button-text-content');

    // If native text exists, use it directly to avoid duplication
    if (textSpan && textSpan.id !== 'ytp-plus-dislike-text') {
      textSpan.id = 'ytp-plus-dislike-text';
      return textSpan;
    }

    // For view-model buttons, find the proper container
    const viewModelHost = dislikeButton.closest('ytDislikeButtonViewModelHost') || dislikeButton;
    const buttonShape =
      viewModelHost.querySelector('button-view-model button') ||
      viewModelHost.querySelector('button[aria-label]') ||
      dislikeButton.querySelector('button') ||
      dislikeButton;

    // Check if text container already exists
    let textContainer = buttonShape.querySelector(
      '.yt-spec-button-shape-next__button-text-content'
    );

    // Create a dedicated span with proper styling to match like button
    // Use min-width to prevent CLS when count loads
    const created = document.createElement('span');
    created.id = 'ytp-plus-dislike-text';
    created.setAttribute('role', 'text');
    created.className = 'yt-core-attributed-string yt-core-attributed-string--white-space-no-wrap';
    const isShorts = window.location.pathname.startsWith('/shorts');
    // Added min-width to reserve space and prevent CLS
    created.style.cssText = isShorts
      ? 'margin-left: 4px; font-size: 1.2rem; line-height: 1.8rem; font-weight: 500; min-width: 1.5em; display: inline-block; text-align: center;'
      : 'margin-left: 6px; font-size: 1.4rem; line-height: 2rem; font-weight: 500; min-width: 2em; display: inline-block; text-align: center;';

    try {
      if (!textContainer) {
        // Create text container if it doesn't exist (matching like button structure)
        textContainer = document.createElement('div');
        textContainer.className = 'yt-spec-button-shape-next__button-text-content';
        textContainer.appendChild(created);
        buttonShape.appendChild(textContainer);
      } else {
        textContainer.appendChild(created);
      }

      // Ensure button has proper width
      buttonShape.style.minWidth = 'auto';
      buttonShape.style.width = 'auto';
      if (viewModelHost !== dislikeButton) {
        viewModelHost.style.minWidth = 'auto';
      }
    } catch (e) {
      console.warn('YTP: Failed to create dislike text:', e);
    }
    return created;
  };

  const setDislikeDisplay = (dislikeButton, count) => {
    try {
      const container = getOrCreateDislikeText(dislikeButton);
      if (!container) return;

      const formatted = formatCompactNumber(count);
      if (container.innerText !== String(formatted)) {
        container.innerText = String(formatted);

        // Ensure the text is visible and properly styled
        container.style.display = 'inline-block';
        container.style.visibility = 'visible';
        container.style.opacity = '1';

        // Make sure parent button container is wide enough
        const buttonShape = container.closest('button') || dislikeButton.querySelector('button');
        if (buttonShape) {
          buttonShape.style.minWidth = 'fit-content';
          buttonShape.style.width = 'auto';
        }
      }
    } catch (e) {
      console.warn('YTP: Failed to set dislike display:', e);
    }
  };

  const setupDislikeObserver = dislikeButton => {
    if (!dislikeButton) return;
    if (dislikeObserver) {
      dislikeObserver.disconnect();
      dislikeObserver = null;
    }

    // Don't observe if we already have text displayed
    const existingText = dislikeButton.querySelector('#ytp-plus-dislike-text');
    if (existingText?.textContent && existingText.textContent !== '0') {
      return;
    }

    dislikeObserver = new MutationObserver(() => {
      // on any mutation, update displayed cached value
      const vid = getVideoIdForDislike();
      const cached = dislikeCache.get(vid);
      if (cached) {
        const btn = getDislikeButton();
        if (btn) setDislikeDisplay(btn, cached.value);
      }
    });
    try {
      dislikeObserver.observe(dislikeButton, { childList: true, subtree: true, attributes: true });
    } catch {}
  };

  const initReturnDislike = async () => {
    try {
      // avoid multiple polls
      if (dislikePollTimer) return;

      // Use MutationObserver instead of setInterval for better performance
      const checkButton = async () => {
        const btn = getDislikeButton();
        if (btn) {
          if (dislikePollTimer) {
            dislikePollTimer.disconnect();
            dislikePollTimer = null;
          }
          const vid = getVideoIdForDislike();
          const val = await fetchDislikes(vid);
          setDislikeDisplay(btn, val);
          setupDislikeObserver(btn);
          return true;
        }
        return false;
      };

      // Check immediately
      if (await checkButton()) return;

      // Set up observer for button appearance - use targeted childList only (no subtree)
      const isShorts = window.location.pathname.startsWith('/shorts');
      const maxTime = 10000; // 10 seconds timeout
      const startTime = Date.now();

      dislikePollTimer = new MutationObserver(async () => {
        if (Date.now() - startTime > maxTime) {
          dislikePollTimer.disconnect();
          dislikePollTimer = null;
          return;
        }
        await checkButton();
      });

      // Observe more targeted containers to reduce mutation callbacks
      const targetEl = isShorts ? $('#shorts-container') : $('ytd-watch-flexy #below');
      if (targetEl) {
        dislikePollTimer.observe(targetEl, { childList: true, subtree: true });
      } else {
        // Fallback: use a short interval instead of expensive body observer
        const pollId = setInterval(async () => {
          if (Date.now() - startTime > maxTime) {
            clearInterval(pollId);
            return;
          }
          if (await checkButton()) clearInterval(pollId);
        }, 500);
        // Register so the global cleanup manager can stop it during navigation teardown
        window.YouTubeUtils?.cleanupManager?.registerInterval?.(pollId);
      }
    } catch {
      // ignore
    }
  };

  const cleanupReturnDislike = () => {
    try {
      if (dislikePollTimer) {
        if (typeof dislikePollTimer.disconnect === 'function') {
          dislikePollTimer.disconnect();
        } else if (typeof dislikePollTimer === 'number') {
          clearInterval(dislikePollTimer);
        }
        dislikePollTimer = null;
      }
      if (dislikeObserver) {
        dislikeObserver.disconnect();
        dislikeObserver = null;
      }
      // Remove all created dislike text spans
      $$('#ytp-plus-dislike-text').forEach(el => {
        try {
          if (el.parentNode) el.parentNode.removeChild(el);
        } catch {}
      });
      // Clear cache to free memory
      dislikeCache.clear();
    } catch (e) {
      console.warn('YTP: Dislike cleanup error:', e);
    }
  };

  /**
   * Observes DOM changes to detect tab switches
   * @returns {MutationObserver|null} The created observer or null on error
   */
  const observeTabChanges = () => {
    try {
      const observer = new MutationObserver(mutations => {
        try {
          if (
            mutations.some(
              m =>
                m.type === 'attributes' &&
                m.attributeName === 'class' &&
                m.target instanceof Element &&
                m.target.classList.contains('tab-content-cld')
            )
          ) {
            setTimeout(setupScrollListener, 100);
          }
        } catch (error) {
          console.error('[YouTube+][Enhanced] Error in mutation observer:', error);
        }
      });

      const rightTabs = $('#right-tabs');
      if (rightTabs) {
        observer.observe(rightTabs, {
          attributes: true,
          subtree: true,
          attributeFilter: ['class'],
        });
        return observer;
      }
      return null;
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error in observeTabChanges:', error);
      return null;
    }
  };

  /**
   * Check if current page needs universal button
   * @returns {boolean}
   */
  const needsUniversalButton = () => {
    const host = window.location.hostname;
    // Always show on Music and Studio
    if (host === 'music.youtube.com' || host === 'studio.youtube.com') return true;

    if (isWatchPage() || isShortsPage()) return false;

    const path = window.location.pathname;
    const { search } = window.location;

    // Search results page
    if (path === '/results' && search.includes('search_query=')) return true;

    // Playlist page
    if (path === '/playlist' && search.includes('list=')) return true;

    // Home/Feed pages
    if (path === '/' || path === '/feed/subscriptions') return true;

    return true;
  };

  /**
   * Handles click events on tab buttons
   * @param {Event} e - Click event
   * @returns {void}
   */
  const handleTabButtonClick = e => {
    try {
      const { target } = /** @type {{ target: HTMLElement }} */ (e);
      const tabButton = target?.closest?.('.tab-btn[tyt-tab-content]');
      if (tabButton) {
        setTimeout(setupScrollListener, 100);
      }
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error in click handler:', error);
    }
  };

  /**
   * Sets up event listeners for tab button clicks
   * @returns {void}
   */
  const setupEvents = () => {
    try {
      if (isTabClickListenerAttached) return;
      const delegator = window.YouTubePlusEventDelegation;
      if (delegator?.on) {
        tabDelegationHandler = (ev, target) => {
          void ev;
          if (!target) return;
          setTimeout(setupScrollListener, 100);
        };
        delegator.on(document, 'click', '.tab-btn[tyt-tab-content]', tabDelegationHandler, {
          capture: true,
        });
        tabDelegationRegistered = true;
      } else {
        document.addEventListener('click', handleTabButtonClick, true);
      }
      isTabClickListenerAttached = true;
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error in setupEvents:', error);
    }
  };

  const cleanupEvents = () => {
    try {
      if (!isTabClickListenerAttached) return;
      const delegator = window.YouTubePlusEventDelegation;
      if (tabDelegationRegistered && delegator?.off && tabDelegationHandler) {
        delegator.off(document, 'click', '.tab-btn[tyt-tab-content]', tabDelegationHandler);
      } else {
        document.removeEventListener('click', handleTabButtonClick, true);
      }
      tabDelegationHandler = null;
      tabDelegationRegistered = false;
      isTabClickListenerAttached = false;
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error cleaning up events:', error);
    }
  };

  const stopWatchEnhancements = () => {
    watchInitToken++;
    tabCheckTimeoutId = clearTimeoutSafe(tabCheckTimeoutId);
    playlistPanelCheckTimeoutId = clearTimeoutSafe(playlistPanelCheckTimeoutId);

    try {
      tabChangesObserver?.disconnect?.();
    } catch {}
    tabChangesObserver = null;

    cleanupEvents();

    try {
      cleanupReturnDislike();
    } catch {}
  };

  const startWatchEnhancements = () => {
    if (!config.enabled) return;
    if (!isWatchPage()) return;

    const token = ++watchInitToken;
    setupEvents();

    const maxTabAttempts = 40;
    const checkForTabs = (attempt = 0) => {
      if (token !== watchInitToken) return;
      if (!isWatchPage()) return;

      if ($('#right-tabs')) {
        createButton();
        try {
          tabChangesObserver?.disconnect?.();
        } catch {}
        tabChangesObserver = observeTabChanges();
        return;
      }

      if (attempt >= maxTabAttempts) return;
      tabCheckTimeoutId = setTimeout(() => checkForTabs(attempt + 1), 250);
    };

    const maxPlaylistPanelAttempts = 30;
    const checkForPlaylistPanel = (attempt = 0) => {
      if (token !== watchInitToken) return;
      if (!isWatchPage()) return;

      try {
        const playlistPanel = $('ytd-playlist-panel-renderer');
        if (playlistPanel && !byId('playlist-panel-top-button')) {
          createPlaylistPanelButton();
          return;
        }
      } catch (error) {
        console.error('[YouTube+][Enhanced] Error checking for playlist panel:', error);
      }

      if (attempt >= maxPlaylistPanelAttempts) return;
      playlistPanelCheckTimeoutId = setTimeout(() => checkForPlaylistPanel(attempt + 1), 300);
    };

    checkForTabs();
    checkForPlaylistPanel();
  };

  /**
   * Initialize scroll-to-top button module
   * @returns {void}
   */
  const init = () => {
    try {
      addStyles();

      const checkPageType = () => {
        try {
          if (needsUniversalButton() && !byId('universal-top-button')) {
            createUniversalButton();
          }
          if (window.location.hostname === 'music.youtube.com' && !byId('music-side-top-button')) {
            createMusicSidePanelButton();
          }
        } catch (error) {
          console.error('[YouTube+][Enhanced] Error checking page type:', error);
        }
      };

      const onNavigate = () => {
        stopWatchEnhancements();
        checkPageType();

        if (shouldInitReturnDislike()) {
          try {
            initReturnDislike();
          } catch (e) {
            console.warn('[YouTube+] initReturnDislike error:', e);
          }
        }

        // Watch-specific UI only initializes on /watch
        startWatchEnhancements();
      };

      // Initial run
      onNavigate();

      // Listen for navigation changes (YouTube is SPA)
      if (window.YouTubeUtils?.cleanupManager?.registerListener) {
        YouTubeUtils.cleanupManager.registerListener(
          document,
          'yt-navigate-finish',
          () => setTimeout(onNavigate, 200),
          { passive: true }
        );
      } else {
        window.addEventListener('yt-navigate-finish', () => {
          setTimeout(onNavigate, 200);
        });
      }

      // For YouTube Music: also listen on popstate and observe #side-panel appearance
      if (window.location.hostname === 'music.youtube.com') {
        window.addEventListener('popstate', () => setTimeout(onNavigate, 200));
        // Observe DOM for side-panel becoming scrollable
        const sidePanelObserver = new MutationObserver(() => {
          if (!byId('music-side-top-button') && config.enabled) {
            createMusicSidePanelButton();
          }
        });
        const observeTarget = $('ytmusic-app-layout') || $('ytmusic-app') || document.body;
        if (observeTarget) {
          sidePanelObserver.observe(observeTarget, {
            childList: true,
            subtree: true,
          });
        }
      }
    } catch (error) {
      console.error('[YouTube+][Enhanced] Error in initialization:', error);
    }
  };

  const scheduleInit = () => {
    if (typeof requestIdleCallback === 'function') {
      requestIdleCallback(init, { timeout: 4000 });
    } else {
      setTimeout(init, 0);
    }
  };

  window.addEventListener('youtube-plus-settings-updated', e => {
    try {
      const nextEnabled = e?.detail?.enableScrollToTopButton !== false;
      if (nextEnabled === config.enabled) return;
      config.enabled = nextEnabled;
      if (!config.enabled) {
        cleanupTopButtons();
        stopWatchEnhancements();
        return;
      }
      addStyles();
      if (needsUniversalButton() && !byId('universal-top-button')) {
        createUniversalButton();
      }
      if (window.location.hostname === 'music.youtube.com' && !byId('music-side-top-button')) {
        createMusicSidePanelButton();
      }
      startWatchEnhancements();
    } catch {}
  });

  onDomReady(scheduleInit);
})();

// Styles
(function () {
  try {
    const host = typeof location === 'undefined' ? '' : location.hostname;
    if (!host) return;
    if (!/(^|\.)youtube\.com$/.test(host) && !/\.youtube\.google/.test(host)) return;

    const SETTINGS_KEY = 'youtube_plus_settings';
    const STYLE_ELEMENT_ID = 'ytp-zen-features-style';
    const NON_CRITICAL_STYLE_ID = 'ytp-zen-features-style-noncritical';
    const STYLE_MANAGER_KEY = 'zen-features-style';
    let nonCriticalTimer = null;

    const DEFAULTS = {
      enableZenStyles: true,
      // legacy (kept for backward compat)
      hideSideGuide: false,
      zenStyles: {
        thumbnailHover: true,
        immersiveSearch: true,
        hideVoiceSearch: true,
        transparentHeader: true,
        hideSideGuide: true,
        cleanSideGuide: false,
        fixFeedLayout: true,
        betterCaptions: true,
        playerBlur: true,
        theaterEnhancements: true,
        misc: true,
      },
    };

    const loadSettings = () => {
      /** @type {any} */
      let parsed = null;
      try {
        const raw = localStorage.getItem(SETTINGS_KEY);
        if (raw) parsed = JSON.parse(raw);
      } catch (e) {
        console.warn('[YouTube+] Zen settings parse error:', e);
      }

      const merged = {
        ...DEFAULTS,
        ...(parsed && typeof parsed === 'object' ? parsed : null),
      };

      merged.zenStyles = {
        ...DEFAULTS.zenStyles,
        ...(merged.zenStyles && typeof merged.zenStyles === 'object' ? merged.zenStyles : null),
      };

      // Backward compat: if legacy hideSideGuide is set, also enable the style flag.
      if (merged.hideSideGuide === true && merged.zenStyles.hideSideGuide !== true) {
        merged.zenStyles.hideSideGuide = true;
      }

      return merged;
    };

    const CSS_BLOCKS = {
      thumbnailHover: `
        /* yt-thumbnail hover */
        #inline-preview-player {transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) 1s !important; transform: scale(1) !important;}
        #video-preview-container:has(#inline-preview-player) {transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; border-radius: 1.2em !important; overflow: hidden !important; transform: scale(1) !important;}
        #video-preview-container:has(#inline-preview-player):hover {transform: scale(1.25) !important; box-shadow: #0008 0px 0px 60px !important; transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s !important;}
        ytd-app #content {opacity: 1 !important; transition: opacity 0.3s ease-in-out !important;}
        ytd-app:has(#video-preview-container:hover) #content {opacity: 0.5 !important; transition: opacity 4s ease-in-out 1s !important;}
      `,
      immersiveSearch: `
        /* yt-Immersive search */
        #page-manager, yt-searchbox {transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.35) !important;}
        #masthead yt-searchbox button[aria-label="Search"] {display: none !important;}
        .ytSearchboxComponentInputBox {border-radius: 2em !important;}
        yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) {position: relative !important; left: 0vw !important; top: -30vh !important; height: 40px !important; max-width: 600px !important; transform: scale(1) !important;}
        @media only screen and (min-width: 1400px) {yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) { height: 60px !important; max-width: 700px !important; transform: scale(1.1) !important;}}
        yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) .ytSearchboxComponentInputBox,
        yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {background-color: #fffb !important; box-shadow: black 0 0 30px !important;}
        @media (prefers-color-scheme: dark) {
          yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) .ytSearchboxComponentInputBox,
          yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {background-color: #000b !important;}
        }
        yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {margin-top: 10px !important;}
        @media only screen and (min-width: 1400px) {yt-searchbox:has(.ytSearchboxComponentInputBoxHasFocus) #i0 {margin-top: 30px !important;}}
        .ytd-masthead #center:has(.ytSearchboxComponentInputBoxHasFocus) {height: 100vh !important; width: 100vw !important; left: 0 !important; top: 0 !important; position: fixed !important; justify-content: center !important; align-items: center !important;}
        #content:has(.ytSearchboxComponentInputBoxHasFocus) #page-manager {filter: blur(20px) !important; transform: scale(1.05) !important;}
      `,
      hideVoiceSearch: `
        /* No voice search button */
        #voice-search-button {display: none !important;}
      `,
      transparentHeader: `
        /* Transparent header */
        #masthead-container, #background.ytd-masthead { background-color: transparent !important; }
      `,
      hideSideGuide: `
        /* Hide side guide */
        ytd-mini-guide-renderer, [theater=""] #contentContainer::after {display: none !important;}
        tp-yt-app-drawer > #contentContainer:not([opened=""]),
        #contentContainer:not([opened=""]) #guide-content,
        ytd-mini-guide-renderer,
        ytd-mini-guide-entry-renderer {background-color: var(--yt-spec-text-primary-inverse) !important; background: var(--yt-spec-text-primary-inverse) !important;}
        #content:not(:has(#contentContainer[opened=""])) #page-manager {margin-left: 0 !important;}
        ytd-app:not([guide-persistent-and-visible=""]) tp-yt-app-drawer > #contentContainer {background-color: var(--yt-spec-text-primary-inverse) !important;}
        ytd-alert-with-button-renderer {align-items: center !important; justify-content: center !important;}
      `,
      cleanSideGuide: `
        /* Clean side guide */
        ytd-guide-section-renderer:has([title="YouTube Premium"]),
        ytd-guide-renderer #footer {display: none !important;}
        ytd-guide-section-renderer, ytd-guide-collapsible-section-entry-renderer {border: none !important;}
      `,
      fixFeedLayout: `
        /* Fix new feed layout */
        ytd-rich-item-renderer[rendered-from-rich-grid] {  @media only screen and (min-width: 1400px) { --ytd-rich-grid-items-per-row: 4 !important; @media only screen and (min-width: 1700px) { --ytd-rich-grid-items-per-row: 5 !important; @media only screen and (min-width: 2180px) {--ytd-rich-grid-items-per-row: 6 !important;}}}} ytd-rich-item-renderer[is-in-first-column="\"] { margin-left: calc(var(--ytd-rich-grid-item-margin) / 2) !important;}#contents { padding-left: calc(var(--ytd-rich-grid-item-margin) / 2 + var(--ytd-rich-grid-gutter-margin)) !important;}
      `,
      betterCaptions: `
        /* Better captions */
        .caption-window { backdrop-filter: blur(10px) brightness(70%) !important; border-radius: 1em !important; padding: 1em !important; box-shadow: #0008 0 0 20px !important; width: fit-content !important; }
        .ytp-caption-segment { background: none !important; }
      `,
      playerBlur: `
        /* Player controls blur */
        .ytp-left-controls .ytp-play-button,
        .ytp-left-controls .ytp-volume-area,
        .ytp-left-controls .ytp-time-display.notranslate > span,
        .ytp-left-controls .ytp-chapter-container > button,
        .ytp-left-controls .ytp-prev-button,
        .ytp-left-controls .ytp-next-button,
        .ytp-right-controls,
        .ytp-time-wrapper,
        .ytPlayerQuickActionButtonsHost,
        .ytPlayerQuickActionButtonsHostCompactControls,
        .ytPlayerQuickActionButtonsHostDisableBackdropFilter { backdrop-filter: blur(5px) !important; background-color: #0004 !important; }
        .ytp-popup { backdrop-filter: blur(10px) !important; background-color: #0007 !important; }
      `,
      theaterEnhancements: `
        /* Zen view comments (from zeninternet) */
        /* Hide secondary column visually but break containment so fixed children can escape */
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #columns #secondary { display: block !important;width: 0 !important;min-width: 0 !important;max-width: 0 !important;padding: 0 !important;margin: 0 !important;border: 0 !important;overflow: visible !important;pointer-events: none !important;flex: 0 0 0px !important;contain: none !important;
        }
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #secondary-inner { overflow: visible !important;contain: none !important;position: static !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #secondary-inner secondary-wrapper,
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #secondary-inner .tabview-secondary-wrapper { contain: none !important;overflow: visible !important;position: static !important;max-height: none !important;height: auto !important;padding: 0 !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #right-tabs { display: block !important;overflow: visible !important;contain: none !important;position: static !important;width: 0 !important;height: 0 !important;padding: 0 !important;margin: 0 !important;border: 0 !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #right-tabs > header { display: none !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #right-tabs .tab-content { display: block !important;overflow: visible !important;contain: none !important;position: static !important;width: 0 !important;height: 0 !important;padding: 0 !important;margin: 0 !important;border: 0 !important;}
        /* Break containment on tab-comments so its fixed-position child can escape */
        /* Extra .tab-content-hidden selector to beat main.js specificity (line 5169) */
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments,
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-hidden,
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-cld { contain: none !important;overflow: visible !important;position: static !important;display: block !important;visibility: visible !important;width: 0 !important;height: 0 !important;padding: 0 !important;margin: 0 !important;z-index: auto !important;pointer-events: none !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-hidden ytd-comments#comments > ytd-item-section-renderer#sections,
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-hidden ytd-comments#comments > ytd-item-section-renderer#sections > #contents,
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-hidden ytd-comments#comments #contents { contain: none !important;width: auto !important;height: auto !important;max-height: none !important;overflow: visible !important;visibility: visible !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-comments.tab-content-hidden ytd-comments#comments #contents > * { display: block !important;}
        /* Hide other tabs content */
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-info,
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-videos,
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #tab-list { display: none !important;}
        /* Comments overlay panel */
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) ytd-comments { visibility: visible !important;display: block !important;background-color: var(--yt-live-chat-shimmer-background-color) !important;backdrop-filter: blur(20px) !important;padding: 0 2em !important;border-radius: 2em 0 0 2em !important;max-height: calc(100vh - 120px) !important;overflow-y: auto !important;position: fixed !important;z-index: 2000 !important;top: 3vh !important;right: -42em !important;width: 40em !important;height: 90vh !important;opacity: 0 !important;pointer-events: auto !important;transition: opacity 0.4s ease, right 0.4s ease !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) ytd-comments:hover { opacity: 1 !important;right: 0 !important;}
        /* Transparent overlay chat — fixed panel */
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) [tyt-chat-container],
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat-container { contain: none !important;overflow: visible !important;position: static !important;display: block !important;pointer-events: none !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat { visibility: visible !important;display: block !important;position: fixed !important;top: 3vh !important;right: 0 !important;width: 400px !important;height: calc(100vh - 120px) !important;max-height: calc(100vh - 120px) !important;z-index: 2001 !important;opacity: 0.85 !important;pointer-events: auto !important;border-radius: 2em 0 0 2em !important;overflow: hidden !important;backdrop-filter: blur(20px) !important;transition: opacity 0.4s ease !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat[collapsed] { visibility: visible !important;display: block !important;position: fixed !important;top: 3vh !important;right: 0 !important;width: 400px !important;height: calc(100vh - 120px) !important;max-height: calc(100vh - 120px) !important;z-index: 2001 !important;opacity: 0.85 !important;pointer-events: auto !important;overflow: hidden !important;border-radius: 2em 0 0 2em !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat[collapsed] > #show-hide-button,
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat[collapsed] > .ytd-live-chat-frame#show-hide-button { display: none !important;visibility: hidden !important;opacity: 0 !important;pointer-events: none !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat[collapsed] iframe { display: block !important;visibility: visible !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #chat iframe { height: 100% !important;width: 100% !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) yt-live-chat-renderer { background: transparent !important;}
        /* Ambient mode: fix black bars in theater */
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #cinematics-container,
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #cinematics { position: absolute !important;top: 0 !important;left: 0 !important;width: 100% !important;height: 100% !important;overflow: hidden !important;pointer-events: none !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #cinematics canvas,
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #cinematics video { position: absolute !important;top: 50% !important;left: 50% !important;transform: translate(-50%, -50%) scale(1.2) !important;min-width: 100% !important;min-height: 100% !important;object-fit: cover !important;}
        ytd-watch-flexy:is([theater],[full-bleed-player]):not([fullscreen]) #player-full-bleed-container { overflow: hidden !important;}
        ytd-watch-flexy[fullscreen] ytd-live-chat-frame { background-color: var(--app-drawer-content-container-background-color) !important;}
      `,
      misc: `
        /* Compact feed – reduced spacing, hover menus, inline details */
        ytd-rich-item-renderer { margin-bottom: 15px !important;}
        ytd-rich-item-renderer[rendered-from-rich-grid] { --ytd-rich-item-row-usable-width: calc(100% - var(--ytd-rich-grid-gutter-margin) * 1) !important;}
        ytd-rich-item-renderer #metadata.ytd-video-meta-block { flex-direction: row !important;}
        ytd-rich-item-renderer #metadata.ytd-video-meta-block #metadata-line span:nth-child(3) { height: 1em !important;margin-left: 1em !important;}
        ytd-rich-grid-media { border-radius: 1.2em;height: 100% !important;}
        ytd-rich-grid-media ytd-menu-renderer #button { opacity: 0 !important;transition: opacity 0.3s ease-in-out !important;}
        ytd-rich-grid-media:hover ytd-menu-renderer #button { opacity: 1 !important;}
        /* Show video meta on hover */
        #content #dismissible:hover ytd-video-meta-block { opacity: 1 !important;}
        #frosted-glass { display: none !important;}
      `,
      // CLS Prevention styles - always loaded to reserve space for dynamic elements
      clsPrevention: `
        /* CLS Prevention - Reserve space for dynamic elements */
        #ytp-plus-dislike-text { min-width: 1.5em;display: inline-block !important;}
        /* Contain layout only for our own panels (not YouTube layout elements) */
        .ytp-plus-stats-panel, .ytp-plus-modal-content { contain: layout style;}
        /* Prevent layout shifts from search box animations */
        yt-searchbox { will-change: transform;}
        /* Reduce CLS from late-loading channel avatars */
        #owner #avatar { min-width: 40px; min-height: 40px; }
        /* Reserve space for action buttons to prevent shift */
        ytd-menu-renderer.ytd-watch-metadata { min-height: 36px; }
        /* Subscribe button stability */
        ytd-subscribe-button-renderer { min-width: 90px; }
      `,
    };

    const buildCriticalCss = settings => {
      const z = settings?.zenStyles || {};
      let css = CSS_BLOCKS.clsPrevention; // Always include CLS prevention
      if (z.hideSideGuide) css += CSS_BLOCKS.hideSideGuide;
      if (z.fixFeedLayout) css += CSS_BLOCKS.fixFeedLayout;
      // theaterEnhancements in critical so overlay CSS applies immediately on DOMContentLoaded
      // (previously non-critical, could take up to 5s to appear on theater mode switch)
      if (z.theaterEnhancements) css += CSS_BLOCKS.theaterEnhancements;
      return css.trim();
    };

    const buildNonCriticalCss = settings => {
      const z = settings?.zenStyles || {};
      let css = '';
      if (z.thumbnailHover) css += CSS_BLOCKS.thumbnailHover;
      if (z.immersiveSearch) css += CSS_BLOCKS.immersiveSearch;
      if (z.hideVoiceSearch) css += CSS_BLOCKS.hideVoiceSearch;
      if (z.transparentHeader) css += CSS_BLOCKS.transparentHeader;
      if (z.cleanSideGuide) css += CSS_BLOCKS.cleanSideGuide;
      if (z.betterCaptions) css += CSS_BLOCKS.betterCaptions;
      if (z.playerBlur) css += CSS_BLOCKS.playerBlur;
      if (z.misc) css += CSS_BLOCKS.misc;
      return css.trim();
    };

    const removeStyles = () => {
      try {
        if (window.YouTubeUtils?.StyleManager?.remove) {
          window.YouTubeUtils.StyleManager.remove(STYLE_MANAGER_KEY);
        }
      } catch {}

      if (nonCriticalTimer) {
        if (typeof window !== 'undefined' && typeof window.cancelIdleCallback === 'function') {
          try {
            window.cancelIdleCallback(nonCriticalTimer);
          } catch {}
        } else {
          clearTimeout(nonCriticalTimer);
        }
        nonCriticalTimer = null;
      }

      const el = document.getElementById(STYLE_ELEMENT_ID);
      if (el) {
        try {
          el.remove();
        } catch {}
      }

      const ncEl = document.getElementById(NON_CRITICAL_STYLE_ID);
      if (ncEl) {
        try {
          ncEl.remove();
        } catch {}
      }
    };

    const applyNonCriticalStyles = css => {
      if (!css) {
        const ncEl = document.getElementById(NON_CRITICAL_STYLE_ID);
        if (ncEl) ncEl.remove();
        return;
      }

      let ncEl = document.getElementById(NON_CRITICAL_STYLE_ID);
      if (!ncEl) {
        ncEl = document.createElement('style');
        ncEl.id = NON_CRITICAL_STYLE_ID;
        (document.head || document.documentElement).appendChild(ncEl);
      }
      ncEl.textContent = css;
    };

    const applyStyles = settings => {
      const enabled = settings?.enableZenStyles !== false;
      if (!enabled) {
        removeStyles();
        return;
      }

      const criticalCss = buildCriticalCss(settings);
      const nonCriticalCss = buildNonCriticalCss(settings);
      if (!criticalCss && !nonCriticalCss) {
        removeStyles();
        return;
      }

      try {
        if (window.YouTubeUtils?.StyleManager?.add) {
          window.YouTubeUtils.StyleManager.add(STYLE_MANAGER_KEY, criticalCss || '');
          // Ensure legacy <style> isn't left behind
          const el = document.getElementById(STYLE_ELEMENT_ID);
          if (el) el.remove();
          if (nonCriticalTimer) {
            if (typeof window !== 'undefined' && typeof window.cancelIdleCallback === 'function') {
              try {
                window.cancelIdleCallback(nonCriticalTimer);
              } catch {}
            } else {
              clearTimeout(nonCriticalTimer);
            }
          }
          if (typeof requestIdleCallback === 'function') {
            nonCriticalTimer = requestIdleCallback(() => applyNonCriticalStyles(nonCriticalCss), {
              timeout: 5000,
            });
          } else {
            nonCriticalTimer = setTimeout(() => applyNonCriticalStyles(nonCriticalCss), 200);
          }
          return;
        }
      } catch {}

      let el = document.getElementById(STYLE_ELEMENT_ID);
      if (!el) {
        el = document.createElement('style');
        el.id = STYLE_ELEMENT_ID;
        (document.head || document.documentElement).appendChild(el);
      }
      el.textContent = criticalCss || '';

      if (nonCriticalTimer) {
        if (typeof window !== 'undefined' && typeof window.cancelIdleCallback === 'function') {
          try {
            window.cancelIdleCallback(nonCriticalTimer);
          } catch {}
        } else {
          clearTimeout(nonCriticalTimer);
        }
      }
      if (typeof requestIdleCallback === 'function') {
        nonCriticalTimer = requestIdleCallback(() => applyNonCriticalStyles(nonCriticalCss), {
          timeout: 5000,
        });
      } else {
        nonCriticalTimer = setTimeout(() => applyNonCriticalStyles(nonCriticalCss), 200);
      }
    };

    const applyFromStorage = () => applyStyles(loadSettings());

    // Initial apply
    applyFromStorage();

    // Live updates
    window.addEventListener('youtube-plus-settings-updated', e => {
      try {
        applyStyles(e?.detail || loadSettings());
      } catch {
        applyFromStorage();
      }
    });
  } catch (err) {
    console.error('zen-youtube-features injection failed', err);
  }
})();

// Theater overlay runtime fixes
// 1) Auto-expand live chat in theater overlay (avoid "Show chat" placeholder)
// 2) Preload comments content so Zen comments panel is not empty
(function () {
  'use strict';

  const host = typeof location === 'undefined' ? '' : location.hostname;
  if (!host) return;
  if (!/(^|\.)youtube\.com$/.test(host) && !/\.youtube\.google/.test(host)) return;

  const SETTINGS_KEY = 'youtube_plus_settings';
  const PRELOADED_ATTR = 'data-ytp-zen-comments-preloaded';

  const isWatchPage = () => location.pathname === '/watch';

  const readSettings = () => {
    try {
      const raw = localStorage.getItem(SETTINGS_KEY);
      if (!raw) return null;
      return JSON.parse(raw);
    } catch {
      return null;
    }
  };

  const isTheaterEnhancementEnabled = () => {
    const settings = readSettings();
    if (!settings) return true;
    if (settings.enableZenStyles === false) return false;
    if (settings.zenStyles && settings.zenStyles.theaterEnhancements === false) return false;
    return true;
  };

  const clickElement = element => {
    if (!element) return;
    try {
      element.dispatchEvent(
        new window.MouseEvent('click', { bubbles: true, cancelable: true, view: window })
      );
    } catch {
      try {
        element.click();
      } catch {}
    }
  };

  const preloadCommentsInBackground = flexy => {
    const commentsTab = document.querySelector('#tab-comments');
    const commentsBtn = document.querySelector('#material-tabs a[tyt-tab-content="#tab-comments"]');
    if (!commentsTab || !commentsBtn || commentsTab.getAttribute(PRELOADED_ATTR) === '1') return;

    // Disable tiny pre-load mode CSS from main.js for theater overlay comments.
    if (flexy && !flexy.hasAttribute('keep-comments-scroller')) {
      flexy.setAttribute('keep-comments-scroller', '');
    }

    const activeBtn = document.querySelector('#material-tabs a[tyt-tab-content].active');
    clickElement(commentsBtn);

    requestAnimationFrame(() => {
      commentsTab.setAttribute(PRELOADED_ATTR, '1');
      if (activeBtn && activeBtn !== commentsBtn && activeBtn.isConnected) {
        clickElement(activeBtn);
      }
    });
  };

  // Re-enabled: now safe because ytBtnCancelTheater() in main.js is guarded by
  // isZenTheaterOverlayActive() — it won't exit theater when zen overlay CSS is active.
  // Without this JS step, the iframe inside #chat doesn't load when [collapsed].
  let expandAttempts = 0;
  const MAX_EXPAND_ATTEMPTS = 3;

  const expandLiveChat = () => {
    const chat = document.querySelector('ytd-live-chat-frame#chat');
    if (!chat) return;

    // Step 1: Uncollapse the chat element if it has [collapsed] attribute
    if (chat.hasAttribute('collapsed')) {
      if (expandAttempts >= MAX_EXPAND_ATTEMPTS) return;
      expandAttempts++;

      // Method 1: Polymer internal API (same approach as main.js ytBtnExpandChat)
      let expanded = false;
      try {
        const cnt =
          chat.polymerController ||
          (typeof chat.__data !== 'undefined' ? chat : null) ||
          (chat.inst ? chat.inst : null);
        if (cnt && typeof cnt.setCollapsedState === 'function') {
          cnt.setCollapsedState({
            setLiveChatCollapsedStateAction: { collapsed: false },
          });
          expanded = cnt.collapsed === false;
        }
        if (!expanded && cnt && typeof cnt.collapsed === 'boolean') {
          cnt.collapsed = false;
          if (cnt.isHiddenByUser === true) cnt.isHiddenByUser = false;
          expanded = cnt.collapsed === false;
        }
      } catch {}

      // Method 2: click the "Show chat" button as fallback
      if (!expanded) {
        const showBtn = chat.querySelector(
          '#show-hide-button div.yt-spec-touch-feedback-shape, ' +
            '#show-hide-button ytd-toggle-button-renderer, ' +
            '#show-hide-button button'
        );
        if (showBtn) clickElement(showBtn);
      }
    }

    // Step 2: Ensure the iframe has its src loaded.
    // YouTube's Polymer binding may not fire when we uncollapse programmatically,
    // leaving the iframe empty. Manually set src from the element's URL property.
    const iframe = chat.querySelector('iframe#chatframe');
    if (iframe && !iframe.src && chat.url) {
      iframe.src = chat.url;
    }
  };

  const runOverlayFixes = () => {
    if (!isWatchPage()) return;
    if (!isTheaterEnhancementEnabled()) return;

    const flexy = document.querySelector('ytd-watch-flexy');
    if (!flexy || flexy.hasAttribute('fullscreen')) return;
    const isTheaterLike =
      flexy.hasAttribute('theater') ||
      flexy.hasAttribute('full-bleed-player') ||
      flexy.hasAttribute('theater-requested_');
    if (!isTheaterLike) return;

    expandLiveChat();
    preloadCommentsInBackground(flexy);
  };

  let debounceTimer = 0;
  const scheduleRun = () => {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(runOverlayFixes, 150);
  };

  // --- Targeted observers (NOT document.body subtree — that fires hundreds of times during page parse) ---
  const setupOverlayObservers = () => {
    // Observer 1: watch ytd-watch-flexy for theater / fullscreen attribute changes
    const flexyObserver = new MutationObserver(scheduleRun);
    let observedFlexy = null;

    const attachFlexyObserver = () => {
      const flexy = document.querySelector('ytd-watch-flexy');
      if (flexy && flexy !== observedFlexy) {
        if (observedFlexy) flexyObserver.disconnect();
        flexyObserver.observe(flexy, {
          attributes: true,
          attributeFilter: ['theater', 'full-bleed-player', 'theater-requested_', 'fullscreen'],
        });
        observedFlexy = flexy;
      }
    };

    // Observer 2: watch #chat for [collapsed] changes
    // (CSS handles display; observer just schedules overlay fixes like comment preloading)
    const chatObserver = new MutationObserver(scheduleRun);
    let observedChat = null;

    const attachChatObserver = () => {
      const chat = document.querySelector('ytd-live-chat-frame#chat');
      if (chat && chat !== observedChat) {
        if (observedChat) chatObserver.disconnect();
        chatObserver.observe(chat, {
          attributes: true,
          attributeFilter: ['collapsed'],
        });
        observedChat = chat;
      }
    };

    attachFlexyObserver();
    attachChatObserver();

    // Re-attach after SPA navigation (new flexy/chat elements are created)
    window.addEventListener(
      'yt-navigate-finish',
      () => {
        expandAttempts = 0; // reset on navigation so chat can expand on new page
        setTimeout(() => {
          attachFlexyObserver();
          attachChatObserver();
          scheduleRun();
        }, 180);
      },
      { passive: true }
    );

    try {
      if (window.YouTubeUtils?.cleanupManager?.registerObserver) {
        window.YouTubeUtils.cleanupManager.registerObserver(flexyObserver);
        window.YouTubeUtils.cleanupManager.registerObserver(chatObserver);
      }
    } catch {}
  };

  // Defer observer setup to after DOMContentLoaded so it does NOT fire during page parse
  // (observing document.documentElement at document-start fires hundreds of times and hurts LCP)
  if (document.readyState === 'loading') {
    document.addEventListener(
      'DOMContentLoaded',
      () => {
        scheduleRun();
        setupOverlayObservers();
      },
      { once: true }
    );
  } else {
    scheduleRun();
    setupOverlayObservers();
  }
})();

// Comment Translation Button
// Restores the "Translate to ..." button that YouTube removed from comments
(function () {
  'use strict';

  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    return key;
  };

  const TRANSLATE_BTN_CLASS = 'ytp-comment-translate-btn';
  const TRANSLATED_ATTR = 'data-ytp-translated';
  const ORIGINAL_ATTR = 'data-ytp-original-text';

  /**
   * Map YouTube+/YouTube locale codes → Google Translate BCP-47 codes.
   * Google Translate uses mostly ISO 639-1 with some regional variants.
   */
  const LANG_MAP = {
    // YouTube+ internal codes
    cn: 'zh-CN',
    tw: 'zh-TW',
    kr: 'ko',
    jp: 'ja',
    ng: 'en',
    du: 'nl',
    be: 'be',
    bg: 'bg',
    kk: 'kk',
    ky: 'ky',
    uz: 'uz',
    uk: 'uk',
    // YouTube locale codes → ISO 639-1
    'zh-hans': 'zh-CN',
    'zh-hant': 'zh-TW',
    'zh-cn': 'zh-CN',
    'zh-tw': 'zh-TW',
    'zh-hk': 'zh-TW',
    iw: 'he', // YouTube uses 'iw' for Hebrew
    jv: 'jw', // Javanese
    'sr-latn': 'sr',
    'pt-br': 'pt',
    'pt-pt': 'pt',
    // Pass-through for standard ISO 639-1 codes
    ar: 'ar',
    az: 'az',
    cs: 'cs',
    da: 'da',
    de: 'de',
    el: 'el',
    en: 'en',
    es: 'es',
    fi: 'fi',
    fr: 'fr',
    hi: 'hi',
    hr: 'hr',
    hu: 'hu',
    id: 'id',
    it: 'it',
    lt: 'lt',
    lv: 'lv',
    ms: 'ms',
    nl: 'nl',
    no: 'no',
    pl: 'pl',
    ro: 'ro',
    ru: 'ru',
    sk: 'sk',
    sl: 'sl',
    sq: 'sq',
    sv: 'sv',
    th: 'th',
    tr: 'tr',
    vi: 'vi',
  };

  /** Normalise any locale/YouTube+ code to a Google-Translate-compatible code */
  const toGoogleLang = code => {
    if (!code) return 'en';
    const lower = code.toLowerCase();
    if (LANG_MAP[lower]) return LANG_MAP[lower];
    // Strip region suffix for unknown codes (e.g. 'es-419' → 'es')
    const base = lower.split('-')[0];
    return LANG_MAP[base] || base || 'en';
  };

  /** Detect user's preferred language (returns Google-Translate-compatible code) */
  const getUserLanguage = () => {
    try {
      // 1. YouTube+ i18n internal code (e.g. 'cn', 'kr', 'jp')
      if (window.YouTubePlusI18n?.getLanguage) {
        return toGoogleLang(window.YouTubePlusI18n.getLanguage());
      }
      // 2. <html lang="..."> attribute set by YouTube
      const htmlLang = document.documentElement.lang;
      if (htmlLang) return toGoogleLang(htmlLang);
    } catch {}
    // 3. Browser navigator.language
    return toGoogleLang(navigator.language) || 'en';
  };

  /** Translate text using Google Translate (free endpoint) */
  const translateText = async (text, targetLang) => {
    try {
      const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${encodeURIComponent(targetLang)}&dt=t&q=${encodeURIComponent(text)}`;
      const resp = await fetch(url);
      if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
      const data = await resp.json();
      if (Array.isArray(data) && Array.isArray(data[0])) {
        return data[0].map(s => (s && s[0]) || '').join('');
      }
    } catch (e) {
      console.warn('[YouTube+] Translation failed:', e);
    }
    return null;
  };

  /** Get the translate button label (uses i18n) */
  const getTranslateLabel = () => t('translateComment') || 'Translate';

  /** Get the show-original button label (uses i18n) */
  const getShowOriginalLabel = () => t('showOriginal') || 'Show original';

  /** Inject CSS for translate button */
  const injectStyles = (() => {
    let injected = false;
    return () => {
      if (injected) return;
      injected = true;
      const css = `
        .${TRANSLATE_BTN_CLASS}{
          display:inline-flex;align-items:center;gap:4px;
          background:none;border:none;cursor:pointer;
          color:var(--yt-spec-text-secondary,#aaa);
          font-size:1.2rem;line-height:1.8rem;font-weight:400;
          padding:4px 0;margin-top:4px;
          font-family:'Roboto','Arial',sans-serif;
          transition:color .2s;
        }
        .${TRANSLATE_BTN_CLASS}:hover{color:var(--yt-spec-text-primary,#fff);}
        .${TRANSLATE_BTN_CLASS}[disabled]{opacity:.5;cursor:wait;}
        .${TRANSLATE_BTN_CLASS} svg{flex-shrink:0;}
      `;
      try {
        if (window.YouTubeUtils?.StyleManager?.add) {
          window.YouTubeUtils.StyleManager.add('ytp-comment-translate-styles', css);
          return;
        }
      } catch {}
      const style = document.createElement('style');
      style.id = 'ytp-comment-translate-styles';
      style.textContent = css;
      (document.head || document.documentElement).appendChild(style);
    };
  })();

  const translateIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12.87 15.07l-2.54-2.51.03-.03A17.52 17.52 0 0014.07 6H17V4h-7V2H8v2H1v2h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg>`;

  /** Add translate button to a comment element */
  const addTranslateButton = commentEl => {
    if (commentEl.querySelector(`.${TRANSLATE_BTN_CLASS}`)) return;

    // Find the text content element
    const contentEl = commentEl.querySelector(
      '#content-text.ytd-comment-view-model, ' +
        '#content-text.ytd-comment-renderer, ' +
        'yt-attributed-string#content-text, ' +
        'yt-formatted-string#content-text, ' +
        '#content-text'
    );
    if (!contentEl) return;

    const text = (contentEl.textContent || '').trim();
    if (!text || text.length < 2) return;

    // Don't add if comment is already in user's language (basic heuristic)
    const userLang = getUserLanguage();

    const btn = document.createElement('button');
    btn.className = TRANSLATE_BTN_CLASS;
    btn.type = 'button';
    btn.innerHTML = `${translateIcon} ${getTranslateLabel()}`;
    btn.setAttribute('aria-label', getTranslateLabel());

    btn.addEventListener('click', async e => {
      e.preventDefault();
      e.stopPropagation();

      if (contentEl.hasAttribute(TRANSLATED_ATTR)) {
        // Toggle back to original
        const original = contentEl.getAttribute(ORIGINAL_ATTR);
        if (original) {
          contentEl.textContent = original;
          contentEl.removeAttribute(TRANSLATED_ATTR);
          btn.innerHTML = `${translateIcon} ${getTranslateLabel()}`;
        }
        return;
      }

      btn.disabled = true;
      btn.innerHTML = `${translateIcon} ...`;

      const originalText = contentEl.textContent || '';
      const translated = await translateText(originalText, userLang);

      if (translated && translated !== originalText) {
        contentEl.setAttribute(ORIGINAL_ATTR, originalText);
        contentEl.setAttribute(TRANSLATED_ATTR, 'true');
        contentEl.textContent = translated;
        btn.innerHTML = `${translateIcon} ${getShowOriginalLabel()}`;
      } else {
        btn.innerHTML = `${translateIcon} ${getTranslateLabel()}`;
      }
      btn.disabled = false;
    });

    // Insert after the text content
    const actionBar = commentEl.querySelector(
      '#action-buttons, ytd-comment-action-buttons-renderer, #toolbar'
    );
    if (actionBar) {
      actionBar.parentElement.insertBefore(btn, actionBar);
    } else {
      contentEl.after(btn);
    }
  };

  /** Process all visible comments */
  const processComments = () => {
    const commentSelectors = [
      'ytd-comment-view-model',
      'ytd-comment-renderer',
      'ytd-comment-thread-renderer',
    ];
    for (const sel of commentSelectors) {
      document.querySelectorAll(sel).forEach(addTranslateButton);
    }
  };

  /** Debounced processing */
  let processTimeout = null;
  const scheduleProcess = () => {
    if (processTimeout) clearTimeout(processTimeout);
    processTimeout = setTimeout(processComments, 300);
  };

  /** Initialize */
  const init = () => {
    injectStyles();
    processComments();

    // Observe for new comments
    const commentsContainer = document.querySelector('#comments, #tab-comments, #content');
    const target = commentsContainer || document.body;

    const observer = new MutationObserver(mutations => {
      let hasNewComments = false;
      for (const m of mutations) {
        for (const node of m.addedNodes) {
          if (!(node instanceof Element)) continue;
          if (
            node.matches?.(
              'ytd-comment-view-model, ytd-comment-renderer, ytd-comment-thread-renderer'
            ) ||
            node.querySelector?.('ytd-comment-view-model, ytd-comment-renderer, #content-text')
          ) {
            hasNewComments = true;
            break;
          }
        }
        if (hasNewComments) break;
      }
      if (hasNewComments) scheduleProcess();
    });

    observer.observe(target, { childList: true, subtree: true });

    try {
      if (window.YouTubeUtils?.cleanupManager) {
        window.YouTubeUtils.cleanupManager.registerObserver(observer);
      }
    } catch {}
  };

  // Lazy init on watch pages
  const scheduleInit = () => {
    const isVideoPage = location.pathname === '/watch' || location.pathname.startsWith('/shorts/');
    if (!isVideoPage) return;

    if (typeof requestIdleCallback === 'function') {
      requestIdleCallback(() => init(), { timeout: 3000 });
    } else {
      setTimeout(init, 1500);
    }
  };

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

  window.addEventListener('yt-navigate-finish', scheduleInit, { passive: true });
})();

// --- MODULE: adblocker.js ---

// Ad Blocker
(function () {
  'use strict';

  // DOM cache helpers with fallback
  const $ = selector => {
    if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.get === 'function') {
      return window.YouTubeDOMCache.get(selector);
    }
    return document.querySelector(selector);
  };
  const $$ = selector => {
    if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.getAll === 'function') {
      return window.YouTubeDOMCache.getAll(selector);
    }
    return document.querySelectorAll(selector);
  };

  /**
   * Translation helper - uses centralized i18n system
   * @param {string} key - Translation key
   * @param {Object} params - Interpolation parameters
   * @returns {string} Translated string
   */
  function t(key, params = {}) {
    try {
      if (typeof window !== 'undefined') {
        if (window.YouTubePlusI18n && typeof window.YouTubePlusI18n.t === 'function') {
          return window.YouTubePlusI18n.t(key, params);
        }
        if (window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function') {
          return window.YouTubeUtils.t(key, params);
        }
      }
    } catch {
      // Fallback to key if central i18n unavailable
    }
    return key;
  }

  /**
   * Ad blocking functionality for YouTube
   * @namespace AdBlocker
   */
  const AdBlocker = {
    /**
     * Configuration settings
     * @type {Object}
     */
    config: {
      skipInterval: 1000, // Combined ad-check interval (skip + remove + dismiss).
      removeInterval: 3000,
      enableLogging: false,
      maxRetries: 2,
      enabled: true,
      storageKey: 'youtube_adblocker_settings',
    },

    /**
     * Current state tracking
     * @type {Object}
     */
    state: {
      isYouTubeShorts: false,
      isYouTubeMusic: location.hostname === 'music.youtube.com',
      lastSkipAttempt: 0,
      retryCount: 0,
      initialized: false,
    },

    /**
     * Cached DOM queries for performance
     * @type {Object}
     */
    cache: {
      moviePlayer: null,
      ytdPlayer: null,
      lastCacheTime: 0,
      cacheTimeout: 10000, // Increased cache timeout for better performance
    },

    /**
     * Optimized CSS selectors for ad elements
     * @type {Object}
     */
    selectors: {
      // Only hide minor ad UI elements that YouTube doesn't monitor
      ads: '.ytp-ad-timed-pie-countdown-container,.ytp-ad-survey-questions,.ytp-ad-overlay-container,.ytp-ad-progress,.ytp-ad-progress-list',
      // These are removed via DOM manipulation only (not CSS) to avoid detection
      elements:
        '#masthead-ad,ytd-merch-shelf-renderer,.yt-mealbar-promo-renderer,ytmusic-mealbar-promo-renderer,ytmusic-statement-banner-renderer,.ytp-featured-product,ytd-in-feed-ad-layout-renderer,ytd-banner-promo-renderer,ytd-statement-banner-renderer,ytd-brand-video-singleton-renderer,ytd-brand-video-shelf-renderer,ytd-promoted-sparkles-web-renderer,ytd-display-ad-renderer,ytd-promoted-video-renderer,.ytd-mealbar-promo-renderer',
      video: 'video.html5-main-video',
      // Match both ad-slot renderers inside reels and standalone ad-slot-renderer nodes
      removal:
        'ytd-reel-video-renderer .ytd-ad-slot-renderer, ytd-ad-slot-renderer, #player-ads, ytd-in-feed-ad-layout-renderer, ytd-display-ad-renderer, ytd-promoted-sparkles-web-renderer, ytd-promoted-video-renderer, ad-slot-renderer, ytd-player-legacy-desktop-watch-ads-renderer',
    },

    // Known item wrapper selectors that should be removed when they only contain ads
    wrappers: [
      'ytd-rich-item-renderer',
      'ytd-grid-video-renderer',
      'ytd-compact-video-renderer',
      'ytd-rich-grid-media',
      'ytd-rich-shelf-renderer',
      'ytd-rich-grid-row',
      'ytd-video-renderer',
      'ytd-playlist-renderer',
      'ytd-reel-video-renderer',
    ],

    /**
     * Settings management with localStorage persistence
     * @type {Object}
     */
    settings: {
      /**
       * Load settings from localStorage with validation
       * @returns {void}
       */
      load() {
        try {
          const saved = localStorage.getItem(AdBlocker.config.storageKey);
          if (!saved) return;

          const parsed = JSON.parse(saved);
          if (typeof parsed !== 'object' || parsed === null) {
            console.warn('[AdBlocker] Invalid settings format');
            return;
          }

          // Validate and apply settings
          if (typeof parsed.enabled === 'boolean') {
            AdBlocker.config.enabled = parsed.enabled;
          } else {
            AdBlocker.config.enabled = true; // Default to enabled
          }

          if (typeof parsed.enableLogging === 'boolean') {
            AdBlocker.config.enableLogging = parsed.enableLogging;
          } else {
            AdBlocker.config.enableLogging = false; // Default to disabled
          }
        } catch (error) {
          console.error('[AdBlocker] Error loading settings:', error);
          // Set safe defaults on error
          AdBlocker.config.enabled = true;
          AdBlocker.config.enableLogging = false;
        }
      },

      /**
       * Save settings to localStorage with error handling
       * @returns {void}
       */
      save() {
        try {
          const settingsToSave = {
            enabled: AdBlocker.config.enabled,
            enableLogging: AdBlocker.config.enableLogging,
          };
          localStorage.setItem(AdBlocker.config.storageKey, JSON.stringify(settingsToSave));
        } catch (error) {
          console.error('[AdBlocker] Error saving settings:', error);
        }
      },
    },

    /**
     * Get cached player elements
     * @returns {Object} Object containing player element and controller
     */
    getPlayer() {
      const now = Date.now();
      if (now - AdBlocker.cache.lastCacheTime > AdBlocker.cache.cacheTimeout) {
        AdBlocker.cache.moviePlayer = $('#movie_player');
        AdBlocker.cache.ytdPlayer = $('#ytd-player');
        AdBlocker.cache.lastCacheTime = now;
      }

      const playerEl = AdBlocker.cache.ytdPlayer;
      return {
        element: AdBlocker.cache.moviePlayer,
        player: playerEl?.getPlayer?.() || playerEl,
      };
    },

    /**
     * Skip current ad by clicking skip button or speeding through
     * Uses a stealthier approach to avoid YouTube ad blocker detection
     * @returns {void}
     */
    skipAd() {
      if (!AdBlocker.config.enabled) return;

      const now = Date.now();
      if (now - AdBlocker.state.lastSkipAttempt < 300) return;
      AdBlocker.state.lastSkipAttempt = now;

      if (location.pathname.startsWith('/shorts/')) return;

      // Check for ad-showing class on player
      const moviePlayer = $('#movie_player');
      const isAdShowing =
        moviePlayer &&
        (moviePlayer.classList.contains('ad-showing') ||
          moviePlayer.classList.contains('ad-interrupting'));
      if (!isAdShowing) {
        AdBlocker.state.retryCount = 0;
        return;
      }

      try {
        // Strategy 1: Click skip button if available (most natural user action)
        const skipSelectors = [
          '.ytp-ad-skip-button',
          '.ytp-ad-skip-button-modern',
          '.ytp-skip-ad-button',
          '.videoAdUiSkipButton',
          'button.ytp-ad-skip-button-modern',
          '.ytp-ad-skip-button-slot button',
          '.ytp-ad-skip-button-container button',
          '.ytp-ad-skip-button-modern .ytp-ad-skip-button-container',
          // 2025+ new skip button selectors
          '.ytp-skip-ad-button__text',
          'button[class*="skip"]',
          '.ytp-ad-skip-button-modern button',
          'ytd-button-renderer.ytp-ad-skip-button-renderer button',
        ];
        for (const sel of skipSelectors) {
          const skipButton = document.querySelector(sel);
          if (skipButton) {
            // offsetParent is null for position:fixed elements (YouTube skip buttons)
            // so use getBoundingClientRect width/height as the visibility check instead
            const rect = skipButton.getBoundingClientRect();
            if (rect.width > 0 && rect.height > 0) {
              skipButton.click();
              AdBlocker.state.retryCount = 0;
              return;
            }
          }
        }

        // Strategy 2: Speed through ad (mute + seek to end)
        const video = $(AdBlocker.selectors.video);
        if (video) {
          video.muted = true;
          // Attempt to seek to end of ad if duration is available
          if (video.duration && isFinite(video.duration) && video.duration > 0) {
            try {
              video.currentTime = Math.max(video.duration - 0.1, 0);
            } catch (e) {
              console.warn('[YouTube+] Ad seek error:', e);
            }
          }
        }

        // Strategy 3: Close overlay ads
        const overlaySelectors = [
          '.ytp-ad-overlay-close-button',
          '.ytp-ad-overlay-close-container button',
          '.ytp-ad-overlay-close-button button',
          // 2025+ overlay close
          '.ytp-ad-overlay-ad-info-button-container',
          'button[id="dismiss-button"]',
        ];
        for (const sel of overlaySelectors) {
          const overlayClose = $(sel);
          if (overlayClose) {
            overlayClose.click();
            break;
          }
        }

        AdBlocker.state.retryCount = 0;
      } catch {
        if (AdBlocker.state.retryCount < AdBlocker.config.maxRetries) {
          AdBlocker.state.retryCount++;
          setTimeout(AdBlocker.skipAd, 800);
        }
      }
    },

    /**
     * Dismiss YouTube's ad blocker warning popup if detected
     * @returns {void}
     */
    dismissAdBlockerWarning() {
      if (!AdBlocker.config.enabled) return;
      try {
        // Strategy 1: Handle the enforcement message overlay
        const enforcement = document.querySelector('ytd-enforcement-message-view-model');
        if (enforcement) {
          // Find any dismiss/close/allow button
          const btns = enforcement.querySelectorAll(
            'button, tp-yt-paper-button, a.yt-spec-button-shape-next--outline'
          );
          for (const btn of btns) {
            const btnText = (btn.textContent || '').toLowerCase().trim();
            // Click "Allow YouTube ads", "Dismiss", or the X button
            if (
              btnText.includes('allow') ||
              btnText.includes('dismiss') ||
              btnText.includes('разрешить') ||
              btn.getAttribute('aria-label')?.includes('close')
            ) {
              btn.click();
              return;
            }
          }
          // If no matching button, try removing the overlay
          enforcement.remove();
          return;
        }

        // Strategy 2: Handle paper dialog popups
        const dialogs = document.querySelectorAll(
          'tp-yt-paper-dialog, ytd-popup-container tp-yt-paper-dialog, yt-dialog-container'
        );
        for (const dialog of dialogs) {
          const text = (dialog.textContent || '').toLowerCase();
          const isAdBlockWarning =
            text.includes('ad blocker') ||
            text.includes('ad blockers') ||
            text.includes('блокировщик') ||
            text.includes('will be blocked') ||
            text.includes('будет заблокирован') ||
            (text.includes('allow') && text.includes('ads')) ||
            (text.includes('blocker') && text.includes('video'));

          if (!isAdBlockWarning) continue;

          // Try dismiss/allow buttons
          const dismissBtns = dialog.querySelectorAll(
            '#dismiss-button button, .dismiss-button, button[id*="dismiss"], ' +
              'tp-yt-paper-button, yt-button-renderer button, a[href]'
          );
          for (const btn of dismissBtns) {
            const btnText = (btn.textContent || '').toLowerCase();
            if (
              btnText.includes('dismiss') ||
              btnText.includes('allow') ||
              btnText.includes('not using') ||
              btnText.includes('report')
            ) {
              btn.click();
              return;
            }
          }
          // Last resort: remove dialog
          dialog.style.display = 'none';
          dialog.remove();
          return;
        }

        // Strategy 3: Handle overlay/backdrop that blocks interaction
        const overlays = document.querySelectorAll(
          'tp-yt-iron-overlay-backdrop, .yt-dialog-overlay'
        );
        for (const overlay of overlays) {
          if (overlay.style.display !== 'none' && overlay.offsetParent !== null) {
            overlay.style.display = 'none';
          }
        }
      } catch {
        // Silently ignore
      }
    },

    // Minimal CSS injection - only hide minor UI elements that YouTube doesn't monitor
    addCss() {
      if ($('#yt-ab-styles') || !AdBlocker.config.enabled) return;

      // Only use ads selectors (countdown, survey) in CSS
      // element selectors (masthead-ad, merch, etc.) are removed via DOM to avoid detection
      const styles = `${AdBlocker.selectors.ads}{display:none!important;}`;
      YouTubeUtils.StyleManager.add('yt-ab-styles', styles);
    },

    removeCss() {
      YouTubeUtils.StyleManager.remove('yt-ab-styles');
    },

    // Batched element removal
    removeElements() {
      if (!AdBlocker.config.enabled || AdBlocker.state.isYouTubeMusic) return;

      // Use requestIdleCallback for non-blocking removal
      const remove = () => {
        // Remove known ad elements directly (these were previously in CSS)
        try {
          const adElements = document.querySelectorAll(AdBlocker.selectors.elements);
          adElements.forEach(el => {
            try {
              el.remove();
            } catch {}
          });
        } catch {}

        // Remove ad-slot renderers
        const elements = document.querySelectorAll(AdBlocker.selectors.removal);
        elements.forEach(el => {
          try {
            // Prefer removing a known item wrapper (thumbnail card, reel item, etc.)
            for (const w of AdBlocker.wrappers) {
              const wrap = el.closest(w);
              if (wrap) {
                wrap.remove();
                return;
              }
            }

            // If ad is inside a reel item specifically, remove the reel container
            const reel = el.closest('ytd-reel-video-renderer');
            if (reel) {
              reel.remove();
              return;
            }

            // If standalone ad-slot-renderer or other ad container, remove the nearest reasonable container
            const container =
              el.closest('ytd-ad-slot-renderer') || el.closest('.ad-container') || el;
            if (container && container.remove) container.remove();
          } catch (e) {
            if (AdBlocker.config.enableLogging) console.warn('[AdBlocker] removeElements error', e);
          }
        });
      };

      if (window.requestIdleCallback) {
        requestIdleCallback(remove, { timeout: 100 });
      } else {
        setTimeout(remove, 0);
      }
    },

    // Optimized settings UI
    addSettingsUI() {
      const section = $('.ytp-plus-settings-section[data-section="basic"]');
      if (!section || section.querySelector('.ab-settings')) return;

      try {
        const item = document.createElement('div');
        item.className = 'ytp-plus-settings-item ab-settings';
        item.innerHTML = `
          <div>
            <label class="ytp-plus-settings-item-label">${t('adBlocker')}</label>
            <div class="ytp-plus-settings-item-description">${t('adBlockerDescription')}</div>
          </div>
          <input type="checkbox" class="ytp-plus-settings-checkbox" ${AdBlocker.config.enabled ? 'checked' : ''}>
        `;

        section.appendChild(item);

        item.querySelector('input').addEventListener('change', e => {
          const target = /** @type {EventTarget & HTMLInputElement} */ (e.target);
          AdBlocker.config.enabled = target.checked;
          AdBlocker.settings.save();
          AdBlocker.config.enabled ? AdBlocker.addCss() : AdBlocker.removeCss();
        });
      } catch (error) {
        YouTubeUtils.logError('AdBlocker', 'Failed to add settings UI', error);
      }
    },

    // Streamlined initialization
    init() {
      if (AdBlocker.state.initialized) return;
      AdBlocker.state.initialized = true;

      AdBlocker.settings.load();

      if (AdBlocker.config.enabled) {
        AdBlocker.addCss();
        AdBlocker.removeElements();
      }

      // Start optimized intervals with cleanup registration
      // Use single combined interval instead of 3 separate ones
      const combinedAdCheck = () => {
        if (AdBlocker.config.enabled) {
          AdBlocker.skipAd();
          AdBlocker.removeElements();
          AdBlocker.dismissAdBlockerWarning();
        }
      };
      // Single interval for all ad-related checks (faster interval for responsive skip)
      const adInterval = setInterval(combinedAdCheck, AdBlocker.config.skipInterval);
      YouTubeUtils.cleanupManager.registerInterval(adInterval);

      // Also monitor video play events for immediate ad detection
      try {
        const handleVideoPlay = () => {
          if (AdBlocker.config.enabled) {
            setTimeout(AdBlocker.skipAd, 50);
            setTimeout(AdBlocker.skipAd, 200);
            setTimeout(AdBlocker.skipAd, 500);
          }
        };
        document.addEventListener('playing', handleVideoPlay, { capture: true, passive: true });
      } catch (e) {
        console.warn('[YouTube+] Ad play listener error:', e);
      }

      // Navigation handling
      const handleNavigation = () => {
        AdBlocker.state.isYouTubeShorts = location.pathname.startsWith('/shorts/');
        AdBlocker.cache.lastCacheTime = 0; // Reset cache
      };

      // Use centralized pushState/replaceState event from utils.js
      window.addEventListener('ytp-history-navigate', () => setTimeout(handleNavigation, 50));

      // Settings modal integration — use event instead of MutationObserver
      document.addEventListener('youtube-plus-settings-modal-opened', () => {
        setTimeout(AdBlocker.addSettingsUI, 50);
      });

      // Observe DOM for dynamically inserted ad slots and remove them
      // Use targeted observation rather than full subtree to reduce detection risk
      try {
        const adSlotObserver = new MutationObserver(mutations => {
          let shouldRemove = false;
          for (const m of mutations) {
            for (const node of m.addedNodes) {
              if (!(node instanceof Element)) continue;
              try {
                if (
                  node.matches &&
                  node.matches('ytd-ad-slot-renderer, ytd-merch-shelf-renderer')
                ) {
                  shouldRemove = true;
                  break;
                }
                if (node.querySelector && node.querySelector('ytd-ad-slot-renderer')) {
                  shouldRemove = true;
                  break;
                }
              } catch (e) {
                if (AdBlocker.config.enableLogging) {
                  console.warn('[AdBlocker] adSlotObserver node check', e);
                }
              }
            }
            if (shouldRemove) break;
          }
          if (shouldRemove) {
            AdBlocker.removeElements();
          }
        });

        // Observe only specific content containers, not the entire body
        const observeContentContainers = () => {
          const containers = [
            document.querySelector('#content'),
            document.querySelector('#page-manager'),
            document.querySelector('ytd-browse'),
            document.querySelector('ytd-search'),
          ].filter(Boolean);

          if (containers.length === 0) {
            // Fallback to body with reduced scope
            adSlotObserver.observe(document.body, { childList: true, subtree: true });
          } else {
            containers.forEach(container => {
              adSlotObserver.observe(container, { childList: true, subtree: true });
            });
          }
        };

        if (document.body) {
          observeContentContainers();
        } else {
          document.addEventListener('DOMContentLoaded', observeContentContainers);
        }

        // Register for cleanup
        YouTubeUtils.cleanupManager.registerObserver(adSlotObserver);
      } catch (e) {
        if (AdBlocker.config.enableLogging) {
          console.warn('[AdBlocker] Failed to create adSlotObserver', e);
        }
      }

      const clickHandler = e => {
        const target = /** @type {EventTarget & HTMLElement} */ (e.target);
        if (target.dataset?.section === 'basic') {
          setTimeout(AdBlocker.addSettingsUI, 25);
        }
      };
      YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, {
        passive: true,
        capture: true,
      });

      // Initial skip attempt
      if (AdBlocker.config.enabled) {
        setTimeout(AdBlocker.skipAd, 200);
        // Also check for ad blocker warning popup
        setTimeout(AdBlocker.dismissAdBlockerWarning, 500);
      }
    },
  };

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

// --- MODULE: pip.js ---

// YouTube Picture-in-Picture settings
(function () {
  'use strict';

  /**
   * Translation helper - uses centralized i18n system
   * @param {string} key - Translation key
   * @param {Object} params - Interpolation parameters
   * @returns {string} Translated string
   */
  function t(key, params = {}) {
    try {
      if (typeof window !== 'undefined') {
        if (window.YouTubePlusI18n && typeof window.YouTubePlusI18n.t === 'function') {
          return window.YouTubePlusI18n.t(key, params);
        }
        if (window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function') {
          return window.YouTubeUtils.t(key, params);
        }
      }
    } catch {
      // Fallback to key if central i18n unavailable
    }
    return key;
  }

  /**
   * PiP settings configuration
   * @type {Object}
   * @property {boolean} enabled - Whether PiP is enabled
   * @property {Object} shortcut - Keyboard shortcut configuration
   * @property {string} storageKey - LocalStorage key for persistence
   */
  const pipSettings = {
    enabled: true,
    shortcut: { key: 'P', shiftKey: true, altKey: false, ctrlKey: false },
    storageKey: 'youtube_pip_settings',
  };

  const PIP_SESSION_KEY = 'youtube_plus_pip_session';

  /**
   * Get video element with validation
   * @returns {HTMLVideoElement|null} Video element or null if not found
   */
  const getVideoElement = () => {
    try {
      const candidate =
        (typeof YouTubeUtils?.querySelector === 'function' &&
          YouTubeUtils.querySelector('video')) ||
        document.querySelector('video');

      if (candidate && candidate.tagName && candidate.tagName.toLowerCase() === 'video') {
        return /** @type {HTMLVideoElement} */ (candidate);
      }

      return null;
    } catch (error) {
      console.error('[PiP] Error getting video element:', error);
      return null;
    }
  };

  /**
   * Wait for video metadata to load with timeout
   * @param {HTMLVideoElement} video - Video element
   * @returns {Promise<void>} Resolves when metadata is loaded
   */
  const waitForMetadata = video => {
    if (!video) {
      return Promise.reject(new Error('[PiP] Invalid video element'));
    }

    if (video.readyState >= 1 && !video.seeking) {
      return Promise.resolve();
    }

    return new Promise((resolve, reject) => {
      let settled = false;

      const cleanup = () => {
        video.removeEventListener('loadedmetadata', onLoaded);
        video.removeEventListener('error', onError);
        if (timeoutId) {
          clearTimeout(timeoutId);
        }
      };

      const onLoaded = () => {
        if (settled) return;
        settled = true;
        cleanup();
        resolve();
      };

      const onError = () => {
        if (settled) return;
        settled = true;
        cleanup();
        reject(new Error('[PiP] Video metadata failed to load'));
      };

      let timeoutId = setTimeout(() => {
        if (settled) return;
        settled = true;
        cleanup();
        reject(new Error('[PiP] Timed out waiting for video metadata'));
      }, 3000);

      const registeredTimeout = YouTubeUtils?.cleanupManager?.registerTimeout?.(timeoutId);
      if (registeredTimeout) {
        timeoutId = registeredTimeout;
      }

      video.addEventListener('loadedmetadata', onLoaded, { once: true });
      video.addEventListener('error', onError, { once: true });
    });
  };

  const setSessionActive = isActive => {
    try {
      if (isActive) {
        sessionStorage.setItem(PIP_SESSION_KEY, 'true');
      } else {
        sessionStorage.removeItem(PIP_SESSION_KEY);
      }
    } catch {}
  };

  const wasSessionActive = () => {
    try {
      return sessionStorage.getItem(PIP_SESSION_KEY) === 'true';
    } catch {
      return false;
    }
  };

  /**
   * Load settings from localStorage with validation
   * @returns {void}
   */
  const loadSettings = () => {
    try {
      const saved = localStorage.getItem(pipSettings.storageKey);
      if (!saved) return;

      const parsed = JSON.parse(saved);
      if (typeof parsed !== 'object' || parsed === null) {
        console.warn('[PiP] Invalid settings format');
        return;
      }

      // Validate and merge settings
      if (typeof parsed.enabled === 'boolean') {
        pipSettings.enabled = parsed.enabled;
      }

      // Validate shortcut object
      if (parsed.shortcut && typeof parsed.shortcut === 'object') {
        if (typeof parsed.shortcut.key === 'string' && parsed.shortcut.key.length > 0) {
          pipSettings.shortcut.key = parsed.shortcut.key;
        }
        if (typeof parsed.shortcut.shiftKey === 'boolean') {
          pipSettings.shortcut.shiftKey = parsed.shortcut.shiftKey;
        }
        if (typeof parsed.shortcut.altKey === 'boolean') {
          pipSettings.shortcut.altKey = parsed.shortcut.altKey;
        }
        if (typeof parsed.shortcut.ctrlKey === 'boolean') {
          pipSettings.shortcut.ctrlKey = parsed.shortcut.ctrlKey;
        }
      }
    } catch (e) {
      console.error('[PiP] Error loading settings:', e);
    }
  };

  /**
   * Save settings to localStorage with error handling
   * @returns {void}
   */
  const saveSettings = () => {
    try {
      const settingsToSave = {
        enabled: pipSettings.enabled,
        shortcut: pipSettings.shortcut,
      };
      localStorage.setItem(pipSettings.storageKey, JSON.stringify(settingsToSave));
    } catch (e) {
      console.error('[PiP] Error saving settings:', e);
    }
  };

  /**
   * Get current PiP element as HTMLVideoElement when available
   * @returns {HTMLVideoElement|null}
   */
  const getCurrentPiPElement = () => {
    const current = document.pictureInPictureElement;
    if (current && typeof current === 'object' && 'tagName' in current) {
      const tag = /** @type {{ tagName?: string }} */ (current).tagName;
      if (typeof tag === 'string' && tag.toLowerCase() === 'video') {
        return /** @type {HTMLVideoElement} */ (/** @type {unknown} */ (current));
      }
    }
    return null;
  };

  /**
   * Toggle Picture-in-Picture mode
   * @param {HTMLVideoElement} video - The video element
   * @returns {Promise<void>}
   */
  const togglePictureInPicture = async video => {
    if (!pipSettings.enabled || !video) return;

    try {
      const currentPiP = getCurrentPiPElement();

      if (currentPiP && currentPiP !== video) {
        await document.exitPictureInPicture();
        setSessionActive(false);
      }

      if (getCurrentPiPElement() === video) {
        await document.exitPictureInPicture();
        setSessionActive(false);
        return;
      }

      if (video.disablePictureInPicture) {
        throw new Error('Picture-in-Picture is disabled by the video element');
      }

      await waitForMetadata(video);

      await video.requestPictureInPicture();
      setSessionActive(true);
    } catch (error) {
      console.error('[YouTube+][PiP] Failed to toggle Picture-in-Picture:', error);
    }
  };

  /**
   * Add PiP settings UI to advanced settings modal
   * @returns {void}
   */
  const addPipSettingsToModal = () => {
    const advancedSection = YouTubeUtils.querySelector(
      '.ytp-plus-settings-section[data-section="advanced"]'
    );
    if (!advancedSection || YouTubeUtils.querySelector('.pip-settings-item')) return;

    const getSubmenuExpanded = () => {
      try {
        const raw = localStorage.getItem('ytp-plus-submenu-states');
        if (!raw) return null;
        const parsed = JSON.parse(raw);
        if (parsed && typeof parsed.pip === 'boolean') return parsed.pip;
      } catch {}
      return null;
    };
    const storedExpanded = getSubmenuExpanded();
    const initialExpanded = typeof storedExpanded === 'boolean' ? storedExpanded : true;

    // Add styles if they don't exist
    if (!document.getElementById('pip-styles')) {
      const styles = `
          .pip-shortcut-editor { display: flex; align-items: center; gap: 8px; }
          .pip-shortcut-editor select, #pip-key {background: rgba(34, 34, 34, var(--yt-header-bg-opacity)); color: var(--yt-spec-text-primary); border: 1px solid var(--yt-spec-10-percent-layer); border-radius: var(--yt-radius-sm); padding: 4px;}
        `;
      YouTubeUtils.StyleManager.add('pip-styles', styles);
    }

    // Enable/disable toggle
    const enableItem = document.createElement('div');
    enableItem.className =
      'ytp-plus-settings-item pip-settings-item ytp-plus-settings-item--with-submenu';
    enableItem.innerHTML = `
        <div>
          <label class="ytp-plus-settings-item-label" for="pip-enable-checkbox">${t(
            'pipTitle'
          )}</label>
          <div class="ytp-plus-settings-item-description">${t('pipDescription')}</div>
        </div>
        <div class="ytp-plus-settings-item-actions">
          <button
            type="button"
            class="ytp-plus-submenu-toggle"
            data-submenu="pip"
            aria-label="Toggle PiP submenu"
            aria-expanded="${initialExpanded ? 'true' : 'false'}"
            ${pipSettings.enabled ? '' : 'disabled'}
            style="display:${pipSettings.enabled ? 'inline-flex' : 'none'};"
          >
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="6 9 12 15 18 9"></polyline>
            </svg>
          </button>
          <input type="checkbox" class="ytp-plus-settings-checkbox" data-setting="enablePiP" id="pip-enable-checkbox" ${pipSettings.enabled ? 'checked' : ''}>
        </div>
      `;
    advancedSection.appendChild(enableItem);

    // Shortcut settings
    const submenuWrap = document.createElement('div');
    submenuWrap.className = 'pip-submenu';
    submenuWrap.dataset.submenu = 'pip';
    submenuWrap.style.display = pipSettings.enabled && initialExpanded ? 'block' : 'none';
    submenuWrap.style.marginLeft = '12px';
    submenuWrap.style.marginBottom = '12px';

    const submenuCard = document.createElement('div');
    submenuCard.className = 'glass-card';
    submenuCard.style.display = 'flex';
    submenuCard.style.flexDirection = 'column';
    submenuCard.style.gap = '8px';

    const shortcutItem = document.createElement('div');
    shortcutItem.className = 'ytp-plus-settings-item pip-shortcut-item';
    shortcutItem.style.display = 'flex';

    const { ctrlKey, altKey, shiftKey } = pipSettings.shortcut;
    const modifierValue =
      ctrlKey && altKey && shiftKey
        ? 'ctrl+alt+shift'
        : ctrlKey && altKey
          ? 'ctrl+alt'
          : ctrlKey && shiftKey
            ? 'ctrl+shift'
            : altKey && shiftKey
              ? 'alt+shift'
              : ctrlKey
                ? 'ctrl'
                : altKey
                  ? 'alt'
                  : shiftKey
                    ? 'shift'
                    : 'none';

    shortcutItem.innerHTML = `
        <div>
          <label class="ytp-plus-settings-item-label">${t('pipShortcutTitle')}</label>
          <div class="ytp-plus-settings-item-description">${t('pipShortcutDescription')}</div>
        </div>
        <div class="pip-shortcut-editor">
          <!-- hidden native select kept for compatibility -->
          <select id="pip-modifier-combo" style="display:none;">
            ${[
              'none',
              'ctrl',
              'alt',
              'shift',
              'ctrl+alt',
              'ctrl+shift',
              'alt+shift',
              'ctrl+alt+shift',
            ]
              .map(
                v =>
                  `<option value="${v}" ${v === modifierValue ? 'selected' : ''}>${
                    v === 'none'
                      ? t('none')
                      : v
                          .replace(/\+/g, '+')
                          .split('+')
                          .map(k => t(k.toLowerCase()))
                          .join('+')
                          .split('+')
                          .map(k => k.charAt(0).toUpperCase() + k.slice(1))
                          .join('+')
                  }</option>`
              )
              .join('')}
          </select>

          <div class="glass-dropdown" id="pip-modifier-dropdown" tabindex="0" role="listbox" aria-expanded="false">
            <button class="glass-dropdown__toggle" type="button" aria-haspopup="listbox">
              <span class="glass-dropdown__label">${
                modifierValue === 'none'
                  ? t('none')
                  : modifierValue
                      .replace(/\+/g, '+')
                      .split('+')
                      .map(k => t(k.toLowerCase()))
                      .map(k => k.charAt(0).toUpperCase() + k.slice(1))
                      .join('+')
              }</span>
              <svg class="glass-dropdown__chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
            </button>
            <ul class="glass-dropdown__list" role="presentation">
              ${[
                'none',
                'ctrl',
                'alt',
                'shift',
                'ctrl+alt',
                'ctrl+shift',
                'alt+shift',
                'ctrl+alt+shift',
              ]
                .map(v => {
                  const label =
                    v === 'none'
                      ? t('none')
                      : v
                          .replace(/\+/g, '+')
                          .split('+')
                          .map(k => t(k.toLowerCase()))
                          .map(k => k.charAt(0).toUpperCase() + k.slice(1))
                          .join('+');
                  const sel = v === modifierValue ? ' aria-selected="true"' : '';
                  return `<li class="glass-dropdown__item" data-value="${v}" role="option"${sel}>${label}</li>`;
                })
                .join('')}
            </ul>
          </div>

          <span>+</span>
          <input type="text" id="pip-key" value="${pipSettings.shortcut.key}" maxlength="1" style="width: 30px; text-align: center;">
        </div>
      `;
    submenuCard.appendChild(shortcutItem);
    submenuWrap.appendChild(submenuCard);
    advancedSection.appendChild(submenuWrap);

    // Initialize glass dropdown interactions for PiP selector
    const initPipDropdown = () => {
      const hidden = document.getElementById('pip-modifier-combo');
      const dropdown = document.getElementById('pip-modifier-dropdown');
      if (!hidden || !dropdown) return;

      const toggle = dropdown.querySelector('.glass-dropdown__toggle');
      const list = dropdown.querySelector('.glass-dropdown__list');
      const label = dropdown.querySelector('.glass-dropdown__label');
      let items = Array.from(list.querySelectorAll('.glass-dropdown__item'));
      let idx = items.findIndex(it => it.getAttribute('aria-selected') === 'true');
      if (idx < 0) idx = 0;

      const openList = () => {
        dropdown.setAttribute('aria-expanded', 'true');
        list.style.display = 'block';
        items = Array.from(list.querySelectorAll('.glass-dropdown__item'));
      };
      const closeList = () => {
        dropdown.setAttribute('aria-expanded', 'false');
        list.style.display = 'none';
      };

      toggle.addEventListener('click', () => {
        const expanded = dropdown.getAttribute('aria-expanded') === 'true';
        if (expanded) closeList();
        else openList();
      });

      document.addEventListener('click', e => {
        if (!dropdown.contains(e.target)) closeList();
      });

      // Arrow-key navigation and selection
      dropdown.addEventListener('keydown', e => {
        const expanded = dropdown.getAttribute('aria-expanded') === 'true';
        if (e.key === 'ArrowDown') {
          e.preventDefault();
          if (!expanded) openList();
          idx = Math.min(idx + 1, items.length - 1);
          items.forEach(it => it.removeAttribute('aria-selected'));
          items[idx].setAttribute('aria-selected', 'true');
          items[idx].scrollIntoView({ block: 'nearest' });
        } else if (e.key === 'ArrowUp') {
          e.preventDefault();
          if (!expanded) openList();
          idx = Math.max(idx - 1, 0);
          items.forEach(it => it.removeAttribute('aria-selected'));
          items[idx].setAttribute('aria-selected', 'true');
          items[idx].scrollIntoView({ block: 'nearest' });
        } else if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          if (!expanded) {
            openList();
            return;
          }
          const it = items[idx];
          if (it) {
            hidden.value = it.dataset.value;
            hidden.dispatchEvent(new Event('change', { bubbles: true }));
            label.textContent = it.textContent;
            closeList();
          }
        } else if (e.key === 'Escape') {
          closeList();
        }
      });

      list.addEventListener('click', e => {
        const it = e.target.closest('.glass-dropdown__item');
        if (!it) return;
        const val = it.dataset.value;
        hidden.value = val;
        list
          .querySelectorAll('.glass-dropdown__item')
          .forEach(li => li.removeAttribute('aria-selected'));
        it.setAttribute('aria-selected', 'true');
        label.textContent = it.textContent;
        hidden.dispatchEvent(new Event('change', { bubbles: true }));
        closeList();
      });
    };

    setTimeout(initPipDropdown, 0);

    // Event listeners
    document.getElementById('pip-enable-checkbox').addEventListener('change', e => {
      const target = /** @type {EventTarget & HTMLInputElement} */ (e.target);
      pipSettings.enabled = target.checked;
      const submenuToggle = enableItem.querySelector(
        '.ytp-plus-submenu-toggle[data-submenu="pip"]'
      );
      if (submenuToggle instanceof HTMLElement) {
        if (pipSettings.enabled) {
          const stored = getSubmenuExpanded();
          const nextExpanded = typeof stored === 'boolean' ? stored : true;
          submenuToggle.removeAttribute('disabled');
          submenuToggle.style.display = 'inline-flex';
          submenuToggle.setAttribute('aria-expanded', nextExpanded ? 'true' : 'false');
          submenuWrap.style.display = nextExpanded ? 'block' : 'none';
        } else {
          submenuToggle.setAttribute('disabled', '');
          submenuToggle.style.display = 'none';
          submenuWrap.style.display = 'none';
        }
      }
      saveSettings();
    });

    document.getElementById('pip-modifier-combo').addEventListener('change', e => {
      const target = /** @type {EventTarget & HTMLSelectElement} */ (e.target);
      const value = target.value;
      pipSettings.shortcut.ctrlKey = value.includes('ctrl');
      pipSettings.shortcut.altKey = value.includes('alt');
      pipSettings.shortcut.shiftKey = value.includes('shift');
      saveSettings();
    });

    document.getElementById('pip-key').addEventListener('input', e => {
      const target = /** @type {EventTarget & HTMLInputElement} */ (e.target);
      if (target.value) {
        pipSettings.shortcut.key = target.value.toUpperCase();
        saveSettings();
      }
    });

    document.getElementById('pip-key').addEventListener('keydown', e => e.stopPropagation());
  };

  // Initialize
  loadSettings();

  // Event listeners
  document.addEventListener('keydown', e => {
    if (!pipSettings.enabled) return;
    const { shiftKey, altKey, ctrlKey, key } = pipSettings.shortcut;
    if (
      e.shiftKey === shiftKey &&
      e.altKey === altKey &&
      e.ctrlKey === ctrlKey &&
      e.key.toUpperCase() === key
    ) {
      const video = getVideoElement();
      if (video) {
        void togglePictureInPicture(video);
      }
      e.preventDefault();
    }
  });

  window.addEventListener('storage', e => {
    if (e.key === pipSettings.storageKey) {
      loadSettings();
    }
  });

  window.addEventListener('load', () => {
    if (!pipSettings.enabled || !wasSessionActive() || document.pictureInPictureElement) {
      return;
    }

    const resumePiP = () => {
      const video = getVideoElement();
      if (!video) return;

      togglePictureInPicture(video).catch(() => {
        // If resume fails we reset the session flag to avoid loops
        setSessionActive(false);
      });
    };

    const ensureCleanup = handler => {
      if (!handler) return;
      try {
        document.removeEventListener('pointerdown', handler, true);
      } catch {}
    };

    const cleanupListeners = () => {
      ensureCleanup(pointerListener);
      ensureCleanup(keyListener);
    };

    const pointerListener = () => {
      cleanupListeners();
      resumePiP();
    };

    const keyListener = () => {
      cleanupListeners();
      resumePiP();
    };

    document.addEventListener('pointerdown', pointerListener, { once: true, capture: true });
    document.addEventListener('keydown', keyListener, { once: true, capture: true });
  });

  // Settings modal integration — use event instead of MutationObserver
  document.addEventListener('youtube-plus-settings-modal-opened', () => {
    setTimeout(addPipSettingsToModal, 100);
  });

  document.addEventListener('leavepictureinpicture', () => {
    setSessionActive(false);
  });

  const clickHandler = e => {
    const target = /** @type {EventTarget & HTMLElement} */ (e.target);
    if (target.classList && target.classList.contains('ytp-plus-settings-nav-item')) {
      if (target.dataset?.section === 'advanced') {
        setTimeout(addPipSettingsToModal, 50);
      }
    }
  };
  YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, true);
})();

// --- MODULE: timecode.js ---

// YouTube Timecode Panel
(function () {
  'use strict';

  // DOM Cache Helper - reduces repeated queries
  const getCache = () => typeof window !== 'undefined' && window.YouTubeDOMCache;
  /**
   * Query single element with optional caching
   * @param {string} sel - CSS selector
   * @param {Element|Document} [ctx] - Context element
   * @returns {Element|null}
   */
  const $ = (sel, ctx) =>
    getCache()?.querySelector(sel, ctx) || (ctx || document).querySelector(sel);
  /**
   * Query all elements with optional caching
   * @param {string} sel - CSS selector
   * @param {Element|Document} [ctx] - Context element
   * @returns {Element[]}
   */
  const $$ = (sel, ctx) =>
    getCache()?.querySelectorAll(sel, ctx) || Array.from((ctx || document).querySelectorAll(sel));
  /**
   * Get element by ID with optional caching
   * @param {string} id - Element ID
   * @returns {Element|null}
   */
  const byId = id => getCache()?.getElementById(id) || document.getElementById(id);

  if (window.location.hostname !== 'www.youtube.com' || window.frameElement) {
    return;
  }

  // Prevent multiple initializations
  if (window._timecodeModuleInitialized) return;
  window._timecodeModuleInitialized = true;

  /**
   * Translation helper - uses centralized i18n system
   * @param {string} key - Translation key
   * @param {Object} params - Interpolation parameters
   * @returns {string} Translated string
   */
  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    // Fallback for initialization phase
    return key || '';
  };

  // Configuration
  const config = {
    enabled: true,
    autoDetect: true,
    shortcut: { key: 'T', shiftKey: true, altKey: false, ctrlKey: false },
    storageKey: 'youtube_timecode_settings',
    autoSave: true,
    autoTrackPlayback: true,
    panelPosition: null,
    export: true,
  };

  // State management
  const state = {
    timecodes: new Map(),
    dom: {},
    isReloading: false,
    activeIndex: null,
    trackingId: 0,
    dragging: false,
    editingIndex: null,
    resizeListenerKey: null,
  };

  let initStarted = false;

  const isRelevantRoute = () => {
    try {
      return location.pathname === '/watch';
    } catch {
      return false;
    }
  };

  const scheduleInitRetry = () => {
    const timeoutId = setTimeout(init, 250);
    YouTubeUtils.cleanupManager?.registerTimeout?.(timeoutId);
  };

  // Utilities
  /**
   * Load settings from localStorage with error handling
   * @returns {void}
   */
  const loadSettings = () => {
    try {
      const saved = localStorage.getItem(config.storageKey);
      if (!saved) return;

      const parsed = JSON.parse(saved);
      if (typeof parsed !== 'object' || parsed === null) {
        console.warn('[Timecode] Invalid settings format');
        return;
      }

      // Validate and merge settings
      if (typeof parsed.enabled === 'boolean') {
        config.enabled = parsed.enabled;
      }
      if (typeof parsed.autoDetect === 'boolean') {
        config.autoDetect = parsed.autoDetect;
      }
      if (typeof parsed.autoSave === 'boolean') {
        config.autoSave = parsed.autoSave;
      }
      if (typeof parsed.autoTrackPlayback === 'boolean') {
        config.autoTrackPlayback = parsed.autoTrackPlayback;
      }
      if (typeof parsed.export === 'boolean') {
        config.export = parsed.export;
      }

      // Validate shortcut object
      if (parsed.shortcut && typeof parsed.shortcut === 'object') {
        if (typeof parsed.shortcut.key === 'string') {
          config.shortcut.key = parsed.shortcut.key;
        }
        if (typeof parsed.shortcut.shiftKey === 'boolean') {
          config.shortcut.shiftKey = parsed.shortcut.shiftKey;
        }
        if (typeof parsed.shortcut.altKey === 'boolean') {
          config.shortcut.altKey = parsed.shortcut.altKey;
        }
        if (typeof parsed.shortcut.ctrlKey === 'boolean') {
          config.shortcut.ctrlKey = parsed.shortcut.ctrlKey;
        }
      }

      // Validate panel position
      if (parsed.panelPosition && typeof parsed.panelPosition === 'object') {
        const { left, top } = parsed.panelPosition;
        if (
          typeof left === 'number' &&
          typeof top === 'number' &&
          !isNaN(left) &&
          !isNaN(top) &&
          left >= 0 &&
          top >= 0
        ) {
          config.panelPosition = { left, top };
        }
      }
    } catch (error) {
      console.error('[Timecode] Error loading settings:', error);
    }
  };

  /**
   * Save settings to localStorage with error handling
   * @returns {void}
   */
  const saveSettings = () => {
    try {
      const settingsToSave = {
        enabled: config.enabled,
        autoDetect: config.autoDetect,
        shortcut: config.shortcut,
        autoSave: config.autoSave,
        autoTrackPlayback: config.autoTrackPlayback,
        panelPosition: config.panelPosition,
        export: config.export,
      };
      localStorage.setItem(config.storageKey, JSON.stringify(settingsToSave));
    } catch (error) {
      console.error('[Timecode] Error saving settings:', error);
    }
  };

  /**
   * Clamp panel position within viewport bounds
   * @param {HTMLElement} panel - Panel element
   * @param {number} left - Desired left position
   * @param {number} top - Desired top position
   * @returns {{left: number, top: number}} Clamped position
   */
  const clampPanelPosition = (panel, left, top) => {
    try {
      if (!panel || !(panel instanceof HTMLElement)) {
        console.warn('[Timecode] Invalid panel element');
        return { left: 0, top: 0 };
      }

      // Validate input coordinates
      if (typeof left !== 'number' || typeof top !== 'number' || isNaN(left) || isNaN(top)) {
        console.warn('[Timecode] Invalid position coordinates');
        return { left: 0, top: 0 };
      }

      const rect = panel.getBoundingClientRect();
      const width = rect.width || panel.offsetWidth || 0;
      const height = rect.height || panel.offsetHeight || 0;

      const maxLeft = Math.max(0, window.innerWidth - width);
      const maxTop = Math.max(0, window.innerHeight - height);

      return {
        left: Math.min(Math.max(0, left), maxLeft),
        top: Math.min(Math.max(0, top), maxTop),
      };
    } catch (error) {
      console.error('[Timecode] Error clamping panel position:', error);
      return { left: 0, top: 0 };
    }
  };

  /**
   * Save panel position to settings
   * @param {number} left - Left position
   * @param {number} top - Top position
   * @returns {void}
   */
  /**
   * Save panel position to configuration and localStorage
   * @param {number} left - X coordinate in pixels
   * @param {number} top - Y coordinate in pixels
   * @returns {void}
   */
  const savePanelPosition = (left, top) => {
    try {
      if (typeof left !== 'number' || typeof top !== 'number' || isNaN(left) || isNaN(top)) {
        console.warn('[Timecode] Invalid position coordinates for saving');
        return;
      }
      config.panelPosition = { left, top };
      saveSettings();
    } catch (error) {
      console.error('[Timecode] Error saving panel position:', error);
    }
  };

  /**
   * Apply saved panel position to a panel element
   * @param {HTMLElement} panel - The panel element to position
   * @returns {void}
   */
  const applySavedPanelPosition = panel => {
    if (!panel || !config.panelPosition) return;

    requestAnimationFrame(() => {
      const { left, top } = clampPanelPosition(
        panel,
        config.panelPosition.left,
        config.panelPosition.top
      );
      panel.style.left = `${left}px`;
      panel.style.top = `${top}px`;
      panel.style.right = 'auto';
    });
  };

  /**
   * Display a notification message to the user
   * @param {string} message - The message to display
   * @param {number} [duration=2000] - Duration in milliseconds
   * @param {string} [type='info'] - Notification type (info, success, warning, error)
   * @returns {void}
   */
  const showNotification = (message, duration = 2000, type = 'info') => {
    YouTubeUtils.NotificationManager.show(message, { duration, type });
  };

  /**
   * Format seconds into HH:MM:SS or MM:SS time string
   * @param {number} seconds - Number of seconds to format
   * @returns {string} Formatted time string
   */
  const formatTime = seconds => {
    if (isNaN(seconds)) return '00:00';
    seconds = Math.round(seconds);
    const h = Math.floor(seconds / 3600);
    const m = Math.floor((seconds % 3600) / 60);
    const s = seconds % 60;
    return h > 0
      ? `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
      : `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
  };

  /**
   * Remove duplicate text patterns from a string
   * @param {string} text - Text to deduplicate
   * @returns {string} Deduplicated text
   */
  const removeDuplicateText = text => {
    if (!text || text.length < 10) return text;

    let cleaned = text.trim();

    // Remove trailing ellipsis and truncation markers
    cleaned = cleaned.replace(/\s*\.{2,}$/, '').replace(/\s*…$/, '');

    const words = cleaned.split(/\s+/);
    if (words.length < 4) return cleaned; // Too short to have meaningful duplicates

    // Try exact half split first
    const half = Math.floor(words.length / 2);
    if (half >= 2) {
      const firstHalf = words.slice(0, half).join(' ');
      const secondHalf = words.slice(half, half * 2).join(' ');
      if (firstHalf === secondHalf) {
        return firstHalf;
      }
    }

    // Try sliding window approach for partial duplicates
    // Search for the longest repeating pattern
    const minPatternLength = Math.max(2, Math.floor(words.length / 4));
    const maxPatternLength = Math.floor(words.length / 2);

    for (let len = maxPatternLength; len >= minPatternLength; len--) {
      const pattern = words.slice(0, len).join(' ');
      const patternWords = words.slice(0, len);

      // Check if this pattern appears again anywhere in the text
      for (let offset = 1; offset <= words.length - len; offset++) {
        let matchCount = 0;
        let partialWordMatch = false;
        const testWords = words.slice(offset, Math.min(offset + len, words.length));

        for (let i = 0; i < patternWords.length; i++) {
          const patternWord = patternWords[i];
          const testWord = testWords[i];

          if (!testWord) break;

          // Exact match
          if (patternWord === testWord) {
            matchCount++;
          }
          // Partial match (for truncated words like "сте..." vs "стекла")
          else if (testWord.length >= 3 && patternWord.startsWith(testWord)) {
            matchCount += 0.8; // Partial credit
            partialWordMatch = true;
          } else if (patternWord.length >= 3 && testWord.startsWith(patternWord)) {
            matchCount += 0.8; // Partial credit
            partialWordMatch = true;
          }
        }

        // If 70%+ of the pattern matches (allowing for partial words), it's a duplicate
        const similarity = matchCount / patternWords.length;
        const effectiveMatches = Math.floor(matchCount);
        if (
          similarity >= 0.7 &&
          (effectiveMatches >= 2 || (matchCount >= 1.5 && partialWordMatch))
        ) {
          return pattern;
        }
      }
    }

    return cleaned;
  }; /**
   * Parse time string to seconds with validation
   * @param {string} timeStr - Time string (MM:SS or HH:MM:SS)
   * @returns {number|null} Seconds or null if invalid
   */
  const parseTime = timeStr => {
    try {
      if (!timeStr || typeof timeStr !== 'string') return null;

      const str = timeStr.trim();
      if (str.length === 0 || str.length > 12) return null; // Sanity check

      // Handle HH:MM:SS format
      let match = str.match(/^(\d+):(\d{1,2}):(\d{2})$/);
      if (match) {
        const [, h, m, s] = match.map(Number);
        // Validate ranges
        if (isNaN(h) || isNaN(m) || isNaN(s)) return null;
        if (m >= 60 || s >= 60 || h < 0 || m < 0 || s < 0) return null;
        const total = h * 3600 + m * 60 + s;
        // Sanity check: max 24 hours
        return total <= 86400 ? total : null;
      }

      // Handle MM:SS format
      match = str.match(/^(\d{1,2}):(\d{2})$/);
      if (match) {
        const [, m, s] = match.map(Number);
        // Validate ranges
        if (isNaN(m) || isNaN(s)) return null;
        if (m >= 60 || s >= 60 || m < 0 || s < 0) return null;
        return m * 60 + s;
      }

      return null;
    } catch (error) {
      console.error('[Timecode] Error parsing time:', error);
      return null;
    }
  };

  /**
   * Extract timecodes from text with validation
   * @param {string} text - Text containing timecodes
   * @returns {Array<{time: number, label: string, originalText: string}>} Extracted timecodes
   */
  const extractTimecodes = text => {
    try {
      if (!text || typeof text !== 'string') return [];

      // Security: limit text length to prevent DoS
      if (text.length > 50000) {
        console.warn('[Timecode] Text too long, truncating');
        text = text.substring(0, 50000);
      }

      const timecodes = [];
      const seen = new Set();
      const patterns = [
        /(\d{1,2}:\d{2}(?::\d{2})?)\s*[-–—]\s*(.+?)$/gm,
        /^(\d{1,2}:\d{2}(?::\d{2})?)\s+(.+?)$/gm,
        /(\d{1,2}:\d{2}(?::\d{2})?)\s*[-–—:]\s*([^\n\r]{1,100}?)(?=\s*\d{1,2}:\d{2}|\s*$)/g,
        /(\d{1,2}:\d{2}(?::\d{2})?)\s*[–—-]\s*([^\n]+)/gm,
        /^(\d{1,2}:\d{2}(?::\d{2})?)\s*(.+)$/gm,
      ];

      for (const pattern of patterns) {
        let match;
        let iterations = 0;
        const maxIterations = 1000; // Prevent infinite loops

        while ((match = pattern.exec(text)) !== null && iterations++ < maxIterations) {
          const time = parseTime(match[1]);
          if (time !== null && !seen.has(time)) {
            seen.add(time);
            // Sanitize label text - only use match[2] if it exists and is not empty
            let label = (match[2] || '')
              .trim()
              .replace(/^\d+[\.\)]\s*/, '')
              .replace(/\s+/g, ' ') // Normalize whitespace
              .substring(0, 100); // Limit label length

            // Debug logging
            const originalLabel = label;

            // Remove potentially dangerous characters
            label = label.replace(/[<>\"']/g, '');

            // Remove duplicate text in label
            label = removeDuplicateText(label);

            if (originalLabel !== label && label.length > 0) {
              console.warn('[Timecode] Description deduplicated:', originalLabel, '->', label);
            }

            // Only add if we have actual content (time is always added, label can be empty)
            timecodes.push({ time, label: label || '', originalText: match[1] });
          }
        }

        if (iterations >= maxIterations) {
          console.warn('[Timecode] Maximum iterations reached during extraction');
        }
      }

      return timecodes.sort((a, b) => a.time - b.time);
    } catch (error) {
      console.error('[Timecode] Error extracting timecodes:', error);
      return [];
    }
  };

  const DESCRIPTION_SELECTORS = [
    '#description-inline-expander yt-attributed-string',
    '#description-inline-expander yt-formatted-string',
    '#description-inline-expander ytd-text-inline-expander',
    '#description-inline-expander .yt-core-attributed-string',
    '#description ytd-text-inline-expander',
    '#description ytd-expandable-video-description-body-renderer',
    '#description.ytd-watch-metadata yt-formatted-string',
    '#description.ytd-watch-metadata #description-inline-expander',
    '#tab-info ytd-expandable-video-description-body-renderer yt-formatted-string',
    '#tab-info ytd-expandable-video-description-body-renderer yt-attributed-string',
    '#structured-description ytd-text-inline-expander',
    '#structured-description yt-formatted-string',
    'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"] yt-formatted-string',
    'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"] yt-attributed-string',
    'ytd-watch-metadata #description',
    'ytd-watch-metadata #description-inline-expander',
    '#description',
  ];

  const DESCRIPTION_SELECTOR_COMBINED = DESCRIPTION_SELECTORS.join(',');

  const DESCRIPTION_EXPANDERS = [
    '#description-inline-expander yt-button-shape button',
    '#description-inline-expander tp-yt-paper-button#expand',
    '#description-inline-expander tp-yt-paper-button[aria-label]',
    'ytd-watch-metadata #description-inline-expander yt-button-shape button',
    'ytd-text-inline-expander[collapsed] yt-button-shape button',
    'ytd-text-inline-expander[collapsed] tp-yt-paper-button#expand',
    'ytd-expandable-video-description-body-renderer #expand',
    'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-macro-markers-description-chapters"] #expand',
  ];

  /**
   * Sleep/delay utility using Promises
   * @param {number} [ms=250] - Milliseconds to wait
   * @returns {Promise<void>}
   */
  const sleep = (ms = 250) => new Promise(resolve => setTimeout(resolve, ms));

  /**
   * Collect and concatenate text from video description using multiple selectors
   * @returns {string} Concatenated description text
   */
  const collectDescriptionText = () => {
    const snippets = [];
    DESCRIPTION_SELECTORS.forEach(selector => {
      $$(selector).forEach(node => {
        const text = node?.textContent?.trim();
        if (text) {
          snippets.push(text);
        }
      });
    });
    return snippets.join('\n');
  };

  // Collect visible comments text (top-level few comments) to search for timecodes
  const COMMENT_SELECTORS = [
    'ytd-comment-thread-renderer #content-text',
    'ytd-comment-renderer #content-text',
    'ytd-comment-thread-renderer yt-formatted-string#content-text',
    'ytd-comment-renderer yt-formatted-string#content-text',
    '#comments ytd-comment-thread-renderer #content-text',
  ];

  /**
   * Collect text from visible comments to search for timecodes
   * @param {number} [maxComments=30] - Maximum number of comments to collect
   * @returns {string} Concatenated comments text
   */
  const collectCommentsText = (maxComments = 30) => {
    try {
      const snippets = [];
      for (const sel of COMMENT_SELECTORS) {
        $$(sel).forEach(node => {
          if (snippets.length >= maxComments) return;
          const text = node?.textContent?.trim();
          if (text) snippets.push(text);
        });
        if (snippets.length >= maxComments) break;
      }
      return snippets.join('\n');
    } catch (error) {
      YouTubeUtils.logError('TimecodePanel', 'collectCommentsText failed', error);
      return '';
    }
  };

  const expandDescriptionIfNeeded = async () => {
    for (const selector of DESCRIPTION_EXPANDERS) {
      const button = $(selector);
      if (!button) continue;

      const ariaExpanded = button.getAttribute('aria-expanded');
      if (ariaExpanded === 'true') return false;

      const ariaLabel = button.getAttribute('aria-label')?.toLowerCase();
      if (ariaLabel && ariaLabel.includes('less')) return false;

      if (button.offsetParent !== null) {
        try {
          /** @type {HTMLElement} */ (button).click();
          await sleep(400);
          return true;
        } catch (error) {
          console.warn('[Timecode] Failed to click expand button:', error);
        }
      }
    }

    const inlineExpander = $('ytd-text-inline-expander[collapsed]');
    if (inlineExpander) {
      try {
        inlineExpander.removeAttribute('collapsed');
      } catch (error) {
        YouTubeUtils.logError('TimecodePanel', 'Failed to expand description', error);
      }
      await sleep(300);
      return true;
    }

    return false;
  };

  const ensureDescriptionReady = async () => {
    const initialText = collectDescriptionText();
    if (initialText) return;

    const maxAttempts = 3;
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      try {
        await YouTubeUtils.waitForElement(DESCRIPTION_SELECTOR_COMBINED, 1500);
      } catch {
        // Continue trying
      }

      await sleep(200);
      const expanded = await expandDescriptionIfNeeded();

      await sleep(expanded ? 500 : 200);
      const text = collectDescriptionText();

      if (text && text.length > initialText.length) {
        return;
      }
    }
  };
  const getCurrentVideoId = () => new URLSearchParams(window.location.search).get('v');

  // Detection
  const detectTimecodes = async (options = {}) => {
    const { force = false } = options;

    if (!config.enabled) return [];
    if (!force && !config.autoDetect) return [];

    const videoId = getCurrentVideoId();
    if (!videoId) return [];

    const cacheKey = `detect_${videoId}`;
    if (!force && state.timecodes.has(cacheKey)) {
      const cached = state.timecodes.get(cacheKey);
      if (Array.isArray(cached) && cached.length) {
        return cached;
      }
      state.timecodes.delete(cacheKey);
    }

    await ensureDescriptionReady();

    const uniqueMap = new Map();
    const descriptionText = collectDescriptionText();

    if (descriptionText) {
      const extracted = extractTimecodes(descriptionText);
      extracted.forEach(tc => {
        if (tc.time >= 0) {
          uniqueMap.set(tc.time.toString(), tc);
        }
      });
    }

    // Get native chapters
    const chapters = getYouTubeChapters();

    chapters.forEach(chapter => {
      if (chapter.time >= 0) {
        const key = chapter.time.toString();
        const existing = uniqueMap.get(key);
        // Prefer chapter label if existing label is empty or duplicate
        if (existing && chapter.label && chapter.label.length > existing.label.length) {
          uniqueMap.set(key, { ...existing, label: chapter.label, isChapter: true });
        } else if (!existing) {
          uniqueMap.set(key, chapter);
        } else {
          // Mark existing as chapter
          uniqueMap.set(key, { ...existing, isChapter: true });
        }
      }
    });

    // If no timecodes from description/chapters, try scanning visible comments
    if (uniqueMap.size === 0) {
      try {
        const commentsText = collectCommentsText();
        if (commentsText) {
          const extractedComments = extractTimecodes(commentsText);
          extractedComments.forEach(tc => {
            if (tc.time >= 0) uniqueMap.set(tc.time.toString(), tc);
          });
        }
      } catch (error) {
        YouTubeUtils.logError('TimecodePanel', 'Comment scanning failed', error);
      }
    }

    const result = Array.from(uniqueMap.values()).sort((a, b) => a.time - b.time);
    const hadExistingItems = state.dom.list?.childElementCount > 0;

    if (result.length > 0) {
      updateTimecodePanel(result);
      state.timecodes.set(cacheKey, result);
      if (config.autoSave) saveTimecodesToStorage(result);
    } else {
      if (force || !hadExistingItems) {
        updateTimecodePanel([]);
      }
      if (force) {
        state.timecodes.delete(cacheKey);
      }
    }

    return result;
  };

  /**
   * Reload timecodes by re-detecting them from the current video
   * @param {HTMLElement|null} [buttonOverride=null] - Optional reload button element
   * @returns {Promise<void>}
   */
  const reloadTimecodes = async (buttonOverride = null) => {
    const button = buttonOverride || state.dom.reloadButton || byId('timecode-reload');

    if (state.isReloading || !config.enabled) return;

    state.isReloading = true;
    if (button) {
      button.disabled = true;
      button.classList.add('loading');
    }

    try {
      const result = await detectTimecodes({ force: true });

      if (Array.isArray(result) && result.length) {
        showNotification(t('foundTimecodes').replace('{count}', result.length));
      } else {
        updateTimecodePanel([]);
        showNotification(t('noTimecodesFound'));
      }
    } catch (error) {
      YouTubeUtils.logError('TimecodePanel', 'Reload failed', error);
      showNotification(t('reloadError'));
    } finally {
      if (button) {
        button.disabled = false;
        button.classList.remove('loading');
      }
      state.isReloading = false;
    }
  };

  /**
   * Extract chapter markers from YouTube's native chapter system
   * @returns {Array<{time: number, label: string, isChapter: boolean}>} Array of chapter objects
   */
  const getYouTubeChapters = () => {
    // Расширенный поиск глав/эпизодов
    const selectors = [
      'ytd-macro-markers-list-item-renderer',
      'ytd-chapter-renderer',
      'ytd-engagement-panel-section-list-renderer[target-id*="description-chapters"] ytd-macro-markers-list-item-renderer',
      'ytd-engagement-panel-section-list-renderer[target-id*="description-chapters"] #details',
      '#structured-description ytd-horizontal-card-list-renderer ytd-macro-markers-list-item-renderer',
    ];

    const items = $$(selectors.join(', '));
    const chapters = new Map();

    items.forEach(item => {
      // Попробуем разные способы извлечения времени и заголовка
      const timeSelectors = ['.time-info', '.timestamp', '#time', 'span[id*="time"]'];
      const titleSelectors = ['.marker-title', '.chapter-title', '#details', 'h4', '.title'];

      let timeText = null;
      for (const sel of timeSelectors) {
        const el = item.querySelector(sel);
        if (el?.textContent) {
          timeText = el.textContent;
          break;
        }
      }

      let titleText = null;
      for (const sel of titleSelectors) {
        const el = item.querySelector(sel);
        if (el?.textContent) {
          titleText = el.textContent;
          break;
        }
      }

      if (timeText) {
        const time = parseTime(timeText.trim());
        if (time !== null) {
          // Очищаем заголовок от лишних пробелов и переносов строк
          let cleanTitle = titleText?.trim().replace(/\s+/g, ' ') || '';

          // Debug logging
          if (cleanTitle && cleanTitle.length > 0) {
            console.warn('[Timecode Debug] Raw chapter title:', cleanTitle);
          }

          // Remove time prefix if present in label
          cleanTitle = cleanTitle.replace(/^\d{1,2}:\d{2}(?::\d{2})?\s*[-–—:]?\s*/, '');

          // Remove duplicate text (some YouTube chapters repeat the title)
          const deduplicated = removeDuplicateText(cleanTitle);

          if (cleanTitle !== deduplicated) {
            console.warn('[Timecode] Removed duplicate:', cleanTitle, '->', deduplicated);
          }

          cleanTitle = deduplicated;
          chapters.set(time.toString(), {
            time,
            label: cleanTitle,
            isChapter: true,
          });
        }
      }
    });
    const result = Array.from(chapters.values()).sort((a, b) => a.time - b.time);
    return result;
  };

  // Settings panel
  const addTimecodePanelSettings = () => {
    const advancedSection = YouTubeUtils.querySelector
      ? YouTubeUtils.querySelector('.ytp-plus-settings-section[data-section="advanced"]')
      : $('.ytp-plus-settings-section[data-section="advanced"]');
    if (
      !advancedSection ||
      (YouTubeUtils.querySelector
        ? YouTubeUtils.querySelector('.timecode-settings-item')
        : $('.timecode-settings-item'))
    ) {
      return;
    }

    const getSubmenuExpanded = () => {
      try {
        const raw = localStorage.getItem('ytp-plus-submenu-states');
        if (!raw) return null;
        const parsed = JSON.parse(raw);
        if (parsed && typeof parsed.timecode === 'boolean') return parsed.timecode;
      } catch {}
      return null;
    };
    const storedExpanded = getSubmenuExpanded();
    const initialExpanded = typeof storedExpanded === 'boolean' ? storedExpanded : true;

    const { ctrlKey, altKey, shiftKey } = config.shortcut;
    const modifierValue =
      [
        ctrlKey && altKey && shiftKey && 'ctrl+alt+shift',
        ctrlKey && altKey && 'ctrl+alt',
        ctrlKey && shiftKey && 'ctrl+shift',
        altKey && shiftKey && 'alt+shift',
        ctrlKey && 'ctrl',
        altKey && 'alt',
        shiftKey && 'shift',
      ].find(Boolean) || 'none';

    const enableDiv = document.createElement('div');
    enableDiv.className =
      'ytp-plus-settings-item timecode-settings-item ytp-plus-settings-item--with-submenu';
    enableDiv.innerHTML = `
        <div>
          <label class="ytp-plus-settings-item-label" for="timecode-enable-checkbox">${t(
            'enableTimecode'
          )}</label>
          <div class="ytp-plus-settings-item-description">${t('enableDescription')}</div>
        </div>
        <div class="ytp-plus-settings-item-actions">
          <button
            type="button"
            class="ytp-plus-submenu-toggle"
            data-submenu="timecode"
            aria-label="Toggle timecode submenu"
            aria-expanded="${initialExpanded ? 'true' : 'false'}"
            ${config.enabled ? '' : 'disabled'}
            style="display:${config.enabled ? 'inline-flex' : 'none'};"
          >
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="6 9 12 15 18 9"></polyline>
            </svg>
          </button>
          <input type="checkbox" id="timecode-enable-checkbox" class="ytp-plus-settings-checkbox" data-setting="enabled" ${
            config.enabled ? 'checked' : ''
          }>
        </div>
      `;

    const submenuWrap = document.createElement('div');
    submenuWrap.className = 'timecode-submenu';
    submenuWrap.dataset.submenu = 'timecode';
    submenuWrap.style.display = config.enabled && initialExpanded ? 'block' : 'none';
    submenuWrap.style.marginLeft = '12px';
    submenuWrap.style.marginBottom = '12px';

    const submenuCard = document.createElement('div');
    submenuCard.className = 'glass-card';
    submenuCard.style.display = 'flex';
    submenuCard.style.flexDirection = 'column';
    submenuCard.style.gap = '8px';

    const shortcutDiv = document.createElement('div');
    shortcutDiv.className = 'ytp-plus-settings-item timecode-settings-item timecode-shortcut-item';
    shortcutDiv.style.display = 'flex';
    shortcutDiv.innerHTML = `
        <div>
          <label class="ytp-plus-settings-item-label">${t('keyboardShortcut')}</label>
          <div class="ytp-plus-settings-item-description">${t('shortcutDescription')}</div>
        </div>
        <div style="display: flex; align-items: center; gap: 8px;">
          <!-- Hidden native select kept for programmatic compatibility -->
          <select id="timecode-modifier-combo" style="display:none;">
            ${[
              'none',
              'ctrl',
              'alt',
              'shift',
              'ctrl+alt',
              'ctrl+shift',
              'alt+shift',
              'ctrl+alt+shift',
            ]
              .map(
                v =>
                  `<option value="${v}" ${v === modifierValue ? 'selected' : ''}>${
                    v === 'none'
                      ? 'None'
                      : v
                          .split('+')
                          .map(k => k.charAt(0).toUpperCase() + k.slice(1))
                          .join('+')
                  }</option>`
              )
              .join('')}
          </select>

          <div class="glass-dropdown" id="timecode-modifier-dropdown" tabindex="0" role="listbox" aria-expanded="false">
            <button class="glass-dropdown__toggle" type="button" aria-haspopup="listbox">
              <span class="glass-dropdown__label">${
                modifierValue === 'none'
                  ? 'None'
                  : modifierValue
                      .split('+')
                      .map(k => k.charAt(0).toUpperCase() + k.slice(1))
                      .join('+')
              }</span>
              <svg class="glass-dropdown__chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
            </button>
            <ul class="glass-dropdown__list" role="presentation">
              ${[
                'none',
                'ctrl',
                'alt',
                'shift',
                'ctrl+alt',
                'ctrl+shift',
                'alt+shift',
                'ctrl+alt+shift',
              ]
                .map(v => {
                  const label =
                    v === 'none'
                      ? 'None'
                      : v
                          .split('+')
                          .map(k => k.charAt(0).toUpperCase() + k.slice(1))
                          .join('+');
                  const sel = v === modifierValue ? ' aria-selected="true"' : '';
                  return `<li class="glass-dropdown__item" data-value="${v}" role="option"${sel}>${label}</li>`;
                })
                .join('')}
            </ul>
          </div>

          <span style="color:inherit;opacity:0.8;">+</span>
          <input type="text" id="timecode-key" value="${config.shortcut.key}" maxlength="1" style="width: 30px; text-align: center; background: rgba(34, 34, 34, var(--yt-header-bg-opacity)); color: white; border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 4px;">
        </div>
      `;

    submenuCard.appendChild(shortcutDiv);
    submenuWrap.appendChild(submenuCard);
    advancedSection.append(enableDiv, submenuWrap);

    // Initialize custom glass dropdown interactions
    const initGlassDropdown = () => {
      const hiddenSelect = byId('timecode-modifier-combo');
      const dropdown = byId('timecode-modifier-dropdown');
      if (!hiddenSelect || !dropdown) return;

      const toggle = $('.glass-dropdown__toggle', dropdown);
      const list = $('.glass-dropdown__list', dropdown);
      const label = $('.glass-dropdown__label', dropdown);

      let items = Array.from($$('.glass-dropdown__item', list));
      let idx = items.findIndex(it => it.getAttribute('aria-selected') === 'true');
      if (idx < 0) idx = 0;

      const closeList = () => {
        dropdown.setAttribute('aria-expanded', 'false');
        list.style.display = 'none';
      };

      const openList = () => {
        dropdown.setAttribute('aria-expanded', 'true');
        list.style.display = 'block';
        items = Array.from($$('.glass-dropdown__item', list));
      };

      // Set initial state
      closeList();

      toggle.addEventListener('click', () => {
        const expanded = dropdown.getAttribute('aria-expanded') === 'true';
        if (expanded) closeList();
        else openList();
      });

      // Click outside to close
      document.addEventListener('click', e => {
        if (!dropdown.contains(e.target)) closeList();
      });

      // Item selection
      list.addEventListener('click', e => {
        const it = e.target.closest('.glass-dropdown__item');
        if (!it) return;
        const val = it.dataset.value;
        hiddenSelect.value = val;
        // update aria-selected
        list
          .querySelectorAll('.glass-dropdown__item')
          .forEach(li => li.removeAttribute('aria-selected'));
        it.setAttribute('aria-selected', 'true');
        idx = items.indexOf(it);
        label.textContent = it.textContent;
        // trigger change to reuse existing save logic
        hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
        closeList();
      });

      // keyboard support with arrow navigation
      dropdown.addEventListener('keydown', e => {
        const expanded = dropdown.getAttribute('aria-expanded') === 'true';
        if (e.key === 'ArrowDown') {
          e.preventDefault();
          if (!expanded) openList();
          idx = Math.min(idx + 1, items.length - 1);
          items.forEach(it => it.removeAttribute('aria-selected'));
          items[idx].setAttribute('aria-selected', 'true');
          items[idx].scrollIntoView({ block: 'nearest' });
        } else if (e.key === 'ArrowUp') {
          e.preventDefault();
          if (!expanded) openList();
          idx = Math.max(idx - 1, 0);
          items.forEach(it => it.removeAttribute('aria-selected'));
          items[idx].setAttribute('aria-selected', 'true');
          items[idx].scrollIntoView({ block: 'nearest' });
        } else if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          if (!expanded) {
            openList();
            return;
          }
          const it = items[idx];
          if (it) {
            hiddenSelect.value = it.dataset.value;
            hiddenSelect.dispatchEvent(new Event('change', { bubbles: true }));
            label.textContent = it.textContent;
            closeList();
          }
        } else if (e.key === 'Escape') {
          closeList();
        }
      });
    };

    // Defer init to ensure elements are in DOM
    setTimeout(initGlassDropdown, 0);

    // Event listeners
    advancedSection.addEventListener('change', e => {
      const target = /** @type {EventTarget & HTMLElement} */ (e.target);
      if (target.matches && target.matches('.ytp-plus-settings-checkbox[data-setting="enabled"]')) {
        config.enabled = /** @type {HTMLInputElement} */ (target).checked;
        const submenuToggle = enableDiv.querySelector(
          '.ytp-plus-submenu-toggle[data-submenu="timecode"]'
        );
        if (submenuToggle instanceof HTMLElement) {
          if (config.enabled) {
            const stored = getSubmenuExpanded();
            const nextExpanded = typeof stored === 'boolean' ? stored : true;
            submenuToggle.removeAttribute('disabled');
            submenuToggle.style.display = 'inline-flex';
            submenuToggle.setAttribute('aria-expanded', nextExpanded ? 'true' : 'false');
            submenuWrap.style.display = nextExpanded ? 'block' : 'none';
          } else {
            submenuToggle.setAttribute('disabled', '');
            submenuToggle.style.display = 'none';
            submenuWrap.style.display = 'none';
          }
        }
        toggleTimecodePanel(config.enabled);
        saveSettings();
      }
    });

    byId('timecode-modifier-combo')?.addEventListener('change', e => {
      const target = /** @type {EventTarget & HTMLSelectElement} */ (e.target);
      const value = target.value;
      config.shortcut.ctrlKey = value.includes('ctrl');
      config.shortcut.altKey = value.includes('alt');
      config.shortcut.shiftKey = value.includes('shift');
      saveSettings();
    });

    byId('timecode-key')?.addEventListener('input', e => {
      const target = /** @type {EventTarget & HTMLInputElement} */ (e.target);
      if (target.value) {
        config.shortcut.key = target.value.toUpperCase();
        saveSettings();
      }
    });
  };

  const ensureTimecodePanelSettings = (attempt = 0) => {
    const advancedVisible = $('.ytp-plus-settings-section[data-section="advanced"]:not(.hidden)');
    if (!advancedVisible) {
      if (attempt < 20) setTimeout(() => ensureTimecodePanelSettings(attempt + 1), 80);
      return;
    }

    addTimecodePanelSettings();
    if (!$('.timecode-settings-item') && attempt < 20) {
      setTimeout(() => ensureTimecodePanelSettings(attempt + 1), 80);
    }
  };

  // CSS
  const insertTimecodeStyles = () => {
    if (byId('timecode-panel-styles')) return;

    const styles = `
        :root{--tc-panel-bg:rgba(255,255,255,0.06);--tc-panel-border:rgba(255,255,255,0.12);--tc-panel-color:#fff}
        html[dark],body[dark]{--tc-panel-bg:rgba(34,34,34,0.75);--tc-panel-border:rgba(255,255,255,0.12);--tc-panel-color:#fff}
        html:not([dark]){--tc-panel-bg:rgba(255,255,255,0.95);--tc-panel-border:rgba(0,0,0,0.08);--tc-panel-color:#222}
        #timecode-panel{position:fixed;right:20px;top:80px;background:var(--tc-panel-bg);border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,0.45);width:320px;max-height:70vh;z-index:10000;color:var(--tc-panel-color);backdrop-filter:blur(14px) saturate(140%);-webkit-backdrop-filter:blur(14px) saturate(140%);border:1.5px solid var(--tc-panel-border);transition:transform .28s cubic-bezier(.4,0,.2,1),opacity .28s;overflow:hidden;display:flex;flex-direction:column}
        #timecode-panel.hidden{transform:translateX(300px);opacity:0;pointer-events:none}
        #timecode-panel.auto-tracking{box-shadow:0 12px 48px rgba(255,0,0,0.12);border-color:rgba(255,0,0,0.25)}
        #timecode-header{display:flex;justify-content:space-between;align-items:center;padding:14px;border-bottom:1px solid rgba(255,255,255,0.04);background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);cursor:move}
        #timecode-title{font-weight:600;margin:0;font-size:15px;user-select:none;display:flex;align-items:center;gap:8px}
        #timecode-tracking-indicator{width:8px;height:8px;background:red;border-radius:50%;opacity:0;transition:opacity .3s}
        #timecode-panel.auto-tracking #timecode-tracking-indicator{opacity:1}
        #timecode-current-time{font-family:monospace;font-size:12px;padding:2px 6px;background:rgba(255,0,0,.3);border-radius:3px;margin-left:auto}
        #timecode-header-controls{display:flex;align-items:center;gap:6px}
        #timecode-reload,#timecode-close{background:transparent;border:none;color:inherit;cursor:pointer;width:28px;height:28px;padding:0;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:background .18s,color .18s}
        #timecode-reload:hover,#timecode-close:hover{background:rgba(255,255,255,0.04)}
        #timecode-reload.loading{animation:timecode-spin .8s linear infinite}
        #timecode-list{overflow-y:auto;padding:8px 0;max-height:calc(70vh - 80px);scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}
        #timecode-list::-webkit-scrollbar{width:6px}
        #timecode-list::-webkit-scrollbar-thumb{background:rgba(255,255,255,.3);border-radius:3px}
        .timecode-item{padding:10px 14px;display:flex;align-items:center;cursor:pointer;transition:background-color .16s,transform .12s;border-left:3px solid transparent;position:relative;border-radius:8px;margin:6px 10px}
        .timecode-item:hover{background:rgba(255,255,255,0.04);transform:translateY(-2px)}
        .timecode-item:hover .timecode-actions{opacity:1}
        .timecode-item.active{background:linear-gradient(90deg, rgba(255,68,68,0.12), rgba(255,68,68,0.04));border-left-color:#ff6666;box-shadow:inset 0 0 0 1px rgba(255,68,68,0.03)}
        .timecode-item.active.pulse{animation:pulse .8s ease-out}
        .timecode-item.editing{background:linear-gradient(90deg, rgba(255,170,0,0.08), rgba(255,170,0,0.03));border-left-color:#ffaa00}
        .timecode-item.editing .timecode-actions{opacity:1}
        @keyframes pulse{0%{transform:scale(1)}50%{transform:scale(1.02)}100%{transform:scale(1)}}
        @keyframes timecode-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
        .timecode-time{font-family:monospace;margin-right:10px;color:rgba(255,255,255,.8);font-size:13px;min-width:45px;flex-shrink:0}
        .timecode-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px;flex:1;margin-left:4px}
        .timecode-item:not(:has(.timecode-label)) .timecode-time{flex:1;text-align:left}
        .timecode-item.has-chapter .timecode-time{color:#ff4444}
        .timecode-progress{width:0;height:2px;background:#ff4444;position:absolute;bottom:0;left:0;transition:width .3s;opacity:.8}
        .timecode-actions{position:absolute;right:8px;top:50%;transform:translateY(-50%);display:flex;gap:4px;opacity:0;transition:opacity .2s;background:rgba(0,0,0,.8);border-radius:4px;padding:2px}
        .timecode-action{background:none;border:none;color:rgba(255,255,255,.8);cursor:pointer;padding:4px;font-size:12px;border-radius:2px;transition:color .2s,background-color .2s}
        .timecode-action:hover{color:#fff;background:rgba(255,255,255,.2)}
        .timecode-action.edit:hover{color:#ffaa00}
        .timecode-action.delete:hover{color:#ff4444}
        #timecode-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;text-align:center;color:rgba(255,255,255,.7);font-size:13px}
        #timecode-form{padding:12px;border-top:1px solid rgba(255,255,255,.04);display:none}
        #timecode-form.visible{display:block}
        #timecode-form input{width:100%;margin-bottom:8px;padding:8px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);border-radius:4px;color:#fff;font-size:13px}
        #timecode-form input::placeholder{color:rgba(255,255,255,.6)}
        #timecode-form-buttons{display:flex;gap:8px;justify-content:flex-end}
        #timecode-form-buttons button{padding:6px 12px;border:none;border-radius:4px;cursor:pointer;font-size:12px;transition:background-color .2s}
        #timecode-form-cancel{background:rgba(255,255,255,.2);color:#fff}
        #timecode-form-cancel:hover{background:rgba(255,255,255,.3)}
        #timecode-form-save{background:#ff4444;color:#fff}
        #timecode-form-save:hover{background:#ff6666}
        #timecode-actions{padding:10px;border-top:1px solid rgba(255,255,255,.04);display:flex;gap:8px;background:linear-gradient(180deg,transparent,rgba(0,0,0,0.03))}
        #timecode-actions button{padding:8px 12px;border:none;border-radius:8px;cursor:pointer;font-size:13px;transition:background .18s;color:inherit;background:rgba(255,255,255,0.02)}
        #timecode-actions button:hover{background:rgba(255,255,255,0.04)}
        #timecode-track-toggle.active{background:linear-gradient(90deg,#ff6b6b,#ff4444);color:#fff}
      `;
    YouTubeUtils.StyleManager.add('timecode-panel-styles', styles);
  };

  // Panel creation
  const createTimecodePanel = () => {
    if (state.dom.panel) return state.dom.panel;

    // Remove any existing panels (for redundancy)
    $$('#timecode-panel').forEach(p => p.remove());

    const panel = document.createElement('div');
    panel.id = 'timecode-panel';
    panel.className = config.enabled ? '' : 'hidden';
    if (config.autoTrackPlayback) panel.classList.add('auto-tracking');

    panel.innerHTML = `
        <div id="timecode-header">
          <h3 id="timecode-title">
            <div id="timecode-tracking-indicator"></div>
            ${t('timecodes')}
            <span id="timecode-current-time"></span>
          </h3>
          <div id="timecode-header-controls">
            <button id="timecode-reload" title="${t('reload')}" aria-label="${t('reload')}">⟳</button>
            <button id="timecode-close" title="${t('close')}" aria-label="${t('close')}">
              <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
              </svg>
            </button>
          </div>
        </div>
        <div id="timecode-list"></div>
        <div id="timecode-empty">
          <div>${t('noTimecodesFound')}</div>
          <div style="margin-top:5px;font-size:12px">${t('clickToAdd')}</div>
        </div>
        <div id="timecode-form">
          <input type="text" id="timecode-form-time" placeholder="${t('timePlaceholder')}">
          <input type="text" id="timecode-form-label" placeholder="${t('labelPlaceholder')}">
          <div id="timecode-form-buttons">
            <button type="button" id="timecode-form-cancel">${t('cancel')}</button>
            <button type="button" id="timecode-form-save" class="save">${t('save')}</button>
          </div>
        </div>
        <div id="timecode-actions">
          <button id="timecode-add-btn">${t('add')}</button>
          <button id="timecode-export-btn" ${config.export ? '' : 'style="display:none"'}>${t('export')}</button>
          <button id="timecode-track-toggle" class="${config.autoTrackPlayback ? 'active' : ''}">${config.autoTrackPlayback ? t('tracking') : t('track')}</button>
        </div>
      `;

    // Cache DOM elements
    state.dom = {
      panel,
      list: panel.querySelector('#timecode-list'),
      empty: panel.querySelector('#timecode-empty'),
      form: panel.querySelector('#timecode-form'),
      timeInput: panel.querySelector('#timecode-form-time'),
      labelInput: panel.querySelector('#timecode-form-label'),
      currentTime: panel.querySelector('#timecode-current-time'),
      trackToggle: panel.querySelector('#timecode-track-toggle'),
      reloadButton: panel.querySelector('#timecode-reload'),
    };

    // Event delegation
    panel.addEventListener('click', handlePanelClick);
    makeDraggable(panel);

    document.body.appendChild(panel);
    applySavedPanelPosition(panel);
    return panel;
  };

  // Event handling
  const handlePanelClick = e => {
    const { target } = e;
    const item = target.closest('.timecode-item');

    // Use closest so clicks on child SVG/path elements are detected
    const reloadButton = target.closest
      ? target.closest('#timecode-reload')
      : target.id === 'timecode-reload'
        ? target
        : null;
    if (reloadButton) {
      e.preventDefault();
      reloadTimecodes(reloadButton);
      return;
    }

    const closeButton = target.closest
      ? target.closest('#timecode-close')
      : target.id === 'timecode-close'
        ? target
        : null;
    if (closeButton) {
      toggleTimecodePanel(false);
    } else if (target.id === 'timecode-add-btn') {
      const video = YouTubeUtils.querySelector ? YouTubeUtils.querySelector('video') : $('video');
      if (video) showTimecodeForm(video.currentTime);
    } else if (target.id === 'timecode-track-toggle') {
      config.autoTrackPlayback = !config.autoTrackPlayback;
      target.textContent = config.autoTrackPlayback ? t('tracking') : t('track');
      target.classList.toggle('active', config.autoTrackPlayback);
      state.dom.panel.classList.toggle('auto-tracking', config.autoTrackPlayback);
      saveSettings();
      if (config.autoTrackPlayback) startTracking();
    } else if (target.id === 'timecode-export-btn') {
      exportTimecodes();
    } else if (target.id === 'timecode-form-cancel') {
      hideTimecodeForm();
    } else if (target.id === 'timecode-form-save') {
      saveTimecodeForm();
    } else if (target.classList.contains('timecode-action')) {
      e.stopPropagation();
      const action = target.dataset.action;
      const index = parseInt(target.closest('.timecode-item').dataset.index, 10);

      if (action === 'edit') {
        editTimecode(index);
      } else if (action === 'delete') {
        deleteTimecode(index);
      }
    } else if (item && !target.closest('.timecode-actions')) {
      const time = parseFloat(item.dataset.time);
      const video = $('video');
      if (video && !isNaN(time)) {
        /** @type {HTMLVideoElement} */ (video).currentTime = time;
        if (video.paused) video.play();
        updateActiveItem(item);
      }
    }
  };

  // Edit timecode
  const editTimecode = index => {
    const timecodes = getCurrentTimecodes();
    if (index < 0 || index >= timecodes.length) return;

    const timecode = timecodes[index];
    state.editingIndex = index;

    // Update item appearance
    const item = state.dom.list.querySelector(`.timecode-item[data-index="${index}"]`);
    if (item) {
      item.classList.add('editing');
      // Hide other editing items
      state.dom.list.querySelectorAll('.timecode-item.editing').forEach(el => {
        if (el !== item) el.classList.remove('editing');
      });
    }

    showTimecodeForm(timecode.time, timecode.label);
  };

  // Delete timecode
  const deleteTimecode = index => {
    const timecodes = getCurrentTimecodes();
    if (index < 0 || index >= timecodes.length) return;

    const timecode = timecodes[index];

    // Don't allow deletion of native YouTube chapters
    if (timecode.isChapter && !timecode.isUserAdded) {
      showNotification(t('cannotDeleteChapter'));
      return;
    }

    // Confirm deletion
    if (!confirm(t('confirmDelete').replace('{label}', timecode.label))) return;

    timecodes.splice(index, 1);
    updateTimecodePanel(timecodes);
    saveTimecodesToStorage(timecodes);
    showNotification(t('timecodeDeleted'));
  };

  // Form handling
  const showTimecodeForm = (currentTime, existingLabel = '') => {
    const { form, timeInput, labelInput } = state.dom;
    form.classList.add('visible');
    timeInput.value = formatTime(currentTime);
    labelInput.value = existingLabel;
    requestAnimationFrame(() => labelInput.focus());
  };

  const hideTimecodeForm = () => {
    state.dom.form.classList.remove('visible');
    state.editingIndex = null;
    // Remove editing class from all items
    state.dom.list?.querySelectorAll('.timecode-item.editing').forEach(el => {
      el.classList.remove('editing');
    });
  };

  const saveTimecodeForm = () => {
    const { timeInput, labelInput } = state.dom;
    const timeValue = timeInput.value.trim();
    const labelValue = labelInput.value.trim();

    const time = parseTime(timeValue);
    if (time === null) {
      showNotification(t('invalidTimeFormat'));
      return;
    }

    const timecodes = getCurrentTimecodes();
    const newTimecode = {
      time,
      label: labelValue || '',
      isUserAdded: true,
      isChapter: false,
    };

    if (state.editingIndex !== null) {
      // Editing existing timecode
      const oldTimecode = timecodes[state.editingIndex];
      if (oldTimecode.isChapter && !oldTimecode.isUserAdded) {
        showNotification(t('cannotEditChapter'));
        hideTimecodeForm();
        return;
      }

      timecodes[state.editingIndex] = { ...oldTimecode, ...newTimecode };
      showNotification(t('timecodeUpdated'));
    } else {
      // Adding new timecode
      timecodes.push(newTimecode);
      showNotification(t('timecodeAdded'));
    }

    const sorted = timecodes.sort((a, b) => a.time - b.time);
    updateTimecodePanel(sorted);
    saveTimecodesToStorage(sorted);
    hideTimecodeForm();
  };

  // Export
  const exportTimecodes = () => {
    const timecodes = getCurrentTimecodes();
    if (!timecodes.length) {
      showNotification(t('noTimecodesToExport'));
      return;
    }

    const exportBtn = state.dom.panel?.querySelector('#timecode-export-btn');
    if (exportBtn) {
      exportBtn.textContent = t('copied');
      exportBtn.style.backgroundColor = 'rgba(0,220,0,0.8)';
      setTimeout(() => {
        exportBtn.textContent = t('export');
        exportBtn.style.backgroundColor = '';
      }, 2000);
    }

    const videoTitle = document.title.replace(/\s-\sYouTube$/, '');
    let content = `${videoTitle}\n\nTimecodes:\n`;
    timecodes.forEach(tc => {
      const label = tc.label?.trim();
      content += label ? `${formatTime(tc.time)} - ${label}\n` : `${formatTime(tc.time)}\n`;
    });

    if (navigator.clipboard?.writeText) {
      navigator.clipboard.writeText(content).then(() => {
        showNotification(t('timecodesCopied'));
      });
    }
  };

  // Panel updates
  const updateTimecodePanel = timecodes => {
    const { list, empty } = state.dom;
    if (!list || !empty) return;

    const isEmpty = !timecodes.length;
    empty.style.display = isEmpty ? 'flex' : 'none';
    list.style.display = isEmpty ? 'none' : 'block';

    if (isEmpty) {
      list.innerHTML = '';
      return;
    }

    list.innerHTML = timecodes
      .map((tc, i) => {
        const timeStr = formatTime(tc.time);
        // Only use label if it exists and is different from time
        let rawLabel = tc.label?.trim() || '';

        // Remove time prefix from label if it starts with the same time
        const timePattern = /^\d{1,2}:\d{2}(?::\d{2})?\s*[-–—:]?\s*/;
        rawLabel = rawLabel.replace(timePattern, '');

        // Remove duplicate text in label (final safety check)
        const beforeDedup = rawLabel;
        rawLabel = removeDuplicateText(rawLabel);

        if (beforeDedup !== rawLabel && rawLabel.length > 0) {
          console.warn('[Timecode] Display deduplicated:', beforeDedup, '->', rawLabel);
        }

        // Normalize time comparisons (remove leading zeros for comparison)
        const normalizedTime = timeStr.replace(/^0+:/, '');
        const normalizedLabel = rawLabel.replace(/^0+:/, '');

        const hasCustomLabel =
          rawLabel &&
          rawLabel !== timeStr &&
          normalizedLabel !== normalizedTime &&
          rawLabel !== tc.originalText &&
          rawLabel.length > 0;
        const displayLabel = hasCustomLabel ? rawLabel : '';
        const safeLabel = displayLabel.replace(
          /[<>&"']/g,
          c => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;' })[c]
        );
        const isEditable = !tc.isChapter || tc.isUserAdded;

        return `
          <div class="timecode-item ${tc.isChapter ? 'has-chapter' : ''}" data-time="${tc.time}" data-index="${i}">
            <div class="timecode-time">${timeStr}</div>
            ${safeLabel ? `<div class="timecode-label" title="${safeLabel}">${safeLabel}</div>` : ''}
            <div class="timecode-progress"></div>
            ${
              isEditable
                ? `
              <div class="timecode-actions">
                <button class="timecode-action edit" data-action="edit" title="${t('edit')}">✎</button>
                <button class="timecode-action delete" data-action="delete" title="${t('delete')}">✕</button>
              </div>
            `
                : ''
            }
          </div>
        `;
      })
      .join('');
  };

  const updateActiveItem = activeItem => {
    const items = state.dom.list?.querySelectorAll('.timecode-item');
    if (!items) return;

    items.forEach(item => item.classList.remove('active', 'pulse'));
    if (activeItem) {
      activeItem.classList.add('active', 'pulse');
      setTimeout(() => activeItem.classList.remove('pulse'), 800);
    }
  };

  // Tracking
  const startTracking = () => {
    if (state.trackingId) return;

    const track = () => {
      try {
        const video = $('video');
        const { panel, currentTime, list } = state.dom;

        // Stop tracking if essential elements are missing or panel is hidden
        if (!video || !panel || panel.classList.contains('hidden') || !config.autoTrackPlayback) {
          if (state.trackingId) {
            cancelAnimationFrame(state.trackingId);
            state.trackingId = 0;
          }
          return;
        }

        // Update current time display
        if (currentTime && !isNaN(video.currentTime)) {
          currentTime.textContent = formatTime(video.currentTime);
        }

        // Update active item
        const items = list?.querySelectorAll('.timecode-item');
        if (items?.length) {
          let activeIndex = -1;
          let nextIndex = -1;

          for (let i = 0; i < items.length; i++) {
            const timeData = items[i].dataset.time;
            if (!timeData) continue;

            const time = parseFloat(timeData);
            if (isNaN(time)) continue;

            if (video.currentTime >= time) {
              activeIndex = i;
            } else if (nextIndex === -1) {
              nextIndex = i;
            }
          }

          // Update active state
          if (state.activeIndex !== activeIndex) {
            // Remove previous active state
            if (state.activeIndex !== null && state.activeIndex >= 0 && items[state.activeIndex]) {
              items[state.activeIndex].classList.remove('active');
            }

            // Set new active state
            if (activeIndex >= 0 && items[activeIndex]) {
              items[activeIndex].classList.add('active');
              try {
                items[activeIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
              } catch {
                // Fallback for browsers that don't support smooth scrolling
                items[activeIndex].scrollIntoView(false);
              }
            }

            state.activeIndex = activeIndex;
          }

          // Update progress bar
          if (activeIndex >= 0 && nextIndex >= 0 && items[activeIndex]) {
            const currentTimeData = items[activeIndex].dataset.time;
            const nextTimeData = items[nextIndex].dataset.time;

            if (currentTimeData && nextTimeData) {
              const current = parseFloat(currentTimeData);
              const next = parseFloat(nextTimeData);

              if (!isNaN(current) && !isNaN(next) && next > current) {
                const progress = ((video.currentTime - current) / (next - current)) * 100;
                const progressEl = items[activeIndex].querySelector('.timecode-progress');
                if (progressEl) {
                  const clampedProgress = Math.min(100, Math.max(0, progress));
                  progressEl.style.width = `${clampedProgress}%`;
                }
              }
            }
          }
        }

        // Continue tracking if enabled
        if (config.autoTrackPlayback) {
          state.trackingId = requestAnimationFrame(track);
        }
      } catch (error) {
        console.warn('Timecode tracking error:', error);
        // Stop tracking on error to prevent infinite error loops
        if (state.trackingId) {
          cancelAnimationFrame(state.trackingId);
          state.trackingId = 0;
        }
      }
    };

    state.trackingId = requestAnimationFrame(track);
  };

  // Stop tracking function
  const stopTracking = () => {
    if (state.trackingId) {
      cancelAnimationFrame(state.trackingId);
      state.trackingId = 0;
    }
  };

  // Drag functionality
  const makeDraggable = panel => {
    const header = panel.querySelector('#timecode-header');
    if (!header) return;

    let startX, startY, startLeft, startTop;

    const mouseDownHandler = e => {
      if (e.button !== 0) return;

      state.dragging = true;
      startX = e.clientX;
      startY = e.clientY;

      const rect = panel.getBoundingClientRect();

      if (!panel.style.left) {
        panel.style.left = `${rect.left}px`;
      }
      if (!panel.style.top) {
        panel.style.top = `${rect.top}px`;
      }

      panel.style.right = 'auto';

      startLeft = parseFloat(panel.style.left) || rect.left;
      startTop = parseFloat(panel.style.top) || rect.top;

      const handleMove = event => {
        if (!state.dragging) return;

        const deltaX = event.clientX - startX;
        const deltaY = event.clientY - startY;
        const { left, top } = clampPanelPosition(panel, startLeft + deltaX, startTop + deltaY);

        panel.style.left = `${left}px`;
        panel.style.top = `${top}px`;
        panel.style.right = 'auto';
      };

      const handleUp = () => {
        if (!state.dragging) return;

        state.dragging = false;
        document.removeEventListener('mousemove', handleMove);
        document.removeEventListener('mouseup', handleUp);

        const rectAfter = panel.getBoundingClientRect();
        const { left, top } = clampPanelPosition(panel, rectAfter.left, rectAfter.top);

        panel.style.left = `${left}px`;
        panel.style.top = `${top}px`;
        panel.style.right = 'auto';

        savePanelPosition(left, top);
      };

      document.addEventListener('mousemove', handleMove);
      document.addEventListener('mouseup', handleUp);
    };

    YouTubeUtils.cleanupManager.registerListener(header, 'mousedown', mouseDownHandler);
  };

  // Storage
  const saveTimecodesToStorage = timecodes => {
    const videoId = new URLSearchParams(window.location.search).get('v');
    if (!videoId) return;

    try {
      const minimal = timecodes.map(tc => ({
        t: tc.time,
        l: tc.label?.trim() || '',
        c: tc.isChapter || false,
        u: tc.isUserAdded || false,
      }));
      localStorage.setItem(`yt_tc_${videoId}`, JSON.stringify(minimal));
    } catch {}
  };

  const loadTimecodesFromStorage = () => {
    const videoId = new URLSearchParams(window.location.search).get('v');
    if (!videoId) return null;

    try {
      const data = localStorage.getItem(`yt_tc_${videoId}`);
      return data
        ? JSON.parse(data)
            .map(tc => ({
              time: tc.t,
              label: tc.l,
              isChapter: tc.c,
              isUserAdded: tc.u || false,
            }))
            .sort((a, b) => a.time - b.time)
        : null;
    } catch {
      return null;
    }
  };

  const getCurrentTimecodes = () => {
    const items = state.dom.list?.querySelectorAll('.timecode-item');
    if (!items) return [];

    return Array.from(items)
      .map(item => {
        const time = parseFloat(item.dataset.time);
        const labelEl = item.querySelector('.timecode-label');
        // Only use label if element exists and has actual text content
        const label = labelEl?.textContent?.trim() || '';

        return {
          time,
          label: label, // Keep original label (can be empty)
          isChapter: item.classList.contains('has-chapter'),
          isUserAdded: !item.classList.contains('has-chapter') || false,
        };
      })
      .sort((a, b) => a.time - b.time);
  };

  // Toggle panel
  const toggleTimecodePanel = show => {
    // Close any existing panels first (cleanup)
    $$('#timecode-panel').forEach(panel => {
      if (panel !== state.dom.panel) panel.remove();
    });

    const panel = state.dom.panel || createTimecodePanel();
    if (show === undefined) show = panel.classList.contains('hidden');

    panel.classList.toggle('hidden', !show);

    if (show) {
      applySavedPanelPosition(panel);

      const saved = loadTimecodesFromStorage();
      if (saved?.length) {
        updateTimecodePanel(saved);
      } else if (config.autoDetect) {
        detectTimecodes().catch(err => console.error('[Timecode] Detection failed:', err));
      }

      if (config.autoTrackPlayback) startTracking();
    } else if (state.trackingId) {
      cancelAnimationFrame(state.trackingId);
      state.trackingId = 0;
    }
  };

  // Navigation handling
  const setupNavigation = () => {
    let currentVideoId = new URLSearchParams(window.location.search).get('v');

    const handleNavigationChange = () => {
      const newVideoId = new URLSearchParams(window.location.search).get('v');
      if (newVideoId === currentVideoId || window.location.pathname !== '/watch') return;

      currentVideoId = newVideoId;
      state.activeIndex = null;
      state.editingIndex = null;
      state.timecodes.clear();

      if (config.enabled && state.dom.panel && !state.dom.panel.classList.contains('hidden')) {
        const saved = loadTimecodesFromStorage();
        if (saved?.length) {
          updateTimecodePanel(saved);
        } else if (config.autoDetect) {
          setTimeout(
            () =>
              detectTimecodes().catch(err => console.error('[Timecode] Detection failed:', err)),
            500
          );
        }
        if (config.autoTrackPlayback) startTracking();
      }
    };

    document.addEventListener('yt-navigate-finish', handleNavigationChange);
  };

  // Keyboard shortcuts
  const setupKeyboard = () => {
    document.addEventListener('keydown', e => {
      if (!config.enabled) return;

      const target = /** @type {EventTarget & HTMLElement} */ (e.target);
      if (target.matches && target.matches('input, textarea, [contenteditable]')) return;

      const { key, shiftKey, altKey, ctrlKey } = config.shortcut;
      if (
        e.key.toUpperCase() === key &&
        e.shiftKey === shiftKey &&
        e.altKey === altKey &&
        e.ctrlKey === ctrlKey
      ) {
        e.preventDefault();
        toggleTimecodePanel();
      }
    });
  };

  // Cleanup on unload
  const cleanup = () => {
    stopTracking();
    if (state.dom.panel) {
      state.dom.panel.remove();
      state.dom.panel = null;
    }
  };

  // Initialize
  const init = () => {
    if (initStarted) return;
    if (!isRelevantRoute()) return;

    const appRoot =
      (typeof YouTubeUtils?.querySelector === 'function' &&
        YouTubeUtils.querySelector('ytd-app')) ||
      $('ytd-app');

    if (!appRoot) {
      scheduleInitRetry();
      return;
    }

    initStarted = true;

    loadSettings();
    insertTimecodeStyles();
    setupKeyboard();
    setupNavigation();

    // Settings modal observer
    let modalObserver = null;
    let modalObserverTimeout = null;

    const attachModalObserver = modalEl => {
      if (!modalEl || !(modalEl instanceof Element)) return;
      if (modalObserver) {
        try {
          modalObserver.disconnect();
        } catch {}
        modalObserver = null;
      }

      modalObserver = new MutationObserver(() => {
        // Debounce modal observer to reduce unnecessary checks
        if (modalObserverTimeout) return;
        modalObserverTimeout = setTimeout(() => {
          modalObserverTimeout = null;
          if (
            $('.ytp-plus-settings-section[data-section="advanced"]:not(.hidden)') &&
            !$('.timecode-settings-item')
          ) {
            setTimeout(() => ensureTimecodePanelSettings(), 50);
          }
        }, 30);
      });

      YouTubeUtils.cleanupManager.registerObserver(modalObserver);
      modalObserver.observe(modalEl, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['class'],
      });
    };

    // Settings modal integration — use event instead of body MutationObserver
    document.addEventListener('youtube-plus-settings-modal-opened', () => {
      const modal = document.querySelector('.ytp-plus-settings-modal');
      if (modal) {
        attachModalObserver(modal);
        setTimeout(() => ensureTimecodePanelSettings(), 100);
      }
    });

    const clickHandler = e => {
      const target = /** @type {HTMLElement} */ (e.target);
      const navItem = target?.closest?.('.ytp-plus-settings-nav-item');
      if (navItem?.dataset?.section === 'advanced') {
        setTimeout(() => ensureTimecodePanelSettings(), 50);
      }
    };
    YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, true);

    if (config.enabled && !state.resizeListenerKey) {
      const onResize = YouTubeUtils.throttle(() => {
        if (!state.dom.panel) return;

        const rect = state.dom.panel.getBoundingClientRect();
        const { left, top } = clampPanelPosition(state.dom.panel, rect.left, rect.top);

        state.dom.panel.style.left = `${left}px`;
        state.dom.panel.style.top = `${top}px`;
        state.dom.panel.style.right = 'auto';

        savePanelPosition(left, top);
      }, 200);

      state.resizeListenerKey = YouTubeUtils.cleanupManager.registerListener(
        window,
        'resize',
        onResize
      );
    }
  };

  const handleNavigate = () => {
    if (!isRelevantRoute()) {
      if (initStarted) cleanup();
      return;
    }
    init();
  };

  // Start on document ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', handleNavigate, { once: true });
  } else {
    handleNavigate();
  }

  if (window.YouTubeUtils?.cleanupManager?.registerListener) {
    YouTubeUtils.cleanupManager.registerListener(document, 'yt-navigate-finish', handleNavigate, {
      passive: true,
    });
  } else {
    document.addEventListener('yt-navigate-finish', handleNavigate, { passive: true });
  }

  // Cleanup on beforeunload
  window.addEventListener('beforeunload', cleanup);
})();

// --- MODULE: playlist-search.js ---

// Playlist Search
(function () {
  'use strict';

  let featureEnabled = true;
  const loadFeatureEnabled = () => {
    try {
      const settings = localStorage.getItem('youtube_plus_settings');
      if (settings) {
        const parsed = JSON.parse(settings);
        return parsed.enablePlaylistSearch !== false;
      }
    } catch {}
    return true;
  };
  const setFeatureEnabled = nextEnabled => {
    featureEnabled = nextEnabled !== false;
    if (!featureEnabled) {
      cleanup();
    } else {
      ensureInit();
      handleNavigation();
    }
  };

  featureEnabled = loadFeatureEnabled();

  // Prevent multiple initializations
  if (window._playlistSearchInitialized) return;
  window._playlistSearchInitialized = true;

  // DOM cache helpers with fallback
  const qs = selector => {
    if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.get === 'function') {
      return window.YouTubeDOMCache.get(selector);
    }
    return document.querySelector(selector);
  };

  /**
   * Translation helper - uses centralized i18n system
   * @param {string} key - Translation key
   * @param {Object} params - Interpolation parameters
   * @returns {string} Translated string
   */
  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    // Embedded English fallback (prevents showing raw keys during early init)
    try {
      const embeddedEn = window.YouTubePlusEmbeddedTranslations?.en;
      if (embeddedEn && embeddedEn[key]) {
        let text = embeddedEn[key];
        if (params && Object.keys(params).length > 0) {
          Object.keys(params).forEach(param => {
            text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]);
          });
        }
        return text;
      }
    } catch {}
    // Fallback for initialization phase
    return key || '';
  };

  // This module targets playlist content on both /watch and /playlist pages.
  const shouldRunOnThisPage = () => {
    return (
      window.location.hostname.endsWith('youtube.com') &&
      window.location.hostname !== 'music.youtube.com' &&
      (window.location.pathname === '/watch' || window.location.pathname === '/playlist')
    );
  };

  const isWatchPage = () => window.location.pathname === '/watch';
  const isPlaylistPage = () => window.location.pathname === '/playlist';

  const isRelevantRoute = () => {
    if (!shouldRunOnThisPage()) return false;
    try {
      const params = new URLSearchParams(window.location.search);
      return params.has('list');
    } catch {
      return false;
    }
  };

  const onDomReady = cb => {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', cb, { once: true });
    } else {
      cb();
    }
  };

  // Use shared debounce/throttle from YouTubeUtils
  const debounce = (func, wait) => {
    if (window.YouTubeUtils?.debounce) return window.YouTubeUtils.debounce(func, wait);
    let timeout;
    return (...args) => {
      clearTimeout(timeout);
      timeout = setTimeout(() => func(...args), wait);
    };
  };

  const throttle = (func, limit) => {
    if (window.YouTubeUtils?.throttle) return window.YouTubeUtils.throttle(func, limit);
    let inThrottle;
    return (...args) => {
      if (!inThrottle) {
        func(...args);
        inThrottle = true;
        setTimeout(() => (inThrottle = false), limit);
      }
    };
  };

  // Previously limited to specific lists (LL/WL). Now support any playlist id.

  const config = {
    enabled: true,
    storageKey: 'youtube_playlist_search_settings',
    searchDebounceMs: 150, // Optimized debounce for better responsiveness
    observerThrottleMs: 300, // Reduced throttle for faster updates
    maxPlaylistItems: 10000, // Increased limit for large playlists
    maxQueryLength: 300, // Increased for more flexible search
    deleteDelay: 250, // Delay between sequential delete actions
  };

  const state = {
    searchInput: null,
    searchResults: null,
    originalItems: [],
    currentPlaylistId: null,
    mutationObserver: null,
    rafId: null,
    itemsCache: new Map(), // Cache for faster lookups
    itemsContainer: null,
    itemSelector: null,
    itemTagName: null,
    playlistPanel: null,
    isPlaylistPage: false,
    // Deletion state
    isDeleting: false,
    deleteMode: false,
    selectedItems: new Set(),
  };

  const inputDebouncers = new WeakMap();
  const setupInputDelegation = (() => {
    let attached = false;
    return () => {
      if (attached) return;
      attached = true;

      const handleFocus = input => {
        input.style.borderColor = 'var(--yt-spec-call-to-action)';
      };

      const handleBlur = input => {
        input.style.borderColor = 'var(--yt-spec-10-percent-layer)';
      };

      const handleInput = input => {
        let debounced = inputDebouncers.get(input);
        if (!debounced) {
          debounced = debounce(value => {
            if (value.length > config.maxQueryLength) {
              const truncated = value.substring(0, config.maxQueryLength);
              input.value = truncated;
              filterPlaylistItems(truncated);
              return;
            }
            filterPlaylistItems(value);
          }, config.searchDebounceMs);
          inputDebouncers.set(input, debounced);
        }
        debounced(input.value || '');
      };

      const delegator = window.YouTubePlusEventDelegation;
      if (delegator?.on) {
        delegator.on(document, 'focusin', '.ytplus-playlist-search-input', (ev, target) => {
          void ev;
          if (target) handleFocus(target);
        });
        delegator.on(document, 'focusout', '.ytplus-playlist-search-input', (ev, target) => {
          void ev;
          if (target) handleBlur(target);
        });
        delegator.on(document, 'input', '.ytplus-playlist-search-input', (ev, target) => {
          void ev;
          if (target) handleInput(target);
        });
      } else {
        document.addEventListener(
          'focusin',
          ev => {
            const target = ev.target?.closest?.('.ytplus-playlist-search-input');
            if (target) handleFocus(target);
          },
          true
        );
        document.addEventListener(
          'focusout',
          ev => {
            const target = ev.target?.closest?.('.ytplus-playlist-search-input');
            if (target) handleBlur(target);
          },
          true
        );
        document.addEventListener(
          'input',
          ev => {
            const target = ev.target?.closest?.('.ytplus-playlist-search-input');
            if (target) handleInput(target);
          },
          true
        );
      }
    };
  })();

  // Load settings from localStorage
  const loadSettings = () => {
    try {
      const globalSettings = localStorage.getItem('youtube_plus_settings');
      if (globalSettings) {
        const parsedGlobal = JSON.parse(globalSettings);
        if (typeof parsedGlobal.enablePlaylistSearch === 'boolean') {
          config.enabled = parsedGlobal.enablePlaylistSearch;
        }
      }
      const saved = localStorage.getItem(config.storageKey);
      if (saved) {
        const parsed = JSON.parse(saved);
        // Use safeMerge to prevent prototype pollution
        if (window.YouTubeUtils && window.YouTubeUtils.safeMerge) {
          window.YouTubeUtils.safeMerge(config, parsed);
        } else {
          // Fallback: only copy known safe keys
          if (typeof parsed.enabled === 'boolean') config.enabled = parsed.enabled;
        }
      }
    } catch (error) {
      console.warn('[Playlist Search] Failed to load settings:', error);
    }
  };

  // (saveSettings removed - settings are static for this module)

  /**
   * Get current playlist id with validation
   * @returns {string|null} Valid playlist ID or null
   */
  const getCurrentPlaylistId = () => {
    try {
      const urlParams = new URLSearchParams(window.location.search);
      const listId = urlParams.get('list');

      // Validate playlist ID format (alphanumeric, dashes, underscores)
      if (listId && /^[a-zA-Z0-9_-]+$/.test(listId)) {
        return listId;
      }

      return null;
    } catch (error) {
      console.warn('[Playlist Search] Failed to get playlist ID:', error);
      return null;
    }
  };

  /**
   * Try to obtain a display name for the current playlist from DOM
   * @param {Element|HTMLElement} playlistPanel - Playlist panel element
   * @param {string} listId - Playlist ID
   * @returns {string} Playlist display name
   */
  const getPlaylistDisplayName = (playlistPanel, listId) => {
    try {
      // Common places for title: .title, h3 a, #header-title, #title
      const sel = [
        'ytd-playlist-header-renderer #title',
        'ytd-playlist-header-renderer .title',
        '.title',
        'h3 a',
        '#header-title',
        '#title',
        '.playlist-title',
        'h1.title',
      ];
      for (const s of sel) {
        const el = playlistPanel.querySelector(s) || qs(s);
        if (el && el.textContent && el.textContent.trim()) {
          // Sanitize and limit length
          const title = el.textContent.trim();
          return title.length > 100 ? title.substring(0, 100) + '...' : title;
        }
      }

      // Fallback to meta or channel-specific metadata
      const meta = qs('meta[name="title"]') || qs('meta[property="og:title"]');
      if (meta && meta.content) {
        const title = meta.content.trim();
        return title.length > 100 ? title.substring(0, 100) + '...' : title;
      }
    } catch (error) {
      console.warn('[Playlist Search] Failed to get display name:', error);
    }

    // Default to sanitized id if nothing else
    if (listId && typeof listId === 'string') {
      return listId.substring(0, 50); // Limit length
    }

    return 'playlist';
  };

  const getPlaylistContext = () => {
    if (isPlaylistPage()) {
      const panel = qs('ytd-playlist-video-list-renderer');
      if (!panel) return null;
      const itemsContainer =
        panel.querySelector('#contents') ||
        panel.querySelector('ytd-playlist-video-list-renderer #contents');
      return {
        panel,
        itemsContainer,
        itemSelector: 'ytd-playlist-video-renderer',
        itemTagName: 'YTD-PLAYLIST-VIDEO-RENDERER',
        isPlaylistPage: true,
      };
    }

    if (isWatchPage()) {
      const panel = qs('ytd-playlist-panel-renderer');
      if (!panel) return null;
      const itemsContainer =
        panel.querySelector('#items') ||
        panel.querySelector('.playlist-items.style-scope.ytd-playlist-panel-renderer') ||
        panel.querySelector('.playlist-items');
      return {
        panel,
        itemsContainer,
        itemSelector: 'ytd-playlist-panel-video-renderer',
        itemTagName: 'YTD-PLAYLIST-PANEL-VIDEO-RENDERER',
        isPlaylistPage: false,
      };
    }

    return null;
  };

  // Add search UI to playlist panel
  const addSearchUI = () => {
    if (!config.enabled) return;

    if (!shouldRunOnThisPage()) return;

    const playlistId = getCurrentPlaylistId();
    if (!playlistId) return;

    const context = getPlaylistContext();
    if (!context) return;
    const { panel: playlistPanel, itemsContainer, itemSelector, itemTagName } = context;

    // Don't add search UI twice
    if (playlistPanel.querySelector('.ytplus-playlist-search')) return;

    state.currentPlaylistId = playlistId;
    state.itemsContainer = itemsContainer || null;
    state.itemSelector = itemSelector;
    state.itemTagName = itemTagName;
    state.playlistPanel = playlistPanel;
    state.isPlaylistPage = context.isPlaylistPage;

    // Create search container
    const searchContainer = document.createElement('div');
    searchContainer.className = 'ytplus-playlist-search';
    searchContainer.style.cssText = `
      padding: 8px 16px;
      background: transparent;
      border-bottom: 1px solid var(--yt-spec-10-percent-layer);
      z-index: 50;
      width: 94%;
    `;

    // Make search (and delete bar inside it) sticky within the playlist area.
    // We try to use `position: sticky` when possible; if the DOM structure
    // prevents sticky from working, fall back to `position: fixed` anchored
    // to the playlist panel so the UI remains visible while scrolling.
    const ensureSticky = () => {
      try {
        // If we're on the /watch page, keep the previous simple sticky style
        // to avoid changing the look/positioning inside the right-hand panel.
        if (!state.isPlaylistPage) {
          searchContainer.style.position = 'sticky';
          searchContainer.style.top = '0';
          searchContainer.style.zIndex = '1';
          searchContainer.style.background = 'transparent';
          return;
        }

        const panel = state.playlistPanel || getPlaylistContext()?.panel;
        // Prefer small top offset on watch page (inside right panel), larger
        // offset on playlist page to account for header/thumbnail column.
        const topOffset = state.isPlaylistPage ? 84 : 8;

        // Try to find a scrollable ancestor for sticky positioning
        let scrollAncestor = panel;
        while (scrollAncestor && scrollAncestor !== document.body) {
          const style = window.getComputedStyle(scrollAncestor);
          const overflowY = style.overflowY;
          if (
            (overflowY === 'auto' || overflowY === 'scroll') &&
            scrollAncestor.scrollHeight > scrollAncestor.clientHeight
          ) {
            break;
          }
          scrollAncestor = scrollAncestor.parentElement;
        }

        if (scrollAncestor && scrollAncestor !== document.body) {
          // If a scrollable ancestor exists, use sticky
          searchContainer.style.position = 'sticky';
          searchContainer.style.top = `${topOffset}px`;
          searchContainer.style.background = 'var(--yt-spec-badge-chip-background)';
          searchContainer.style.backdropFilter = 'blur(6px)';
          searchContainer.style.boxShadow = 'var(--yt-shadow)';
        } else if (panel) {
          // Fallback: position fixed near the playlist panel so it remains visible
          const rect = panel.getBoundingClientRect();
          searchContainer.style.position = 'fixed';
          searchContainer.style.top = `${topOffset}px`;
          // Place horizontally aligned with the panel
          searchContainer.style.left = `${rect.left}px`;
          searchContainer.style.width = `${rect.width}px`;
          searchContainer.style.background = 'var(--yt-spec-badge-chip-background)';
          searchContainer.style.backdropFilter = 'blur(6px)';
          searchContainer.style.boxShadow = '0 6px 20px rgba(0,0,0,0.4)';
          searchContainer.style.zIndex = '9999';

          // Recompute on resize/scroll to keep alignment
          const recompute = debounce(() => {
            const r = panel.getBoundingClientRect();
            searchContainer.style.left = `${r.left}px`;
            searchContainer.style.width = `${r.width}px`;
          }, 120);
          window.addEventListener('resize', recompute, { passive: true });
          // If panel scrolls inside the page, adjust on scroll
          window.addEventListener('scroll', recompute, { passive: true });
        } else {
          // Last fallback: simple sticky at top
          searchContainer.style.position = 'sticky';
          searchContainer.style.top = `${topOffset}px`;
          searchContainer.style.background = 'var(--yt-spec-badge-chip-background)';
        }
      } catch {
        // Ignore errors and leave default styles
      }
    };

    // Ensure sticky after insertion as DOM layout may change
    setTimeout(ensureSticky, 100);

    const searchInput = document.createElement('input');
    searchInput.type = 'text';
    const playlistName = getPlaylistDisplayName(playlistPanel, playlistId);
    const placeholderKey = state.isPlaylistPage
      ? 'searchPlaceholderPlaylistPage'
      : 'searchPlaceholder';
    searchInput.placeholder = t(placeholderKey, { playlist: playlistName });
    searchInput.className = 'ytplus-playlist-search-input';
    searchInput.style.cssText = `
      width: 93%;
      padding: 8px 16px;
      border: 1px solid var(--yt-spec-10-percent-layer);
      border-radius: 20px;
      background: var(--yt-spec-badge-chip-background);
      color: var(--yt-spec-text-primary);
      font-size: 14px;
      font-family: 'Roboto', Arial, sans-serif;
      outline: none;
      transition: border-color 0.2s;
    `;

    setupInputDelegation();

    searchContainer.appendChild(searchInput);
    state.searchInput = searchInput;

    // Try to insert the search UI into the playlist items container so it appears
    // inline with the list of videos. Prefer inserting before the first
    // ytd-playlist-panel-video-renderer if present.
    // Use more specific selector first for better performance
    if (itemsContainer) {
      /** @type {Element|null} */
      const firstVideo = itemsContainer.querySelector(itemSelector);
      if (firstVideo && firstVideo.parentElement === itemsContainer) {
        itemsContainer.insertBefore(searchContainer, /** @type {Node} */ (firstVideo));
      } else {
        // Append to items container if no video element found
        itemsContainer.appendChild(searchContainer);
      }
    } else {
      // Fallback: prepend to the panel root to ensure visibility
      if (playlistPanel.firstChild) {
        playlistPanel.insertBefore(searchContainer, playlistPanel.firstChild);
      } else {
        playlistPanel.appendChild(searchContainer);
      }
    }

    // Store original items
    collectOriginalItems();

    // Add delete UI (toggle button + action bar)
    addDeleteUI(searchContainer);

    // Setup MutationObserver to watch for new playlist items
    setupPlaylistObserver();
  };

  // Setup MutationObserver for dynamic playlist updates
  const setupPlaylistObserver = () => {
    // Disconnect existing observer if any
    if (state.mutationObserver) {
      state.mutationObserver.disconnect();
    }

    const playlistPanel = state.playlistPanel || getPlaylistContext()?.panel;
    if (!playlistPanel || !state.itemTagName) return;

    let lastUpdateCount = state.originalItems.length;
    let updateScheduled = false;
    const itemTagName = state.itemTagName;
    const itemSelector = state.itemSelector;
    const itemsRoot = state.itemsContainer || playlistPanel;

    // Throttled handler for mutations with better batching
    const handleMutations = throttle(mutations => {
      // Skip if update already scheduled
      if (updateScheduled) return;

      // Fast check: only process if playlist items were actually added/removed
      const hasRelevantChange = mutations.some(mutation => {
        if (mutation.type !== 'childList') return false;
        if (mutation.addedNodes.length === 0 && mutation.removedNodes.length === 0) return false;

        // Check if added/removed nodes contain playlist items
        for (let i = 0; i < mutation.addedNodes.length; i++) {
          const node = mutation.addedNodes[i];
          if (node.nodeType === 1) {
            const element = /** @type {Element} */ (node);
            if (element.tagName === itemTagName) return true;
          }
        }
        for (let i = 0; i < mutation.removedNodes.length; i++) {
          const node = mutation.removedNodes[i];
          if (node.nodeType === 1) {
            const element = /** @type {Element} */ (node);
            if (element.tagName === itemTagName) return true;
          }
        }
        return false;
      });

      if (!hasRelevantChange) return;

      updateScheduled = true;
      requestAnimationFrame(() => {
        const currentCount = lastUpdateCount;
        const newItems = itemsRoot
          ? itemsRoot.querySelectorAll(itemSelector)
          : /** @type {NodeListOf<Element>} */ ([]);

        // Only recollect if item count changed
        if (newItems.length !== currentCount) {
          lastUpdateCount = newItems.length;
          collectOriginalItems();

          // Re-apply current search filter if any
          if (state.searchInput && state.searchInput.value) {
            filterPlaylistItems(state.searchInput.value);
          }
        }
        updateScheduled = false;
      });
    }, config.observerThrottleMs);

    state.mutationObserver = new MutationObserver(handleMutations);

    // Observe only the items container, not entire subtree
    const targetElement = itemsRoot || playlistPanel;

    state.mutationObserver.observe(targetElement, {
      childList: true,
      subtree: itemsRoot ? false : true, // Only observe subtree if we couldn't find items container
    });
  };

  /**
   * Collect all playlist items for filtering with limit and improved caching
   */
  const collectOriginalItems = () => {
    const itemsRoot = state.itemsContainer || state.playlistPanel;
    if (!itemsRoot || !state.itemSelector) return;
    const items = itemsRoot.querySelectorAll(state.itemSelector);

    // Limit number of items to prevent performance issues
    if (items.length > config.maxPlaylistItems) {
      console.warn(
        `[Playlist Search] Playlist has ${items.length} items, limiting to ${config.maxPlaylistItems}`
      );
    }

    // Don't clear cache - keep existing cached items to avoid reprocessing
    // Only remove items that are no longer in the DOM
    const currentVideoIds = new Set();

    const itemsArray = Array.from(items).slice(0, config.maxPlaylistItems);

    state.originalItems = itemsArray.map((item, index) => {
      const videoId = item.getAttribute('video-id') || `item-${index}`;
      currentVideoIds.add(videoId);

      // Check if this item is already cached and element is still the same
      if (state.itemsCache.has(videoId)) {
        const cached = state.itemsCache.get(videoId);
        if (cached.element === item) {
          return cached;
        }
      }

      // Optimize: use textContent directly without extra trim/toLowerCase calls
      const titleEl = item.querySelector('#video-title') || item.querySelector('a#video-title');
      const bylineEl =
        item.querySelector('#byline') ||
        item.querySelector('#channel-name') ||
        item.querySelector('ytd-channel-name a');

      const title = titleEl?.textContent || '';
      const channel = bylineEl?.textContent || '';

      const itemData = {
        element: item,
        videoId,
        // Store original text and lowercased version separately for better performance
        titleOriginal: title,
        channelOriginal: channel,
        title: title.trim().toLowerCase(),
        channel: channel.trim().toLowerCase(),
      };

      // Cache the item data
      state.itemsCache.set(videoId, itemData);

      return itemData;
    });

    // Clean up cache - remove items no longer in DOM
    for (const [videoId] of state.itemsCache) {
      if (!currentVideoIds.has(videoId)) {
        state.itemsCache.delete(videoId);
      }
    }
  };

  /**
   * Filter playlist items based on search query with validation
   * @param {string} query - Search query
   */
  const filterPlaylistItems = query => {
    // Cancel any pending RAF
    if (state.rafId) {
      cancelAnimationFrame(state.rafId);
    }

    // Validate and sanitize query
    if (query && typeof query !== 'string') {
      console.warn('[Playlist Search] Invalid query type');
      return;
    }

    // Limit query length to prevent performance issues
    if (query && query.length > config.maxQueryLength) {
      query = query.substring(0, config.maxQueryLength);
    }

    if (!query || query.trim() === '') {
      // Show all items using RAF for smooth update
      state.rafId = requestAnimationFrame(() => {
        state.originalItems.forEach(item => {
          item.element.style.display = '';
        });
        state.rafId = null;
      });
      return;
    }

    const searchTerm = query.toLowerCase().trim();
    let visibleCount = 0;

    // Batch DOM updates using RAF
    state.rafId = requestAnimationFrame(() => {
      // Use document fragment approach - collect changes first
      const updates = [];

      state.originalItems.forEach(item => {
        const matches = item.title.includes(searchTerm) || item.channel.includes(searchTerm);

        if (matches) {
          if (item.element.style.display === 'none') {
            updates.push({ element: item.element, display: '' });
          }
          visibleCount++;
        } else {
          if (item.element.style.display !== 'none') {
            updates.push({ element: item.element, display: 'none' });
          }
        }
      });

      // Apply all updates in one batch to minimize reflows
      updates.forEach(update => {
        update.element.style.display = update.display;
      });

      // Update results count indicator if needed
      updateResultsCount(visibleCount, state.originalItems.length);

      state.rafId = null;
    });
  };

  // Update results count (optional visual feedback)
  const updateResultsCount = (visible, total) => {
    // Could add a results counter here if desired
    window.YouTubeUtils &&
      YouTubeUtils.logger &&
      YouTubeUtils.logger.debug &&
      YouTubeUtils.logger.debug(`[Playlist Search] Showing ${visible} of ${total} videos`);
  };

  // ── Video Deletion Feature (similar to comment.js pattern) ──

  /**
   * Log error with error boundary integration
   * @param {string} context - Error context
   * @param {Error|string|unknown} error - Error object or message
   */
  const logError = (context, error) => {
    const errorObj = error instanceof Error ? error : new Error(String(error));
    if (window.YouTubeErrorBoundary) {
      window.YouTubeErrorBoundary.logError(errorObj, { context });
    } else {
      console.error(`[YouTube+][PlaylistSearch] ${context}:`, error);
    }
  };

  /**
   * Wraps function with error boundary protection
   * @template {Function} T
   * @param {T} fn - Function to wrap
   * @param {string} context - Error context for debugging
   * @returns {T} Wrapped function
   */
  // Use shared withErrorBoundary from YouTubeErrorBoundary
  const withErrorBoundary = (fn, context) => {
    if (window.YouTubeErrorBoundary?.withErrorBoundary) {
      return /** @type {any} */ (
        window.YouTubeErrorBoundary.withErrorBoundary(fn, 'PlaylistSearch')
      );
    }
    return /** @type {any} */ (
      (...args) => {
        try {
          return fn(...args);
        } catch (e) {
          logError(context, e);
          return null;
        }
      }
    );
  };

  /**
   * Toggle delete mode — shows/hides checkboxes on playlist items
   */
  const toggleDeleteMode = withErrorBoundary(() => {
    state.deleteMode = !state.deleteMode;
    state.selectedItems.clear();

    const container = state.playlistPanel || getPlaylistContext()?.panel;
    if (!container) return;

    const toggleBtn = container.querySelector('.ytplus-playlist-delete-toggle');
    const deleteBar = container.querySelector('.ytplus-playlist-delete-bar');

    if (state.deleteMode) {
      if (toggleBtn) {
        toggleBtn.classList.add('active');
        toggleBtn.setAttribute('aria-pressed', 'true');
        toggleBtn.title = t('playlistDeleteModeExit');
      }
      if (deleteBar) deleteBar.style.display = '';
      addCheckboxesToItems();
    } else {
      if (toggleBtn) {
        toggleBtn.classList.remove('active');
        toggleBtn.setAttribute('aria-pressed', 'false');
        toggleBtn.title = t('playlistDeleteMode');
      }
      if (deleteBar) deleteBar.style.display = 'none';
      removeCheckboxesFromItems();
    }
    updateDeleteBarState();
  }, 'toggleDeleteMode');

  /**
   * Add selection checkboxes to each playlist video item
   */
  const addCheckboxesToItems = withErrorBoundary(() => {
    const itemsRoot = state.itemsContainer || state.playlistPanel;
    if (!itemsRoot || !state.itemSelector) return;

    const items = itemsRoot.querySelectorAll(state.itemSelector);
    items.forEach((item, idx) => {
      if (item.querySelector('.ytplus-playlist-item-checkbox')) return;

      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      // Use shared settings checkbox styling for consistent look
      checkbox.className = 'ytplus-playlist-item-checkbox ytp-plus-settings-checkbox';
      checkbox.setAttribute('aria-label', t('playlistSelectVideo'));
      checkbox.dataset.index = String(idx);
      checkbox.style.cssText = `
        position: absolute;
        top: 8px;
        left: 8px;
        z-index: 2;
        cursor: pointer;
      `;

      checkbox.addEventListener('change', () => {
        const videoId = item.getAttribute('video-id') || `item-${idx}`;
        if (checkbox.checked) {
          state.selectedItems.add(videoId);
        } else {
          state.selectedItems.delete(videoId);
        }
        updateDeleteBarState();
      });
      checkbox.addEventListener('click', e => e.stopPropagation());

      // Ensure the parent has relative positioning for the checkbox
      item.style.position = 'relative';
      item.insertBefore(checkbox, item.firstChild);
    });
  }, 'addCheckboxesToItems');

  /**
   * Remove all checkboxes from playlist items
   */
  const removeCheckboxesFromItems = withErrorBoundary(() => {
    const itemsRoot = state.itemsContainer || state.playlistPanel;
    if (!itemsRoot) return;
    itemsRoot.querySelectorAll('.ytplus-playlist-item-checkbox').forEach(cb => cb.remove());
    state.selectedItems.clear();
  }, 'removeCheckboxesFromItems');

  /**
   * Update delete action bar button state
   */
  const updateDeleteBarState = withErrorBoundary(() => {
    const container = state.playlistPanel || getPlaylistContext()?.panel;
    if (!container) return;

    const deleteBtn = container.querySelector('.ytplus-playlist-delete-selected');
    const countSpan = container.querySelector('.ytplus-playlist-selected-count');

    if (deleteBtn) {
      deleteBtn.disabled = state.selectedItems.size === 0;
      deleteBtn.style.opacity = state.selectedItems.size > 0 ? '1' : '0.5';
    }
    if (countSpan) {
      countSpan.textContent = t('playlistSelectedCount', { count: state.selectedItems.size });
    }
  }, 'updateDeleteBarState');

  /**
   * Select all visible playlist items
   */
  const selectAllItems = withErrorBoundary(() => {
    const itemsRoot = state.itemsContainer || state.playlistPanel;
    if (!itemsRoot) return;

    itemsRoot.querySelectorAll('.ytplus-playlist-item-checkbox').forEach(cb => {
      const item = cb.closest(state.itemSelector);
      if (item && item.style.display !== 'none') {
        cb.checked = true;
        const videoId = item.getAttribute('video-id') || `item-${cb.dataset.index}`;
        state.selectedItems.add(videoId);
      }
    });
    updateDeleteBarState();
  }, 'selectAllItems');

  /**
   * Clear all checkbox selections
   */
  const clearAllItems = withErrorBoundary(() => {
    const itemsRoot = state.itemsContainer || state.playlistPanel;
    if (!itemsRoot) return;

    itemsRoot.querySelectorAll('.ytplus-playlist-item-checkbox').forEach(cb => {
      cb.checked = false;
    });
    state.selectedItems.clear();
    updateDeleteBarState();
  }, 'clearAllItems');

  /**
   * Find and click the native "Remove from playlist" menu option for a given item.
   * YouTube provides a three-dot menu on each playlist item. We simulate a click on
   * the menu button, wait for the popup, then click the remove option.
   * @param {Element} item - playlist video renderer element
   * @returns {Promise<boolean>} Whether the item was successfully removed
   */
  const removeItemViaMenu = item => {
    return new Promise(resolve => {
      try {
        // Find the three-dot menu button (⋮)
        const menuBtn =
          item.querySelector('button#button[aria-label]') ||
          item.querySelector('yt-icon-button#button') ||
          item.querySelector('ytd-menu-renderer button') ||
          item.querySelector('[aria-haspopup="menu"]') ||
          item.querySelector('button.yt-icon-button');

        if (!menuBtn) {
          console.warn('[Playlist Search] Could not find menu button for item');
          resolve(false);
          return;
        }

        // Click the menu button to open popup
        menuBtn.click();

        // Wait for the popup menu to appear
        setTimeout(() => {
          try {
            // Look for the "Remove from playlist" option in the popup
            const menuItems = document.querySelectorAll(
              'tp-yt-paper-listbox ytd-menu-service-item-renderer, ' +
                'ytd-menu-popup-renderer ytd-menu-service-item-renderer, ' +
                'tp-yt-iron-dropdown ytd-menu-service-item-renderer'
            );

            let removeOption = null;
            for (const mi of menuItems) {
              const text = (mi.textContent || '').toLowerCase();
              // Match various translations of "Remove from playlist"
              if (
                text.includes('remove') ||
                text.includes('удалить') ||
                text.includes('supprimer') ||
                text.includes('entfernen') ||
                text.includes('eliminar') ||
                text.includes('rimuovi') ||
                text.includes('kaldır') ||
                text.includes('削除') ||
                text.includes('삭제') ||
                text.includes('移除') ||
                text.includes('oʻchirish') ||
                text.includes('жою') ||
                text.includes('өчүрүү') ||
                text.includes('выдаліць') ||
                text.includes('премахване') ||
                text.includes('xóa')
              ) {
                removeOption = mi;
                break;
              }
            }

            if (removeOption) {
              removeOption.click();
              // Close any remaining popup
              setTimeout(() => {
                document.body.click();
                resolve(true);
              }, 100);
            } else {
              // Close the menu if we can't find the option
              document.body.click();
              console.warn('[Playlist Search] Could not find "Remove" option in menu');
              resolve(false);
            }
          } catch (err) {
            document.body.click();
            logError('removeItemViaMenu:findOption', err);
            resolve(false);
          }
        }, 350);
      } catch (err) {
        logError('removeItemViaMenu', err);
        resolve(false);
      }
    });
  };

  /**
   * Delete selected videos from the playlist sequentially
   */
  const deleteSelectedItems = withErrorBoundary(async () => {
    if (state.isDeleting || state.selectedItems.size === 0) return;

    const count = state.selectedItems.size;
    const confirmed = confirm(t('playlistDeleteConfirm', { count }));
    if (!confirmed) return;

    state.isDeleting = true;
    const itemsRoot = state.itemsContainer || state.playlistPanel;
    if (!itemsRoot || !state.itemSelector) {
      state.isDeleting = false;
      return;
    }

    const allItems = Array.from(itemsRoot.querySelectorAll(state.itemSelector));
    const toDelete = allItems.filter((item, idx) => {
      const videoId = item.getAttribute('video-id') || `item-${idx}`;
      return state.selectedItems.has(videoId);
    });

    let successCount = 0;
    let failCount = 0;

    for (const item of toDelete) {
      const result = await removeItemViaMenu(item);
      if (result) {
        successCount++;
      } else {
        failCount++;
      }
      // Delay between actions to let YouTube process
      await new Promise(r => setTimeout(r, config.deleteDelay));
    }

    state.isDeleting = false;
    state.selectedItems.clear();

    // Re-collect items after deletion
    setTimeout(() => {
      collectOriginalItems();
      if (state.deleteMode) {
        addCheckboxesToItems();
      }
      updateDeleteBarState();
    }, 500);

    // Notify user
    const msg =
      failCount > 0
        ? t('playlistDeletePartial', { success: successCount, fail: failCount })
        : t('playlistDeleteSuccess', { count: successCount });
    window.YouTubeUtils?.logger?.debug?.(`[Playlist Search] ${msg}`);
  }, 'deleteSelectedItems');

  /**
   * Add delete mode toggle button and action bar to the search UI
   * @param {HTMLElement} searchContainer - The .ytplus-playlist-search container
   */
  const addDeleteUI = searchContainer => {
    if (!searchContainer || searchContainer.querySelector('.ytplus-playlist-delete-toggle')) return;

    // Add styles for delete UI (once)
    addDeleteStyles();

    // Toggle button (trash icon) next to the search input
    const toggleBtn = document.createElement('button');
    toggleBtn.type = 'button';
    toggleBtn.className = 'ytplus-playlist-delete-toggle';
    toggleBtn.setAttribute('aria-pressed', 'false');
    toggleBtn.title = t('playlistDeleteMode');
    toggleBtn.innerHTML = `
      <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
           stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <polyline points="3 6 5 6 21 6"/>
        <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
        <line x1="10" y1="11" x2="10" y2="17"/>
        <line x1="14" y1="11" x2="14" y2="17"/>
      </svg>
    `;
    toggleBtn.style.cssText = `
      background: transparent;
      border: 1px solid var(--yt-spec-10-percent-layer);
      border-radius: 50%;
      width: 36px;
      height: 36px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      color: var(--yt-spec-text-secondary);
      transition: all 0.2s;
      vertical-align: middle;
      margin-left: 6px;
      flex-shrink: 0;
    `;
    toggleBtn.addEventListener('click', toggleDeleteMode);

    // Wrap search input and toggle in a flex container
    const inputWrapper = document.createElement('div');
    inputWrapper.style.cssText = 'display:flex;align-items:center;gap:6px;';
    const searchInput = searchContainer.querySelector('.ytplus-playlist-search-input');
    if (searchInput) {
      searchInput.style.width = ''; // Reset fixed width
      searchInput.style.flex = '1';
      searchInput.parentNode.insertBefore(inputWrapper, searchInput);
      inputWrapper.appendChild(searchInput);
      inputWrapper.appendChild(toggleBtn);
    }

    // Action bar (hidden initially)
    const deleteBar = document.createElement('div');
    deleteBar.className = 'ytplus-playlist-delete-bar';
    deleteBar.style.cssText = `
      display: none;
      padding: 6px 0 0;
      gap: 8px;
      align-items: center;
      flex-wrap: wrap;
    `;
    deleteBar.style.display = 'none';

    const countSpan = document.createElement('span');
    countSpan.className = 'ytplus-playlist-selected-count';
    countSpan.style.cssText = `
      font-size: 12px;
      color: var(--yt-spec-text-secondary);
      margin-right: auto;
    `;
    countSpan.textContent = t('playlistSelectedCount', { count: 0 });

    const createBtn = (label, cls, onClick) => {
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.textContent = label;
      btn.className = cls;
      btn.style.cssText = `
        padding: 5px 12px;
        border-radius: 16px;
        border: 1px solid var(--yt-spec-10-percent-layer);
        cursor: pointer;
        font-size: 12px;
        font-weight: 500;
        background: var(--yt-spec-badge-chip-background);
        color: var(--yt-spec-text-primary);
        transition: all 0.2s;
      `;
      btn.addEventListener('click', onClick);
      return btn;
    };

    const selectAllBtn = createBtn(t('selectAll'), 'ytplus-playlist-select-all', selectAllItems);
    const clearAllBtn = createBtn(t('clearAll'), 'ytplus-playlist-clear-all', clearAllItems);
    const deleteBtn = createBtn(
      t('deleteSelected'),
      'ytplus-playlist-delete-selected',
      deleteSelectedItems
    );
    deleteBtn.disabled = true;
    deleteBtn.style.opacity = '0.5';
    deleteBtn.style.background = 'rgba(255,99,71,.12)';
    deleteBtn.style.borderColor = 'rgba(255,99,71,.25)';
    deleteBtn.style.color = '#ff5c5c';

    deleteBar.append(countSpan, selectAllBtn, clearAllBtn, deleteBtn);
    searchContainer.appendChild(deleteBar);
  };

  /**
   * Add CSS styles for the delete UI components
   */
  const addDeleteStyles = () => {
    if (document.getElementById('ytplus-playlist-delete-styles')) return;
    const css = `
      .ytplus-playlist-delete-toggle.active {
        color: #ff5c5c !important;
        border-color: rgba(255,99,71,.4) !important;
        background: rgba(255,99,71,.1) !important;
      }
      .ytplus-playlist-delete-toggle:hover {
        color: var(--yt-spec-text-primary);
        border-color: var(--yt-spec-text-secondary);
      }
      .ytplus-playlist-delete-bar {
        display: flex;
      }
      .ytplus-playlist-delete-selected:not(:disabled):hover {
        background: rgba(255,99,71,.22) !important;
      }
      .ytplus-playlist-select-all:hover,
      .ytplus-playlist-clear-all:hover {
        background: var(--yt-spec-10-percent-layer) !important;
      }
      .ytplus-playlist-item-checkbox {
        opacity: 0.85;
        transition: opacity 0.15s;
      }
      .ytplus-playlist-item-checkbox:hover {
        opacity: 1;
      }
      /* Use the shared settings checkbox styling for playlist item checkboxes */
      .ytplus-playlist-item-checkbox.ytp-plus-settings-checkbox{appearance:none;-webkit-appearance:none;-moz-appearance:none;width:20px;height:20px;min-width:20px;min-height:20px;margin-left:auto;border:2px solid var(--yt-glass-border);border-radius:50%;background:transparent;display:inline-flex;align-items:center;justify-content:center;transition:all 250ms cubic-bezier(.4,0,.23,1);cursor:pointer;position:relative;flex-shrink:0;color:#fff;box-sizing:border-box;}
      html:not([dark]) .ytplus-playlist-item-checkbox.ytp-plus-settings-checkbox{border-color:rgba(0,0,0,.25);color:#222;}
      .ytplus-playlist-item-checkbox.ytp-plus-settings-checkbox:focus-visible{outline:2px solid var(--yt-accent);outline-offset:2px;}
      .ytplus-playlist-item-checkbox.ytp-plus-settings-checkbox:hover{background:var(--yt-hover-bg);transform:scale(1.1);}
      .ytplus-playlist-item-checkbox.ytp-plus-settings-checkbox::before{content:"";width:5px;height:2px;background:var(--yt-text-primary);position:absolute;transform:rotate(45deg);top:6px;left:3px;transition:width 100ms ease 50ms,opacity 50ms;transform-origin:0% 0%;opacity:0;}
      .ytplus-playlist-item-checkbox.ytp-plus-settings-checkbox::after{content:"";width:0;height:2px;background:var(--yt-text-primary);position:absolute;transform:rotate(305deg);top:11px;left:7px;transition:width 100ms ease,opacity 50ms;transform-origin:0% 0%;opacity:0;}
      .ytplus-playlist-item-checkbox.ytp-plus-settings-checkbox:checked{transform:rotate(0deg) scale(1.15);}
      .ytplus-playlist-item-checkbox.ytp-plus-settings-checkbox:checked::before{width:9px;opacity:1;background:#fff;transition:width 150ms ease 100ms,opacity 150ms ease 100ms;}
      .ytplus-playlist-item-checkbox.ytp-plus-settings-checkbox:checked::after{width:16px;opacity:1;background:#fff;transition:width 150ms ease 250ms,opacity 150ms ease 250ms;}
    `;
    try {
      if (window.YouTubeUtils?.StyleManager) {
        window.YouTubeUtils.StyleManager.add('ytplus-playlist-delete-styles', css);
        return;
      }
    } catch {}
    const style = document.createElement('style');
    style.id = 'ytplus-playlist-delete-styles';
    style.textContent = css;
    (document.head || document.documentElement).appendChild(style);
  };

  // Clean up search UI
  const cleanup = () => {
    // Exit delete mode if active
    if (state.deleteMode) {
      removeCheckboxesFromItems();
      state.deleteMode = false;
    }
    state.isDeleting = false;
    state.selectedItems.clear();

    const searchUI = qs('.ytplus-playlist-search');
    if (searchUI) {
      searchUI.remove();
    }

    // Disconnect mutation observer
    if (state.mutationObserver) {
      state.mutationObserver.disconnect();
      state.mutationObserver = null;
    }

    // Cancel any pending RAF
    if (state.rafId) {
      cancelAnimationFrame(state.rafId);
      state.rafId = null;
    }

    // Clear cache
    state.itemsCache.clear();

    state.searchInput = null;
    state.originalItems = [];
    state.currentPlaylistId = null;
    state.itemsContainer = null;
    state.itemSelector = null;
    state.itemTagName = null;
    state.playlistPanel = null;
    state.isPlaylistPage = false;
  };

  // Handle navigation changes with debouncing
  const handleNavigation = debounce(() => {
    if (!featureEnabled) {
      cleanup();
      return;
    }
    if (!shouldRunOnThisPage()) {
      cleanup();
      return;
    }
    // Check if we're still on a playlist page
    const newPlaylistId = getCurrentPlaylistId();

    // If playlist hasn't changed and UI exists, no action needed
    if (newPlaylistId === state.currentPlaylistId && qs('.ytplus-playlist-search')) {
      return;
    }

    cleanup();

    // Only add UI if we're on a playlist page
    if (newPlaylistId) {
      setTimeout(addSearchUI, 300);
    }
  }, 250);

  let initialized = false;

  const ensureInit = () => {
    if (initialized || !featureEnabled || !isRelevantRoute()) return;
    initialized = true;

    const run = () => {
      loadSettings();
      if (!featureEnabled || config.enabled === false) return;
      addSearchUI();
    };

    if (typeof requestIdleCallback === 'function') {
      requestIdleCallback(run, { timeout: 1500 });
    } else {
      setTimeout(run, 0);
    }
  };

  const handleNavigate = () => {
    if (!isRelevantRoute()) {
      cleanup();
      return;
    }
    ensureInit();
    handleNavigation();
  };

  onDomReady(ensureInit);

  if (window.YouTubeUtils?.cleanupManager?.registerListener) {
    YouTubeUtils.cleanupManager.registerListener(document, 'yt-navigate-finish', handleNavigate, {
      passive: true,
    });
    YouTubeUtils.cleanupManager.registerListener(window, 'beforeunload', cleanup, {
      passive: true,
    });
  } else {
    document.addEventListener('yt-navigate-finish', handleNavigate);
    window.addEventListener('beforeunload', cleanup);
  }

  window.addEventListener('youtube-plus-settings-updated', e => {
    try {
      const nextEnabled = e?.detail?.enablePlaylistSearch !== false;
      if (nextEnabled === featureEnabled) return;
      setFeatureEnabled(nextEnabled);
    } catch {
      setFeatureEnabled(loadFeatureEnabled());
    }
  });
})();

// --- MODULE: thumbnail.js ---

(function () {
  'use strict';

  // DOM cache helpers with fallback
  const qs = selector => {
    if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.get === 'function') {
      return window.YouTubeDOMCache.get(selector);
    }
    return document.querySelector(selector);
  };
  const qsAll = selector => {
    if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.getAll === 'function') {
      return window.YouTubeDOMCache.getAll(selector);
    }
    return document.querySelectorAll(selector);
  };

  // Use centralized i18n from YouTubePlusI18n or YouTubeUtils
  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    // Fallback for initialization phase
    if (!key) return '';
    let result = String(key);
    for (const [k, v] of Object.entries(params || {})) {
      result = result.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
    }
    return result;
  };

  const SETTINGS_KEY = 'youtube_plus_settings';
  const DEFAULT_ENABLE_THUMBNAIL = true;

  function loadEnableThumbnail() {
    try {
      const raw = localStorage.getItem(SETTINGS_KEY);
      if (!raw) return DEFAULT_ENABLE_THUMBNAIL;
      const parsed = JSON.parse(raw);
      return parsed?.enableThumbnail !== false;
    } catch {
      return DEFAULT_ENABLE_THUMBNAIL;
    }
  }

  let thumbnailFeatureEnabled = loadEnableThumbnail();
  const isEnabled = () => thumbnailFeatureEnabled;

  let started = false;
  let startScheduled = false;
  /** @type {MutationObserver|null} */
  let mutationObserver = null;
  /** @type {null | (() => void)} */
  let urlChangeCleanup = null;
  let thumbnailStylesInjected = false;

  /**
   * Extract video ID from thumbnail source with validation
   * @param {string} thumbnailSrc - Thumbnail source URL
   * @returns {string|null} Video ID or null if invalid
   */
  function extractVideoId(thumbnailSrc) {
    try {
      if (!thumbnailSrc || typeof thumbnailSrc !== 'string') return null;
      const match = thumbnailSrc.match(/\/vi\/([^\/]+)\//);
      const videoId = match ? match[1] : null;
      // Validate video ID format (11 characters, alphanumeric + - and _)
      if (videoId && !/^[a-zA-Z0-9_-]{11}$/.test(videoId)) {
        console.warn('[YouTube+][Thumbnail]', 'Invalid video ID format:', videoId);
        return null;
      }
      return videoId;
    } catch (error) {
      console.error('[YouTube+][Thumbnail]', 'Error extracting video ID:', error);
      return null;
    }
  }

  /**
   * Extract shorts ID from URL with validation
   * @param {string} href - URL to extract shorts ID from
   * @returns {string|null} Shorts ID or null if invalid
   */
  function extractShortsId(href) {
    try {
      if (!href || typeof href !== 'string') return null;
      const match = href.match(/\/shorts\/([^\/\?]+)/);
      const shortsId = match ? match[1] : null;
      // Validate shorts ID format (11 characters, alphanumeric + - and _)
      if (shortsId && !/^[a-zA-Z0-9_-]{11}$/.test(shortsId)) {
        console.warn('[YouTube+][Thumbnail]', 'Invalid shorts ID format:', shortsId);
        return null;
      }
      return shortsId;
    } catch (error) {
      console.error('[YouTube+][Thumbnail]', 'Error extracting shorts ID:', error);
      return null;
    }
  }

  /**
   * Check if image exists with timeout and error handling
   * @param {string} url - Image URL to check
   * @returns {Promise<boolean>} True if image exists and is accessible
   */
  /**
   * Validate URL string format
   * @param {string} url - URL to validate
   * @returns {boolean} True if valid
   */
  function isValidUrlString(url) {
    if (!url || typeof url !== 'string') {
      console.warn('[YouTube+][Thumbnail]', 'Invalid URL provided');
      return false;
    }
    return true;
  }

  /**
   * Validate URL protocol (HTTPS only)
   * @param {URL} parsedUrl - Parsed URL object
   * @returns {boolean} True if valid
   */
  function hasValidProtocol(parsedUrl) {
    if (parsedUrl.protocol !== 'https:') {
      console.warn('[YouTube+][Thumbnail]', 'Only HTTPS URLs are allowed');
      return false;
    }
    return true;
  }

  /**
   * Validate URL domain (YouTube only)
   * @param {URL} parsedUrl - Parsed URL object
   * @returns {boolean} True if valid
   */
  function hasValidDomain(parsedUrl) {
    const { hostname } = parsedUrl;
    if (!hostname.endsWith('ytimg.com') && !hostname.endsWith('youtube.com')) {
      console.warn('[YouTube+][Thumbnail]', 'Only YouTube image domains are allowed');
      return false;
    }
    return true;
  }

  /**
   * Parse and validate URL
   * @param {string} url - URL to parse
   * @returns {URL|null} Parsed URL or null if invalid
   */
  function parseAndValidateUrl(url) {
    try {
      const parsedUrl = new URL(url);
      if (!hasValidProtocol(parsedUrl)) return null;
      if (!hasValidDomain(parsedUrl)) return null;
      return parsedUrl;
    } catch (error) {
      console.error('[YouTube+][Thumbnail]', 'Invalid URL:', error);
      return null;
    }
  }

  /**
   * Check image via HEAD request
   * @param {string} url - Image URL
   * @returns {Promise<boolean>} True if image exists
   */
  async function checkViaHeadRequest(url) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 5000);

    try {
      const response = await fetch(url, {
        method: 'HEAD',
        signal: controller.signal,
      }).catch(() => null);

      clearTimeout(timeoutId);
      return response ? response.ok : true;
    } catch {
      clearTimeout(timeoutId);
      return null;
    }
  }

  /**
   * Cleanup image element
   * @param {HTMLImageElement} img - Image element
   */
  function cleanupImageElement(img) {
    if (img.parentNode) {
      document.body.removeChild(img);
    }
  }

  /**
   * Check image via image load test
   * @param {string} url - Image URL
   * @returns {Promise<boolean>} True if image loads
   */
  function checkViaImageLoad(url) {
    return new Promise(resolve => {
      const img = document.createElement('img');
      img.style.display = 'none';

      const timeout = setTimeout(() => {
        cleanupImageElement(img);
        resolve(false);
      }, 3000);

      img.onload = () => {
        clearTimeout(timeout);
        cleanupImageElement(img);
        resolve(true);
      };

      img.onerror = () => {
        clearTimeout(timeout);
        cleanupImageElement(img);
        resolve(false);
      };

      document.body.appendChild(img);
      img.src = url;
    });
  }

  /**
   * Check if image exists at URL
   * @param {string} url - Image URL to check
   * @returns {Promise<boolean>} True if image exists
   */
  async function checkImageExists(url) {
    try {
      if (!isValidUrlString(url)) return false;

      const parsedUrl = parseAndValidateUrl(url);
      if (!parsedUrl) return false;

      // Try HEAD request first
      const headResult = await checkViaHeadRequest(url);
      if (headResult !== null) return headResult;

      // Fallback to image load test
      return await checkViaImageLoad(url);
    } catch (error) {
      console.error('[YouTube+][Thumbnail]', 'Error checking image:', error);
      return false;
    }
  }

  function createSpinner() {
    const spinner = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    spinner.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    spinner.setAttribute('width', '16');
    spinner.setAttribute('height', '16');
    spinner.setAttribute('viewBox', '0 0 24 24');
    spinner.setAttribute('fill', 'none');
    spinner.setAttribute('stroke', 'white');
    spinner.setAttribute('stroke-width', '2');
    spinner.setAttribute('stroke-linecap', 'round');
    spinner.setAttribute('stroke-linejoin', 'round');

    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute('d', 'M21 12a9 9 0 1 1-6.219-8.56');
    spinner.appendChild(path);

    spinner.style.animation = 'spin 1s linear infinite';

    if (!qs('#spinner-keyframes')) {
      const style = document.createElement('style');
      style.id = 'spinner-keyframes';
      style.textContent = `
                @keyframes spin {
                    from { transform: rotate(0deg); }
                    to { transform: rotate(360deg); }
                }
            `;
      (document.head || document.documentElement).appendChild(style);
    }

    return spinner;
  }

  /**
   * Open thumbnail in modal with error handling
   * @param {string} videoId - YouTube video ID
   * @param {boolean} isShorts - Whether this is a Shorts video
   * @param {HTMLElement} overlayElement - Overlay element containing the button
   * @returns {Promise<void>}
   */
  /**
   * Validate video ID format
   * @param {string} videoId - Video ID to validate
   * @returns {boolean} True if valid
   */
  function isValidVideoId(videoId) {
    return videoId && typeof videoId === 'string' && /^[a-zA-Z0-9_-]{11}$/.test(videoId);
  }

  /**
   * Validate overlay element
   * @param {HTMLElement} overlayElement - Overlay element to validate
   * @returns {boolean} True if valid
   */
  function isValidOverlayElement(overlayElement) {
    return overlayElement && overlayElement instanceof HTMLElement;
  }

  /**
   * Get thumbnail URLs for shorts
   * @param {string} videoId - Video ID
   * @returns {{primary: string, fallback: string}} Thumbnail URLs
   */
  function getShortsThumbnailUrls(videoId) {
    return {
      primary: `https://i.ytimg.com/vi/${videoId}/oardefault.jpg`,
      fallback: `https://i.ytimg.com/vi/${videoId}/oar2.jpg`,
    };
  }

  /**
   * Get thumbnail URLs for regular videos
   * @param {string} videoId - Video ID
   * @returns {{primary: string, fallback: string}} Thumbnail URLs
   */
  function getVideoThumbnailUrls(videoId) {
    return {
      primary: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`,
      fallback: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`,
    };
  }

  /**
   * Load and show best available thumbnail
   * @param {string} videoId - Video ID
   * @param {boolean} isShorts - Whether this is a shorts video
   */
  async function loadAndShowThumbnail(videoId, isShorts) {
    const urls = isShorts ? getShortsThumbnailUrls(videoId) : getVideoThumbnailUrls(videoId);
    const isPrimaryAvailable = await checkImageExists(urls.primary);
    showImageModal(isPrimaryAvailable ? urls.primary : urls.fallback);
  }

  /**
   * Replace SVG with spinner
   * @param {HTMLElement} overlayElement - Overlay element
   * @param {SVGElement} originalSvg - Original SVG element
   * @returns {HTMLElement} Spinner element
   */
  function replaceWithSpinner(overlayElement, originalSvg) {
    const spinner = createSpinner();
    overlayElement.replaceChild(spinner, originalSvg);
    return spinner;
  }

  /**
   * Restore original SVG after loading
   * @param {HTMLElement} overlayElement - Overlay element
   * @param {HTMLElement} spinner - Spinner element
   * @param {SVGElement} originalSvg - Original SVG element
   */
  function restoreOriginalSvg(overlayElement, spinner, originalSvg) {
    try {
      if (spinner && spinner.parentNode) {
        overlayElement.replaceChild(originalSvg, spinner);
      }
    } catch (restoreError) {
      console.error('[YouTube+][Thumbnail]', 'Error restoring original SVG:', restoreError);
      if (spinner && spinner.parentNode) {
        spinner.parentNode.removeChild(spinner);
      }
    }
  }

  /**
   * Open thumbnail in modal viewer
   * @param {string} videoId - Video ID
   * @param {boolean} isShorts - Whether this is a shorts video
   * @param {HTMLElement} overlayElement - Overlay element
   */
  async function openThumbnail(videoId, isShorts, overlayElement) {
    try {
      if (!isValidVideoId(videoId)) {
        console.error('[YouTube+][Thumbnail]', 'Invalid video ID:', videoId);
        return;
      }

      if (!isValidOverlayElement(overlayElement)) {
        console.error('[YouTube+][Thumbnail]', 'Invalid overlay element');
        return;
      }

      const originalSvg = overlayElement.querySelector('svg');
      if (!originalSvg) {
        console.warn('[YouTube+][Thumbnail]', 'No SVG found in overlay element');
        return;
      }

      const spinner = replaceWithSpinner(overlayElement, originalSvg);

      try {
        await loadAndShowThumbnail(videoId, isShorts);
      } finally {
        restoreOriginalSvg(overlayElement, spinner, originalSvg);
      }
    } catch (error) {
      console.error('[YouTube+][Thumbnail]', 'Error opening thumbnail:', error);
    }
  }

  function ensureThumbnailStyles() {
    if (thumbnailStylesInjected) return;
    try {
      const css = `
        :root { --thumbnail-btn-bg-light: rgba(255, 255, 255, 0.85); --thumbnail-btn-bg-dark: rgba(0, 0, 0, 0.7); --thumbnail-btn-hover-bg-light: rgba(255, 255, 255, 1); --thumbnail-btn-hover-bg-dark: rgba(0, 0, 0, 0.9); --thumbnail-btn-color-light: #222; --thumbnail-btn-color-dark: #fff; --thumbnail-modal-bg-light: rgba(255, 255, 255, 0.95); --thumbnail-modal-bg-dark: rgba(34, 34, 34, 0.85); --thumbnail-modal-title-light: #222; --thumbnail-modal-title-dark: #fff; --thumbnail-modal-btn-bg-light: rgba(0, 0, 0, 0.08); --thumbnail-modal-btn-bg-dark: rgba(255, 255, 255, 0.08); --thumbnail-modal-btn-hover-bg-light: rgba(0, 0, 0, 0.18); --thumbnail-modal-btn-hover-bg-dark: rgba(255, 255, 255, 0.18); --thumbnail-modal-btn-color-light: #222; --thumbnail-modal-btn-color-dark: #fff; --thumbnail-modal-btn-hover-color-light: #ff4444; --thumbnail-modal-btn-hover-color-dark: #ff4444; --thumbnail-glass-blur: blur(18px) saturate(180%); --thumbnail-glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); --thumbnail-glass-border: rgba(255, 255, 255, 0.2); }
        html[dark], body[dark] { --thumbnail-btn-bg: var(--thumbnail-btn-bg-dark); --thumbnail-btn-hover-bg: var(--thumbnail-btn-hover-bg-dark); --thumbnail-btn-color: var(--thumbnail-btn-color-dark); --thumbnail-modal-bg: var(--thumbnail-modal-bg-dark); --thumbnail-modal-title: var(--thumbnail-modal-title-dark); --thumbnail-modal-btn-bg: var(--thumbnail-modal-btn-bg-dark); --thumbnail-modal-btn-hover-bg: var(--thumbnail-modal-btn-hover-bg-dark); --thumbnail-modal-btn-color: var(--thumbnail-modal-btn-color-dark); --thumbnail-modal-btn-hover-color: var(--thumbnail-modal-btn-hover-color-dark); }
        html:not([dark]) { --thumbnail-btn-bg: var(--thumbnail-btn-bg-light); --thumbnail-btn-bg: var(--thumbnail-btn-bg-light); --thumbnail-btn-hover-bg: var(--thumbnail-btn-hover-bg-light); --thumbnail-btn-color: var(--thumbnail-btn-color-light); --thumbnail-modal-bg: var(--thumbnail-modal-bg-light); --thumbnail-modal-title: var(--thumbnail-modal-title-light); --thumbnail-modal-btn-bg: var(--thumbnail-modal-btn-bg-light); --thumbnail-modal-btn-hover-bg: var(--thumbnail-modal-btn-hover-bg-light); --thumbnail-modal-btn-color: var(--thumbnail-modal-btn-color-light); --thumbnail-modal-btn-hover-color: var(--thumbnail-modal-btn-hover-color-light); }
        .thumbnail-overlay-container { position: absolute; bottom: 8px; left: 8px; z-index: 9999; opacity: 0; transition: opacity 0.2s ease; }
        .thumbnail-overlay-button { width: 28px; height: 28px; background: var(--thumbnail-btn-bg); border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--thumbnail-btn-color); position: relative; box-shadow: var(--thumbnail-glass-shadow); backdrop-filter: var(--thumbnail-glass-blur); -webkit-backdrop-filter: var(--thumbnail-glass-blur); border: 1px solid var(--thumbnail-glass-border); }
        .thumbnail-overlay-button:hover { background: var(--thumbnail-btn-hover-bg); }
        .thumbnail-dropdown { position: absolute; bottom: 100%; left: 0; background: var(--thumbnail-btn-hover-bg); border-radius: 8px; padding: 4px; margin-bottom: 4px; display: none; flex-direction: column; min-width: 140px; box-shadow: var(--thumbnail-glass-shadow); z-index: 10000; backdrop-filter: var(--thumbnail-glass-blur); -webkit-backdrop-filter: var(--thumbnail-glass-blur); border: 1px solid var(--thumbnail-glass-border); }
        .thumbnail-dropdown.show { display: flex !important; }
        .thumbnail-dropdown-item { background: none; border: none; color: var(--thumbnail-btn-color); padding: 8px 12px; cursor: pointer; border-radius: 4px; font-size: 12px; text-align: left; white-space: nowrap; transition: background-color 0.2s ease; }
        .thumbnail-dropdown-item:hover { background: rgba(255,255,255,0.06); }
        .thumbnailPreview-button { position: absolute; bottom: 10px; left: 5px; background-color: var(--thumbnail-btn-bg); color: var(--thumbnail-btn-color); border: none; border-radius: 6px; padding: 3px; font-size: 18px; cursor: pointer; z-index: 2000; opacity: 0; transition: opacity 0.3s; display: flex; align-items: center; justify-content: center; box-shadow: var(--thumbnail-glass-shadow); backdrop-filter: var(--thumbnail-glass-blur); -webkit-backdrop-filter: var(--thumbnail-glass-blur); border: 1px solid var(--thumbnail-glass-border); }
        .thumbnailPreview-container { position: relative; }
        .thumbnailPreview-container:hover .thumbnailPreview-button { opacity: 1; }
        .thumbnail-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.55); z-index: 100000; display: flex; align-items: center; justify-content: center; animation: fadeInModal 0.22s cubic-bezier(.4,0,.2,1); backdrop-filter: blur(8px) saturate(140%); -webkit-backdrop-filter: blur(8px) saturate(140%); }
        .thumbnail-modal-content { background: var(--thumbnail-modal-bg); border-radius: 20px; box-shadow: 0 12px 40px rgba(0,0,0,0.45); max-width: 78vw; max-height: 90vh; overflow: auto; position: relative; display: flex; flex-direction: column; align-items: center; animation: scaleInModal 0.22s cubic-bezier(.4,0,.2,1); border: 1.5px solid var(--thumbnail-glass-border); backdrop-filter: blur(14px) saturate(150%); -webkit-backdrop-filter: blur(14px) saturate(150%);}
        /* Wrapper to place content and action buttons side-by-side */
        .thumbnail-modal-wrapper { display: flex; align-items: flex-start; gap: 12px; }
        .thumbnail-modal-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }
        .thumbnail-modal-action-btn { width: 40px; height: 40px; border-radius: 50%; background: var(--thumbnail-modal-btn-bg); border: 1px solid rgba(0,0,0,0.08); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,0.2); transition: transform 0.12s ease, background 0.12s ease; color: var(--thumbnail-modal-btn-color); }
        .thumbnail-modal-action-btn:hover { transform: translateY(-2px); }
        .thumbnail-modal-close { }
        .thumbnail-modal-open { }
        .thumbnail-modal-img { max-width: 72vw; max-height: 70vh; box-shadow: var(--thumbnail-glass-shadow); background: #222; border: 1px solid var(--thumbnail-glass-border); }
        .thumbnail-modal-options { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }
        .thumbnail-modal-option-btn { background: var(--thumbnail-modal-btn-bg); color: var(--thumbnail-modal-btn-color); border: none; border-radius: 8px; padding: 8px 18px; font-size: 14px; cursor: pointer; transition: background 0.2s; margin-bottom: 6px; box-shadow: var(--thumbnail-glass-shadow); backdrop-filter: var(--thumbnail-glass-blur); -webkit-backdrop-filter: var(--thumbnail-glass-blur); border: 1px solid var(--thumbnail-glass-border); }
        .thumbnail-modal-option-btn:hover { background: var(--thumbnail-modal-btn-hover-bg); color: var(--thumbnail-modal-btn-hover-color); }
        .thumbnail-modal-title { font-size: 18px; font-weight: 600; color: var(--thumbnail-modal-title); margin-bottom: 10px; text-align: center; text-shadow: 0 2px 8px rgba(0,0,0,0.15); }
        @keyframes fadeInModal { from { opacity: 0; } to { opacity: 1; } }
        @keyframes scaleInModal { from { transform: scale(0.95); } to { transform: scale(1); } }
      `;

      if (
        window.YouTubeUtils &&
        YouTubeUtils.StyleManager &&
        typeof YouTubeUtils.StyleManager.add === 'function'
      ) {
        YouTubeUtils.StyleManager.add('thumbnail-viewer-styles', css);
      } else {
        const s = document.createElement('style');
        s.id = 'ytplus-thumbnail-styles';
        s.textContent = css;
        (document.head || document.documentElement).appendChild(s);
      }
      thumbnailStylesInjected = true;
    } catch {
      // fallback: inject minimal styles
      if (!document.getElementById('ytplus-thumbnail-styles')) {
        const s = document.createElement('style');
        s.id = 'ytplus-thumbnail-styles';
        s.textContent = '.thumbnail-modal-img{max-width:72vw;max-height:70vh;}';
        (document.head || document.documentElement).appendChild(s);
      }
      thumbnailStylesInjected = true;
    }
  }

  function removeThumbnailStyles() {
    try {
      if (window.YouTubeUtils?.StyleManager?.remove) {
        window.YouTubeUtils.StyleManager.remove('thumbnail-viewer-styles');
      }
    } catch {}

    const el = document.getElementById('ytplus-thumbnail-styles');
    if (el) {
      try {
        el.remove();
      } catch {}
    }

    thumbnailStylesInjected = false;
  }

  /**
   * Validate modal URL security
   * @param {string} url - URL to validate
   * @returns {boolean} True if valid
   */
  function validateModalUrl(url) {
    if (!url || typeof url !== 'string') {
      console.error('[YouTube+][Thumbnail]', 'Invalid URL provided to modal');
      return false;
    }

    try {
      const parsedUrl = new URL(url);
      if (parsedUrl.protocol !== 'https:') {
        console.error('[YouTube+][Thumbnail]', 'Only HTTPS URLs are allowed');
        return false;
      }
      const allowedDomains = ['ytimg.com', 'youtube.com', 'ggpht.com', 'googleusercontent.com'];
      if (!allowedDomains.some(d => parsedUrl.hostname.endsWith(d))) {
        console.error('[YouTube+][Thumbnail]', 'Image domain not allowed:', parsedUrl.hostname);
        return false;
      }
      return true;
    } catch (urlError) {
      console.error('[YouTube+][Thumbnail]', 'Invalid URL format:', urlError);
      return false;
    }
  }

  /**
   * Create modal image element
   * @param {string} url - Image URL
   * @returns {HTMLImageElement} Image element
   */
  function createModalImage(url) {
    const img = document.createElement('img');
    img.className = 'thumbnail-modal-img';
    img.src = url;
    img.alt = t('thumbnailPreview');
    img.title = '';
    img.style.cursor = 'pointer';
    img.addEventListener('click', () => window.open(img.src, '_blank'));
    return img;
  }

  /**
   * Create close button for modal
   * @param {HTMLElement} overlay - Overlay element to remove on click
   * @returns {HTMLButtonElement} Close button
   */
  function createCloseButton(overlay) {
    const closeBtn = document.createElement('button');
    closeBtn.className = 'thumbnail-modal-close thumbnail-modal-action-btn';
    closeBtn.innerHTML = `\n            <svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n                <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>\n            </svg>\n            `;
    closeBtn.title = t('close');
    closeBtn.setAttribute('aria-label', t('close'));
    closeBtn.addEventListener('click', e => {
      e.preventDefault();
      e.stopPropagation();
      overlay.remove();
    });
    return closeBtn;
  }

  /**
   * Create open in new tab button for modal
   * @param {HTMLImageElement} img - Image element
   * @returns {HTMLButtonElement} New tab button
   */
  function createNewTabButton(img) {
    const newTabBtn = document.createElement('button');
    newTabBtn.className = 'thumbnail-modal-open thumbnail-modal-action-btn';
    newTabBtn.innerHTML = `\n            <svg fill="currentColor" viewBox="0 0 24 24" width="18" height="18" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">\n        <g id="SVGRepo_bgCarrier" stroke-width="0"></g>\n        <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>\n        <g id="SVGRepo_iconCarrier"><path d="M14.293,9.707a1,1,0,0,1,0-1.414L18.586,4H16a1,1,0,0,1,0-2h5a1,1,0,0,1,1,1V8a1,1,0,0,1-2,0V5.414L15.707,9.707a1,1,0,0,1-1.414,0ZM3,22H8a1,1,0,0,0,0-2H5.414l4.293-4.293a1,1,0,0,0-1.414-1.414L4,18.586V16a1,1,0,0,0-2,0v5A1,1,0,0,0,3,22Z"></path></g>\n      </svg>\n        `;
    newTabBtn.title = t('clickToOpen');
    newTabBtn.setAttribute('aria-label', t('clickToOpen'));
    newTabBtn.addEventListener('click', e => {
      e.preventDefault();
      e.stopPropagation();
      window.open(img.src, '_blank');
    });
    return newTabBtn;
  }

  /**
   * Download image as blob
   * @param {string} imgSrc - Image source URL
   * @returns {Promise<void>}
   */
  async function downloadImageAsBlob(imgSrc) {
    const response = await fetch(imgSrc);
    if (!response.ok) throw new Error('Network response was not ok');
    const blob = await response.blob();
    const blobUrl = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = blobUrl;

    try {
      const urlObj = new URL(imgSrc);
      const segments = urlObj.pathname.split('/');
      a.download = segments[segments.length - 1] || 'thumbnail.jpg';
    } catch {
      a.download = 'thumbnail.jpg';
    }

    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(() => URL.revokeObjectURL(blobUrl), 1500);
  }

  /**
   * Create download button for modal
   * @param {HTMLImageElement} img - Image element
   * @returns {HTMLButtonElement} Download button
   */
  function createDownloadButton(img) {
    const downloadBtn = document.createElement('button');
    downloadBtn.className = 'thumbnail-modal-download thumbnail-modal-action-btn';
    downloadBtn.innerHTML = `\n            <svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n                <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>\n                <polyline points="7 10 12 15 17 10"/>\n                <line x1="12" y1="15" x2="12" y2="3"/>\n            </svg>\n        `;
    downloadBtn.title = t('download');
    downloadBtn.setAttribute('aria-label', t('download'));
    downloadBtn.addEventListener('click', async e => {
      e.preventDefault();
      e.stopPropagation();
      try {
        await downloadImageAsBlob(img.src);
      } catch {
        window.open(img.src, '_blank');
      }
    });
    return downloadBtn;
  }

  /**
   * Setup modal keyboard handlers
   * @param {HTMLElement} overlay - Overlay element
   */
  function setupModalKeyboard(overlay) {
    function escHandler(e) {
      if (e.key === 'Escape') {
        overlay.remove();
        window.removeEventListener('keydown', escHandler, true);
      }
    }
    window.addEventListener('keydown', escHandler, true);
  }

  /**
   * Setup modal image error handler
   * @param {HTMLImageElement} img - Image element
   * @param {HTMLElement} content - Content container
   */
  function setupImageErrorHandler(img, content) {
    img.addEventListener('error', () => {
      const err = document.createElement('div');
      err.textContent = t('thumbnailLoadFailed');
      err.style.color = 'white';
      content.appendChild(err);
    });
  }

  /**
   * Show image in modal with error handling and security
   * @param {string} url - Image URL to display
   * @returns {void}
   */
  function showImageModal(url) {
    try {
      if (!isEnabled()) return;
      if (!validateModalUrl(url)) return;

      // Remove existing modals
      qsAll('.thumbnail-modal-overlay').forEach(m => m.remove());

      const overlay = document.createElement('div');
      overlay.className = 'thumbnail-modal-overlay';

      const content = document.createElement('div');
      content.className = 'thumbnail-modal-content';

      const img = createModalImage(url);

      const optionsDiv = document.createElement('div');
      optionsDiv.className = 'thumbnail-modal-options';

      const closeBtn = createCloseButton(overlay);
      const newTabBtn = createNewTabButton(img);
      const downloadBtn = createDownloadButton(img);

      content.appendChild(img);
      content.appendChild(optionsDiv);

      const wrapper = document.createElement('div');
      wrapper.className = 'thumbnail-modal-wrapper';

      const actionsDiv = document.createElement('div');
      actionsDiv.className = 'thumbnail-modal-actions';

      actionsDiv.appendChild(closeBtn);
      actionsDiv.appendChild(newTabBtn);
      actionsDiv.appendChild(downloadBtn);

      wrapper.appendChild(content);
      wrapper.appendChild(actionsDiv);
      overlay.appendChild(wrapper);

      overlay.addEventListener('click', ({ target }) => {
        if (target === overlay) overlay.remove();
      });

      setupModalKeyboard(overlay);
      setupImageErrorHandler(img, content);

      document.body.appendChild(overlay);
    } catch (error) {
      console.error('[YouTube+][Thumbnail]', 'Error showing modal:', error);
    }
  }

  let thumbnailPreviewCurrentVideoId = '';
  let thumbnailPreviewClosed = false;
  let thumbnailInsertionAttempts = 0;
  const MAX_ATTEMPTS = 10;
  const RETRY_DELAY = 500;

  function isWatchPage() {
    const url = new URL(window.location.href);
    return url.pathname === '/watch' && url.searchParams.has('v');
  }

  /**
   * Get current video ID from URL
   * @returns {string|null} Video ID or null
   */
  function getCurrentVideoId() {
    return new URLSearchParams(window.location.search).get('v');
  }

  /**
   * Remove old thumbnail overlay
   */
  function removeOldOverlay() {
    const oldOverlay = qs('#thumbnailPreview-player-overlay');
    if (oldOverlay) {
      oldOverlay.remove();
    }
  }

  /**
   * Check if thumbnail update should be skipped
   * @param {string|null} newVideoId - New video ID
   * @returns {boolean} True if should skip
   */
  function shouldSkipThumbnailUpdate(newVideoId) {
    return !newVideoId || newVideoId === thumbnailPreviewCurrentVideoId || thumbnailPreviewClosed;
  }

  /**
   * Find player element with retry logic
   * @returns {HTMLElement|null} Player element or null
   */
  function findPlayerElement() {
    return qs('#movie_player') || qs('ytd-player');
  }

  /**
   * Create thumbnail overlay for player
   * @param {string} videoId - Video ID
   * @param {HTMLElement} player - Player element
   * @returns {HTMLElement} Created overlay element
   */
  function createPlayerThumbnailOverlay(videoId, player) {
    const overlay = /** @type {any} */ (createThumbnailOverlay(videoId, player));
    overlay.id = 'thumbnailPreview-player-overlay';
    overlay.dataset.videoId = videoId;
    overlay.style.cssText = `
      position: absolute;
      top: 10%;
      right: 8px;
      width: 36px;
      height: 36px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 6px;
      cursor: pointer;
      z-index: 1001;
      transition: all 0.15s ease;
      opacity: 0;
    `;
    return overlay;
  }

  /**
   * Attempt to insert thumbnail overlay
   */
  function attemptInsertion() {
    const player = findPlayerElement();

    if (!player) {
      thumbnailInsertionAttempts++;
      if (thumbnailInsertionAttempts < MAX_ATTEMPTS) {
        setTimeout(attemptInsertion, RETRY_DELAY);
      } else {
        thumbnailInsertionAttempts = 0;
      }
      return;
    }

    const overlayId = 'thumbnailPreview-player-overlay';
    let overlay = player.querySelector(`#${overlayId}`);

    if (!overlay) {
      overlay = createPlayerThumbnailOverlay(thumbnailPreviewCurrentVideoId, player);

      // Add hover and focus behaviour so overlay becomes fully visible when interacted with
      overlay.tabIndex = 0; // make focusable for keyboard users
      overlay.onmouseenter = () => {
        try {
          overlay.style.opacity = '0.5';
        } catch {}
      };
      overlay.onmouseleave = () => {
        try {
          overlay.style.opacity = '0';
        } catch {}
      };
      overlay.onfocus = () => {
        try {
          overlay.style.opacity = '0.5';
        } catch {}
      };
      overlay.onblur = () => {
        try {
          overlay.style.opacity = '0';
        } catch {}
      };
      // allow Enter/Space to open the thumbnail
      overlay.addEventListener('keydown', e => {
        // cast to KeyboardEvent for lint/type safety
        const ke = /** @type {KeyboardEvent} */ (e);
        if (ke && (ke.key === 'Enter' || ke.key === ' ')) {
          ke.preventDefault();
          overlay.click();
        }
      });

      // ensure the player is positioned to allow absolute child
      const playerAny = /** @type {any} */ (player);
      if (/** @type {any} */ (getComputedStyle(playerAny)).position === 'static') {
        playerAny.style.position = 'relative';
      }
      playerAny.appendChild(overlay);
      return;
    }

    // overlay already exists — verify it matches current video ID, otherwise remove and recreate
    if (overlay.dataset.videoId !== thumbnailPreviewCurrentVideoId) {
      overlay.remove();
      // Recursively call to create new overlay
      attemptInsertion();
    }

    thumbnailInsertionAttempts = 0;
  }

  /**
   * Add or update thumbnail image on watch page
   */
  function addOrUpdateThumbnailImage() {
    if (!isEnabled()) return;
    if (!isWatchPage()) return;

    const newVideoId = getCurrentVideoId();

    if (newVideoId !== thumbnailPreviewCurrentVideoId) {
      thumbnailPreviewClosed = false;
      removeOldOverlay();
    }

    if (shouldSkipThumbnailUpdate(newVideoId)) {
      return;
    }

    thumbnailPreviewCurrentVideoId = newVideoId;
    attemptInsertion();
  }

  function createThumbnailOverlay(videoId, container) {
    const overlay = document.createElement('div');

    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('width', '16');
    svg.setAttribute('height', '16');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('fill', 'none');
    svg.setAttribute('stroke', 'white');
    svg.setAttribute('stroke-width', '2');
    svg.setAttribute('stroke-linecap', 'round');
    svg.setAttribute('stroke-linejoin', 'round');
    svg.style.transition = 'stroke 0.2s ease';

    const mainRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    mainRect.setAttribute('width', '18');
    mainRect.setAttribute('height', '18');
    mainRect.setAttribute('x', '3');
    mainRect.setAttribute('y', '3');
    mainRect.setAttribute('rx', '2');
    mainRect.setAttribute('ry', '2');
    svg.appendChild(mainRect);

    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    circle.setAttribute('cx', '9');
    circle.setAttribute('cy', '9');
    circle.setAttribute('r', '2');
    svg.appendChild(circle);

    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute('d', 'm21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21');
    svg.appendChild(path);

    overlay.appendChild(svg);
    overlay.style.cssText = `
        position: absolute;
        bottom: 8px;
        left: 8px;
        background: rgba(0, 0, 0, 0.3);
        width: 28px;
        height: 28px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 4px;
        cursor: pointer;
        z-index: 1000;
        opacity: 0;
        transition: all 0.2s ease;
      `;

    overlay.onmouseenter = () => {
      overlay.style.background = 'rgba(0, 0, 0, 0.7)';
    };
    overlay.onmouseleave = () => {
      overlay.style.background = 'rgba(0, 0, 0, 0.3)';
    };

    overlay.onclick = async e => {
      e.preventDefault();
      e.stopPropagation();

      const isShorts =
        container.closest('ytm-shorts-lockup-view-model') ||
        container.closest('.shortsLockupViewModelHost') ||
        container.closest('[class*="shortsLockupViewModelHost"]') ||
        container.querySelector('a[href*="/shorts/"]');

      await openThumbnail(videoId, !!isShorts, overlay);
    };

    return overlay;
  }

  /**
   * Find thumbnail container from image
   * @param {HTMLImageElement} img - Image element
   * @returns {HTMLElement|null} Thumbnail container
   */
  function findThumbnailContainerFromImage(img) {
    return img.closest('yt-thumbnail-view-model') || img.parentElement;
  }

  /**
   * Find thumbnail container for shorts
   * @param {HTMLImageElement} shortsImg - Shorts image
   * @returns {HTMLElement|null} Thumbnail container
   */
  function findShortsThumbnailContainer(shortsImg) {
    if (!shortsImg) return null;

    return (
      shortsImg.closest('.ytCoreImageHost') ||
      shortsImg.closest('[class*="ThumbnailContainer"]') ||
      shortsImg.closest('[class*="ImageHost"]') ||
      shortsImg.parentElement
    );
  }

  /**
   * Extract video ID and container from regular video
   * @param {HTMLElement} container - Container element
   * @returns {{videoId: string|null, thumbnailContainer: HTMLElement|null}} Result
   */
  function extractVideoInfo(container) {
    const img = container.querySelector('img[src*="ytimg.com"]');
    if (!img?.src) return { videoId: null, thumbnailContainer: null };

    const videoId = extractVideoId(img.src);
    const thumbnailContainer = findThumbnailContainerFromImage(img);
    return { videoId, thumbnailContainer };
  }

  /**
   * Extract shorts ID and container
   * @param {HTMLElement} container - Container element
   * @returns {{videoId: string|null, thumbnailContainer: HTMLElement|null}} Result
   */
  function extractShortsInfo(container) {
    const link = container.querySelector('a[href*="/shorts/"]');
    if (!link?.href) return { videoId: null, thumbnailContainer: null };

    const videoId = extractShortsId(link.href);
    const shortsImg = container.querySelector('img[src*="ytimg.com"]');
    const thumbnailContainer = findShortsThumbnailContainer(shortsImg);
    return { videoId, thumbnailContainer };
  }

  /**
   * Ensure container has relative positioning
   * @param {HTMLElement} thumbnailContainer - Thumbnail container
   * @returns {void}
   */
  function ensureRelativePosition(thumbnailContainer) {
    if (getComputedStyle(thumbnailContainer).position === 'static') {
      thumbnailContainer.style.position = 'relative';
    }
  }

  /**
   * Setup hover effects for overlay
   * @param {HTMLElement} thumbnailContainer - Thumbnail container
   * @param {HTMLElement} overlay - Overlay element
   * @returns {void}
   */
  function setupOverlayHoverEffects(thumbnailContainer, overlay) {
    thumbnailContainer.onmouseenter = () => {
      overlay.style.opacity = '1';
    };
    thumbnailContainer.onmouseleave = () => {
      overlay.style.opacity = '0';
    };
  }

  /**
   * Add thumbnail overlay to container
   * @param {HTMLElement} container - Container element
   * @returns {void}
   */
  function addThumbnailOverlay(container) {
    if (!isEnabled()) return;
    if (container.querySelector('.thumb-overlay')) return;

    // Try regular video first
    let { videoId, thumbnailContainer } = extractVideoInfo(container);

    // If no video found, try shorts
    if (!videoId) {
      ({ videoId, thumbnailContainer } = extractShortsInfo(container));
    }

    if (!videoId || !thumbnailContainer) return;

    ensureRelativePosition(thumbnailContainer);

    const overlay = createThumbnailOverlay(videoId, container);
    overlay.className = 'thumb-overlay';
    thumbnailContainer.appendChild(overlay);

    setupOverlayHoverEffects(thumbnailContainer, overlay);
  }

  function createAvatarOverlay() {
    const overlay = document.createElement('div');

    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('width', '16');
    svg.setAttribute('height', '16');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('fill', 'none');
    svg.setAttribute('stroke', 'white');
    svg.setAttribute('stroke-width', '2');
    svg.setAttribute('stroke-linecap', 'round');
    svg.setAttribute('stroke-linejoin', 'round');
    svg.style.transition = 'stroke 0.2s ease';

    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    circle.setAttribute('cx', '12');
    circle.setAttribute('cy', '8');
    circle.setAttribute('r', '5');
    svg.appendChild(circle);

    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute('d', 'M20 21a8 8 0 0 0-16 0');
    svg.appendChild(path);

    overlay.appendChild(svg);

    overlay.style.cssText = `
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: rgba(0, 0, 0, 0.7);
        width: 28px;
        height: 28px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        cursor: pointer;
        z-index: 1000;
        opacity: 0;
        transition: all 0.2s ease;
      `;

    overlay.onmouseenter = () => {
      overlay.style.background = 'rgba(0, 0, 0, 0.9)';
    };
    overlay.onmouseleave = () => {
      overlay.style.background = 'rgba(0, 0, 0, 0.7)';
    };

    return overlay;
  }

  function addAvatarOverlay(img) {
    if (!isEnabled()) return;
    const container = img.parentElement;
    if (!container) return;

    // Don't add avatar overlay on avatar buttons or when inside a button.
    // This avoids adding the overlay on elements like `avatar-btn` which are
    // already interactive controls and may conflict with their behavior.
    if (
      img.closest('.avatar-btn, #avatar-btn') ||
      container.closest('.avatar-btn, #avatar-btn') ||
      img.closest('button') ||
      container.closest('button') ||
      // Skip when inside the thumbnail modal wrapper (don't add overlays inside modals)
      img.closest('.thumbnail-modal-wrapper') ||
      container.closest('.thumbnail-modal-wrapper')
    ) {
      return;
    }

    // Don't add avatar overlay inside Shorts lockups/containers — these are
    // special UI elements where avatar overlays are undesirable.
    if (
      img.closest('ytm-shorts-lockup-view-model') ||
      container.closest('ytm-shorts-lockup-view-model') ||
      img.closest('.shortsLockupViewModelHost') ||
      container.closest('.shortsLockupViewModelHost') ||
      img.closest('[class*="shortsLockupViewModelHost"]') ||
      container.closest('[class*="shortsLockupViewModelHost"]') ||
      img.closest('[class*="shorts"]') ||
      container.closest('[class*="shorts"]')
    ) {
      return;
    }

    if (container.querySelector('.avatar-overlay')) return;

    if (getComputedStyle(container).position === 'static') {
      container.style.position = 'relative';
    }

    const overlay = createAvatarOverlay();
    overlay.className = 'avatar-overlay';

    overlay.onclick = e => {
      e.preventDefault();
      e.stopPropagation();
      const highResUrl = img.src.replace(/=s\d+-c-k-c0x00ffffff-no-rj.*/, '=s0');
      showImageModal(highResUrl);
    };

    container.appendChild(overlay);

    container.onmouseenter = () => {
      overlay.style.opacity = '1';
    };
    container.onmouseleave = () => {
      overlay.style.opacity = '0';
    };
  }

  function createBannerOverlay() {
    const overlay = document.createElement('div');

    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('width', '16');
    svg.setAttribute('height', '16');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('fill', 'none');
    svg.setAttribute('stroke', 'white');
    svg.setAttribute('stroke-width', '2');
    svg.setAttribute('stroke-linecap', 'round');
    svg.setAttribute('stroke-linejoin', 'round');
    svg.style.transition = 'stroke 0.2s ease';

    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    rect.setAttribute('x', '3');
    rect.setAttribute('y', '3');
    rect.setAttribute('width', '18');
    rect.setAttribute('height', '18');
    rect.setAttribute('rx', '2');
    rect.setAttribute('ry', '2');
    svg.appendChild(rect);

    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    circle.setAttribute('cx', '9');
    circle.setAttribute('cy', '9');
    circle.setAttribute('r', '2');
    svg.appendChild(circle);

    const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
    polyline.setAttribute('points', '21,15 16,10 5,21');
    svg.appendChild(polyline);

    overlay.appendChild(svg);

    overlay.style.cssText = `
        position: absolute;
        bottom: 8px;
        left: 8px;
        background: rgba(0, 0, 0, 0.7);
        width: 28px;
        height: 28px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 4px;
        cursor: pointer;
        z-index: 1000;
        opacity: 0;
        transition: all 0.2s ease;
      `;

    overlay.onmouseenter = () => {
      overlay.style.background = 'rgba(0, 0, 0, 0.9)';
    };
    overlay.onmouseleave = () => {
      overlay.style.background = 'rgba(0, 0, 0, 0.7)';
    };

    return overlay;
  }

  function addBannerOverlay(img) {
    if (!isEnabled()) return;
    const container = img.parentElement;
    if (container.querySelector('.banner-overlay')) return;

    if (getComputedStyle(container).position === 'static') {
      container.style.position = 'relative';
    }

    const overlay = createBannerOverlay();
    overlay.className = 'banner-overlay';

    overlay.onclick = e => {
      e.preventDefault();
      e.stopPropagation();
      const highResUrl = img.src.replace(/=w\d+-.*/, '=s0');
      showImageModal(highResUrl);
    };

    container.appendChild(overlay);

    container.onmouseenter = () => {
      overlay.style.opacity = '1';
    };
    container.onmouseleave = () => {
      overlay.style.opacity = '0';
    };
  }

  function processAvatars() {
    const avatarSelectors = [
      'yt-avatar-shape img',
      '#avatar img',
      'ytd-channel-avatar-editor img',
      '.ytd-video-owner-renderer img[src*="yt"]',
      'img[src*="yt3.ggpht.com"]', // Добавляем прямой селектор для аватаров
      'img[src*="yt4.ggpht.com"]',
    ];

    avatarSelectors.forEach(selector => {
      qsAll(selector).forEach(img => {
        if (!img.src) return;
        if (!img.src.includes('yt')) return;
        if (img.closest('.avatar-overlay')) return;

        // Проверяем, что это действительно аватар (квадратное изображение)
        const isAvatar = img.naturalWidth > 0 && img.naturalWidth === img.naturalHeight;

        if (isAvatar || img.src.includes('ggpht.com')) {
          addAvatarOverlay(img);
        }
      });
    });
  }

  function processBanners() {
    const bannerSelectors = [
      'yt-image-banner-view-model img',
      'ytd-c4-tabbed-header-renderer img[src*="yt"]',
      '#channel-header img[src*="banner"]',
      'img[src*="banner"]', // Более общий селектор для баннеров
    ];

    bannerSelectors.forEach(selector => {
      qsAll(selector).forEach(img => {
        if (!img.src) return;
        if (img.closest('.banner-overlay')) return;

        const isBanner =
          (img.src.includes('banner') || img.src.includes('yt')) &&
          img.naturalWidth > img.naturalHeight * 2; // Баннеры обычно широкие

        if (isBanner || img.src.includes('banner')) {
          addBannerOverlay(img);
        }
      });
    });
  }

  function processThumbnails() {
    // Cache NodeLists to avoid repeated DOM lookups and reduce GC churn
    const n1 = qsAll('yt-thumbnail-view-model');
    for (let i = 0; i < n1.length; i++) addThumbnailOverlay(n1[i]);

    const n2 = qsAll('.ytd-thumbnail');
    for (let i = 0; i < n2.length; i++) addThumbnailOverlay(n2[i]);

    const n3 = qsAll('ytm-shorts-lockup-view-model');
    for (let i = 0; i < n3.length; i++) addThumbnailOverlay(n3[i]);

    const n4 = qsAll('.shortsLockupViewModelHost');
    for (let i = 0; i < n4.length; i++) addThumbnailOverlay(n4[i]);

    const n5 = qsAll('[class*="shortsLockupViewModelHost"]');
    for (let i = 0; i < n5.length; i++) addThumbnailOverlay(n5[i]);
  }

  function processAll() {
    if (!isEnabled()) return;
    processThumbnails();
    processAvatars();
    processBanners();
    addOrUpdateThumbnailImage();
  }

  // Throttle/debounce processing to avoid expensive full-page rescans on every DOM mutation.
  let processAllTimerId = null;
  let lastProcessAllTime = 0;
  const MIN_PROCESS_ALL_INTERVAL = 350;

  function scheduleProcessAll(minDelay = 0) {
    if (processAllTimerId) return;
    const now = Date.now();
    const dueIn = Math.max(
      minDelay,
      Math.max(0, MIN_PROCESS_ALL_INTERVAL - (now - lastProcessAllTime))
    );

    processAllTimerId = setTimeout(() => {
      processAllTimerId = null;
      lastProcessAllTime = Date.now();
      try {
        if (!isEnabled()) return;
        processAll();
      } catch (e) {
        console.error('[YouTube+][Thumbnail]', 'processAll failed:', e);
      }
    }, dueIn);
  }

  function setupMutationObserver() {
    if (mutationObserver) return;
    mutationObserver = new MutationObserver(() => {
      scheduleProcessAll(120);
    });

    // Scope to #content or #page-manager instead of full body for performance
    const startObserving = () => {
      if (!mutationObserver) return;
      const target =
        document.querySelector('#content') ||
        document.querySelector('#page-manager') ||
        document.body;
      // Optimize observer: avoid subtree on body, only childList needed for route changes
      mutationObserver.observe(target, {
        childList: true,
        subtree: target !== document.body,
      });
    };

    if (document.body) {
      startObserving();
    } else {
      document.addEventListener('DOMContentLoaded', startObserving);
    }
  }

  function teardownMutationObserver() {
    if (!mutationObserver) return;
    try {
      mutationObserver.disconnect();
    } catch {}
    mutationObserver = null;
  }

  function setupUrlChangeDetection() {
    let currentUrl = location.href;

    const onNavChange = () => {
      setTimeout(() => {
        if (!isEnabled()) return;
        if (location.href !== currentUrl) {
          currentUrl = location.href;
          scheduleProcessAll(250);
        }
      }, 100);
    };

    const ytNavigateHandler = () => {
      if (!isEnabled()) return;
      if (location.href !== currentUrl) {
        currentUrl = location.href;
      }
      scheduleProcessAll(120);
    };

    // Use centralized pushState/replaceState event from utils.js
    window.addEventListener('ytp-history-navigate', onNavChange);
    window.addEventListener('popstate', onNavChange);
    window.addEventListener('yt-navigate-finish', ytNavigateHandler);

    return () => {
      try {
        window.removeEventListener('ytp-history-navigate', onNavChange);
        window.removeEventListener('popstate', onNavChange);
        window.removeEventListener('yt-navigate-finish', ytNavigateHandler);
      } catch {}
    };
  }

  function removeInjectedUi() {
    try {
      qsAll('.thumbnail-modal-overlay').forEach(m => m.remove());
    } catch {}
    try {
      qsAll('.thumb-overlay, .avatar-overlay, .banner-overlay').forEach(el => el.remove());
    } catch {}
    try {
      const playerOverlay = qs('#thumbnailPreview-player-overlay');
      if (playerOverlay) playerOverlay.remove();
    } catch {}
  }

  function stop() {
    if (!started) return;
    started = false;

    try {
      if (processAllTimerId) {
        clearTimeout(processAllTimerId);
        processAllTimerId = null;
      }
    } catch {}

    teardownMutationObserver();

    if (urlChangeCleanup) {
      try {
        urlChangeCleanup();
      } catch {}
      urlChangeCleanup = null;
    }

    removeInjectedUi();
    removeThumbnailStyles();
  }

  function start() {
    if (started) return;
    if (!isEnabled()) return;

    started = true;
    ensureThumbnailStyles();

    if (!urlChangeCleanup) {
      urlChangeCleanup = setupUrlChangeDetection();
    }
    setupMutationObserver();

    // Defer heavy work off the critical path.
    if (typeof requestIdleCallback === 'function') {
      requestIdleCallback(() => scheduleProcessAll(0), { timeout: 2000 });
    } else {
      scheduleProcessAll(400);
    }

    // A couple of spaced retries for late-loaded nodes.
    setTimeout(() => scheduleProcessAll(0), 900);
    setTimeout(() => scheduleProcessAll(0), 1800);
  }

  function startMaybe() {
    if (started || startScheduled) return;
    if (!isEnabled()) return;

    startScheduled = true;
    const run = () => {
      startScheduled = false;
      start();
    };

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', () => setTimeout(run, 100), {
        once: true,
      });
    } else {
      setTimeout(run, 100);
    }
  }

  function setEnabled(nextEnabled) {
    thumbnailFeatureEnabled = nextEnabled !== false;
    if (thumbnailFeatureEnabled) startMaybe();
    else stop();
  }

  // Initial state
  startMaybe();

  // Live updates
  window.addEventListener('youtube-plus-settings-updated', e => {
    try {
      const enabledFromEvent = e?.detail?.enableThumbnail;
      setEnabled(enabledFromEvent !== false);
    } catch {
      setEnabled(loadEnableThumbnail());
    }
  });
})();

// --- MODULE: shorts.js ---

// Shorts Keyboard controls
(function () {
  'use strict';

  // Use centralized i18n from YouTubePlusI18n or YouTubeUtils
  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    // Fallback for initialization phase
    if (!key) return '';
    let result = String(key);
    for (const [k, v] of Object.entries(params || {})) {
      result = result.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
    }
    return result;
  };

  // Configuration - Using lazy getters for translations to avoid early loading
  const config = {
    enabled: true,
    get shortcuts() {
      return {
        seekBackward: {
          key: 'ArrowLeft',
          get description() {
            return t('seekBackward');
          },
        },
        seekForward: {
          key: 'ArrowRight',
          get description() {
            return t('seekForward');
          },
        },
        volumeUp: {
          key: '+',
          get description() {
            return t('volumeUp');
          },
        },
        volumeDown: {
          key: '-',
          get description() {
            return t('volumeDown');
          },
        },
        mute: {
          key: 'm',
          get description() {
            return t('muteUnmute');
          },
        },
        toggleCaptions: {
          key: 'c',
          get description() {
            return t('toggleCaptions');
          },
        },
        showHelp: {
          key: '?',
          get description() {
            return t('showHideHelp');
          },
          editable: false,
        },
      };
    },
    storageKey: 'youtube_shorts_keyboard_settings',
  };

  // State management
  const state = {
    helpVisible: false,
    lastAction: null,
    actionTimeout: null,
    editingShortcut: null,
    cachedVideo: null,
    lastVideoCheck: 0,
    initialized: false,
    routeObserver: null,
  };

  /**
   * Get the currently active video element in YouTube Shorts with caching
   * Optimizes performance by caching results for 100ms
   * @returns {HTMLVideoElement|null} The active video element or null if not found
   */
  const getCurrentVideo = (() => {
    const selectors = ['ytd-reel-video-renderer[is-active] video', '#shorts-player video', 'video'];

    return () => {
      const now = Date.now();
      if (state.cachedVideo?.isConnected && now - state.lastVideoCheck < 100) {
        return state.cachedVideo;
      }

      for (const selector of selectors) {
        const video = YouTubeUtils.querySelector(selector);
        if (video) {
          state.cachedVideo = video;
          state.lastVideoCheck = now;
          return video;
        }
      }

      state.cachedVideo = null;
      return null;
    };
  })();

  // Optimized utilities
  const utils = {
    /**
     * Check if current page is a YouTube Shorts page
     * @returns {boolean} True if on Shorts page
     */
    isInShortsPage: () => location.pathname.startsWith('/shorts/'),

    /**
     * Check if an input element currently has focus
     * @returns {boolean} True if input/textarea/contenteditable is focused
     */
    isInputFocused: () => {
      const el = document.activeElement;
      return el?.matches?.('input, textarea, [contenteditable="true"]') || el?.isContentEditable;
    },

    /**
     * Load settings from localStorage with validation
     * @returns {void}
     */
    loadSettings: () => {
      try {
        const saved = localStorage.getItem(config.storageKey);
        if (!saved) return;

        const parsed = JSON.parse(saved);
        if (typeof parsed !== 'object' || parsed === null) {
          console.warn('[YouTube+][Shorts]', 'Invalid settings format');
          return;
        }

        // Validate enabled flag
        if (typeof parsed.enabled === 'boolean') {
          config.enabled = parsed.enabled;
        }

        // Validate shortcuts object
        if (parsed.shortcuts && typeof parsed.shortcuts === 'object') {
          const defaultShortcuts = utils.getDefaultShortcuts();

          for (const [action, shortcut] of Object.entries(parsed.shortcuts)) {
            // Only restore valid shortcut actions
            if (!defaultShortcuts[action]) continue;
            if (!shortcut || typeof shortcut !== 'object') continue;

            const { key: sKey, editable: sEditable } =
              /** @type {{ key?: string, editable?: boolean }} */ (shortcut);
            if (typeof sKey === 'string' && sKey.length > 0 && sKey.length <= 20) {
              config.shortcuts[action] = {
                key: sKey,
                description: defaultShortcuts[action].description,
                editable: sEditable !== false,
              };
            }
          }
        }
      } catch (error) {
        console.error('[YouTube+][Shorts]', 'Error loading settings:', error);
      }
    },

    /**
     * Save settings to localStorage with error handling
     * @returns {void}
     */
    saveSettings: () => {
      try {
        const settingsToSave = {
          enabled: config.enabled,
          shortcuts: config.shortcuts,
        };
        localStorage.setItem(config.storageKey, JSON.stringify(settingsToSave));
      } catch (error) {
        console.error('[YouTube+][Shorts]', 'Error saving settings:', error);
      }
    },

    /**
     * Get default keyboard shortcuts configuration
     * @returns {Object} Object containing default shortcut definitions
     */
    getDefaultShortcuts: () => ({
      seekBackward: {
        key: 'ArrowLeft',
        get description() {
          return t('seekBackward');
        },
      },
      seekForward: {
        key: 'ArrowRight',
        get description() {
          return t('seekForward');
        },
      },
      volumeUp: {
        key: '+',
        get description() {
          return t('volumeUp');
        },
      },
      volumeDown: {
        key: '-',
        get description() {
          return t('volumeDown');
        },
      },
      mute: {
        key: 'm',
        get description() {
          return t('muteUnmute');
        },
      },
      toggleCaptions: {
        key: 'c',
        get description() {
          return t('toggleCaptions');
        },
      },
      showHelp: {
        key: '?',
        get description() {
          return t('showHideHelp');
        },
        editable: false,
      },
    }),
  };

  /**
   * Feedback system for displaying temporary notifications in Shorts
   * Uses glassmorphism design for visual feedback
   */
  const feedback = (() => {
    let element = null;

    /**
     * Create or retrieve the feedback element
     * @returns {HTMLElement} The feedback container element
     */
    const create = () => {
      if (element) return element;

      element = document.createElement('div');
      element.id = 'shorts-keyboard-feedback';
      element.style.cssText = `
          position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
          background:var(--shorts-feedback-bg,rgba(255,255,255,.1));
          backdrop-filter:blur(16px) saturate(150%);
          border:1px solid var(--shorts-feedback-border,rgba(255,255,255,.15));
          border-radius:20px;
          color:var(--shorts-feedback-color,#fff);
          padding:18px 32px;font-size:20px;font-weight:700;
          z-index:10000;opacity:0;visibility:hidden;pointer-events:none;
          transition:all .3s cubic-bezier(.4,0,.2,1);text-align:center;
          box-shadow:0 8px 32px rgba(0,0,0,.4);
          background: rgba(155, 155, 155, 0.15);
          border: 1px solid rgba(255,255,255,0.2);
          box-shadow: 0 8px 32px 0 rgba(31,38,135,0.37);
          backdrop-filter: blur(12px) saturate(180%);
          -webkit-backdrop-filter: blur(12px) saturate(180%);
        `;
      document.body.appendChild(element);
      return element;
    };

    return {
      /**
       * Display a feedback message to the user
       * @param {string} text - Message text to display
       * @returns {void}
       */
      show: text => {
        state.lastAction = text;
        clearTimeout(state.actionTimeout);

        const el = create();
        el.textContent = text;

        requestAnimationFrame(() => {
          el.style.opacity = '1';
          el.style.visibility = 'visible';
          el.style.transform = 'translate(-50%, -50%) scale(1.05)';
        });

        state.actionTimeout = setTimeout(() => {
          el.style.opacity = '0';
          el.style.visibility = 'hidden';
          el.style.transform = 'translate(-50%, -50%) scale(0.95)';
        }, 1500);
      },
    };
  })();

  // Optimized actions
  const actions = {
    /**
     * Seek backward 5 seconds in the video
     * @returns {void}
     */
    seekBackward: () => {
      const video = getCurrentVideo();
      if (video) {
        video.currentTime = Math.max(0, video.currentTime - 5);
        feedback.show('-5s');
      }
    },

    /**
     * Seek forward 5 seconds in the video
     * @returns {void}
     */
    seekForward: () => {
      const video = getCurrentVideo();
      if (video) {
        video.currentTime = Math.min(video.duration || Infinity, video.currentTime + 5);
        feedback.show('+5s');
      }
    },

    /**
     * Toggle captions/subtitles on or off
     * Attempts to click UI button first, then falls back to programmatic toggle
     * @returns {void}
     */
    toggleCaptions: () => {
      // Try to click a captions/subtitles button first
      try {
        const container =
          document.querySelector(
            'ytd-shorts-player-controls, ytd-reel-video-renderer, #shorts-player'
          ) || document;
        const buttons = container.querySelectorAll('button[aria-label]');
        for (const b of buttons) {
          const aria = (b.getAttribute('aria-label') || '').toLowerCase();
          if (
            aria.includes('subtit') ||
            aria.includes('caption') ||
            aria.includes('субтит') ||
            aria.includes('субтитр') ||
            aria.includes('cc')
          ) {
            if (b.offsetParent !== null) {
              b.click();
              // It's hard to know exact state, so try to use textTracks below for feedback
              break;
            }
          }
        }
      } catch {
        // Continue to fallback
      }

      const video = getCurrentVideo();
      if (video && video.textTracks && video.textTracks.length) {
        const tracks = Array.from(video.textTracks).filter(
          tr => tr.kind === 'subtitles' || tr.kind === 'captions' || !tr.kind
        );
        if (tracks.length) {
          const anyShowing = tracks.some(tr => tr.mode === 'showing');
          tracks.forEach(tr => {
            tr.mode = anyShowing ? 'hidden' : 'showing';
          });
          feedback.show(anyShowing ? t('captionsOff') : t('captionsOn'));
          return;
        }
      }

      // If we couldn't toggle via textTracks or button, inform user
      feedback.show(t('captionsUnavailable'));
    },

    /**
     * Increase video volume by 10%
     * @returns {void}
     */
    volumeUp: () => {
      const video = getCurrentVideo();
      if (video) {
        video.volume = Math.min(1, video.volume + 0.1);
        feedback.show(`${Math.round(video.volume * 100)}%`);
      }
    },

    /**
     * Decrease video volume by 10%
     * @returns {void}
     */
    volumeDown: () => {
      const video = getCurrentVideo();
      if (video) {
        video.volume = Math.max(0, video.volume - 0.1);
        feedback.show(`${Math.round(video.volume * 100)}%`);
      }
    },

    /**
     * Toggle video mute state
     * Attempts to click UI mute button first, then falls back to programmatic toggle
     * @returns {void}
     */
    mute: () => {
      const video = getCurrentVideo();

      // Try to click a visible mute/volume button so the player UI updates its icon
      try {
        const container =
          document.querySelector(
            'ytd-shorts-player-controls, ytd-reel-video-renderer, #shorts-player'
          ) || document;
        const buttons = container.querySelectorAll('button[aria-label]');
        for (const b of buttons) {
          const aria = (b.getAttribute('aria-label') || '').toLowerCase();
          if (
            aria.includes('mute') ||
            aria.includes('unmute') ||
            aria.includes('sound') ||
            aria.includes('volume') ||
            aria.includes('звук') ||
            aria.includes('громк')
          ) {
            if (b.offsetParent !== null) {
              b.click();
              // Give the player a moment to update state, then show feedback based on video.muted
              setTimeout(() => {
                const v = getCurrentVideo();
                if (v) feedback.show(v.muted ? '🔇' : '🔊');
              }, 60);
              return;
            }
          }
        }
      } catch {
        // ignore and fallback
      }

      // Fallback: toggle programmatically
      if (video) {
        video.muted = !video.muted;
        feedback.show(video.muted ? '🔇' : '🔊');
      }
    },

    /**
     * Show or hide the keyboard shortcuts help panel
     * @returns {void}
     */
    showHelp: () => helpPanel.toggle(),
  };

  /**
   * Help panel system for displaying keyboard shortcuts reference
   * Provides interactive UI for viewing and editing shortcuts
   */
  const helpPanel = (() => {
    let panel = null;

    /**
     * Create or retrieve the help panel element
     * @returns {HTMLElement} The help panel container element
     */
    const create = () => {
      if (panel) return panel;

      panel = document.createElement('div');
      panel.id = 'shorts-keyboard-help';
      panel.className = 'glass-panel shorts-help-panel';
      panel.setAttribute('role', 'dialog');
      panel.setAttribute('aria-modal', 'true');
      panel.tabIndex = -1;

      const render = () => {
        panel.innerHTML = `
            <div class="help-header">
              <h3>${t('keyboardShortcuts')}</h3>
              <button class="ytp-plus-settings-close help-close" type="button" aria-label="${t('closeButton')}">
                <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                  <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
                </svg>
              </button>
            </div>
            <div class="help-content">
              ${Object.entries(config.shortcuts)
                .map(
                  ([action, shortcut]) =>
                    `<div class="help-item">
                  <kbd data-action="${action}" ${shortcut.editable === false ? 'class="non-editable"' : ''}>${shortcut.key === ' ' ? 'Space' : shortcut.key}</kbd>
                  <span>${shortcut.description}</span>
                </div>`
                )
                .join('')}
            </div>
            <div class="help-footer">
              <button class="ytp-plus-button ytp-plus-button-primary reset-all-shortcuts">${t('resetAll')}</button>
            </div>
          `;

        panel.querySelector('.help-close').onclick = () => helpPanel.hide();
        panel.querySelector('.reset-all-shortcuts').onclick = () => {
          if (confirm(t('resetAllConfirm'))) {
            config.shortcuts = utils.getDefaultShortcuts();
            utils.saveSettings();
            feedback.show(t('shortcutsReset'));
            render();
          }
        };

        panel.querySelectorAll('kbd[data-action]:not(.non-editable)').forEach(kbd => {
          kbd.onclick = () =>
            editShortcut(kbd.dataset.action, config.shortcuts[kbd.dataset.action].key);
        });
      };

      render();
      document.body.appendChild(panel);
      return panel;
    };

    return {
      /**
       * Display the help panel
       * @returns {void}
       */
      show: () => {
        const p = create();
        p.classList.add('visible');
        state.helpVisible = true;
        p.focus();
      },

      /**
       * Hide the help panel
       * @returns {void}
       */
      hide: () => {
        if (panel) {
          panel.classList.remove('visible');
          state.helpVisible = false;
        }
      },

      /**
       * Toggle help panel visibility
       * @returns {void}
       */
      toggle: () => (state.helpVisible ? helpPanel.hide() : helpPanel.show()),

      /**
       * Refresh the help panel by removing and recreating it
       * @returns {void}
       */
      refresh: () => {
        if (panel) {
          panel.remove();
          panel = null;
        }
      },
    };
  })();

  /**
   * Open dialog to edit a keyboard shortcut
   * @param {string} actionKey - The action identifier to edit
   * @param {string} currentKey - The current key binding
   * @returns {void}
   */
  const editShortcut = (actionKey, currentKey) => {
    const dialog = document.createElement('div');
    dialog.className = 'glass-modal shortcut-edit-dialog';
    dialog.setAttribute('role', 'dialog');
    dialog.setAttribute('aria-modal', 'true');
    dialog.innerHTML = `
        <div class="glass-panel shortcut-edit-content">
          <h4>${t('editShortcut')}: ${config.shortcuts[actionKey].description}</h4>
          <p>${t('pressAnyKey')}</p>
          <div class="current-shortcut">${t('current')}: <kbd>${currentKey === ' ' ? 'Space' : currentKey}</kbd></div>
          <button class="ytp-plus-button ytp-plus-button-primary shortcut-cancel" type="button">${t('cancel')}</button>
        </div>
      `;

    document.body.appendChild(dialog);
    state.editingShortcut = actionKey;

    const handleKey = e => {
      e.preventDefault();
      e.stopPropagation();
      if (e.key === 'Escape') return cleanup();

      const conflict = Object.keys(config.shortcuts).find(
        key => key !== actionKey && config.shortcuts[key].key === e.key
      );
      if (conflict) {
        feedback.show(t('keyAlreadyUsed', { key: e.key }));
        return;
      }

      config.shortcuts[actionKey].key = e.key;
      utils.saveSettings();
      feedback.show(t('shortcutUpdated'));
      helpPanel.refresh();
      cleanup();
    };

    const cleanup = () => {
      document.removeEventListener('keydown', handleKey, true);
      dialog.remove();
      state.editingShortcut = null;
    };

    dialog.querySelector('.shortcut-cancel').onclick = cleanup;
    // Use parameter destructuring to satisfy prefer-destructuring rule
    dialog.onclick = ({ target }) => {
      // target is expected to be an Element here
      if (target === dialog) cleanup();
    };
    document.addEventListener('keydown', handleKey, true);
  };

  /**
   * Add glassmorphism styles for Shorts keyboard controls
   * Uses CSS custom properties for theme support
   * @returns {void}
   */
  const addStyles = () => {
    if (document.getElementById('shorts-keyboard-styles')) return;

    const styles = `
                :root{--shorts-feedback-bg:rgba(255,255,255,.15);--shorts-feedback-border:rgba(255,255,255,.2);--shorts-feedback-color:#fff;--shorts-help-bg:rgba(255,255,255,.15);--shorts-help-border:rgba(255,255,255,.2);--shorts-help-color:#fff;}
                html[dark],body[dark]{--shorts-feedback-bg:rgba(34,34,34,.7);--shorts-feedback-border:rgba(255,255,255,.15);--shorts-feedback-color:#fff;--shorts-help-bg:rgba(34,34,34,.7);--shorts-help-border:rgba(255,255,255,.1);--shorts-help-color:#fff;}
                html:not([dark]){--shorts-feedback-bg:rgba(255,255,255,.95);--shorts-feedback-border:rgba(0,0,0,.08);--shorts-feedback-color:#222;--shorts-help-bg:rgba(255,255,255,.98);--shorts-help-border:rgba(0,0,0,.08);--shorts-help-color:#222;}
                .shorts-help-panel{position:fixed;top:50%;left:25%;transform:translate(-50%,-50%) scale(.9);z-index:10001;opacity:0;visibility:hidden;transition:all .3s ease;width:340px;max-width:95vw;max-height:80vh;overflow:hidden;outline:none;color:var(--shorts-help-color,#fff);}
                .shorts-help-panel.visible{opacity:1;visibility:visible;transform:translate(-50%,-50%) scale(1);}
                .help-header{display:flex;justify-content:space-between;align-items:center;padding:24px 24px 12px;border-bottom:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);}
                html:not([dark]) .help-header{background:rgba(0,0,0,.04);border-bottom:1px solid rgba(0,0,0,.08);}
                .help-header h3{margin:0;font-size:20px;font-weight:700;}
                .help-close{display:flex;align-items:center;justify-content:center;padding:4px;}
                .help-content{padding:18px 24px;max-height:400px;overflow-y:auto;}
                .help-item{display:flex;align-items:center;margin-bottom:14px;gap:18px;}
                .help-item kbd{background:rgba(255,255,255,.15);color:inherit;padding:7px 14px;border-radius:8px;font-family:monospace;font-size:15px;font-weight:700;min-width:60px;text-align:center;border:1.5px solid rgba(255,255,255,.2);cursor:pointer;transition:all .2s;position:relative;}
                html:not([dark]) .help-item kbd{background:rgba(0,0,0,.06);color:#222;border:1.5px solid rgba(0,0,0,.08);}
                .help-item kbd:hover{background:rgba(255,255,255,.22);transform:scale(1.07);}
                .help-item kbd:after{content:"✎";position:absolute;top:-7px;right:-7px;font-size:11px;opacity:0;transition:opacity .2s;}
                .help-item kbd:hover:after{opacity:.7;}
                .help-item kbd.non-editable{cursor:default;opacity:.7;}
                .help-item kbd.non-editable:hover{background:rgba(255,255,255,.15);transform:none;}
                .help-item kbd.non-editable:after{display:none;}
                .help-item span{font-size:15px;color:rgba(255,255,255,.92);}
                html:not([dark]) .help-item span{color:#222;}
                .help-footer{padding:16px 24px 20px;border-top:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);text-align:center;}
                html:not([dark]) .help-footer{background:rgba(0,0,0,.04);border-top:1px solid rgba(0,0,0,.08);}
                .reset-all-shortcuts{display:inline-flex;align-items:center;justify-content:center;gap:var(--yt-space-sm);}
                .shortcut-edit-dialog{z-index:10002;}
                .shortcut-edit-content{padding:28px 32px;min-width:320px;text-align:center;display:flex;flex-direction:column;gap:var(--yt-space-md);color:inherit;}
                html:not([dark]) .shortcut-edit-content{color:#222;}
                .shortcut-edit-content h4{margin:0 0 14px;font-size:17px;font-weight:700;}
                .shortcut-edit-content p{margin:0 0 18px;font-size:15px;color:rgba(255,255,255,.85);}
                html:not([dark]) .shortcut-edit-content p{color:#222;}
                .current-shortcut{margin:18px 0;font-size:15px;}
                .current-shortcut kbd{background:rgba(255,255,255,.15);padding:5px 12px;border-radius:6px;font-family:monospace;border:1.5px solid rgba(255,255,255,.2);}
                html:not([dark]) .current-shortcut kbd{background:rgba(0,0,0,.06);color:#222;border:1.5px solid rgba(0,0,0,.08);}
                .shortcut-cancel{display:inline-flex;align-items:center;justify-content:center;gap:var(--yt-space-sm);}
                @media(max-width:480px){.shorts-help-panel{width:98vw;max-height:85vh}.help-header{padding:16px 10px 8px 10px}.help-content{padding:12px 10px}.help-item{gap:10px}.help-item kbd{min-width:44px;font-size:13px;padding:5px 7px}.shortcut-edit-content{margin:20px;min-width:auto}}
                #shorts-keyboard-feedback{background:var(--shorts-feedback-bg,rgba(255,255,255,.15));color:var(--shorts-feedback-color,#fff);border:1.5px solid var(--shorts-feedback-border,rgba(255,255,255,.2));border-radius:20px;box-shadow:0 8px 32px 0 rgba(31,38,135,.37);backdrop-filter:blur(12px) saturate(180%);-webkit-backdrop-filter:blur(12px) saturate(180%);}
                html:not([dark]) #shorts-keyboard-feedback{background:var(--shorts-feedback-bg,rgba(255,255,255,.95));color:var(--shorts-feedback-color,#222);border:1.5px solid var(--shorts-feedback-border,rgba(0,0,0,.08));}
            `;
    YouTubeUtils.StyleManager.add('shorts-keyboard-styles', styles);
  };

  /**
   * Main keyboard event handler for Shorts controls
   * Routes keypress events to appropriate actions
   * @param {KeyboardEvent} e - The keyboard event
   * @returns {void}
   */
  const handleKeydown = e => {
    if (
      !config.enabled ||
      !utils.isInShortsPage() ||
      utils.isInputFocused() ||
      state.editingShortcut
    ) {
      return;
    }

    let { key } = e;
    if (e.code === 'NumpadAdd') key = '+';
    else if (e.code === 'NumpadSubtract') key = '-';

    const action = Object.keys(config.shortcuts).find(k => config.shortcuts[k].key === key);
    if (action && actions[action]) {
      e.preventDefault();
      e.stopPropagation();
      actions[action]();
    }
  };

  /**
   * Check if current route is /shorts page
   * @returns {boolean} True if on /shorts/* route
   */
  const isOnShortsPage = () => location.pathname.startsWith('/shorts/');

  /**
   * Cleanup when leaving shorts page
   */
  const cleanup = () => {
    if (!state.initialized) return;

    // Remove help panel if visible
    if (state.helpVisible) {
      helpPanel.hide();
    }

    // Clear any pending timeouts
    if (state.actionTimeout) {
      clearTimeout(state.actionTimeout);
      state.actionTimeout = null;
    }

    // Clear cached video
    state.cachedVideo = null;

    state.initialized = false;
  };

  /**
   * Initialize the Shorts keyboard controls module
   * Sets up event listeners and styles
   * @returns {void}
   */
  const init = () => {
    // Strict route guard
    if (!isOnShortsPage()) return;
    if (state.initialized) return;

    state.initialized = true;
    utils.loadSettings();
    addStyles();

    YouTubeUtils.cleanupManager.registerListener(document, 'keydown', handleKeydown, true);

    // Prefer destructuring the event parameter
    const clickHandler = ({ target }) => {
      if (state.helpVisible && target?.closest && !target.closest('#shorts-keyboard-help')) {
        helpPanel.hide();
      }
    };
    YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler);

    document.addEventListener('keydown', e => {
      if (e.key === 'Escape' && state.helpVisible) {
        e.preventDefault();
        helpPanel.hide();
      }
    });
  };

  // Route observer to cleanup when leaving /shorts
  const observeRoute = () => {
    let lastPath = location.pathname;
    let isCurrentlyOnShorts = isOnShortsPage();

    state.routeObserver = new MutationObserver(() => {
      const currentPath = location.pathname;
      // Quick path check first before expensive isOnShortsPage()
      if (currentPath === lastPath) return;

      lastPath = currentPath;
      const nowOnShorts = isOnShortsPage();

      if (nowOnShorts !== isCurrentlyOnShorts) {
        isCurrentlyOnShorts = nowOnShorts;

        if (!nowOnShorts && state.initialized) {
          // Left shorts page
          cleanup();
        } else if (nowOnShorts && !state.initialized) {
          // Entered shorts page
          init();
        }
      }
    });

    if (document.body) {
      state.routeObserver.observe(document.body, {
        childList: true,
        subtree: false,
      });
    }
  };

  // Initialize if on shorts page
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
      init();
      observeRoute();
    });
  } else {
    init();
    observeRoute();
  }

  // Show help tip on first visit
  if (isOnShortsPage() && !localStorage.getItem('shorts_keyboard_help_shown')) {
    setTimeout(() => {
      if (isOnShortsPage()) {
        feedback.show('Press ? for shortcuts');
        localStorage.setItem('shorts_keyboard_help_shown', 'true');
      }
    }, 2000);
  }
})();

// --- MODULE: stats.js ---

// Stats button and menu
(function () {
  'use strict';

  // DOM Cache Helper - reduces repeated queries
  const getCache = () => typeof window !== 'undefined' && window.YouTubeDOMCache;
  /**
   * Query single element with optional caching
   * @param {string} sel - CSS selector
   * @param {Element|Document} [ctx] - Context element
   * @returns {Element|null}
   */
  const $ = (sel, ctx) =>
    getCache()?.querySelector(sel, ctx) || (ctx || document).querySelector(sel);
  /**
   * Query all elements with optional caching
   * @param {string} sel - CSS selector
   * @param {Element|Document} [ctx] - Context element
   * @returns {Element[]}
   */
  const $$ = (sel, ctx) =>
    getCache()?.querySelectorAll(sel, ctx) || Array.from((ctx || document).querySelectorAll(sel));
  /**
   * Get element by ID with optional caching
   * @param {string} id - Element ID
   * @returns {Element|null}
   */
  const byId = id => getCache()?.getElementById(id) || document.getElementById(id);

  // Do not run this module inside YouTube Studio (studio.youtube.com)
  const isStudioPage = () => {
    try {
      const host = location.hostname || '';
      const href = location.href || '';
      return (
        host.includes('studio.youtube.com') ||
        host.includes('studio.') ||
        href.includes('studio.youtube.com')
      );
    } catch {
      return false;
    }
  };

  if (isStudioPage()) return;

  let statsInitialized = false;

  const isStatsRelevant = () => {
    try {
      const path = location.pathname || '';
      if (path === '/watch' || path.startsWith('/shorts')) return true;
      return isChannelPage(location.href);
    } catch {
      return false;
    }
  };

  const runWhenReady = cb => {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', cb, { once: true });
    } else {
      cb();
    }
  };

  // Use centralized i18n from YouTubePlusI18n or YouTubeUtils
  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    // Fallback for initialization phase
    if (!key) return '';
    let result = String(key);
    for (const [k, v] of Object.entries(params || {})) {
      result = result.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
    }
    return result;
  };

  // Glassmorphism styles for stats button and menu (shorts-keyboard-feedback look)
  const styles = `
        .videoStats{width:36px;height:36px;border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;margin-left:8px;margin-right:8px;background:transparent;backdrop-filter:blur(10px) saturate(160%);-webkit-backdrop-filter:blur(10px) saturate(160%);border:none;transition:transform .18s ease,background .18s}
        html[dark] .videoStats{background:transparent;border:none}html:not([dark]) .videoStats{background:transparent;border:none}.videoStats:hover{transform:translateY(-2px)}.videoStats svg{width:18px;height:18px;fill:var(--yt-spec-text-primary,#030303)}html[dark] .videoStats svg{fill:#fff}html:not([dark]) .videoStats svg{fill:#222}
        .shortsStats{display:flex;align-items:center;justify-content:center;margin-top:16px;margin-bottom:16px;width:48px;height:48px;border-radius:50%;cursor:pointer;background:rgba(255,255,255,0.12);box-shadow:0 12px 30px rgba(0,0,0,0.32);backdrop-filter:blur(10px) saturate(160%);-webkit-backdrop-filter:blur(10px) saturate(160%);border:1.25px solid rgba(255,255,255,0.12);transition:transform .22s ease}html[dark] .shortsStats{background:rgba(24,24,24,0.68);border:1.25px solid rgba(255,255,255,0.08)}html:not([dark]) .shortsStats{background:rgba(255,255,255,0.12);border:1.25px solid rgba(0,0,0,0.06)}
        .shortsStats:hover{transform:translateY(-3px)}.shortsStats svg{width:24px;height:24px;fill:#222}html[dark] .shortsStats svg{fill:#fff}html:not([dark]) .shortsStats svg{fill:#222}
        .stats-menu-container{position:relative;display:inline-block}.stats-horizontal-menu{position:absolute;display:flex;left:100%;top:0;height:100%;visibility:hidden;opacity:0;transition:visibility 0s,opacity 0.2s linear;z-index:100}.stats-menu-container:hover .stats-horizontal-menu{visibility:visible;opacity:1}.stats-menu-button{margin-left:8px;white-space:nowrap}
        /* Modal overlay and container with glassmorphism */
        .stats-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:linear-gradient(rgba(0,0,0,0.45),rgba(0,0,0,0.55));z-index:99999;display:flex;align-items:center;justify-content:center;animation:fadeInModal .18s;backdrop-filter:blur(20px) saturate(170%);-webkit-backdrop-filter:blur(20px) saturate(170%)}
        .stats-modal-container{max-width:1100px;max-height:calc(100vh - 32px);display:flex;flex-direction:column}
        .stats-modal-content{background:rgba(24,24,24,0.92);border-radius:20px;box-shadow:0 18px 40px rgba(0,0,0,0.45);overflow:hidden;display:flex;flex-direction:column;animation:scaleInModal .18s;border:1.5px solid rgba(255,255,255,0.08);backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%)}
        /* Fix custom element display for Chrome */
        button-view-model{display:inline-flex;align-items:center;justify-content:center;}
        button-view-model.yt-spec-button-view-model{vertical-align:top;}
        html[dark] .stats-modal-content{background:rgba(24, 24, 24, 0.25)}
        html:not([dark]) .stats-modal-content{background:rgba(255,255,255,0.95);color:#222;border:1.25px solid rgba(0,0,0,0.06)}
        .stats-modal-close{background:transparent;border:none;color:#fff;font-size:36px;line-height:1;width:36px;height:36px;cursor:pointer;transition:transform .15s ease,color .15s;display:flex;align-items:center;justify-content:center;border-radius:8px;padding:0}
        .stats-modal-close:hover{color:#ff6b6b;transform:scale(1.1)}
        html:not([dark]) .stats-modal-close{color:#666}
        html:not([dark]) .stats-modal-close:hover{color:#ff6b6b}            
        /* Modal body */
        .stats-modal-body{padding:16px;overflow:visible;flex:1;display:flex;flex-direction:column}
        /* Thumbnail preview */
        .stats-thumb-title-centered{font-size:16px;font-weight:600;color:#fff;margin:0 0 15px 0;text-align:center}
        html:not([dark]) .stats-thumb-title-centered{color:#111}
        .stats-thumb-row{display:flex;gap:12px;align-items:flex-start;flex-wrap:wrap}
        .stats-thumb-img{width:36vw;max-width:420px;height:auto;object-fit:cover;border-radius:8px;flex-shrink:0;border:1px solid rgba(255,255,255,0.06);max-height:44vh}
        html:not([dark]) .stats-thumb-img{border:1px solid rgba(0,0,0,0.06)}
        /* ensure the grid takes remaining horizontal space */
        .stats-thumb-row .stats-grid{flex:1;min-width:0}
        .stats-side-column{flex:1;min-width:280px;display:flex;flex-direction:column}
        .stats-thumb-left{display:flex;flex-direction:column;align-items:center;gap:8px}
        .stats-thumb-left .stats-thumb-sub{font-size:13px;color:rgba(255,255,255,0.65)}
        html:not([dark]) .stats-thumb-left .stats-thumb-sub{color:rgba(0,0,0,0.6)}
        /* extras row under thumbnail: inline, single line */
        .stats-thumb-extras{display:flex;flex-direction:row;gap:10px;align-items:center;margin-top:8px}
        .stats-thumb-extras .stats-card{padding:8px 10px}
        .stats-thumb-meta{display:flex;flex-direction:column;justify-content:center}
        .stats-thumb-sub{font-size:13px;color:rgba(255,255,255,0.65)}
        html:not([dark]) .stats-thumb-sub{color:rgba(0,0,0,0.6)}            
        /* Loading state */
        .stats-loader{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;color:#fff}
        html:not([dark]) .stats-loader{color:#666}
        .stats-spinner{width:60px;height:60px;animation:spin 1s linear infinite;margin-bottom:16px}
        .stats-spinner circle{stroke-dasharray:80;stroke-dashoffset:60;animation:dash 1.5s ease-in-out infinite}            
        /* Error state */
        .stats-error{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;color:#ff6b6b;text-align:center}
        .stats-error-icon{width:60px;height:60px;margin-bottom:16px;stroke:#ff6b6b}            
        /* Stats grid */
        .stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px}            
        /* Stats card */
        .stats-card{background:rgba(255,255,255,0.05);border-radius:12px;padding:12px;display:flex;align-items:center;gap:12px;border:1px solid rgba(255,255,255,0.08);transition:transform .18s ease,box-shadow .18s ease}
        html:not([dark]) .stats-card{background:rgba(0,0,0,0.03);border:1px solid rgba(0,0,0,0.1)}
        .stats-card:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,0.3)}            
        /* Stats icon */
        .stats-icon{width:48px;height:48px;border-radius:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
        .stats-icon svg{width:24px;height:24px}
        .stats-icon-views{background:rgba(59,130,246,0.15);color:#3b82f6}
        .stats-icon-likes{background:rgba(34,197,94,0.15);color:#22c55e}
        .stats-icon-dislikes{background:rgba(239,68,68,0.15);color:#ef4444}
        .stats-icon-comments{background:rgba(168,85,247,0.15);color:#a855f7}
        .stats-icon-viewers{background:rgba(234,179,8,0.15);color:#eab308}
        .stats-icon-subscribers{background:rgba(236,72,153,0.15);color:#ec4899}
        .stats-icon-videos{background:rgba(14,165,233,0.15);color:#0ea5e9}
        /* Pair likes/dislikes into a single grid cell */
        .stats-card-pair{display:flex;gap:8px;align-items:stretch}
        .stats-card-pair .stats-card{flex:1;margin:0}
        @media(max-width:480px){.stats-card-pair{flex-direction:column}}            
        /* Stats info */
        .stats-info{flex:1;min-width:0}
        .stats-label{font-size:13px;color:rgba(255,255,255,0.72);margin-bottom:4px;font-weight:500}
        html:not([dark]) .stats-label{color:rgba(0,0,0,0.6)}
        .stats-value{font-size:20px;font-weight:700;color:#fff;line-height:1.2;margin-bottom:2px}
        html:not([dark]) .stats-value{color:#111}
        .stats-exact{font-size:13px;color:rgba(255,255,255,0.5);font-weight:400}
        html:not([dark]) .stats-exact{color:rgba(0,0,0,0.5)}            
        /* Animations */
        @keyframes fadeInModal{from{opacity:0}to{opacity:1}}
        @keyframes scaleInModal{from{transform:scale(0.95);opacity:0}to{transform:scale(1);opacity:1}}
        @keyframes spin{to{transform:rotate(360deg)}}
        @keyframes dash{0%{stroke-dashoffset:80}50%{stroke-dashoffset:10}100%{stroke-dashoffset:80}}            
        /* Responsive */
        @media(max-width:768px){.stats-modal-container{width:95vw}.stats-grid{grid-template-columns:1fr}.stats-card{padding:16px}.stats-side-column{min-width:0;width:100%}}
        /* Centered large author handle (preferred) */
        .stats-author-big{display:block;text-align:center;margin-top:13px;padding-inline:8px}
        .stats-author-name-big{display:block;color:rgba(255,255,255,0.9);font-weight:600;font-size:16px}
        .stats-author-handle-big{display:inline-block;color:#ffffff;font-weight:700;font-size:20px;text-decoration:none;padding:6px 10px;border-radius:6px}
        .stats-author-handle-big:hover{color:#e6f0ff;text-decoration:underline}
        html:not([dark]) .stats-author-name-big{color:rgba(0,0,0,0.8)}
        html:not([dark]) .stats-author-handle-big{color:#0b61d6}
        html:not([dark]) .stats-author-handle-big:hover{color:#0647a6}
        `;

  // Settings state
  const SETTINGS_KEY = 'youtube_stats_button_enabled';
  let statsButtonEnabled = localStorage.getItem(SETTINGS_KEY) !== 'false';

  let previousUrl = location.href;
  let isChecking = false;
  let experimentalNavListenerKey = null;
  let channelFeatures = {
    hasStreams: false,
    hasShorts: false,
  };

  /**
   * Rate limiter for API calls
   * @type {Object}
   */
  const rateLimiter = {
    requests: new Map(),
    maxRequests: 10,
    timeWindow: 60000, // 1 minute

    /**
     * Check if request is allowed
     * @param {string} key - Request identifier
     * @returns {boolean} Whether request is allowed
     */
    canRequest: key => {
      const now = Date.now();
      const requests = rateLimiter.requests.get(key) || [];

      // Remove old requests outside time window
      const recentRequests = requests.filter(time => now - time < rateLimiter.timeWindow);

      if (recentRequests.length >= rateLimiter.maxRequests) {
        console.warn(
          `[YouTube+][Stats] Rate limit exceeded for ${key}. Max ${rateLimiter.maxRequests} requests per minute.`
        );
        return false;
      }

      recentRequests.push(now);
      rateLimiter.requests.set(key, recentRequests);
      return true;
    },

    /**
     * Clear rate limiter state
     */
    clear: () => {
      rateLimiter.requests.clear();
    },
  };

  function addStyles() {
    if (!byId('youtube-enhancer-styles')) {
      YouTubeUtils.StyleManager.add('youtube-enhancer-styles', styles);
    }
  }

  /**
   * Get current video URL with validation
   * @returns {string|null} Valid YouTube video URL or null
   */
  /**
   * Validate if a string is a valid YouTube video ID
   * @param {string} id - Video ID to validate
   * @returns {boolean} True if valid
   */
  function isValidVideoId(id) {
    return id && /^[a-zA-Z0-9_-]{11}$/.test(id);
  }

  /**
   * Extract video ID from URL parameters
   * @returns {string|null} Video ID or null
   */
  function getVideoIdFromParams() {
    const urlParams = new URLSearchParams(window.location.search);
    const videoId = urlParams.get('v');
    return isValidVideoId(videoId) ? `https://www.youtube.com/watch?v=${videoId}` : null;
  }

  /**
   * Extract video ID from shorts URL
   * @param {string} url - Current URL
   * @returns {string|null} Video ID or null
   */
  function getVideoIdFromShorts(url) {
    const shortsMatch = url.match(/\/shorts\/([^?]+)/);
    if (shortsMatch && isValidVideoId(shortsMatch[1])) {
      return `https://www.youtube.com/shorts/${shortsMatch[1]}`;
    }
    return null;
  }

  function getCurrentVideoUrl() {
    try {
      const url = window.location.href;

      // Validate URL is from YouTube domain
      if (!url.includes('youtube.com')) {
        return null;
      }

      // Try to get video ID from query params first
      const fromParams = getVideoIdFromParams();
      if (fromParams) return fromParams;

      // Try to get from shorts URL
      return getVideoIdFromShorts(url);
    } catch (error) {
      YouTubeUtils?.logError?.('Stats', 'Failed to get video URL', error);
      return null;
    }
  }

  /**
   * Get channel identifier with validation
   * @returns {string} Channel identifier
   */
  function getChannelIdentifier() {
    try {
      const url = window.location.href;
      let identifier = '';

      if (url.includes('/channel/')) {
        identifier = url.split('/channel/')[1].split('/')[0];
      } else if (url.includes('/@')) {
        identifier = url.split('/@')[1].split('/')[0];
      }

      // Validate identifier (alphanumeric, dashes, underscores)
      if (identifier && /^[a-zA-Z0-9_-]+$/.test(identifier)) {
        return identifier;
      }

      return '';
    } catch (error) {
      YouTubeUtils?.logError?.('Stats', 'Failed to get channel identifier', error);
      return '';
    }
  }

  /**
   * Validate YouTube URL
   * @param {string} url - URL to validate
   * @returns {boolean} True if valid YouTube URL
   */
  function validateYouTubeUrl(url) {
    if (!url || typeof url !== 'string') {
      return false;
    }

    try {
      const parsedUrl = new URL(url);
      if (parsedUrl.hostname !== 'www.youtube.com' && parsedUrl.hostname !== 'youtube.com') {
        console.warn('[YouTube+][Stats] Invalid domain for channel check');
        return false;
      }
      return true;
    } catch (error) {
      YouTubeUtils?.logError?.('Stats', 'Invalid URL for channel check', error);
      return false;
    }
  }

  /**
   * Fetch channel page HTML with timeout
   * @param {string} url - URL to fetch
   * @returns {Promise<string|null>} HTML content or null on error
   */
  async function fetchChannelHtml(url) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout

    try {
      const response = await fetch(url, {
        credentials: 'same-origin',
        signal: controller.signal,
        headers: {
          Accept: 'text/html',
        },
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        console.warn(`[YouTube+][Stats] HTTP ${response.status} when checking channel tabs`);
        return null;
      }

      const html = await response.text();

      // Limit response size to prevent memory issues
      if (html.length > 5000000) {
        // 5MB limit
        console.warn('[YouTube+][Stats] Response too large, skipping parse');
        return null;
      }

      return html;
    } catch (error) {
      if (error.name === 'AbortError') {
        console.warn('[YouTube+][Stats] Channel check timed out');
      }
      throw error;
    }
  }

  /**
   * Extract YouTube initial data from HTML
   * @param {string} html - HTML content
   * @returns {Object|null} Parsed YouTube data or null
   */
  function extractYouTubeData(html) {
    const match = html.match(/var ytInitialData = (.+?);<\/script>/);

    if (!match || !match[1]) {
      return null;
    }

    try {
      return JSON.parse(match[1]);
    } catch (parseError) {
      YouTubeUtils?.logError?.('Stats', 'Failed to parse ytInitialData', parseError);
      return null;
    }
  }

  /**
   * Analyze channel tabs for streams and shorts
   * @param {Object} data - YouTube initial data
   * @returns {{hasStreams: boolean, hasShorts: boolean}} Channel features
   */
  /**
   * Extract tab URL from tab renderer
   * @param {Object} tab - Tab object
   * @returns {string|null} Tab URL or null
   */
  function getTabUrl(tab) {
    return tab?.tabRenderer?.endpoint?.commandMetadata?.webCommandMetadata?.url || null;
  }

  /**
   * Check if tab matches a URL pattern
   * @param {string} url - Tab URL
   * @param {RegExp} pattern - Pattern to match
   * @returns {boolean} True if matches
   */
  function tabMatches(url, pattern) {
    return typeof url === 'string' && pattern.test(url);
  }

  /**
   * Analyze channel tabs for presence of streams and shorts
   * @param {Object} data - Channel data
   * @returns {{hasStreams: boolean, hasShorts: boolean}} Tab analysis result
   */
  /**
   * Check if tab is a streams tab
   * @param {string} tabUrl - Tab URL
   * @returns {boolean} True if streams tab
   */
  function isStreamsTab(tabUrl) {
    return tabMatches(tabUrl, /\/streams$/);
  }

  /**
   * Check if tab is a shorts tab
   * @param {string} tabUrl - Tab URL
   * @returns {boolean} True if shorts tab
   */
  function isShortsTab(tabUrl) {
    return tabMatches(tabUrl, /\/shorts$/);
  }

  /**
   * Check if both streams and shorts are found
   * @param {boolean} hasStreams - Has streams flag
   * @param {boolean} hasShorts - Has shorts flag
   * @returns {boolean} True if both found
   */
  function hasBothContentTypes(hasStreams, hasShorts) {
    return hasStreams && hasShorts;
  }

  /**
   * Update content type flags based on tab URL
   * @param {string} tabUrl - Tab URL
   * @param {Object} flags - Flags object with hasStreams and hasShorts
   */
  function updateContentTypeFlags(tabUrl, flags) {
    if (!flags.hasStreams && isStreamsTab(tabUrl)) {
      flags.hasStreams = true;
    }
    if (!flags.hasShorts && isShortsTab(tabUrl)) {
      flags.hasShorts = true;
    }
  }

  /**
   * Analyze channel tabs to determine available content types
   * @param {Object} data - Channel data
   * @returns {{hasStreams: boolean, hasShorts: boolean}} Analysis result
   */
  function analyzeChannelTabs(data) {
    const tabs = data?.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
    const flags = { hasStreams: false, hasShorts: false };

    for (const tab of tabs) {
      const tabUrl = getTabUrl(tab);
      if (!tabUrl) continue;

      updateContentTypeFlags(tabUrl, flags);

      // Early exit if both found
      if (hasBothContentTypes(flags.hasStreams, flags.hasShorts)) break;
    }

    return flags;
  }

  /**
   * Refresh stats menu after checking tabs
   */
  function refreshStatsMenu() {
    const existingMenu = $('.stats-menu-container');
    if (existingMenu) {
      existingMenu.remove();
      createStatsMenu();
    }
  }

  /**
   * Check channel tabs with rate limiting and enhanced security
   * @param {string} url - Channel URL to check
   * @returns {Promise<void>}
   */
  async function checkChannelTabs(url) {
    if (isChecking) return;

    // Validate URL
    if (!validateYouTubeUrl(url)) {
      return;
    }

    // Rate limiting
    if (!rateLimiter.canRequest('checkChannelTabs')) {
      return;
    }

    isChecking = true;

    try {
      const html = await fetchChannelHtml(url);
      if (!html) {
        isChecking = false;
        return;
      }

      const data = extractYouTubeData(html);
      if (!data) {
        isChecking = false;
        return;
      }

      channelFeatures = analyzeChannelTabs(data);
      refreshStatsMenu();
    } catch (error) {
      YouTubeUtils?.logError?.('Stats', 'Failed to check channel tabs', error);
    } finally {
      isChecking = false;
    }
  }

  /**
   * Check if URL is a channel page
   * @param {string} url - URL to check
   * @returns {boolean} Whether URL is a channel page
   */
  function isChannelPage(url) {
    try {
      return (
        url &&
        typeof url === 'string' &&
        url.includes('youtube.com/') &&
        (url.includes('/channel/') || url.includes('/@')) &&
        !url.includes('/video/') &&
        !url.includes('/watch')
      );
    } catch {
      return false;
    }
  }

  /**
   * Check for URL changes with debouncing
   */
  const checkUrlChange =
    YouTubeUtils?.debounce?.(() => {
      try {
        const currentUrl = location.href;
        if (currentUrl !== previousUrl) {
          previousUrl = currentUrl;
          if (isChannelPage(currentUrl)) {
            setTimeout(() => checkChannelTabs(currentUrl), 500);
          }
        }
      } catch (error) {
        YouTubeUtils?.logError?.('Stats', 'URL change check failed', error);
      }
    }, 300) ||
    function () {
      try {
        const currentUrl = location.href;
        if (currentUrl !== previousUrl) {
          previousUrl = currentUrl;
          if (isChannelPage(currentUrl)) {
            setTimeout(() => checkChannelTabs(currentUrl), 500);
          }
        }
      } catch (error) {
        console.error('[YouTube+][Stats] URL change check failed:', error);
      }
    };

  function createStatsIcon() {
    const icon = document.createElement('div');
    // single universal icon for all pages
    icon.className = 'videoStats';

    const SVG_NS = window.YouTubePlusConstants?.SVG_NS || 'http://www.w3.org/2000/svg';
    const svg = document.createElementNS(SVG_NS, 'svg');
    svg.setAttribute('viewBox', '0 0 512 512');

    const path = document.createElementNS(SVG_NS, 'path');
    path.setAttribute(
      'd',
      'M500 89c13.8-11 16-31.2 5-45s-31.2-16-45-5L319.4 151.5 211.2 70.4c-11.7-8.8-27.8-8.5-39.2 .6L12 199c-13.8 11-16 31.2-5 45s31.2 16 45 5L192.6 136.5l108.2 81.1c11.7 8.8 27.8 8.5 39.2-.6L500 89zM160 256l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32zM32 352l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32s-32 14.3-32 32zm288-64c-17.7 0-32 14.3-32 32l0 128c0 17.7 14.3 32 32 32s32-14.3 32-32l0-128c0-17.7-14.3-32-32-32zm96-32l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32z'
    );

    svg.appendChild(path);
    icon.appendChild(svg);

    icon.addEventListener('click', e => {
      e.preventDefault();
      e.stopPropagation();
      const videoUrl = getCurrentVideoUrl();
      if (videoUrl) {
        const urlParams = new URLSearchParams(new URL(videoUrl).search);
        const videoId = urlParams.get('v') || videoUrl.match(/\/shorts\/([^?]+)/)?.[1];
        if (videoId) {
          openStatsModal('video', videoId);
        }
      }
    });

    return icon;
  }

  function insertUniversalIcon() {
    if (!statsButtonEnabled) return;

    // Try to insert into masthead area (requested: "style-scope ytd-masthead").
    // Prefer element matching 'ytd-masthead.style-scope' if present, otherwise fallback to 'ytd-masthead'.
    let masthead = $('ytd-masthead.style-scope');
    if (!masthead) masthead = $('ytd-masthead');

    if (!masthead || $('.videoStats')) return;

    const statsIcon = createStatsIcon();

    // Preferred target: element with id="end" and class containing 'style-scope' inside masthead
    let endElem = $('#end.style-scope.ytd-masthead', masthead);
    if (!endElem) endElem = $('#end', masthead);

    if (endElem) {
      // Insert as first child of #end so it appears at the beginning
      endElem.insertBefore(statsIcon, endElem.firstChild);
    } else {
      // Fallback: append to masthead
      masthead.appendChild(statsIcon);
    }
  }

  function createButton(text, svgPath, viewBox, className, onClick) {
    const buttonViewModel = document.createElement('button-view-model');
    buttonViewModel.className = `yt-spec-button-view-model ${className}-view-model`;

    const button = document.createElement('button');
    button.className = `yt-spec-button-shape-next yt-spec-button-shape-next--outline yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--enable-backdrop-filter-experiment ${className}-button`;
    button.setAttribute('aria-disabled', 'false');
    button.setAttribute('aria-label', text);
    button.style.display = 'flex';
    button.style.alignItems = 'center';
    button.style.justifyContent = 'center';
    button.style.gap = '8px';

    button.addEventListener('click', e => {
      e.preventDefault();
      e.stopPropagation();
      onClick();
    });

    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('viewBox', viewBox);
    svg.style.width = '20px';
    svg.style.height = '20px';
    svg.style.fill = 'currentColor';

    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute('d', svgPath);
    svg.appendChild(path);

    const buttonText = document.createElement('div');
    buttonText.className = `yt-spec-button-shape-next__button-text-content ${className}-text`;
    buttonText.textContent = text;
    buttonText.style.display = 'flex';
    buttonText.style.alignItems = 'center';

    const touchFeedback = document.createElement('yt-touch-feedback-shape');
    touchFeedback.style.borderRadius = 'inherit';

    const touchFeedbackDiv = document.createElement('div');
    touchFeedbackDiv.className =
      'yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response';
    touchFeedbackDiv.setAttribute('aria-hidden', 'true');

    const strokeDiv = document.createElement('div');
    strokeDiv.className = 'yt-spec-touch-feedback-shape__stroke';

    const fillDiv = document.createElement('div');
    fillDiv.className = 'yt-spec-touch-feedback-shape__fill';

    touchFeedbackDiv.appendChild(strokeDiv);
    touchFeedbackDiv.appendChild(fillDiv);
    touchFeedback.appendChild(touchFeedbackDiv);

    button.appendChild(svg);
    button.appendChild(buttonText);
    button.appendChild(touchFeedback);
    buttonViewModel.appendChild(button);

    return buttonViewModel;
  }

  /**
   * InnerTube API configuration
   */
  const INNERTUBE_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
  const INNERTUBE_CLIENT_VERSION = '2.20201209.01.00';

  /**
   * Fetch video stats from InnerTube API (more complete data)
   * @param {string} videoId - Video ID
   * @returns {Promise<Object|null>} Video stats with views, likes, country, monetization
   */
  /**
   * Create InnerTube API request body
   * @param {string} videoId - Video ID
   * @returns {Object} Request body
   */
  function createInnerTubeRequestBody(videoId) {
    return {
      context: {
        client: {
          clientName: 'WEB',
          clientVersion: INNERTUBE_CLIENT_VERSION,
          hl: 'en',
          gl: 'US',
        },
      },
      videoId,
    };
  }

  /**
   * Create InnerTube API fetch options
   * @param {string} videoId - Video ID
   * @returns {Object} Fetch options
   */
  function createInnerTubeFetchOptions(videoId) {
    return {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-YouTube-Client-Name': '1',
        'X-YouTube-Client-Version': INNERTUBE_CLIENT_VERSION,
      },
      body: JSON.stringify(createInnerTubeRequestBody(videoId)),
    };
  }

  /**
   * Extract best thumbnail URL from details
   * @param {Object} details - Video details
   * @returns {string|null} Thumbnail URL
   */
  function extractThumbnailUrl(details) {
    const thumbnails = details.thumbnail?.thumbnails;
    return thumbnails?.[thumbnails.length - 1]?.url || null;
  }

  /**
   * Parse video stats from InnerTube response
   * @param {Object} data - InnerTube response data
   * @returns {Object} Parsed video stats
   */
  function parseVideoStatsFromResponse(data) {
    const details = data.videoDetails || {};
    const microformat = data.microformat?.playerMicroformatRenderer || {};

    // Extract channel handle from ownerProfileUrl (e.g. "/@handle" → "@handle")
    const ownerProfileUrl = microformat.ownerProfileUrl || microformat.ownerUrls?.[0] || '';
    const handleMatch = ownerProfileUrl.match(/\/@([\w.-]+)/);
    const authorHandle = handleMatch ? `@${handleMatch[1]}` : null;

    return {
      videoId: details.videoId,
      title: details.title,
      author: details.author || null,
      authorHandle: authorHandle,
      views: details.viewCount ? parseInt(details.viewCount, 10) : null,
      likes: null, // Will be fetched separately
      thumbnail: extractThumbnailUrl(details),
      duration: details.lengthSeconds,
      country: null, // Fetched separately from channel browse (availableCountries is geo-restriction, not creator country)
      monetized: microformat.isFamilySafe !== undefined,
      channelId: details.channelId,
    };
  }

  /**
   * Fetch the channel creator's country from InnerTube browse API
   * @param {string} channelId - YouTube channel ID (UCxxxx)
   * @returns {Promise<string|null>} 2-letter country code or null
   */
  async function fetchChannelCountryFromInnerTube(channelId) {
    if (!channelId) return null;
    try {
      const url = `https://www.youtube.com/youtubei/v1/browse?key=${INNERTUBE_API_KEY}&prettyPrint=false`;
      const body = {
        browseId: channelId,
        context: {
          client: {
            clientName: 'WEB',
            clientVersion: INNERTUBE_CLIENT_VERSION,
            hl: 'en',
            gl: 'US',
          },
        },
      };
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-YouTube-Client-Name': '1',
          'X-YouTube-Client-Version': INNERTUBE_CLIENT_VERSION,
        },
        body: JSON.stringify(body),
      });
      if (!response.ok) return null;
      const data = await response.json();
      // Try multiple header paths — YouTube returns c4TabbedHeaderRenderer for
      // older-UI channels and pageHeaderRenderer for newer-UI channels.
      // Also check frameworkUpdates mutations for new-UI country metadata.
      const country =
        data?.header?.c4TabbedHeaderRenderer?.country ||
        data?.header?.pageHeaderRenderer?.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows?.[0]?.metadataParts?.find?.(
          p => p?.text?.content?.length === 2
        )?.text?.content ||
        (() => {
          const mutations = data?.frameworkUpdates?.entityBatchUpdate?.mutations || [];
          for (const m of mutations) {
            const c =
              m?.payload?.channelHeaderMetadataEntityViewModel?.country ||
              m?.payload?.channelBasicInfoEntityViewModel?.country;
            if (c) return c;
          }
          return null;
        })();
      return country || null;
    } catch {
      return null;
    }
  }

  /**
   * Fallback: fetch channel country from TubeInsights backend API
   * @param {string} channelId - YouTube channel ID (UCxxxx)
   * @returns {Promise<string|null>} 2-letter country code or null
   */
  async function fetchChannelCountryFromTubeInsights(channelId) {
    if (!channelId) return null;
    try {
      const response = await fetch(`https://tubeinsights.exyezed.cc/api/channels/${channelId}`);
      if (!response.ok) return null;
      const data = await response.json();
      const country = data?.items?.[0]?.snippet?.country;
      return typeof country === 'string' && country.trim() ? country.trim().toUpperCase() : null;
    } catch {
      return null;
    }
  }

  /**
   * Fetch video stats from InnerTube API
   * @param {string} videoId - Video ID
   * @returns {Promise<Object|null>} Video stats or null
   */
  async function fetchVideoStatsInnerTube(videoId) {
    if (!videoId) return null;

    try {
      const url = `https://www.youtube.com/youtubei/v1/player?key=${INNERTUBE_API_KEY}&prettyPrint=false`;
      const response = await fetch(url, createInnerTubeFetchOptions(videoId));

      if (!response.ok) {
        console.warn(`[YouTube+][Stats] InnerTube API failed:`, response.status);
        return null;
      }

      const data = await response.json();
      const stats = parseVideoStatsFromResponse(data);

      // Fetch the real creator country from channel browse API
      if (stats.channelId) {
        stats.country = await fetchChannelCountryFromInnerTube(stats.channelId);
        if (!stats.country) {
          stats.country = await fetchChannelCountryFromTubeInsights(stats.channelId);
        }
      }

      return stats;
    } catch (error) {
      console.error('[YouTube+][Stats] InnerTube fetch error:', error);
      return null;
    }
  }

  /**
   * Fetch dislikes from Return YouTube Dislike API
   * @param {string} videoId - Video ID
   * @returns {Promise<Object|null>} Likes and dislikes data
   */
  async function fetchDislikesData(videoId) {
    if (!videoId) return null;

    try {
      const response = await fetch(`https://returnyoutubedislikeapi.com/votes?videoId=${videoId}`);
      if (!response.ok) return null;

      const data = await response.json();
      return {
        likes: data.likes || null,
        dislikes: data.dislikes || null,
        rating: data.rating || null,
      };
    } catch (error) {
      console.error('[YouTube+][Stats] Failed to fetch dislikes:', error);
      return null;
    }
  }

  /**
   * Fetch video or channel stats from API (combines InnerTube + RYD)
   * @param {string} type - 'video' or 'channel'
   * @param {string} id - Video ID or Channel ID
   * @returns {Promise<Object|null>} Stats data or null on error
   */
  async function fetchStats(type, id) {
    if (!id) return { ok: false, status: 0, data: null };

    try {
      if (type === 'video') {
        // Use InnerTube API for video data
        const videoData = await fetchVideoStatsInnerTube(id);
        if (!videoData) {
          return { ok: false, status: 404, data: null };
        }

        // Fetch likes/dislikes from RYD API
        const dislikeData = await fetchDislikesData(id);
        if (dislikeData) {
          videoData.likes = dislikeData.likes;
          videoData.dislikes = dislikeData.dislikes;
          videoData.rating = dislikeData.rating;
        }

        return { ok: true, status: 200, data: videoData };
      }

      // For channels, use existing API
      const endpoint = `https://api.livecounts.io/youtube-live-subscriber-counter/stats/${id}`;
      const response = await fetch(endpoint, {
        method: 'GET',
        headers: {
          Accept: 'application/json',
        },
      });

      if (!response.ok) {
        console.warn(`[YouTube+][Stats] Failed to fetch ${type} stats:`, response.status);
        return { ok: false, status: response.status, data: null, url: endpoint };
      }

      const data = await response.json();
      return { ok: true, status: response.status, data, url: endpoint };
    } catch (error) {
      YouTubeUtils?.logError?.('Stats', `Failed to fetch ${type} stats`, error);
      return { ok: false, status: 0, data: null };
    }
  }

  /**
   * Attempt to read basic video stats from the current page DOM as a fallback
   * Returns an object with views, likes, comments, subscribers when available
   */
  /**
   * Get video stats from current page DOM
   * Refactored to use helper functions and reduce complexity
   * @returns {Object|null} Stats object or null
   */
  function getPageVideoStats() {
    try {
      // Use centralized helpers from YouTubeStatsHelpers when available.
      // If not present (some runtime environments), provide a lightweight
      // DOM-based fallback to avoid noisy errors and still surface basic stats.
      const helpers = window.YouTubeStatsHelpers || {};

      const fallbackHelpers = {
        extractViews() {
          try {
            const el = $('yt-view-count-renderer, #count .view-count');
            const text = el && el.textContent ? el.textContent.trim() : '';
            const match = text.replace(/[^0-9,\.]/g, '').replace(/,/g, '');
            return match ? { views: Number(match) || null } : {};
          } catch {
            return {};
          }
        },
        extractLikes() {
          try {
            const btn =
              $('ytd-toggle-button-renderer[is-icon-button] yt-formatted-string') ||
              $(
                '#top-level-buttons-computed ytd-toggle-button-renderer:first-child yt-formatted-string'
              );
            const text = btn && btn.textContent ? btn.textContent.trim() : '';
            const match = text.replace(/[^0-9,\.]/g, '').replace(/,/g, '');
            return match ? { likes: Number(match) || null } : {};
          } catch {
            return {};
          }
        },
        extractDislikes() {
          // Dislike counts may not be available; return empty
          return {};
        },
        extractComments() {
          try {
            const el = $(
              '#count > ytd-comment-thread-renderer, ytd-comments-header-renderer #count'
            );
            const text = el && el.textContent ? el.textContent.trim() : '';
            const match = text.replace(/[^0-9,\.]/g, '').replace(/,/g, '');
            return match ? { comments: Number(match) || null } : {};
          } catch {
            return {};
          }
        },
        extractSubscribers() {
          try {
            const el = $('#owner-sub-count, #subscriber-count');
            const text = el && el.textContent ? el.textContent.trim() : '';
            return text ? { subscribers: text } : {};
          } catch {
            return {};
          }
        },
        extractThumbnail() {
          try {
            const meta = $('link[rel="image_src"]') || $('meta[property="og:image"]');
            const url = meta && (meta.href || meta.content) ? meta.href || meta.content : null;
            return url ? { thumbnail: url } : {};
          } catch {
            return {};
          }
        },
        extractTitle() {
          try {
            const el = $('h1.title yt-formatted-string') || $('h1');
            const text = el && el.textContent ? el.textContent.trim() : '';
            return text ? { title: text } : {};
          } catch {
            return {};
          }
        },
        extractAuthor() {
          try {
            // Try to get the @handle from the owner link
            const handleEl =
              $('ytd-video-owner-renderer #channel-handle') ||
              $('ytd-video-owner-renderer yt-formatted-string.ytd-channel-name a') ||
              $('#owner ytd-channel-name a') ||
              $('ytd-video-owner-renderer #owner-name a');
            const handleText = handleEl?.textContent?.trim() || '';
            // Some links contain the @handle, others contain the channel name
            const handle = handleText.startsWith('@') ? handleText : null;
            const nameEl =
              $('ytd-video-owner-renderer #channel-name') ||
              $('ytd-video-owner-renderer #owner-name');
            const authorName = nameEl?.textContent?.trim() || null;
            if (handle || authorName) {
              return { authorHandle: handle, author: authorName };
            }
            return {};
          } catch {
            return {};
          }
        },
      };

      const use = helpers && helpers.extractViews ? helpers : fallbackHelpers;

      // Merge all extracted stats (helpers may return partial objects)
      const result = Object.assign(
        {},
        use.extractViews?.() || {},
        use.extractLikes?.() || {},
        use.extractDislikes?.() || {},
        use.extractComments?.() || {},
        use.extractSubscribers?.() || {},
        use.extractThumbnail?.() || {},
        use.extractTitle?.() || {},
        use.extractAuthor?.() || {}
      );

      return Object.keys(result).length > 0 ? result : null;
    } catch (e) {
      YouTubeUtils?.logError?.('Stats', 'Failed to read page stats', e);
      return null;
    }
  }

  /**
   * Render a small grid from page-derived stats into container
   * @param {HTMLElement} container
   * @param {Object} pageStats
   */
  // Helper to create a stats card HTML when value exists
  function buildPageStatCard(value, labelKey, iconClass, iconSvg) {
    if (value === undefined || value === null) return '';
    return `
        <div class="stats-card">
          <div class="stats-icon ${iconClass}">
            ${iconSvg}
          </div>
          <div class="stats-info">
            <div class="stats-label">${t(labelKey)}</div>
            <div class="stats-value">${formatNumber(value)}</div>
            <div class="stats-exact">${(value || 0).toLocaleString()}</div>
          </div>
        </div>
      `;
  }

  // Helper to create a stats-card that shows only a value (no label)
  // iconOrClass can be either an HTML string (SVG) or a class name like 'stats-icon-views'
  function buildValueOnlyCard(
    value,
    iconOrClass = '',
    options = { showValue: true, showIcon: true }
  ) {
    const { showValue, showIcon } = options;
    if (!showValue && !showIcon) return '';

    // If value is null/undefined and we are to show value, treat as unknown
    let displayVal = '';
    if (showValue) {
      displayVal = value === undefined || value === null ? t('unknown') : value;
    }

    // Determine whether iconOrClass is HTML (contains '<') or a plain class name
    let iconContent = '';
    let extraClass = '';
    if (showIcon) {
      if (iconOrClass && typeof iconOrClass === 'string' && iconOrClass.indexOf('<') >= 0) {
        // it's HTML (SVG), render inside
        iconContent = iconOrClass;
      } else if (iconOrClass && typeof iconOrClass === 'string') {
        // treat as a class name to apply to the icon wrapper
        extraClass = ` ${iconOrClass}`;
      }
    }

    return `
      <div class="stats-card">
        <div class="stats-icon${extraClass}">${iconContent}</div>
        <div class="stats-info">
          ${showValue ? `<div class="stats-value">${displayVal}</div>` : ''}
        </div>
      </div>
    `;
  }

  /**
   * Build stat cards for all metrics
   * @param {Object} pageStats - Page statistics
   * @returns {Array<string>} Array of card HTML strings
   * @private
   */
  function buildStatCards(pageStats) {
    const cardConfigs = [
      {
        value: pageStats.views,
        key: 'views',
        icon: 'stats-icon-views',
        svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>',
      },
      {
        value: pageStats.likes,
        key: 'likes',
        icon: 'stats-icon-likes',
        svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>',
      },
      {
        value: pageStats.dislikes,
        key: 'dislikes',
        icon: 'stats-icon-dislikes',
        svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg>',
      },
      {
        value: pageStats.comments,
        key: 'comments',
        icon: 'stats-icon-comments',
        svg: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>',
      },
    ];

    return cardConfigs
      .map(config => buildPageStatCard(config.value, config.key, config.icon, config.svg))
      .filter(card => card);
  }

  /**
   * Get thumbnail URL from various sources
   * @param {string} id - Video ID
   * @param {Object} pageStats - Page statistics
   * @returns {string} Thumbnail URL or empty string
   * @private
   */
  function getThumbnailUrl(id, pageStats) {
    if (pageStats && pageStats.thumbnail) {
      return pageStats.thumbnail;
    }
    if (id) {
      return `https://i.ytimg.com/vi/${id}/hqdefault.jpg`;
    }
    return '';
  }

  /**
   * Build extra metadata cards
   * @param {Object} extras - Extra metadata
   * @returns {string} HTML for extra cards
   * @private
   */
  function buildExtraCards(extras) {
    const monetizationText = extras.monetization || t('unknown');
    const countryText = extras.country || t('unknown');
    const durationText = extras.duration || t('unknown');

    const extraMonCard = buildValueOnlyCard(monetizationText, 'stats-icon-subscribers', {
      showValue: false,
      showIcon: true,
    });
    const extraCountryCard = buildValueOnlyCard(countryText, 'stats-icon-views', {
      showValue: false,
      showIcon: true,
    });
    const extraDurationCard = buildValueOnlyCard(durationText, 'stats-icon-videos', {
      showValue: true,
      showIcon: false,
    });

    return `${extraMonCard}${extraCountryCard}${extraDurationCard}`;
  }

  /**
   * Build complete HTML with thumbnail layout
   * @param {string} titleHtml - Title HTML
   * @param {string} thumbUrl - Thumbnail URL
   * @param {string} gridHtml - Grid HTML
   * @param {Object} extras - Extra metadata
   * @returns {string} Complete HTML
   * @private
   */
  function buildThumbnailLayout(titleHtml, thumbUrl, gridHtml, extras) {
    const extraCards = buildExtraCards(extras);
    const leftHtml = `<div class="stats-thumb-left"><img class="stats-thumb-img" src="${thumbUrl}" alt="thumbnail"><div class="stats-thumb-extras">${extraCards}</div></div>`;
    return `${titleHtml}<div class="stats-thumb-row">${leftHtml}${gridHtml}</div>`;
  }

  /**
   * Render page statistics fallback view
   * @param {HTMLElement} container - Container element
   * @param {Object} pageStats - Page statistics
   * @param {string} id - Video ID
   */
  function renderPageFallback(container, pageStats, id) {
    // Build stat cards
    const cards = buildStatCards(pageStats);
    const gridHtml = `<div class="stats-grid">${cards.join('')}</div>`;

    // Get title and escape for XSS prevention
    const title = (pageStats && pageStats.title) || document.title || '';
    const escapeHtml =
      window.YouTubeSecurityUtils?.escapeHtml ||
      (s => {
        const d = document.createElement('div');
        d.textContent = s;
        return d.innerHTML;
      });
    const safeTitle = escapeHtml(title);
    const titleHtml = safeTitle ? `<div class="stats-thumb-title-centered">${safeTitle}</div>` : '';

    // Get thumbnail and extras
    const thumbUrl = getThumbnailUrl(id, pageStats);
    const extras = getVideoExtras(null, pageStats, id);

    // Render appropriate layout
    if (thumbUrl) {
      container.innerHTML = buildThumbnailLayout(titleHtml, thumbUrl, gridHtml, extras);
    } else {
      container.innerHTML = `${titleHtml}${gridHtml}`;
    }
  }

  /**
   * Format number with K/M/B suffixes
   * @param {number} num - Number to format
   * @returns {string} Formatted number
   */
  function formatNumber(num) {
    if (!num || isNaN(num)) return '0';
    const absNum = Math.abs(num);

    if (absNum >= 1e9) {
      return `${(num / 1e9).toFixed(1)}B`;
    }
    if (absNum >= 1e6) {
      return `${(num / 1e6).toFixed(1)}M`;
    }
    if (absNum >= 1e3) {
      return `${(num / 1e3).toFixed(1)}K`;
    }
    return num.toLocaleString();
  }

  /**
   * Create a stats card HTML fragment
   * @param {string} labelKey
   * @param {number|null} value
   * @param {number|null} exact
   * @param {string} iconClass
   * @param {string} iconSvg
   * @returns {string}
   */
  function makeStatsCard(labelKey, value, exact, iconClass, iconSvg) {
    const display = value == null ? t('unknown') : formatNumber(value);
    // Show exact 0 as "0" (0 is falsy), only show dash when null/undefined
    // Ensure numeric values are properly converted to integers for exact display
    let exactText = '—';
    if (exact !== null && exact !== undefined) {
      const numExact = Number(exact);
      exactText = !isNaN(numExact) ? Math.floor(numExact).toLocaleString() : String(exact);
    }
    return `
        <div class="stats-card">
          <div class="stats-icon ${iconClass}">
            ${iconSvg}
          </div>
          <div class="stats-info">
            <div class="stats-label">${t(labelKey)}</div>
            <div class="stats-value">${display}</div>
            <div class="stats-exact">${exactText}</div>
          </div>
        </div>
      `;
  }

  /**
   * Normalize and pick preferred video fields
   * @param {Object} stats
   * @returns {{views: number|null, likes: number|null, dislikes: number|null, comments: number|null, liveViewer: number|null, title: string, thumbUrl: string, country: string|null, monetized: boolean|null}}
   */
  /**
   * Extract video fields from stats object
   * Simplified by using more consistent field access
   * @param {Object} stats - Stats object
   * @param {string} id - Video ID
   * @returns {Object} Extracted fields
   */
  /**
   * Get first available field from stats object
   * @param {Object} stats - Stats object
   * @param {string[]} fields - Field names to check
   * @returns {*} First available value or null
   */
  function getFirstAvailableField(stats, ...fields) {
    for (const field of fields) {
      if (stats?.[field] != null) return stats[field];
    }
    return null;
  }

  /**
   * Get thumbnail URL for video
   * @param {Object} stats - Stats object
   * @param {string} id - Video ID
   * @returns {string} Thumbnail URL
   */
  function getThumbnailUrl(stats, id) {
    return stats?.thumbnail || (id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : '');
  }

  /**
   * Extract video fields from stats object
   * @param {Object} stats - Stats data
   * @param {string} id - Video ID
   * @returns {Object} Extracted fields
   */
  function extractVideoFields(stats, id) {
    return {
      views: getFirstAvailableField(stats, 'liveViews', 'views', 'viewCount'),
      likes: getFirstAvailableField(stats, 'liveLikes', 'likes', 'likeCount'),
      dislikes: getFirstAvailableField(stats, 'dislikes', 'liveDislikes', 'dislikeCount'),
      comments: getFirstAvailableField(stats, 'liveComments', 'comments', 'commentCount'),
      liveViewer: getFirstAvailableField(stats, 'liveViewer', 'live_viewers'),
      title: stats?.title || document.title || '',
      thumbUrl: getThumbnailUrl(stats, id),
      country: getFirstAvailableField(stats, 'country'),
      monetized: stats?.monetized ?? null,
      duration: getFirstAvailableField(stats, 'duration'),
      author: getFirstAvailableField(stats, 'author'),
      authorHandle: getFirstAvailableField(stats, 'authorHandle'),
    };
  }

  /**
   * Merge API-provided video stats with page-derived stats
   * Simplified to use helper function for field extraction
   * @param {Object|null} apiStats - API stats
   * @param {Object|null} pageStats - Page stats
   * @returns {Object} Merged stats
   */
  function mergeVideoStats(apiStats, pageStats) {
    if (!pageStats) return apiStats || {};

    const getValue = (...fields) => {
      for (const field of fields) {
        if (apiStats?.[field] != null) return apiStats[field];
      }
      for (const field of fields) {
        if (pageStats?.[field] != null) return pageStats[field];
      }
      return null;
    };

    return {
      ...apiStats,
      views: getValue('views', 'viewCount'),
      likes: getValue('likes', 'likeCount'),
      dislikes: getValue('dislikes'),
      comments: getValue('comments', 'commentCount'),
      thumbnail: getValue('thumbnail'),
      title: getValue('title'),
      liveViewer: getValue('liveViewer'),
      // Preserve extra metadata when available (duration, country, monetization)
      duration: getValue('duration'),
      country: getValue('country'),
      monetized: getValue('monetized', 'isMonetized', 'monetization'),
      author: getValue('author'),
      authorHandle: getValue('authorHandle'),
    };
  }

  /**
   * Extract extra metadata (duration, monetization, country) from API or page
   * @param {Object|null} apiStats - API stats
   * @param {Object|null} pageStats - Page stats
   * @returns {{duration: string|null, monetization: string|null, country: string|null}} Metadata
   */
  function getVideoExtras(apiStats, pageStats) {
    const helpers = window.YouTubeStatsHelpers || {};
    // Prefer explicit fields on the stats objects first, then fall back to helper functions
    const duration =
      apiStats?.duration ??
      pageStats?.duration ??
      helpers.getDurationFromSources?.(apiStats, pageStats) ??
      null;
    const country =
      apiStats?.country ??
      pageStats?.country ??
      helpers.getCountryFromSources?.(apiStats, pageStats) ??
      null;

    // Monetization can be boolean or descriptive string from helpers
    let monetization = null;
    if (apiStats?.monetized != null) {
      monetization = apiStats.monetized === true ? t('yes') : t('no');
    } else if (pageStats?.monetized != null) {
      monetization = pageStats.monetized === true ? t('yes') : t('no');
    } else {
      monetization = helpers.getMonetizationFromSources?.(apiStats, pageStats, t) ?? null;
    }

    return { duration, country, monetization };
  }

  /**
   * Open stats modal with live data display
   * @param {string} type - 'video' or 'channel'
   * @param {string} id - Video ID or Channel ID
   */
  /**
   * Create close button for stats modal
   * @param {HTMLElement} overlay - Overlay element to close
   * @returns {HTMLElement} Close button
   */
  function createStatsModalCloseButton(overlay) {
    const closeBtn = document.createElement('button');
    closeBtn.className = 'thumbnail-modal-close thumbnail-modal-action-btn';
    closeBtn.innerHTML = `
            <svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
            </svg>
            `;
    closeBtn.title = t('close');
    closeBtn.setAttribute('aria-label', t('close'));
    closeBtn.addEventListener('click', e => {
      e.preventDefault();
      e.stopPropagation();
      overlay.remove();
    });
    return closeBtn;
  }

  /**
   * Create loading spinner element
   * @returns {HTMLElement} Loading spinner
   */
  function createLoadingSpinner() {
    const loader = document.createElement('div');
    loader.className = 'stats-loader';
    loader.innerHTML = `
      <svg class="stats-spinner" viewBox="0 0 50 50">
        <circle cx="25" cy="25" r="20" fill="none" stroke="currentColor" stroke-width="4"></circle>
      </svg>
      <p>${t('loadingStats')}</p>
    `;
    return loader;
  }

  /**
   * Create stats modal structure
   * @param {HTMLElement} overlay - Overlay element
   * @returns {{body: HTMLElement, container: HTMLElement}} Modal elements
   */
  function createStatsModalStructure(overlay) {
    const container = document.createElement('div');
    container.className = 'stats-modal-container';

    const content = document.createElement('div');
    content.className = 'stats-modal-content';

    const body = document.createElement('div');
    body.className = 'stats-modal-body';
    body.appendChild(createLoadingSpinner());

    content.appendChild(body);

    const wrapper = document.createElement('div');
    wrapper.className = 'thumbnail-modal-wrapper';

    const actionsDiv = document.createElement('div');
    actionsDiv.className = 'thumbnail-modal-actions';
    actionsDiv.appendChild(createStatsModalCloseButton(overlay));

    wrapper.appendChild(content);
    wrapper.appendChild(actionsDiv);
    container.appendChild(wrapper);

    return { body, container };
  }

  /**
   * Setup modal event handlers
   * @param {HTMLElement} overlay - Overlay element
   * @returns {void}
   */
  function setupModalEventHandlers(overlay) {
    // Close when clicking outside
    overlay.addEventListener('click', ({ target }) => {
      if (target === overlay) overlay.remove();
    });

    // ESC to close
    function escHandler(e) {
      if (e.key === 'Escape') {
        overlay.remove();
        window.removeEventListener('keydown', escHandler, true);
      }
    }
    window.addEventListener('keydown', escHandler, true);
  }

  /**
   * Render error message in modal
   * @param {HTMLElement} body - Body element
   * @param {Object} result - Fetch result
   * @returns {void}
   */
  function renderErrorMessage(body, result) {
    const statusText = result?.status ? ` (${result.status})` : '';
    const endpointHint = result?.url
      ? `<div style="margin-top:8px;font-size:12px;opacity:0.8;word-break:break-all">${result.url}</div>`
      : '';
    body.innerHTML = `
        <div class="stats-error">
          <svg class="stats-error-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <circle cx="12" cy="12" r="10"></circle>
            <line x1="12" y1="8" x2="12" y2="12"></line>
            <line x1="12" y1="16" x2="12.01" y2="16"></line>
          </svg>
          <p>${t('failedToLoadStats')}${statusText}</p>
          ${endpointHint}
        </div>
      `;
  }

  /**
   * Handle failed stats fetch
   * @param {HTMLElement} body - Body element
   * @param {Object} result - Fetch result
   * @param {string} id - Video/channel ID
   * @returns {void}
   */
  function handleFailedFetch(body, result, id) {
    const pageStats = getPageVideoStats();
    if (pageStats) {
      renderPageFallback(body, pageStats, id);
    } else {
      renderErrorMessage(body, result);
    }
  }

  /**
   * Display stats based on type
   * @param {HTMLElement} body - Body element
   * @param {string} type - Stats type (video/channel)
   * @param {Object} stats - Stats data
   * @param {string} id - Video/channel ID
   * @returns {void}
   */
  function displayStatsBasedOnType(body, type, stats, id) {
    if (type === 'video') {
      try {
        const pageStats = getPageVideoStats();
        const merged = mergeVideoStats(stats, pageStats);
        displayVideoStats(body, merged, id);
      } catch {
        displayVideoStats(body, stats, id);
      }
    } else {
      displayChannelStats(body, stats);
    }
  }

  /**
   * Open stats modal
   * @param {string} type - Stats type (video/channel)
   * @param {string} id - Video/channel ID
   * @returns {Promise<void>}
   */
  async function openStatsModal(type, id) {
    if (!type || !id) {
      console.error('[YouTube+][Stats] Invalid parameters for modal');
      return;
    }

    // Remove existing overlays (cache NodeList to avoid repeated lookups)
    const existingOverlays = $$('.stats-modal-overlay');
    for (let i = 0; i < existingOverlays.length; i++) {
      try {
        existingOverlays[i].remove();
      } catch {
        /* ignore individual failures */
      }
    }

    // Create modal structure
    const overlay = document.createElement('div');
    overlay.className = 'stats-modal-overlay';

    const { body, container } = createStatsModalStructure(overlay);
    overlay.appendChild(container);

    setupModalEventHandlers(overlay);
    document.body.appendChild(overlay);

    // Fetch and display stats
    const result = await fetchStats(type, id);

    if (!result?.ok) {
      handleFailedFetch(body, result, id);
      return;
    }

    displayStatsBasedOnType(body, type, result.data, id);
  }

  /**
   * Display video statistics
   * @param {HTMLElement} container - Container element
   * @param {Object} stats - Stats data
   */
  /**
   * Get stat card definitions for video stats
   * @param {Object} fields - Extracted video fields
   * @returns {Array} Card definitions
   */
  function getVideoStatDefinitions(fields) {
    const { views, likes, dislikes, comments } = fields;
    return [
      {
        label: 'views',
        value: views,
        exact: views,
        iconClass: 'stats-icon-views',
        iconSvg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>`,
      },
      {
        label: 'likes',
        value: likes,
        exact: likes,
        iconClass: 'stats-icon-likes',
        iconSvg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>`,
      },
      {
        label: 'dislikes',
        value: dislikes,
        exact: dislikes,
        iconClass: 'stats-icon-dislikes',
        iconSvg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg>`,
      },
      {
        label: 'comments',
        value: comments,
        exact: comments,
        iconClass: 'stats-icon-comments',
        iconSvg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`,
      },
    ];
  }

  /**
   * Create live viewer stats card if available
   * @param {*} liveViewer - Live viewer count
   * @returns {string} HTML string or empty string
   */
  function createLiveViewerCard(liveViewer) {
    if (liveViewer === undefined || liveViewer === null) return '';
    return makeStatsCard(
      'liveViewers',
      liveViewer,
      liveViewer,
      'stats-icon-viewers',
      `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>`
    );
  }

  /**
   * Create monetization meta card
   * @param {Object} extras - Video extras
   * @param {Object} stats - Stats object
   * @returns {string} HTML string
   */
  function createMonetizationCard(extras, stats) {
    const monetizationValue = extras.monetization || t('unknown');
    const isMonetized = extras.monetization === t('yes') || stats?.monetized === true;
    const monIcon = isMonetized
      ? `<svg viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path></svg>`
      : `<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>`;
    return `<div class="stats-card" style="padding:10px;"><div class="stats-icon stats-icon-subscribers">${monIcon}</div><div class="stats-info"><div class="stats-label" style="font-size:12px;">${t('monetization')}</div><div class="stats-value" style="font-size:16px;">${monetizationValue}</div></div></div>`;
  }

  /**
   * Create country meta card with flag
   * @param {Object} extras - Video extras
   * @returns {string} HTML string
   */
  function createCountryCard(extras) {
    const countryValue = extras.country || t('unknown');
    const countryCode =
      extras.country && extras.country !== t('unknown') ? extras.country.toUpperCase() : '';
    const globeIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`;

    if (countryCode) {
      const flagUrl = `https://cdn.jsdelivr.net/gh/lipis/[email protected]/flags/4x3/${countryCode.toLowerCase()}.svg`;
      return `<div class="stats-card" style="padding:10px;"><div class="stats-icon stats-icon-views" data-fallback-icon="globe"><img class="country-flag" src="${flagUrl}" alt="${countryCode}" width="32" height="24" style="border-radius:4px;"/></div><div class="stats-info"><div class="stats-label" style="font-size:12px;">${t('country')}</div><div class="stats-value" style="font-size:16px;">${countryCode}</div></div></div>`;
    }
    return `<div class="stats-card" style="padding:10px;"><div class="stats-icon stats-icon-views">${globeIcon}</div><div class="stats-info"><div class="stats-label" style="font-size:12px;">${t('country')}</div><div class="stats-value" style="font-size:16px;">${countryValue}</div></div></div>`;
  }

  /**
   * Create duration meta card
   * @param {Object} extras - Video extras
   * @returns {string} HTML string
   */
  /**
   * Format duration values into human readable strings.
   * Accepts seconds (number or numeric string), ISO8601 (PT1H2M3S),
   * or colon-formatted strings (MM:SS or HH:MM:SS).
   * Returns null when value cannot be parsed.
   * @param {number|string} value
   * @returns {string|null}
   */
  function formatDuration(value) {
    if (value == null) return null;

    function pad(n) {
      return String(n).padStart(2, '0');
    }

    function secToHms(sec) {
      sec = Math.max(0, Math.floor(Number(sec) || 0));
      const h = Math.floor(sec / 3600);
      const m = Math.floor((sec % 3600) / 60);
      const s = sec % 60;
      if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
      return `${m}:${pad(s)}`;
    }

    // number -> seconds
    if (typeof value === 'number' && Number.isFinite(value)) return secToHms(value);

    // numeric string
    if (typeof value === 'string') {
      const s = value.trim();
      if (/^\d+$/.test(s)) return secToHms(Number(s));

      // ISO 8601 duration PT#H#M#S
      const iso = /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/i.exec(s);
      if (iso) {
        const h = parseInt(iso[1] || '0', 10);
        const m = parseInt(iso[2] || '0', 10);
        const sec = parseInt(iso[3] || '0', 10);
        const total = h * 3600 + m * 60 + sec;
        return secToHms(total);
      }

      // Already colon formatted like M:SS or H:MM:SS
      if (/^\d+:\d{1,2}(:\d{1,2})?$/.test(s)) {
        const parts = s.split(':').map(p => p.replace(/^0+(\d)/, '$1'));
        // normalize to pad minutes/seconds
        if (parts.length === 2) {
          const [mm, ss] = parts;
          return `${Number(mm)}:${pad(Number(ss))}`;
        }
        if (parts.length === 3) {
          const [hh, mm, ss] = parts;
          return `${Number(hh)}:${pad(Number(mm))}:${pad(Number(ss))}`;
        }
      }

      // fallback: return as-is (useful when API already provides formatted text)
      return s || null;
    }

    return null;
  }
  function createDurationCard(extras) {
    const raw = extras?.duration ?? null;
    const formatted = formatDuration(raw);
    const durationValue = formatted || (raw ? String(raw) : null) || t('unknown');
    const durationIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`;
    return `<div class="stats-card" style="padding:10px;"><div class="stats-icon stats-icon-videos">${durationIcon}</div><div class="stats-info"><div class="stats-label" style="font-size:12px;">${t('duration')}</div><div class="stats-value" style="font-size:16px;">${durationValue}</div></div></div>`;
  }

  /**
   * Build metadata cards HTML
   * @param {Object} stats - Stats object
   * @param {Object} extras - Video extras
   * @returns {string} HTML string
   */
  function buildMetaCardsHtml(stats, extras) {
    const cards = [
      createMonetizationCard(extras, stats),
      createCountryCard(extras),
      createDurationCard(extras),
    ];
    return cards.filter(Boolean).join('');
  }

  /**
   * Display video statistics
   * @param {HTMLElement} container - Container element
   * @param {Object} stats - Stats data
   * @param {string} id - Video ID
   */
  function displayVideoStats(container, stats, id) {
    const fields = extractVideoFields(stats, id);
    const { liveViewer, title, thumbUrl } = fields;

    // Escape title for XSS prevention
    const escapeHtml =
      window.YouTubeSecurityUtils?.escapeHtml ||
      (s => {
        const d = document.createElement('div');
        d.textContent = s;
        return d.innerHTML;
      });
    const safeTitle = escapeHtml(title);
    const titleHtml = safeTitle ? `<div class="stats-thumb-title-centered">${safeTitle}</div>` : '';
    const defs = getVideoStatDefinitions(fields);

    // Build individual cards but group likes+dislikes into a single grid cell so
    // they appear side-by-side on one line.
    const viewsDef = defs.find(d => d.label === 'views');
    const likesDef = defs.find(d => d.label === 'likes');
    const dislikesDef = defs.find(d => d.label === 'dislikes');
    const commentsDef = defs.find(d => d.label === 'comments');

    const viewsHtml = viewsDef
      ? makeStatsCard(
          viewsDef.label,
          viewsDef.value,
          viewsDef.exact,
          viewsDef.iconClass,
          viewsDef.iconSvg
        )
      : '';
    const likesHtml = likesDef
      ? makeStatsCard(
          likesDef.label,
          likesDef.value,
          likesDef.exact,
          likesDef.iconClass,
          likesDef.iconSvg
        )
      : '';
    const dislikesHtml = dislikesDef
      ? makeStatsCard(
          dislikesDef.label,
          dislikesDef.value,
          dislikesDef.exact,
          dislikesDef.iconClass,
          dislikesDef.iconSvg
        )
      : '';
    const commentsHtml = commentsDef
      ? makeStatsCard(
          commentsDef.label,
          commentsDef.value,
          commentsDef.exact,
          commentsDef.iconClass,
          commentsDef.iconSvg
        )
      : '';

    const pairHtml =
      likesHtml || dislikesHtml
        ? `<div class="stats-card-pair">${likesHtml}${dislikesHtml}</div>`
        : '';

    // Build centered large author/handle display (placed below stats grid)
    const { author, authorHandle } = fields;
    const safeAuthor = author ? escapeHtml(String(author)) : '';
    const safeHandle = authorHandle ? escapeHtml(String(authorHandle)) : '';
    const authorBigHtml =
      safeHandle || safeAuthor
        ? `<div class="stats-author-big">${
            safeHandle
              ? `<a class="stats-author-handle-big" href="https://www.youtube.com/${encodeURIComponent(
                  authorHandle
                )}" target="_blank" rel="noopener noreferrer">${safeHandle}</a>`
              : `<span class="stats-author-name-big">${safeAuthor}</span>`
          }</div>`
        : '';

    const parts = [viewsHtml, pairHtml, commentsHtml].filter(Boolean);

    const liveViewerCard = createLiveViewerCard(liveViewer);
    if (liveViewerCard) parts.push(liveViewerCard);

    const gridHtml = `<div class="stats-grid">${parts.join('')}</div>`;
    const sideColumnHtml = `<div class="stats-side-column">${gridHtml}${authorBigHtml}</div>`;

    if (thumbUrl) {
      const extras = getVideoExtras(stats, null);
      const metaCardsHtml = buildMetaCardsHtml(stats, extras);
      const metaExtrasHtml = metaCardsHtml
        ? `<div class="stats-thumb-extras" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;">${metaCardsHtml}</div>`
        : '';
      const leftHtml = `<div class="stats-thumb-left"><img class="stats-thumb-img" src="${thumbUrl}" alt="thumbnail">${metaExtrasHtml}</div>`;
      container.innerHTML = `${titleHtml}<div class="stats-thumb-row">${leftHtml}${sideColumnHtml}</div>`;
    } else {
      container.innerHTML = `${titleHtml}${sideColumnHtml}`;
    }

    // Set up error handlers for country flag images
    setupFlagImageErrorHandlers(container);
  }

  /**
   * Setup error handlers for country flag images to prevent XSS
   * @param {HTMLElement} container - Container element
   */
  function setupFlagImageErrorHandlers(container) {
    const flagImages = $$('.country-flag', container);
    const globeIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`;

    flagImages.forEach(img => {
      img.addEventListener(
        'error',
        function () {
          const iconContainer = this.parentElement;
          if (iconContainer && iconContainer.dataset.fallbackIcon === 'globe') {
            this.style.display = 'none';
            iconContainer.innerHTML = globeIcon;
          }
        },
        { once: true }
      );
    });
  }

  /**
   * Display channel statistics
   * @param {HTMLElement} container - Container element
   * @param {Object} stats - Stats data
   */
  function displayChannelStats(container, stats) {
    const { liveSubscriber, liveViews, liveVideos } = stats;

    container.innerHTML = `
      <div class="stats-grid">
        <div class="stats-card">
          <div class="stats-icon stats-icon-subscribers">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
              <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
              <circle cx="9" cy="7" r="4"></circle>
              <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
              <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
            </svg>
          </div>
          <div class="stats-info">
            <div class="stats-label">${t('subscribers')}</div>
            <div class="stats-value">${formatNumber(liveSubscriber)}</div>
            <div class="stats-exact">${(liveSubscriber || 0).toLocaleString()}</div>
          </div>
        </div>

        <div class="stats-card">
          <div class="stats-icon stats-icon-views">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
              <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
              <circle cx="12" cy="12" r="3"></circle>
            </svg>
          </div>
          <div class="stats-info">
            <div class="stats-label">${t('totalViews')}</div>
            <div class="stats-value">${formatNumber(liveViews)}</div>
            <div class="stats-exact">${(liveViews || 0).toLocaleString()}</div>
          </div>
        </div>

        <div class="stats-card">
          <div class="stats-icon stats-icon-videos">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
              <polygon points="23 7 16 12 23 17 23 7"></polygon>
              <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
            </svg>
          </div>
          <div class="stats-info">
            <div class="stats-label">${t('totalVideos')}</div>
            <div class="stats-value">${formatNumber(liveVideos)}</div>
            <div class="stats-exact">${(liveVideos || 0).toLocaleString()}</div>
          </div>
        </div>
      </div>
    `;
  }

  function createStatsMenu() {
    if (!statsButtonEnabled) return undefined;
    if ($('.stats-menu-container')) {
      return undefined;
    }

    const containerDiv = document.createElement('div');
    containerDiv.className = 'yt-flexible-actions-view-model-wiz__action stats-menu-container';

    const mainButtonViewModel = document.createElement('button-view-model');
    mainButtonViewModel.className = 'yt-spec-button-view-model main-stats-view-model';

    const mainButton = document.createElement('button');
    mainButton.className =
      'yt-spec-button-shape-next yt-spec-button-shape-next--outline yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--enable-backdrop-filter-experiment main-stats-button';
    mainButton.setAttribute('aria-disabled', 'false');
    mainButton.setAttribute('aria-label', t('stats'));
    mainButton.style.display = 'flex';
    mainButton.style.alignItems = 'center';
    mainButton.style.justifyContent = 'center';
    mainButton.style.gap = '8px';

    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('viewBox', '0 0 512 512');
    svg.style.width = '20px';
    svg.style.height = '20px';
    svg.style.fill = 'currentColor';

    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute(
      'd',
      'M500 89c13.8-11 16-31.2 5-45s-31.2-16-45-5L319.4 151.5 211.2 70.4c-11.7-8.8-27.8-8.5-39.2 .6L12 199c-13.8 11-16 31.2-5 45s31.2 16 45 5L192.6 136.5l108.2 81.1c11.7 8.8 27.8 8.5 39.2-.6L500 89zM160 256l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32zM32 352l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32s-32 14.3-32 32zm288-64c-17.7 0-32 14.3-32 32l0 128c0 17.7 14.3 32 32 32s32-14.3 32-32l0-128c0-17.7-14.3-32-32-32zm96-32l0 192c0 17.7 14.3 32 32 32s32-14.3 32-32l0-192c0-17.7-14.3-32-32-32s-32 14.3-32 32z'
    );
    svg.appendChild(path);

    const buttonText = document.createElement('div');
    buttonText.className = 'yt-spec-button-shape-next__button-text-content main-stats-text';
    buttonText.textContent = t('stats');
    buttonText.style.display = 'flex';
    buttonText.style.alignItems = 'center';

    const touchFeedback = document.createElement('yt-touch-feedback-shape');
    touchFeedback.style.borderRadius = 'inherit';

    const touchFeedbackDiv = document.createElement('div');
    touchFeedbackDiv.className =
      'yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response';
    touchFeedbackDiv.setAttribute('aria-hidden', 'true');

    const strokeDiv = document.createElement('div');
    strokeDiv.className = 'yt-spec-touch-feedback-shape__stroke';

    const fillDiv = document.createElement('div');
    fillDiv.className = 'yt-spec-touch-feedback-shape__fill';

    touchFeedbackDiv.appendChild(strokeDiv);
    touchFeedbackDiv.appendChild(fillDiv);
    touchFeedback.appendChild(touchFeedbackDiv);

    mainButton.appendChild(svg);
    mainButton.appendChild(buttonText);
    mainButton.appendChild(touchFeedback);
    mainButtonViewModel.appendChild(mainButton);
    containerDiv.appendChild(mainButtonViewModel);

    const horizontalMenu = document.createElement('div');
    horizontalMenu.className = 'stats-horizontal-menu';

    const channelButtonContainer = document.createElement('div');
    channelButtonContainer.className = 'stats-menu-button channel-stats-container';

    const channelButton = createButton(
      t('channel'),
      'M64 48c-8.8 0-16 7.2-16 16l0 288c0 8.8 7.2 16 16 16l512 0c8.8 0 16-7.2 16-16l0-288c0-8.8-7.2-16-16-16L64 48zM0 64C0 28.7 28.7 0 64 0L576 0c35.3 0 64 28.7 64 64l0 288c0 35.3-28.7 64-64 64L64 416c-35.3 0-64-28.7-64-64L0 64zM120 464l400 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-400 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z',
      '0 0 640 512',
      'channel-stats',
      () => {
        const channelId = getChannelIdentifier();
        if (channelId) {
          openStatsModal('channel', channelId);
        }
      }
    );
    channelButtonContainer.appendChild(channelButton);
    horizontalMenu.appendChild(channelButtonContainer);

    if (channelFeatures.hasStreams) {
      const liveButtonContainer = document.createElement('div');
      liveButtonContainer.className = 'stats-menu-button live-stats-container';

      const liveButton = createButton(
        t('live'),
        'M99.8 69.4c10.2 8.4 11.6 23.6 3.2 33.8C68.6 144.7 48 197.9 48 256s20.6 111.3 55 152.8c8.4 10.2 7 25.3-3.2 33.8s-25.3 7-33.8-3.2C24.8 389.6 0 325.7 0 256S24.8 122.4 66 72.6c8.4-10.2 23.6-11.6 33.8-3.2zm376.5 0c10.2-8.4 25.3-7 33.8 3.2c41.2 49.8 66 113.8 66 183.4s-24.8 133.6-66 183.4c-8.4 10.2-23.6 11.6-33.8 3.2s-11.6-23.6-3.2-33.8c34.3-41.5 55-94.7 55-152.8s-20.6-111.3-55-152.8c-8.4-10.2-7-25.3 3.2-33.8zM248 256a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zm-61.1-78.5C170 199.2 160 226.4 160 256s10 56.8 26.9 78.5c8.1 10.5 6.3 25.5-4.2 33.7s-25.5 6.3-33.7-4.2c-23.2-29.8-37-67.3-37-108s13.8-78.2 37-108c8.1-10.5 23.2-12.3 33.7-4.2s12.3 23.2 4.2 33.7zM427 148c23.2 29.8 37 67.3 37 108s-13.8 78.2-37 108c-8.1 10.5-23.2 12.3-33.7 4.2s-12.3-23.2-4.2-33.7C406 312.8 416 285.6 416 256s-10-56.8-26.9-78.5c-8.1-10.5-6.3-25.5 4.2-33.7s25.5-6.3 33.7 4.2z',
        '0 0 576 512',
        'live-stats',
        () => {
          const channelId = getChannelIdentifier();
          if (channelId) {
            openStatsModal('channel', channelId);
          }
        }
      );

      liveButtonContainer.appendChild(liveButton);
      horizontalMenu.appendChild(liveButtonContainer);
    }

    if (channelFeatures.hasShorts) {
      const shortsButtonContainer = document.createElement('div');
      shortsButtonContainer.className = 'stats-menu-button shorts-stats-container';

      const shortsButton = createButton(
        t('shorts'),
        'M80 48c-8.8 0-16 7.2-16 16l0 384c0 8.8 7.2 16 16 16l224 0c8.8 0 16-7.2 16-16l0-384c0-8.8-7.2-16-16-16L80 48zM16 64C16 28.7 44.7 0 80 0L304 0c35.3 0 64 28.7 64 64l0 384c0 35.3-28.7 64-64 64L80 512c-35.3 0-64-28.7-64-64L16 64zM160 400l64 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-64 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z',
        '0 0 384 512',
        'shorts-stats',
        () => {
          const channelId = getChannelIdentifier();
          if (channelId) {
            openStatsModal('channel', channelId);
          }
        }
      );

      shortsButtonContainer.appendChild(shortsButton);
      horizontalMenu.appendChild(shortsButtonContainer);
    }

    containerDiv.appendChild(horizontalMenu);

    const joinButton = $('.yt-flexible-actions-view-model-wiz__action:not(.stats-menu-container)');
    if (joinButton) {
      joinButton.parentNode.appendChild(containerDiv);
    } else {
      const buttonContainer = $('#subscribe-button + #buttons');
      if (buttonContainer) {
        buttonContainer.appendChild(containerDiv);
      }
    }

    return containerDiv;
  }

  function checkAndAddMenu() {
    if (!statsButtonEnabled) return;
    const joinButton = $('.yt-flexible-actions-view-model-wiz__action:not(.stats-menu-container)');
    const statsMenu = $('.stats-menu-container');

    if (joinButton && !statsMenu) {
      createStatsMenu();
    }
  }

  function checkAndInsertIcon() {
    if (!statsButtonEnabled) return;
    // Always ensure universal icon is present in the masthead
    insertUniversalIcon();
  }

  function addSettingsUI() {
    const section = $('.ytp-plus-settings-section[data-section="experimental"]');
    if (!section) return false;

    const existingItem = $('.stats-button-settings-item', section);
    if (existingItem) {
      const label = existingItem.querySelector('.ytp-plus-settings-item-label');
      const description = existingItem.querySelector('.ytp-plus-settings-item-description');
      if (label) label.textContent = t('statisticsButton');
      if (description) description.textContent = t('statisticsButtonDescription');
      return true;
    }

    const item = document.createElement('div');
    item.className = 'ytp-plus-settings-item stats-button-settings-item';
    item.innerHTML = `
        <div>
          <label class="ytp-plus-settings-item-label">${t('statisticsButton')}</label>
          <div class="ytp-plus-settings-item-description">${t('statisticsButtonDescription')}</div>
        </div>
        <input type="checkbox" class="ytp-plus-settings-checkbox" ${statsButtonEnabled ? 'checked' : ''}>
      `;
    section.appendChild(item);

    item.querySelector('input')?.addEventListener('change', e => {
      const { target } = e;
      const input = /** @type {EventTarget & HTMLInputElement} */ (target);
      statsButtonEnabled = input.checked;
      localStorage.setItem(SETTINGS_KEY, statsButtonEnabled ? 'true' : 'false');
      // Remove all stats buttons and menus
      $$('.videoStats,.stats-menu-container').forEach(el => el.remove());
      if (statsButtonEnabled) {
        checkAndInsertIcon();
        checkAndAddMenu();
      }
    });

    return true;
  }

  function ensureSettingsUI(attempt = 0) {
    const attached = addSettingsUI();
    if (attached || attempt >= 20) return;
    setTimeout(() => ensureSettingsUI(attempt + 1), 100);
  }

  // Settings modal integration — use event instead of MutationObserver
  document.addEventListener('youtube-plus-settings-modal-opened', () => {
    ensureSettingsUI();
  });

  const handleExperimentalNavClick = e => {
    const { target } = e;
    const el = /** @type {EventTarget & HTMLElement} */ (target);
    const navItem = el?.closest?.('.ytp-plus-settings-nav-item');
    if (navItem?.dataset?.section === 'experimental') {
      ensureSettingsUI();
    }
  };

  document.addEventListener('youtube-plus-language-changed', () => {
    ensureSettingsUI();
  });

  if (!experimentalNavListenerKey) {
    experimentalNavListenerKey = YouTubeUtils.cleanupManager.registerListener(
      document,
      'click',
      handleExperimentalNavClick,
      true
    );
  }

  function init() {
    addStyles();
    if (statsButtonEnabled) {
      checkAndInsertIcon();
      checkAndAddMenu();
    }

    // Use centralized pushState/replaceState event from utils.js instead of wrapping independently
    window.addEventListener('ytp-history-navigate', checkUrlChange);
    window.addEventListener('popstate', checkUrlChange);

    if (isChannelPage(location.href)) {
      checkChannelTabs(location.href);
    }
  }

  const scheduleInit = () => {
    if (statsInitialized || !isStatsRelevant()) return;
    statsInitialized = true;

    const run = () => {
      try {
        init();
      } catch (e) {
        statsInitialized = false;
        throw e;
      }
    };

    if (typeof requestIdleCallback === 'function') {
      requestIdleCallback(run, { timeout: 2000 });
    } else {
      setTimeout(run, 0);
    }
  };

  runWhenReady(scheduleInit);

  const handleNavigate = () => {
    scheduleInit();
    if (!statsInitialized || !statsButtonEnabled) return;
    checkAndInsertIcon();
    checkAndAddMenu();
    if (isChannelPage(location.href)) {
      checkChannelTabs(location.href);
    }
  };

  if (window.YouTubeUtils?.cleanupManager?.registerListener) {
    YouTubeUtils.cleanupManager.registerListener(document, 'yt-navigate-finish', handleNavigate, {
      passive: true,
    });
  } else {
    window.addEventListener('yt-navigate-finish', handleNavigate);
  }

  const handleAction = event => {
    scheduleInit();
    if (!statsInitialized || !statsButtonEnabled) return;
    const ev = /** @type {CustomEvent<any>} */ (event);
    if (ev.detail && ev.detail.actionName === 'yt-reload-continuation-items-command') {
      checkAndInsertIcon();
      checkAndAddMenu();
    }
  };

  if (window.YouTubeUtils?.cleanupManager?.registerListener) {
    YouTubeUtils.cleanupManager.registerListener(document, 'yt-action', handleAction, {
      passive: true,
    });
  } else {
    document.addEventListener('yt-action', handleAction);
  }
})();

// count
(function () {
  'use strict';

  // Reuse shared helpers (separate IIFE scope requires local aliases)
  const getCache = () => typeof window !== 'undefined' && window.YouTubeDOMCache;
  const $ = (sel, ctx) =>
    getCache()?.querySelector(sel, ctx) || (ctx || document).querySelector(sel);
  const $$ = (sel, ctx) =>
    getCache()?.querySelectorAll(sel, ctx) || Array.from((ctx || document).querySelectorAll(sel));
  const byId = id => getCache()?.getElementById(id) || document.getElementById(id);

  // Do not run this module inside YouTube Studio (studio.youtube.com)
  const isStudioPageCount = () => {
    try {
      const host = location.hostname || '';
      const href = location.href || '';
      return (
        host.includes('studio.youtube.com') ||
        host.includes('studio.') ||
        href.includes('studio.youtube.com')
      );
    } catch {
      return false;
    }
  };

  if (isStudioPageCount()) return;

  // i18n alias
  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    if (!key) return '';
    let r = String(key);
    for (const [k, v] of Object.entries(params || {})) {
      r = r.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
    }
    return r;
  };

  // Enhanced configuration with better defaults
  const CONFIG = {
    OPTIONS: ['subscribers', 'views', 'videos'],
    FONT_LINK: 'https://fonts.googleapis.com/css2?family=Rubik:wght@400;700&display=swap',
    STATS_API_URL: 'https://api.livecounts.io/youtube-live-subscriber-counter/stats/',
    DEFAULT_UPDATE_INTERVAL: 2000,
    DEFAULT_OVERLAY_OPACITY: 0.75,
    MAX_RETRIES: 3,
    CACHE_DURATION: 300000, // 5 minutes
    DEBOUNCE_DELAY: 100,
    STORAGE_KEY: 'youtube_channel_stats_settings',
  };

  // Global state management
  const state = {
    overlay: null,
    isUpdating: false,
    intervalId: null,
    currentChannelName: null,
    currentChannelId: null,
    enabled: localStorage.getItem(CONFIG.STORAGE_KEY) !== 'false',
    updateInterval:
      parseInt(localStorage.getItem('youtubeEnhancerInterval'), 10) ||
      CONFIG.DEFAULT_UPDATE_INTERVAL,
    overlayOpacity:
      parseFloat(localStorage.getItem('youtubeEnhancerOpacity')) || CONFIG.DEFAULT_OVERLAY_OPACITY,
    lastSuccessfulStats: new Map(),
    previousStats: new Map(),
    channelIdCache: new Map(),
    lastChannelIdWarnAt: 0,
    previousUrl: location.href,
    isChecking: false,
    documentListenerKeys: new Set(),
  };

  // Utility functions
  const utils = {
    log: (message, ...args) => {
      window.YouTubeUtils &&
        YouTubeUtils.logger &&
        YouTubeUtils.logger.debug &&
        YouTubeUtils.logger.debug('[YouTube+][Stats]', message, ...args);
    },

    warn: (message, ...args) => {
      console.warn('[YouTube+][Stats]', message, ...args);
    },

    error: (message, ...args) => {
      console.error('[YouTube+][Stats]', message, ...args);
    },

    // Use shared debounce from YouTubeUtils
    debounce:
      window.YouTubeUtils?.debounce ||
      ((func, wait) => {
        let timeout;
        return function executedFunction(...args) {
          const later = () => {
            clearTimeout(timeout);
            func(...args);
          };
          clearTimeout(timeout);
          timeout = setTimeout(later, wait);
        };
      }),
  };

  const { OPTIONS } = CONFIG;
  const { FONT_LINK } = CONFIG;
  const { STATS_API_URL } = CONFIG;

  /**
   * Fetches channel data from YouTube
   * @param {string} url - The channel URL to fetch
   * @returns {Promise<Object|null>} The parsed channel data or null on error
   */
  async function fetchChannel(url) {
    if (state.isChecking) return null;
    state.isChecking = true;

    try {
      const response = await fetch(url, {
        credentials: 'same-origin',
      });

      if (!response.ok) return null;

      const html = await response.text();
      const match = html.match(/var ytInitialData = (.+?);<\/script>/);
      return match && match[1] ? JSON.parse(match[1]) : null;
    } catch (error) {
      utils.warn('Failed to fetch channel data:', error);
      return null;
    } finally {
      state.isChecking = false;
    }
  }

  async function getChannelInfo(url) {
    const data = await fetchChannel(url);
    if (!data) return null;

    try {
      const channelName = data?.metadata?.channelMetadataRenderer?.title || t('unknown');
      const channelId = data?.metadata?.channelMetadataRenderer?.externalId || null;

      return { channelName, channelId };
    } catch {
      return null;
    }
  }

  function isChannelPageUrl(url) {
    return (
      url.includes('youtube.com/') &&
      (url.includes('/channel/') || url.includes('/@')) &&
      !url.includes('/video/') &&
      !url.includes('/watch')
    );
  }

  function checkUrlChange() {
    const currentUrl = location.href;
    if (currentUrl !== state.previousUrl) {
      state.previousUrl = currentUrl;
      if (isChannelPageUrl(currentUrl)) {
        setTimeout(() => getChannelInfo(currentUrl), 500);
      }
    }
  }

  // YouTube SPA navigation — yt-navigate-finish fires for all pushState/replaceState
  // transitions on youtube.com, so wrapping history APIs is redundant and creates
  // an ever-growing wrapper chain when multiple modules do the same thing.
  window.addEventListener('yt-navigate-finish', checkUrlChange, { passive: true });
  window.addEventListener('popstate', checkUrlChange, { passive: true });

  function init() {
    try {
      utils.log('Initializing YouTube Enhancer v1.6');

      loadFonts();
      initializeLocalStorage();
      addStyles();
      if (state.enabled) {
        observePageChanges();
        addNavigationListener();

        if (isChannelPageUrl(location.href)) {
          getChannelInfo(location.href);
        }
      }

      utils.log('YouTube Enhancer initialized successfully');
    } catch (error) {
      utils.error('Failed to initialize YouTube Enhancer:', error);
    }
  }

  function loadFonts() {
    const fontLink = document.createElement('link');
    fontLink.rel = 'stylesheet';
    fontLink.href = FONT_LINK;
    (document.head || document.documentElement).appendChild(fontLink);
  }

  function initializeLocalStorage() {
    OPTIONS.forEach(option => {
      if (localStorage.getItem(`show-${option}`) === null) {
        localStorage.setItem(`show-${option}`, 'true');
      }
    });
  }

  function addStyles() {
    const styles = `
      .channel-banner-overlay{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:12px;z-index:9;display:flex;justify-content:space-around;align-items:center;color:#fff;font-family:var(--stats-font-family,'Rubik',sans-serif);font-size:var(--stats-font-size,24px);box-sizing:border-box;transition:background-color .3s ease;backdrop-filter:blur(2px)}        
      .settings-button{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;cursor:pointer;z-index:11;transition:all .2s ease;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.4);backdrop-filter:blur(4px);border:1px solid rgba(255,255,255,0.1);opacity:0.7}
      .channel-banner-overlay:hover .settings-button{opacity:1}
      .settings-button:hover{transform:rotate(30deg) scale(1.1);opacity:1;background:rgba(0,0,0,0.6);border-color:rgba(255,255,255,0.3)}
      .settings-button svg{width:18px;height:18px;fill:white;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.5))}        
      .settings-menu{position:absolute;top:52px;right:12px;background:rgba(28,28,28,0.75);padding:16px;border-radius:16px;z-index:12;display:flex;flex-direction:column;gap:12px;backdrop-filter:blur(16px) saturate(180%);border:1px solid rgba(255,255,255,0.08);box-shadow:0 8px 32px rgba(0,0,0,0.6);min-width:320px;opacity:0;visibility:hidden;transform:translateY(-10px) scale(0.98);transition:all 0.2s cubic-bezier(0.2,0,0.2,1);pointer-events:none}
      .settings-menu.show{opacity:1;visibility:visible;transform:translateY(0) scale(1);pointer-events:auto}        
      .settings-menu .ytp-plus-settings-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-radius:8px;background:rgba(255,255,255,0.02);}
      .settings-menu .ytp-plus-settings-item + .ytp-plus-settings-item{margin-top:6px}
      .settings-menu .ytp-plus-settings-item .ytp-plus-settings-item-label{color:#eee;font-size:14px;font-weight:500}
      .settings-menu label{color:#eee!important;font-size:14px!important;font-weight:500!important;margin-bottom:6px!important}        
      .settings-menu input[type="range"]{-webkit-appearance:none;width:100%!important;height:4px;background:rgba(255,255,255,0.2)!important;border-radius:2px;margin:12px 0 4px 0!important;cursor:pointer}
      .settings-menu input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;height:16px;width:16px;border-radius:50%;background:#3ea6ff;margin-top:-6px;box-shadow:0 2px 4px rgba(0,0,0,0.3);border:2px solid #fff;transition:transform .1s;cursor:pointer}
      .settings-menu input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}        
      .settings-menu select{width:100%!important;background:rgba(255,255,255,0.1)!important;border:1px solid rgba(255,255,255,0.1)!important;color:#fff!important;padding:8px 12px!important;border-radius:6px!important;font-size:13px!important;margin-bottom:12px!important;cursor:pointer;outline:none}
      .settings-menu select:hover{background:rgba(255,255,255,0.15)!important}
      .settings-menu select option{background:#333;color:#fff}        
      /* Don't override the shared settings checkbox styling; only target non-shared inputs */
      .settings-menu input[type="checkbox"]:not(.ytp-plus-settings-checkbox){appearance:none;width:18px!important;height:18px!important;border:2px solid rgba(255,255,255,0.4)!important;border-radius:4px!important;background:transparent!important;cursor:pointer;position:relative;margin-right:12px!important;vertical-align:middle;transition:all .2s}
      .settings-menu input[type="checkbox"]:not(.ytp-plus-settings-checkbox):checked{background:#3ea6ff!important;border-color:#3ea6ff!important}
      .settings-menu input[type="checkbox"]:not(.ytp-plus-settings-checkbox):checked::after{content:'';position:absolute;left:5px;top:1px;width:4px;height:10px;border:solid white;border-width:0 2px 2px 0;transform:rotate(45deg)}        
      .stat-container{display:flex;flex-direction:column;align-items:center;justify-content:center;visibility:hidden;width:33%;height:100%;padding:0 1rem;text-shadow:0 2px 4px rgba(0,0,0,0.3)}
      .number-container{display:flex;align-items:center;justify-content:center;font-weight:700;min-height:3rem}
      .label-container{display:flex;align-items:center;margin-top:.5rem;font-size:1.2rem;opacity:.9}
      .label-container svg{width:1.5rem;height:1.5rem;margin-right:.5rem;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.3))}
      .difference{font-size:1.8rem;height:2rem;margin-bottom:.5rem;transition:opacity .3s}
      .spinner-container{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center}
      .loading-spinner{animation:spin 1s linear infinite}
      @keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
      @media(max-width:768px){.channel-banner-overlay{flex-direction:column;padding:8px;min-height:160px}.settings-menu{width:280px!important;right:4px!important;top:48px!important}}
      .setting-group{margin-bottom:12px}
      .setting-group:last-child{margin-bottom:0}
      .setting-value{color:#bbb;font-size:12px;margin-top:4px}
      `;
    YouTubeUtils.StyleManager.add('channel-stats-overlay', styles);
  }

  function createSettingsButton() {
    const button = document.createElement('div');
    button.className = 'settings-button';

    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    svg.setAttribute('viewBox', '0 0 512 512');

    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute('fill', 'white');
    path.setAttribute(
      'd',
      'M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z'
    );

    svg.appendChild(path);
    button.appendChild(svg);

    return button;
  }

  function createSettingsMenu() {
    const menu = document.createElement('div');
    menu.className = 'settings-menu';
    menu.style.gap = '15px';
    menu.style.width = '360px';
    menu.setAttribute('tabindex', '-1');
    menu.setAttribute('aria-modal', 'true');

    const displaySection = createDisplaySection();
    const controlsSection = createControlsSection();

    menu.appendChild(displaySection);
    menu.appendChild(controlsSection);

    return menu;
  }

  function createDisplaySection() {
    const displaySection = document.createElement('div');
    displaySection.style.flex = '1';

    const displayLabel = document.createElement('label');
    displayLabel.textContent = t('displayOptions');
    displayLabel.style.marginBottom = '10px';
    displayLabel.style.display = 'block';
    displayLabel.style.fontSize = '16px';
    displayLabel.style.fontWeight = 'bold';
    displaySection.appendChild(displayLabel);

    // Use event delegation for all checkboxes
    displaySection.addEventListener('change', e => {
      const checkbox = e.target;
      if (checkbox.type === 'checkbox' && checkbox.id.startsWith('show-')) {
        const option = checkbox.id.replace('show-', '');
        localStorage.setItem(`show-${option}`, String(checkbox.checked));
        updateDisplayState();
      }
    });
    // Render options as single-line settings items using shared classes
    OPTIONS.forEach(option => {
      const item = document.createElement('div');
      item.className = 'ytp-plus-settings-item';

      const left = document.createElement('div');

      const label = document.createElement('label');
      label.className = 'ytp-plus-settings-item-label';
      label.htmlFor = `show-${option}`;
      label.textContent = t(option);
      left.appendChild(label);

      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.id = `show-${option}`;
      checkbox.checked = localStorage.getItem(`show-${option}`) !== 'false';
      checkbox.className = 'ytp-plus-settings-checkbox';

      item.appendChild(left);
      item.appendChild(checkbox);
      displaySection.appendChild(item);
    });

    return displaySection;
  }

  function createControlsSection() {
    const controlsSection = document.createElement('div');
    controlsSection.style.flex = '1';

    // Use event delegation for all sliders and selects
    controlsSection.addEventListener('input', e => {
      const target = e.target;

      // Handle font size slider
      if (target.classList.contains('font-size-slider')) {
        const input = /** @type {HTMLInputElement} */ (target);
        const fontSizeValue = controlsSection.querySelector('.font-size-value');
        if (fontSizeValue) fontSizeValue.textContent = `${input.value}px`;
        localStorage.setItem('youtubeEnhancerFontSize', input.value);
        if (state.overlay) {
          state.overlay
            .querySelectorAll('.subscribers-number,.views-number,.videos-number')
            .forEach(el => {
              el.style.fontSize = `${input.value}px`;
            });
        }
      }

      // Handle interval slider
      if (target.classList.contains('interval-slider')) {
        const input = /** @type {HTMLInputElement} */ (target);
        const newInterval = parseInt(input.value, 10) * 1000;
        const intervalValue = controlsSection.querySelector('.interval-value');
        if (intervalValue) intervalValue.textContent = `${input.value}s`;
        state.updateInterval = newInterval;
        localStorage.setItem('youtubeEnhancerInterval', String(newInterval));

        if (state.intervalId) {
          clearInterval(state.intervalId);
          state.intervalId = setInterval(() => {
            updateOverlayContent(state.overlay, state.currentChannelName);
          }, newInterval);
          YouTubeUtils.cleanupManager.registerInterval(state.intervalId);
        }
      }

      // Handle opacity slider
      if (target.classList.contains('opacity-slider')) {
        const input = /** @type {HTMLInputElement} */ (target);
        const newOpacity = parseInt(input.value, 10) / 100;
        const opacityValue = controlsSection.querySelector('.opacity-value');
        if (opacityValue) opacityValue.textContent = `${input.value}%`;
        state.overlayOpacity = newOpacity;
        localStorage.setItem('youtubeEnhancerOpacity', String(newOpacity));

        if (state.overlay) {
          state.overlay.style.backgroundColor = `rgba(0, 0, 0, ${newOpacity})`;
        }
      }
    });

    // Font family selector - using glass-dropdown style
    const fontLabel = document.createElement('label');
    fontLabel.textContent = t('fontFamily');
    fontLabel.style.display = 'block';
    fontLabel.style.marginBottom = '5px';
    fontLabel.style.fontSize = '16px';
    fontLabel.style.fontWeight = 'bold';

    const fonts = [
      { name: 'Rubik', value: 'Rubik, sans-serif' },
      { name: 'Impact', value: 'Impact, Charcoal, sans-serif' },
      { name: 'Verdana', value: 'Verdana, Geneva, sans-serif' },
      { name: 'Tahoma', value: 'Tahoma, Geneva, sans-serif' },
    ];
    const savedFont = localStorage.getItem('youtubeEnhancerFontFamily') || 'Rubik, sans-serif';
    const savedFontName = fonts.find(f => f.value === savedFont)?.name || 'Rubik';

    // Hidden native select for compatibility
    const fontSelect = document.createElement('select');
    fontSelect.className = 'font-family-select';
    fontSelect.style.display = 'none';
    fonts.forEach(f => {
      const opt = document.createElement('option');
      opt.value = f.value;
      opt.textContent = f.name;
      if (f.value === savedFont) opt.selected = true;
      fontSelect.appendChild(opt);
    });

    // Glass dropdown
    const fontDropdown = document.createElement('div');
    fontDropdown.className = 'glass-dropdown';
    fontDropdown.id = 'stats-font-dropdown';
    fontDropdown.tabIndex = 0;
    fontDropdown.setAttribute('role', 'listbox');
    fontDropdown.setAttribute('aria-expanded', 'false');
    fontDropdown.style.marginBottom = '12px';
    fontDropdown.innerHTML = `
      <button class="glass-dropdown__toggle" type="button" aria-haspopup="listbox">
        <span class="glass-dropdown__label">${savedFontName}</span>
        <svg class="glass-dropdown__chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
      </button>
      <ul class="glass-dropdown__list" role="presentation">
        ${fonts
          .map(f => {
            const sel = f.value === savedFont ? ' aria-selected="true"' : '';
            return `<li class="glass-dropdown__item" data-value="${f.value}" role="option"${sel}>${f.name}</li>`;
          })
          .join('')}
      </ul>
    `;

    // Initialize glass dropdown interactions
    const initFontDropdown = () => {
      const toggle = fontDropdown.querySelector('.glass-dropdown__toggle');
      const list = fontDropdown.querySelector('.glass-dropdown__list');
      const label = fontDropdown.querySelector('.glass-dropdown__label');
      // eslint-disable-next-line no-unused-vars
      let items = Array.from(fontDropdown.querySelectorAll('.glass-dropdown__item'));

      const closeList = () => {
        fontDropdown.setAttribute('aria-expanded', 'false');
        if (list) list.style.display = 'none';
      };

      const openList = () => {
        fontDropdown.setAttribute('aria-expanded', 'true');
        if (list) list.style.display = 'block';
        items = Array.from(fontDropdown.querySelectorAll('.glass-dropdown__item'));
      };

      closeList();

      if (toggle) {
        toggle.addEventListener('click', e => {
          e.stopPropagation();
          const expanded = fontDropdown.getAttribute('aria-expanded') === 'true';
          if (expanded) closeList();
          else openList();
        });
      }

      document.addEventListener('click', e => {
        if (!fontDropdown.contains(e.target)) closeList();
      });

      if (list) {
        list.addEventListener('click', e => {
          const it = e.target.closest('.glass-dropdown__item');
          if (!it) return;
          const val = it.dataset.value;
          fontDropdown
            .querySelectorAll('.glass-dropdown__item')
            .forEach(i => i.removeAttribute('aria-selected'));
          it.setAttribute('aria-selected', 'true');
          if (label) label.textContent = it.textContent;
          fontSelect.value = val;
          closeList();

          // Apply font change
          localStorage.setItem('youtubeEnhancerFontFamily', val);
          if (state.overlay) {
            state.overlay
              .querySelectorAll('.subscribers-number,.views-number,.videos-number')
              .forEach(el => {
                el.style.fontFamily = val;
              });
          }
        });
      }
    };

    // Delay initialization to ensure DOM is ready
    setTimeout(initFontDropdown, 0);

    // Font size slider
    const fontSizeLabel = document.createElement('label');
    fontSizeLabel.textContent = t('fontSize');
    fontSizeLabel.style.display = 'block';
    fontSizeLabel.style.marginBottom = '5px';
    fontSizeLabel.style.fontSize = '16px';
    fontSizeLabel.style.fontWeight = 'bold';

    const fontSizeSlider = document.createElement('input');
    fontSizeSlider.type = 'range';
    fontSizeSlider.min = '16';
    fontSizeSlider.max = '72';
    fontSizeSlider.value = localStorage.getItem('youtubeEnhancerFontSize') || '24';
    fontSizeSlider.step = '1';
    fontSizeSlider.className = 'font-size-slider';

    const fontSizeValue = document.createElement('div');
    fontSizeValue.className = 'font-size-value';
    fontSizeValue.textContent = `${fontSizeSlider.value}px`;
    fontSizeValue.style.fontSize = '14px';
    fontSizeValue.style.marginBottom = '15px';

    // Update interval slider
    const intervalLabel = document.createElement('label');
    intervalLabel.textContent = t('updateInterval');
    intervalLabel.style.display = 'block';
    intervalLabel.style.marginBottom = '5px';
    intervalLabel.style.fontSize = '16px';
    intervalLabel.style.fontWeight = 'bold';

    const intervalSlider = document.createElement('input');
    intervalSlider.type = 'range';
    intervalSlider.min = '2';
    intervalSlider.max = '10';
    intervalSlider.value = String(state.updateInterval / 1000);
    intervalSlider.step = '1';
    intervalSlider.className = 'interval-slider';

    const intervalValue = document.createElement('div');
    intervalValue.className = 'interval-value';
    intervalValue.textContent = `${intervalSlider.value}s`;
    intervalValue.style.marginBottom = '15px';
    intervalValue.style.fontSize = '14px';

    // Opacity slider
    const opacityLabel = document.createElement('label');
    opacityLabel.textContent = t('backgroundOpacity');
    opacityLabel.style.display = 'block';
    opacityLabel.style.marginBottom = '5px';
    opacityLabel.style.fontSize = '16px';
    opacityLabel.style.fontWeight = 'bold';

    const opacitySlider = document.createElement('input');
    opacitySlider.type = 'range';
    opacitySlider.min = '50';
    opacitySlider.max = '90';
    opacitySlider.value = String(state.overlayOpacity * 100);
    opacitySlider.step = '5';
    opacitySlider.className = 'opacity-slider';

    const opacityValue = document.createElement('div');
    opacityValue.className = 'opacity-value';
    opacityValue.textContent = `${opacitySlider.value}%`;
    opacityValue.style.fontSize = '14px';

    controlsSection.appendChild(fontLabel);
    controlsSection.appendChild(fontSelect);
    controlsSection.appendChild(fontDropdown);
    controlsSection.appendChild(fontSizeLabel);
    controlsSection.appendChild(fontSizeSlider);
    controlsSection.appendChild(fontSizeValue);
    controlsSection.appendChild(intervalLabel);
    controlsSection.appendChild(intervalSlider);
    controlsSection.appendChild(intervalValue);
    controlsSection.appendChild(opacityLabel);
    controlsSection.appendChild(opacitySlider);
    controlsSection.appendChild(opacityValue);

    return controlsSection;
  }

  function createSpinner() {
    const spinnerContainer = document.createElement('div');
    spinnerContainer.style.position = 'absolute';
    spinnerContainer.style.top = '0';
    spinnerContainer.style.left = '0';
    spinnerContainer.style.width = '100%';
    spinnerContainer.style.height = '100%';
    spinnerContainer.style.display = 'flex';
    spinnerContainer.style.justifyContent = 'center';
    spinnerContainer.style.alignItems = 'center';
    spinnerContainer.classList.add('spinner-container');

    const spinner = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    spinner.setAttribute('viewBox', '0 0 512 512');
    spinner.setAttribute('width', '64');
    spinner.setAttribute('height', '64');
    spinner.classList.add('loading-spinner');
    spinner.style.animation = 'spin 1s linear infinite';

    const secondaryPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    secondaryPath.setAttribute(
      'd',
      'M0 256C0 114.9 114.1 .5 255.1 0C237.9 .5 224 14.6 224 32c0 17.7 14.3 32 32 32C150 64 64 150 64 256s86 192 192 192c69.7 0 130.7-37.1 164.5-92.6c-3 6.6-3.3 14.8-1 22.2c1.2 3.7 3 7.2 5.4 10.3c1.2 1.5 2.6 3 4.1 4.3c.8 .7 1.6 1.3 2.4 1.9c.4 .3 .8 .6 1.3 .9s.9 .6 1.3 .8c5 2.9 10.6 4.3 16 4.3c11 0 21.8-5.7 27.7-16c-44.3 76.5-127 128-221.7 128C114.6 512 0 397.4 0 256z'
    );
    secondaryPath.style.opacity = '0.4';
    secondaryPath.style.fill = 'white';

    const primaryPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    primaryPath.setAttribute(
      'd',
      'M224 32c0-17.7 14.3-32 32-32C397.4 0 512 114.6 512 256c0 46.6-12.5 90.4-34.3 128c-8.8 15.3-28.4 20.5-43.7 11.7s-20.5-28.4-11.7-43.7c16.3-28.2 25.7-61 25.7-96c0-106-86-192-192-192c-17.7 0-32-14.3-32-32z'
    );
    primaryPath.style.fill = 'white';

    spinner.appendChild(secondaryPath);
    spinner.appendChild(primaryPath);
    spinnerContainer.appendChild(spinner);
    return spinnerContainer;
  }

  function createSVGIcon(path) {
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('viewBox', '0 0 640 512');
    svg.setAttribute('width', '2rem');
    svg.setAttribute('height', '2rem');
    svg.style.marginRight = '0.5rem';
    svg.style.display = 'none';

    const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    svgPath.setAttribute('d', path);
    svgPath.setAttribute('fill', 'white');

    svg.appendChild(svgPath);
    return svg;
  }

  function createStatContainer(className, iconPath) {
    const container = document.createElement('div');
    Object.assign(container.style, {
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      justifyContent: 'center',
      visibility: 'hidden',
      width: '33%',
      height: '100%',
      padding: '0 1rem',
    });

    const numberContainer = document.createElement('div');
    Object.assign(numberContainer.style, {
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      justifyContent: 'center',
    });

    const differenceElement = document.createElement('div');
    differenceElement.classList.add(`${className}-difference`);
    Object.assign(differenceElement.style, {
      fontSize: '2.5rem',
      height: '2.5rem',
      marginBottom: '1rem',
    });

    const digitContainer = createNumberContainer();
    digitContainer.classList.add(`${className}-number`);
    Object.assign(digitContainer.style, {
      fontSize: `${localStorage.getItem('youtubeEnhancerFontSize') || '24'}px`,
      fontWeight: 'bold',
      lineHeight: '1',
      height: '4rem',
      fontFamily: localStorage.getItem('youtubeEnhancerFontFamily') || 'Rubik, sans-serif',
      letterSpacing: '0.025em',
    });

    numberContainer.appendChild(differenceElement);
    numberContainer.appendChild(digitContainer);

    const labelContainer = document.createElement('div');
    Object.assign(labelContainer.style, {
      display: 'flex',
      alignItems: 'center',
      marginTop: '0.5rem',
    });

    const icon = createSVGIcon(iconPath);
    Object.assign(icon.style, {
      width: '2rem',
      height: '2rem',
      marginRight: '0.75rem',
    });

    const labelElement = document.createElement('div');
    labelElement.classList.add(`${className}-label`);
    labelElement.style.fontSize = '2rem';

    labelContainer.appendChild(icon);
    labelContainer.appendChild(labelElement);

    container.appendChild(numberContainer);
    container.appendChild(labelContainer);

    return container;
  }

  /**
   * Create base overlay element with styling
   * @returns {HTMLElement} Overlay element
   */
  function createOverlayElement() {
    const overlay = document.createElement('div');
    overlay.classList.add('channel-banner-overlay');
    Object.assign(overlay.style, {
      position: 'absolute',
      top: '0',
      left: '0',
      width: '100%',
      height: '100%',
      backgroundColor: `rgba(0, 0, 0, ${state.overlayOpacity})`,
      borderRadius: '15px',
      zIndex: '10',
      display: 'flex',
      justifyContent: 'space-around',
      alignItems: 'center',
      color: 'white',
      fontFamily: localStorage.getItem('youtubeEnhancerFontFamily') || 'Rubik, sans-serif',
      fontSize: `${localStorage.getItem('youtubeEnhancerFontSize') || '24'}px`,
      boxSizing: 'border-box',
      transition: 'background-color 0.3s ease',
    });
    return overlay;
  }

  /**
   * Apply accessibility attributes to overlay
   * @param {HTMLElement} overlay - Overlay element
   */
  function applyOverlayAccessibility(overlay) {
    overlay.setAttribute('role', 'region');
    overlay.setAttribute('aria-label', t('overlayAriaLabel'));
    overlay.setAttribute('tabindex', '-1');
  }

  /**
   * Apply responsive mobile styling
   * @param {HTMLElement} overlay - Overlay element
   */
  function applyMobileResponsiveness(overlay) {
    if (window.innerWidth <= 768) {
      overlay.style.flexDirection = 'column';
      overlay.style.padding = '10px';
      overlay.style.minHeight = '200px';
    }
  }

  /**
   * Setup settings button with accessibility
   * @returns {HTMLElement} Settings button
   */
  function setupSettingsButton() {
    const button = createSettingsButton();
    button.setAttribute('tabindex', '0');
    button.setAttribute('aria-label', t('settingsAriaLabel'));
    button.setAttribute('role', 'button');
    return button;
  }

  /**
   * Setup settings menu with accessibility
   * @returns {HTMLElement} Settings menu
   */
  function setupSettingsMenu() {
    const menu = createSettingsMenu();
    menu.setAttribute('aria-label', t('settingsMenuAriaLabel'));
    menu.setAttribute('role', 'dialog');
    return menu;
  }

  /**
   * Attach menu toggle event handlers
   * @param {HTMLElement} settingsButton - Settings button
   * @param {HTMLElement} settingsMenu - Settings menu
   */
  function attachMenuEventHandlers(settingsButton, settingsMenu) {
    const toggleMenu = show => {
      settingsMenu.classList.toggle('show', show);
      settingsButton.setAttribute('aria-expanded', show);
      if (show) settingsMenu.focus();
    };

    settingsButton.addEventListener('click', e => {
      e.stopPropagation();
      toggleMenu(!settingsMenu.classList.contains('show'));
    });

    settingsButton.addEventListener('keydown', e => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        toggleMenu(!settingsMenu.classList.contains('show'));
      }
    });

    // Register document-level event handlers
    const clickHandler = e => {
      const node = /** @type {EventTarget & Node} */ (e.target);
      if (!settingsMenu.contains(node) && !settingsButton.contains(node)) {
        toggleMenu(false);
      }
    };

    const keyHandler = e => {
      if (e.key === 'Escape' && settingsMenu.classList.contains('show')) {
        toggleMenu(false);
        settingsButton.focus();
      }
    };

    const clickKey = YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler);
    const keyKey = YouTubeUtils.cleanupManager.registerListener(document, 'keydown', keyHandler);
    state.documentListenerKeys.add(clickKey);
    state.documentListenerKeys.add(keyKey);
  }

  /**
   * Add stat containers to overlay
   * @param {HTMLElement} overlay - Overlay element
   */
  function addStatContainers(overlay) {
    const subscribersElement = createStatContainer(
      'subscribers',
      'M144 160c-44.2 0-80-35.8-80-80S99.8 0 144 0s80 35.8 80 80s-35.8 80-80 80zm368 0c-44.2 0-80-35.8-80-80s35.8-80 80-80s80 35.8 80 80s-35.8 80-80 80zM0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0H21.3C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3H405.3zM416 224c0 53-43 96-96 96s-96-43-96-96s43-96 96-96s96 43 96 96zM128 485.3C128 411.7 187.7 352 261.3 352H378.7C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7H154.7c-14.7 0-26.7-11.9-26.7-26.7z'
    );
    const viewsElement = createStatContainer(
      'views',
      'M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z'
    );
    const videosElement = createStatContainer(
      'videos',
      'M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128zM559.1 99.8c10.4 5.6 16.9 16.4 16.9 28.2V384c0 11.8-6.5 22.6-16.9 28.2s-23 5-32.9-1.6l-96-64L416 337.1V320 192 174.9l14.2-9.5 96-64c9.8-6.5 22.4-7.2 32.9-1.6z'
    );

    overlay.appendChild(subscribersElement);
    overlay.appendChild(viewsElement);
    overlay.appendChild(videosElement);
  }

  function createOverlay(bannerElement) {
    clearExistingOverlay();
    if (!bannerElement) return null;

    const overlay = createOverlayElement();
    applyOverlayAccessibility(overlay);
    applyMobileResponsiveness(overlay);

    const settingsButton = setupSettingsButton();
    const settingsMenu = setupSettingsMenu();

    overlay.appendChild(settingsButton);
    overlay.appendChild(settingsMenu);

    attachMenuEventHandlers(settingsButton, settingsMenu);

    const spinner = createSpinner();
    overlay.appendChild(spinner);

    addStatContainers(overlay);

    bannerElement.appendChild(overlay);
    updateDisplayState();
    return overlay;
  }

  function fetchWithGM(url, headers = {}) {
    const requestHeaders = {
      Accept: 'application/json',
      ...headers,
    };
    // Access GM_xmlhttpRequest via window to avoid TS "Cannot find name" when d.ts isn't picked up
    const gm = /** @type {any} */ (window).GM_xmlhttpRequest;
    if (typeof gm === 'function') {
      return new Promise((resolve, reject) => {
        gm({
          method: 'GET',
          url,
          headers: requestHeaders,
          timeout: 10000,
          onload: response => {
            if (response.status >= 200 && response.status < 300) {
              try {
                resolve(JSON.parse(response.responseText));
              } catch (parseError) {
                reject(new Error(`Failed to parse response: ${parseError.message}`));
              }
            } else {
              reject(new Error(`Failed to fetch: ${response.status}`));
            }
          },
          onerror: error => reject(error),
          ontimeout: () => reject(new Error('Request timed out')),
        });
      });
    }

    utils.warn('GM_xmlhttpRequest unavailable, falling back to fetch API');
    return fetch(url, {
      method: 'GET',
      headers: requestHeaders,
      credentials: 'omit',
      mode: 'cors',
    })
      .then(response => {
        if (!response.ok) {
          throw new Error(`Failed to fetch: ${response.status}`);
        }
        return response.json();
      })
      .catch(error => {
        utils.error('Fallback fetch failed:', error);
        throw error;
      });
  }

  async function fetchChannelId(channelName) {
    const cacheKey = channelName || state.currentChannelName || window.location.pathname;

    if (cacheKey && state.channelIdCache.has(cacheKey)) {
      return state.channelIdCache.get(cacheKey);
    }

    if (state.currentChannelId) {
      return state.currentChannelId;
    }

    if (typeof channelName === 'string' && /^UC[\w-]{22}$/.test(channelName)) {
      state.currentChannelId = channelName;
      if (cacheKey) state.channelIdCache.set(cacheKey, channelName);
      return channelName;
    }

    // Try meta tag first
    const metaTag = $('meta[itemprop="channelId"]');
    if (metaTag && metaTag.content) {
      state.currentChannelId = metaTag.content;
      if (cacheKey) state.channelIdCache.set(cacheKey, metaTag.content);
      return metaTag.content;
    }

    // Try URL pattern
    const urlMatch = window.location.href.match(/channel\/(UC[\w-]+)/);
    if (urlMatch && urlMatch[1]) {
      state.currentChannelId = urlMatch[1];
      if (cacheKey) state.channelIdCache.set(cacheKey, urlMatch[1]);
      return urlMatch[1];
    }

    // Try ytInitialData
    const channelInfo = await getChannelInfo(window.location.href);
    if (channelInfo && channelInfo.channelId) {
      state.currentChannelId = channelInfo.channelId;
      if (cacheKey) state.channelIdCache.set(cacheKey, channelInfo.channelId);
      return channelInfo.channelId;
    }

    return null;
  }

  /**
   * Fetch channel statistics with retry logic and fallback
   * Refactored to use channel-stats-helpers module
   * @param {string} channelId - Channel ID
   * @returns {Promise<Object>} Channel stats
   */
  async function fetchChannelStats(channelId) {
    const helpers =
      typeof window !== 'undefined' && window.YouTubePlusChannelStatsHelpers
        ? window.YouTubePlusChannelStatsHelpers
        : null;

    if (!helpers) {
      utils.error('Channel stats helpers not loaded');
      return {
        followerCount: 0,
        bottomOdos: [0, 0],
        error: true,
        timestamp: Date.now(),
      };
    }

    try {
      // Attempt to fetch with retry logic
      const fetchFn = () =>
        fetchWithGM(`${STATS_API_URL}${channelId}`, {
          origin: 'https://livecounts.io',
          referer: 'https://livecounts.io/',
        });

      const stats = await helpers.fetchWithRetry(fetchFn, CONFIG.MAX_RETRIES, utils);

      // If fetch succeeded, cache and return
      if (stats) {
        helpers.cacheStats(state.lastSuccessfulStats, channelId, stats);
        return stats;
      }

      // Try to use cached data if fetch failed
      const cachedStats = helpers.getCachedStats(
        state.lastSuccessfulStats,
        channelId,
        CONFIG.CACHE_DURATION,
        utils
      );

      if (cachedStats) {
        return cachedStats;
      }

      // Fallback: try to extract subscriber count from page
      const fallbackCount = helpers.extractSubscriberCountFromPage();
      if (fallbackCount > 0) {
        utils.log('Extracted fallback subscriber count:', fallbackCount);
      }

      return helpers.createFallbackStats(fallbackCount);
    } catch (error) {
      utils.error('Failed to fetch channel stats:', error);
      return helpers.createFallbackStats(0);
    }
  }

  function clearExistingOverlay() {
    const existingOverlay = $('.channel-banner-overlay');
    if (existingOverlay) {
      try {
        existingOverlay.remove();
      } catch {
        console.warn('[YouTube+] Failed to remove overlay');
      }
    }
    if (state.intervalId) {
      try {
        clearInterval(state.intervalId);
        YouTubeUtils.cleanupManager.unregisterInterval(state.intervalId);
      } catch {
        console.warn('[YouTube+] Failed to clear interval');
      }
      state.intervalId = null;
    }
    if (state.documentListenerKeys && state.documentListenerKeys.size) {
      state.documentListenerKeys.forEach(key => {
        try {
          YouTubeUtils.cleanupManager.unregisterListener(key);
        } catch {
          console.warn('[YouTube+] Failed to unregister listener');
        }
      });
      state.documentListenerKeys.clear();
    }
    if (state.lastSuccessfulStats) state.lastSuccessfulStats.clear();
    if (state.previousStats) state.previousStats.clear();
    state.currentChannelId = null;
    state.isUpdating = false;
    state.overlay = null;
    utils.log('Cleared existing overlay');
  }

  function createDigitElement() {
    const digit = document.createElement('span');
    Object.assign(digit.style, {
      display: 'inline-block',
      width: '0.6em',
      textAlign: 'center',
      marginRight: '0.025em',
      marginLeft: '0.025em',
    });
    return digit;
  }

  function createCommaElement() {
    const comma = document.createElement('span');
    comma.textContent = ',';
    Object.assign(comma.style, {
      display: 'inline-block',
      width: '0.3em',
      textAlign: 'center',
    });
    return comma;
  }

  function createNumberContainer() {
    const container = document.createElement('div');
    Object.assign(container.style, {
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      letterSpacing: '0.025em',
    });
    return container;
  }

  /**
   * Split number into groups of 3 digits for formatting
   * @param {string} valueStr - Number as string
   * @returns {string[]} Array of digit groups
   */
  function splitIntoDigitGroups(valueStr) {
    const digits = [];
    for (let i = valueStr.length - 1; i >= 0; i -= 3) {
      const start = Math.max(0, i - 2);
      digits.unshift(valueStr.slice(start, i + 1));
    }
    return digits;
  }

  /**
   * Clear all children from container
   * @param {HTMLElement} container - Container element
   */
  function clearContainer(container) {
    while (container.firstChild) {
      container.removeChild(container.firstChild);
    }
  }

  /**
   * Render digit groups in container
   * @param {HTMLElement} container - Container element
   * @param {string[]} digitGroups - Array of digit groups
   */
  function renderDigitGroups(container, digitGroups) {
    for (let i = 0; i < digitGroups.length; i++) {
      const group = digitGroups[i];
      for (let j = 0; j < group.length; j++) {
        const digitElement = createDigitElement();
        digitElement.textContent = group[j];
        container.appendChild(digitElement);
      }
      if (i < digitGroups.length - 1) {
        container.appendChild(createCommaElement());
      }
    }
  }

  /**
   * Animate digit changes in container
   * @param {HTMLElement} container - Container element
   * @param {string[]} digitGroups - Array of digit groups
   */
  function animateDigitChanges(container, digitGroups) {
    let elementIndex = 0;
    for (let i = 0; i < digitGroups.length; i++) {
      const group = digitGroups[i];
      for (let j = 0; j < group.length; j++) {
        const digitElement = container.children[elementIndex];
        const newDigit = parseInt(group[j], 10);
        const currentDigit = parseInt(digitElement.textContent || '0', 10);

        if (currentDigit !== newDigit) {
          animateDigit(digitElement, currentDigit, newDigit);
        }
        elementIndex++;
      }
      if (i < digitGroups.length - 1) {
        elementIndex++; // Skip comma
      }
    }
  }

  function updateDigits(container, newValue) {
    const newValueStr = newValue.toString();
    const digitGroups = splitIntoDigitGroups(newValueStr);

    clearContainer(container);
    renderDigitGroups(container, digitGroups);
    animateDigitChanges(container, digitGroups);
  }

  function animateDigit(element, start, end) {
    const duration = 1000;
    const startTime = performance.now();

    function update(currentTime) {
      const elapsed = currentTime - startTime;
      const progress = Math.min(elapsed / duration, 1);
      const easeOutQuart = 1 - Math.pow(1 - progress, 4);
      const current = Math.round(start + (end - start) * easeOutQuart);
      element.textContent = current;

      if (progress < 1) {
        requestAnimationFrame(update);
      }
    }

    requestAnimationFrame(update);
  }

  function showContent(overlay) {
    const spinnerContainer = overlay.querySelector('.spinner-container');
    if (spinnerContainer) {
      spinnerContainer.remove();
    }

    const containers = overlay.querySelectorAll('div[style*="visibility: hidden"]');
    containers.forEach(container => {
      container.style.visibility = 'visible';
    });

    const icons = overlay.querySelectorAll('svg[style*="display: none"]');
    icons.forEach(icon => {
      icon.style.display = 'block';
    });
  }

  function updateDifferenceElement(element, currentValue, previousValue) {
    if (!previousValue) return;

    const difference = currentValue - previousValue;
    if (difference === 0) {
      element.textContent = '';
      return;
    }

    const sign = difference > 0 ? '+' : '';
    element.textContent = `${sign}${difference.toLocaleString()}`;
    element.style.color = difference > 0 ? '#1ed760' : '#f3727f';

    setTimeout(() => {
      element.textContent = '';
    }, 1000);
  }

  function updateDisplayState() {
    const overlay = $('.channel-banner-overlay');
    if (!overlay) return;

    const statContainers = overlay.querySelectorAll('div[style*="width"]');
    if (!statContainers.length) return;

    let visibleCount = 0;
    const visibleContainers = [];

    statContainers.forEach(container => {
      const numberContainer = container.querySelector('[class$="-number"]');
      if (!numberContainer) return;

      const type = numberContainer.className.replace('-number', '');

      const isVisible = localStorage.getItem(`show-${type}`) !== 'false';

      if (isVisible) {
        container.style.display = 'flex';
        visibleCount++;
        visibleContainers.push(container);
      } else {
        container.style.display = 'none';
      }
    });

    visibleContainers.forEach(container => {
      container.style.width = '';
      container.style.margin = '';

      switch (visibleCount) {
        case 1:
          container.style.width = '100%';
          break;
        case 2:
          container.style.width = '50%';
          break;
        case 3:
          container.style.width = '33.33%';
          break;
        default:
          container.style.display = 'none';
      }
    });

    // Only update font size and font family for .subscribers-number, .views-number, .videos-number
    const fontSize = localStorage.getItem('youtubeEnhancerFontSize') || '24';
    const fontFamily = localStorage.getItem('youtubeEnhancerFontFamily') || 'Rubik, sans-serif';
    overlay.querySelectorAll('.subscribers-number,.views-number,.videos-number').forEach(el => {
      el.style.fontSize = `${fontSize}px`;
      el.style.fontFamily = fontFamily;
    });

    overlay.style.display = 'flex';
  }

  /**
   * Check if overlay update should proceed
   * @param {string} channelName - Channel name to update
   * @returns {boolean} True if should proceed
   */
  function shouldUpdateOverlay(channelName) {
    return !state.isUpdating && channelName === state.currentChannelName;
  }

  /**
   * Handle stats error by showing fallback values
   * @param {HTMLElement} overlay - Overlay element
   * @param {Object} stats - Stats object with error
   * @returns {void}
   */
  function handleStatsError(overlay, stats) {
    const containers = overlay.querySelectorAll('[class$="-number"]');
    containers.forEach(container => {
      if (container.classList.contains('subscribers-number') && stats.followerCount > 0) {
        updateDigits(container, stats.followerCount);
      } else {
        container.textContent = '---';
      }
    });
    utils.warn('Using fallback stats due to API error');
  }

  /**
   * Get previous stat value for comparison
   * @param {string} channelId - Channel ID
   * @param {string} className - Stat type class name
   * @returns {number|null} Previous value or null
   */
  function getPreviousStatValue(channelId, className) {
    const prevStats = state.previousStats.get(channelId);
    if (!prevStats) return null;

    if (className === 'subscribers') {
      return prevStats.followerCount;
    }
    const index = className === 'views' ? 0 : 1;
    return prevStats.bottomOdos[index];
  }

  /**
   * Update single stat element in overlay
   * @param {HTMLElement} overlay - Overlay element
   * @param {string} channelId - Channel ID
   * @param {string} className - Stat class name
   * @param {number} value - Stat value
   * @param {string} label - Stat label
   * @returns {void}
   */
  function updateStatElement(overlay, channelId, className, value, label) {
    const numberContainer = overlay.querySelector(`.${className}-number`);
    const differenceElement = overlay.querySelector(`.${className}-difference`);
    const labelElement = overlay.querySelector(`.${className}-label`);

    if (numberContainer) {
      updateDigits(numberContainer, value);
    }

    if (differenceElement && state.previousStats.has(channelId)) {
      const previousValue = getPreviousStatValue(channelId, className);
      if (previousValue !== null) {
        updateDifferenceElement(differenceElement, value, previousValue);
      }
    }

    if (labelElement) {
      labelElement.textContent = label;
    }
  }

  /**
   * Update all stat elements in overlay
   * @param {HTMLElement} overlay - Overlay element
   * @param {string} channelId - Channel ID
   * @param {Object} stats - Stats object
   * @returns {void}
   */
  function updateAllStatElements(overlay, channelId, stats) {
    updateStatElement(overlay, channelId, 'subscribers', stats.followerCount, t('subscribers'));
    updateStatElement(overlay, channelId, 'views', stats.bottomOdos[0], t('views'));
    updateStatElement(overlay, channelId, 'videos', stats.bottomOdos[1], t('videos'));
  }

  /**
   * Show error state in overlay
   * @param {HTMLElement} overlay - Overlay element
   * @returns {void}
   */
  function showOverlayError(overlay) {
    const containers = overlay.querySelectorAll('[class$="-number"]');
    containers.forEach(container => {
      container.textContent = '---';
    });
  }

  /**
   * Update overlay content with channel stats
   * @param {HTMLElement} overlay - Overlay element
   * @param {string} channelName - Channel name
   * @returns {Promise<void>}
   */
  async function updateOverlayContent(overlay, channelName) {
    if (!shouldUpdateOverlay(channelName)) return;
    if (!overlay || !overlay.isConnected) return;
    if (document.visibilityState === 'hidden') return;
    state.isUpdating = true;

    try {
      const channelId = await fetchChannelId(channelName);
      if (!channelId) {
        const now = Date.now();
        if (now - state.lastChannelIdWarnAt > 15000) {
          state.lastChannelIdWarnAt = now;
          utils.warn('Skipping overlay update: channel ID is not available yet');
        }
        return;
      }
      state.currentChannelId = channelId;
      const stats = await fetchChannelStats(channelId);

      // Check if channel changed during async operations
      if (channelName !== state.currentChannelName) {
        return;
      }

      if (stats.error) {
        handleStatsError(overlay, stats);
        return;
      }

      updateAllStatElements(overlay, channelId, stats);

      if (!state.previousStats.has(channelId)) {
        showContent(overlay);
        utils.log('Displayed initial stats for channel:', channelName);
      }

      state.previousStats.set(channelId, stats);
    } catch (error) {
      utils.error('Failed to update overlay content:', error);
      showOverlayError(overlay);
    } finally {
      state.isUpdating = false;
    }
  }

  // Add settings UI to experimental section
  function addSettingsUI() {
    const section = $('.ytp-plus-settings-section[data-section="experimental"]');
    if (!section) return false;

    const existingItem = section.querySelector('.count-settings-item');
    if (existingItem) {
      const label = existingItem.querySelector('.ytp-plus-settings-item-label');
      const description = existingItem.querySelector('.ytp-plus-settings-item-description');
      if (label) label.textContent = t('channelStatsTitle');
      if (description) description.textContent = t('channelStatsDescription');
      return true;
    }

    const item = document.createElement('div');
    item.className = 'ytp-plus-settings-item count-settings-item';
    item.innerHTML = `
        <div>
          <label class="ytp-plus-settings-item-label">${t('channelStatsTitle')}</label>
          <div class="ytp-plus-settings-item-description">${t('channelStatsDescription')}</div>
        </div>
        <input type="checkbox" class="ytp-plus-settings-checkbox" ${state.enabled ? 'checked' : ''}>
      `;
    section.appendChild(item);

    item.querySelector('input')?.addEventListener('change', e => {
      const { target } = e;
      const input = /** @type {EventTarget & HTMLInputElement} */ (target);
      state.enabled = input.checked;
      localStorage.setItem(CONFIG.STORAGE_KEY, state.enabled ? 'true' : 'false');
      if (state.enabled) {
        observePageChanges();
        addNavigationListener();
        setTimeout(() => {
          const bannerElement = byId('page-header-banner-sizer');
          if (bannerElement && isChannelPage()) {
            addOverlay(bannerElement);
          }
        }, 100);
      } else {
        clearExistingOverlay();
      }
    });

    return true;
  }

  function ensureSettingsUI(attempt = 0) {
    const attached = addSettingsUI();
    if (attached || attempt >= 20) return;
    setTimeout(() => ensureSettingsUI(attempt + 1), 100);
  }

  // Settings modal integration — use event instead of MutationObserver
  document.addEventListener('youtube-plus-settings-modal-opened', () => {
    ensureSettingsUI();
  });

  const experimentalNavClickHandler = e => {
    const { target } = e;
    const el = /** @type {EventTarget & HTMLElement} */ (target);
    const navItem = el?.closest?.('.ytp-plus-settings-nav-item');
    if (navItem?.dataset?.section === 'experimental') {
      ensureSettingsUI();
    }
  };

  document.addEventListener('youtube-plus-language-changed', () => {
    ensureSettingsUI();
  });

  const listenerKey = YouTubeUtils.cleanupManager.registerListener(
    document,
    'click',
    experimentalNavClickHandler,
    true
  );
  state.documentListenerKeys.add(listenerKey);

  /**
   * Extract channel name from URL pathname
   * @param {string} pathname - URL pathname
   * @returns {string|null} Channel name or null
   */
  function extractChannelName(pathname) {
    if (pathname.startsWith('/@')) {
      return pathname.split('/')[1].replace('@', '');
    }
    if (pathname.startsWith('/channel/')) {
      return pathname.split('/')[2];
    }
    if (pathname.startsWith('/c/')) {
      return pathname.split('/')[2];
    }
    if (pathname.startsWith('/user/')) {
      return pathname.split('/')[2];
    }
    return null;
  }

  /**
   * Check if overlay should be skipped
   * @param {string|null} channelName - Channel name
   * @returns {boolean} True if should skip
   */
  function shouldSkipOverlay(channelName) {
    return !channelName || (channelName === state.currentChannelName && state.overlay);
  }

  /**
   * Ensure banner element has proper positioning
   * @param {HTMLElement} bannerElement - Banner element
   */
  function ensureBannerPosition(bannerElement) {
    if (bannerElement && !bannerElement.style.position) {
      bannerElement.style.position = 'relative';
    }
  }

  /**
   * Clear existing update interval
   */
  function clearUpdateInterval() {
    if (state.intervalId) {
      clearInterval(state.intervalId);
      state.intervalId = null;
    }
  }

  /**
   * Create debounced update function
   * @param {HTMLElement} overlay - Overlay element
   * @param {string} channelName - Channel name
   * @returns {Function} Debounced update function
   */
  function createDebouncedUpdate(overlay, channelName) {
    let lastUpdateTime = 0;
    return () => {
      if (!overlay || !overlay.isConnected) return;
      if (document.visibilityState === 'hidden') return;
      const now = Date.now();
      if (now - lastUpdateTime >= state.updateInterval - 100) {
        updateOverlayContent(overlay, channelName);
        lastUpdateTime = now;
      }
    };
  }

  /**
   * Set up overlay update interval
   * @param {HTMLElement} overlay - Overlay element
   * @param {string} channelName - Channel name
   */
  function setupUpdateInterval(overlay, channelName) {
    const debouncedUpdate = createDebouncedUpdate(overlay, channelName);
    state.intervalId = setInterval(debouncedUpdate, state.updateInterval);
    YouTubeUtils.cleanupManager.registerInterval(state.intervalId);
  }

  /**
   * Add overlay to channel page banner
   * @param {HTMLElement} bannerElement - Banner element
   */
  function addOverlay(bannerElement) {
    const channelName = extractChannelName(window.location.pathname);

    if (shouldSkipOverlay(channelName)) {
      return;
    }

    ensureBannerPosition(bannerElement);

    state.currentChannelName = channelName;
    state.overlay = createOverlay(bannerElement);

    if (state.overlay) {
      clearUpdateInterval();
      setupUpdateInterval(state.overlay, channelName);
      updateOverlayContent(state.overlay, channelName);
      utils.log('Added overlay for channel:', channelName);
    }
  }

  function isChannelPage() {
    return (
      window.location.pathname.startsWith('/@') ||
      window.location.pathname.startsWith('/channel/') ||
      window.location.pathname.startsWith('/c/')
    );
  }

  /**
   * Find banner element with fallback selectors
   * @returns {HTMLElement|null} Banner element
   */
  function findBannerElement() {
    let bannerElement = byId('page-header-banner-sizer');

    if (!bannerElement) {
      const alternatives = [
        '[id*="banner"]',
        '.ytd-c4-tabbed-header-renderer',
        '#channel-header',
        '.channel-header',
      ];

      for (const selector of alternatives) {
        bannerElement = $(selector);
        if (bannerElement) break;
      }
    }

    return bannerElement;
  }

  /**
   * Ensure banner has proper positioning
   * @param {HTMLElement} bannerElement - Banner element
   * @returns {void}
   */
  function ensureBannerPositioning(bannerElement) {
    if (bannerElement.style.position !== 'relative') {
      bannerElement.style.position = 'relative';
    }
  }

  /**
   * Handle page update for banner overlay
   * @returns {void}
   */
  function handleBannerUpdate() {
    const bannerElement = findBannerElement();

    if (bannerElement && isChannelPage()) {
      ensureBannerPositioning(bannerElement);
      addOverlay(bannerElement);
    } else if (!isChannelPage()) {
      clearExistingOverlay();
      state.currentChannelName = null;
    }
  }

  /**
   * Cleanup observer timeout
   * @param {MutationObserver} observer - Observer instance
   * @returns {void}
   */
  function clearObserverTimeout(observer) {
    if (/** @type {any} */ (observer)._timeout) {
      YouTubeUtils.cleanupManager.unregisterTimeout(/** @type {any} */ (observer)._timeout);
      clearTimeout(/** @type {any} */ (observer)._timeout);
    }
  }

  /**
   * Setup observer for monitoring page changes
   * @param {MutationObserver} observer - Observer instance
   * @returns {void}
   */
  function setupObserver(observer) {
    const observerConfig = {
      childList: true,
      subtree: true,
      attributes: false,
    };

    // Scope to #page-manager or #content instead of full document.body
    const startObserver = () => {
      const target =
        document.querySelector('#page-manager') ||
        document.querySelector('#content') ||
        document.body;
      observer.observe(target, observerConfig);
    };

    if (document.body) {
      startObserver();
    } else {
      document.addEventListener('DOMContentLoaded', startObserver);
    }
  }

  /**
   * Observe page changes and update banner overlay
   * @returns {MutationObserver|undefined} Observer instance
   */
  function observePageChanges() {
    if (!state.enabled) return undefined;

    const observer = new MutationObserver(_mutations => {
      clearObserverTimeout(observer);

      /** @type {any} */ (observer)._timeout = YouTubeUtils.cleanupManager.registerTimeout(
        setTimeout(handleBannerUpdate, 100)
      );
    });

    setupObserver(observer);

    // Store timeout reference for cleanup
    /** @type {any} */ (observer)._timeout = null;

    // Store observer for cleanup on page unload
    if (typeof state.observers === 'undefined') {
      state.observers = [];
    }
    state.observers.push(observer);

    return observer;
  }

  function addNavigationListener() {
    if (!state.enabled) return;

    window.addEventListener('yt-navigate-finish', () => {
      if (isChannelPage()) {
        const bannerElement = byId('page-header-banner-sizer');
        if (bannerElement) {
          addOverlay(bannerElement);
          utils.log('Navigated to channel page');
        }
      } else {
        clearExistingOverlay();
        state.currentChannelName = null;
        utils.log('Navigated away from channel page');
      }
    });
  }

  // Cleanup function for page unload
  function cleanup() {
    // Disconnect all observers
    if (state.observers && Array.isArray(state.observers)) {
      state.observers.forEach(observer => {
        try {
          observer.disconnect();
        } catch (e) {
          console.warn('[YouTube+] Failed to disconnect observer:', e);
        }
      });
      state.observers = [];
    }

    // Clear overlay and intervals
    clearExistingOverlay();

    utils.log('Cleanup completed');
  }

  // Register cleanup on page unload
  window.addEventListener('beforeunload', cleanup);

  // Export module to global scope for module loader
  if (typeof window !== 'undefined') {
    window.YouTubeStats = {
      init,
      cleanup,
      version: '2.4.4',
    };
  }

  init();
})();

// --- MODULE: comment.js ---

/**
 * Comment Manager Module
 * Provides bulk delete functionality and comment management tools for YouTube
 * @module CommentManager
 */
(function () {
  'use strict';

  /**
   * Translation helper - uses centralized i18n system
   * @param {string} key - Translation key
   * @param {Object} params - Interpolation parameters
   * @returns {string} Translated string
   */
  function t(key, params = {}) {
    try {
      if (typeof window !== 'undefined') {
        if (window.YouTubePlusI18n && typeof window.YouTubePlusI18n.t === 'function') {
          return window.YouTubePlusI18n.t(key, params);
        }
        if (window.YouTubeUtils && typeof window.YouTubeUtils.t === 'function') {
          return window.YouTubeUtils.t(key, params);
        }
      }
    } catch {
      // Fallback to key if central i18n unavailable
    }
    return key;
  }

  /**
   * Configuration object for comment manager
   * @const {Object}
   */
  const CONFIG = {
    selectors: {
      deleteButtons:
        'div[class^="VfPpkd-Bz112c-"], button[aria-label*="Delete"], button[aria-label*="Удалить"], button[aria-label*="Remove"]',
      menuButton: '[aria-haspopup="menu"]',
    },
    classes: {
      checkbox: 'comment-checkbox',
      checkboxAnchor: 'comment-checkbox-anchor',
      checkboxFloating: 'comment-checkbox-floating',
      container: 'comment-controls-container',
      panel: 'comment-controls-panel',
      header: 'comment-controls-header',
      title: 'comment-controls-title',
      actions: 'comment-controls-actions',
      button: 'comment-controls-button',
      buttonDanger: 'comment-controls-button--danger',
      buttonPrimary: 'comment-controls-button--primary',
      buttonSuccess: 'comment-controls-button--success',
      close: 'comment-controls-close',
      deleteButton: 'comment-controls-button-delete',
    },
    debounceDelay: 100,
    deleteDelay: 200,
    enabled: true,
    storageKey: 'youtube_comment_manager_settings',
  };

  // State management
  const state = {
    observer: null,
    isProcessing: false,
    settingsNavListenerKey: null,
    panelCollapsed: false,
    initialized: false,
  };

  const COMMENT_HISTORY_URL = (() => {
    let lang = 'en';
    try {
      if (window.YouTubePlusI18n?.getLanguage) lang = window.YouTubePlusI18n.getLanguage();
      else if (document.documentElement.lang) lang = document.documentElement.lang.split('-')[0];
    } catch {}
    return `https://myactivity.google.com/page?hl=${encodeURIComponent(lang)}&utm_medium=web&utm_source=youtube&page=youtube_comments`;
  })();

  const isMyActivityCommentsPage = () => {
    try {
      const host = location.hostname || '';
      if (!host.includes('myactivity.google.com')) return false;
      const params = new URLSearchParams(location.search || '');
      return params.get('page') === 'youtube_comments';
    } catch {
      return false;
    }
  };

  const registerObserverSafe = observer => {
    try {
      if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
        YouTubeUtils.cleanupManager.registerObserver(observer);
      }
    } catch {}
  };

  const registerListenerSafe = (target, event, handler, options) => {
    try {
      if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
        return YouTubeUtils.cleanupManager.registerListener(target, event, handler, options);
      }
    } catch {}
    try {
      target.addEventListener(event, handler, options);
    } catch {}
    return null;
  };

  const addStyleBlock = cssText => {
    try {
      if (window.YouTubeUtils && YouTubeUtils.StyleManager) {
        YouTubeUtils.StyleManager.add('comment-delete-styles', cssText);
        return;
      }
    } catch {}
    try {
      if (document.getElementById('comment-delete-styles')) return;
      const style = document.createElement('style');
      style.id = 'comment-delete-styles';
      style.textContent = cssText;
      (document.head || document.documentElement).appendChild(style);
    } catch {}
  };

  // Optimized settings
  const settings = {
    load: () => {
      try {
        const saved = localStorage.getItem(CONFIG.storageKey);
        if (saved) CONFIG.enabled = JSON.parse(saved).enabled ?? true;
      } catch {}
    },
    save: () => {
      try {
        localStorage.setItem(CONFIG.storageKey, JSON.stringify({ enabled: CONFIG.enabled }));
      } catch {}
    },
  };

  // Use shared debounce from YouTubeUtils (loaded before this module)
  const debounce = (func, wait) => {
    if (window.YouTubeUtils?.debounce) {
      const d = window.YouTubeUtils.debounce(func, wait);
      if (typeof d === 'function') return d;
    }
    let timeout;
    return (...args) => {
      clearTimeout(timeout);
      timeout = setTimeout(() => func(...args), wait);
    };
  };

  /**
   * Safely query a single element
   * @param {string} selector - CSS selector
   * @returns {HTMLElement|null} The first matching element or null
   */
  const $ = selector => /** @type {HTMLElement|null} */ (document.querySelector(selector));

  /**
   * Safely query multiple elements
   * @param {string} selector - CSS selector
   * @returns {NodeListOf<HTMLElement>} NodeList of matching elements
   */
  const $$ = selector =>
    /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(selector));

  /**
   * Log error with error boundary integration
   * @param {string} context - Error context
   * @param {Error|string|unknown} error - Error object or message
   */
  const logError = (context, error) => {
    const errorObj = error instanceof Error ? error : new Error(String(error));
    if (window.YouTubeErrorBoundary) {
      window.YouTubeErrorBoundary.logError(errorObj, { context });
    } else {
      console.error(`[YouTube+][CommentManager] ${context}:`, error);
    }
  };

  /**
   * Wraps function with error boundary protection
   * @template {Function} T
   * @param {T} fn - Function to wrap
   * @param {string} context - Error context for debugging
   * @returns {T} Wrapped function
   */
  // Use shared withErrorBoundary from YouTubeErrorBoundary
  const withErrorBoundary = (fn, context) => {
    if (window.YouTubeErrorBoundary?.withErrorBoundary) {
      return /** @type {any} */ (
        window.YouTubeErrorBoundary.withErrorBoundary(fn, 'CommentManager')
      );
    }
    return /** @type {any} */ (
      (...args) => {
        try {
          return fn(...args);
        } catch (e) {
          logError(context, e);
          return null;
        }
      }
    );
  };

  /**
   * Add checkboxes to comment elements for selection
   * Core functionality for bulk operations
   */
  const addCheckboxes = withErrorBoundary(() => {
    if (!CONFIG.enabled || state.isProcessing) return;

    const deleteButtons = $$(CONFIG.selectors.deleteButtons);

    deleteButtons.forEach(button => {
      const parent = button.parentNode;
      if (
        button.closest(CONFIG.selectors.menuButton) ||
        (parent && parent.querySelector && parent.querySelector(`.${CONFIG.classes.checkbox}`))
      ) {
        return;
      }

      const commentElement =
        button.closest('[class*="comment"]') || button.closest('[role="article"]') || parent;

      if (commentElement && commentElement instanceof Element) {
        if (!commentElement.hasAttribute('data-comment-text')) {
          commentElement.setAttribute(
            'data-comment-text',
            (commentElement.textContent || '').toLowerCase()
          );
        }
      }

      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.className = `${CONFIG.classes.checkbox} ytp-plus-settings-checkbox`;
      checkbox.setAttribute('aria-label', t('selectComment'));

      checkbox.addEventListener('change', updateDeleteButtonState);
      checkbox.addEventListener('click', e => e.stopPropagation());

      // Optimized positioning
      const dateElement =
        commentElement && commentElement.querySelector
          ? commentElement.querySelector(
              '[class*="date"],[class*="time"],time,[title*="20"],[aria-label*="ago"]'
            )
          : null;

      if (dateElement && dateElement instanceof Element) {
        dateElement.classList.add(CONFIG.classes.checkboxAnchor);
        checkbox.classList.add(CONFIG.classes.checkboxFloating);
        dateElement.appendChild(checkbox);
      } else if (parent && parent.insertBefore) {
        parent.insertBefore(checkbox, button);
      }
    });
  }, 'addCheckboxes');

  /**
   * Add control panel with bulk action buttons
   */
  const addControlButtons = withErrorBoundary(() => {
    if (!CONFIG.enabled || $(`.${CONFIG.classes.container}`)) return;

    const deleteButtons = $$(CONFIG.selectors.deleteButtons);
    if (!deleteButtons.length) return;

    const first = deleteButtons[0];
    const container = first && first.parentNode && first.parentNode.parentNode;
    if (!container || !(container instanceof Element)) return;

    const panel = document.createElement('div');
    panel.className = `${CONFIG.classes.container} ${CONFIG.classes.panel} glass-panel`;
    panel.setAttribute('role', 'region');
    panel.setAttribute('aria-label', t('commentManagerControls'));

    const header = document.createElement('div');
    header.className = CONFIG.classes.header;

    const title = document.createElement('div');
    title.className = CONFIG.classes.title;
    title.textContent = t('commentManager');

    const collapseButton = document.createElement('button');
    collapseButton.className = `${CONFIG.classes.close} ytp-plus-settings-close`;
    collapseButton.setAttribute('type', 'button');
    collapseButton.setAttribute('aria-expanded', String(!state.panelCollapsed));
    collapseButton.setAttribute('aria-label', t('togglePanel'));
    collapseButton.innerHTML = `
        <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
        </svg>
      `;

    const togglePanelState = collapsed => {
      state.panelCollapsed = collapsed;
      header.classList.toggle('is-collapsed', collapsed);
      actions.classList.toggle('is-hidden', collapsed);
      collapseButton.setAttribute('aria-expanded', String(!collapsed));
      panel.classList.toggle('is-collapsed', collapsed);
    };

    collapseButton.addEventListener('click', () => {
      state.panelCollapsed = !state.panelCollapsed;
      togglePanelState(state.panelCollapsed);
    });

    header.append(title, collapseButton);

    const actions = document.createElement('div');
    actions.className = CONFIG.classes.actions;

    const createActionButton = (label, className, onClick, options = {}) => {
      const button = document.createElement('button');
      button.type = 'button';
      button.textContent = label;
      button.className = `${CONFIG.classes.button} ${className}`;
      if (options.id) button.id = options.id;
      if (options.disabled) button.disabled = true;
      button.addEventListener('click', onClick);
      return button;
    };

    const deleteAllButton = createActionButton(
      t('deleteSelected'),
      `${CONFIG.classes.buttonDanger} ${CONFIG.classes.deleteButton}`,
      deleteSelectedComments,
      { disabled: true }
    );

    const selectAllButton = createActionButton(t('selectAll'), CONFIG.classes.buttonPrimary, () => {
      $$(`.${CONFIG.classes.checkbox}`).forEach(cb => (cb.checked = true));
      updateDeleteButtonState();
    });

    const clearAllButton = createActionButton(t('clearAll'), CONFIG.classes.buttonSuccess, () => {
      $$(`.${CONFIG.classes.checkbox}`).forEach(cb => (cb.checked = false));
      updateDeleteButtonState();
    });

    actions.append(deleteAllButton, selectAllButton, clearAllButton);
    togglePanelState(state.panelCollapsed);

    panel.append(header, actions);

    const refNode = deleteButtons[0] && deleteButtons[0].parentNode;
    if (refNode && refNode.parentNode) {
      container.insertBefore(panel, refNode);
    } else {
      container.appendChild(panel);
    }
  }, 'addControlButtons');

  /**
   * Update delete button state based on checkbox selection
   */
  const updateDeleteButtonState = withErrorBoundary(() => {
    const deleteAllButton = $(`.${CONFIG.classes.deleteButton}`);
    if (!deleteAllButton) return;

    const hasChecked = Array.from($$(`.${CONFIG.classes.checkbox}`)).some(cb => cb.checked);
    deleteAllButton.disabled = !hasChecked;
    deleteAllButton.style.opacity = hasChecked ? '1' : '0.6';
  }, 'updateDeleteButtonState');

  /**
   * Delete selected comments with confirmation
   */
  const deleteSelectedComments = withErrorBoundary(() => {
    const checkedBoxes = Array.from($$(`.${CONFIG.classes.checkbox}`)).filter(cb => cb.checked);

    if (!checkedBoxes.length || !confirm(`Delete ${checkedBoxes.length} comment(s)?`)) return;

    state.isProcessing = true;
    checkedBoxes.forEach((checkbox, index) => {
      setTimeout(() => {
        const deleteButton =
          checkbox.nextElementSibling ||
          checkbox.parentNode.querySelector(CONFIG.selectors.deleteButtons);
        deleteButton?.click();
      }, index * CONFIG.deleteDelay);
    });

    setTimeout(() => (state.isProcessing = false), checkedBoxes.length * CONFIG.deleteDelay + 1000);
  }, 'deleteSelectedComments');

  /**
   * Clean up all comment manager elements
   */
  const cleanup = withErrorBoundary(() => {
    $$(`.${CONFIG.classes.checkbox}`).forEach(el => el.remove());
    $(`.${CONFIG.classes.container}`)?.remove();
  }, 'cleanup');

  /**
   * Initialize or cleanup script based on enabled state
   */
  const initializeScript = withErrorBoundary(() => {
    if (CONFIG.enabled) {
      addCheckboxes();
      addControlButtons();
      updateDeleteButtonState();
    } else {
      cleanup();
    }
  }, 'initializeScript');

  /**
   * Add enhanced CSS styles for comment manager UI
   */
  const addStyles = withErrorBoundary(() => {
    if ($('#comment-delete-styles')) return;

    const styles = `
  .${CONFIG.classes.checkboxAnchor}{position:relative;display:inline-flex;align-items:center;gap:8px;width:auto;}
        .${CONFIG.classes.checkboxFloating}{position:absolute;top:-4px;right:-32px;margin:0;}
        /* Panel styled to match shorts feedback: glassmorphism, rounded corners, soft shadow */
        .${CONFIG.classes.panel}{position:fixed;top:50%;right:24px;transform:translateY(-50%);display:flex;flex-direction:column;gap:14px;z-index:10000;padding:16px 18px;background:var(--yt-glass-bg);border:1.5px solid var(--yt-glass-border);border-radius:20px;box-shadow:0 12px 40px rgba(0,0,0,0.45);backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%);min-width:220px;max-width:300px;color:var(--yt-text-primary);transition:transform .22s cubic-bezier(.4,0,.2,1),opacity .22s,box-shadow .2s}
        html:not([dark]) .${CONFIG.classes.panel}{background:var(--yt-glass-bg);}
        .${CONFIG.classes.header}{display:flex;align-items:center;justify-content:space-between;gap:12px;}
        .${CONFIG.classes.panel}.is-collapsed{padding:14px 18px;}
        .${CONFIG.classes.panel}.is-collapsed .${CONFIG.classes.title}{font-weight:500;opacity:.85;}
        .${CONFIG.classes.panel}.is-collapsed .${CONFIG.classes.close}{transform:rotate(45deg);}
        .${CONFIG.classes.panel}.is-collapsed .${CONFIG.classes.actions}{display:none!important;}
        .${CONFIG.classes.title}{font-size:15px;font-weight:600;letter-spacing:.3px;}
        .${CONFIG.classes.close}{background:transparent;border:none;cursor:pointer;padding:6px;border-radius:12px;display:flex;align-items:center;justify-content:center;color:var(--yt-text-primary);transition:all .2s ease;}
        .${CONFIG.classes.close}:hover{transform:rotate(90deg) scale(1.05);color:var(--yt-accent);}
        .${CONFIG.classes.actions}{display:flex;flex-direction:column;gap:10px;}
        .${CONFIG.classes.actions}.is-hidden{display:none!important;}
        .${CONFIG.classes.button}{padding:12px 16px;border-radius:var(--yt-radius-md);border:1px solid var(--yt-glass-border);cursor:pointer;font-size:13px;font-weight:500;background:var(--yt-button-bg);color:var(--yt-text-primary);transition:all .2s ease;text-align:center;}
        .${CONFIG.classes.button}:disabled{opacity:.5;cursor:not-allowed;}
        .${CONFIG.classes.button}:not(:disabled):hover{transform:translateY(-1px);box-shadow:var(--yt-shadow);}
        .${CONFIG.classes.buttonDanger}{background:rgba(255,99,71,.12);border-color:rgba(255,99,71,.25);color:#ff5c5c;}
        .${CONFIG.classes.buttonPrimary}{background:rgba(33,150,243,.12);border-color:rgba(33,150,243,.25);color:#2196f3;}
        .${CONFIG.classes.buttonSuccess}{background:rgba(76,175,80,.12);border-color:rgba(76,175,80,.25);color:#4caf50;}
        .${CONFIG.classes.buttonDanger}:not(:disabled):hover{background:rgba(255,99,71,.22);}
        .${CONFIG.classes.buttonPrimary}:not(:disabled):hover{background:rgba(33,150,243,.22);}
        .${CONFIG.classes.buttonSuccess}:not(:disabled):hover{background:rgba(76,175,80,.22);}
        @media(max-width:1280px){
          .${CONFIG.classes.panel}{top:auto;bottom:24px;transform:none;right:16px;}
        }
        @media(max-width:768px){
          .${CONFIG.classes.panel}{position:fixed;left:16px;right:16px;bottom:16px;top:auto;transform:none;max-width:none;}
          .${CONFIG.classes.actions}{flex-direction:row;flex-wrap:wrap;}
          .${CONFIG.classes.button}{flex:1;min-width:140px;}
        }
      `;
    addStyleBlock(styles);
  }, 'addStyles');

  /**
   * Add comment manager settings to YouTube+ settings panel
   */
  const addCommentManagerSettings = withErrorBoundary(() => {
    const experimentalSection = $('.ytp-plus-settings-section[data-section="experimental"]');
    if (!experimentalSection) return;

    // If already exists, move it to the bottom to ensure Comment Manager is last
    const existing = $('.comment-manager-settings-item');
    if (existing) {
      try {
        experimentalSection.appendChild(existing);
      } catch {
        // ignore
      }
      return;
    }

    const settingsItem = document.createElement('div');
    settingsItem.className = 'ytp-plus-settings-item comment-manager-settings-item';
    settingsItem.innerHTML = `
        <div>
          <label class="ytp-plus-settings-item-label">${t('commentManagement')}</label>
          <div class="ytp-plus-settings-item-description">${t('bulkDeleteDescription')}</div>
        </div>
        <button class="ytp-plus-button" id="open-comment-history-page" style="margin:0 0 0 30px;padding:12px 16px;font-size:13px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2)">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="gray" stroke-width="2">
            <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
            <polyline points="15,3 21,3 21,9"/>
            <line x1="10" y1="14" x2="21" y2="3"/>
          </svg>
        </button>
      `;

    // Append to end (ensure it's the bottom-most item)
    experimentalSection.appendChild(settingsItem);

    $('#open-comment-history-page').addEventListener('click', () => {
      window.open(COMMENT_HISTORY_URL, '_blank');
    });
  }, 'addCommentManagerSettings');

  const ensureCommentManagerSettings = (attempt = 0) => {
    const experimentalVisible = $(
      '.ytp-plus-settings-section[data-section="experimental"]:not(.hidden)'
    );
    if (!experimentalVisible) {
      if (attempt < 20) setTimeout(() => ensureCommentManagerSettings(attempt + 1), 80);
      return;
    }

    addCommentManagerSettings();
    if (!$('.comment-manager-settings-item') && attempt < 20) {
      setTimeout(() => ensureCommentManagerSettings(attempt + 1), 80);
    }
  };

  /**
   * Initialize comment manager module
   * Sets up observers, event listeners, and initial state
   */
  const init = withErrorBoundary(() => {
    // Early exit if already initialized to prevent duplicate work
    if (state.initialized && state.observer) return;

    settings.load();
    addStyles();

    // Setup observer with throttling — scope to #comments or #content for performance
    state.observer?.disconnect();
    state.observer = new MutationObserver(debounce(initializeScript, CONFIG.debounceDelay));

    registerObserverSafe(state.observer);

    const observeTarget = () => {
      const target =
        document.querySelector('#comments') || document.querySelector('#content') || document.body;
      state.observer.observe(target, { childList: true, subtree: true });
    };

    if (document.body) {
      observeTarget();
    } else {
      document.addEventListener('DOMContentLoaded', observeTarget);
    }

    // Re-scope observer after navigation (comments container may change)
    window.addEventListener(
      'yt-navigate-finish',
      () => {
        state.observer.disconnect();
        setTimeout(observeTarget, 200);
      },
      { passive: true }
    );

    // Initial setup
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', initializeScript);
    } else {
      initializeScript();
    }

    // Settings modal integration — use event instead of MutationObserver
    document.addEventListener('youtube-plus-settings-modal-opened', () => {
      setTimeout(() => ensureCommentManagerSettings(), 100);
    });

    const handleExperimentalNavClick = e => {
      const target = /** @type {EventTarget & HTMLElement} */ (e.target);
      const navItem = target?.closest?.('.ytp-plus-settings-nav-item');
      if (navItem?.dataset?.section === 'experimental') {
        setTimeout(() => ensureCommentManagerSettings(), 50);
      }
    };

    if (!state.settingsNavListenerKey) {
      state.settingsNavListenerKey = registerListenerSafe(
        document,
        'click',
        handleExperimentalNavClick,
        { passive: true, capture: true }
      );
    }
  }, 'init');

  /**
   * Check if current route is relevant for comment manager
   * @returns {boolean} True if on /watch, /shorts, or channel pages
   */
  const isRelevantRoute = () => {
    if (isMyActivityCommentsPage()) return true;
    const path = location.pathname;
    return (
      path === '/watch' ||
      path.startsWith('/shorts/') ||
      path.startsWith('/@') ||
      path.startsWith('/channel/')
    );
  };

  /**
   * Schedule lazy initialization with route checking
   */
  const scheduleInit = () => {
    if (state.initialized || !isRelevantRoute()) return;

    requestIdleCallback(
      () => {
        if (!state.initialized && isRelevantRoute()) {
          init();
          state.initialized = true;
        }
      },
      { timeout: 2000 }
    );
  };

  // Navigation observer to trigger lazy init
  const navigationObserver = new MutationObserver(
    debounce(() => {
      if (!state.initialized && isRelevantRoute()) {
        scheduleInit();
      }
    }, 300)
  );

  // Watch for navigation changes
  if (document.body) {
    navigationObserver.observe(document.body, {
      childList: true,
      subtree: false,
      attributes: false,
    });
  }

  // Start the module (lazy)
  scheduleInit();
})();

// --- MODULE: report.js ---

/* Report module: populates the settings 'report' section and provides report submission helpers.
 * Features:
 * - Small reporting form (type, title, description, email optional)
 * - Prepares debug info (version, UA, page URL, settings snapshot)
 * - Opens a prefilled GitHub issue in a new tab or copies the report to clipboard
 * - Designed to work in a userscript (no server required)
 */
(function () {
  'use strict';

  // Minimal guards for shared utils
  const Y = /** @type {any} */ (window).YouTubeUtils || {};

  /**
   * Translation function - uses centralized i18n system
   * @param {string} key - Translation key
   * @param {Object} params - Interpolation parameters
   * @returns {string} Translated text
   */
  function t(key, params = {}) {
    try {
      if (typeof window !== 'undefined') {
        if (window.YouTubePlusI18n?.t && typeof window.YouTubePlusI18n.t === 'function') {
          return window.YouTubePlusI18n.t(key, params);
        }
        if (window.YouTubeUtils?.t && typeof window.YouTubeUtils.t === 'function') {
          return window.YouTubeUtils.t(key, params);
        }
      }
    } catch {
      // Fallback to key if central i18n unavailable
    }
    return key;
  }

  /**
   * Create DOM element with properties and children
   * @param {string} tag - HTML tag name
   * @param {Object} props - Element properties
   * @param {Array} children - Child elements or text
   * @returns {HTMLElement} Created element
   */
  function mk(tag, props = {}, children = []) {
    const el = document.createElement(tag);
    Object.entries(props).forEach(([k, v]) => {
      if (k === 'class') {
        el.className = /** @type {string} */ (v);
      } else if (k === 'html') {
        if (typeof window._ytplusCreateHTML === 'function') {
          el.innerHTML = window._ytplusCreateHTML(/** @type {string} */ (v));
        } else {
          // Fallback: sanitize and set
          el.innerHTML = sanitizeHTML(/** @type {string} */ (v));
        }
      } else if (k.startsWith('on') && typeof v === 'function') {
        el.addEventListener(k.substring(2).toLowerCase(), /** @type {EventListener} */ (v));
      } else {
        el.setAttribute(k, String(v));
      }
    });
    children.forEach(c =>
      typeof c === 'string' ? el.appendChild(document.createTextNode(c)) : el.appendChild(c)
    );
    return el;
  }

  /**
   * Sanitize HTML to prevent XSS attacks
   * @param {string} html - HTML string to sanitize
   * @returns {string} Sanitized HTML
   */
  function sanitizeHTML(html) {
    if (Y?.sanitizeHTML && typeof Y.sanitizeHTML === 'function') {
      return Y.sanitizeHTML(html);
    }
    // Fallback sanitizer
    if (typeof html !== 'string') return '';
    const map = {
      '<': '&lt;',
      '>': '&gt;',
      '&': '&amp;',
      '"': '&quot;',
      "'": '&#39;',
      '/': '&#x2F;',
      '`': '&#x60;',
      '=': '&#x3D;',
    };
    return html.replace(/[<>&"'\/`=]/g, char => map[char] || char);
  }

  /**
   * Validate email address format
   * @param {string} email - Email to validate
   * @returns {boolean} Whether email is valid
   */
  function isValidEmail(email) {
    if (!email || typeof email !== 'string') return false;
    // Basic email regex - simple but effective
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email) && email.length <= 254; // RFC 5321
  }

  /**
   * Validate and sanitize title input
   * @param {string} title - Title to validate
   * @returns {string} Sanitized title
   */
  function validateTitle(title) {
    if (!title || typeof title !== 'string') return '';
    // Limit length and sanitize
    return sanitizeHTML(title.trim().substring(0, 200));
  }

  /**
   * Validate and sanitize description input
   * @param {string} description - Description to validate
   * @returns {string} Sanitized description
   */
  function validateDescription(description) {
    if (!description || typeof description !== 'string') return '';
    // Limit length and sanitize
    return sanitizeHTML(description.trim().substring(0, 5000));
  }

  /**
   * Collect debug information for reports
   * @returns {Object} Debug information object
   */
  function getDebugInfo() {
    try {
      const debug = {
        version: /** @type {any} */ (window.YouTubePlusDebug)?.version || 'unknown',
        userAgent: navigator?.userAgent || 'unknown',
        url: location?.href || 'unknown',
        language: document.documentElement?.lang || navigator?.language || 'unknown',
        settings: typeof Y?.SettingsManager === 'object' ? Y.SettingsManager.load() : null,
      };
      return debug;
    } catch (err) {
      if (Y && typeof Y.logError === 'function') {
        Y.logError('Report', 'Failed to collect debug info', err);
      }
      return {
        version: 'unknown',
        userAgent: 'unknown',
        url: 'unknown',
        language: 'unknown',
        settings: null,
        error: 'Failed to collect debug info',
      };
    }
  }

  /**
   * Build GitHub issue payload from report data
   * @param {Object} params - Report parameters
   * @param {string} params.type - Report type (bug/feature/other)
   * @param {string} params.title - Report title
   * @param {string} params.description - Report description
   * @param {string} params.email - Optional email
   * @param {boolean} params.includeDebug - Include debug info
   * @returns {{title: string, body: string}} Issue payload
   */
  function buildIssuePayload({ type, title, description, email, includeDebug }) {
    const debug = includeDebug ? getDebugInfo() : null;

    const lines = [];
    const typeLabel =
      type === 'bug' ? t('typeBug') : type === 'feature' ? t('typeFeature') : t('typeOther');
    lines.push(`**Type:** ${typeLabel}`);
    if (email) lines.push(`**Reporter email (optional):** ${email}`);
    lines.push('\n**Description:**\n');
    lines.push(description || '(no description)');
    if (debug) {
      lines.push('\n---\n**Debug info**\n');
      lines.push('```json');
      try {
        lines.push(JSON.stringify(debug, null, 2));
      } catch (err) {
        if (Y && typeof Y.logError === 'function') {
          Y.logError('Report', 'Failed to stringify debug info', err);
        }
        // Fallback to minimal debug info
        const minimalDebug = {
          version: debug.version || 'unknown',
          userAgent: debug.userAgent || 'unknown',
          url: debug.url || 'unknown',
        };
        try {
          lines.push(JSON.stringify(minimalDebug, null, 2));
        } catch {
          lines.push('{ "error": "Failed to stringify debug info" }');
        }
      }
      lines.push('```');
      lines.push('\n_Please do not include sensitive personal data._');
    }

    const body = lines.join('\n');
    const issueTitle =
      `${type === 'bug' ? '[Bug]' : type === 'feature' ? '[Feature]' : '[Report]'} ${title || ''}`.trim();
    return { title: issueTitle, body };
  }

  /**
   * Open GitHub issue in a new tab
   * @param {{title: string, body: string}} payload - Issue payload
   */
  function openGitHubIssue(payload) {
    try {
      // Repository configured for issue creation
      const repoOwner = 'diorhc';
      const repo = 'YTP';
      const url = `https://github.com/${repoOwner}/${repo}/issues/new?title=${encodeURIComponent(
        payload.title
      )}&body=${encodeURIComponent(payload.body)}`;
      window.open(url, '_blank');
    } catch (err) {
      if (Y && typeof Y.logError === 'function') {
        Y.logError('Report', 'Failed to open GitHub issue', err);
      }
      throw err;
    }
  }

  /**
   * Copy text to clipboard with fallback
   * @param {string} text - Text to copy
   * @returns {Promise<void>} Promise that resolves when copied
   */
  function copyToClipboard(text) {
    // Modern clipboard API
    if (navigator.clipboard && navigator.clipboard.writeText) {
      return navigator.clipboard.writeText(text);
    }
    // Fallback for older browsers
    return new Promise((resolve, reject) => {
      const ta = document.createElement('textarea');
      ta.value = text;
      ta.style.position = 'fixed';
      ta.style.left = '-9999px';
      ta.style.opacity = '0';
      document.body.appendChild(ta);
      try {
        ta.select();
        ta.setSelectionRange(0, text.length);
        const success = document.execCommand('copy');
        document.body.removeChild(ta);
        if (success) {
          resolve();
        } else {
          reject(new Error('execCommand failed'));
        }
      } catch (err) {
        document.body.removeChild(ta);
        reject(err);
      }
    });
  }

  /**
   * Render report section in settings modal with glassmorphism styling
   * @param {HTMLElement} modal - Settings modal element
   */
  function renderReportSection(modal) {
    if (!modal || !modal.querySelector) return;

    const section = modal.querySelector('.ytp-plus-settings-section[data-section="report"]');
    if (!section) return;

    // Clear existing content and build a small form
    section.innerHTML = '';

    const form = mk('div', {
      style:
        'display:flex;flex-direction:column;gap:var(--yt-space-sm);margin-top:var(--yt-space-md);',
    });

    // Hidden native select (kept for form value access); visible UI will be a custom glass-dropdown
    const typeSelect = mk('select', { style: 'display:none;' }, []);
    const typeOptions = [
      { v: 'bug', l: t('typeBug') },
      { v: 'feature', l: t('typeFeature') },
      { v: 'other', l: t('typeOther') },
    ];
    typeOptions.forEach(opt => {
      const o = mk('option', { value: opt.v }, [opt.l]);
      typeSelect.appendChild(o);
    });

    // Build visible glass-dropdown using shared styles
    const typeDropdown = mk('div', {
      class: 'glass-dropdown',
      id: 'report-type-dropdown',
      tabindex: '0',
      role: 'listbox',
      'aria-expanded': 'false',
    });

    const defaultLabel = typeOptions[0].l;
    const toggleBtn = mk(
      'button',
      { class: 'glass-dropdown__toggle', type: 'button', 'aria-haspopup': 'listbox' },
      [mk('span', { class: 'glass-dropdown__label' }, [defaultLabel])]
    );

    // add chevron svg to toggle
    toggleBtn.appendChild(
      mk(
        'svg',
        {
          class: 'glass-dropdown__chev',
          width: '12',
          height: '12',
          viewBox: '0 0 24 24',
          fill: 'none',
          stroke: 'currentColor',
          'stroke-width': '2',
        },
        [mk('polyline', { points: '6 9 12 15 18 9' }, [])]
      )
    );

    const listEl = mk('ul', { class: 'glass-dropdown__list', role: 'presentation' }, []);
    typeOptions.forEach((opt, i) => {
      const li = mk('li', { class: 'glass-dropdown__item', 'data-value': opt.v, role: 'option' }, [
        opt.l,
      ]);
      if (i === 0) li.setAttribute('aria-selected', 'true');
      listEl.appendChild(li);
    });

    typeDropdown.appendChild(toggleBtn);
    typeDropdown.appendChild(listEl);

    const inputStyle =
      'padding:var(--yt-space-sm);border-radius:var(--yt-radius-sm);background:var(--yt-input-bg);color:var(--yt-text-primary);border:1px solid var(--yt-glass-border);backdrop-filter:var(--yt-glass-blur-light);-webkit-backdrop-filter:var(--yt-glass-blur-light);font-size:14px;transition:var(--yt-transition);box-sizing:border-box;';

    const titleInput = mk('input', {
      placeholder: t('shortTitle'),
      style: inputStyle,
    });
    const emailInput = mk('input', {
      placeholder: t('emailOptional'),
      type: 'email',
      style: inputStyle,
    });
    const descInput = mk('textarea', {
      placeholder: t('descriptionPlaceholder'),
      rows: 6,
      style: inputStyle + 'resize:vertical;font-family:inherit;',
    });

    // checkbox input is created separately so we can listen to changes and show a preview
    const debugCheckboxInput = mk('input', {
      type: 'checkbox',
      class: 'ytp-plus-settings-checkbox',
    });
    const includeDebug = mk(
      'label',
      {
        style:
          'font-size:13px;display:flex;gap:var(--yt-space-sm);align-items:center;color:var(--yt-text-primary);cursor:pointer;align-self:center;',
      },
      [debugCheckboxInput, ' ' + t('includeDebug')]
    );

    const actions = mk('div', {
      style: 'display:flex;gap:var(--yt-space-sm);margin-top:var(--yt-space-sm);flex-wrap:wrap;',
    });
    const submitBtn = mk('button', { class: 'glass-button' }, [t('openGitHub')]);
    const copyBtn = mk('button', { class: 'glass-button' }, [t('copyReport')]);
    const emailBtn = mk('button', { class: 'glass-button' }, [t('prepareEmail')]);

    actions.appendChild(submitBtn);
    actions.appendChild(copyBtn);
    actions.appendChild(emailBtn);

    // append hidden select and visible dropdown
    form.appendChild(typeSelect);
    form.appendChild(typeDropdown);
    form.appendChild(titleInput);
    form.appendChild(emailInput);
    form.appendChild(descInput);
    form.appendChild(includeDebug);
    // Debug preview area: hidden by default, placed directly under the includeDebug checkbox
    // Use a container `div` so we can build structured, safe DOM (header + collapsible JSON)
    const debugPreview = mk(
      'div',
      {
        class: 'glass-card',
        style:
          'overflow:auto;max-height:240px;font-size:11px;display:none;margin-top:var(--yt-space-sm);padding:8px;box-sizing:border-box;',
      },
      []
    );
    form.appendChild(debugPreview);

    form.appendChild(actions);

    const privacy = mk(
      'div',
      {
        class: 'ytp-plus-settings-item-description',
        style: 'margin-top:var(--yt-space-sm);font-size:12px;color:var(--yt-text-secondary);',
      },
      [t('privacy')]
    );

    section.appendChild(form);
    section.appendChild(privacy);

    // Initialize interactions for the glass-dropdown (sync to hidden select)
    (function initReportTypeDropdown() {
      try {
        const hidden = typeSelect; // the hidden native select
        const dropdown = typeDropdown;
        const toggle = dropdown.querySelector('.glass-dropdown__toggle');
        const list = dropdown.querySelector('.glass-dropdown__list');
        const label = dropdown.querySelector('.glass-dropdown__label');
        let items = Array.from(list.querySelectorAll('.glass-dropdown__item'));
        let idx = items.findIndex(it => it.getAttribute('aria-selected') === 'true');
        if (idx < 0) idx = 0;

        const openList = () => {
          dropdown.setAttribute('aria-expanded', 'true');
          list.style.display = 'block';
          items = Array.from(list.querySelectorAll('.glass-dropdown__item'));
        };
        const closeList = () => {
          dropdown.setAttribute('aria-expanded', 'false');
          list.style.display = 'none';
        };

        // ensure initial hidden select value matches selected item
        const selectedItem = items[idx];
        if (selectedItem) {
          hidden.value = selectedItem.dataset.value || '';
          label.textContent = selectedItem.textContent || '';
        }

        toggle.addEventListener('click', () => {
          const expanded = dropdown.getAttribute('aria-expanded') === 'true';
          if (expanded) closeList();
          else openList();
        });

        document.addEventListener('click', e => {
          if (!dropdown.contains(e.target)) closeList();
        });

        list.addEventListener('click', e => {
          const it = e.target.closest('.glass-dropdown__item');
          if (!it) return;
          const val = it.dataset.value;
          hidden.value = val;
          list
            .querySelectorAll('.glass-dropdown__item')
            .forEach(li => li.removeAttribute('aria-selected'));
          it.setAttribute('aria-selected', 'true');
          label.textContent = it.textContent;
          hidden.dispatchEvent(new Event('change', { bubbles: true }));
          closeList();
        });

        // keyboard navigation
        dropdown.addEventListener('keydown', e => {
          const expanded = dropdown.getAttribute('aria-expanded') === 'true';
          if (e.key === 'ArrowDown') {
            e.preventDefault();
            if (!expanded) openList();
            idx = Math.min(idx + 1, items.length - 1);
            items.forEach(it => it.removeAttribute('aria-selected'));
            items[idx].setAttribute('aria-selected', 'true');
            items[idx].scrollIntoView({ block: 'nearest' });
          } else if (e.key === 'ArrowUp') {
            e.preventDefault();
            if (!expanded) openList();
            idx = Math.max(idx - 1, 0);
            items.forEach(it => it.removeAttribute('aria-selected'));
            items[idx].setAttribute('aria-selected', 'true');
            items[idx].scrollIntoView({ block: 'nearest' });
          } else if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            if (!expanded) {
              openList();
              return;
            }
            const it = items[idx];
            if (it) {
              hidden.value = it.dataset.value;
              hidden.dispatchEvent(new Event('change', { bubbles: true }));
              label.textContent = it.textContent;
              closeList();
            }
          } else if (e.key === 'Escape') {
            closeList();
          }
        });
      } catch (err) {
        if (Y && typeof Y.logError === 'function') {
          Y.logError('Report', 'initReportTypeDropdown', err);
        }
      }
    })();

    // (debugPreview is appended inside the form, directly under the checkbox)

    /**
     * Update debug preview based on checkbox state
     */
    function updateDebugPreview() {
      try {
        if (debugCheckboxInput.checked) {
          const d = getDebugInfo();

          // Clear previous content
          debugPreview.innerHTML = '';

          // Header with important fields
          const header = mk(
            'div',
            { style: 'display:flex;flex-direction:column;gap:6px;margin-bottom:6px;' },
            []
          );
          header.appendChild(
            mk('div', {}, ['Version: ', mk('strong', {}, [String(d.version || 'unknown')])])
          );
          header.appendChild(
            mk('div', {}, [
              'User agent: ',
              mk('code', { style: 'font-size:11px;color:var(--yt-text-secondary);' }, [
                String(d.userAgent || ''),
              ]),
            ])
          );

          const urlStr = String(d.url || 'unknown');
          let urlEl = mk('span', {}, [urlStr]);
          try {
            if (/^https?:\/\//i.test(urlStr)) {
              urlEl = mk(
                'a',
                {
                  href: urlStr,
                  target: '_blank',
                  rel: 'noopener noreferrer',
                  style: 'color:var(--yt-accent);word-break:break-all;',
                },
                [urlStr]
              );
            }
          } catch (e) {
            // leave as plain text if anything odd and log
            if (Y && typeof Y.logError === 'function') {
              Y.logError('Report', 'URL link creation failed', e);
            }
            urlEl = mk('span', {}, [String(urlStr)]);
          }
          header.appendChild(mk('div', {}, ['URL: ', urlEl]));
          header.appendChild(
            mk('div', {}, ['Language: ', mk('code', {}, [String(d.language || '')])])
          );

          debugPreview.appendChild(header);

          // Settings (if available) – collapsible
          if (d.settings) {
            const settingsDetails = mk('details', {}, [mk('summary', {}, ['Settings'])]);
            settingsDetails.appendChild(
              mk('pre', { style: 'white-space:pre-wrap;margin:6px 0 0 0;font-size:11px;' }, [
                JSON.stringify(d.settings, null, 2),
              ])
            );
            debugPreview.appendChild(settingsDetails);
          }

          // Full debug JSON (collapsible)
          const fullDetails = mk('details', {}, [mk('summary', {}, ['Full debug JSON'])]);
          fullDetails.appendChild(
            mk('pre', { style: 'white-space:pre-wrap;margin:6px 0 0 0;font-size:11px;' }, [
              JSON.stringify(d, null, 2),
            ])
          );
          debugPreview.appendChild(fullDetails);

          debugPreview.style.display = 'block';
        } else {
          debugPreview.innerHTML = '';
          debugPreview.style.display = 'none';
        }
      } catch (err) {
        if (Y && typeof Y.logError === 'function') {
          Y.logError('Report', 'updateDebugPreview failed', err);
        }
      }
    }

    // wire up checkbox to preview
    debugCheckboxInput.addEventListener('change', updateDebugPreview);

    /**
     * Gather and validate form data
     * @returns {{type: string, title: string, description: string, email: string, includeDebug: boolean, errors: string[]}}
     */
    function gather() {
      const type = /** @type {HTMLSelectElement} */ (typeSelect).value;
      const rawTitle = /** @type {HTMLInputElement} */ (titleInput).value.trim();
      const rawDescription = /** @type {HTMLTextAreaElement} */ (descInput).value.trim();
      const rawEmail = /** @type {HTMLInputElement} */ (emailInput).value.trim();
      const includeDebugValue = /** @type {HTMLInputElement} */ (
        includeDebug.querySelector('input')
      ).checked;

      const errors = [];

      // Validate title
      if (!rawTitle) {
        errors.push(t('titleRequired'));
      } else if (rawTitle.length < 5) {
        errors.push(t('titleMin'));
      }

      // Validate description
      if (!rawDescription) {
        errors.push(t('descRequired'));
      } else if (rawDescription.length < 10) {
        errors.push(t('descMin'));
      }

      // Validate email if provided
      if (rawEmail && !isValidEmail(rawEmail)) {
        errors.push(t('invalidEmail'));
      }

      return {
        type,
        title: validateTitle(rawTitle),
        description: validateDescription(rawDescription),
        email: rawEmail && isValidEmail(rawEmail) ? rawEmail : '',
        includeDebug: includeDebugValue,
        errors,
      };
    }

    submitBtn.addEventListener('click', e => {
      e.preventDefault();
      if (submitBtn.disabled) return; // Prevent double-click

      try {
        const data = gather();

        // Check for validation errors
        if (data.errors && data.errors.length > 0) {
          const errorMsg = t('fixErrorsPrefix') + data.errors.join('\n• ');
          if (Y.NotificationManager && typeof Y.NotificationManager.show === 'function') {
            Y.NotificationManager.show(errorMsg, { duration: 4000, type: 'error' });
          } else {
            console.warn('[Report] Validation errors:', data.errors);
          }
          return;
        }

        // Add loading state
        const originalText = submitBtn.textContent;
        submitBtn.disabled = true;
        submitBtn.textContent = t('opening');
        submitBtn.style.opacity = '0.6';

        const payload = buildIssuePayload(data);
        openGitHubIssue(payload);

        if (Y.NotificationManager && typeof Y.NotificationManager.show === 'function') {
          Y.NotificationManager.show(t('openingGithubNotification'), { duration: 2500 });
        }

        // Reset button after a delay
        setTimeout(() => {
          submitBtn.disabled = false;
          submitBtn.textContent = originalText;
          submitBtn.style.opacity = '1';
        }, 2000);
      } catch (err) {
        if (Y.logError) Y.logError('Report', 'Failed to open GitHub issue', err);
        if (Y.NotificationManager && typeof Y.NotificationManager.show === 'function') {
          Y.NotificationManager.show(t('failedOpenGithub'), {
            duration: 3000,
            type: 'error',
          });
        }
        submitBtn.disabled = false;
        submitBtn.textContent = t('openGitHub');
        submitBtn.style.opacity = '1';
      }
    });

    copyBtn.addEventListener('click', e => {
      e.preventDefault();
      if (copyBtn.disabled) return; // Prevent double-click

      try {
        const data = gather();

        // Check for validation errors
        if (data.errors && data.errors.length > 0) {
          const errorMsg = t('fixErrorsPrefix') + data.errors.join('\n• ');
          if (Y.NotificationManager && typeof Y.NotificationManager.show === 'function') {
            Y.NotificationManager.show(errorMsg, { duration: 4000, type: 'error' });
          } else {
            console.warn('[Report] Validation errors:', data.errors);
          }
          return;
        }

        // Add loading state
        const originalText = copyBtn.textContent;
        copyBtn.disabled = true;
        copyBtn.textContent = t('copying');
        copyBtn.style.opacity = '0.6';

        const payload = buildIssuePayload(data);
        const full = `Title: ${payload.title}\n\n${payload.body}`;

        copyToClipboard(full)
          .then(() => {
            if (Y.NotificationManager && typeof Y.NotificationManager.show === 'function') {
              Y.NotificationManager.show(t('reportCopied'), { duration: 2000 });
            }
            copyBtn.textContent = t('copied');
            copyBtn.style.opacity = '1';
            setTimeout(() => {
              copyBtn.disabled = false;
              copyBtn.textContent = originalText;
            }, 2000);
          })
          .catch(err => {
            if (Y && typeof Y.logError === 'function') Y.logError('Report', 'copy failed', err);
            if (Y && Y.NotificationManager && typeof Y.NotificationManager.show === 'function') {
              Y.NotificationManager.show(t('copyFailed'), {
                duration: 3000,
                type: 'error',
              });
            } else {
              console.warn('Copy failed; please copy manually', err);
            }
            copyBtn.disabled = false;
            copyBtn.textContent = originalText;
            copyBtn.style.opacity = '1';
          });
      } catch (err) {
        if (Y.logError) Y.logError('Report', 'Failed to copy report', err);
        copyBtn.disabled = false;
        copyBtn.textContent = t('copyReport');
        copyBtn.style.opacity = '1';
      }
    });

    emailBtn.addEventListener('click', e => {
      e.preventDefault();
      if (emailBtn.disabled) return; // Prevent double-click

      try {
        const data = gather();

        // Check for validation errors
        if (data.errors && data.errors.length > 0) {
          const errorMsg = t('fixErrorsPrefix') + data.errors.join('\n• ');
          if (Y.NotificationManager && typeof Y.NotificationManager.show === 'function') {
            Y.NotificationManager.show(errorMsg, { duration: 4000, type: 'error' });
          } else {
            console.warn('[Report] Validation errors:', data.errors);
          }
          return;
        }

        const originalText = emailBtn.textContent;
        emailBtn.disabled = true;
        emailBtn.textContent = t('opening');
        emailBtn.style.opacity = '0.6';

        const payload = buildIssuePayload(data);
        const subject = payload.title;
        // No dedicated mail address in userscript; open mail client with prefilled body
        const mailto = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(
          payload.body
        )}`;
        window.location.href = mailto;

        setTimeout(() => {
          emailBtn.disabled = false;
          emailBtn.textContent = originalText;
          emailBtn.style.opacity = '1';
        }, 2000);
      } catch (err) {
        if (Y.logError) Y.logError('Report', 'Failed to prepare email', err);
        emailBtn.disabled = false;
        emailBtn.textContent = t('prepareEmail');
        emailBtn.style.opacity = '1';
      }
    });
  }

  // Expose render function
  try {
    /** @type {any} */ (window).youtubePlusReport =
      /** @type {any} */ (window).youtubePlusReport || {};
    /** @type {any} */ (window).youtubePlusReport.render = renderReportSection;
  } catch (e) {
    if (Y.logError) Y.logError('Report', 'Failed to attach report module to window', e);
  }
})();

// --- MODULE: update.js ---

// Update checker module
(function () {
  'use strict';

  // Use centralized i18n from YouTubePlusI18n or YouTubeUtils
  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    // Fallback for initialization phase
    if (!key) return '';
    let result = String(key);
    for (const [k, v] of Object.entries(params || {})) {
      result = result.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
    }
    return result;
  };

  // Language helper delegating to global i18n when available
  const getLanguage = () => {
    if (window.YouTubePlusI18n?.getLanguage) return window.YouTubePlusI18n.getLanguage();
    if (window.YouTubeUtils?.getLanguage) return window.YouTubeUtils.getLanguage();
    const lang = document.documentElement.lang || navigator.language || 'en';
    return lang.startsWith('ru') ? 'ru' : 'en';
  };

  const UPDATE_CONFIG = {
    enabled: true,
    checkInterval: 24 * 60 * 60 * 1000, // 24 hours
    updateUrl: 'https://update.greasyfork.icu/scripts/537017/YouTube%20%2B.meta.js',
    currentVersion: '2.4.4',
    storageKey: 'youtube_plus_update_check',
    notificationDuration: 8000,
    autoInstallUrl: 'https://update.greasyfork.icu/scripts/537017/YouTube%20%2B.user.js',
    // If true, attempt to automatically initiate installation when an update is found
    // NOTE: This will try to open the install URL (GM_openInTab / window.open / navigation).
    // Keep disabled by default for safety; enable only if you want auto-install behavior.
    autoInstallOnCheck: false,
    // When false, hide the small SVG icon shown at the left of update notifications
    // Set to `true` to show the icon again.
    showNotificationIcon: false,
  };

  const windowRef = typeof window === 'undefined' ? null : window;
  const GM_namespace = windowRef?.GM || null;
  const GM_info_safe = windowRef?.GM_info || null;
  const GM_openInTab_safe = (() => {
    if (windowRef) {
      if (typeof windowRef.GM_openInTab === 'function') {
        return windowRef.GM_openInTab.bind(windowRef);
      }
      if (GM_namespace?.openInTab) {
        return GM_namespace.openInTab.bind(GM_namespace);
      }
    }
    return null;
  })();

  if (GM_info_safe?.script?.version) {
    UPDATE_CONFIG.currentVersion = GM_info_safe.script.version;
  }

  const updateState = {
    lastCheck: 0,
    lastVersion: UPDATE_CONFIG.currentVersion,
    updateAvailable: false,
    checkInProgress: false,
    updateDetails: null,
  };

  // Pluralization helper for time units (available to this module)
  /**
   * Get Russian plural form index
   * @param {number} num - Number to check
   * @returns {number} Form index (0, 1, or 2)
   */
  function getRussianPluralIndex(num) {
    const mod10 = num % 10;
    const mod100 = num % 100;

    if (mod10 === 1 && mod100 !== 11) return 0;
    if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) return 1;
    return 2;
  }

  /**
   * Get plural forms for a unit in Russian
   * @param {string} unit - Time unit
   * @returns {string[]} Array of forms
   */
  function getRussianForms(unit) {
    return {
      day: ['день', 'дня', 'дней'],
      hour: ['час', 'часа', 'часов'],
      minute: ['минута', 'минуты', 'минут'],
    }[unit];
  }

  /**
   * Get plural forms for a unit in English
   * @param {string} unit - Time unit
   * @returns {string[]} Array of forms
   */
  function getEnglishForms(unit) {
    return {
      day: ['day', 'days'],
      hour: ['hour', 'hours'],
      minute: ['minute', 'minutes'],
    }[unit];
  }

  function pluralizeTime(n, unit) {
    const lang = getLanguage();
    const num = Math.abs(Number(n)) || 0;

    if (lang === 'ru') {
      const forms = getRussianForms(unit);
      const idx = getRussianPluralIndex(num);
      return `${num} ${forms[idx]}`;
    }

    // English (default)
    const enForms = getEnglishForms(unit);
    return `${num} ${num === 1 ? enForms[0] : enForms[1]}`;
  }

  // Optimized utilities
  const utils = {
    /**
     * Load update settings from localStorage with validation
     * @returns {void}
     */
    loadSettings: () => {
      try {
        const saved = localStorage.getItem(UPDATE_CONFIG.storageKey);
        if (!saved) {
          return;
        }

        const parsed = JSON.parse(saved);

        // Validate parsed object structure
        if (typeof parsed !== 'object' || parsed === null) {
          console.error('[YouTube+][Update]', 'Invalid settings structure');
          return;
        }

        // Validate individual properties with type checking
        if (typeof parsed.lastCheck === 'number' && parsed.lastCheck >= 0) {
          updateState.lastCheck = parsed.lastCheck;
        }

        // Accept version formats like '2.2' or '2.2.0' or 'v2.2.0'
        if (typeof parsed.lastVersion === 'string') {
          const ver = parsed.lastVersion.replace(/^v/i, '');
          if (/^\d+(?:\.\d+){0,2}$/.test(ver)) {
            updateState.lastVersion = ver;
          }
        }

        if (typeof parsed.updateAvailable === 'boolean') {
          updateState.updateAvailable = parsed.updateAvailable;
        }

        if (parsed.updateDetails && typeof parsed.updateDetails === 'object') {
          // Validate updateDetails properties
          if (
            typeof parsed.updateDetails.version === 'string' &&
            /^\d+\.\d+\.\d+/.test(parsed.updateDetails.version)
          ) {
            updateState.updateDetails = parsed.updateDetails;
          }
        }
      } catch (e) {
        console.error('[YouTube+][Update]', 'Failed to load update settings:', e);
      }
    },

    /**
     * Save update settings to localStorage
     * @returns {void}
     */
    saveSettings: () => {
      try {
        const dataToSave = {
          lastCheck: updateState.lastCheck,
          lastVersion: updateState.lastVersion,
          updateAvailable: updateState.updateAvailable,
          updateDetails: updateState.updateDetails,
        };

        localStorage.setItem(UPDATE_CONFIG.storageKey, JSON.stringify(dataToSave));
      } catch (e) {
        console.error('[YouTube+][Update]', 'Failed to save update settings:', e);
      }
    },

    /**
     * Compare two version strings
     * @param {string} v1 - First version
     * @param {string} v2 - Second version
     * @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
     */
    compareVersions: (v1, v2) => {
      // Validate version format
      if (typeof v1 !== 'string' || typeof v2 !== 'string') {
        console.error('[YouTube+][Update]', 'Invalid version format - must be strings');
        return 0;
      }

      const normalize = v =>
        v
          .replace(/[^\d.]/g, '')
          .split('.')
          .map(n => parseInt(n, 10) || 0);
      const [parts1, parts2] = [normalize(v1), normalize(v2)];
      const maxLength = Math.max(parts1.length, parts2.length);

      for (let i = 0; i < maxLength; i++) {
        const diff = (parts1[i] || 0) - (parts2[i] || 0);
        if (diff !== 0) {
          return diff;
        }
      }
      return 0;
    },

    /**
     * Parse metadata from update script with validation
     * @param {string} text - Metadata text
     * @returns {Object} Parsed metadata with version, description, downloadUrl
     */
    parseMetadata: text => {
      if (typeof text !== 'string' || text.length > 100000) {
        console.error('[YouTube+][Update]', 'Invalid metadata text');
        return { version: null, description: '', downloadUrl: UPDATE_CONFIG.autoInstallUrl };
      }

      const extractField = field =>
        text.match(new RegExp(`@${field}\\s+([^\\r\\n]+)`))?.[1]?.trim();

      let version = extractField('version');
      const description = extractField('description') || '';
      const downloadUrl = extractField('downloadURL') || UPDATE_CONFIG.autoInstallUrl;

      // Validate extracted version
      if (version) {
        version = version.replace(/^v/i, '').trim();
        // Accept '2.2' or '2.2.0' or '2'
        if (!/^\d+(?:\.\d+){0,2}$/.test(version)) {
          console.error('[YouTube+][Update]', 'Invalid version format in metadata:', version);
          return { version: null, description: '', downloadUrl: UPDATE_CONFIG.autoInstallUrl };
        }
      }

      return {
        version,
        description: description.substring(0, 500), // Limit description length
        downloadUrl,
      };
    },

    formatTimeAgo: timestamp => {
      if (!timestamp) return t('never');
      const diffMs = Date.now() - timestamp;
      const diffDays = Math.floor(diffMs / 86400000);
      const diffHours = Math.floor(diffMs / 3600000);
      const diffMinutes = Math.floor(diffMs / 60000);

      if (diffDays > 0) return pluralizeTime(diffDays, 'day');
      if (diffHours > 0) return pluralizeTime(diffHours, 'hour');
      if (diffMinutes > 0) return pluralizeTime(diffMinutes, 'minute');
      return t('justNow');
    },

    showNotification: (text, type = 'info', duration = 3000) => {
      try {
        YouTubeUtils.NotificationManager.show(text, { type, duration });
      } catch (error) {
        window.YouTubeUtils &&
          YouTubeUtils.logger &&
          YouTubeUtils.logger.debug &&
          YouTubeUtils.logger.debug(`[YouTube+] ${type.toUpperCase()}:`, text, error);
      }
    },
  };
  /**
   * Validate update download URL for security
   * @param {string} downloadUrl - URL to validate
   * @returns {{valid: boolean, error: string|null}} Validation result
   */
  const validateDownloadUrl = downloadUrl => {
    if (!downloadUrl || typeof downloadUrl !== 'string') {
      return { valid: false, error: 'Invalid download URL for installation' };
    }

    try {
      const parsedUrl = new URL(downloadUrl);
      const allowedDomains = ['update.greasyfork.org', 'greasyfork.org'];

      if (parsedUrl.protocol !== 'https:') {
        return { valid: false, error: 'Only HTTPS URLs allowed for updates' };
      }

      if (!allowedDomains.includes(parsedUrl.hostname)) {
        return { valid: false, error: `Update URL domain not in allowlist: ${parsedUrl.hostname}` };
      }

      return { valid: true, error: null };
    } catch (error) {
      return { valid: false, error: `Invalid URL format: ${error.message}` };
    }
  };

  /**
   * Mark update as dismissed in session storage
   * @param {Object} details - Update details
   */
  const markUpdateDismissed = details => {
    if (details?.version && typeof details.version === 'string') {
      try {
        sessionStorage.setItem('update_dismissed', details.version);
      } catch (err) {
        console.error('[YouTube+][Update]', 'Failed to persist dismissal state:', err);
      }
    }
  };

  /**
   * Try different methods to open update URL
   * @param {string} url - URL to open
   * @returns {boolean} Success status
   */
  const tryOpenUpdateUrl = url => {
    // Method 1: GM_openInTab
    if (GM_openInTab_safe) {
      try {
        GM_openInTab_safe(url, { active: true, insert: true, setParent: true });
        return true;
      } catch (gmError) {
        console.error('[YouTube+] GM_openInTab update install failed:', gmError);
      }
    }

    // Method 2: window.open
    try {
      const popup = window.open(url, '_blank', 'noopener');
      if (popup) return true;
    } catch (popupError) {
      console.error('[YouTube+] window.open update install failed:', popupError);
    }

    // Method 3: Navigate
    try {
      window.location.assign(url);
      return true;
    } catch (navigationError) {
      console.error('[YouTube+] Navigation to update URL failed:', navigationError);
    }

    return false;
  };

  /**
   * Install update with URL validation
   * @param {Object} details - Update details containing downloadUrl and version
   * @returns {boolean} True if installation initiated successfully
   */
  const installUpdate = (details = updateState.updateDetails) => {
    const downloadUrl = details?.downloadUrl || UPDATE_CONFIG.autoInstallUrl;

    // Validate URL
    const validation = validateDownloadUrl(downloadUrl);
    if (!validation.valid) {
      console.error('[YouTube+][Update]', validation.error);
      return false;
    }

    // Try to open URL
    const success = tryOpenUpdateUrl(downloadUrl);
    if (success) {
      markUpdateDismissed(details);
    }

    return success;
  };

  // Enhanced update notification
  const showUpdateNotification = updateDetails => {
    // Optionally render notification icon (can be disabled via config)
    const iconHtml = UPDATE_CONFIG.showNotificationIcon
      ? `<div style="background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03));
                        border-radius: 10px; padding: 10px; flex-shrink: 0; border: 1px solid rgba(255,255,255,0.08);
                        backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
              <path d="M21 12c0 1-1 2-1 2s-1-1-1-2 1-2 1-2 1 1 1 2z"/>
              <path d="m21 12-5-5v3H8v4h8v3l5-5z"/>
            </svg>
          </div>`
      : '';
    const notification = document.createElement('div');
    notification.className = 'youtube-enhancer-notification update-notification';
    // Use centralized notification container for consistent placement. Keep visual styles but remove fixed positioning.
    notification.style.cssText = `
      z-index: 10001; max-width: 360px;
      background: rgba(255,255,255,0.04); padding: 16px 18px; border-radius: 14px;
      color: rgba(255,255,255,0.95);
      box-shadow: 0 8px 30px rgba(11, 15, 25, 0.45), inset 0 1px 0 rgba(255,255,255,0.02);
      border: 1px solid rgba(255,255,255,0.08);
      -webkit-backdrop-filter: blur(10px) saturate(120%);
      backdrop-filter: blur(10px) saturate(120%);
      animation: slideInFromBottom 0.4s ease-out;
    `;

    notification.innerHTML = `
        <div style="position: relative; display: flex; align-items: flex-start; gap: 12px;">
            ${iconHtml}
          <div style="flex: 1; min-width: 0;">
            <div style="font-weight: 600; font-size: 15px; margin-bottom: 4px;">${t('updateAvailableTitle')}</div>
            <div style="font-size: 13px; opacity: 0.9; margin-bottom: 8px;">
              ${t('version')} ${updateDetails.version}
            </div>
            ${
              updateDetails.changelog || updateDetails.description
                ? (function () {
                    const header = t('changelogHeader');

                    // Prefer fetched changelog, fall back to metadata description
                    const raw =
                      updateDetails.changelog && updateDetails.changelog.length > 0
                        ? updateDetails.changelog
                        : updateDetails.description || '';

                    // Sanitize and normalize incoming text: convert HTML breaks to newlines,
                    // strip tags and decode a few common entities.
                    const sanitize = s =>
                      String(s)
                        .replace(/<br\s*\/?>/gi, '\n')
                        .replace(/<\/p>/gi, '\n')
                        .replace(/<[^>]*>?/g, '') // strip complete AND incomplete HTML tags
                        .replace(/&amp;/g, '&')
                        .replace(/&lt;/g, '<')
                        .replace(/&gt;/g, '>')
                        .replace(/&quot;/g, '"')
                        .replace(/&#039;/g, "'")
                        .trim();

                    const text = sanitize(raw);

                    // Split into non-empty lines and render each as its own block so wrapping works.
                    const lines = text
                      .split(/\n+/)
                      .map(l => l.trim())
                      .filter(Boolean);
                    const listHtml = lines
                      .map(
                        l =>
                          `<div style="font-size:12px; opacity:0.85; margin-bottom:6px;">${l}</div>`
                      )
                      .join('');

                    return (
                      `<div style="font-size:12px; font-weight:600; opacity:0.95; margin-bottom:6px;">${header}</div>` +
                      `<div style="font-size:12px; line-height:1.4; max-height:120px; overflow-y:auto; padding:8px; background: rgba(0,0,0,0.2); border-radius:6px; border:1px solid rgba(255,255,255,0.05); white-space:normal;">${listHtml}</div>`
                    );
                  })()
                : `<div style="font-size: 12px; opacity: 0.85; margin-bottom: 12px;">${t('newFeatures')}</div>`
            }
            <div style="display: flex; gap: 8px;">
              <button id="update-install-btn" style="
                background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
                color: #ff5a1a; border: 1px solid rgba(255,90,30,0.12);
                padding: 8px 16px; border-radius: 8px; cursor: pointer;
                font-size: 13px; font-weight: 700; transition: transform 0.15s ease;
                box-shadow: 0 6px 18px rgba(90,30,0,0.12);
                backdrop-filter: blur(6px);
              ">${t('installUpdate')}</button>
              <button id="update-dismiss-btn" style="
                background: transparent; color: rgba(255,255,255,0.9);
                border: 1px solid rgba(255,255,255,0.06); padding: 8px 12px;
                border-radius: 8px; cursor: pointer; font-size: 13px; transition: all 0.12s ease;
              ">${t('later')}</button>
            </div>
          </div>
          <button id="update-close-btn" aria-label="${t('dismiss')}" style="
            position: absolute; top: -8px; right: -8px; width: 28px; height: 28px;
            border-radius: 50%; border: none; cursor: pointer; display: flex;
            align-items: center; justify-content: center; font-size: 16px; line-height: 1;
            background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.85); transition: background 0.18s ease;
            border: 1px solid rgba(255,255,255,0.06);
          ">&times;</button>
        </div>
        <style>
          @keyframes slideInFromBottom {
            from { transform: translateY(100%); opacity: 0; }
            to { transform: translateY(0); opacity: 1; }
          }
          @keyframes slideOutToBottom {
            from { transform: translateY(0); opacity: 1; }
            to { transform: translateY(100%); opacity: 0; }
          }
          #update-close-btn:hover {
            background: rgba(255, 255, 255, 0.25);
          }
        </style>
      `;

    // Append into centralized notification container (created if missing)
    const _containerId = 'youtube-enhancer-notification-container';
    let _container = document.getElementById(_containerId);
    if (!_container) {
      _container = document.createElement('div');
      _container.id = _containerId;
      _container.className = 'youtube-enhancer-notification-container';
      try {
        document.body.appendChild(_container);
      } catch {
        document.body.appendChild(notification);
      }
    }
    try {
      _container.insertBefore(notification, _container.firstChild);
    } catch {
      document.body.appendChild(notification);
    }

    const removeNotification = () => {
      // use explicit slide-out animation so it exits downward like the entry
      notification.style.animation = 'slideOutToBottom 0.35s ease-in forwards';
      setTimeout(() => notification.remove(), 360);
    };

    // Event handlers
    const installBtn = notification.querySelector('#update-install-btn');
    if (installBtn) {
      installBtn.addEventListener('click', () => {
        const success = installUpdate(updateDetails);
        if (success) {
          removeNotification();
          setTimeout(() => utils.showNotification(t('installing')), 500);
        } else {
          utils.showNotification(t('manualInstallHint'), 'error', 5000);
          window.open('http://greasyfork.icu/en/scripts/537017-youtube', '_blank');
        }
      });
    }

    const dismissBtn = notification.querySelector('#update-dismiss-btn');
    if (dismissBtn) {
      dismissBtn.addEventListener('click', () => {
        if (updateDetails?.version) {
          sessionStorage.setItem('update_dismissed', updateDetails.version);
        }
        removeNotification();
      });
    }

    const closeBtn = notification.querySelector('#update-close-btn');
    if (closeBtn) {
      closeBtn.addEventListener('click', () => {
        if (updateDetails?.version) {
          sessionStorage.setItem('update_dismissed', updateDetails.version);
        }
        removeNotification();
      });
    }

    // Auto-dismiss
    setTimeout(() => {
      if (notification.isConnected) removeNotification();
    }, UPDATE_CONFIG.notificationDuration);
  };

  /**
   * Validate update URL
   * @param {string} url - URL to validate
   * @throws {Error} If URL is invalid
   */
  const validateUpdateUrl = url => {
    const parsedUrl = new URL(url);
    if (parsedUrl.protocol !== 'https:') {
      throw new Error('Update URL must use HTTPS');
    }
    if (!parsedUrl.hostname.includes('greasyfork.org')) {
      throw new Error('Update URL must be from greasyfork.org');
    }
  };

  /**
   * Fetch update metadata with timeout protection. Accepts a URL so callers can
   * request alternate endpoints (for example the .user.js auto-install URL) as a
   * fallback when the primary metadata does not include a usable version.
   * @param {string} [url=UPDATE_CONFIG.updateUrl] - URL to fetch metadata from
   * @returns {Promise<string>} Metadata text
   */
  const fetchUpdateMetadata = async (url = UPDATE_CONFIG.updateUrl) => {
    // Use GM_xmlhttpRequest if available to avoid CORS issues.
    const fetchMeta = async requestUrl => {
      if (typeof GM_xmlhttpRequest !== 'undefined') {
        return new Promise((resolve, reject) => {
          const timeoutId = setTimeout(() => reject(new Error('Update check timeout')), 10000);
          GM_xmlhttpRequest({
            method: 'GET',
            url: requestUrl,
            timeout: 10000,
            headers: { Accept: 'text/plain', 'User-Agent': 'YouTube+ UpdateChecker' },
            onload: response => {
              clearTimeout(timeoutId);
              if (response.status >= 200 && response.status < 300) resolve(response.responseText);
              else reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
            },
            onerror: e => {
              clearTimeout(timeoutId);
              reject(new Error(`Network error: ${e}`));
            },
            ontimeout: () => {
              clearTimeout(timeoutId);
              reject(new Error('Update check timeout'));
            },
          });
        });
      }

      // Fallback to fetch with AbortController timeout
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 10000);
      try {
        const res = await fetch(requestUrl, {
          method: 'GET',
          cache: 'no-cache',
          signal: controller.signal,
          headers: { Accept: 'text/plain', 'User-Agent': 'YouTube+ UpdateChecker' },
        });
        if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
        return await res.text();
      } finally {
        clearTimeout(timeoutId);
      }
    };

    return fetchMeta(url);
  };

  /**
   * Handle update availability check results
   * @param {Object} updateDetails - Update details object
   * @param {boolean} force - Whether check was forced
   */
  const handleUpdateResult = (updateDetails, force) => {
    const shouldShowNotification =
      updateState.updateAvailable &&
      (force || sessionStorage.getItem('update_dismissed') !== updateDetails.version);

    if (shouldShowNotification) {
      showUpdateNotification(updateDetails);
      window.YouTubeUtils &&
        YouTubeUtils.logger &&
        YouTubeUtils.logger.debug &&
        YouTubeUtils.logger.debug(`YouTube + Update available: ${updateDetails.version}`);
      return;
    }

    if (force) {
      const message = updateState.updateAvailable
        ? t('updateAvailableMsg').replace('{version}', updateDetails.version)
        : t('upToDateMsg').replace('{version}', UPDATE_CONFIG.currentVersion);
      utils.showNotification(message);
    }
  };

  /**
   * Determine if error is transient and retryable
   * @param {Error} error - Error object
   * @returns {boolean} True if error is transient
   */
  const isTransientError = error => {
    return (
      error.name === 'AbortError' ||
      error.name === 'NetworkError' ||
      (error.message && error.message.includes('fetch')) ||
      (error.message && error.message.includes('network'))
    );
  };

  /**
   * Fetch changelog for a specific version from GreasyFork
   * @param {string} version - Version to fetch changelog for
   * @returns {Promise<string>} Changelog text
   */
  const fetchChangelog = async version => {
    try {
      const lang = getLanguage();
      const url = `http://greasyfork.icu/${lang}/scripts/537017-youtube/versions`;

      const fetchPage = async requestUrl => {
        if (typeof GM_xmlhttpRequest !== 'undefined') {
          return new Promise((resolve, reject) => {
            const timeoutId = setTimeout(() => reject(new Error('Changelog fetch timeout')), 10000);
            GM_xmlhttpRequest({
              method: 'GET',
              url: requestUrl,
              timeout: 10000,
              headers: { Accept: 'text/html' },
              onload: response => {
                clearTimeout(timeoutId);
                if (response.status >= 200 && response.status < 300) resolve(response.responseText);
                else reject(new Error(`HTTP ${response.status}`));
              },
              onerror: _e => {
                clearTimeout(timeoutId);
                reject(new Error('Network error'));
              },
              ontimeout: () => {
                clearTimeout(timeoutId);
                reject(new Error('Timeout'));
              },
            });
          });
        }

        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 10000);
        try {
          const res = await fetch(requestUrl, {
            method: 'GET',
            cache: 'no-cache',
            signal: controller.signal,
            headers: { Accept: 'text/html' },
          });
          if (!res.ok) throw new Error(`HTTP ${res.status}`);
          return await res.text();
        } finally {
          clearTimeout(timeoutId);
        }
      };

      const html = await fetchPage(url);

      // Parse changelog from HTML
      // Look for version link followed by changelog span
      // Structure: <a ...>v2.4.4</a> ... <span class="version-changelog">...</span>
      const escapedVersion = version.replace(/\./g, '\\.');
      // Match anchor tag content that contains the version number (handling prefixes like 'v', 'вер. ', etc.)
      const versionRegex = new RegExp(
        `>[^<]*?${escapedVersion}</a>[\\s\\S]*?class="version-changelog"[^>]*>([\\s\\S]*?)</span>`,
        'i'
      );

      const match = html.match(versionRegex);

      if (match && match[1]) {
        let changelog = match[1].trim();

        // Convert HTML breaks/paragraphs to newlines and strip tags
        changelog = changelog
          .replace(/<br\s*\/?>/gi, '\n')
          .replace(/<\/p>/gi, '\n')
          .replace(/<[^>]+>/g, '')
          .replace(/&amp;/g, '&')
          .replace(/&lt;/g, '<')
          .replace(/&gt;/g, '>')
          .replace(/&quot;/g, '"')
          .replace(/&#039;/g, "'");

        // Clean up whitespace
        changelog = changelog
          .split('\n')
          .map(line => line.trim())
          .filter(line => line.length > 0)
          .join('\n');

        return changelog || '';
      }

      return '';
    } catch (error) {
      console.warn('[YouTube+][Update] Failed to fetch changelog:', error.message);
      return '';
    }
  };

  /**
   * Retrieve update details trying primary metadata endpoint first and
   * falling back to the auto-install .user.js URL when necessary.
   * @returns {Promise<Object>} Parsed updateDetails object
   */
  const retrieveUpdateDetails = async () => {
    // Attempt primary metadata fetch
    let metaText = await fetchUpdateMetadata(UPDATE_CONFIG.updateUrl);
    let details = utils.parseMetadata(metaText);

    if (!details.version) {
      try {
        const fallbackText = await fetchUpdateMetadata(UPDATE_CONFIG.autoInstallUrl);
        const fallbackDetails = utils.parseMetadata(fallbackText);
        if (fallbackDetails.version) {
          details = fallbackDetails;
          metaText = fallbackText;
        }
      } catch (fallbackErr) {
        if (typeof console !== 'undefined' && console.warn) {
          console.warn('[YouTube+][Update] Fallback metadata fetch failed:', fallbackErr.message);
        }
      }
    }

    // Fetch changelog from GreasyFork versions page and store separately
    if (details.version) {
      try {
        const changelog = await fetchChangelog(details.version);
        // Keep original metadata description but expose fetched changelog on a separate property
        details.changelog = typeof changelog === 'string' && changelog.length > 0 ? changelog : '';
      } catch (changelogErr) {
        console.warn('[YouTube+][Update] Failed to fetch changelog:', changelogErr.message);
        details.changelog = '';
      }
    } else {
      details.changelog = '';
    }

    return details;
  };

  /**
   * Check for updates with URL validation, timeout protection, and retry logic
   * @param {boolean} force - Force update check even if recently checked
   * @param {number} retryCount - Current retry attempt (for internal use)
   * @returns {Promise<void>}
   */
  /**
   * Check if update check should proceed
   * @param {boolean} force - Force update check
   * @returns {boolean} True if should proceed
   */
  const shouldCheckForUpdates = (force, now) => {
    if (!UPDATE_CONFIG.enabled || updateState.checkInProgress) {
      return false;
    }
    return force || now - updateState.lastCheck >= UPDATE_CONFIG.checkInterval;
  };

  /**
   * Validate update configuration
   * @returns {boolean} True if valid
   * @throws {Error} If configuration is invalid
   */
  const validateUpdateConfiguration = () => {
    try {
      validateUpdateUrl(UPDATE_CONFIG.updateUrl);
      return true;
    } catch (urlError) {
      console.error('[YouTube+][Update]', 'Invalid update URL configuration:', urlError);
      throw urlError;
    }
  };

  /**
   * Process successful update details
   * @param {Object} updateDetails - Update details
   * @param {boolean} force - Force flag
   * @param {number} now - Current timestamp
   * @returns {void}
   */
  const processUpdateDetails = (updateDetails, force, now) => {
    updateState.lastCheck = now;
    updateState.lastVersion = updateDetails.version;
    updateState.updateDetails = updateDetails;

    const comparison = utils.compareVersions(UPDATE_CONFIG.currentVersion, updateDetails.version);
    updateState.updateAvailable = comparison < 0;

    handleUpdateResult(updateDetails, force);
    utils.saveSettings();

    // Auto-install if configured and update wasn't dismissed
    if (updateState.updateAvailable && UPDATE_CONFIG.autoInstallOnCheck) {
      try {
        const dismissed = sessionStorage.getItem('update_dismissed');
        if (dismissed !== updateDetails.version) {
          const started = installUpdate(updateDetails);
          if (started) {
            // Persist that we've acted on this update so we don't keep reopening it
            markUpdateDismissed(updateDetails);
            try {
              utils.showNotification(t('installing'));
            } catch {}
          } else {
            console.warn(
              '[YouTube+][Update] Auto-install could not be initiated for',
              updateDetails.downloadUrl
            );
          }
        }
      } catch (e) {
        console.error('[YouTube+][Update] Auto-installation failed:', e);
      }
    }
  };

  /**
   * Handle missing update information
   * @param {boolean} force - Force flag
   * @returns {void}
   */
  const handleMissingUpdateInfo = force => {
    updateState.updateAvailable = false;
    if (force) {
      utils.showNotification(
        t('updateCheckFailed').replace('{msg}', t('noUpdateInfo')),
        'error',
        4000
      );
    }
  };

  /**
   * Handle retry logic for update check
   * @param {Error} error - Error object
   * @param {boolean} force - Force flag
   * @param {number} retryCount - Current retry count
   * @returns {Promise<void>}
   */
  const handleUpdateRetry = async (error, force, retryCount) => {
    const MAX_RETRIES = 2;
    const RETRY_DELAY = 2000;

    if (isTransientError(error) && retryCount < MAX_RETRIES) {
      console.warn(
        `[YouTube+][Update] Retry ${retryCount + 1}/${MAX_RETRIES} after error:`,
        error.message
      );
      await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * Math.pow(2, retryCount)));
      return checkForUpdates(force, retryCount + 1);
    }

    console.error('[YouTube+][Update] Check failed after retries:', error);
    if (force) {
      utils.showNotification(t('updateCheckFailed').replace('{msg}', error.message), 'error', 4000);
    }
  };

  /**
   * Check for available updates
   * @param {boolean} force - Force update check
   * @param {number} retryCount - Retry count
   * @returns {Promise<void>}
   */
  const checkForUpdates = async (force = false, retryCount = 0) => {
    const now = Date.now();

    if (!shouldCheckForUpdates(force, now)) {
      return;
    }

    updateState.checkInProgress = true;

    try {
      validateUpdateConfiguration();
      const updateDetails = await retrieveUpdateDetails();

      if (updateDetails.version) {
        processUpdateDetails(updateDetails, force, now);
      } else {
        handleMissingUpdateInfo(force);
      }
    } catch (error) {
      await handleUpdateRetry(error, force, retryCount);
    } finally {
      updateState.checkInProgress = false;
    }
  };

  // Optimized settings UI
  const addUpdateSettings = () => {
    const aboutSection = YouTubeUtils.querySelector(
      '.ytp-plus-settings-section[data-section="about"]'
    );
    if (!aboutSection || YouTubeUtils.querySelector('.update-settings-container')) return;

    const updateContainer = document.createElement('div');
    updateContainer.className = 'update-settings-container';
    updateContainer.style.cssText = `
        padding: 16px; margin-top: 20px; border-radius: 12px;
        background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);
        -webkit-backdrop-filter: blur(10px) saturate(120%);
        backdrop-filter: blur(10px) saturate(120%);
        box-shadow: 0 6px 20px rgba(6, 10, 20, 0.45);
      `;

    const lastCheckTime = utils.formatTimeAgo(updateState.lastCheck);

    updateContainer.innerHTML = `
        <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
          <h3 style="margin: 0; font-size: 16px; font-weight: 600; color: var(--yt-spec-text-primary);">
            ${t('enhancedExperience')}
          </h3>
        </div>
        
        <div style="display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: center; 
                    padding: 16px; background: rgba(255, 255, 255, 0.03); border-radius: 10px; margin-bottom: 16px;">
          <div>
            <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
              <span style="font-size: 14px; font-weight: 600; color: var(--yt-spec-text-primary);">${t('currentVersion')}</span>
              <span style="font-size: 13px; font-weight: 600; color: var(--yt-spec-text-primary); 
                           padding: 3px 10px; background: rgba(255, 255, 255, 0.1); border-radius: 12px; 
                           border: 1px solid rgba(255, 255, 255, 0.2);">${UPDATE_CONFIG.currentVersion}</span>
            </div>
            <div style="font-size: 12px; color: var(--yt-spec-text-secondary);">
              ${t('lastChecked')}: <span style="font-weight: 500;">${lastCheckTime}</span>
              ${
                updateState.lastVersion && updateState.lastVersion !== UPDATE_CONFIG.currentVersion
                  ? `<br>${t('latestAvailable')}: <span style="color: #ff6666; font-weight: 600;">${updateState.lastVersion}</span>`
                  : ''
              }
            </div>
          </div>
          
          ${
            updateState.updateAvailable
              ? `
            <div style="display: flex; flex-direction: column; align-items: flex-end; gap: 8px;">
              <div style="display: flex; align-items: center; gap: 8px; padding: 6px 12px; 
                          background: linear-gradient(135deg, rgba(255, 68, 68, 0.2), rgba(255, 68, 68, 0.3)); 
                          border: 1px solid rgba(255, 68, 68, 0.4); border-radius: 20px;">
                <div style="width: 6px; height: 6px; background: #ff4444; border-radius: 50%; animation: pulse 2s infinite;"></div>
                <span style="font-size: 11px; color: #ff6666; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
                  ${t('updateAvailable')}
                </span>
              </div>
              <button id="install-update-btn" style="background: linear-gradient(135deg, #ff4500, #ff6b35); 
                      color: white; border: none; padding: 8px 16px; border-radius: 8px; cursor: pointer; 
                      font-size: 12px; font-weight: 600; transition: all 0.3s ease; 
                      box-shadow: 0 4px 12px rgba(255, 69, 0, 0.3);">${t('installUpdate')}</button>
            </div>
          `
              : `
            <div style="display: flex; align-items: center; gap: 8px; padding: 6px 12px; 
                        background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(34, 197, 94, 0.3)); 
                        border: 1px solid rgba(34, 197, 94, 0.4); border-radius: 20px;">
              <div style="width: 6px; height: 6px; background: #22c55e; border-radius: 50%;"></div>
              <span style="font-size: 11px; color: #22c55e; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
                ${t('upToDate')}
              </span>
            </div>
          `
          }
        </div>
        
        <div style="display: flex; gap: 12px;">
          <button class="ytp-plus-button ytp-plus-button-primary" id="manual-update-check" 
                  style="flex: 1; padding: 12px; font-size: 13px; font-weight: 600;">
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
              <path d="M21.5 2v6h-6M2.5 22v-6h6M19.13 11.48A10 10 0 0 0 12 2C6.48 2 2 6.48 2 12c0 .34.02.67.05 1M4.87 12.52A10 10 0 0 0 12 22c5.52 0 10-4.48 10-10 0-.34-.02-.67-.05-1"/>
            </svg>
            ${t('checkForUpdates')}
          </button>
          <button class="ytp-plus-button" id="open-update-page" 
                  style="padding: 12px 16px; font-size: 13px; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2);">
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="gray" stroke-width="2">
              <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
              <polyline points="15,3 21,3 21,9"/>
              <line x1="10" y1="14" x2="21" y2="3"/>
            </svg>
          </button>
        </div>

        <style>
          @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.1); } }
          @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
        </style>
      `;

    aboutSection.appendChild(updateContainer);

    // Event listeners with optimization
    const attachClickHandler = (id, handler) => {
      const element = document.getElementById(id);
      if (element) YouTubeUtils.cleanupManager.registerListener(element, 'click', handler);
    };

    // Destructure event parameter to prefer destructuring
    attachClickHandler('manual-update-check', async ({ target }) => {
      const button = /** @type {HTMLElement} */ (target);
      const originalHTML = button.innerHTML;

      button.innerHTML = `
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 
               style="margin-right: 6px; animation: spin 1s linear infinite;">
            <path d="M21.5 2v6h-6M2.5 22v-6h6M19.13 11.48A10 10 0 0 0 12 2C6.48 2 2 6.48 2 12c0 .34.02.67.05 1M4.87 12.52A10 10 0 0 0 12 22c5.52 0 10-4.48 10-10 0-.34-.02-.67-.05-1"/>
          </svg>
          ${t('checkingForUpdates')}
        `;
      button.disabled = true;

      await checkForUpdates(true);

      setTimeout(() => {
        button.innerHTML = originalHTML;
        button.disabled = false;
      }, 1000);
    });

    attachClickHandler('install-update-btn', () => {
      const success = installUpdate();
      if (success) {
        utils.showNotification(t('installing'));
      } else {
        utils.showNotification(t('manualInstallHint'), 'error', 5000);
        window.open('http://greasyfork.icu/en/scripts/537017-youtube', '_blank');
      }
    });

    attachClickHandler('open-update-page', () => {
      utils.showNotification(t('updatePageFallback'));
      window.open('http://greasyfork.icu/en/scripts/537017-youtube', '_blank');
    });
  };

  // Optimized initialization
  /**
   * Setup initial and periodic update checks
   * @returns {void}
   */
  const setupUpdateChecks = () => {
    // Initial check with delay
    setTimeout(() => checkForUpdates(), 3000);

    // Periodic checks - register interval in cleanupManager
    const intervalId = setInterval(() => checkForUpdates(), UPDATE_CONFIG.checkInterval);
    YouTubeUtils.cleanupManager.registerInterval(intervalId);
    window.addEventListener('beforeunload', () => clearInterval(intervalId));
  };

  /**
   * Setup settings modal event listener
   * @returns {void}
   */
  const setupSettingsObserver = () => {
    document.addEventListener('youtube-plus-settings-modal-opened', () => {
      setTimeout(addUpdateSettings, 100);
    });
  };

  /**
   * Setup click handler for about section
   * @returns {void}
   */
  const setupAboutClickHandler = () => {
    const clickHandler = ({ target }) => {
      const el = /** @type {HTMLElement} */ (target);
      if (el.classList?.contains('ytp-plus-settings-nav-item') && el.dataset?.section === 'about') {
        setTimeout(addUpdateSettings, 50);
      }
    };

    YouTubeUtils.cleanupManager.registerListener(document, 'click', clickHandler, {
      passive: true,
      capture: true,
    });
  };

  /**
   * Log initialization status
   * @returns {void}
   */
  const logInitialization = () => {
    try {
      if (window.YouTubeUtils && YouTubeUtils.logger && YouTubeUtils.logger.debug) {
        YouTubeUtils.logger.debug('YouTube + Update Checker initialized', {
          version: UPDATE_CONFIG.currentVersion,
          enabled: UPDATE_CONFIG.enabled,
          lastCheck: new Date(updateState.lastCheck).toLocaleString(),
          updateAvailable: updateState.updateAvailable,
        });
      }
    } catch {}
  };

  /**
   * Initialize update checker
   * @returns {void}
   */
  const init = () => {
    utils.loadSettings();
    setupUpdateChecks();
    setupSettingsObserver();
    setupAboutClickHandler();
    logInitialization();
  };

  // Start
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

// --- MODULE: music.js ---

/**
 * YouTube Music Enhancement Module
 * Provides UI improvements and features for YouTube Music
 * @module music
 * @version 2.3
 *
 * Features:
 * - Scroll-to-top button with smart container detection
 * - Enhanced navigation styles (centered search, immersive mode)
 * - Sidebar hover effects and player enhancements
 * - Health monitoring and automatic recovery
 * - SPA navigation support with debounced updates
 */

/* global GM_addStyle, GM_getValue, GM_addValueChangeListener */

(function () {
  'use strict';

  if (typeof location !== 'undefined' && location.hostname !== 'music.youtube.com') {
    return;
  }

  // DOM cache helper with fallback
  const qs = selector => {
    if (window.YouTubeDOMCache && typeof window.YouTubeDOMCache.get === 'function') {
      return window.YouTubeDOMCache.get(selector);
    }
    return document.querySelector(selector);
  };

  /**
   * Read YouTube Music settings from localStorage with defaults.
   * Kept in sync with defaults in settings UI.
   */
  const MUSIC_SETTINGS_DEFAULTS = {
    enableMusic: true,
    immersiveSearchStyles: true,
    hoverStyles: true,
    playerSidebarStyles: true,
    centeredPlayerStyles: true,
    playerBarStyles: true,
    centeredPlayerBarStyles: true,
    miniPlayerStyles: true,
  };

  function mergeMusicSettings(parsed) {
    const merged = { ...MUSIC_SETTINGS_DEFAULTS };
    if (!parsed || typeof parsed !== 'object') return merged;

    if (typeof parsed.enableMusic === 'boolean') merged.enableMusic = parsed.enableMusic;
    for (const key of Object.keys(MUSIC_SETTINGS_DEFAULTS)) {
      if (key === 'enableMusic') continue;
      if (typeof parsed[key] === 'boolean') merged[key] = parsed[key];
    }

    // Legacy flags mapping
    if (typeof parsed.enableImmersiveSearch === 'boolean') {
      merged.immersiveSearchStyles = parsed.enableImmersiveSearch;
    }
    if (typeof parsed.enableSidebarHover === 'boolean') {
      merged.hoverStyles = parsed.enableSidebarHover;
    }
    if (typeof parsed.enableCenteredPlayer === 'boolean') {
      merged.centeredPlayerStyles = parsed.enableCenteredPlayer;
    }

    // Backward-compat: if legacy flags exist and enableMusic wasn't set, infer enableMusic
    const legacyEnabled = !!(
      parsed.enableMusicStyles ||
      parsed.enableMusicEnhancements ||
      parsed.enableImmersiveSearch ||
      parsed.enableSidebarHover ||
      parsed.enableCenteredPlayer
    );
    if (legacyEnabled && typeof parsed.enableMusic !== 'boolean') merged.enableMusic = true;

    return merged;
  }

  function readMusicSettings() {
    // Prefer userscript-global storage so youtube.com and music.youtube.com share the setting.
    try {
      if (typeof GM_getValue !== 'undefined') {
        const stored = GM_getValue('youtube-plus-music-settings', null);
        if (typeof stored === 'string' && stored) {
          const parsed = JSON.parse(stored);
          return mergeMusicSettings(parsed);
        }
      }
    } catch {
      // fall back to localStorage
    }

    try {
      const stored = localStorage.getItem('youtube-plus-music-settings');
      if (!stored) return { ...MUSIC_SETTINGS_DEFAULTS };
      const parsed = JSON.parse(stored);
      return mergeMusicSettings(parsed);
    } catch {
      return { ...MUSIC_SETTINGS_DEFAULTS };
    }
  }

  function isMusicModuleEnabled(settings) {
    return !!(settings && settings.enableMusic);
  }

  // Scroll-to-top is now handled globally by enhanced.js
  // This function is kept for backward compatibility but always returns false
  function isScrollToTopEnabled() {
    return false;
  }

  /**
   * Mutable settings snapshot for live-apply.
   * @type {ReturnType<typeof readMusicSettings>}
   */
  let musicSettingsSnapshot = readMusicSettings();

  /** @type {HTMLStyleElement|null} */
  let musicStyleEl = null;

  /** @type {MutationObserver|null} */
  let observer = null;

  /** @type {number|null} */
  let healthCheckIntervalId = null;

  /** @type {(() => void)|null} */
  let detachNavigationListeners = null;

  /**
   * Enhanced styles for YouTube Music interface
   * Includes: navigation cleanup, immersive search, sidebar effects, centered player, etc.
   * @type {string}
   * @const
   */
  const enhancedStyles = `
        /* Remove borders and shadows from nav/guide when bauhaus sidenav is enabled */
        ytmusic-app-layout[is-bauhaus-sidenav-enabled] #nav-bar-background.ytmusic-app-layout { border-bottom: none !important; box-shadow: none !important; }
        ytmusic-app-layout[is-bauhaus-sidenav-enabled] #nav-bar-divider.ytmusic-app-layout { border-top: none !important; }
        ytmusic-app-layout[is-bauhaus-sidenav-enabled] #mini-guide-background.ytmusic-app-layout { border-right: 0 !important; }
        ytmusic-nav-bar, ytmusic-app-layout[is-bauhaus-sidenav-enabled] .ytmusic-nav-bar { border: none !important; box-shadow: none !important; }
        /* Center the settings button in the top nav bar (fixes it being rendered at the bottom) */
        ytmusic-settings-button.style-scope.ytmusic-nav-bar, ytmusic-nav-bar ytmusic-settings-button.style-scope.ytmusic-nav-bar {position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; bottom: auto !important; margin: 0 !important; z-index: 1000 !important;}
        /* Center the search box in the top nav bar */
        ytmusic-search-box, ytmusic-nav-bar ytmusic-search-box, ytmusic-searchbox, ytmusic-nav-bar ytmusic-searchbox {position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; margin: 0 !important; max-width: 75% !important; width: auto !important; z-index: 900 !important;}
  `;

  const immersiveSearchStyles = `
      /* yt-Immersive search behaviour for YouTube Music: expand/center the search when focused */
      ytmusic-search-box:has(input:focus), ytmusic-searchbox:has(input:focus), ytmusic-search-box:focus-within, ytmusic-searchbox:focus-within {position: fixed !important; left: 50% !important; top: 12vh !important; transform: translateX(-50%) !important; height: auto !important; max-width: 900px !important; width: min(90vw, 900px) !important; z-index: 1200 !important; display: block !important;}
      @media only screen and (min-width: 1400px) {ytmusic-search-box:has(input:focus), ytmusic-searchbox:has(input:focus) {top: 10vh !important; max-width: 1000px !important; transform: translateX(-50%) scale(1.05) !important;}}
      /* Highlight the input and add a soft glow */
      ytmusic-search-box:has(input:focus) input, ytmusic-searchbox:has(input:focus) input, ytmusic-search-box:focus-within input, ytmusic-searchbox:focus-within input {background-color: #fffb !important; box-shadow: black 0 0 30px !important;}
      @media (prefers-color-scheme: dark) {ytmusic-search-box:has(input:focus) input, ytmusic-searchbox:has(input:focus) input {background-color: #000b !important;}}
      /* Blur/scale the main content when immersive search is active */
      ytmusic-app-layout:has(ytmusic-search-box:has(input:focus)) #main-panel, ytmusic-app-layout:has(ytmusic-searchbox:has(input:focus)) #main-panel {filter: blur(18px) !important; transform: scale(1.03) !important;}
    `;

  // Ховер эффекты для боковой панели
  const hoverStyles = `
        .ytmusic-guide-renderer {opacity: 0.01 !important; transition: opacity 0.5s ease-in-out !important;}        
        .ytmusic-guide-renderer:hover { opacity: 1 !important;}        
        ytmusic-app[is-bauhaus-sidenav-enabled] #guide-wrapper.ytmusic-app {background-color: transparent !important; border: none !important;}    
    `;

  // Боковая панель плеера
  const playerSidebarStyles = `
        #side-panel {width: 40em !important; height: 80vh !important; padding: 0 2em !important; right: -30em !important; top: 10vh !important; opacity: 0 !important; position: absolute !important; transition: all 0.3s ease-in-out !important; backdrop-filter: blur(5px) !important; background-color: #0005 !important; border-radius: 1em !important; box-shadow: rgba(0, 0, 0, 0.15) 0px -36px 30px inset, rgba(0, 0, 0, 0.1) 0px -79px 40px inset, rgba(0, 0, 0, 0.06) 0px 2px 1px, rgba(0, 0, 0, 0.09) 0px 4px 2px, rgba(0, 0, 0, 0.09) 0px 8px 4px, rgba(0, 0, 0, 0.09) 0px 16px 8px, rgba(0, 0, 0, 0.09) 0px 32px 16px !important;}        
        #side-panel tp-yt-paper-tabs {transition: height 0.3s ease-in-out !important; height: 0 !important;}        
        #side-panel:hover {right: 0 !important; opacity: 1 !important;}        
        #side-panel:hover tp-yt-paper-tabs {height: 4em !important;}        
        #side-panel:has(ytmusic-tab-renderer[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"]):not(:has(ytmusic-message-renderer:not([style="display: none;"]))) {right: 0 !important; opacity: 1 !important;}        
        #side-panel {min-width: auto !important;}
      /* Allow JS to control visibility; ensure pointer-events and positioning only. */
        #side-panel .ytmusic-top-button { opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; }
      /* When button is placed inside the panel, prefer absolute positioning inside it
         so it won't be forced to fixed by the global rule. Use high specificity + !important */
        #side-panel .ytmusic-top-button {position: absolute !important; bottom: 20px !important; right: 20px !important; z-index: 1200 !important;}
    `;

  // Центрированный плеер
  const centeredPlayerStyles = `
        ytmusic-app-layout:not([player-ui-state="FULLSCREEN"]) #main-panel {position: absolute !important; height: 70vh !important; max-width: 70vw !important; aspect-ratio: 1 !important; top: 50vh !important; left: 50vw !important; transform: translate(-50%, -50%) !important;}        
        #player-page {padding: 0 !important; margin: 0 !important; left: 0 !important; top: 0 !important; height: 100% !important; width: 100% !important;}
    `;

  // Стилизация плеер бара (центрированная версия)
  const playerBarStyles = `
        ytmusic-player-bar, #player-bar-background {margin: 1vw !important; width: 98vw !important; border-radius: 1em !important; overflow: hidden !important; transition: all 0.5s ease-in-out !important; background-color: #0002 !important; box-shadow: rgba(0, 0, 0, 0.15) 0px -36px 30px inset, rgba(0, 0, 0, 0.1) 0px -79px 40px inset, rgba(0, 0, 0, 0.06) 0px 2px 1px, rgba(0, 0, 0, 0.09) 0px 4px 2px, rgba(0, 0, 0, 0.09) 0px 8px 4px, rgba(0, 0, 0, 0.09) 0px 16px 8px, rgba(0, 0, 0, 0.09) 0px 32px 16px !important;}        
        #layout:not([player-ui-state="PLAYER_PAGE_OPEN"]) #player-bar-background {background-color: #0005 !important;}
    `;

  // Центрирование плеер бара
  const centeredPlayerBarStyles = `
        #left-controls {position: absolute !important; left: 49vw !important; bottom: 15px !important; transform: translateX(-50%) !important; width: fit-content !important; order: 1 !important;}        
        .time-info {position: absolute !important; bottom: -10px !important; left: 0 !important; width: 100% !important; text-align: center !important; padding: 0 !important; margin: 0 !important;}
        .middle-controls {position: absolute !important; left: 1vw !important; bottom: 15px !important; max-width: 30vw !important; order: 0 !important;}
    `;

  // Настройки мини-плеера
  const miniPlayerStyles = `
        #main-panel:has(ytmusic-player[player-ui-state="MINIPLAYER"]) {position: fixed !important; width: 100vw !important; height: 100vh !important; top: -100vh !important; left: 0 !important; margin: 0 !important; padding: 0 !important; transform: none !important; max-width: 100vw !important;}        
        ytmusic-player[player-ui-state="MINIPLAYER"] {position: fixed !important; bottom: calc(100vh + 120px) !important; right: 30px !important; width: 350px !important; height: fit-content !important;}        
        #av-id:has(ytmusic-av-toggle) {position: absolute !important; left: 50% !important; transform: translateX(-50%) !important; top: -4em !important; opacity: 0 !important; transition: all 0.3s ease-in-out !important;}        
        #av-id:has(ytmusic-av-toggle):hover {opacity: 1 !important;}        
        #player[player-ui-state="MINIPLAYER"] {display: none !important;}
      /* Chrome-specific robustness: ensure the AV toggle container is above overlays
         and can receive hover even if :has() behaves differently. Also provide a
         non-:has fallback so the element is hoverable regardless of child matching. */
      /* Use absolute positioning (keeps internal menu alignment) but promote
         stacking and rendering to ensure it sits above overlays and receives clicks. */
        #av-id {position: absolute !important; left: 50% !important; transform: translateX(-50%) translateZ(0) !important; top: -4em !important; z-index: 10000 !important; pointer-events: auto !important; display: block !important; visibility: visible !important; width: auto !important; height: auto !important; will-change: transform, opacity !important;}
        #av-id ytmusic-av-toggle {pointer-events: auto !important;}
        #av-id:hover {opacity: 1 !important;}
      /* Prevent overlapping overlays from stealing clicks when hovering the toggle.
         This is a conservative rule; if a specific overlay still steals clicks we
         can target it explicitly later. */
        #av-id:hover, #av-id:active { filter: none !important; }
    `;

  // Scroll-to-top styles removed - now handled by enhanced.js universal button

  /**
   * Applies all enhanced styles to YouTube Music interface
   * Only applies styles when on music.youtube.com domain
   * @function applyStyles
   * @returns {void}
   */
  function applyStyles() {
    if (window.location.hostname !== 'music.youtube.com') return;

    const s = musicSettingsSnapshot || readMusicSettings();
    if (!s.enableMusic) return;

    const styleParts = [enhancedStyles];
    if (s.immersiveSearchStyles) styleParts.push(immersiveSearchStyles);
    if (s.hoverStyles) styleParts.push(hoverStyles);
    if (s.playerSidebarStyles) styleParts.push(playerSidebarStyles);
    if (s.centeredPlayerStyles) styleParts.push(centeredPlayerStyles);
    if (s.playerBarStyles) styleParts.push(playerBarStyles);
    if (s.centeredPlayerBarStyles) styleParts.push(centeredPlayerBarStyles);
    if (s.miniPlayerStyles) styleParts.push(miniPlayerStyles);

    const allStyles = `\n${styleParts.join('\n')}\n`;

    // Reuse single managed <style> for live updates.
    if (musicStyleEl && musicStyleEl.isConnected) {
      musicStyleEl.textContent = allStyles;
      window.YouTubeUtils?.logger?.debug?.('[YouTube+][Music]', 'Styles updated');
      return;
    }

    try {
      if (typeof GM_addStyle !== 'undefined') {
        const el = GM_addStyle(allStyles);
        if (el && el.tagName === 'STYLE') {
          musicStyleEl = /** @type {HTMLStyleElement} */ (el);
          try {
            musicStyleEl.id = 'youtube-plus-music-styles';
          } catch {}
        }
      }
    } catch {
      // ignore and fallback
    }

    if (!musicStyleEl || !musicStyleEl.isConnected) {
      const style = document.createElement('style');
      style.id = 'youtube-plus-music-styles';
      style.textContent = allStyles;
      document.head.appendChild(style);
      musicStyleEl = style;
    }

    window.YouTubeUtils?.logger?.debug?.('[YouTube+][Music]', 'Styles applied');
  }

  /**
   * Reference to global i18n instance
   * @type {Object|null}
   * @private
   */
  const _globalI18n_music =
    typeof window !== 'undefined' && window.YouTubePlusI18n ? window.YouTubePlusI18n : null;

  /**
   * Get debounce utility from YouTubeUtils or provide fallback
   * @function getDebounce
   * @returns {Function} Debounce function
   * @private
   */
  const getDebounce = () => {
    if (window.YouTubeUtils?.debounce) {
      return window.YouTubeUtils.debounce;
    }
    // Fallback debounce implementation
    return (fn, delay) => {
      let timeoutId;
      return (...args) => {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => fn(...args), delay);
      };
    };
  };

  /**
   * Translation helper function with fallback support
   * @function t
   * @param {string} key - Translation key
   * @param {Object} [params={}] - Optional parameters for interpolation
   * @returns {string} Translated string or key if translation not found
   */
  const t = (key, params = {}) => {
    try {
      if (_globalI18n_music && typeof _globalI18n_music.t === 'function') {
        return _globalI18n_music.t(key, params);
      }
      if (
        typeof window !== 'undefined' &&
        window.YouTubeUtils &&
        typeof window.YouTubeUtils.t === 'function'
      ) {
        return window.YouTubeUtils.t(key, params);
      }
    } catch {
      // fallback
    }
    if (!key || typeof key !== 'string') return '';
    if (Object.keys(params).length === 0) return key;
    let result = key;
    for (const [k, v] of Object.entries(params)) result = result.split(`{${k}}`).join(String(v));
    return result;
  };

  /**
   * Create button element with attributes
   * @returns {HTMLElement} Button element
   * @private
   */
  function createButton() {
    const button = document.createElement('button');
    button.id = 'ytmusic-side-panel-top-button';
    // Add both music-specific and shared class so global styles from enhanced.js
    // (the `.top-button` rules) can be applied when present.
    button.className = 'ytmusic-top-button top-button';
    button.title = t('scrollToTop');
    button.setAttribute('aria-label', t('scrollToTop'));
    button.innerHTML =
      '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>';

    // Add data attribute for debugging
    button.setAttribute('data-ytmusic-scroll-button', 'true');

    window.YouTubeUtils?.logger?.debug?.('[YouTube+][Music]', 'Button element created', {
      id: button.id,
      className: button.className,
    });

    return button;
  }

  /**
   * Cache for scroll containers to avoid repeated searches
   * @type {WeakMap<HTMLElement, HTMLElement|null>}
   * @private
   */
  const scrollContainerCache = new WeakMap();

  /**
   * Find scrollable container in side panel
   * @param {HTMLElement} sidePanel - Side panel element
   * @param {Object} MusicUtils - Utility module
   * @returns {HTMLElement|null} Scroll container or null
   * @private
   */
  function findScrollContainer(sidePanel, MusicUtils) {
    // Check cache first
    if (scrollContainerCache.has(sidePanel)) {
      const cached = scrollContainerCache.get(sidePanel);
      // Verify cached element is still in DOM and scrollable
      if (
        cached &&
        document.body.contains(cached) &&
        cached.scrollHeight > cached.clientHeight + 10
      ) {
        return cached;
      }
      // Cache invalidated
      scrollContainerCache.delete(sidePanel);
    }

    if (MusicUtils.findScrollContainer) {
      const result = MusicUtils.findScrollContainer(sidePanel);
      if (result) scrollContainerCache.set(sidePanel, result);
      return result;
    }

    // Try multiple selectors for scroll container
    // Prioritize queue/playlist containers from the screenshot
    const selectors = [
      // Tab-specific content containers (most specific)
      'ytmusic-tab-renderer[tab-identifier="FEmusic_queue"] #contents',
      'ytmusic-tab-renderer[tab-identifier="FEmusic_up_next"] #contents',
      'ytmusic-tab-renderer[tab-identifier="FEmusic_lyrics"] #contents',
      'ytmusic-tab-renderer[selected] #contents', // Currently selected tab
      'ytmusic-tab-renderer #contents', // Any tab contents
      // Queue and playlist containers
      'ytmusic-queue-renderer #contents',
      'ytmusic-playlist-shelf-renderer #contents',
      // Generic selectors
      '#side-panel #contents',
      '#contents.ytmusic-tab-renderer',
      '.ytmusic-section-list-renderer',
      '[role="tabpanel"]',
      '.ytmusic-player-queue',
      // Broader fallbacks
      'ytmusic-tab-renderer',
      '.scroller',
      '[scroll-container]',
    ];

    for (const selector of selectors) {
      const container = sidePanel?.querySelector(selector);
      if (container) {
        const isScrollable = container.scrollHeight > container.clientHeight + 10;
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          `Checking ${selector}: scrollHeight=${container.scrollHeight}, clientHeight=${container.clientHeight}, isScrollable=${isScrollable}`
        );
        if (isScrollable) {
          window.YouTubeUtils?.logger?.debug?.(
            '[YouTube+][Music]',
            `✓ Found scroll container: ${selector}`
          );
          scrollContainerCache.set(sidePanel, container);
          return container;
        }
      }
    }

    // Fallback: check if side-panel itself is scrollable
    if (sidePanel && sidePanel.scrollHeight > sidePanel.clientHeight + 10) {
      window.YouTubeUtils?.logger?.debug?.(
        '[YouTube+][Music]',
        '✓ Using side-panel as scroll container'
      );
      scrollContainerCache.set(sidePanel, sidePanel);
      return sidePanel;
    }

    // Last resort: walk direct children and a few levels deep for scrollable elements
    // Avoids querySelectorAll('*') + getComputedStyle which is extremely expensive
    if (sidePanel) {
      const fallbackSelectors = [
        'div[id]',
        'div[class]',
        '[role="tabpanel"]',
        '[role="list"]',
        '[role="listbox"]',
      ];
      let best = null;
      let bestDelta = 0;

      for (const sel of fallbackSelectors) {
        try {
          const candidates = sidePanel.querySelectorAll(sel);
          for (const el of candidates) {
            const delta = (el.scrollHeight || 0) - (el.clientHeight || 0);
            if (delta > 10 && delta > bestDelta) {
              bestDelta = delta;
              best = el;
            }
          }
        } catch {
          // ignore
        }
      }

      if (best) {
        const tag = best.tagName.toLowerCase();
        const id = best.id ? `#${best.id}` : '';
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          `✓ Best scroll container chosen: ${tag}${id}`,
          { scrollHeight: best.scrollHeight, clientHeight: best.clientHeight }
        );
        scrollContainerCache.set(sidePanel, best);
        return best;
      }
    }

    // Don't cache null result - content may load asynchronously
    window.YouTubeUtils?.logger?.debug?.(
      '[YouTube+][Music]',
      '✗ No scroll container found in side-panel'
    );
    return null;
  }

  /**
   * Setup scroll to top click behavior
   * @param {HTMLElement} button - Button element
   * @param {HTMLElement} sc - Scroll container
   * @param {Object} MusicUtils - Utility module
   * @private
   */
  function setupScrollBehavior(button, sc, MusicUtils, sidePanel) {
    if (MusicUtils.setupScrollToTop) {
      MusicUtils.setupScrollToTop(button, sc);
      return;
    }

    const findNearestScrollable = startEl => {
      let el = startEl;
      while (el && el !== document.body) {
        try {
          if (el.scrollHeight > el.clientHeight + 10) return el;
        } catch {
          // ignore errors accessing scroll properties on cross-origin or detached nodes
        }
        el = el.parentElement;
      }
      return null;
    };

    const clickHandler = ev => {
      // Prevent other handlers or navigation from interfering
      try {
        ev.preventDefault?.();
      } catch {}
      try {
        ev.stopPropagation?.();
      } catch {}

      // Determine best candidate to scroll: provided sc, fallback to nearest scrollable in sidePanel, then walk from button
      let target = sc;
      if (!target || !(target.scrollHeight > target.clientHeight + 1)) {
        target = sidePanel && findNearestScrollable(sidePanel);
      }
      if (!target) {
        target = findNearestScrollable(button.parentElement);
      }
      // As a last resort, use document.scrollingElement or window
      if (!target) {
        target = document.scrollingElement || document.documentElement || document.body;
      }

      // Debug info: record chosen target and sizes
      try {
        const info = {
          chosen: target && (target.id || target.tagName || '(window)'),
          scrollTop: target && 'scrollTop' in target ? target.scrollTop : null,
          scrollHeight: target && 'scrollHeight' in target ? target.scrollHeight : null,
          clientHeight: target && 'clientHeight' in target ? target.clientHeight : null,
        };
        // Expose last click debug info for manual inspection
        try {
          window.YouTubeMusic = window.YouTubeMusic || {};
          window.YouTubeMusic._lastClickDebug = info;
        } catch {}
        // Log via available logger or console
        window.YouTubeUtils?.logger?.debug?.('[YouTube+][Music]', 'ScrollToTop click target', info);
      } catch {}

      // Try smooth scroll then fallback to instant. Attempt multiple targets (target, sc, document)
      const tryScroll = el => {
        if (!el) return false;
        try {
          if (typeof el.scrollTo === 'function') {
            el.scrollTo({ top: 0, behavior: 'smooth' });
            return true;
          }
          if ('scrollTop' in el) {
            el.scrollTop = 0;
            return true;
          }
        } catch {
          // ignore and continue
        }
        return false;
      };

      let scrolled = false;
      scrolled = tryScroll(target) || scrolled;
      // If we have a provided sc and it differs from target, try it too
      if (sc && sc !== target) scrolled = tryScroll(sc) || scrolled;
      // Finally, try document/window
      scrolled =
        tryScroll(document.scrollingElement || document.documentElement || document.body) ||
        scrolled;

      if (!scrolled) {
        // Last-resort direct window scroll
        try {
          window.scrollTo(0, 0);
        } catch (err2) {
          window.YouTubeUtils?.logger?.debug?.(
            '[YouTube+][Music]',
            'Final scroll fallback failed',
            err2
          );
        }
      }
    };

    // Use non-passive so preventDefault works if needed
    button.addEventListener('click', clickHandler, { passive: false });
  }

  /**
   * Setup button positioning styles
   * @param {HTMLElement} button - Button element
   * @param {HTMLElement} sidePanel - Side panel element (not used with fixed positioning)
   * @param {Object} MusicUtils - Utility module
   * @private
   */
  function setupButtonPosition(button, sidePanel, MusicUtils, options = {}) {
    // options.insideSidePanel: boolean - if true, position the button inside the side panel
    if (MusicUtils.setupButtonStyles) {
      MusicUtils.setupButtonStyles(button, sidePanel, options);
      return;
    }

    if (options.insideSidePanel && sidePanel) {
      // When visually aligning with the side-panel but appending to `body`,
      // use fixed positioning so the button won't be clipped by panel transforms.
      button.style.setProperty('position', 'absolute', 'important');
      button.style.setProperty('bottom', '20px', 'important');
      button.style.setProperty('right', '20px', 'important');
      // Keep z-index high enough to be above panel content but below full-screen overlays
      button.style.setProperty('z-index', '1200', 'important');
      button.style.setProperty('pointer-events', 'auto', 'important');
      button.style.display = 'flex';
    } else {
      // Use fixed positioning so button stays visible regardless of side-panel state
      button.style.position = 'fixed';
      button.style.bottom = '100px'; // Above player bar (player bar is ~72px height)
      button.style.right = '20px'; // Match CSS definition
      button.style.zIndex = '10000'; // Higher than side-panel
      button.style.pointerEvents = 'auto';
      button.style.display = 'flex'; // Ensure flex display
    }

    window.YouTubeUtils?.logger?.debug?.('[YouTube+][Music]', 'Button positioned:', {
      position: button.style.position,
      bottom: button.style.bottom,
      right: button.style.right,
      zIndex: button.style.zIndex,
      insideSidePanel: !!options.insideSidePanel,
    });
  }

  /**
   * Setup scroll visibility toggle handler
   * @param {HTMLElement} button - Button element
   * @param {HTMLElement} sc - Scroll container
   * @param {Object} MusicUtils - Utility module
   * @private
   */
  function setupScrollVisibility(button, sc, MusicUtils) {
    // Try to use ScrollManager for better performance
    if (window.YouTubePlusScrollManager && window.YouTubePlusScrollManager.addScrollListener) {
      try {
        const cleanup = window.YouTubePlusScrollManager.addScrollListener(
          sc,
          () => {
            const shouldShow = sc.scrollTop > 100;
            button.classList.toggle('visible', shouldShow);
            window.YouTubeUtils?.logger?.debug?.(
              '[YouTube+][Music]',
              `Scroll position: ${sc.scrollTop}px, button visible: ${shouldShow}`
            );
          },
          { debounce: 100, runInitial: true }
        );

        button._scrollCleanup = cleanup;
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          'Using ScrollManager for scroll handling'
        );
        return;
      } catch {
        console.error('[YouTube+][Music] ScrollManager failed, using fallback');
      }
    }

    if (MusicUtils.setupScrollVisibility) {
      MusicUtils.setupScrollVisibility(button, sc, 100);
      return;
    }

    // Fallback implementation
    let isTabVisible = !document.hidden;
    let rafId = null;

    const updateVisibility = () => {
      // Cancel any pending animation frame
      if (rafId) {
        cancelAnimationFrame(rafId);
      }

      rafId = requestAnimationFrame(() => {
        // Don't update if tab is hidden (performance optimization)
        if (!isTabVisible) return;

        const currentScroll = sc.scrollTop || 0;
        const shouldShow = currentScroll > 100;
        const wasVisible = button.classList.contains('visible');

        button.classList.toggle('visible', shouldShow);

        // Log only on state changes to reduce noise
        if (shouldShow !== wasVisible) {
          window.YouTubeUtils?.logger?.debug?.(
            '[YouTube+][Music]',
            `Button visibility changed: ${shouldShow ? 'SHOWN' : 'HIDDEN'} (scroll: ${currentScroll}px)`
          );
        }
      });
    };

    const debounce = getDebounce();
    const scrollHandler = debounce(updateVisibility, 100);

    // Listen for page visibility changes
    const visibilityHandler = () => {
      isTabVisible = !document.hidden;
      if (isTabVisible) {
        updateVisibility();
      }
    };

    sc.addEventListener('scroll', scrollHandler, { passive: true });
    document.addEventListener('visibilitychange', visibilityHandler);

    // Initial check with slight delay to ensure layout is complete
    setTimeout(updateVisibility, 100);
    // Additional check after longer delay in case content loads asynchronously
    setTimeout(updateVisibility, 500);

    // Store cleanup function
    button._scrollCleanup = () => {
      if (rafId) cancelAnimationFrame(rafId);
      sc.removeEventListener('scroll', scrollHandler);
      document.removeEventListener('visibilitychange', visibilityHandler);
    };

    window.YouTubeUtils?.logger?.debug?.('[YouTube+][Music]', 'Using fallback scroll handler');
  }

  /**
   * Attach button to container with all setup
   * @param {HTMLElement} button - Button element
   * @param {HTMLElement} sidePanel - Side panel element (for context, not attachment)
   * @param {HTMLElement} sc - Scroll container
   * @param {Object} MusicUtils - Utility module
   * @private
   */
  function attachButtonToContainer(button, sidePanel, sc, MusicUtils) {
    try {
      setupScrollBehavior(button, sc, MusicUtils, sidePanel);

      // Prefer to visually align the button with the side-panel, but always
      // append to `document.body` to avoid clipping when the panel uses transforms.
      const attachInsidePanel = !!sidePanel;
      setupButtonPosition(button, sidePanel, MusicUtils, { insideSidePanel: attachInsidePanel });

      // Always append to `body` so the button is never clipped by panel
      // transforms/overflow. If `attachInsidePanel` is true we'll try panel first.

      if (attachInsidePanel) {
        try {
          sidePanel.appendChild(button);
        } catch (err) {
          // Fallback to body if append fails for any reason
          document.body.appendChild(button);
          // Reference the error to avoid "defined but never used" lint errors
          void err;
          window.YouTubeUtils?.logger?.debug?.(
            '[YouTube+][Music]',
            'Appending to sidePanel failed, appended to body',
            err
          );
        }
      } else {
        document.body.appendChild(button);
      }

      setupScrollVisibility(button, sc, MusicUtils);

      // Initial visibility check - show immediately if already scrolled
      const initialScroll = sc.scrollTop || 0;
      if (initialScroll > 100) {
        button.classList.add('visible');
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          `Button shown immediately (scroll: ${initialScroll}px)`
        );
      }

      window.YouTubeUtils?.logger?.debug?.(
        '[YouTube+][Music]',
        'Scroll to top button created successfully',
        {
          buttonId: button.id,
          scrollContainer: sc.tagName,
          scrollContainerId: sc.id || 'no-id',
          scrollHeight: sc.scrollHeight,
          clientHeight: sc.clientHeight,
          scrollTop: initialScroll,
          position: button.style.position,
          computedDisplay: window.getComputedStyle(button).display,
          computedOpacity: window.getComputedStyle(button).opacity,
          computedVisibility: window.getComputedStyle(button).visibility,
        }
      );
    } catch (err) {
      console.error('[YouTube+][Music] attachButton error:', err);
    }
  }

  /**
   * State tracking for button creation attempts
   * @type {Object}
   * @private
   */
  const buttonCreationState = {
    attempts: 0,
    maxAttempts: 5,
    lastAttempt: 0,
    minInterval: 500, // Minimum time between attempts
  };

  /**
   * Creates a "Scroll to Top" button in YouTube Music's side panel
   * Button appears when scrollable content is detected and user scrolls down
   * @function createScrollToTopButton
   * @returns {void}
   */
  function createScrollToTopButton() {
    try {
      // Early exit checks
      if (window.location.hostname !== 'music.youtube.com') return;

      // Check if button already exists and is properly attached
      const existingButton = document.getElementById('ytmusic-side-panel-top-button');
      if (existingButton) {
        // Verify it's in the DOM and has event listeners
        if (document.body.contains(existingButton) && existingButton._scrollCleanup) {
          window.YouTubeUtils?.logger?.debug?.(
            '[YouTube+][Music]',
            'Button already exists and is properly attached'
          );
          return;
        } else {
          // Button exists but is orphaned, remove it
          window.YouTubeUtils?.logger?.debug?.('[YouTube+][Music]', 'Removing orphaned button');
          existingButton.remove();
        }
      }

      // Rate limiting
      const now = Date.now();
      if (now - buttonCreationState.lastAttempt < buttonCreationState.minInterval) {
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          'Rate limited, skipping button creation'
        );
        return;
      }

      buttonCreationState.attempts++;
      buttonCreationState.lastAttempt = now;

      if (buttonCreationState.attempts > buttonCreationState.maxAttempts) {
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          `Max attempts (${buttonCreationState.maxAttempts}) reached, stopping retries`
        );
        return;
      }

      window.YouTubeUtils?.logger?.debug?.(
        '[YouTube+][Music]',
        `Creating button (attempt ${buttonCreationState.attempts}/${buttonCreationState.maxAttempts})`
      );

      const sidePanel = qs('#side-panel');
      const MusicUtils = window.YouTubePlusMusicUtils || {};
      const button = createButton();

      // If no side-panel, try to find the main content area or queue
      if (!sidePanel) {
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          'No side-panel found, checking for main content or queue'
        );

        // Try queue renderer (shown in playlist/queue view)
        const queueRenderer = qs('ytmusic-queue-renderer');
        if (queueRenderer) {
          const queueContents = queueRenderer.querySelector('#contents');
          if (queueContents) {
            attachButtonToContainer(button, queueRenderer, queueContents, MusicUtils);
            buttonCreationState.attempts = 0; // Reset on success
            return;
          }
        }

        // Try to find main scrollable area on homepage/explore pages
        const mainContent = qs('ytmusic-browse');
        if (mainContent) {
          const scrollContainer = mainContent.querySelector('ytmusic-section-list-renderer');
          if (scrollContainer) {
            attachButtonToContainer(button, mainContent, scrollContainer, MusicUtils);
            buttonCreationState.attempts = 0; // Reset on success
            return;
          }
        }

        // Retry later
        setTimeout(createScrollToTopButton, 1000);
        return;
      }

      const scrollContainer = findScrollContainer(sidePanel, MusicUtils);

      if (!scrollContainer) {
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          'No scroll container found, will retry with backoff'
        );

        // Retry with exponential backoff
        const backoffDelay = Math.min(500 * buttonCreationState.attempts, 3000);
        setTimeout(createScrollToTopButton, backoffDelay);
        return;
      }

      attachButtonToContainer(button, sidePanel, scrollContainer, MusicUtils);
      buttonCreationState.attempts = 0; // Reset on success

      window.YouTubeUtils?.logger?.debug?.('[YouTube+][Music]', '✓ Button created successfully');
    } catch (error) {
      console.error('[YouTube+][Music] Error creating scroll to top button:', error);
      // Retry on error if we haven't exceeded max attempts
      if (buttonCreationState.attempts < buttonCreationState.maxAttempts) {
        setTimeout(createScrollToTopButton, 1000);
      }
    }
  }

  /**
   * Checks if side panel exists and creates scroll-to-top button if needed
   * @function checkAndCreateButton
   * @returns {void}
   */
  function checkAndCreateButton() {
    try {
      const existingButton = document.getElementById('ytmusic-side-panel-top-button');

      // Clean up if button exists but is orphaned (no scroll listener)
      if (existingButton) {
        if (!existingButton._scrollCleanup || !document.body.contains(existingButton)) {
          window.YouTubeUtils?.logger?.debug?.(
            '[YouTube+][Music]',
            'Cleaning up orphaned/detached button'
          );
          if (existingButton._scrollCleanup) {
            try {
              existingButton._scrollCleanup();
            } catch {
              // ignore cleanup errors
            }
          }
          if (existingButton._positionCleanup) {
            try {
              existingButton._positionCleanup();
            } catch {
              // ignore cleanup errors
            }
          }
          existingButton.remove();
        } else {
          // Button exists and is healthy
          window.YouTubeUtils?.logger?.debug?.(
            '[YouTube+][Music]',
            'Button is healthy, no action needed'
          );
          return;
        }
      }

      // Look for containers that need a button
      const sidePanel = qs('#side-panel');
      const mainContent = qs('ytmusic-browse');
      const queueRenderer = qs('ytmusic-queue-renderer');
      const tabRenderer = qs('ytmusic-tab-renderer[tab-identifier]');

      if (sidePanel || mainContent || queueRenderer || tabRenderer) {
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          'Found container, scheduling button creation'
        );
        setTimeout(createScrollToTopButton, 300);
      } else {
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          'No suitable container found yet'
        );
      }
    } catch (error) {
      console.error('[YouTube+][Music] Error in checkAndCreateButton:', error);
    }
  }

  // Lazy init: do not inject styles/observers until settings enable it.

  /**
   * Create and configure the mutation observer
   * @function createObserver
   * @returns {MutationObserver}
   * @private
   */
  const createObserver = () => {
    const debounce = getDebounce();
    const debouncedCheck = debounce(checkAndCreateButton, 200);
    let lastCheckTime = 0;
    const minCheckInterval = 300; // Minimum 300ms between checks (reduced for better responsiveness)

    return new MutationObserver(mutations => {
      // Rate limiting: skip if checked too recently
      const now = Date.now();
      if (now - lastCheckTime < minCheckInterval) return;

      // Don't disconnect - keep observing for tab changes and navigation
      const existingButton = document.getElementById('ytmusic-side-panel-top-button');

      // If button exists and is properly attached, just verify it's working
      if (
        existingButton &&
        document.body.contains(existingButton) &&
        existingButton._scrollCleanup
      ) {
        // Button is healthy, no action needed
        return;
      }

      // Check if any mutation added side-panel, main content, or queue
      const hasRelevantChange = mutations.some(mutation => {
        // Fast path: skip mutations with no added nodes
        if (mutation.addedNodes.length === 0) return false;

        // Early filter: check if any added node is an Element
        let hasElements = false;
        for (let i = 0; i < mutation.addedNodes.length; i++) {
          if (mutation.addedNodes[i].nodeType === 1) {
            hasElements = true;
            break;
          }
        }
        if (!hasElements) return false;

        return Array.from(mutation.addedNodes).some(node => {
          if (node.nodeType !== 1) return false;

          const element = /** @type {Element} */ (node);
          // Direct ID check is fastest
          if (element.id === 'side-panel' || element.id === 'contents') return true;

          // Tag name check is faster than querySelector
          const tagName = element.tagName;
          if (
            tagName === 'YTMUSIC-BROWSE' ||
            tagName === 'YTMUSIC-PLAYER-PAGE' ||
            tagName === 'YTMUSIC-QUEUE-RENDERER' ||
            tagName === 'YTMUSIC-TAB-RENDERER'
          ) {
            return true;
          }

          // Only do querySelector as last resort
          return (
            element.querySelector?.(
              '#side-panel, #contents, ytmusic-browse, ytmusic-queue-renderer, ytmusic-tab-renderer'
            ) != null
          );
        });
      });

      // Also check for attribute changes that might indicate tab switches
      const hasTabChange = mutations.some(
        mutation =>
          mutation.type === 'attributes' &&
          mutation.attributeName === 'selected' &&
          mutation.target instanceof Element &&
          mutation.target.matches?.('ytmusic-tab-renderer, tp-yt-paper-tab')
      );

      if (hasRelevantChange || hasTabChange) {
        lastCheckTime = now;
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          'Detected relevant DOM change, checking button'
        );
        debouncedCheck();
      }
    });
  };

  /**
   * Safely observe document body for side-panel appearance
   * @function observeDocumentBodySafely
   * @returns {void}
   */
  const observeDocumentBodySafely = () => {
    if (observer) return; // Already observing

    const startObserving = () => {
      if (!document.body) return;

      try {
        observer = createObserver();
        observer.observe(document.body, {
          childList: true,
          subtree: true,
          attributes: true, // Watch for attribute changes (tab switches)
          attributeFilter: ['selected', 'tab-identifier', 'page-type'], // Only specific attributes
        });
        window.YouTubeUtils?.logger?.debug?.(
          '[YouTube+][Music]',
          '✓ Observer started with enhanced config'
        );
      } catch (observeError) {
        console.error('[YouTube+][Music] Failed to observe document.body:', observeError);
        // Retry with basic config
        try {
          observer = createObserver();
          observer.observe(document.body, {
            childList: true,
            subtree: true,
          });
          window.YouTubeUtils?.logger?.debug?.(
            '[YouTube+][Music]',
            '✓ Observer started with basic config'
          );
        } catch (retryError) {
          console.error('[YouTube+][Music] Failed to start observer (retry):', retryError);
        }
      }
    };

    if (document.body) {
      startObserving();
    } else {
      document.addEventListener('DOMContentLoaded', startObserving, { once: true });
    }
  };

  function stopScrollToTopRuntime() {
    try {
      if (healthCheckIntervalId != null) {
        clearInterval(healthCheckIntervalId);
        healthCheckIntervalId = null;
      }

      if (observer) {
        observer.disconnect();
        observer = null;
      }

      if (detachNavigationListeners) {
        try {
          detachNavigationListeners();
        } catch {}
        detachNavigationListeners = null;
      }

      const button = document.getElementById('ytmusic-side-panel-top-button');
      if (button?._scrollCleanup) {
        try {
          button._scrollCleanup();
        } catch {}
      }
      if (button?._positionCleanup) {
        try {
          button._positionCleanup();
        } catch {}
      }
      if (button) button.remove();
    } catch (e) {
      console.error('[YouTube+][Music] stopScrollToTopRuntime error:', e);
    }
  }

  function startScrollToTopRuntime() {
    if (!isScrollToTopEnabled(musicSettingsSnapshot)) return;

    // Already running
    if (observer || healthCheckIntervalId != null || detachNavigationListeners) return;

    // Ensure styles (button relies on CSS)
    applyStyles();

    // Create button on load
    if (document.readyState === 'loading') {
      document.addEventListener(
        'DOMContentLoaded',
        () => {
          checkAndCreateButton();
        },
        { once: true }
      );
    } else {
      checkAndCreateButton();
    }

    // Navigation hooks (non-invasive: no history monkeypatch)
    const debounce = getDebounce();
    const onNavigate = debounce(() => {
      if (!isScrollToTopEnabled(musicSettingsSnapshot)) return;
      applyStyles();
      buttonCreationState.attempts = 0;
      buttonCreationState.lastAttempt = 0;
      checkAndCreateButton();
    }, 150);

    const popstateHandler = () => onNavigate();
    const ytNavigateHandler = () => onNavigate();

    window.addEventListener('popstate', popstateHandler);
    window.addEventListener('yt-navigate-finish', ytNavigateHandler);

    detachNavigationListeners = () => {
      window.removeEventListener('popstate', popstateHandler);
      window.removeEventListener('yt-navigate-finish', ytNavigateHandler);
    };

    // Start observer
    observeDocumentBodySafely();

    // Periodic health check
    healthCheckIntervalId = setInterval(() => {
      try {
        if (!isScrollToTopEnabled(musicSettingsSnapshot)) return;
        if (document.hidden) return;

        const button = document.getElementById('ytmusic-side-panel-top-button');

        if (button && (!button._scrollCleanup || !document.body.contains(button))) {
          window.YouTubeUtils?.logger?.debug?.(
            '[YouTube+][Music]',
            'Health check: removing unhealthy button'
          );
          button.remove();
          checkAndCreateButton();
        }

        if (!button) {
          const sidePanel = qs('#side-panel');
          if (sidePanel) checkAndCreateButton();
        }
      } catch (error) {
        console.error('[YouTube+][Music] Health check error:', error);
      }
    }, 30000);
  }

  function startIfEnabled() {
    // Never start outside YouTube Music.
    if (window.location.hostname !== 'music.youtube.com') return;

    musicSettingsSnapshot = readMusicSettings();
    if (!isMusicModuleEnabled(musicSettingsSnapshot)) return;

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

    if (isScrollToTopEnabled(musicSettingsSnapshot)) {
      startScrollToTopRuntime();
    }
  }

  function applySettingsChanges() {
    // Re-read persisted settings and (re)apply.
    musicSettingsSnapshot = readMusicSettings();

    // If disabled, tear everything down immediately, regardless of hostname.
    if (!isMusicModuleEnabled(musicSettingsSnapshot)) {
      stopScrollToTopRuntime();
      if (musicStyleEl && musicStyleEl.isConnected) musicStyleEl.remove();
      // Defensive: remove any stray YouTube Music style tags we may have added earlier.
      try {
        document
          .querySelectorAll('#youtube-plus-music-styles')
          .forEach(el => el !== musicStyleEl && el.remove());
      } catch {}
      musicStyleEl = null;
      return;
    }

    if (window.location.hostname !== 'music.youtube.com') return;

    // Styles
    applyStyles();

    // Scroll-to-top runtime
    if (isScrollToTopEnabled(musicSettingsSnapshot)) {
      if (!observer) startScrollToTopRuntime();
    } else {
      stopScrollToTopRuntime();
    }
  }

  function saveSettings(s) {
    // Caller already saves to localStorage; keep an in-memory snapshot.
    if (s && typeof s === 'object') {
      musicSettingsSnapshot = { ...musicSettingsSnapshot, ...s };
    } else {
      musicSettingsSnapshot = readMusicSettings();
    }
  }

  // Export module to global scope for settings live-apply
  if (typeof window !== 'undefined') {
    window.YouTubeMusic = {
      observeDocumentBodySafely,
      checkAndCreateButton,
      createScrollToTopButton,
      saveSettings,
      applySettingsChanges,
      version: '2.4.1',
    };
  }

  // Cleanup on page unload
  window.addEventListener('beforeunload', () => {
    try {
      stopScrollToTopRuntime();
      if (musicStyleEl && musicStyleEl.isConnected) musicStyleEl.remove();
      musicStyleEl = null;
      window.YouTubeUtils?.logger?.debug?.('[YouTube+][Music]', 'Cleanup completed');
    } catch (error) {
      console.error('[YouTube+][Music] Cleanup error:', error);
    }
  });

  // Start only if enabled; otherwise remain dormant.
  startIfEnabled();

  // Cross-subdomain live sync: react to changes made on youtube.com settings UI.
  try {
    if (typeof GM_addValueChangeListener !== 'undefined') {
      GM_addValueChangeListener('youtube-plus-music-settings', (_name, _oldValue, newValue) => {
        try {
          if (typeof newValue === 'string' && newValue) {
            const parsed = JSON.parse(newValue);
            musicSettingsSnapshot = mergeMusicSettings(parsed);
          } else {
            musicSettingsSnapshot = readMusicSettings();
          }
        } catch {
          musicSettingsSnapshot = readMusicSettings();
        }

        // Apply immediately (will teardown if disabled).
        applySettingsChanges();
      });
    }
  } catch {}

  window.YouTubeUtils?.logger?.debug?.('[YouTube+][Music]', 'Module loaded (lazy)', {
    version: '2.4.1',
    hostname: window.location.hostname,
    enabled:
      window.location.hostname === 'music.youtube.com' &&
      isMusicModuleEnabled(musicSettingsSnapshot),
  });
})();

// --- MODULE: end.js ---

// YouTube End Screen Remover
(function () {
  'use strict';

  // DOM helpers
  const _getDOMCache = () => typeof window !== 'undefined' && window.YouTubeDOMCache;
  const $ = (sel, ctx) =>
    _getDOMCache()?.querySelector(sel, ctx) || (ctx || document).querySelector(sel);
  const $$ = (sel, ctx) =>
    _getDOMCache()?.querySelectorAll(sel, ctx) ||
    Array.from((ctx || document).querySelectorAll(sel));
  const onDomReady = (() => {
    let ready = document.readyState !== 'loading';
    const queue = [];
    const run = () => {
      ready = true;
      while (queue.length) {
        const cb = queue.shift();
        try {
          cb();
        } catch {}
      }
    };
    if (!ready) document.addEventListener('DOMContentLoaded', run, { once: true });
    return cb => {
      if (ready) cb();
      else queue.push(cb);
    };
  })();

  // Optimized configuration
  const CONFIG = {
    enabled: true,
    storageKey: 'youtube_endscreen_settings',
    // Added .teaser-carousel to cover variants named 'teaser-carousel'
    selectors:
      '.ytp-ce-element-show,.ytp-ce-element,.ytp-endscreen-element,.ytp-ce-covering-overlay,.ytp-cards-teaser,.teaser-carousel,.ytp-cards-button,.iv-drawer,.iv-branding,.video-annotations,.ytp-cards-teaser-text',
    debounceMs: 32,
    batchSize: 20,
  };

  // Minimal state with better tracking
  const state = {
    observer: null,
    styleEl: null,
    isActive: false,
    removeCount: 0,
    lastCheck: 0,
    ytNavigateListenerKey: null,
    settingsNavListenerKey: null,
  };

  // High-performance utilities: use shared debounce when available
  const debounce = (fn, ms) => {
    try {
      if (window.YouTubeUtils?.debounce) {
        return window.YouTubeUtils.debounce(fn, ms);
      }
      let id;
      return (...args) => {
        clearTimeout(id);
        id = setTimeout(() => fn(...args), ms);
      };
    } catch {
      let id;
      return (...args) => {
        clearTimeout(id);
        id = setTimeout(() => fn(...args), ms);
      };
    }
  };

  const fastRemove = elements => {
    const len = Math.min(elements.length, CONFIG.batchSize);
    for (let i = 0; i < len; i++) {
      const el = elements[i];
      if (el?.isConnected) {
        el.style.cssText = 'display:none!important;visibility:hidden!important';
        try {
          el.remove();
          state.removeCount++;
        } catch {}
      }
    }
  };

  // Settings with caching
  const settings = {
    load: () => {
      try {
        const data = localStorage.getItem(CONFIG.storageKey);
        CONFIG.enabled = data ? (JSON.parse(data).enabled ?? true) : true;
      } catch {
        CONFIG.enabled = true;
      }
    },

    save: () => {
      try {
        localStorage.setItem(CONFIG.storageKey, JSON.stringify({ enabled: CONFIG.enabled }));
      } catch {}
      settings.apply();
    },

    apply: () => (CONFIG.enabled ? init() : cleanup()),
  };

  // Optimized core functions
  const injectCSS = () => {
    if (state.styleEl || !CONFIG.enabled) return;

    const styles = `${CONFIG.selectors}{display:none!important;opacity:0!important;visibility:hidden!important;pointer-events:none!important;transform:scale(0)!important}`;
    YouTubeUtils.StyleManager.add('end-screen-remover', styles);
    // store the style id so it can be removed via StyleManager.remove
    state.styleEl = 'end-screen-remover';
  };

  const removeEndScreens = () => {
    if (!CONFIG.enabled) return;
    const now = performance.now();
    if (now - state.lastCheck < CONFIG.debounceMs) return;
    state.lastCheck = now;

    const elements = $$(CONFIG.selectors);
    if (elements.length) fastRemove(elements);
  };

  const getClassNameValue = node => {
    if (typeof node.className === 'string') {
      return node.className;
    }
    if (node.className && typeof node.className === 'object' && 'baseVal' in node.className) {
      return /** @type {any} */ (node.className).baseVal;
    }
    return '';
  };

  /**
   * Check if node is relevant for end screen removal
   * @param {Node} node - DOM node to check
   * @returns {boolean} True if relevant
   */
  const isRelevantNode = node => {
    if (!(node instanceof Element)) return false;

    const classNameValue = getClassNameValue(node);
    return classNameValue.includes('ytp-') || node.querySelector?.('.ytp-ce-element');
  };

  /**
   * Check if mutations contain relevant changes
   * @param {MutationRecord[]} mutations - Mutation records
   * @returns {boolean} True if has relevant changes
   */
  const hasRelevantChanges = mutations => {
    for (const { addedNodes } of mutations) {
      for (const node of addedNodes) {
        if (isRelevantNode(node)) return true;
      }
    }
    return false;
  };

  /**
   * Create mutation observer for end screens
   * @param {Function} throttledRemove - Throttled remove function
   * @returns {MutationObserver} Observer instance
   */
  const createEndScreenObserver = throttledRemove => {
    return new MutationObserver(mutations => {
      if (hasRelevantChanges(mutations)) {
        throttledRemove();
      }
    });
  };

  /**
   * Setup watcher for end screens
   * @returns {void}
   */
  const setupWatcher = () => {
    if (state.observer || !CONFIG.enabled) return;

    const throttledRemove = debounce(removeEndScreens, CONFIG.debounceMs);
    state.observer = createEndScreenObserver(throttledRemove);

    YouTubeUtils.cleanupManager.registerObserver(state.observer);

    const target = $('#movie_player') || document.body;
    state.observer.observe(target, {
      childList: true,
      subtree: true,
      attributeFilter: ['class', 'style'],
    });
  };

  const cleanup = () => {
    state.observer?.disconnect();
    state.observer = null;
    if (state.styleEl) {
      try {
        YouTubeUtils.StyleManager.remove(state.styleEl);
      } catch {}
    }
    state.styleEl = null;
    state.isActive = false;
  };

  const init = () => {
    if (state.isActive || !CONFIG.enabled) return;
    state.isActive = true;
    injectCSS();
    removeEndScreens();
    setupWatcher();
  };

  const setupEndscreenSettingsDelegation = (() => {
    let attached = false;
    return () => {
      if (attached) return;
      attached = true;

      const delegator = window.YouTubePlusEventDelegation;
      const handler = (ev, target) => {
        const input = /** @type {HTMLInputElement | null} */ (target);
        if (!input) return;
        if (!input.classList?.contains('ytp-plus-settings-checkbox')) return;
        if (!input.closest?.('.endscreen-settings')) return;
        CONFIG.enabled = input.checked;
        settings.save();
        void ev;
      };

      if (delegator?.on) {
        delegator.on(
          document,
          'change',
          '.endscreen-settings .ytp-plus-settings-checkbox',
          handler,
          { passive: true }
        );
      } else {
        document.addEventListener(
          'change',
          ev => {
            const target = ev.target?.closest?.('.ytp-plus-settings-checkbox');
            if (target) handler(ev, target);
          },
          { passive: true, capture: true }
        );
      }
    };
  })();

  // Streamlined settings UI
  const addSettingsUI = () => {
    const enhancedSlot = $('.endscreen-settings-slot');
    const enhancedCard = $('.enhanced-submenu .glass-card');
    const host = enhancedSlot || enhancedCard;
    if (!host || $('.endscreen-settings', host)) return;

    const container = document.createElement('div');
    container.className = 'ytp-plus-settings-item endscreen-settings';
    container.innerHTML = `
        <div>
          <label class="ytp-plus-settings-item-label">${YouTubeUtils.t('endscreenHideLabel')}</label>
          <div class="ytp-plus-settings-item-description">${YouTubeUtils.t('endscreenHideDesc')}${state.removeCount ? ` (${state.removeCount} ${YouTubeUtils.t('removedSuffix').replace('{n}', '')?.trim() || 'removed'})` : ''}</div>
        </div>
        <input type="checkbox" class="ytp-plus-settings-checkbox" ${CONFIG.enabled ? 'checked' : ''}>
      `;

    if (enhancedSlot) {
      enhancedSlot.replaceWith(container);
    } else {
      host.appendChild(container);
    }
    setupEndscreenSettingsDelegation();
  };

  // Optimized navigation handler
  const handlePageChange = debounce(() => {
    if (location.pathname === '/watch') {
      cleanup();
      requestIdleCallback ? requestIdleCallback(init) : setTimeout(init, 1);
    }
  }, 50);

  // Initialize
  settings.load();

  onDomReady(init);

  const handleSettingsNavClick = e => {
    const { target } = /** @type {{ target: HTMLElement }} */ (e);
    if (target?.dataset?.section === 'advanced') {
      setTimeout(addSettingsUI, 10);
    }
  };

  if (!state.ytNavigateListenerKey) {
    state.ytNavigateListenerKey = YouTubeUtils.cleanupManager.registerListener(
      document,
      'yt-navigate-finish',
      /** @type {EventListener} */ (handlePageChange),
      { passive: true }
    );
  }

  // Settings modal integration — use event instead of MutationObserver
  const settingsModalHandler = () => setTimeout(addSettingsUI, 25);
  document.addEventListener('youtube-plus-settings-modal-opened', settingsModalHandler);

  if (!state.settingsNavListenerKey) {
    state.settingsNavListenerKey = YouTubeUtils.cleanupManager.registerListener(
      document,
      'click',
      handleSettingsNavClick,
      { passive: true, capture: true }
    );
  }
})();

// --- MODULE: playall.js ---

// Play All
(async function () {
  'use strict';

  let featureEnabled = true;
  let stopRandomPlayTimers = null;
  let scheduleApplyRandomPlay = null;
  let addButtonRetryTimer = null;
  let addButtonRetryAttempts = 0;
  const loadFeatureEnabled = () => {
    try {
      const settings = localStorage.getItem('youtube_plus_settings');
      if (settings) {
        const parsed = JSON.parse(settings);
        return parsed.enablePlayAll !== false;
      }
    } catch {}
    return true;
  };
  const setFeatureEnabled = nextEnabled => {
    featureEnabled = nextEnabled !== false;
    if (!featureEnabled) {
      try {
        removeButton();
      } catch {}
      try {
        if (addButtonRetryTimer) clearTimeout(addButtonRetryTimer);
        addButtonRetryTimer = null;
        addButtonRetryAttempts = 0;
      } catch {}
      try {
        if (typeof stopRandomPlayTimers === 'function') stopRandomPlayTimers();
      } catch {}
    } else {
      try {
        queueDesktopAddButton();
      } catch {}
      try {
        if (typeof scheduleApplyRandomPlay === 'function') scheduleApplyRandomPlay();
      } catch {}
    }
  };

  featureEnabled = loadFeatureEnabled();

  // DOM helpers
  const _getDOMCache = () => typeof window !== 'undefined' && window.YouTubeDOMCache;
  const $ = (sel, ctx) =>
    _getDOMCache()?.querySelector(sel, ctx) || (ctx || document).querySelector(sel);
  const $$ = (sel, ctx) =>
    _getDOMCache()?.querySelectorAll(sel, ctx) ||
    Array.from((ctx || document).querySelectorAll(sel));
  const onDomReady = (() => {
    let ready = document.readyState !== 'loading';
    const queue = [];
    const run = () => {
      ready = true;
      while (queue.length) {
        const cb = queue.shift();
        try {
          cb();
        } catch {}
      }
    };
    if (!ready) document.addEventListener('DOMContentLoaded', run, { once: true });
    return cb => {
      if (ready) cb();
      else queue.push(cb);
    };
  })();

  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    return key;
  };

  const hasTranslation = key => {
    try {
      if (window.YouTubePlusI18n?.hasTranslation) return window.YouTubePlusI18n.hasTranslation(key);
    } catch {}
    return false;
  };

  const getPlayAllLabel = () => {
    if (hasTranslation('playAllButton')) {
      const localized = t('playAllButton');
      if (localized && localized !== 'playAllButton') return localized;
    }
    return 'Play All';
  };

  const getPlayAllAriaLabel = () => {
    const localized = t('enablePlayAllLabel');
    return localized && localized !== 'enablePlayAllLabel' ? localized : getPlayAllLabel();
  };

  const scheduleNonCritical = fn => {
    if (typeof requestIdleCallback === 'function') {
      requestIdleCallback(fn, { timeout: 2000 });
    } else {
      setTimeout(fn, 200);
    }
  };

  /** @type {any} */
  const globalContext =
    typeof unsafeWindow !== 'undefined'
      ? /** @type {any} */ (unsafeWindow)
      : /** @type {any} */ (window);

  const gmInfo = globalContext?.GM_info ?? null;

  const scriptVersion = gmInfo?.script?.version ?? null;
  if (scriptVersion && /-(alpha|beta|dev|test)$/.test(scriptVersion)) {
    try {
      window.YouTubeUtils &&
        YouTubeUtils.logger &&
        YouTubeUtils.logger.info &&
        YouTubeUtils.logger.info(
          '%cytp - YouTube Play All\n',
          'color: #bf4bcc; font-size: 32px; font-weight: bold',
          'You are currently running a test version:',
          scriptVersion
        );
    } catch {}
  }

  if (
    Object.prototype.hasOwnProperty.call(window, 'trustedTypes') &&
    !window.trustedTypes.defaultPolicy
  ) {
    window.trustedTypes.createPolicy('default', { createHTML: string => string });
  }

  const insertStylesSafely = html => {
    try {
      const target = document.head || document.documentElement;
      if (target && typeof target.insertAdjacentHTML === 'function') {
        target.insertAdjacentHTML('beforeend', html);
        return;
      }

      // If head isn't available yet, wait for DOMContentLoaded and insert then.
      const onReady = () => {
        try {
          const t = document.head || document.documentElement;
          if (t && typeof t.insertAdjacentHTML === 'function') {
            t.insertAdjacentHTML('beforeend', html);
          }
        } catch {}
      };
      onDomReady(onReady);
    } catch {}
  };

  scheduleNonCritical(() =>
    insertStylesSafely(`<style>
        .ytp-btn {border-radius: 8px; font-family: 'Roboto', 'Arial', sans-serif; font-size: 1.4rem; line-height: 3.2rem; font-weight: 500; padding: 0 12px; margin-left: 0; user-select: none; white-space: nowrap;}        
        .ytp-btn, .ytp-btn > * {text-decoration: none; cursor: pointer;}        
        .ytp-badge {border-radius: 8px; padding: 0.2em; font-size: 0.8em; vertical-align: top;} 
        .ytp-random-badge, .ytp-random-notice {background-color: #2b66da; color: white;} 
        /* Style Play All as a YouTube chip button */
        .ytp-play-all-btn {display:inline-flex;align-items:center;justify-content:center;height:32px;padding:0 12px;white-space:nowrap;flex-shrink:0;max-width:fit-content;border-radius:8px;font-size:1.4rem;line-height:2rem;font-weight:500;background-color:var(--yt-spec-badge-chip-background,rgba(255,255,255,0.1));color:var(--yt-spec-text-primary,#fff);border:none;transition:background-color .2s;cursor:pointer;text-decoration:none;}
        .ytp-play-all-btn:hover {background-color:var(--yt-spec-badge-chip-background-hover,rgba(255,255,255,0.2));}        
        html:not([dark]) .ytp-play-all-btn {background-color:var(--yt-spec-badge-chip-background,rgba(0,0,0,0.05));color:var(--yt-spec-text-primary,#0f0f0f);}
        html:not([dark]) .ytp-play-all-btn:hover {background-color:var(--yt-spec-badge-chip-background-hover,rgba(0,0,0,0.1));}
        .ytp-button-row-wrapper {width: 100%; display: block; margin: 0 0 0.6rem 0;} 
        .ytp-button-container {display: inline-flex; align-items: center; gap: 0.6em; width: auto; margin: 0; flex-wrap: nowrap; overflow-x: auto; max-width: 100%;} 
        /* Ensure Play All sits inside chip bar container flow */
        ytd-feed-filter-chip-bar-renderer .ytp-play-all-btn,
        yt-chip-cloud-renderer .ytp-play-all-btn,
        chip-bar-view-model.ytChipBarViewModelHost .ytp-play-all-btn,
        .ytp-button-container .ytp-play-all-btn {height:32px;line-height:32px;vertical-align:middle;}
        ytd-rich-grid-renderer .ytp-button-row-wrapper {margin-left: 0;}        
        /* fetch() API introduces a race condition. This hides the occasional duplicate buttons */
        .ytp-play-all-btn ~ .ytp-play-all-btn {display: none;}        
        /* Fix for mobile view */
        ytm-feed-filter-chip-bar-renderer .ytp-btn {margin-right: 12px; padding: 0.4em;}        
        body:has(#secondary ytd-playlist-panel-renderer[ytp-random]) .ytp-prev-button.ytp-button, body:has(#secondary ytd-playlist-panel-renderer[ytp-random]) .ytp-next-button.ytp-button:not([ytp-random="applied"]) {display: none !important;}        
        #secondary ytd-playlist-panel-renderer[ytp-random] ytd-menu-renderer.ytd-playlist-panel-renderer {height: 1em; visibility: hidden;}        
        #secondary ytd-playlist-panel-renderer[ytp-random]:not(:hover) ytd-playlist-panel-video-renderer {filter: blur(2em);} 
        #secondary ytd-playlist-panel-renderer[ytp-random] #header {display: flex; align-items: center; gap: 8px; flex-wrap: nowrap;}       
        .ytp-random-notice {padding: 0.3em 0.7em; z-index: 1000; white-space: nowrap;}        
    </style>`)
  );

  const getVideoId = url => {
    try {
      return new URLSearchParams(new URL(url).search).get('v');
    } catch {
      return null;
    }
  };

  const queryHTMLElement = selector => {
    const el = $(selector);
    return el instanceof HTMLElement ? el : null;
  };

  /**
   * @typedef {HTMLDivElement & {
   *   getProgressState: () => { current: number, duration: number, number: number },
   *   pauseVideo: () => void,
   *   seekTo: (seconds: number, allowSeekAhead?: boolean) => void,
   *   isLifaAdPlaying: () => boolean
   * }} PlayerElement
   */

  /**
   * @return {{ getProgressState: () => { current: number, duration, number }, pauseVideo: () => void, seekTo: (number) => void, isLifaAdPlaying: () => boolean }} player
   */
  const getPlayer = () => /** @type {PlayerElement | null} */ ($('#movie_player'));

  const isAdPlaying = () => !!$('.ad-interrupting');

  const redirect = (v, list, ytpRandom = null) => {
    if (location.host === 'm.youtube.com') {
      // Mobile: use direct navigation
      const url = `/watch?v=${v}&list=${list}${ytpRandom !== null ? `&ytp-random=${ytpRandom}` : ''}`;
      window.location.href = url;
    } else {
      // Desktop: try YouTube's client-side routing first, with fallback
      try {
        const playlistPanel = $('ytd-playlist-panel-renderer #items');
        if (playlistPanel) {
          const redirector = document.createElement('a');
          redirector.className = 'yt-simple-endpoint style-scope ytd-playlist-panel-video-renderer';
          redirector.setAttribute('hidden', '');
          redirector.data = {
            commandMetadata: {
              webCommandMetadata: {
                url: `/watch?v=${v}&list=${list}${ytpRandom !== null ? `&ytp-random=${ytpRandom}` : ''}`,
                webPageType: 'WEB_PAGE_TYPE_WATCH',
                rootVe: 3832, // ??? required though
              },
            },
            watchEndpoint: {
              videoId: v,
              playlistId: list,
            },
          };
          playlistPanel.append(redirector);
          redirector.click();
        } else {
          // Fallback: use direct navigation if playlist panel not found
          const url = `/watch?v=${v}&list=${list}${ytpRandom !== null ? `&ytp-random=${ytpRandom}` : ''}`;
          window.location.href = url;
        }
      } catch {
        // Fallback: use direct navigation on error
        const url = `/watch?v=${v}&list=${list}${ytpRandom !== null ? `&ytp-random=${ytpRandom}` : ''}`;
        window.location.href = url;
      }
    }
  };

  let id = '';
  const apply = (retryCount = 0) => {
    if (id === '') {
      // do not apply prematurely, caused by mutation observer
      console.warn('[Play All] Channel ID not yet determined');
      return;
    }

    let parent = null;
    if (location.host === 'm.youtube.com') {
      parent = queryHTMLElement(
        'ytm-feed-filter-chip-bar-renderer .chip-bar-contents, ytm-feed-filter-chip-bar-renderer > div'
      );
    } else {
      // Use document.querySelector directly to bypass the DOM cache, which can
      // return a stale null when the chip bar renders after the first apply() call.
      // Use chip-bar-view-model.ytChipBarViewModelHost as primary (new 2026 UI),
      // matching the reference script at greasyfork.org/ru/scripts/490557.
      const desktopParentSelectors = [
        'chip-bar-view-model.ytChipBarViewModelHost',
        'ytd-feed-filter-chip-bar-renderer iron-selector#chips',
        'ytd-feed-filter-chip-bar-renderer #chips-wrapper',
        'yt-chip-cloud-renderer #chips',
        'yt-chip-cloud-renderer .yt-chip-cloud-renderer',
      ];

      for (const selector of desktopParentSelectors) {
        const candidate = document.querySelector(selector);
        if (candidate instanceof HTMLElement) {
          parent = candidate;
          break;
        }
      }
    }

    // #5: add a custom container for buttons if chip bar not found
    if (parent === null) {
      const grid = queryHTMLElement(
        'ytd-rich-grid-renderer, ytm-rich-grid-renderer, div.ytChipBarViewModelChipWrapper'
      );
      if (!grid) {
        // Grid not yet rendered — retry (handles SPA navigation timing)
        if (retryCount < 12) {
          setTimeout(() => apply(retryCount + 1), 300);
        }
        return;
      }

      // Also search inside the grid for chip bar in case it is a child
      const chipBarInGrid = grid.querySelector(
        'chip-bar-view-model.ytChipBarViewModelHost, ytd-feed-filter-chip-bar-renderer iron-selector#chips, ytd-feed-filter-chip-bar-renderer #chips-wrapper, yt-chip-cloud-renderer #chips'
      );
      if (chipBarInGrid instanceof HTMLElement) {
        parent = chipBarInGrid;
      } else if (retryCount < 8) {
        // Chip bar not rendered yet — wait and retry (up to ~2.4s total)
        setTimeout(() => apply(retryCount + 1), 300);
        return;
      } else {
        // Last resort: insert a wrapper at the top of the grid
        let existingContainer = grid.querySelector('.ytp-button-container');
        if (!existingContainer) {
          grid.insertAdjacentHTML('afterbegin', '<div class="ytp-button-container"></div>');
          existingContainer = grid.querySelector('.ytp-button-container');
        }
        parent = existingContainer instanceof HTMLElement ? existingContainer : null;
      }
    }

    if (!parent) {
      console.warn('[Play All] Could not find parent container');
      return;
    }

    // Prevent duplicate buttons
    if (parent.querySelector('.ytp-play-all-btn')) {
      try {
        window.YouTubeUtils &&
          YouTubeUtils.logger &&
          YouTubeUtils.logger.debug &&
          YouTubeUtils.logger.debug('[Play All] Buttons already exist, skipping');
      } catch {}
      return;
    }

    // See: available-lists.md
    const [allPlaylist] = window.location.pathname.endsWith('/videos')
      ? // Normal videos
        // list=UU<ID> adds shorts into the playlist, list=UULF<ID> has videos without shorts
        ['UULF']
      : // Shorts
        window.location.pathname.endsWith('/shorts')
        ? ['UUSH']
        : // Live streams
          ['UULV'];

    const playlistSuffix = id.startsWith('UC') ? id.substring(2) : id;

    // Insert button directly into the container (chip bar or fallback wrapper)
    parent.insertAdjacentHTML(
      'beforeend',
      `<a class="ytp-btn ytp-play-all-btn" href="/playlist?list=${allPlaylist}${playlistSuffix}&playnext=1&ytp-random=random&ytp-random-initial=1" title="${getPlayAllAriaLabel()}" aria-label="${getPlayAllAriaLabel()}">${getPlayAllLabel()}</a>`
    );

    const navigate = href => {
      window.location.assign(href);
    };

    if (location.host === 'm.youtube.com') {
      // Use event delegation for mobile buttons
      if (!parent.hasAttribute('data-ytp-delegated')) {
        parent.setAttribute('data-ytp-delegated', 'true');
        parent.addEventListener('click', event => {
          const btn = event.target.closest('.ytp-btn');
          if (btn && btn.href) {
            event.preventDefault();
            navigate(btn.href);
          }
        });
      }
    } else {
      // Use event delegation for desktop buttons
      if (!parent.hasAttribute('data-ytp-delegated')) {
        parent.setAttribute('data-ytp-delegated', 'true');
        parent.addEventListener('click', event => {
          const btn = event.target.closest('.ytp-play-all-btn');
          if (btn && btn.href) {
            event.preventDefault();
            event.stopPropagation();
            navigate(btn.href);
          }
        });
      }
    }
  };

  let observerFrame = 0;
  const runObserverWork = () => {
    observerFrame = 0;
    if (!featureEnabled) return;
    removeButton();
    apply();
  };

  const observer = new MutationObserver(() => {
    if (!featureEnabled) return;
    if (observerFrame) return;
    if (typeof requestAnimationFrame === 'function') {
      observerFrame = requestAnimationFrame(runObserverWork);
      return;
    }
    observerFrame = setTimeout(runObserverWork, 16);
  });

  const addButton = async () => {
    observer.disconnect();

    if (!featureEnabled) return;

    if (
      !(
        window.location.pathname.endsWith('/videos') ||
        window.location.pathname.endsWith('/shorts') ||
        window.location.pathname.endsWith('/streams')
      )
    ) {
      return;
    }

    // Regenerate button if switched between Latest and Popular.
    // Observe the grid (attribute changes when chip selection changes) and
    // also observe chip-bar-view-model directly for the new 2026 UI.
    const observeTarget =
      document.querySelector('ytd-rich-grid-renderer') ||
      document.querySelector('chip-bar-view-model.ytChipBarViewModelHost') ||
      $(
        'ytm-feed-filter-chip-bar-renderer .iron-selected, ytm-feed-filter-chip-bar-renderer .chip-bar-contents .selected'
      );
    if (observeTarget) {
      observer.observe(observeTarget, {
        attributes: true,
        childList: false,
        subtree: false,
      });
    }

    // This check is necessary for the mobile Interval
    if ($('.ytp-play-all-btn')) {
      return;
    }

    // Try to extract channel ID from canonical link first
    try {
      const canonical = $('link[rel="canonical"]');
      if (canonical && canonical.href) {
        const match = canonical.href.match(/\/channel\/(UC[a-zA-Z0-9_-]{22})/);
        if (match && match[1]) {
          id = match[1];
          apply();
          return;
        }

        // Also try @handle format
        const handleMatch = canonical.href.match(/\/@([^\/]+)/);
        if (handleMatch) {
          // Try to get channel ID from page data
          const pageData = $('ytd-browse[page-subtype="channels"]');
          if (pageData) {
            const channelId = pageData.getAttribute('channel-id');
            if (channelId && channelId.startsWith('UC')) {
              id = channelId;
              apply();
              return;
            }
          }
        }
      }
    } catch (e) {
      console.warn('[Play All] Error extracting channel ID from canonical:', e);
    }

    // Fallback: fetch HTML and parse
    try {
      const html = await (await fetch(location.href)).text();
      const canonicalMatch = html.match(
        /<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/(UC[a-zA-Z0-9_-]{22})"/
      );

      if (canonicalMatch && canonicalMatch[1]) {
        id = canonicalMatch[1];
      } else {
        // Try alternative extraction methods
        const channelIdMatch = html.match(/"channelId":"(UC[a-zA-Z0-9_-]{22})"/);
        if (channelIdMatch && channelIdMatch[1]) {
          id = channelIdMatch[1];
        }
      }

      if (id) {
        apply();
      } else {
        console.warn('[Play All] Could not extract channel ID');
      }
    } catch (e) {
      console.error('[Play All] Error fetching channel data:', e);
    }
  };

  const stopAddButtonRetries = () => {
    if (addButtonRetryTimer) clearTimeout(addButtonRetryTimer);
    addButtonRetryTimer = null;
    addButtonRetryAttempts = 0;
  };

  const queueDesktopAddButton = (reset = true) => {
    if (location.host === 'm.youtube.com') {
      addButton();
      return;
    }

    if (reset) {
      stopAddButtonRetries();
    }

    const run = () => {
      if (!featureEnabled) {
        stopAddButtonRetries();
        return;
      }

      if (
        !(
          window.location.pathname.endsWith('/videos') ||
          window.location.pathname.endsWith('/shorts') ||
          window.location.pathname.endsWith('/streams')
        )
      ) {
        stopAddButtonRetries();
        return;
      }

      addButton();

      if (document.querySelector('.ytp-play-all-btn')) {
        stopAddButtonRetries();
        return;
      }

      if (addButtonRetryAttempts >= 14) {
        stopAddButtonRetries();
        return;
      }

      addButtonRetryAttempts += 1;
      addButtonRetryTimer = setTimeout(run, 350);
    };

    run();
  };

  // Removing the button prevents it from still existing when switching between "Videos", "Shorts", and "Live"
  // This is necessary due to the mobile Interval requiring a check for an already existing button
  const removeButton = () => {
    $$('.ytp-play-all-btn, .ytp-random-badge, .ytp-random-notice').forEach(element =>
      element.remove()
    );
  };

  if (location.host === 'm.youtube.com') {
    // The "yt-navigate-finish" event does not fire on mobile
    // Detect URL changes via pushState/replaceState override + popstate (lightweight)
    let lastUrl = location.href;
    const checkUrlChange = () => {
      if (location.href !== lastUrl) {
        lastUrl = location.href;
        addButton();
      }
    };
    // Use centralized pushState/replaceState event from utils.js
    window.addEventListener('ytp-history-navigate', () => setTimeout(checkUrlChange, 50), {
      passive: true,
    });
    window.addEventListener('popstate', checkUrlChange, { passive: true });
    // Initial call
    addButton();
  } else {
    window.addEventListener('yt-navigate-start', () => {
      stopAddButtonRetries();
      removeButton();
    });
    window.addEventListener('yt-navigate-finish', () =>
      setTimeout(() => queueDesktopAddButton(), 120)
    );
    window.addEventListener('pageshow', () => setTimeout(() => queueDesktopAddButton(), 120));
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') {
        queueDesktopAddButton();
      }
    });
    // Also attempt to add buttons on initial script run in case the SPA navigation event
    // already happened before this script was loaded (some browsers/firefox timing).
    try {
      setTimeout(() => queueDesktopAddButton(), 300);
    } catch {}
  }

  window.addEventListener('youtube-plus-settings-updated', e => {
    try {
      const nextEnabled = e?.detail?.enablePlayAll !== false;
      if (nextEnabled === featureEnabled) return;
      setFeatureEnabled(nextEnabled);
    } catch {
      setFeatureEnabled(loadFeatureEnabled());
    }
  });

  // Random play feature
  (() => {
    // Random play is not supported for mobile devices
    if (location.host === 'm.youtube.com') {
      return;
    }

    const getParams = () => new URLSearchParams(window.location.search);

    /** @returns {{ params: URLSearchParams, mode: 'random', list: string, storageKey: string } | null} */
    const getRandomConfig = () => {
      const params = getParams();
      const modeParam = params.get('ytp-random');
      if (!modeParam || modeParam === '0') return null;
      const list = params.get('list') || '';
      if (!list) return null;

      return { params, mode: 'random', list, storageKey: `ytp-random-${list}` };
    };

    const getStorage = storageKey => {
      try {
        return JSON.parse(localStorage.getItem(storageKey) || '{}');
      } catch {
        return {};
      }
    };

    const isWatched = (storageKey, videoId) => getStorage(storageKey)[videoId] || false;
    const markWatched = (storageKey, videoId) => {
      localStorage.setItem(
        storageKey,
        JSON.stringify({ ...getStorage(storageKey), [videoId]: true })
      );
      document
        .querySelectorAll('#wc-endpoint[href*=zsA3X40nz9w]')
        .forEach(element => element.parentElement.setAttribute('hidden', ''));
    };

    const playNextRandom = (cfg, reload = false) => {
      const playerInstance = getPlayer();
      if (playerInstance && typeof playerInstance.pauseVideo === 'function') {
        playerInstance.pauseVideo();
      }

      const videos = Object.entries(getStorage(cfg.storageKey)).filter(([_, watched]) => !watched);
      const params = new URLSearchParams(window.location.search);

      if (videos.length === 0) {
        return;
      }

      let videoIndex = Math.floor(Math.random() * videos.length);

      // Safety clamp in case of unexpected edge cases
      if (videoIndex < 0) videoIndex = 0;
      if (videoIndex >= videos.length) videoIndex = videos.length - 1;

      if (reload) {
        params.set('v', videos[videoIndex][0]);
        params.set('ytp-random', cfg.mode);
        params.delete('t');
        params.delete('index');
        params.delete('ytp-random-initial');
        window.location.href = `${window.location.pathname}?${params.toString()}`;
      } else {
        // Use the redirect() function for consistent navigation
        try {
          redirect(videos[videoIndex][0], params.get('list'), cfg.mode);
        } catch (error) {
          console.error(
            '[Play All] Error using redirect(), falling back to manual redirect:',
            error
          );
          // Fallback to manual redirect if the redirect() function fails
          const redirector = document.createElement('a');
          redirector.className = 'yt-simple-endpoint style-scope ytd-playlist-panel-video-renderer';
          redirector.setAttribute('hidden', '');
          redirector.data = {
            commandMetadata: {
              webCommandMetadata: {
                url: `/watch?v=${videos[videoIndex][0]}&list=${params.get('list')}&ytp-random=${cfg.mode}`,
                webPageType: 'WEB_PAGE_TYPE_WATCH',
                rootVe: 3832,
              },
            },
            watchEndpoint: {
              videoId: videos[videoIndex][0],
              playlistId: params.get('list'),
            },
          };
          const listContainer = $('ytd-playlist-panel-renderer #items');
          if (listContainer instanceof HTMLElement) {
            listContainer.append(redirector);
          } else {
            document.body.appendChild(redirector);
          }
          redirector.click();
        }
      }
    };

    let applyRetryTimeoutId = null;
    let progressIntervalId = null;

    stopRandomPlayTimers = () => {
      if (applyRetryTimeoutId) clearTimeout(applyRetryTimeoutId);
      applyRetryTimeoutId = null;
      // progressIntervalId is now a boolean or event listener, not a timer
      if (progressIntervalId && typeof progressIntervalId !== 'boolean') {
        clearInterval(progressIntervalId);
      }
      progressIntervalId = null;
    };

    const applyRandomPlay = cfg => {
      if (!featureEnabled) return;
      if (!window.location.pathname.endsWith('/watch')) return;

      const playlistContainer = $('#secondary ytd-playlist-panel-renderer');
      if (playlistContainer === null) {
        return;
      }
      if (playlistContainer.hasAttribute('ytp-random')) {
        return;
      }

      playlistContainer.setAttribute('ytp-random', 'applied');
      const headerContainer = playlistContainer.querySelector('#header');
      if (headerContainer && !headerContainer.querySelector('.ytp-random-notice')) {
        headerContainer.insertAdjacentHTML(
          'beforeend',
          `<span class="ytp-random-notice">Play All mode</span>`
        );
      }

      const storage = getStorage(cfg.storageKey);

      // Robustly collect playlist anchors - different YT layouts use different selectors
      const anchorSelectors = [
        '#wc-endpoint',
        'ytd-playlist-panel-video-renderer a#wc-endpoint',
        'ytd-playlist-panel-video-renderer a',
        'a#video-title',
        '#secondary ytd-playlist-panel-renderer a[href*="/watch?"]',
      ];

      const anchors = [];
      anchorSelectors.forEach(sel => {
        playlistContainer.querySelectorAll(sel).forEach(a => {
          if (a instanceof Element && a.tagName === 'A') anchors.push(/** @type {any} */ (a));
        });
      });

      // Deduplicate by href
      const uniq = [];
      const seen = new Set();
      anchors.forEach(a => {
        const href = a.href || a.getAttribute('href') || '';
        if (!seen.has(href)) {
          seen.add(href);
          uniq.push(a);
        }
      });

      const navigate = href => (window.location.href = href);

      // Mark videos and prepare links
      uniq.forEach(element => {
        let videoId = null;
        try {
          videoId = new URL(element.href, window.location.origin).searchParams.get('v');
        } catch {
          videoId = new URLSearchParams(element.search || '').get('v');
        }

        if (!videoId) return;

        if (!isWatched(cfg.storageKey, videoId)) {
          storage[videoId] = false;
        }

        // Ensure ytp-random param present
        try {
          const u = new URL(element.href, window.location.origin);
          u.searchParams.set('ytp-random', cfg.mode);
          element.href = u.toString();
        } catch {}

        element.setAttribute('data-ytp-random-link', 'true');

        const entryKey = getVideoId(element.href);
        if (isWatched(cfg.storageKey, entryKey)) {
          element.parentElement?.setAttribute('hidden', '');
        }
      });

      // Use event delegation for video links
      if (playlistContainer && !playlistContainer.hasAttribute('data-ytp-random-delegated')) {
        playlistContainer.setAttribute('data-ytp-random-delegated', 'true');
        playlistContainer.addEventListener('click', event => {
          const link = event.target.closest('a[data-ytp-random-link]');
          if (link && link.href) {
            event.preventDefault();
            navigate(link.href);
          }
        });
      }
      localStorage.setItem(cfg.storageKey, JSON.stringify(storage));

      if (
        cfg.params.get('ytp-random-initial') === '1' ||
        isWatched(cfg.storageKey, getVideoId(location.href))
      ) {
        playNextRandom(cfg);

        return;
      }

      const header = playlistContainer.querySelector('h3 a');
      if (header && header.tagName === 'A') {
        const anchorHeader = /** @type {HTMLAnchorElement} */ (/** @type {unknown} */ (header));
        anchorHeader.insertAdjacentHTML(
          'beforeend',
          ` <span class="ytp-badge ytp-random-badge">Play All <span style="font-size: 2rem; vertical-align: top">&times;</span></span>`
        );
        anchorHeader.href = '#';
        const badge = anchorHeader.querySelector('.ytp-random-badge');
        if (badge) {
          badge.addEventListener('click', event => {
            event.preventDefault();

            localStorage.removeItem(cfg.storageKey);

            const params = new URLSearchParams(location.search);
            params.delete('ytp-random');
            window.location.href = `${window.location.pathname}?${params.toString()}`;
          });
        }
      }

      document.addEventListener(
        'keydown',
        event => {
          // SHIFT + N
          if (event.shiftKey && event.key.toLowerCase() === 'n') {
            event.stopImmediatePropagation();
            event.preventDefault();

            const videoId = getVideoId(location.href);
            markWatched(cfg.storageKey, videoId);
            // Unfortunately there is no workaround to YouTube redirecting to the next in line without a reload
            playNextRandom(cfg, true);
          }
        },
        true
      );

      if (progressIntervalId) return;

      // Use video timeupdate event instead of setInterval for better performance
      const videoEl = $('video');
      if (!videoEl) return;

      const handleProgress = () => {
        const videoId = getVideoId(location.href);

        const params = new URLSearchParams(location.search);
        params.set('ytp-random', cfg.mode);
        window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`);

        const player = getPlayer();
        if (!player || typeof player.getProgressState !== 'function') {
          return;
        }

        const progressState = player.getProgressState();
        if (
          !progressState ||
          typeof progressState.current !== 'number' ||
          typeof progressState.duration !== 'number'
        ) {
          return;
        }

        // Do not listen for watch progress when watching advertisements
        if (!isAdPlaying()) {
          if (progressState.current / progressState.duration >= 0.9) {
            if (videoId) markWatched(cfg.storageKey, videoId);
          }

          // Autoplay random video
          if (progressState.current >= progressState.duration - 2) {
            // make sure vanilla autoplay doesnt take over
            if (typeof player.pauseVideo === 'function') player.pauseVideo();
            if (typeof player.seekTo === 'function') player.seekTo(0);
            playNextRandom(cfg);
          }
        }

        const nextButton = $('#ytd-player .ytp-next-button.ytp-button:not([ytp-random="applied"])');
        if (nextButton instanceof HTMLElement) {
          // Replace with span to prevent anchor click events
          const newButton = document.createElement('span');
          newButton.className = nextButton.className;
          newButton.innerHTML = nextButton.innerHTML;
          nextButton.replaceWith(newButton);

          newButton.setAttribute('ytp-random', 'applied');
          newButton.addEventListener('click', () => {
            if (videoId) markWatched(cfg.storageKey, videoId);
            playNextRandom(cfg);
          });
        }
      };

      videoEl.addEventListener('timeupdate', handleProgress, { passive: true });
      progressIntervalId = true; // Mark as initialized
    };

    scheduleApplyRandomPlay = (attempt = 0) => {
      if (!featureEnabled) return;
      stopRandomPlayTimers();

      if (!window.location.pathname.endsWith('/watch')) return;

      const cfg = getRandomConfig();
      if (!cfg) return;

      // Storage needs to now be { [videoId]: bool }
      try {
        const current = localStorage.getItem(cfg.storageKey);
        if (current && Array.isArray(JSON.parse(current))) {
          localStorage.removeItem(cfg.storageKey);
        }
      } catch {
        localStorage.removeItem(cfg.storageKey);
      }

      applyRandomPlay(cfg);

      // If the playlist panel isn't ready yet, retry a few times (no always-on polling)
      if (attempt >= 30) return;
      applyRetryTimeoutId = setTimeout(() => scheduleApplyRandomPlay(attempt + 1), 250);
    };

    const onNavigate = () => {
      if (!featureEnabled) {
        stopRandomPlayTimers();
        return;
      }
      stopRandomPlayTimers();
      scheduleApplyRandomPlay();
    };

    onNavigate();
    window.addEventListener('yt-navigate-finish', () => setTimeout(onNavigate, 200));
  })();
})().catch(error =>
  console.error(
    '%cytp - YouTube Play All\n',
    'color: #bf4bcc; font-size: 32px; font-weight: bold',
    error
  )
);

// --- MODULE: time.js ---

// Time to Read (Resume Playback)
(function () {
  'use strict';

  let featureEnabled = true;
  let activeCleanup = null;
  const loadFeatureEnabled = () => {
    try {
      const settings = localStorage.getItem('youtube_plus_settings');
      if (settings) {
        const parsed = JSON.parse(settings);
        return parsed.enableResumeTime !== false;
      }
    } catch {}
    return true;
  };
  const setFeatureEnabled = nextEnabled => {
    featureEnabled = nextEnabled !== false;
    if (!featureEnabled) {
      const existingOverlay = byId(OVERLAY_ID);
      if (existingOverlay) {
        try {
          existingOverlay.remove();
        } catch {}
      }
      if (typeof activeCleanup === 'function') {
        try {
          activeCleanup();
        } catch {}
        activeCleanup = null;
      }
    } else {
      try {
        initResume();
      } catch {}
    }
  };

  featureEnabled = loadFeatureEnabled();

  // DOM helpers
  const _getDOMCache = () => typeof window !== 'undefined' && window.YouTubeDOMCache;
  const $ = (sel, ctx) =>
    _getDOMCache()?.querySelector(sel, ctx) || (ctx || document).querySelector(sel);
  const byId = id => _getDOMCache()?.getElementById(id) || document.getElementById(id);
  const onDomReady = (() => {
    let ready = document.readyState !== 'loading';
    const queue = [];
    const run = () => {
      ready = true;
      while (queue.length) {
        const cb = queue.shift();
        try {
          cb();
        } catch {}
      }
    };
    if (!ready) document.addEventListener('DOMContentLoaded', run, { once: true });
    return cb => {
      if (ready) cb();
      else queue.push(cb);
    };
  })();

  const setupResumeDelegation = (() => {
    let attached = false;
    return () => {
      if (attached) return;
      attached = true;

      const delegator = window.YouTubePlusEventDelegation;
      const handler = (ev, target) => {
        const action = target?.dataset?.ytpResumeAction;
        if (!action) return;
        const wrap = target.closest('.ytp-resume-overlay');
        if (!wrap) return;

        if (action === 'resume') {
          wrap.dispatchEvent(new CustomEvent('ytp:resume', { bubbles: true }));
        } else if (action === 'restart') {
          wrap.dispatchEvent(new CustomEvent('ytp:restart', { bubbles: true }));
        }
      };

      if (delegator?.on) {
        delegator.on(document, 'click', '.ytp-resume-btn', handler);
        delegator.on(document, 'keydown', '.ytp-resume-btn', (ev, target) => {
          if (ev.key === 'Enter' || ev.key === ' ') {
            ev.preventDefault();
            handler(ev, target);
          }
        });
      } else {
        document.addEventListener(
          'click',
          ev => {
            const target = ev.target?.closest?.('.ytp-resume-btn');
            if (target) handler(ev, target);
          },
          true
        );
        document.addEventListener(
          'keydown',
          ev => {
            const target = ev.target?.closest?.('.ytp-resume-btn');
            if (!target) return;
            if (ev.key === 'Enter' || ev.key === ' ') {
              ev.preventDefault();
              handler(ev, target);
            }
          },
          true
        );
      }
    };
  })();

  const RESUME_STORAGE_KEY = 'youtube_resume_times_v1';
  const OVERLAY_ID = 'yt-resume-overlay';
  const AUTO_HIDE_MS = 10000; // hide overlay after 10s

  // Localization: prefer centralized i18n with local fallback for critical keys
  const _localFallback = {
    resumePlayback: { en: 'Resume playback?', ru: 'Продолжить воспроизведение?' },
    resume: { en: 'Resume', ru: 'Продолжить' },
    startOver: { en: 'Start over', ru: 'Начать сначала' },
  };

  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);

    // Fallback to local tiny map for this module's critical keys
    const htmlLang = document.documentElement.lang || 'en';
    const lang = htmlLang.startsWith('ru') ? 'ru' : 'en';
    const val = _localFallback[key]?.[lang] || _localFallback[key]?.en || key;

    if (!params || Object.keys(params).length === 0) return val;
    let result = val;
    for (const [k, v] of Object.entries(params)) {
      result = result.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
    }
    return result;
  };

  const readStorage = () => {
    try {
      return JSON.parse(localStorage.getItem(RESUME_STORAGE_KEY) || '{}');
    } catch {
      return {};
    }
  };

  const writeStorage = obj => {
    try {
      localStorage.setItem(RESUME_STORAGE_KEY, JSON.stringify(obj));
    } catch {}
  };

  // Get current video id from the page (works on standard watch pages)
  const getVideoId = () => {
    try {
      // First try URL parameters (most reliable)
      const urlParams = new URLSearchParams(window.location.search);
      const videoIdFromUrl = urlParams.get('v');
      if (videoIdFromUrl) return videoIdFromUrl;

      // Try canonical link
      const meta = $('link[rel="canonical"]');
      if (meta && meta.href) {
        const u = new URL(meta.href);
        const vParam = u.searchParams.get('v');
        if (vParam) return vParam;

        // Try extracting from pathname (for /watch/ or /shorts/ URLs)
        const pathMatch = u.pathname.match(/\/(watch|shorts)\/([^\/\?]+)/);
        if (pathMatch && pathMatch[2]) return pathMatch[2];
      }

      // Fallback to ytInitialPlayerResponse
      if (
        window.ytInitialPlayerResponse &&
        window.ytInitialPlayerResponse.videoDetails &&
        window.ytInitialPlayerResponse.videoDetails.videoId
      ) {
        return window.ytInitialPlayerResponse.videoDetails.videoId;
      }

      // Last resort: try to extract from current URL pathname
      const pathMatch = window.location.pathname.match(/\/(watch|shorts)\/([^\/\?]+)/);
      if (pathMatch && pathMatch[2]) return pathMatch[2];

      return null;
    } catch {
      return null;
    }
  };

  const createOverlay = (seconds, onResume, onRestart) => {
    if (byId(OVERLAY_ID)) return null;
    const wrap = document.createElement('div');
    wrap.id = OVERLAY_ID;

    // Try to insert overlay inside the player so it appears above the progress bar
    const player = $('#movie_player');
    const inPlayer = !!player;

    // Ensure glassmorphism styles are available for the overlay
    const resumeOverlayStyles = `
      .ytp-resume-overlay{min-width:180px;max-width:36vw;background:rgba(24, 24, 24, 0.3);color:var(--yt-spec-text-primary,#fff);padding:12px 14px;border-radius:12px;backdrop-filter:blur(8px) saturate(150%);-webkit-backdrop-filter:blur(8px) saturate(150%);box-shadow:0 14px 40px rgba(0,0,0,0.48);border:1.25px solid rgba(255,255,255,0.06);font-family:Arial,Helvetica,sans-serif;display:flex;flex-direction:column;align-items:center;text-align:center;animation:ytp-resume-fadein 0.3s ease-out}
      @keyframes ytp-resume-fadein{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
      .ytp-resume-overlay .ytp-resume-title{font-weight:600;margin-bottom:8px;font-size:13px}
      .ytp-resume-overlay .ytp-resume-actions{display:flex;gap:8px;justify-content:center;margin-top:6px}
      .ytp-resume-overlay .ytp-resume-btn{padding:6px 12px;border-radius:8px;border:none;cursor:pointer;font-size:12px;font-weight:500;transition:all 0.2s ease;outline:none}
      .ytp-resume-overlay .ytp-resume-btn:focus{box-shadow:0 0 0 2px rgba(255,255,255,0.3);outline:2px solid transparent}
      .ytp-resume-overlay .ytp-resume-btn:hover{transform:translateY(-1px)}
      .ytp-resume-overlay .ytp-resume-btn:active{transform:translateY(0)}
      .ytp-resume-overlay .ytp-resume-btn.primary{background:#1e88e5;color:#fff}
      .ytp-resume-overlay .ytp-resume-btn.primary:hover{background:#1976d2}
      .ytp-resume-overlay .ytp-resume-btn.ghost{background:rgba(255,255,255,0.06);color:#fff}
      .ytp-resume-overlay .ytp-resume-btn.ghost:hover{background:rgba(255,255,255,0.12)}
    `;
    try {
      if (window.YouTubeUtils && YouTubeUtils.StyleManager) {
        YouTubeUtils.StyleManager.add('ytp-resume-overlay-styles', resumeOverlayStyles);
      } else if (!byId('ytp-resume-overlay-styles')) {
        const s = document.createElement('style');
        s.id = 'ytp-resume-overlay-styles';
        s.textContent = resumeOverlayStyles;
        (document.head || document.documentElement).appendChild(s);
      }
    } catch {}

    if (inPlayer) {
      try {
        // Ensure player can be a positioning context
        const playerStyle = window.getComputedStyle(
          /** @type {Element} */ (/** @type {unknown} */ (player))
        );
        if (playerStyle.position === 'static') player.style.position = 'relative';
      } catch {}

      // Position centered inside the player
      wrap.className = 'ytp-resume-overlay';
      // absolute center (use transform to center by both axes)
      wrap.style.cssText =
        'position:absolute;left:50%;bottom:5%;transform:translate(-50%,-50%);z-index:9999;pointer-events:auto;';
      player.appendChild(wrap);
    } else {
      // Fallback: fixed centered on the page
      wrap.className = 'ytp-resume-overlay';
      wrap.style.cssText =
        'position:fixed;left:50%;bottom:5%;transform:translate(-50%,-50%);z-index:1200;pointer-events:auto;';
      document.body.appendChild(wrap);
    }

    const title = document.createElement('div');
    title.className = 'ytp-resume-title';
    title.textContent = `${t('resumePlayback')} (${formatTime(seconds)})`;

    const btnResume = document.createElement('button');
    btnResume.className = 'ytp-resume-btn primary';
    btnResume.textContent = t('resume');
    btnResume.setAttribute('aria-label', `${t('resume')} at ${formatTime(seconds)}`);
    btnResume.tabIndex = 0;
    btnResume.dataset.ytpResumeAction = 'resume';

    const btnRestart = document.createElement('button');
    btnRestart.className = 'ytp-resume-btn ghost';
    btnRestart.textContent = t('startOver');
    btnRestart.setAttribute('aria-label', t('startOver'));
    btnRestart.tabIndex = 0;
    btnRestart.dataset.ytpResumeAction = 'restart';

    const handleResume = () => {
      try {
        onResume();
      } catch (err) {
        console.error('[YouTube+] Resume error:', err);
      }
      try {
        wrap.remove();
      } catch {}
    };

    const handleRestart = () => {
      try {
        onRestart();
      } catch (err) {
        console.error('[YouTube+] Restart error:', err);
      }
      try {
        wrap.remove();
      } catch {}
    };

    setupResumeDelegation();

    wrap.addEventListener('ytp:resume', () => handleResume(), { once: true });
    wrap.addEventListener('ytp:restart', () => handleRestart(), { once: true });

    // group actions and center them
    const actions = document.createElement('div');
    actions.className = 'ytp-resume-actions';
    actions.appendChild(btnResume);
    actions.appendChild(btnRestart);

    wrap.appendChild(title);
    wrap.appendChild(actions);

    // Set focus to primary button for keyboard accessibility
    try {
      requestAnimationFrame(() => {
        btnResume.focus();
      });
    } catch {}

    const to = setTimeout(() => {
      try {
        wrap.remove();
      } catch {}
    }, AUTO_HIDE_MS);

    // Return function to cancel timeout
    const cancel = () => clearTimeout(to);

    // Register cleanup: cancel timeout and remove overlay when cleanup runs
    if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
      YouTubeUtils.cleanupManager.register(() => {
        try {
          cancel();
        } catch {}
        try {
          wrap.remove();
        } catch {}
      });
    }

    return cancel;
  };

  const formatTime = secs => {
    const s = Math.floor(secs % 60)
      .toString()
      .padStart(2, '0');
    const m = Math.floor((secs / 60) % 60).toString();
    const h = Math.floor(secs / 3600);
    return h ? `${h}:${m.padStart(2, '0')}:${s}` : `${m}:${s}`;
  };

  const attachResumeHandlers = videoEl => {
    if (!featureEnabled) return null;
    if (!videoEl || videoEl.tagName !== 'VIDEO') {
      console.warn('[YouTube+] Invalid video element for resume handlers');
      return;
    }

    // Mark element to prevent duplicate handlers
    if (videoEl._ytpResumeAttached) return;
    videoEl._ytpResumeAttached = true;

    // Get current video ID dynamically each time
    const getCurrentVideoId = () => getVideoId();
    const vid = getCurrentVideoId();
    if (!vid) return;

    const storage = readStorage();
    const saved = storage[vid];

    // Save current time using `timeupdate` event (throttled) instead of interval
    let timeUpdateHandler = null;
    let lastSavedAt = 0;
    const SAVE_THROTTLE_MS = 800; // minimum ms between writes

    const startSaving = () => {
      if (timeUpdateHandler) return;
      timeUpdateHandler = () => {
        try {
          // Get current video ID each time we save
          const currentVid = getCurrentVideoId();
          if (!currentVid) return;

          const t = Math.floor(videoEl.currentTime || 0);
          const now = Date.now();
          if (t && (!lastSavedAt || now - lastSavedAt > SAVE_THROTTLE_MS)) {
            const s = readStorage();
            s[currentVid] = t;
            writeStorage(s);
            lastSavedAt = now;
          }
        } catch {}
      };
      videoEl.addEventListener('timeupdate', timeUpdateHandler, { passive: true });

      // register cleanup to remove listener
      if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
        YouTubeUtils.cleanupManager.register(() => {
          try {
            videoEl.removeEventListener('timeupdate', timeUpdateHandler);
          } catch {}
        });
      }
    };

    const stopSaving = () => {
      if (!timeUpdateHandler) return;
      try {
        videoEl.removeEventListener('timeupdate', timeUpdateHandler);
      } catch {}
      timeUpdateHandler = null;
      lastSavedAt = 0;
    };

    // If saved time exists and is > 5s, show overlay
    if (saved && saved > 5 && !byId(OVERLAY_ID)) {
      const cancelTimeout = createOverlay(
        saved,
        () => {
          try {
            videoEl.currentTime = saved;
            videoEl.play();
          } catch {}
        },
        () => {
          try {
            videoEl.currentTime = 0;
            videoEl.play();
          } catch {}
        }
      );

      // Tag overlay with current video id so future init calls won't immediately remove it
      try {
        const overlayEl = byId(OVERLAY_ID);
        if (overlayEl && vid) overlayEl.dataset.vid = vid;
      } catch {}

      // register cleanup for overlay timeout
      if (window.YouTubeUtils && YouTubeUtils.cleanupManager && cancelTimeout) {
        YouTubeUtils.cleanupManager.register(cancelTimeout);
      }
    }

    // Start saving when playing
    const onPlay = () => startSaving();
    const onPause = () => stopSaving();
    videoEl.addEventListener('play', onPlay, { passive: true });
    videoEl.addEventListener('pause', onPause, { passive: true });

    // Cleanup listeners when needed
    const cleanupHandlers = () => {
      try {
        videoEl.removeEventListener('play', onPlay);
        videoEl.removeEventListener('pause', onPause);
        if (timeUpdateHandler) {
          videoEl.removeEventListener('timeupdate', timeUpdateHandler);
        }
        delete videoEl._ytpResumeAttached;
      } catch (err) {
        console.error('[YouTube+] Resume cleanup error:', err);
      }
    };

    if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
      YouTubeUtils.cleanupManager.register(cleanupHandlers);
    }

    // Return cleanup function
    activeCleanup = cleanupHandlers;
    return cleanupHandlers;
  };

  // Try to find the primary HTML5 video element on the YouTube watch page
  const findVideoElement = () => {
    // Try multiple selectors for better compatibility
    const selectors = [
      'video.html5-main-video',
      'video.video-stream',
      '#movie_player video',
      'video',
    ];

    for (const selector of selectors) {
      const video = $(selector);
      if (video && video.tagName === 'VIDEO') {
        return /** @type {HTMLVideoElement} */ (video);
      }
    }

    return null;
  };

  const initResume = () => {
    if (!featureEnabled) {
      const existingOverlay = byId(OVERLAY_ID);
      if (existingOverlay) {
        try {
          existingOverlay.remove();
        } catch {}
      }
      return;
    }
    // Only run on watch pages
    if (window.location.pathname !== '/watch') {
      // Remove overlay if we navigate away from watch page
      const existingOverlay = byId(OVERLAY_ID);
      if (existingOverlay) {
        existingOverlay.remove();
      }
      return;
    }

    // Remove any existing overlay from previous video — but keep it if it's for the same video id
    const currentVid = getVideoId();
    const existingOverlay = byId(OVERLAY_ID);
    if (existingOverlay) {
      try {
        if (existingOverlay.dataset && existingOverlay.dataset.vid === currentVid) {
          // overlay matches current video; keep it (prevents immediate disappearance during SPA re-inits)
        } else {
          existingOverlay.remove();
        }
      } catch {
        try {
          existingOverlay.remove();
        } catch {}
      }
    }

    const videoEl = findVideoElement();
    if (videoEl) {
      attachResumeHandlers(videoEl);
    } else {
      // Retry after a short delay if video not found yet
      setTimeout(initResume, 500);
    }
  };

  // Listen for navigation events used by YouTube SPA
  const onNavigate = () => setTimeout(initResume, 150);

  onDomReady(initResume);

  // YouTube internal navigation event
  if (window && window.document) {
    // Prefer custom event registered in other modules
    if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
      YouTubeUtils.cleanupManager.registerListener(document, 'yt-navigate-finish', onNavigate, {
        passive: true,
      });
    } else {
      document.addEventListener('yt-navigate-finish', onNavigate, { passive: true });
    }
  }

  window.addEventListener('youtube-plus-settings-updated', e => {
    try {
      const nextEnabled = e?.detail?.enableResumeTime !== false;
      if (nextEnabled === featureEnabled) return;
      setFeatureEnabled(nextEnabled);
    } catch {
      setFeatureEnabled(loadFeatureEnabled());
    }
  });
})();

// --- MODULE: zoom.js ---

// --- Zoom UI with wheel, pinch and keyboard support ---
(function () {
  'use strict';

  let featureEnabled = true;
  const loadFeatureEnabled = () => {
    try {
      const settings = localStorage.getItem('youtube_plus_settings');
      if (settings) {
        const parsed = JSON.parse(settings);
        return parsed.enableZoom !== false;
      }
    } catch {}
    return true;
  };
  const clearZoomUI = () => {
    try {
      const ui = byId('ytp-zoom-control');
      if (ui) ui.remove();
    } catch {}
    try {
      const styles = byId('ytp-zoom-styles');
      if (styles) styles.remove();
    } catch {}
    try {
      const video = findVideoElement();
      if (video) {
        video.style.transform = '';
        video.style.willChange = '';
        video.style.transition = '';
        video.style.cursor = '';
      }
    } catch {}
  };
  const setFeatureEnabled = nextEnabled => {
    featureEnabled = nextEnabled !== false;
    if (!featureEnabled) {
      clearZoomUI();
    } else {
      try {
        initZoom();
      } catch {}
    }
  };

  featureEnabled = loadFeatureEnabled();

  // DOM helpers
  const _getDOMCache = () => typeof window !== 'undefined' && window.YouTubeDOMCache;
  const $ = (sel, ctx) =>
    _getDOMCache()?.querySelector(sel, ctx) || (ctx || document).querySelector(sel);
  const byId = id => _getDOMCache()?.getElementById(id) || document.getElementById(id);

  const ZOOM_PAN_STORAGE_KEY = 'ytp_zoom_pan';
  const RESTORE_LOG_KEY = 'ytp_zoom_restore_log'; // stored in sessionStorage for debugging
  const DEFAULT_ZOOM = 1;
  const MIN_ZOOM = 0.5;
  const MAX_ZOOM = 2.5;
  const ZOOM_STEP = 0.05;
  // Fullscreen apply timing (ms) and retries — make configurable if needed
  const FULLSCREEN_APPLY_DELAY = 80;
  const FULLSCREEN_APPLY_RETRIES = 4;
  const FULLSCREEN_APPLY_RETRY_DELAY = 120;

  // Helpers for combined zoom+pan storage
  function readZoomPan() {
    try {
      const raw = localStorage.getItem(ZOOM_PAN_STORAGE_KEY);
      if (!raw) return { zoom: DEFAULT_ZOOM, panX: 0, panY: 0 };
      const obj = JSON.parse(raw);
      const zoom = Number(obj && obj.zoom) || DEFAULT_ZOOM;
      const panX = Number(obj && obj.panX) || 0;
      const panY = Number(obj && obj.panY) || 0;
      return { zoom, panX, panY };
    } catch {
      return { zoom: DEFAULT_ZOOM, panX: 0, panY: 0 };
    }
  }

  function saveZoomPan(zoom, panX, panY) {
    try {
      const obj = {
        zoom: Number(zoom) || DEFAULT_ZOOM,
        panX: Number(panX) || 0,
        panY: Number(panY) || 0,
      };
      localStorage.setItem(ZOOM_PAN_STORAGE_KEY, JSON.stringify(obj));
    } catch {}
  }

  function logRestoreEvent(evt) {
    try {
      const entry = Object.assign({ time: new Date().toISOString() }, evt);
      try {
        const raw = sessionStorage.getItem(RESTORE_LOG_KEY);
        const arr = raw ? JSON.parse(raw) : [];
        arr.push(entry);
        // keep last 200 entries
        if (arr.length > 200) arr.splice(0, arr.length - 200);
        sessionStorage.setItem(RESTORE_LOG_KEY, JSON.stringify(arr));
      } catch {
        // fallback: ignore
      }
      // Console output for live debugging (only when debug mode is active)
      if ((typeof window !== 'undefined' && window.YTP_DEBUG) || window.YouTubePlusConfig?.debug) {
        console.warn('[YouTube+] Zoom restore:', entry);
      }
    } catch {}
  }

  const findVideoElement = () => {
    const selectors = ['#movie_player video', 'video.video-stream', 'video'];
    for (const s of selectors) {
      const v = $(s);
      if (v && v.tagName === 'VIDEO') return /** @type {HTMLVideoElement} */ (v);
    }
    return null;
  };

  // Transform tracking state (module scope so helpers can access it)
  let _lastTransformApplied = '';
  let _isApplyingTransform = false;

  const applyZoomToVideo = (
    videoEl,
    zoom,
    panX = 0,
    panY = 0,
    skipTransformTracking = false,
    skipTransition = false
  ) => {
    if (!videoEl) return;
    const container = videoEl.parentElement || videoEl;
    try {
      // Set flag to prevent observer loops
      if (!skipTransformTracking) {
        _isApplyingTransform = true;
      }

      // Ensure container can display overflow content
      container.style.overflow = 'visible';
      if (!container.style.position || container.style.position === 'static') {
        container.style.position = 'relative';
      }

      // Set transform origin to center for natural zoom
      videoEl.style.transformOrigin = 'center center';

      // Apply transform with proper precision
      const transformStr = `translate(${panX.toFixed(2)}px, ${panY.toFixed(2)}px) scale(${zoom.toFixed(3)})`;
      videoEl.style.transform = transformStr;

      // Track the transform we just applied
      if (!skipTransformTracking) {
        _lastTransformApplied = transformStr;
      }

      // Use will-change for GPU acceleration
      videoEl.style.willChange = zoom !== 1 ? 'transform' : 'auto';

      // Smooth transition for better UX (skip during fullscreen transitions to avoid flicker)
      videoEl.style.transition = skipTransition ? 'none' : 'transform .08s ease-out';

      // Reset flag after a short delay
      if (!skipTransformTracking) {
        setTimeout(() => {
          _isApplyingTransform = false;
        }, 100);
      }
    } catch (e) {
      console.error('[YouTube+] applyZoomToVideo error:', e);
      _isApplyingTransform = false;
    }
  };

  function createZoomUI() {
    const player = $('#movie_player');
    if (!player) return null;
    if (byId('ytp-zoom-control')) {
      return byId('ytp-zoom-control');
    }

    // styles (minimal)
    if (!byId('ytp-zoom-styles')) {
      const s = document.createElement('style');
      s.id = 'ytp-zoom-styles';
      s.textContent = `
      /* Compact control bar matching YouTube control style */
      #ytp-zoom-control{position: absolute; left: 12px; bottom: 64px; z-index: 2200; display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 24px; background: rgba(0,0,0,0.35); color: #fff; font-size: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.5); backdrop-filter: blur(6px);}
      #ytp-zoom-control input[type=range]{width: 120px; -webkit-appearance: none; background: transparent; height: 24px;}
      /* WebKit track */
      #ytp-zoom-control input[type=range]::-webkit-slider-runnable-track{height: 4px; background: rgba(255,255,255,0.12); border-radius: 3px;}
      #ytp-zoom-control input[type=range]::-webkit-slider-thumb{-webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 6px rgba(255,255,255,0.06); margin-top: -4px;}
      /* Firefox */
      #ytp-zoom-control input[type=range]::-moz-range-track{height: 4px; background: rgba(255,255,255,0.12); border-radius: 3px;}
      #ytp-zoom-control input[type=range]::-moz-range-thumb{width: 12px; height: 12px; border-radius: 50%; background: #fff; border: none;}
      #ytp-zoom-control .zoom-label{min-width:36px;text-align:center;font-size:11px;padding:0 6px;user-select:none}
      #ytp-zoom-control::after{content:'Shift + Wheel to zoom';position:absolute;bottom:100%;right:0;padding:4px 8px;background:rgba(0,0,0,0.8);color:#fff;font-size:10px;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;transform:translateY(4px);transition:opacity .2s,transform .2s}
      #ytp-zoom-control:hover::after{opacity:1;transform:translateY(-4px)}
      #ytp-zoom-control .zoom-reset{background: rgba(255,255,255,0.06); border: none; color: inherit; padding: 4px; display: flex; align-items: center; justify-content: center; border-radius: 50%; cursor: pointer; width: 28px; height: 28px;}
      #ytp-zoom-control .zoom-reset:hover{background: rgba(255,255,255,0.12)}
      #ytp-zoom-control .zoom-reset svg{display:block;width:14px;height:14px}
      /* Hidden state to mirror YouTube controls autohide */
      #ytp-zoom-control.ytp-hidden{opacity:0;transform:translateY(6px);pointer-events:none}
      #ytp-zoom-control{transition:opacity .18s ease, transform .18s ease}
    `;
      (document.head || document.documentElement).appendChild(s);
    }

    const wrap = document.createElement('div');
    wrap.id = 'ytp-zoom-control';

    const input = document.createElement('input');
    input.type = 'range';
    input.min = String(MIN_ZOOM);
    input.max = String(MAX_ZOOM);
    input.step = String(ZOOM_STEP);

    const label = document.createElement('div');
    label.className = 'zoom-label';
    label.setAttribute('role', 'status');
    label.setAttribute('aria-live', 'polite');
    label.setAttribute('aria-label', 'Current zoom level');

    const reset = document.createElement('button');
    reset.className = 'zoom-reset';
    reset.type = 'button';
    reset.setAttribute('aria-label', 'Reset zoom');
    reset.title = 'Reset zoom';
    reset.innerHTML = `
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M12 4V1l-5 5 5 5V7a7 7 0 1 1-7 7" stroke="currentColor" stroke-width="2" fill="none"/>
    </svg>
  `;

    wrap.appendChild(input);
    wrap.appendChild(label);
    wrap.appendChild(reset);

    let video = findVideoElement();
    const stored = readZoomPan().zoom;
    const initZoomVal = Number.isFinite(stored) && !Number.isNaN(stored) ? stored : DEFAULT_ZOOM;

    const setZoom = z => {
      const clamped = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Number(z)));
      input.value = String(clamped);
      const percentage = Math.round(clamped * 100);
      label.textContent = `${percentage}%`;
      label.setAttribute('aria-label', `Current zoom level ${percentage} percent`);

      if (video) {
        // clamp pan to new zoom limits
        clampPan(clamped);

        // Use RAF for smooth animation
        requestAnimationFrame(() => {
          try {
            applyZoomToVideo(video, clamped, panX, panY);
            // update cursor depending on zoom
            try {
              video.style.cursor = clamped > 1 ? 'grab' : '';
            } catch {}
          } catch (err) {
            console.error('[YouTube+] Apply zoom error:', err);
          }
        });
      }

      try {
        saveZoomPan(clamped, panX, panY);
      } catch (err) {
        console.error('[YouTube+] Save zoom error:', err);
      }
    };

    input.addEventListener('input', e => setZoom(e.target.value));
    reset.addEventListener('click', () => {
      try {
        panX = 0;
        panY = 0;
        setZoom(DEFAULT_ZOOM);
        // persist reset pan immediately
        try {
          // set via combined storage
          saveZoomPan(DEFAULT_ZOOM, 0, 0);
        } catch {}
        // Provide visual feedback
        reset.style.transform = 'scale(0.9)';
        setTimeout(() => {
          reset.style.transform = '';
        }, 150);
      } catch (err) {
        console.error('[YouTube+] Reset zoom error:', err);
      }
    });

    // Wheel: Shift + wheel to zoom (with throttling for performance)
    let wheelThrottleTimer = null;
    // Throttled pan save timer to avoid excessive localStorage writes
    let panSaveTimer = null;
    const scheduleSavePan = () => {
      try {
        if (panSaveTimer) clearTimeout(panSaveTimer);
        panSaveTimer = setTimeout(() => {
          try {
            const currentZoom = parseFloat(input.value) || readZoomPan().zoom || DEFAULT_ZOOM;
            saveZoomPan(currentZoom, panX, panY);
          } catch (err) {
            console.error('[YouTube+] Save pan error:', err);
          }
          panSaveTimer = null;
        }, 220);
      } catch (err) {
        console.error('[YouTube+] Schedule save pan error:', err);
      }
    };
    const wheelHandler = ev => {
      try {
        if (!featureEnabled) return;
        if (!ev.shiftKey) return;
        ev.preventDefault();

        // Throttle wheel events to prevent excessive zoom changes
        if (wheelThrottleTimer) return;

        wheelThrottleTimer = setTimeout(() => {
          wheelThrottleTimer = null;
        }, 50); // 50ms throttle

        // Normalize wheel delta for consistent behavior across browsers
        const delta = ev.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
        const current = readZoomPan().zoom || DEFAULT_ZOOM;
        const newZoom = current + delta;

        // Only zoom if within bounds
        if (newZoom >= MIN_ZOOM && newZoom <= MAX_ZOOM) {
          setZoom(newZoom);
        }
      } catch (err) {
        console.error('[YouTube+] Wheel zoom error:', err);
      }
    };
    // Attach wheel handler to player and video (if present) so it works over controls
    player.addEventListener('wheel', wheelHandler, { passive: false });
    if (video) {
      try {
        video.addEventListener('wheel', wheelHandler, { passive: false });
      } catch (err) {
        console.error('[YouTube+] Failed to attach wheel handler to video:', err);
      }
    }

    // Keyboard +/- (ignore when typing)
    const keydownHandler = ev => {
      try {
        if (!featureEnabled) return;
        const active = document.activeElement;
        if (
          active &&
          (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)
        ) {
          return;
        }
        if (ev.key === '+' || ev.key === '=') {
          ev.preventDefault();
          const current = readZoomPan().zoom || DEFAULT_ZOOM;
          setZoom(Math.min(MAX_ZOOM, current + ZOOM_STEP));
        } else if (ev.key === '-') {
          ev.preventDefault();
          const current = readZoomPan().zoom || DEFAULT_ZOOM;
          setZoom(Math.max(MIN_ZOOM, current - ZOOM_STEP));
        }
      } catch {}
    };
    window.addEventListener('keydown', keydownHandler);

    // Pinch-to-zoom using Pointer Events
    // Panning (drag) state
    let panX = 0;
    let panY = 0;
    // Observer to watch for external changes to the video's style (YouTube may override transform)
    let videoStyleObserver = null;

    let dragging = false;
    let dragStartX = 0;
    let dragStartY = 0;
    let dragStartPanX = 0;
    let dragStartPanY = 0;

    const clampPan = (zoom = readZoomPan().zoom) => {
      try {
        if (!video) return;
        const container = video.parentElement || video;
        if (!container) return;

        const containerRect = container.getBoundingClientRect();
        if (!containerRect || containerRect.width === 0 || containerRect.height === 0) return;

        // Get actual video dimensions respecting aspect ratio
        const baseW = video.videoWidth || video.offsetWidth || containerRect.width;
        const baseH = video.videoHeight || video.offsetHeight || containerRect.height;

        // Validate dimensions
        if (!baseW || !baseH || !Number.isFinite(baseW) || !Number.isFinite(baseH)) return;

        // Calculate scaled dimensions
        const scaledW = baseW * zoom;
        const scaledH = baseH * zoom;

        // Calculate maximum pan distance (how far content can move)
        const maxX = Math.max(0, (scaledW - containerRect.width) / 2);
        const maxY = Math.max(0, (scaledH - containerRect.height) / 2);

        // Clamp pan values with validation
        if (Number.isFinite(maxX) && Number.isFinite(panX)) {
          panX = Math.max(-maxX, Math.min(maxX, panX));
        }
        if (Number.isFinite(maxY) && Number.isFinite(panY)) {
          panY = Math.max(-maxY, Math.min(maxY, panY));
        }
      } catch (err) {
        console.error('[YouTube+] Clamp pan error:', err);
      }
    };

    const pointers = new Map();
    let initialPinchDist = null;
    let pinchStartZoom = null;
    let prevTouchAction = null;
    const getDistance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);

    const pointerDown = ev => {
      try {
        if (!featureEnabled) return;
        pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
        try {
          ev.target.setPointerCapture(ev.pointerId);
        } catch {}
        // Start mouse drag for panning when single mouse pointer and zoomed in.
        // Skip at default zoom so we don't interfere with YouTube's native
        // hold-left-mouse-button → 2× speed feature.
        try {
          const currentZoom = parseFloat(input.value) || readZoomPan().zoom || DEFAULT_ZOOM;
          if (
            ev.pointerType === 'mouse' &&
            ev.button === 0 &&
            pointers.size <= 1 &&
            video &&
            currentZoom > 1
          ) {
            dragging = true;
            dragStartX = ev.clientX;
            dragStartY = ev.clientY;
            dragStartPanX = panX;
            dragStartPanY = panY;
            try {
              video.style.cursor = 'grabbing';
            } catch {}
          }
        } catch {}
        if (pointers.size === 2) {
          const pts = Array.from(pointers.values());
          initialPinchDist = getDistance(pts[0], pts[1]);
          pinchStartZoom = readZoomPan().zoom;
          prevTouchAction = player.style.touchAction;
          try {
            player.style.touchAction = 'none';
          } catch {}
        }
      } catch {}
    };

    const pointerMove = ev => {
      try {
        if (!featureEnabled) return;
        // Update pointers map
        if (pointers.has(ev.pointerId)) {
          pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
        }

        // If dragging with mouse, pan the video
        if (dragging && ev.pointerType === 'mouse' && video) {
          const dx = ev.clientX - dragStartX;
          const dy = ev.clientY - dragStartY;
          // Movement should be independent of scale; adjust if desired
          panX = dragStartPanX + dx;
          panY = dragStartPanY + dy;
          // clamp pan to allowed bounds
          clampPan();
          applyZoomToVideo(video, parseFloat(input.value) || DEFAULT_ZOOM, panX, panY);
          // schedule persisting pan
          scheduleSavePan();
          ev.preventDefault();
          return;
        }

        // Pinch-to-zoom when two pointers
        if (pointers.size === 2 && initialPinchDist && pinchStartZoom != null) {
          const pts = Array.from(pointers.values());
          const dist = getDistance(pts[0], pts[1]);
          if (dist <= 0) return;
          const ratio = dist / initialPinchDist;
          const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, pinchStartZoom * ratio));
          setZoom(newZoom);
          ev.preventDefault();
        }
      } catch {}
    };

    const pointerUp = ev => {
      try {
        if (!featureEnabled) return;
        pointers.delete(ev.pointerId);
        try {
          ev.target.releasePointerCapture(ev.pointerId);
        } catch {}
        // stop dragging
        try {
          if (dragging && ev.pointerType === 'mouse') {
            dragging = false;
            try {
              if (video) video.style.cursor = parseFloat(input.value) > 1 ? 'grab' : '';
            } catch {}
          }
        } catch {}
        if (pointers.size < 2) {
          initialPinchDist = null;
          pinchStartZoom = null;
          if (prevTouchAction != null) {
            try {
              player.style.touchAction = prevTouchAction;
            } catch {}
            prevTouchAction = null;
          }
        }
      } catch {}
    };

    player.addEventListener('pointerdown', pointerDown, { passive: true });
    player.addEventListener('pointermove', pointerMove, { passive: false });
    player.addEventListener('pointerup', pointerUp, { passive: true });
    player.addEventListener('pointercancel', pointerUp, { passive: true });

    // Touch event fallback for browsers that don't fully support Pointer Events
    // Enables pinch-to-zoom and one-finger pan on touchscreens
    let touchDragging = false;
    let touchDragStartX = 0;
    let touchDragStartY = 0;
    let touchDragStartPanX = 0;
    let touchDragStartPanY = 0;
    let touchInitialDist = null;
    let touchPinchStartZoom = null;

    const getTouchDistance = (t1, t2) =>
      Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);

    const touchStart = ev => {
      try {
        if (!featureEnabled) return;
        if (!video) return;
        if (ev.touches.length === 1) {
          // start pan only if zoomed in
          const currentZoom = parseFloat(input.value) || readZoomPan().zoom || DEFAULT_ZOOM;
          if (currentZoom > 1) {
            touchDragging = true;
            touchDragStartX = ev.touches[0].clientX;
            touchDragStartY = ev.touches[0].clientY;
            touchDragStartPanX = panX;
            touchDragStartPanY = panY;
            // prevent page scroll when panning video
            ev.preventDefault();
          }
        } else if (ev.touches.length === 2) {
          // pinch start
          touchInitialDist = getTouchDistance(ev.touches[0], ev.touches[1]);
          touchPinchStartZoom = parseFloat(input.value) || readZoomPan().zoom || DEFAULT_ZOOM;
          // prevent default gestures (scroll/zoom) while pinching
          try {
            prevTouchAction = player.style.touchAction;
            player.style.touchAction = 'none';
          } catch {}
          ev.preventDefault();
        }
      } catch (e) {
        console.error('[YouTube+] touchStart error:', e);
      }
    };

    const touchMove = ev => {
      try {
        if (!featureEnabled) return;
        if (!video) return;
        if (ev.touches.length === 1 && touchDragging) {
          const dx = ev.touches[0].clientX - touchDragStartX;
          const dy = ev.touches[0].clientY - touchDragStartY;
          panX = touchDragStartPanX + dx;
          panY = touchDragStartPanY + dy;
          clampPan();
          applyZoomToVideo(video, parseFloat(input.value) || DEFAULT_ZOOM, panX, panY);
          scheduleSavePan();
          ev.preventDefault();
          return;
        }

        if (ev.touches.length === 2 && touchInitialDist && touchPinchStartZoom != null) {
          const dist = getTouchDistance(ev.touches[0], ev.touches[1]);
          if (dist <= 0) return;
          const ratio = dist / touchInitialDist;
          const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, touchPinchStartZoom * ratio));
          setZoom(newZoom);
          ev.preventDefault();
        }
      } catch (e) {
        console.error('[YouTube+] touchMove error:', e);
      }
    };

    const touchEnd = ev => {
      try {
        if (!featureEnabled) return;
        if (touchDragging && ev.touches.length === 0) {
          touchDragging = false;
        }
        if (ev.touches.length < 2) {
          touchInitialDist = null;
          touchPinchStartZoom = null;
          if (prevTouchAction != null) {
            try {
              player.style.touchAction = prevTouchAction;
            } catch {}
            prevTouchAction = null;
          }
        }
      } catch (e) {
        console.error('[YouTube+] touchEnd error:', e);
      }
    };

    try {
      // Use non-passive handlers so we can preventDefault when needed
      player.addEventListener('touchstart', touchStart, { passive: false });
      player.addEventListener('touchmove', touchMove, { passive: false });
      player.addEventListener('touchend', touchEnd, { passive: true });
      player.addEventListener('touchcancel', touchEnd, { passive: true });
    } catch (e) {
      console.error('[YouTube+] Failed to attach touch handlers:', e);
    }

    // Fallback mouse handlers for more reliable dragging on desktop
    const mouseDownHandler = ev => {
      try {
        if (!featureEnabled) return;
        if (ev.button !== 0 || !video) return;
        // Only intercept mousedown (and call preventDefault) when actually zoomed in.
        // At default zoom (1×) we must NOT call preventDefault() because it breaks
        // YouTube's native hold-left-mouse-button → 2× speed feature.
        const currentZoom = parseFloat(input.value) || readZoomPan().zoom || DEFAULT_ZOOM;
        if (currentZoom <= 1) return;
        dragging = true;
        dragStartX = ev.clientX;
        dragStartY = ev.clientY;
        dragStartPanX = panX;
        dragStartPanY = panY;
        try {
          video.style.cursor = 'grabbing';
        } catch {}
        ev.preventDefault();
      } catch {}
    };

    const mouseMoveHandler = ev => {
      try {
        if (!featureEnabled) return;
        if (!dragging || !video) return;

        const dx = ev.clientX - dragStartX;
        const dy = ev.clientY - dragStartY;
        panX = dragStartPanX + dx;
        panY = dragStartPanY + dy;
        clampPan();

        // Use RAF to avoid excessive repaints
        if (!video._panRAF) {
          video._panRAF = requestAnimationFrame(() => {
            applyZoomToVideo(video, parseFloat(input.value) || DEFAULT_ZOOM, panX, panY);
            // persist pan after RAF'd update
            scheduleSavePan();
            video._panRAF = null;
          });
        }

        ev.preventDefault();
      } catch (err) {
        console.error('[YouTube+] Mouse move error:', err);
      }
    };

    const mouseUpHandler = _ev => {
      try {
        if (!featureEnabled) return;
        if (dragging) {
          dragging = false;
          try {
            if (video) video.style.cursor = parseFloat(input.value) > 1 ? 'grab' : '';
          } catch {}
        }
      } catch {}
    };

    if (video) {
      try {
        video.addEventListener('mousedown', mouseDownHandler);
      } catch {}
      try {
        window.addEventListener('mousemove', mouseMoveHandler);
      } catch {}
      try {
        window.addEventListener('mouseup', mouseUpHandler);
      } catch {}
      // Attach style observer to ensure transform isn't clobbered by YouTube
      try {
        const attachStyleObserver = () => {
          try {
            if (videoStyleObserver) {
              try {
                videoStyleObserver.disconnect();
              } catch {}
              videoStyleObserver = null;
            }
            if (!video) return;
            videoStyleObserver = new MutationObserver(muts => {
              try {
                // Skip if we're currently applying a transform
                if (_isApplyingTransform) return;

                for (const m of muts) {
                  if (m.type === 'attributes' && m.attributeName === 'style') {
                    // If transform has been changed externally, restore expected transform
                    const current = (video && video.style && video.style.transform) || '';
                    const expectedZoom =
                      readZoomPan().zoom || parseFloat(input.value) || DEFAULT_ZOOM;
                    const expected = `translate(${panX.toFixed(2)}px, ${panY.toFixed(2)}px) scale(${expectedZoom.toFixed(3)})`;

                    // Only restore if transform was actually changed by YouTube (not by us)
                    // and the current zoom is not default
                    if (
                      expectedZoom !== DEFAULT_ZOOM &&
                      current !== expected &&
                      current !== _lastTransformApplied
                    ) {
                      // Reapply on next frame to minimize layout thrash
                      requestAnimationFrame(() => {
                        try {
                          applyZoomToVideo(video, expectedZoom, panX, panY);
                          try {
                            logRestoreEvent({
                              action: 'restore_transform',
                              currentTransform: current,
                              expectedTransform: expected,
                              zoom: expectedZoom,
                              panX,
                              panY,
                            });
                          } catch {}
                        } catch {}
                      });
                    }
                  }
                }
              } catch {}
            });
            videoStyleObserver.observe(video, { attributes: true, attributeFilter: ['style'] });
          } catch {}
        };
        attachStyleObserver();
      } catch {}
    }

    // If video element is replaced by YouTube (e.g. fullscreen toggle or navigation), rebind handlers
    const playerObserver = new MutationObserver(() => {
      try {
        const newVideo = findVideoElement();
        if (newVideo && newVideo !== video) {
          // Remove listeners from old video
          try {
            if (video) {
              video.removeEventListener('mousedown', mouseDownHandler);
              video.removeEventListener('wheel', wheelHandler);
              if (video._panRAF) {
                cancelAnimationFrame(video._panRAF);
                video._panRAF = null;
              }
            }
          } catch (err) {
            console.error('[YouTube+] Error detaching from old video:', err);
          }

          // Update reference
          video = newVideo;

          // Reattach style observer for the new video element
          try {
            if (videoStyleObserver) {
              try {
                videoStyleObserver.disconnect();
              } catch {}
              videoStyleObserver = null;
            }
            if (video) {
              videoStyleObserver = new MutationObserver(muts => {
                try {
                  // Skip if we're currently applying a transform
                  if (_isApplyingTransform) return;

                  for (const m of muts) {
                    if (m.type === 'attributes' && m.attributeName === 'style') {
                      const current = (video && video.style && video.style.transform) || '';
                      const expectedZoom =
                        readZoomPan().zoom || parseFloat(input.value) || DEFAULT_ZOOM;
                      const expected = `translate(${panX.toFixed(2)}px, ${panY.toFixed(2)}px) scale(${expectedZoom.toFixed(3)})`;

                      // Only restore if transform was actually changed by YouTube (not by us)
                      // and the current zoom is not default
                      if (
                        expectedZoom !== DEFAULT_ZOOM &&
                        current !== expected &&
                        current !== _lastTransformApplied
                      ) {
                        requestAnimationFrame(() => {
                          try {
                            applyZoomToVideo(video, expectedZoom, panX, panY);
                            try {
                              logRestoreEvent({
                                action: 'restore_transform',
                                currentTransform: current,
                                expectedTransform: expected,
                                zoom: expectedZoom,
                                panX,
                                panY,
                              });
                            } catch {}
                          } catch {}
                        });
                      }
                    }
                  }
                } catch {}
              });
              videoStyleObserver.observe(video, { attributes: true, attributeFilter: ['style'] });
            }
          } catch (err) {
            console.error('[YouTube+] Error attaching style observer to new video:', err);
          }

          // Reapply zoom to the new video
          try {
            const current = readZoomPan().zoom || DEFAULT_ZOOM;
            clampPan(current);
            applyZoomToVideo(video, current, panX, panY);
          } catch (err) {
            console.error('[YouTube+] Error applying zoom to new video:', err);
          }

          // Attach listeners to new video
          try {
            video.addEventListener('mousedown', mouseDownHandler);
          } catch (err) {
            console.error('[YouTube+] Error attaching mousedown to new video:', err);
          }
          try {
            video.addEventListener('wheel', wheelHandler, { passive: false });
          } catch (err) {
            console.error('[YouTube+] Error attaching wheel to new video:', err);
          }
        }
      } catch (err) {
        console.error('[YouTube+] Player observer error:', err);
      }
    });
    try {
      playerObserver.observe(player, { childList: true, subtree: true });
    } catch (err) {
      console.error('[YouTube+] Failed to observe player for video changes:', err);
    }

    // Reapply zoom on fullscreen change since layout may move elements.
    // Use a short timeout to allow YouTube to move/replace the video element
    // when entering/leaving fullscreen, and listen for vendor-prefixed events.
    const fullscreenHandler = () => {
      try {
        const current = readZoomPan().zoom || DEFAULT_ZOOM;
        // Attempt to find/apply multiple times — YouTube may move/replace the video element
        setTimeout(() => {
          try {
            let attempts = 0;
            const tryApply = () => {
              try {
                const newVideo = findVideoElement();
                let swapped = false;
                if (newVideo && newVideo !== video) {
                  // detach from old video listeners safely
                  try {
                    if (video) video.removeEventListener('wheel', wheelHandler);
                  } catch {}

                  video = newVideo;
                  swapped = true;

                  // Reattach wheel handler if needed
                  try {
                    video.addEventListener('wheel', wheelHandler, { passive: false });
                  } catch {}
                }

                clampPan(current);
                // Apply zoom without transition during fullscreen to prevent flicker
                if (video) applyZoomToVideo(video, current, panX, panY, false, true);

                // If we didn't find/replace video yet, retry a few times
                if (!swapped && (!video || attempts < FULLSCREEN_APPLY_RETRIES)) {
                  attempts += 1;
                  setTimeout(tryApply, FULLSCREEN_APPLY_RETRY_DELAY);
                }
              } catch (e) {
                console.error('[YouTube+] Fullscreen apply attempt error:', e);
              }
            };
            tryApply();
          } catch (e) {
            console.error('[YouTube+] Fullscreen inner apply error:', e);
          }
        }, FULLSCREEN_APPLY_DELAY);
      } catch (err) {
        console.error('[YouTube+] Fullscreen handler error:', err);
      }
    };
    [
      'fullscreenchange',
      'webkitfullscreenchange',
      'mozfullscreenchange',
      'MSFullscreenChange',
    ].forEach(evt => document.addEventListener(evt, fullscreenHandler));

    // Apply initial zoom and attach UI
    // Restore stored pan values (if any) and clamp before applying zoom
    try {
      try {
        const s = readZoomPan();
        if (Number.isFinite(s.panX)) panX = s.panX;
        if (Number.isFinite(s.panY)) panY = s.panY;
        // Ensure pan is within limits for the initial zoom
        clampPan(initZoomVal);
      } catch (err) {
        console.error('[YouTube+] Restore pan error:', err);
      }
    } catch (err) {
      console.error('[YouTube+] Initial zoom setup error:', err);
    }

    // Initialize transform tracking with the initial state
    try {
      const initialTransform = `translate(${panX.toFixed(2)}px, ${panY.toFixed(2)}px) scale(${initZoomVal.toFixed(3)})`;
      _lastTransformApplied = initialTransform;
    } catch {}

    setZoom(initZoomVal);
    // Position the zoom control above YouTube's bottom chrome (progress bar / controls).
    const updateZoomPosition = () => {
      try {
        const chrome = player.querySelector('.ytp-chrome-bottom');
        // If chrome exists, place the control just above it; otherwise keep the CSS fallback.
        if (chrome && chrome.offsetHeight) {
          const offset = chrome.offsetHeight + 8; // small gap above controls
          wrap.style.bottom = `${offset}px`;
        } else {
          // fallback to original design value
          wrap.style.bottom = '';
        }
      } catch {
        // ignore positioning errors
      }
    };

    // Initial position and reactive updates for fullscreen / resize / chrome changes
    updateZoomPosition();

    // Use a safe ResizeObserver callback that schedules the actual work on the
    // next animation frame. This reduces the chance of a "ResizeObserver loop
    // completed with undelivered notifications" error caused by synchronous
    // layout work inside the observer callback.
    const ro = new ResizeObserver(_entries => {
      try {
        if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
          requestAnimationFrame(() => {
            try {
              updateZoomPosition();
            } catch (e) {
              try {
                YouTubeUtils &&
                  YouTubeUtils.logError &&
                  YouTubeUtils.logError('Enhanced', 'updateZoomPosition failed', e);
              } catch {}
            }
          });
        } else {
          // fallback
          updateZoomPosition();
        }
      } catch (e) {
        try {
          YouTubeUtils &&
            YouTubeUtils.logError &&
            YouTubeUtils.logError('Enhanced', 'ResizeObserver callback error', e);
        } catch {}
      }
    });

    // Register observer with cleanup manager so it gets disconnected on unload/cleanup
    try {
      if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
        YouTubeUtils.cleanupManager.registerObserver(ro);
      }
    } catch {}

    try {
      const chromeEl = player.querySelector('.ytp-chrome-bottom');
      if (chromeEl) ro.observe(chromeEl);
    } catch (e) {
      try {
        YouTubeUtils &&
          YouTubeUtils.logError &&
          YouTubeUtils.logError('Enhanced', 'Failed to observe chrome element', e);
      } catch {}
    }

    // Keep a window resize listener for fallback positioning
    try {
      window.addEventListener('resize', updateZoomPosition, { passive: true });
      if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
        YouTubeUtils.cleanupManager.registerListener(window, 'resize', updateZoomPosition);
      }
    } catch {}

    // Reposition on fullscreen changes (vendor-prefixed events included)
    [
      'fullscreenchange',
      'webkitfullscreenchange',
      'mozfullscreenchange',
      'MSFullscreenChange',
    ].forEach(evt => {
      try {
        document.addEventListener(evt, updateZoomPosition);
        if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
          YouTubeUtils.cleanupManager.registerListener(document, evt, updateZoomPosition);
        }
      } catch {}
    });

    player.appendChild(wrap);

    // Sync visibility with YouTube controls (autohide)
    const chromeBottom = player.querySelector('.ytp-chrome-bottom');
    const isControlsHidden = () => {
      try {
        // Player class flags
        if (
          player.classList.contains('ytp-autohide') ||
          player.classList.contains('ytp-hide-controls')
        ) {
          return true;
        }
        // Chrome bottom layer opacity/visibility
        if (chromeBottom) {
          const style = window.getComputedStyle(chromeBottom);
          if (
            style &&
            (style.opacity === '0' || style.visibility === 'hidden' || style.display === 'none')
          ) {
            return true;
          }
        }
      } catch {}
      return false;
    };

    const updateHidden = () => {
      try {
        if (isControlsHidden()) {
          wrap.classList.add('ytp-hidden');
        } else {
          wrap.classList.remove('ytp-hidden');
        }
      } catch {}
    };

    // Observe player class changes
    const visObserver = new MutationObserver(() => updateHidden());
    try {
      visObserver.observe(player, { attributes: true, attributeFilter: ['class', 'style'] });
      if (chromeBottom) {
        visObserver.observe(chromeBottom, {
          attributes: true,
          attributeFilter: ['class', 'style'],
        });
      }
    } catch {}

    // Temporary show on mousemove over player (like other controls)
    let showTimer = null;
    const mouseMoveShow = () => {
      try {
        wrap.classList.remove('ytp-hidden');
        if (showTimer) clearTimeout(showTimer);
        showTimer = setTimeout(updateHidden, 2200);
      } catch {}
    };
    player.addEventListener('mousemove', mouseMoveShow, { passive: true });
    // Initial sync
    updateHidden();

    // Cleanup
    const cleanup = () => {
      try {
        // Clear throttle timer
        if (wheelThrottleTimer) {
          clearTimeout(wheelThrottleTimer);
          wheelThrottleTimer = null;
        }

        // Clear pan save timer
        if (panSaveTimer) {
          clearTimeout(panSaveTimer);
          panSaveTimer = null;
        }

        // Cancel pending RAF
        if (video && video._panRAF) {
          cancelAnimationFrame(video._panRAF);
          video._panRAF = null;
        }

        // Remove all event listeners
        player.removeEventListener('wheel', wheelHandler);
        player.removeEventListener('pointerdown', pointerDown);
        player.removeEventListener('pointermove', pointerMove);
        player.removeEventListener('pointerup', pointerUp);
        player.removeEventListener('pointercancel', pointerUp);
        player.removeEventListener('mousemove', mouseMoveShow);
        window.removeEventListener('keydown', keydownHandler);

        if (video) {
          try {
            video.removeEventListener('mousedown', mouseDownHandler);
          } catch {}
          try {
            video.removeEventListener('wheel', wheelHandler);
          } catch {}
          try {
            window.removeEventListener('mousemove', mouseMoveHandler);
          } catch {}
          try {
            window.removeEventListener('mouseup', mouseUpHandler);
          } catch {}
          try {
            // Reset video styles
            video.style.cursor = '';
            video.style.transform = '';
            video.style.willChange = 'auto';
            video.style.transition = '';
          } catch {}
        }

        // Disconnect style observer
        if (videoStyleObserver) {
          try {
            videoStyleObserver.disconnect();
          } catch {}
          videoStyleObserver = null;
        }

        // Disconnect observer
        if (visObserver) {
          try {
            visObserver.disconnect();
          } catch {}
        }
        // Disconnect player mutation observer
        try {
          if (playerObserver) playerObserver.disconnect();
        } catch {}

        // Remove fullscreen handler
        try {
          document.removeEventListener('fullscreenchange', fullscreenHandler);
        } catch {}

        // Clear show timer
        if (showTimer) {
          clearTimeout(showTimer);
          showTimer = null;
        }

        // Remove UI element
        wrap.remove();
      } catch (err) {
        console.error('[YouTube+] Cleanup error:', err);
      }
    };

    if (window.YouTubeUtils && YouTubeUtils.cleanupManager) {
      YouTubeUtils.cleanupManager.register(cleanup);
    }

    return wrap;
  }

  // Guard: track whether the yt-navigate-finish listener was already added so that
  // toggling the zoom feature on/off does not accumulate duplicate listeners.
  let _navigateListenerAdded = false;

  // Call this to initialize zoom (e.g. on page load / SPA navigation)
  function initZoom() {
    try {
      if (!featureEnabled) return;
      const ensure = () => {
        const player = $('#movie_player');
        if (!player) return setTimeout(ensure, 400);
        createZoomUI();
      };
      ensure();
      if (!_navigateListenerAdded) {
        _navigateListenerAdded = true;
        window.addEventListener('yt-navigate-finish', () => setTimeout(() => createZoomUI(), 300));
      }
    } catch {
      console.error('initZoom error');
    }
  }

  window.addEventListener('youtube-plus-settings-updated', e => {
    try {
      const nextEnabled = e?.detail?.enableZoom !== false;
      if (nextEnabled === featureEnabled) return;
      setFeatureEnabled(nextEnabled);
    } catch {
      setFeatureEnabled(loadFeatureEnabled());
    }
  });

  // Ensure initZoom is used to avoid unused-var lint and to initialize feature
  try {
    initZoom();
  } catch {}
})();

// --- MODULE: voting.js ---

/**
 * Feature Voting System
 * Supabase-powered voting via REST API
 */

(function () {
  'use strict';

  if (typeof window === 'undefined') return;

  const SUPABASE_URL = 'https://ldpccocxlrdsyejfhrvc.supabase.co';
  const SUPABASE_KEY =
    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxkcGNjb2N4bHJkc3llamZocnZjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIyMTAyNDYsImV4cCI6MjA4Nzc4NjI0Nn0.QfwrAG4SMJBPLoP-Mcq3hETQXt0ezinoi0CpN57Zn90';

  const PREVIEW_FEATURE_TITLE = '__ytp_preview_vote__';
  const PREVIEW_FEATURE_DESC = 'Internal row for ytp-plus-voting-preview';

  let votingInitialized = false;
  let voteRequestInFlight = false;

  function setVoteControlsBusy(container, busy) {
    if (!container) return;
    container.querySelectorAll('.ytp-plus-vote-btn, .ytp-plus-vote-bar-btn').forEach(el => {
      if (busy) {
        el.setAttribute('aria-disabled', 'true');
        el.style.pointerEvents = 'none';
        el.style.opacity = '0.7';
      } else {
        el.removeAttribute('aria-disabled');
        el.style.pointerEvents = '';
        el.style.opacity = '';
      }
    });
  }

  const t = (key, params = {}) => {
    if (window.YouTubePlusI18n?.t) return window.YouTubePlusI18n.t(key, params);
    if (window.YouTubeUtils?.t) return window.YouTubeUtils.t(key, params);
    return key || '';
  };

  const tf = (key, fallback, params = {}) => {
    try {
      const value = t(key, params);
      if (typeof value === 'string' && value && value !== key) return value;
    } catch {}
    return fallback || key || '';
  };

  function getStatusMeta(status) {
    const normalized = String(status || '').toLowerCase();
    if (normalized === 'completed') {
      return {
        className: 'completed',
        label: tf('statusCompleted', 'Completed'),
      };
    }
    if (normalized === 'in_progress') {
      return {
        className: 'in-progress',
        label: tf('statusInProgress', 'In progress'),
      };
    }
    return {
      className: 'proposed',
      label: tf('statusProposed', 'Proposed'),
    };
  }

  // No fallback feature card — when there are no user feature requests,
  // the list simply shows "No feature requests yet". The preview row in the DB
  // (__ytp_preview_vote__) is used only for the aggregate vote bar.

  function getLocalUserId() {
    let userId = localStorage.getItem('ytp_voting_user_id');
    if (!userId) {
      userId = 'user_' + Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
      localStorage.setItem('ytp_voting_user_id', userId);
    }
    return userId;
  }

  function normalizeVoteType(value) {
    const numeric = Number(value);
    if (numeric === 1) return 1;
    if (numeric === -1) return -1;
    return 0;
  }

  async function supabaseFetch(endpoint, options = {}) {
    const url = `${SUPABASE_URL}/rest/v1/${endpoint}`;
    const headers = {
      apikey: SUPABASE_KEY,
      Authorization: `Bearer ${SUPABASE_KEY}`,
      'Content-Type': 'application/json',
      Prefer: options.prefer || 'return=representation',
    };

    try {
      const response = await fetch(url, {
        ...options,
        headers: { ...headers, ...options.headers },
      });

      if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new Error(error.message || `HTTP ${response.status}`);
      }

      const data = await response.json().catch(() => null);
      return { data, error: null };
    } catch (error) {
      return { data: null, error: error.message };
    }
  }

  async function getFeatures() {
    const { data, error } = await supabaseFetch(
      'ytplus_feature_requests?select=*&order=created_at.desc'
    );
    if (error) {
      console.error('[Voting] Error fetching features:', error);
      return [];
    }
    return data || [];
  }

  async function getAllVotes() {
    const { data, error } = await supabaseFetch(
      'ytplus_feature_votes?select=feature_id,vote_type,ip_address'
    );
    if (error) {
      console.error('[Voting] Error fetching votes:', error);
      return {};
    }
    const votes = {};
    (data || []).forEach(v => {
      if (!votes[v.feature_id]) {
        votes[v.feature_id] = { upvotes: 0, downvotes: 0 };
      }
      const voteType = normalizeVoteType(v.vote_type);
      if (voteType === 1) votes[v.feature_id].upvotes++;
      else if (voteType === -1) votes[v.feature_id].downvotes++;
    });
    return votes;
  }

  async function getUserVotes() {
    const userId = getLocalUserId();
    const { data, error } = await supabaseFetch(
      `ytplus_feature_votes?select=feature_id,vote_type&ip_address=eq.${userId}`
    );
    if (error) {
      console.error('[Voting] Error fetching user votes:', error);
      return {};
    }
    const userVotes = {};
    (data || []).forEach(v => {
      const voteType = normalizeVoteType(v.vote_type);
      if (voteType) userVotes[v.feature_id] = voteType;
    });
    return userVotes;
  }

  async function vote(featureId, voteType) {
    const userId = getLocalUserId();

    const { data: existing } = await supabaseFetch(
      `ytplus_feature_votes?feature_id=eq.${featureId}&ip_address=eq.${userId}&select=id`
    );

    if (existing && existing.length > 0) {
      const existingVote = existing[0];
      if (voteType === 0) {
        await supabaseFetch(`ytplus_feature_votes?id=eq.${existingVote.id}`, { method: 'DELETE' });
        return { success: true, action: 'removed' };
      }
      await supabaseFetch(`ytplus_feature_votes?id=eq.${existingVote.id}`, {
        method: 'PATCH',
        body: JSON.stringify({ vote_type: voteType }),
      });
      return { success: true, action: 'updated' };
    }

    if (voteType === 0) {
      return { success: true, action: 'none' };
    }

    const { error } = await supabaseFetch('ytplus_feature_votes', {
      method: 'POST',
      body: JSON.stringify({
        feature_id: featureId,
        vote_type: voteType,
        ip_address: userId,
      }),
    });

    if (error) {
      console.error('[Voting] Vote error:', error);
      return { success: false, error };
    }
    return { success: true, action: 'added' };
  }

  async function submitFeature(title, description) {
    const userId = getLocalUserId();
    const { error } = await supabaseFetch('ytplus_feature_requests', {
      method: 'POST',
      body: JSON.stringify({
        title,
        description,
        author_ip: userId,
      }),
    });

    if (error) {
      console.error('[Voting] Submit error:', error);
      return { success: false, error };
    }
    return { success: true };
  }

  function isPreviewFeature(feature) {
    return String(feature?.title || '').trim() === PREVIEW_FEATURE_TITLE;
  }

  async function ensurePreviewFeature(features) {
    const fromList = Array.isArray(features) ? features.find(isPreviewFeature) : null;
    if (fromList) return fromList;

    const userId = getLocalUserId();
    const { data, error } = await supabaseFetch('ytplus_feature_requests', {
      method: 'POST',
      body: JSON.stringify({
        title: PREVIEW_FEATURE_TITLE,
        description: PREVIEW_FEATURE_DESC,
        status: 'proposed',
        author_ip: userId,
      }),
    });

    if (error) {
      console.error('[Voting] Error creating preview row:', error);

      // Recover if preview row already exists (e.g. conflict/race condition on insert)
      const encodedTitle = encodeURIComponent(PREVIEW_FEATURE_TITLE);
      const { data: existingPreview } = await supabaseFetch(
        `ytplus_feature_requests?select=id,title,description,status&title=eq.${encodedTitle}&limit=1`
      );
      if (Array.isArray(existingPreview) && existingPreview[0]) {
        return existingPreview[0];
      }
      return null;
    }

    if (Array.isArray(data) && data[0]) return data[0];

    const refreshed = await getFeatures();
    return refreshed.find(isPreviewFeature) || null;
  }

  function createVotingUI(container) {
    container.innerHTML = `
      <div class="ytp-plus-voting">
        <div class="ytp-plus-voting-header">
          <h3>${tf('featureRequests', 'Feature Requests')}</h3>
          <button class="ytp-plus-voting-add-btn" id="ytp-plus-show-add-feature">
            + ${tf('addFeature', 'Add Feature')}
          </button>
        </div>
        <div class="ytp-plus-voting-list" id="ytp-plus-voting-list">
          <div class="ytp-plus-voting-loading">${tf('loading', 'Loading...')}</div>
        </div>
        <div class="ytp-plus-voting-add-form" id="ytp-plus-voting-add-form" style="display:none;">
          <input type="text" id="ytp-plus-feature-title" placeholder="${tf('featureTitle', 'Feature title')}" />
          <textarea id="ytp-plus-feature-desc" placeholder="${tf('featureDescription', 'Description')}"></textarea>
          <div class="ytp-plus-voting-form-actions">
            <button class="ytp-plus-voting-cancel" id="ytp-plus-cancel-feature">${tf('cancel', 'Cancel')}</button>
            <button class="ytp-plus-voting-submit" id="ytp-plus-submit-feature">${tf('submit', 'Submit')}</button>
          </div>
        </div>
      </div>
    `;
  }

  async function loadFeatures() {
    const listEl = document.getElementById('ytp-plus-voting-list');
    if (!listEl) return;

    const allFeaturesRaw = await getFeatures();
    const previewFeature = await ensurePreviewFeature(allFeaturesRaw);
    const features = (allFeaturesRaw || []).filter(f => !isPreviewFeature(f));
    const allVotes = await getAllVotes();
    const userVotes = await getUserVotes();

    const renderFeatures = [...features];

    if (renderFeatures.length === 0) {
      listEl.innerHTML = `<div class="ytp-plus-voting-empty">${tf('noFeatures', 'No feature requests yet')}</div>`;
      // Still update the aggregate vote bar even when there are no user features —
      // the preview feature in the DB tracks the overall like/dislike count.
      updateVoteBar(allVotes, userVotes, previewFeature?.id || null);
      return;
    }

    listEl.innerHTML = renderFeatures
      .map(f => {
        const votes = allVotes[f.id] || { upvotes: 0, downvotes: 0 };
        const userVote = userVotes[f.id] || 0;
        const totalVotes = votes.upvotes + votes.downvotes;
        const upPercent = totalVotes > 0 ? Math.round((votes.upvotes / totalVotes) * 100) : 50;
        const statusMeta = getStatusMeta(f.status);
        return `
          <div class="ytp-plus-voting-item" data-feature-id="${f.id}">
            <div class="ytp-plus-voting-item-content">
              <div class="ytp-plus-voting-item-title">${escapeHtml(f.title)}</div>
              <div class="ytp-plus-voting-item-desc">${escapeHtml(f.description || '')}</div>
              <div class="ytp-plus-voting-item-status ${statusMeta.className}">${escapeHtml(statusMeta.label)}</div>
            </div>
            <div class="ytp-plus-voting-item-votes">
              <div class="ytp-plus-voting-score">
                <span class="ytp-plus-vote-total">${totalVotes} ${tf('votes', 'votes')}</span>
              </div>
              <div class="ytp-plus-voting-buttons">
                <div class="ytp-plus-voting-buttons-track" style="background:linear-gradient(to right, #4caf50 ${upPercent}%, #f44336 ${upPercent}%);"></div>
                <button class="ytp-plus-vote-btn ${userVote === 1 ? 'active' : ''}" data-vote="1" title="${tf('like', 'Like')}" type="button" aria-label="${tf('like', 'Like')}">
                  <svg class="ytp-plus-vote-icon" viewBox="0 0 24 24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
                </button>
                <button class="ytp-plus-vote-btn ${userVote === -1 ? 'active' : ''}" data-vote="-1" title="${tf('dislike', 'Dislike')}" type="button" aria-label="${tf('dislike', 'Dislike')}">
                  <svg class="ytp-plus-vote-icon" viewBox="0 0 24 24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
                </button>
              </div>
            </div>
          </div>
        `;
      })
      .join('');

    listEl.querySelectorAll('.ytp-plus-vote-btn').forEach(btn => {
      btn.addEventListener('click', async () => {
        if (voteRequestInFlight) return;
        const featureId = btn.closest('.ytp-plus-voting-item').dataset.featureId;
        const voteType = parseInt(btn.dataset.vote, 10);
        const currentUserVote = userVotes[featureId] || 0;

        let newVoteType = voteType;
        if (currentUserVote === voteType) {
          newVoteType = 0;
        }

        try {
          voteRequestInFlight = true;
          setVoteControlsBusy(
            listEl.closest('.ytp-plus-settings-section, .ytp-plus-voting') || listEl,
            true
          );

          const result = await vote(featureId, newVoteType);
          if (result.success) {
            await loadFeatures();
          }
        } finally {
          voteRequestInFlight = false;
          setVoteControlsBusy(
            listEl.closest('.ytp-plus-settings-section, .ytp-plus-voting') || listEl,
            false
          );
        }
      });
    });

    // Update aggregate vote bar
    updateVoteBar(allVotes, userVotes, previewFeature?.id || null);
  }

  function escapeHtml(str) {
    if (!str) return '';
    if (window.YouTubeSecurityUtils?.escapeHtml) return window.YouTubeSecurityUtils.escapeHtml(str);
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
  }

  /** Aggregate all feature votes into a single bar above the feature list */
  function updateVoteBar(allVotes, userVotes, previewFeatureId) {
    const fillEl = document.getElementById('ytp-plus-vote-bar-fill');
    const countEl = document.getElementById('ytp-plus-vote-bar-count');
    const upBtn = document.getElementById('ytp-plus-vote-bar-up');
    const downBtn = document.getElementById('ytp-plus-vote-bar-down');
    if (!fillEl || !countEl) return;

    const previewVotes = previewFeatureId
      ? allVotes[previewFeatureId] || { upvotes: 0, downvotes: 0 }
      : { upvotes: 0, downvotes: 0 };

    const totalUp = previewVotes.upvotes || 0;
    const totalDown = previewVotes.downvotes || 0;

    const total = totalUp + totalDown;
    const pct = total > 0 ? Math.round((totalUp / total) * 100) : 50;
    fillEl.style.background = `linear-gradient(to right, #4caf50 ${pct}%, #f44336 ${pct}%)`;
    countEl.textContent = total > 0 ? `${total}` : '0';

    const previewUserVote = previewFeatureId ? userVotes[previewFeatureId] || 0 : 0;
    if (upBtn) upBtn.classList.toggle('active', previewUserVote === 1);
    if (downBtn) downBtn.classList.toggle('active', previewUserVote === -1);
  }

  /** Before/After comparison slider */
  function initSlider() {
    const container = document.querySelector('.ytp-plus-ba-container');
    if (!container || container.dataset.sliderInit) return;
    container.dataset.sliderInit = '1';

    const afterEl = container.querySelector('.ytp-plus-ba-after');
    const divider = container.querySelector('.ytp-plus-ba-divider');
    if (!afterEl || !divider) return;

    let dragging = false;
    let resumeTimer = null;
    let rafId = null;

    function setPosition(pct, manual = false) {
      const clamped = Math.max(2, Math.min(98, pct));
      afterEl.style.clipPath = `inset(0 0 0 ${clamped}%)`;
      if (manual) {
        divider.style.left = `${clamped}%`;
      }
      divider.setAttribute('aria-valuenow', String(Math.round(clamped)));
    }

    function getPct(clientX) {
      const rect = container.getBoundingClientRect();
      return ((clientX - rect.left) / rect.width) * 100;
    }

    function pauseAutoplay() {
      divider.classList.remove('autoplay');
      if (rafId) {
        cancelAnimationFrame(rafId);
        rafId = null;
      }
      if (resumeTimer) clearTimeout(resumeTimer);
      resumeTimer = setTimeout(() => {
        divider.classList.add('autoplay');
        startAutoplayRaf();
      }, 3000);
    }

    function startAutoplayRaf() {
      if (rafId) return;
      function loop() {
        if (!divider.classList.contains('autoplay')) {
          rafId = null;
          return;
        }
        const rect = container.getBoundingClientRect();
        const dRect = divider.getBoundingClientRect();
        const pct = ((dRect.left + dRect.width / 2 - rect.left) / rect.width) * 100;
        setPosition(pct, false);
        rafId = requestAnimationFrame(loop);
      }
      rafId = requestAnimationFrame(loop);
    }

    container.addEventListener('mousedown', e => {
      dragging = true;
      pauseAutoplay();
      setPosition(getPct(e.clientX), true);
      e.preventDefault();
    });
    window.addEventListener('mousemove', e => {
      if (dragging) setPosition(getPct(e.clientX), true);
    });
    window.addEventListener('mouseup', () => {
      dragging = false;
    });

    container.addEventListener(
      'touchstart',
      e => {
        dragging = true;
        pauseAutoplay();
        setPosition(getPct(e.touches[0].clientX), true);
      },
      { passive: true }
    );
    window.addEventListener(
      'touchmove',
      e => {
        if (dragging) setPosition(getPct(e.touches[0].clientX), true);
      },
      { passive: true }
    );
    window.addEventListener('touchend', () => {
      dragging = false;
    });

    divider.addEventListener('keydown', e => {
      pauseAutoplay();
      const cur = parseFloat(divider.getAttribute('aria-valuenow') || '50');
      if (e.key === 'ArrowLeft') {
        setPosition(cur - 2, true);
        e.preventDefault();
      }
      if (e.key === 'ArrowRight') {
        setPosition(cur + 2, true);
        e.preventDefault();
      }
    });

    // initial position 50%
    setPosition(50, true);

    // start autoplay after short delay
    setTimeout(() => {
      divider.classList.add('autoplay');
      startAutoplayRaf();
    }, 400);
  }

  function initVoting() {
    if (votingInitialized) return;
    votingInitialized = true;

    // Vote bar aggregate buttons
    document.addEventListener('click', async e => {
      const barBtn = e.target.closest('.ytp-plus-vote-bar-btn');
      if (barBtn) {
        if (voteRequestInFlight) return;
        const features = await getFeatures();
        const previewFeature = await ensurePreviewFeature(features);
        if (!previewFeature?.id) return;

        const userVotes = await getUserVotes();
        const voteType = parseInt(barBtn.dataset.vote, 10);

        const currentUserVote = userVotes[previewFeature.id] || 0;
        const newVoteType = currentUserVote === voteType ? 0 : voteType;
        const controlsRoot =
          barBtn.closest('.ytp-plus-settings-section, .ytp-plus-voting') || document.body;
        try {
          voteRequestInFlight = true;
          setVoteControlsBusy(controlsRoot, true);
          await vote(previewFeature.id, newVoteType);
          await loadFeatures();
        } finally {
          voteRequestInFlight = false;
          setVoteControlsBusy(controlsRoot, false);
        }
      }
    });

    document.addEventListener('click', e => {
      const showAddBtn = e.target.closest('#ytp-plus-show-add-feature');
      const cancelBtn = e.target.closest('#ytp-plus-cancel-feature');
      const submitBtn = e.target.closest('#ytp-plus-submit-feature');

      if (showAddBtn) {
        const addFormEl = document.getElementById('ytp-plus-voting-add-form');
        const showAddEl = document.getElementById('ytp-plus-show-add-feature');
        if (addFormEl) addFormEl.style.display = 'block';
        if (showAddEl) showAddEl.style.display = 'none';
      }

      if (cancelBtn) {
        const addFormEl = document.getElementById('ytp-plus-voting-add-form');
        const showAddEl = document.getElementById('ytp-plus-show-add-feature');
        const titleEl = document.getElementById('ytp-plus-feature-title');
        const descEl = document.getElementById('ytp-plus-feature-desc');

        if (addFormEl) addFormEl.style.display = 'none';
        if (showAddEl) showAddEl.style.display = 'block';
        if (titleEl) titleEl.value = '';
        if (descEl) descEl.value = '';
      }

      if (submitBtn) {
        const titleInput = document.getElementById('ytp-plus-feature-title');
        const descInput = document.getElementById('ytp-plus-feature-desc');
        const title = titleInput?.value?.trim() || '';
        const desc = descInput?.value?.trim() || '';
        if (!title) return;

        submitBtn.disabled = true;
        submitBtn.textContent = tf('loading', 'Loading...');

        submitFeature(title, desc).then(result => {
          submitBtn.disabled = false;
          submitBtn.textContent = tf('submit', 'Submit');

          if (result.success) {
            const addFormEl = document.getElementById('ytp-plus-voting-add-form');
            const showAddEl = document.getElementById('ytp-plus-show-add-feature');
            if (addFormEl) addFormEl.style.display = 'none';
            if (showAddEl) showAddEl.style.display = 'block';
            if (titleInput) titleInput.value = '';
            if (descInput) descInput.value = '';
            loadFeatures();
          }
        });
      }
    });
  }

  const VotingSystem = {
    init: initVoting,
    createUI: createVotingUI,
    loadFeatures,
    getFeatures,
    vote,
    submitFeature,
    initSlider,
    updateVoteBar,
  };

  if (typeof window.YouTubePlus === 'undefined') {
    window.YouTubePlus = {};
  }
  window.YouTubePlus.Voting = VotingSystem;
})();