Greasy Fork is available in English.

Universal Manga Reader

Smart immersive manga reader overlay with proactive image detection/preload, auto next chapter, auto paging, shake paging, Wake Lock, auto-open rules, bottom more drawer, hide UI, and bilingual UI (English default / Chinese toggle).

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Universal Manga Reader
// @namespace    https://github.com/ymhomer/ym_Userscript
// @version      1.8.0
// @description  Smart immersive manga reader overlay with proactive image detection/preload, auto next chapter, auto paging, shake paging, Wake Lock, auto-open rules, bottom more drawer, hide UI, and bilingual UI (English default / Chinese toggle).
// @author       ymhomer
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const AUTO_NEXT_FLAG = '__UMR_AUTO_OPEN__';
  const UMR_STATE_KEY = '__UMR_READER_STATE__';
  const UMR_AUTO_RULES_KEY = '__UMR_AUTO_RULES__';
  const UMR_LANG_KEY = '__UMR_LANG__';

  const SHAKE_SENSITIVITY_LEVELS = [1, 2, 3, 4, 5];

  const SHAKE_CONFIG_MAP = {
    1: { threshold: 2.7, burst: 5.2, cooldown: 900, decay: 0.80 },
    2: { threshold: 2.2, burst: 4.4, cooldown: 820, decay: 0.78 },
    3: { threshold: 1.8, burst: 3.7, cooldown: 760, decay: 0.76 },
    4: { threshold: 1.45, burst: 3.0, cooldown: 700, decay: 0.74 },
    5: { threshold: 1.1, burst: 2.4, cooldown: 620, decay: 0.72 }
  };

  const I18N = {
    en: {
      menu_open: '📚 Open Manga Reader',
      menu_close: '❌ Close Manga Reader',

      app_title: 'Manga Reader',
      app_title_with_count: ({ title, count }) => `${title || 'Manga Reader'} · ${count} pages`,

      mode_single: 'Single',
      mode_double: 'Double',

      auto_next: 'Auto Next',
      auto_play: 'Auto Play',
      hide_ui: 'Hide UI',
      show_ui: 'Show UI',
      fullscreen: 'Fullscreen',
      close: 'Close',
      more: 'More',
      help: 'Help',
      language: 'Language',
      language_en: 'English',
      language_zh: '中文',
      shake_paging: 'Shake Page',
      shake_sensitivity: ({ level }) => `Sensitivity ${level}`,
      auto_open_manager: 'Auto Open Rules',
      save: 'Save',
      cancel: 'Cancel',
      delete: 'Delete',
      edit: 'Edit',
      rule_dialog_title: 'Auto Open Immersive Reader',
      rule_empty: 'No rules yet. Press "+" to add the current page URL.',
      rule_input_placeholder: 'Enter a URL rule, wildcard * supported',
      rule_tip_html:
        'You can change the trailing part of a URL to <code>*</code> as a wildcard.<br>' +
        'It is recommended to set the rule to the real chapter reading URL, not a search page or list page.<br>' +
        'When adding, the current page URL will be filled in automatically.',

      loader_processing: 'Processing...',
      loader_trigger_lazy: 'Triggering lazy loading...',
      loader_detect_sources: 'Proactively detecting image sources...',
      loader_analyzing_pages: ({ domCount, probeCount }) => `Analyzing manga pages... DOM large images ${domCount} / proactive probe ${probeCount}`,
      loader_detecting_progress: ({ done, total }) => `Proactively detecting images... ${done}/${total}`,
      loader_analyzing_current_page: 'Analyzing images on this page...',
      loader_no_pages: 'No images that look like manga pages were found. Please fully load the page and try again.',

      toast_not_found_pages: 'No usable manga pages found',
      toast_loaded_pages: ({ count }) => `Loaded ${count} manga pages`,
      toast_last_page_no_next: 'Last page: next chapter target not found',
      toast_last_page_auto_next: ({ seconds }) => `Last page: auto entering next chapter in ${seconds} seconds`,
      toast_auto_next_failed_no_target: 'Auto next chapter failed: target not found',
      toast_auto_next_failed_open: 'Auto next chapter failed: unable to open target',
      toast_already_last: 'Already at the last page',
      toast_already_first: 'Already at the first page',
      toast_mode_single: 'Single-page mode',
      toast_mode_double: 'Double-page mode',
      toast_auto_next_off: 'Auto next chapter disabled',
      toast_auto_next_on: 'Auto next chapter enabled',
      toast_shake_off_due_autoplay: 'Shake paging disabled, switched to auto play',
      toast_auto_play_off: 'Auto play disabled',
      toast_auto_play_on: ({ seconds }) => `Auto play enabled (${seconds} sec)`,
      toast_motion_permission_denied: 'Motion permission not granted or unsupported by this browser',
      toast_shake_on: ({ level }) => `Shake paging enabled (sensitivity ${level})`,
      toast_shake_off: 'Shake paging disabled',
      toast_shake_sensitivity: ({ level }) => `Shake paging sensitivity: ${level}`,
      toast_fullscreen_fail: 'Fullscreen is not supported on this page or was blocked by the browser',
      toast_enter_rule: 'Please enter a URL rule',
      toast_rule_saved: 'Auto-open rule saved',
      toast_rule_deleted: 'Rule deleted',
      toast_lang_switched: ({ lang }) => `Language switched to ${lang}`,

      btn_seconds: ({ seconds }) => `${seconds}s`,
      btn_ui_unlock_title: 'Show controls',
      btn_fullscreen_title: 'Fullscreen',
      btn_close_title: 'Close',

      help_html:
        '<div><b>Keyboard:</b></div>' +
        '<div>← / A: Previous page</div>' +
        '<div>→ / D / Space: Next page</div>' +
        '<div>M: Single / Double page</div>' +
        '<div>P: Auto play On/Off</div>' +
        '<div>T: Switch auto play interval</div>' +
        '<div>N: Auto next chapter On/Off</div>' +
        '<div>F: Fullscreen</div>' +
        '<div>U: Hide / Show UI</div>' +
        '<div>Esc: Close</div>' +
        '<div style="margin-top:8px;"><b>Touch:</b></div>' +
        '<div>Swipe left/right to turn pages, tap left/right to turn pages, tap center to hide/show UI.</div>' +
        '<div style="margin-top:8px;"><b>Smart Display:</b></div>' +
        '<div>Portrait images prefer side fitting, landscape images prefer top/bottom fitting; double-page mode tries to keep both pages complete and centered closely together.</div>' +
        '<div style="margin-top:8px;"><b>Proactive Collection:</b></div>' +
        '<div>Scans DOM, lazy-load attributes, srcset, background images, and image URLs in scripts, then warms them up to improve auto-open completeness.</div>' +
        '<div style="margin-top:8px;"><b>Auto Play / Shake Page / Next Chapter:</b></div>' +
        '<div>Auto play and shake page are mutually exclusive. When one is enabled, the other will be disabled automatically. On the last page, if auto next chapter is enabled, it waits 5 seconds before jumping to the next chapter.</div>'
    },

    zh: {
      menu_open: '📚 开启漫画阅读器',
      menu_close: '❌ 关闭漫画阅读器',

      app_title: '漫画阅读器',
      app_title_with_count: ({ title, count }) => `${title || '漫画阅读器'} · ${count} 张`,

      mode_single: '单页',
      mode_double: '双页',

      auto_next: '自动下一章',
      auto_play: '自动翻页',
      hide_ui: '隐藏UI',
      show_ui: '显示UI',
      fullscreen: '全屏',
      close: '关闭',
      more: '更多',
      help: '说明',
      language: '语言',
      language_en: 'English',
      language_zh: '中文',
      shake_paging: '摇晃翻页',
      shake_sensitivity: ({ level }) => `敏感度 ${level}`,
      auto_open_manager: '自动打开沉浸式',
      save: '保存',
      cancel: '取消',
      delete: '删除',
      edit: '改',
      rule_dialog_title: '自动打开沉浸式',
      rule_empty: '目前没有规则。按「+」可把当前页面网址加入管理。',
      rule_input_placeholder: '请输入网址规则,支持 * 万用字元',
      rule_tip_html:
        '可把网址后端改成 <code>*</code> 作为万用字元。<br>' +
        '建议设定为真正进入漫画阅读时的连结,不要设定到搜寻页或列表页。<br>' +
        '新增时会先自动带入当前页面网址。',

      loader_processing: '正在处理...',
      loader_trigger_lazy: '正在触发页面懒加载...',
      loader_detect_sources: '正在主动探测图片来源...',
      loader_analyzing_pages: ({ domCount, probeCount }) => `正在分析漫画页... DOM大图 ${domCount} 张 / 主动探测 ${probeCount} 张`,
      loader_detecting_progress: ({ done, total }) => `正在主动探测图片... ${done}/${total}`,
      loader_analyzing_current_page: '正在分析本页图片...',
      loader_no_pages: '没有找到足够像漫画页的图片,请先把页面完整载入后再试。',

      toast_not_found_pages: '未找到可用漫画页',
      toast_loaded_pages: ({ count }) => `已载入 ${count} 张漫画页`,
      toast_last_page_no_next: '最后一页:未找到下一章按钮',
      toast_last_page_auto_next: ({ seconds }) => `最后一页:${seconds} 秒后自动进入下一章`,
      toast_auto_next_failed_no_target: '自动下一章失败:找不到目标',
      toast_auto_next_failed_open: '自动下一章失败:无法打开目标',
      toast_already_last: '已经是最后一页',
      toast_already_first: '已经是第一页',
      toast_mode_single: '单页模式',
      toast_mode_double: '双页模式',
      toast_auto_next_off: '已关闭自动下一章',
      toast_auto_next_on: '已开启自动下一章',
      toast_shake_off_due_autoplay: '已关闭摇晃翻页,改为自动翻页',
      toast_auto_play_off: '已关闭自动翻页',
      toast_auto_play_on: ({ seconds }) => `已开启自动翻页(${seconds} 秒)`,
      toast_motion_permission_denied: '装置摇晃权限未授予或此浏览器不支持',
      toast_shake_on: ({ level }) => `已开启摇晃翻页(敏感度 ${level})`,
      toast_shake_off: '已关闭摇晃翻页',
      toast_shake_sensitivity: ({ level }) => `摇晃翻页敏感度:${level}`,
      toast_fullscreen_fail: '此页不支持全屏或被浏览器阻止',
      toast_enter_rule: '请输入网址规则',
      toast_rule_saved: '已保存自动打开规则',
      toast_rule_deleted: '已删除规则',
      toast_lang_switched: ({ lang }) => `已切换语言:${lang}`,

      btn_seconds: ({ seconds }) => `${seconds}秒`,
      btn_ui_unlock_title: '显示控制列',
      btn_fullscreen_title: '全屏',
      btn_close_title: '关闭',

      help_html:
        '<div><b>操作:</b></div>' +
        '<div>← / A:上一页</div>' +
        '<div>→ / D / Space:下一页</div>' +
        '<div>M:单页 / 双页</div>' +
        '<div>P:自动翻页 开/关</div>' +
        '<div>T:切换自动翻页秒数</div>' +
        '<div>N:自动下一章 开/关</div>' +
        '<div>F:全屏</div>' +
        '<div>U:隐藏 / 显示 UI</div>' +
        '<div>Esc:关闭</div>' +
        '<div style="margin-top:8px;"><b>触控:</b></div>' +
        '<div>左滑 / 右滑翻页,左右点击翻页,中间点击显示/隐藏 UI。</div>' +
        '<div style="margin-top:8px;"><b>智能显示:</b></div>' +
        '<div>直式图优先左右贴边,横式图优先上下贴边;双页模式会尽量兼顾左右贴边与上下完整,并让两页靠近置中。</div>' +
        '<div style="margin-top:8px;"><b>主动收图:</b></div>' +
        '<div>会主动扫描 DOM、懒加载属性、srcset、背景图与脚本中的图片网址,并预热图片,以提高自动启动时的完整度。</div>' +
        '<div style="margin-top:8px;"><b>自动翻页 / 摇晃翻页 / 下一章:</b></div>' +
        '<div>自动翻页与摇晃翻页互斥;其中一方开启时会自动关闭另一方。到最后一页后,如果已开启自动下一章,才会再等待 5 秒自动跳下一章。</div>'
    }
  };

  const STATE = {
    overlay: null,
    pages: [],
    index: 0,
    mode: 'single',
    fullscreen: false,
    preloadCount: 12,
    isOpen: false,

    touchStartX: 0,
    touchStartY: 0,
    touchMoved: false,

    keyHandlerBound: false,
    resizeHandlerBound: false,
    fullscreenHandlerBound: false,
    visibilityHandlerBound: false,
    scrollLocked: false,

    autoNextEnabled: true,
    autoNextDelay: 5000,
    autoNextTimer: null,
    autoNextTarget: null,

    autoPlayEnabled: false,
    autoPlayDelay: 3000,
    autoPlayTimer: null,

    shakePagingEnabled: false,
    shakeSensitivity: 3,
    shakeLastTriggerTime: 0,
    shakeHandlerBound: false,
    shakeGravity: { x: 0, y: 0, z: 0 },
    shakeLastLinear: { x: 0, y: 0, z: 0 },
    shakeEnergy: 0,
    shakeWarmupFrames: 0,
    shakeLastMotionTime: 0,

    moreOpen: false,

    wakeLockSentinel: null,
    wakeLockSupported: 'wakeLock' in navigator,

    restoreUiHidden: false,

    ruleDialogOpen: false,
    ruleEditIndex: -1,

    proactiveCandidates: [],
    proactiveMetaMap: new Map(),
    proactiveDoneCount: 0,

    lang: loadLanguage(),

    config: {
      minWidth: 240,
      minHeight: 320,
      minArea: 120000,
      aspectMin: 0.2,
      aspectMax: 3.8,
      proactiveMaxCandidates: 300,
      proactiveConcurrency: 8
    }
  };

  const STYLE = `
    #umr-overlay {
      position: fixed;
      inset: 0;
      z-index: 2147483646;
      background: #000;
      color: #fff;
      display: flex;
      flex-direction: column;
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      user-select: none;
      overflow: hidden;
    }

    #umr-overlay.umr-hidden {
      display: none !important;
    }

    #umr-topbar {
      height: 44px;
      min-height: 44px;
      display: flex;
      align-items: center;
      padding: 6px 12px;
      background: linear-gradient(to bottom, rgba(0,0,0,.82), rgba(0,0,0,.14));
      backdrop-filter: blur(8px);
      z-index: 5;
      transition: opacity .2s ease;
    }

    #umr-overlay.umr-ui-hidden #umr-topbar,
    #umr-overlay.umr-ui-hidden #umr-bottombar,
    #umr-overlay.umr-ui-hidden #umr-more-panel {
      opacity: 0;
      pointer-events: none;
    }

    #umr-topbar .umr-title {
      font-size: 13px;
      opacity: .9;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      width: 100%;
      text-align: center;
    }

    .umr-btn {
      height: 34px;
      border: 1px solid rgba(255,255,255,.14);
      background: rgba(255,255,255,.08);
      color: #fff;
      border-radius: 10px;
      padding: 0 12px;
      cursor: pointer;
      font-size: 12px;
      transition: .18s ease;
      white-space: nowrap;
      flex: 0 0 auto;
    }

    .umr-btn:hover {
      background: rgba(255,255,255,.14);
    }

    .umr-btn.umr-active {
      background: rgba(255,255,255,.22);
      border-color: rgba(255,255,255,.28);
    }

    .umr-btn.umr-danger {
      border-color: rgba(255,255,255,.18);
    }

    .umr-icon-btn {
      width: 34px;
      min-width: 34px;
      padding: 0;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      font-size: 16px;
      line-height: 1;
    }

    #umr-stage {
      flex: 1;
      position: relative;
      overflow: hidden;
      background: #000;
    }

    #umr-book {
      position: absolute;
      inset: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 2px;
      padding: 0;
      overflow: hidden;
      perspective: 1400px;
      background: #000;
    }

    #umr-book.umr-book-double {
      gap: 8px;
      justify-content: center;
      padding: 0 12px;
    }

    .umr-pageWrap {
      width: 50%;
      max-width: 50%;
      height: 100%;
      max-height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      transform-style: preserve-3d;
      transition: transform .24s ease, opacity .2s ease, width .16s ease;
      position: relative;
      overflow: hidden;
      background: #000;
    }

    .umr-pageWrap.single {
      width: 100%;
      max-width: 100%;
      height: 100%;
      max-height: 100%;
    }

    .umr-pageWrap.double {
      width: auto;
      max-width: none;
      flex: 0 0 auto;
      overflow: visible;
    }

    .umr-pageWrap.turn-next {
      animation: umrTurnNext .22s ease;
      transform-origin: left center;
    }

    .umr-pageWrap.turn-prev {
      animation: umrTurnPrev .22s ease;
      transform-origin: right center;
    }

    @keyframes umrTurnNext {
      0% { transform: rotateY(0deg) scale(1); opacity: 1; }
      50% { transform: rotateY(-12deg) scale(.992); opacity: .95; }
      100% { transform: rotateY(0deg) scale(1); opacity: 1; }
    }

    @keyframes umrTurnPrev {
      0% { transform: rotateY(0deg) scale(1); opacity: 1; }
      50% { transform: rotateY(12deg) scale(.992); opacity: .95; }
      100% { transform: rotateY(0deg) scale(1); opacity: 1; }
    }

    .umr-page {
      display: block;
      border-radius: 0;
      box-shadow: none;
      background: #000;
      max-width: 100%;
      max-height: 100%;
      image-rendering: auto;
      pointer-events: none;
      object-fit: contain;
    }

    .umr-pageWrap.single.portrait .umr-page {
      width: 100%;
      height: auto;
      max-width: 100%;
      max-height: 100%;
    }

    .umr-pageWrap.single.landscape .umr-page,
    .umr-pageWrap.single.square .umr-page {
      height: 100%;
      width: auto;
      max-height: 100%;
      max-width: 100%;
    }

    .umr-pageWrap.double .umr-page {
      width: 100%;
      height: auto;
      max-width: 100%;
      max-height: 100%;
      object-fit: contain;
    }

    .umr-pageMeta {
      position: absolute;
      left: 8px;
      bottom: 8px;
      background: rgba(0,0,0,.42);
      font-size: 11px;
      padding: 4px 8px;
      border-radius: 999px;
      opacity: .5;
      pointer-events: none;
    }

    .umr-nav-zone {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 30%;
      z-index: 2;
      cursor: pointer;
      background: transparent;
    }

    .umr-nav-left { left: 0; }
    .umr-nav-right { right: 0; }

    .umr-nav-center {
      position: absolute;
      left: 30%;
      right: 30%;
      top: 0;
      bottom: 0;
      z-index: 1;
    }

    #umr-ui-unlock {
      position: absolute;
      top: 10px;
      right: 10px;
      z-index: 9;
      width: 38px;
      height: 38px;
      border: 1px solid rgba(255,255,255,.18);
      background: rgba(0,0,0,.45);
      color: #fff;
      border-radius: 999px;
      display: none;
      align-items: center;
      justify-content: center;
      font-size: 16px;
      line-height: 1;
      backdrop-filter: blur(8px);
      cursor: pointer;
      box-shadow: 0 4px 14px rgba(0,0,0,.28);
    }

    #umr-overlay.umr-ui-hidden #umr-ui-unlock {
      display: flex;
    }

    #umr-ui-unlock:active {
      transform: scale(.96);
    }

    #umr-bottombar {
      min-height: 76px;
      padding: 8px 12px 10px;
      background: linear-gradient(to top, rgba(0,0,0,.90), rgba(0,0,0,.20));
      backdrop-filter: blur(8px);
      display: flex;
      flex-direction: column;
      gap: 8px;
      z-index: 5;
      transition: opacity .2s ease;
    }

    #umr-control-row {
      display: flex;
      gap: 8px;
      flex-wrap: nowrap;
      justify-content: flex-start;
      align-items: center;
      overflow-x: auto;
      overflow-y: hidden;
      scrollbar-width: none;
      -ms-overflow-style: none;
      padding-bottom: 2px;
    }

    #umr-control-row::-webkit-scrollbar {
      display: none;
    }

    #umr-progress-row {
      display: flex;
      align-items: center;
      gap: 12px;
    }

    #umr-progress {
      flex: 1;
      height: 6px;
      background: rgba(255,255,255,.12);
      border-radius: 999px;
      position: relative;
      cursor: pointer;
    }

    #umr-progress-fill {
      position: absolute;
      left: 0;
      top: 0;
      bottom: 0;
      width: 0%;
      border-radius: 999px;
      background: rgba(255,255,255,.9);
    }

    #umr-progress-handle {
      position: absolute;
      top: 50%;
      width: 14px;
      height: 14px;
      border-radius: 50%;
      background: #fff;
      transform: translate(-50%, -50%);
      left: 0%;
      box-shadow: 0 2px 12px rgba(255,255,255,.35);
    }

    #umr-counter {
      min-width: 92px;
      text-align: right;
      font-size: 13px;
      opacity: .92;
      flex: 0 0 auto;
    }

    #umr-toast {
      position: absolute;
      left: 50%;
      bottom: 94px;
      transform: translateX(-50%);
      background: rgba(0,0,0,.74);
      color: #fff;
      padding: 8px 12px;
      border-radius: 999px;
      font-size: 12px;
      opacity: 0;
      pointer-events: none;
      transition: opacity .2s ease, transform .2s ease;
      z-index: 12;
      max-width: min(92vw, 560px);
      text-align: center;
    }

    #umr-toast.show {
      opacity: 1;
      transform: translateX(-50%) translateY(-4px);
    }

    #umr-loader {
      position: absolute;
      inset: 0;
      z-index: 11;
      display: flex;
      align-items: center;
      justify-content: center;
      background: rgba(0,0,0,.78);
      flex-direction: column;
      gap: 10px;
      color: #fff;
    }

    .umr-spinner {
      width: 34px;
      height: 34px;
      border-radius: 50%;
      border: 3px solid rgba(255,255,255,.18);
      border-top-color: rgba(255,255,255,.95);
      animation: umrSpin .8s linear infinite;
    }

    @keyframes umrSpin {
      to { transform: rotate(360deg); }
    }

    #umr-more-panel {
      position: absolute;
      left: 10px;
      right: 10px;
      bottom: 84px;
      z-index: 10;
      border-radius: 16px;
      background: rgba(10,10,10,.94);
      border: 1px solid rgba(255,255,255,.10);
      backdrop-filter: blur(10px);
      box-shadow: 0 16px 36px rgba(0,0,0,.45);
      padding: 12px;
      display: none;
      transform: translateY(16px);
      opacity: 0;
      transition: opacity .22s ease, transform .22s ease;
    }

    #umr-more-panel.show {
      display: block;
      opacity: 1;
      transform: translateY(0);
    }

    #umr-more-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
      gap: 8px;
    }

    .umr-more-item {
      width: 100%;
      justify-content: center;
    }

    #umr-help {
      position: absolute;
      right: 0;
      left: 0;
      bottom: calc(100% + 10px);
      margin: 0 auto;
      width: min(420px, calc(100vw - 40px));
      background: rgba(8,8,8,.96);
      border: 1px solid rgba(255,255,255,.1);
      border-radius: 14px;
      padding: 12px;
      font-size: 13px;
      line-height: 1.55;
      display: none;
      box-shadow: 0 12px 30px rgba(0,0,0,.45);
    }

    #umr-help.show {
      display: block;
    }

    #umr-rule-dialog-backdrop {
      position: absolute;
      inset: 0;
      z-index: 20;
      background: rgba(0,0,0,.52);
      display: none;
      align-items: center;
      justify-content: center;
      padding: 18px;
    }

    #umr-rule-dialog-backdrop.show {
      display: flex;
    }

    #umr-rule-dialog {
      width: min(760px, 100%);
      max-height: min(84vh, 860px);
      background: rgba(10,10,10,.97);
      border: 1px solid rgba(255,255,255,.10);
      border-radius: 18px;
      box-shadow: 0 18px 44px rgba(0,0,0,.48);
      display: flex;
      flex-direction: column;
      overflow: hidden;
      backdrop-filter: blur(12px);
    }

    #umr-rule-toolbar {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 12px;
      border-bottom: 1px solid rgba(255,255,255,.08);
    }

    #umr-rule-title {
      flex: 1;
      font-size: 14px;
      opacity: .95;
    }

    #umr-rule-body {
      padding: 12px;
      overflow: auto;
      display: flex;
      flex-direction: column;
      gap: 12px;
    }

    .umr-rule-tip {
      font-size: 12px;
      line-height: 1.5;
      opacity: .72;
      background: rgba(255,255,255,.04);
      border: 1px solid rgba(255,255,255,.06);
      border-radius: 12px;
      padding: 10px 12px;
    }

    #umr-rule-list {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }

    .umr-rule-row {
      display: flex;
      align-items: center;
      gap: 8px;
      border: 1px solid rgba(255,255,255,.08);
      background: rgba(255,255,255,.04);
      border-radius: 12px;
      padding: 10px;
    }

    .umr-rule-text {
      flex: 1;
      min-width: 0;
      font-size: 12px;
      opacity: .92;
      word-break: break-all;
    }

    #umr-rule-empty {
      font-size: 12px;
      opacity: .65;
      text-align: center;
      padding: 16px 10px;
      border: 1px dashed rgba(255,255,255,.10);
      border-radius: 12px;
    }

    #umr-rule-editor {
      display: none;
      flex-direction: column;
      gap: 8px;
      border: 1px solid rgba(255,255,255,.08);
      background: rgba(255,255,255,.04);
      border-radius: 12px;
      padding: 12px;
    }

    #umr-rule-editor.show {
      display: flex;
    }

    #umr-rule-textarea {
      width: 100%;
      min-height: 110px;
      resize: vertical;
      background: rgba(0,0,0,.35);
      color: #fff;
      border: 1px solid rgba(255,255,255,.12);
      border-radius: 10px;
      padding: 10px;
      font-size: 12px;
      line-height: 1.45;
      box-sizing: border-box;
    }

    .umr-rule-editor-actions {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }

    @media (max-width: 860px) {
      #umr-topbar {
        height: 44px;
        min-height: 44px;
        padding: 6px 10px;
      }

      .umr-btn {
        height: 32px;
        font-size: 12px;
        padding: 0 10px;
      }

      .umr-icon-btn {
        width: 32px;
        min-width: 32px;
      }

      #umr-ui-unlock {
        top: 8px;
        right: 8px;
        width: 36px;
        height: 36px;
        font-size: 15px;
      }

      #umr-bottombar {
        min-height: 82px;
        padding: 8px 10px 10px;
      }

      .umr-nav-zone {
        width: 30%;
      }

      .umr-nav-center {
        left: 30%;
        right: 30%;
      }

      #umr-toast {
        bottom: 98px;
      }

      #umr-more-panel {
        left: 8px;
        right: 8px;
        bottom: 88px;
      }

      #umr-rule-dialog-backdrop {
        padding: 10px;
      }

      #umr-rule-dialog {
        max-height: 88vh;
      }

      #umr-rule-toolbar {
        flex-wrap: wrap;
      }
    }
  `;

  GM_addStyle(STYLE);
  GM_registerMenuCommand(t('menu_open'), openReader);
  GM_registerMenuCommand(t('menu_close'), closeReader);

  function loadLanguage() {
    try {
      const v = localStorage.getItem(UMR_LANG_KEY);
      if (v === 'en' || v === 'zh') return v;
    } catch {}
    return 'en';
  }

  function saveLanguage(lang) {
    try {
      localStorage.setItem(UMR_LANG_KEY, lang);
    } catch {}
  }

  function t(key, vars) {
    const langTable = I18N[STATE.lang] || I18N.en;
    const fallbackTable = I18N.en;
    const value = langTable[key] ?? fallbackTable[key] ?? key;
    if (typeof value === 'function') return value(vars || {});
    return value;
  }

  function tf(key, vars) {
    return String(t(key, vars));
  }

  function escapeHtml(text) {
    return String(text)
      .replaceAll('&', '&amp;')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;')
      .replaceAll('"', '&quot;')
      .replaceAll("'", '&#39;');
  }

  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  function debounce(fn, wait = 100) {
    let tmr = null;
    return (...args) => {
      clearTimeout(tmr);
      tmr = setTimeout(() => fn(...args), wait);
    };
  }

  function safeText(v) {
    return String(v || '').trim();
  }

  function normalizeUrl(url) {
    try {
      if (!url) return '';
      if (url.startsWith('data:')) return '';
      if (url.startsWith('blob:')) return '';
      return new URL(url, location.href).href;
    } catch {
      return '';
    }
  }

  function getAutoOpenRules() {
    try {
      const raw = localStorage.getItem(UMR_AUTO_RULES_KEY);
      if (!raw) return [];
      const parsed = JSON.parse(raw);
      if (!Array.isArray(parsed)) return [];
      return parsed.filter(x => typeof x === 'string' && x.trim());
    } catch {
      return [];
    }
  }

  function saveAutoOpenRules(rules) {
    try {
      localStorage.setItem(UMR_AUTO_RULES_KEY, JSON.stringify(rules));
    } catch {}
  }

  function wildcardToRegex(pattern) {
    const escaped = String(pattern)
      .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
      .replace(/\*/g, '.*');
    return new RegExp(`^${escaped}$`, 'i');
  }

  function matchesAutoOpenRule(url) {
    const rules = getAutoOpenRules();
    return rules.some(rule => {
      try {
        return wildcardToRegex(rule).test(url);
      } catch {
        return false;
      }
    });
  }

  async function requestMotionPermissionIfNeeded() {
    try {
      if (typeof DeviceMotionEvent === 'undefined') return false;
      if (typeof DeviceMotionEvent.requestPermission === 'function') {
        const result = await DeviceMotionEvent.requestPermission();
        return result === 'granted';
      }
      return true;
    } catch {
      return false;
    }
  }

  function getShakeConfig() {
    return SHAKE_CONFIG_MAP[STATE.shakeSensitivity] || SHAKE_CONFIG_MAP[3];
  }

  function resetShakeDetector() {
    STATE.shakeGravity = { x: 0, y: 0, z: 0 };
    STATE.shakeLastLinear = { x: 0, y: 0, z: 0 };
    STATE.shakeEnergy = 0;
    STATE.shakeWarmupFrames = 0;
    STATE.shakeLastMotionTime = 0;
    STATE.shakeLastTriggerTime = 0;
  }

  function onDeviceMotion(event) {
    if (!STATE.isOpen || !STATE.shakePagingEnabled) return;

    const acc = event.accelerationIncludingGravity || event.acceleration;
    if (!acc) return;

    const x = Number(acc.x || 0);
    const y = Number(acc.y || 0);
    const z = Number(acc.z || 0);

    const cfg = getShakeConfig();
    const alpha = 0.84;

    STATE.shakeGravity.x = alpha * STATE.shakeGravity.x + (1 - alpha) * x;
    STATE.shakeGravity.y = alpha * STATE.shakeGravity.y + (1 - alpha) * y;
    STATE.shakeGravity.z = alpha * STATE.shakeGravity.z + (1 - alpha) * z;

    const lx = x - STATE.shakeGravity.x;
    const ly = y - STATE.shakeGravity.y;
    const lz = z - STATE.shakeGravity.z;

    if (STATE.shakeWarmupFrames < 8) {
      STATE.shakeLastLinear = { x: lx, y: ly, z: lz };
      STATE.shakeWarmupFrames++;
      return;
    }

    const jx = lx - STATE.shakeLastLinear.x;
    const jy = ly - STATE.shakeLastLinear.y;
    const jz = lz - STATE.shakeLastLinear.z;

    STATE.shakeLastLinear = { x: lx, y: ly, z: lz };

    const jerkMagnitude = Math.sqrt(jx * jx + jy * jy + jz * jz);
    const linearMagnitude = Math.sqrt(lx * lx + ly * ly + lz * lz);
    const dominantAxis = Math.max(Math.abs(jx), Math.abs(jy), Math.abs(jz));

    const intervalMs = Math.max(12, Number(event.interval || 16));
    const dtFactor = Math.min(1.35, Math.max(0.75, 16 / intervalMs));

    const pulse = Math.max(
      jerkMagnitude * 0.95,
      dominantAxis * 1.12,
      linearMagnitude * 0.42
    ) * dtFactor;

    STATE.shakeEnergy = STATE.shakeEnergy * cfg.decay + pulse;
    STATE.shakeLastMotionTime = Date.now();

    const now = Date.now();
    const enoughCooldown = now - STATE.shakeLastTriggerTime > cfg.cooldown;
    const triggered =
      enoughCooldown &&
      (STATE.shakeEnergy >= cfg.burst || (pulse >= cfg.threshold && STATE.shakeEnergy >= cfg.threshold * 1.55));

    if (triggered) {
      STATE.shakeLastTriggerTime = now;
      STATE.shakeEnergy = 0;
      nextPage();
      return;
    }

    if (now - STATE.shakeLastMotionTime > 260) {
      STATE.shakeEnergy *= 0.65;
    }
  }

  function bindShakeMotionIfNeeded() {
    if (STATE.shakeHandlerBound) return;
    if (typeof window === 'undefined' || typeof DeviceMotionEvent === 'undefined') return;
    window.addEventListener('devicemotion', onDeviceMotion, { passive: true });
    STATE.shakeHandlerBound = true;
  }

  function saveReaderState() {
    try {
      const stateToSave = {
        mode: STATE.mode,
        autoNextEnabled: STATE.autoNextEnabled,
        autoPlayEnabled: STATE.autoPlayEnabled,
        autoPlayDelay: STATE.autoPlayDelay,
        shakePagingEnabled: STATE.shakePagingEnabled,
        shakeSensitivity: STATE.shakeSensitivity,
        uiHidden: !!STATE.overlay?.classList.contains('umr-ui-hidden'),
        lang: STATE.lang
      };
      sessionStorage.setItem(UMR_STATE_KEY, JSON.stringify(stateToSave));
    } catch {}
  }

  function loadReaderState() {
    STATE.restoreUiHidden = false;

    try {
      const raw = sessionStorage.getItem(UMR_STATE_KEY);
      if (!raw) return;

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

      if (saved.mode === 'single' || saved.mode === 'double') {
        STATE.mode = saved.mode;
      }

      if (typeof saved.autoNextEnabled === 'boolean') {
        STATE.autoNextEnabled = saved.autoNextEnabled;
      }

      if (typeof saved.autoPlayEnabled === 'boolean') {
        STATE.autoPlayEnabled = saved.autoPlayEnabled;
      }

      if ([3000, 5000, 7000, 10000].includes(saved.autoPlayDelay)) {
        STATE.autoPlayDelay = saved.autoPlayDelay;
      }

      if (typeof saved.shakePagingEnabled === 'boolean') {
        STATE.shakePagingEnabled = saved.shakePagingEnabled;
      }

      if (SHAKE_SENSITIVITY_LEVELS.includes(saved.shakeSensitivity)) {
        STATE.shakeSensitivity = saved.shakeSensitivity;
      }

      if (saved.lang === 'en' || saved.lang === 'zh') {
        STATE.lang = saved.lang;
      }

      if (saved.shakePagingEnabled && saved.autoPlayEnabled) {
        STATE.autoPlayEnabled = false;
      }

      STATE.restoreUiHidden = !!saved.uiHidden;
    } catch {}
  }

  function clearAutoNextTimer() {
    if (STATE.autoNextTimer) {
      clearTimeout(STATE.autoNextTimer);
      STATE.autoNextTimer = null;
    }
    STATE.autoNextTarget = null;
  }

  function clearAutoPlayTimer() {
    if (STATE.autoPlayTimer) {
      clearTimeout(STATE.autoPlayTimer);
      STATE.autoPlayTimer = null;
    }
  }

  function clearAllAsyncTimers() {
    clearAutoNextTimer();
    clearAutoPlayTimer();
  }

  function scheduleAutoNextIfNeeded() {
    clearAutoNextTimer();
    if (!STATE.isOpen || !STATE.autoNextEnabled || !isAtLastPage()) return;

    const candidate = findNextChapterCandidate();
    STATE.autoNextTarget = candidate;

    if (!candidate) {
      showToast(tf('toast_last_page_no_next'));
      return;
    }

    showToast(tf('toast_last_page_auto_next', { seconds: Math.round(STATE.autoNextDelay / 1000) }));
    STATE.autoNextTimer = setTimeout(() => {
      const latestCandidate = findNextChapterCandidate() || candidate;
      if (!latestCandidate) {
        showToast(tf('toast_auto_next_failed_no_target'));
        return;
      }
      const ok = jumpToNextChapter(latestCandidate);
      if (!ok) showToast(tf('toast_auto_next_failed_open'));
    }, STATE.autoNextDelay);
  }

  function scheduleAutoPlayIfNeeded() {
    clearAutoPlayTimer();
    if (!STATE.isOpen || !STATE.autoPlayEnabled || !STATE.pages.length) return;

    if (isAtLastPage()) {
      scheduleAutoNextIfNeeded();
      return;
    }

    STATE.autoPlayTimer = setTimeout(() => {
      if (!STATE.isOpen || !STATE.autoPlayEnabled) return;
      if (isAtLastPage()) {
        scheduleAutoNextIfNeeded();
        return;
      }
      nextPage(true);
    }, STATE.autoPlayDelay);
  }

  function updateTopbar() {
    if (!STATE.overlay) return;
    const title = STATE.overlay.querySelector('#umr-title');
    if (title) {
      title.textContent = tf('app_title_with_count', {
        title: document.title || tf('app_title'),
        count: STATE.pages.length
      });
    }
  }

  function updateControls() {
    if (!STATE.overlay) return;

    const autoPlayBtn = STATE.overlay.querySelector('#umr-auto-play');
    const autoPlayDelayBtn = STATE.overlay.querySelector('#umr-auto-play-delay');
    const moreBtn = STATE.overlay.querySelector('#umr-more-toggle');
    const modeBtn = STATE.overlay.querySelector('#umr-mode');
    const autoNextBtn = STATE.overlay.querySelector('#umr-auto-next');
    const shakeBtn = STATE.overlay.querySelector('#umr-shake-toggle');
    const shakeSensitivityBtn = STATE.overlay.querySelector('#umr-shake-sensitivity');
    const uiToggleBtn = STATE.overlay.querySelector('#umr-ui-toggle');
    const langBtn = STATE.overlay.querySelector('#umr-language-toggle');

    if (autoPlayBtn) autoPlayBtn.classList.toggle('umr-active', STATE.autoPlayEnabled);
    if (autoPlayBtn) autoPlayBtn.textContent = tf('auto_play');

    if (autoPlayDelayBtn) autoPlayDelayBtn.textContent = tf('btn_seconds', { seconds: Math.round(STATE.autoPlayDelay / 1000) });

    if (moreBtn) {
      moreBtn.classList.toggle('umr-active', STATE.moreOpen);
      moreBtn.textContent = tf('more');
    }

    if (modeBtn) modeBtn.textContent = STATE.mode === 'single' ? tf('mode_single') : tf('mode_double');
    if (autoNextBtn) {
      autoNextBtn.classList.toggle('umr-active', STATE.autoNextEnabled);
      autoNextBtn.textContent = tf('auto_next');
    }
    if (shakeBtn) {
      shakeBtn.classList.toggle('umr-active', STATE.shakePagingEnabled);
      shakeBtn.textContent = tf('shake_paging');
    }
    if (shakeSensitivityBtn) shakeSensitivityBtn.textContent = tf('shake_sensitivity', { level: STATE.shakeSensitivity });

    if (uiToggleBtn) {
      uiToggleBtn.textContent = STATE.overlay.classList.contains('umr-ui-hidden') ? tf('show_ui') : tf('hide_ui');
    }

    if (langBtn) {
      langBtn.textContent = `${tf('language')}: ${STATE.lang === 'en' ? tf('language_en') : tf('language_zh')}`;
    }

    const fullscreenBtn = STATE.overlay.querySelector('#umr-fullscreen');
    const closeBtn = STATE.overlay.querySelector('#umr-close');
    const uiUnlock = STATE.overlay.querySelector('#umr-ui-unlock');
    const helpToggle = STATE.overlay.querySelector('#umr-help-toggle');
    const autoOpenManager = STATE.overlay.querySelector('#umr-auto-open-manager');
    const ruleTitle = STATE.overlay.querySelector('#umr-rule-title');
    const ruleSave = STATE.overlay.querySelector('#umr-rule-save');
    const ruleCancel = STATE.overlay.querySelector('#umr-rule-cancel');
    const ruleDeleteCurrent = STATE.overlay.querySelector('#umr-rule-delete-current');
    const ruleTextarea = STATE.overlay.querySelector('#umr-rule-textarea');
    const help = STATE.overlay.querySelector('#umr-help');
    const ruleTip = STATE.overlay.querySelector('.umr-rule-tip');

    if (fullscreenBtn) fullscreenBtn.title = tf('btn_fullscreen_title');
    if (closeBtn) closeBtn.title = tf('btn_close_title');
    if (uiUnlock) uiUnlock.title = tf('btn_ui_unlock_title');
    if (helpToggle) helpToggle.textContent = tf('help');
    if (autoOpenManager) autoOpenManager.textContent = tf('auto_open_manager');
    if (ruleTitle) ruleTitle.textContent = tf('rule_dialog_title');
    if (ruleSave) ruleSave.textContent = tf('save');
    if (ruleCancel) ruleCancel.textContent = tf('cancel');
    if (ruleDeleteCurrent) ruleDeleteCurrent.textContent = tf('delete');
    if (ruleTextarea) ruleTextarea.placeholder = tf('rule_input_placeholder');
    if (help) help.innerHTML = tf('help_html');
    if (ruleTip) ruleTip.innerHTML = tf('rule_tip_html');
  }

  function updateProgress() {
    if (!STATE.overlay || !STATE.pages.length) return;

    const counter = STATE.overlay.querySelector('#umr-counter');
    const fill = STATE.overlay.querySelector('#umr-progress-fill');
    const handle = STATE.overlay.querySelector('#umr-progress-handle');

    const current = getCurrentVisibleLastPageIndex() + 1;
    const total = STATE.pages.length;
    const baseIndex = Math.min(STATE.index, total - 1);
    const ratio = total <= 1 ? 0 : baseIndex / (total - 1);
    const percent = ratio * 100;

    if (counter) counter.textContent = `${current} / ${total}`;
    if (fill) fill.style.width = `${percent}%`;
    if (handle) handle.style.left = `${percent}%`;
  }

  function renderRuleList() {
    if (!STATE.overlay) return;
    const listEl = STATE.overlay.querySelector('#umr-rule-list');
    if (!listEl) return;

    const rules = getAutoOpenRules();
    if (!rules.length) {
      listEl.innerHTML = `<div id="umr-rule-empty">${escapeHtml(tf('rule_empty'))}</div>`;
      return;
    }

    listEl.innerHTML = rules.map((rule, index) => `
      <div class="umr-rule-row">
        <div class="umr-rule-text">${escapeHtml(rule)}</div>
        <button class="umr-btn" data-rule-edit="${index}">${escapeHtml(tf('edit'))}</button>
        <button class="umr-btn umr-danger" data-rule-delete="${index}">${escapeHtml(tf('delete'))}</button>
      </div>
    `).join('');

    listEl.querySelectorAll('[data-rule-edit]').forEach(btn => {
      btn.addEventListener('click', () => startRuleEdit(Number(btn.getAttribute('data-rule-edit'))));
    });

    listEl.querySelectorAll('[data-rule-delete]').forEach(btn => {
      btn.addEventListener('click', () => deleteRule(Number(btn.getAttribute('data-rule-delete'))));
    });
  }

  function openRuleDialog() {
    if (!STATE.overlay) return;
    const backdrop = STATE.overlay.querySelector('#umr-rule-dialog-backdrop');
    if (!backdrop) return;
    STATE.ruleDialogOpen = true;
    backdrop.classList.add('show');
    renderRuleList();
    cancelRuleEdit();
  }

  function closeRuleDialog() {
    if (!STATE.overlay) return;
    const backdrop = STATE.overlay.querySelector('#umr-rule-dialog-backdrop');
    if (!backdrop) return;
    STATE.ruleDialogOpen = false;
    backdrop.classList.remove('show');
    cancelRuleEdit();
  }

  function startRuleEdit(index = -1) {
    if (!STATE.overlay) return;

    const editor = STATE.overlay.querySelector('#umr-rule-editor');
    const textarea = STATE.overlay.querySelector('#umr-rule-textarea');
    const deleteBtn = STATE.overlay.querySelector('#umr-rule-delete-current');

    if (!editor || !textarea || !deleteBtn) return;

    STATE.ruleEditIndex = index;

    if (index >= 0) {
      const rules = getAutoOpenRules();
      textarea.value = rules[index] || '';
      deleteBtn.style.display = 'inline-flex';
    } else {
      textarea.value = location.href;
      deleteBtn.style.display = 'none';
    }

    editor.classList.add('show');
    textarea.focus();
    textarea.select();
  }

  function cancelRuleEdit() {
    if (!STATE.overlay) return;
    const editor = STATE.overlay.querySelector('#umr-rule-editor');
    const textarea = STATE.overlay.querySelector('#umr-rule-textarea');
    if (!editor || !textarea) return;

    STATE.ruleEditIndex = -1;
    textarea.value = '';
    editor.classList.remove('show');
  }

  function saveRuleFromEditor() {
    if (!STATE.overlay) return;
    const textarea = STATE.overlay.querySelector('#umr-rule-textarea');
    if (!textarea) return;

    const value = textarea.value.trim();
    if (!value) {
      showToast(tf('toast_enter_rule'));
      return;
    }

    const rules = getAutoOpenRules();

    if (STATE.ruleEditIndex >= 0 && STATE.ruleEditIndex < rules.length) {
      rules[STATE.ruleEditIndex] = value;
    } else {
      rules.push(value);
    }

    saveAutoOpenRules(rules);
    renderRuleList();
    cancelRuleEdit();
    showToast(tf('toast_rule_saved'));
  }

  function deleteRule(index) {
    const rules = getAutoOpenRules();
    if (index < 0 || index >= rules.length) return;

    rules.splice(index, 1);
    saveAutoOpenRules(rules);
    renderRuleList();

    if (STATE.ruleEditIndex === index) {
      cancelRuleEdit();
    } else if (STATE.ruleEditIndex > index) {
      STATE.ruleEditIndex--;
    }

    showToast(tf('toast_rule_deleted'));
  }

  function deleteCurrentEditingRule() {
    if (STATE.ruleEditIndex < 0) return;
    deleteRule(STATE.ruleEditIndex);
  }

  function showToast(text, ms = 1800) {
    if (!STATE.overlay) return;
    const toast = STATE.overlay.querySelector('#umr-toast');
    if (!toast) return;
    toast.textContent = text;
    toast.classList.add('show');
    clearTimeout(showToast._timer);
    showToast._timer = setTimeout(() => {
      toast.classList.remove('show');
    }, ms);
  }

  function showLoader(text = tf('loader_processing')) {
    if (!STATE.overlay) return;
    const loader = STATE.overlay.querySelector('#umr-loader');
    const loaderText = STATE.overlay.querySelector('#umr-loader-text');
    if (loaderText) loaderText.textContent = text;
    if (loader) loader.style.display = 'flex';
  }

  function hideLoader() {
    if (!STATE.overlay) return;
    const loader = STATE.overlay.querySelector('#umr-loader');
    if (loader) loader.style.display = 'none';
  }

  function lockPageScroll() {
    if (STATE.scrollLocked) return;
    document.documentElement.style.overflow = 'hidden';
    document.body.style.overflow = 'hidden';
    STATE.scrollLocked = true;
  }

  function unlockPageScroll() {
    if (!STATE.scrollLocked) return;
    document.documentElement.style.overflow = '';
    document.body.style.overflow = '';
    STATE.scrollLocked = false;
  }

  async function requestWakeLock() {
    if (!STATE.wakeLockSupported || document.visibilityState !== 'visible') return;
    try {
      STATE.wakeLockSentinel = await navigator.wakeLock.request('screen');
    } catch {}
  }

  async function releaseWakeLock() {
    try {
      await STATE.wakeLockSentinel?.release();
    } catch {}
    STATE.wakeLockSentinel = null;
  }

  async function refreshWakeLockOnVisibility() {
    if (!STATE.isOpen) return;
    if (document.visibilityState === 'visible') {
      await requestWakeLock();
    } else {
      await releaseWakeLock();
    }
  }

  function extractUrlsFromSrcset(srcset) {
    if (!srcset) return [];
    return String(srcset)
      .split(',')
      .map(part => {
        const m = part.trim().match(/^(.+?)(?:\s+\d+(?:\.\d+)?[wx])?$/);
        return normalizeUrl(m ? m[1].trim() : '');
      })
      .filter(Boolean);
  }

  function extractUrlsFromStyleBackground(styleText) {
    const result = [];
    if (!styleText) return result;
    const regex = /url\((['"]?)(.*?)\1\)/ig;
    let m;
    while ((m = regex.exec(styleText))) {
      const url = normalizeUrl(m[2]);
      if (url) result.push(url);
    }
    return result;
  }

  function uniquePush(list, seen, url) {
    if (!url || seen.has(url)) return;
    seen.add(url);
    list.push(url);
  }

  function collectCandidateUrlsFromImg(img) {
    const urls = [];
    const attrs = [
      img.currentSrc,
      img.src,
      img.getAttribute('data-src'),
      img.getAttribute('data-original'),
      img.getAttribute('data-lazy-src'),
      img.getAttribute('data-url'),
      img.getAttribute('data-echo'),
      img.getAttribute('data-cfsrc'),
      img.getAttribute('data-pagespeed-lazy-src'),
      img.getAttribute('data-lazy'),
      img.getAttribute('data-image'),
      img.getAttribute('data-file'),
      img.getAttribute('data-thumb')
    ];

    attrs.forEach(v => {
      const u = normalizeUrl(v);
      if (u) urls.push(u);
    });

    extractUrlsFromSrcset(img.srcset || img.getAttribute('data-srcset') || '').forEach(u => urls.push(u));
    return urls;
  }

  function collectCandidateUrlsFromDocument() {
    const urls = [];
    const seen = new Set();

    document.querySelectorAll('img').forEach(img => {
      collectCandidateUrlsFromImg(img).forEach(u => uniquePush(urls, seen, u));
    });

    document.querySelectorAll('source').forEach(source => {
      extractUrlsFromSrcset(source.srcset || source.getAttribute('data-srcset') || '').forEach(u => uniquePush(urls, seen, u));
    });

    document.querySelectorAll('[style]').forEach(el => {
      const styleAttr = el.getAttribute('style') || '';
      extractUrlsFromStyleBackground(styleAttr).forEach(u => uniquePush(urls, seen, u));
    });

    Array.from(document.querySelectorAll('a[href]')).forEach(a => {
      const href = normalizeUrl(a.getAttribute('href'));
      if (/\.(?:jpg|jpeg|png|webp|gif|bmp|avif)(?:$|\?)/i.test(href)) {
        uniquePush(urls, seen, href);
      }
    });

    const htmlText = document.documentElement?.outerHTML || '';
    const regex = /(["'`])((?:https?:)?\/\/[^"'`\s<>]+?\.(?:jpg|jpeg|png|webp|gif|bmp|avif)(?:\?[^"'`\s<>]*)?|\/[^"'`\s<>]+?\.(?:jpg|jpeg|png|webp|gif|bmp|avif)(?:\?[^"'`\s<>]*)?)\1/ig;
    let match;
    while ((match = regex.exec(htmlText))) {
      const u = normalizeUrl(match[2]);
      uniquePush(urls, seen, u);
    }

    return urls.slice(0, STATE.config.proactiveMaxCandidates);
  }

  async function ensureLazyImagesVisible() {
    const lazySelectors = [
      'img[loading="lazy"]',
      'img[data-src]',
      'img[data-original]',
      'img[data-lazy-src]',
      'img[data-url]',
      'img[data-echo]',
      'img[data-cfsrc]',
      'img[data-pagespeed-lazy-src]',
      'img[data-lazy]',
      'img[data-image]',
      'img[data-file]',
      'source[data-srcset]'
    ];

    document.querySelectorAll(lazySelectors.join(',')).forEach(el => {
      if (el.tagName === 'IMG') {
        const img = el;
        const candidates = [
          img.getAttribute('data-src'),
          img.getAttribute('data-original'),
          img.getAttribute('data-lazy-src'),
          img.getAttribute('data-url'),
          img.getAttribute('data-echo'),
          img.getAttribute('data-cfsrc'),
          img.getAttribute('data-pagespeed-lazy-src'),
          img.getAttribute('data-lazy'),
          img.getAttribute('data-image'),
          img.getAttribute('data-file')
        ].filter(Boolean);

        if ((!img.src || img.src.startsWith('data:')) && candidates[0]) img.src = candidates[0];
        if (!img.srcset && img.dataset?.srcset) img.srcset = img.dataset.srcset;
        try {
          img.loading = 'eager';
          img.decoding = 'sync';
          img.fetchPriority = 'high';
        } catch {}
      } else if (el.tagName === 'SOURCE') {
        const srcset = el.getAttribute('data-srcset');
        if (srcset && !el.srcset) el.srcset = srcset;
      }
    });

    const allImgs = Array.from(document.images);
    allImgs.forEach(img => {
      try {
        img.loading = 'eager';
        img.decoding = 'sync';
        img.fetchPriority = 'high';
      } catch {}
    });

    const originalY = window.scrollY;
    const maxScroll = Math.max(
      document.documentElement.scrollHeight,
      document.body.scrollHeight
    );
    const step = Math.max(480, Math.floor(window.innerHeight * 0.9));

    for (let y = 0; y <= maxScroll; y += step) {
      window.scrollTo(0, y);
      await sleep(40);
    }

    window.scrollTo(0, maxScroll);
    await sleep(120);
    window.scrollTo(0, originalY);
    await sleep(80);
  }

  function getPageImageLoadSummary() {
    const images = Array.from(document.images);
    let loaded = 0;
    let largeLoaded = 0;

    images.forEach(img => {
      const w = img.naturalWidth || img.width || 0;
      const h = img.naturalHeight || img.height || 0;
      if (w > 0 && h > 0) loaded++;
      if (w >= STATE.config.minWidth && h >= STATE.config.minHeight) largeLoaded++;
    });

    return { total: images.length, loaded, largeLoaded };
  }

  async function warmImageUrl(url, timeoutMs = 9000) {
    if (!url) return null;

    if (STATE.proactiveMetaMap.has(url)) {
      const cached = STATE.proactiveMetaMap.get(url);
      if (cached && cached.ok) return cached;
    }

    return new Promise(resolve => {
      const img = new Image();
      let settled = false;

      const done = (payload) => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        resolve(payload);
      };

      img.decoding = 'async';
      img.referrerPolicy = 'no-referrer-when-downgrade';

      img.onload = () => {
        const meta = {
          src: url,
          width: img.naturalWidth || 0,
          height: img.naturalHeight || 0,
          ok: true,
          fromProbe: true
        };
        STATE.proactiveMetaMap.set(url, meta);
        done(meta);
      };

      img.onerror = () => {
        const meta = { src: url, width: 0, height: 0, ok: false, fromProbe: true };
        STATE.proactiveMetaMap.set(url, meta);
        done(meta);
      };

      const timer = setTimeout(() => {
        const meta = { src: url, width: 0, height: 0, ok: false, fromProbe: true, timeout: true };
        STATE.proactiveMetaMap.set(url, meta);
        done(meta);
      }, timeoutMs);

      img.src = url;
    });
  }

  async function proactiveDiscoverAndWarmImages() {
    STATE.proactiveDoneCount = 0;
    STATE.proactiveCandidates = collectCandidateUrlsFromDocument();

    const total = STATE.proactiveCandidates.length;
    if (!total) return;

    const queue = STATE.proactiveCandidates.slice(0);
    const workers = [];
    const concurrency = Math.min(STATE.config.proactiveConcurrency, total);

    for (let i = 0; i < concurrency; i++) {
      workers.push((async () => {
        while (queue.length) {
          const url = queue.shift();
          if (!url) break;
          await warmImageUrl(url);
          STATE.proactiveDoneCount++;
          if (STATE.overlay) {
            showLoader(tf('loader_detecting_progress', { done: STATE.proactiveDoneCount, total }));
          }
        }
      })());
    }

    await Promise.all(workers);
  }

  function scoreContainerHint(el) {
    if (!el) return 0;
    const text = [
      el.className || '',
      el.id || '',
      el.getAttribute?.('data-role') || '',
      el.getAttribute?.('role') || ''
    ].join(' ');
    return /manga|comic|chapter|viewer|reader|page|content|image|img|post/i.test(text) ? 12 : 0;
  }

  function smartCollectMangaPages() {
    const seen = new Set();
    const result = [];
    const viewportArea = Math.max(1, window.innerWidth * window.innerHeight);
    const minArea = Math.min(STATE.config.minArea, Math.floor(viewportArea * 0.22));

    const allImgs = Array.from(document.images);

    allImgs.forEach((img, idx) => {
      const urls = collectCandidateUrlsFromImg(img);
      const src = urls.find(Boolean);
      if (!src || seen.has(src)) return;

      const width = img.naturalWidth || img.width || 0;
      const height = img.naturalHeight || img.height || 0;
      if (!width || !height) return;

      const area = width * height;
      const ratio = width / Math.max(1, height);

      if (width < STATE.config.minWidth) return;
      if (height < STATE.config.minHeight) return;
      if (area < minArea) return;
      if (ratio < STATE.config.aspectMin || ratio > STATE.config.aspectMax) return;

      const rect = img.getBoundingClientRect();
      const score =
        Math.min(area / 100000, 100) +
        (rect.top >= -window.innerHeight && rect.top <= document.documentElement.clientHeight * 3 ? 8 : 0) +
        scoreContainerHint(img.closest('[class],[id]')) +
        (/page|comic|manga|chapter|content|viewer|reader/i.test(
          [
            img.className,
            img.id,
            img.alt,
            img.closest('[class],[id]')?.className || '',
            img.closest('[class],[id]')?.id || ''
          ].join(' ')
        ) ? 12 : 0);

      result.push({
        src,
        alt: img.alt || '',
        width,
        height,
        score,
        order: idx,
        from: 'dom'
      });
      seen.add(src);
    });

    STATE.proactiveCandidates.forEach((src, idx) => {
      if (!src || seen.has(src)) return;
      const meta = STATE.proactiveMetaMap.get(src);
      if (!meta || !meta.ok) return;

      const width = meta.width || 0;
      const height = meta.height || 0;
      if (!width || !height) return;

      const area = width * height;
      const ratio = width / Math.max(1, height);

      if (width < STATE.config.minWidth) return;
      if (height < STATE.config.minHeight) return;
      if (area < minArea) return;
      if (ratio < STATE.config.aspectMin || ratio > STATE.config.aspectMax) return;

      let score = Math.min(area / 100000, 90);
      if (/chapter|comic|manga|page|reader|viewer/i.test(src)) score += 8;
      if (/\.webp|\.jpg|\.jpeg|\.png/i.test(src)) score += 4;

      result.push({
        src,
        alt: '',
        width,
        height,
        score,
        order: 100000 + idx,
        from: 'probe'
      });
      seen.add(src);
    });

    result.sort((a, b) => a.order - b.order);

    return result.filter((page, i, arr) => {
      const prev = arr[i - 1];
      if (!prev) return true;
      return page.src !== prev.src;
    });
  }

  function preloadAround(index) {
    if (!STATE.pages.length) return;
    const max = Math.min(STATE.pages.length - 1, index + STATE.preloadCount);
    const min = Math.max(0, index - 2);

    for (let i = min; i <= max; i++) {
      const src = STATE.pages[i]?.src;
      if (!src) continue;
      const img = new Image();
      img.decoding = 'async';
      img.src = src;
    }
  }

  function findNextChapterCandidate() {
    const keywords = [
      '下一章', '下页', '下一頁', '下一话', '下一話', '下一回', '下一卷',
      'next', 'next chapter', 'next page', 'older', '继续', '繼續'
    ];

    const selectors = [
      'a[href]',
      'button',
      '[role="button"]',
      '[onclick]'
    ];

    const nodes = Array.from(document.querySelectorAll(selectors.join(',')));
    let best = null;
    let bestScore = -Infinity;

    nodes.forEach(node => {
      const text = (node.textContent || node.getAttribute('aria-label') || node.title || '').trim();
      const href = node.href || node.getAttribute('href') || '';

      const hay = `${text} ${href}`.toLowerCase();
      if (!keywords.some(k => hay.includes(k.toLowerCase()))) return;

      const rect = node.getBoundingClientRect();
      let score = 0;

      if (keywords.some(k => text.toLowerCase().includes(k.toLowerCase()))) score += 40;
      if (/chapter|chap|page|next|下/.test(href.toLowerCase())) score += 20;
      if (rect.right > window.innerWidth * 0.55) score += 8;
      if (rect.top > window.innerHeight * 0.35) score += 5;
      if (node.offsetParent) score += 3;

      if (score > bestScore) {
        bestScore = score;
        best = node;
      }
    });

    return best;
  }

  function jumpToNextChapter(target) {
    if (!target) return false;

    try {
      sessionStorage.setItem(AUTO_NEXT_FLAG, '1');
    } catch {}

    try {
      if (target.href) {
        location.href = target.href;
        return true;
      }
      target.click();
      return true;
    } catch {
      return false;
    }
  }

  function computeDoublePageWidths(renderList, stageRect) {
    const gap = 8;
    const sidePadding = 24;
    const usableHeight = Math.max(100, stageRect.height || window.innerHeight);
    const availableWidth = Math.max(160, (stageRect.width || window.innerWidth) - sidePadding - gap);

    const naturalWidths = renderList.map(page => {
      const pw = Math.max(1, Number(page?.width || 1));
      const ph = Math.max(1, Number(page?.height || 1));
      return Math.max(60, (pw / ph) * usableHeight);
    });

    const totalNatural = naturalWidths.reduce((a, b) => a + b, 0);
    const scale = totalNatural > availableWidth ? availableWidth / totalNatural : 1;

    return naturalWidths.map(w => Math.max(60, Math.floor(w * scale)));
  }

  function getVisiblePages() {
    if (STATE.mode === 'single') return [STATE.pages[STATE.index]].filter(Boolean);
    const first = STATE.pages[STATE.index];
    const second = STATE.pages[STATE.index + 1];
    return [first, second].filter(Boolean);
  }

  function classifyPageShape(page, forceDouble = false) {
    if (!page) return 'portrait';
    const ratio = (page.width || 1) / (page.height || 1);

    if (forceDouble) {
      if (ratio > 1.2) return 'landscape';
      if (ratio > 0.88) return 'square';
      return 'portrait';
    }

    if (ratio > 1.05) return 'landscape';
    if (ratio > 0.9) return 'square';
    return 'portrait';
  }

  function renderPages(direction = '') {
    if (!STATE.overlay || !STATE.pages.length) return;

    const book = STATE.overlay.querySelector('#umr-book');
    const stage = STATE.overlay.querySelector('#umr-stage');
    if (!book || !stage) return;

    book.innerHTML = '';
    const renderList = getVisiblePages();
    const isDouble = STATE.mode === 'double';
    const stageRect = stage.getBoundingClientRect();
    const doubleWidths = isDouble ? computeDoublePageWidths(renderList, stageRect) : [];

    book.classList.toggle('umr-book-double', isDouble);

    renderList.forEach((page, i) => {
      const shape = classifyPageShape(page, isDouble);

      const wrap = document.createElement('div');
      wrap.className = [
        'umr-pageWrap',
        isDouble ? 'double' : 'single',
        shape,
        direction ? `turn-${direction}` : ''
      ].filter(Boolean).join(' ');

      if (isDouble) {
        const targetWidth = doubleWidths[i] || 120;
        wrap.style.width = `${targetWidth}px`;
        wrap.style.maxWidth = `${targetWidth}px`;
        wrap.style.flex = `0 0 ${targetWidth}px`;
      } else {
        wrap.style.width = '';
        wrap.style.maxWidth = '';
        wrap.style.flex = '';
      }

      const img = document.createElement('img');
      img.className = 'umr-page';
      img.src = page.src;
      img.alt = page.alt || `Page ${STATE.index + i + 1}`;
      img.draggable = false;
      img.decoding = 'async';
      img.loading = 'eager';

      const meta = document.createElement('div');
      meta.className = 'umr-pageMeta';
      meta.textContent = `${page.width}×${page.height}`;

      wrap.appendChild(img);
      wrap.appendChild(meta);
      book.appendChild(wrap);
    });

    updateTopbar();
    updateControls();
    updateProgress();
    preloadAround(STATE.index);

    clearAutoNextTimer();
    scheduleAutoPlayIfNeeded();

    if (!STATE.autoPlayEnabled && isAtLastPage()) {
      scheduleAutoNextIfNeeded();
    }
  }

  function nextPage(fromAutoPlay = false) {
    if (!STATE.pages.length) return;
    clearAutoNextTimer();
    clearAutoPlayTimer();

    const step = getCurrentStep();
    const next = Math.min(STATE.pages.length - 1, STATE.index + step);

    if (next === STATE.index) {
      if (!fromAutoPlay) showToast(tf('toast_already_last'));
      scheduleAutoNextIfNeeded();
      return;
    }

    STATE.index = next;
    renderPages('next');
  }

  function prevPage() {
    if (!STATE.pages.length) return;
    clearAllAsyncTimers();
    const step = getCurrentStep();
    const prev = Math.max(0, STATE.index - step);
    if (prev === STATE.index) {
      showToast(tf('toast_already_first'));
      return;
    }
    STATE.index = prev;
    renderPages('prev');
  }

  function goToPage(index) {
    const safeIndex = Math.max(0, Math.min(STATE.pages.length - 1, index));
    if (safeIndex === STATE.index) return;
    const direction = safeIndex > STATE.index ? 'next' : 'prev';
    clearAllAsyncTimers();
    STATE.index = safeIndex;
    renderPages(direction);
  }

  function isAtLastPage() {
    if (!STATE.pages.length) return false;
    if (STATE.mode === 'double') return STATE.index >= STATE.pages.length - 2 || STATE.index >= STATE.pages.length - 1;
    return STATE.index >= STATE.pages.length - 1;
  }

  function getCurrentStep() {
    return STATE.mode === 'double' ? 2 : 1;
  }

  function getCurrentVisibleLastPageIndex() {
    if (!STATE.pages.length) return 0;
    if (STATE.mode === 'double') return Math.min(STATE.pages.length - 1, STATE.index + 1);
    return STATE.index;
  }

  function toggleMode() {
    clearAllAsyncTimers();
    STATE.mode = STATE.mode === 'single' ? 'double' : 'single';
    saveReaderState();
    renderPages();
    showToast(STATE.mode === 'single' ? tf('toast_mode_single') : tf('toast_mode_double'));
  }

  function toggleUI(force) {
    if (!STATE.overlay) return;

    if (typeof force === 'boolean') {
      STATE.overlay.classList.toggle('umr-ui-hidden', force);
    } else {
      STATE.overlay.classList.toggle('umr-ui-hidden');
    }

    saveReaderState();
    updateControls();
  }

  function toggleMore(force) {
    if (!STATE.overlay) return;
    const panel = STATE.overlay.querySelector('#umr-more-panel');
    if (!panel) return;

    STATE.moreOpen = typeof force === 'boolean' ? force : !STATE.moreOpen;
    panel.classList.toggle('show', STATE.moreOpen);
    updateControls();
  }

  function toggleAutoNext() {
    STATE.autoNextEnabled = !STATE.autoNextEnabled;
    saveReaderState();
    updateControls();
    clearAutoNextTimer();

    if (!STATE.autoNextEnabled) {
      showToast(tf('toast_auto_next_off'));
      return;
    }

    showToast(tf('toast_auto_next_on'));
    if (isAtLastPage()) scheduleAutoNextIfNeeded();
  }

  function toggleAutoPlay() {
    STATE.autoPlayEnabled = !STATE.autoPlayEnabled;

    if (STATE.autoPlayEnabled && STATE.shakePagingEnabled) {
      STATE.shakePagingEnabled = false;
      resetShakeDetector();
      showToast(tf('toast_shake_off_due_autoplay'));
    }

    saveReaderState();
    updateControls();
    updateProgress();
    clearAutoPlayTimer();
    clearAutoNextTimer();

    if (!STATE.autoPlayEnabled) {
      showToast(tf('toast_auto_play_off'));
      return;
    }

    showToast(tf('toast_auto_play_on', { seconds: Math.round(STATE.autoPlayDelay / 1000) }));
    scheduleAutoPlayIfNeeded();
  }

  function cycleAutoPlayDelay() {
    const options = [3000, 5000, 7000, 10000];
    const idx = options.indexOf(STATE.autoPlayDelay);
    STATE.autoPlayDelay = options[(idx + 1) % options.length];

    saveReaderState();
    updateControls();
    updateProgress();

    if (STATE.autoPlayEnabled) {
      clearAutoPlayTimer();
      scheduleAutoPlayIfNeeded();
    }
  }

  async function toggleShakePaging() {
    if (!STATE.shakePagingEnabled) {
      const granted = await requestMotionPermissionIfNeeded();
      if (!granted) {
        showToast(tf('toast_motion_permission_denied'));
        return;
      }

      bindShakeMotionIfNeeded();

      if (STATE.autoPlayEnabled) {
        STATE.autoPlayEnabled = false;
        clearAutoPlayTimer();
      }

      STATE.shakePagingEnabled = true;
      resetShakeDetector();

      saveReaderState();
      updateControls();
      updateProgress();
      showToast(tf('toast_shake_on', { level: STATE.shakeSensitivity }));
      return;
    }

    STATE.shakePagingEnabled = false;
    resetShakeDetector();
    saveReaderState();
    updateControls();
    showToast(tf('toast_shake_off'));
  }

  function cycleShakeSensitivity() {
    const idx = SHAKE_SENSITIVITY_LEVELS.indexOf(STATE.shakeSensitivity);
    STATE.shakeSensitivity = SHAKE_SENSITIVITY_LEVELS[(idx + 1) % SHAKE_SENSITIVITY_LEVELS.length];
    resetShakeDetector();
    saveReaderState();
    updateControls();
    showToast(tf('toast_shake_sensitivity', { level: STATE.shakeSensitivity }));
  }

  async function toggleFullscreen() {
    if (!STATE.overlay) return;
    try {
      if (!document.fullscreenElement) {
        await STATE.overlay.requestFullscreen();
      } else {
        await document.exitFullscreen();
      }
    } catch {
      showToast(tf('toast_fullscreen_fail'));
    }
  }

  function syncFullscreenState() {
    STATE.fullscreen = !!document.fullscreenElement;
    const btn = STATE.overlay?.querySelector('#umr-fullscreen');
    if (btn) btn.classList.toggle('umr-active', STATE.fullscreen);
  }

  function toggleLanguage() {
    STATE.lang = STATE.lang === 'en' ? 'zh' : 'en';
    saveLanguage(STATE.lang);
    saveReaderState();
    applyI18nToOverlay();
    renderRuleList();
    updateTopbar();
    updateControls();
    updateProgress();
    showToast(tf('toast_lang_switched', { lang: STATE.lang === 'en' ? tf('language_en') : tf('language_zh') }));
  }

  function applyI18nToOverlay() {
    if (!STATE.overlay) return;
    updateTopbar();
    updateControls();
    renderRuleList();
  }

  async function openReader() {
    createOverlay();
    if (!STATE.overlay) return;

    clearAllAsyncTimers();
    toggleMore(false);

    STATE.overlay.classList.remove('umr-hidden');
    STATE.isOpen = true;
    lockPageScroll();
    await requestWakeLock();

    showLoader(tf('loader_trigger_lazy'));
    await ensureLazyImagesVisible();

    const summary = getPageImageLoadSummary();
    console.log('[UMR] Initial image load summary:', summary);

    showLoader(tf('loader_detect_sources'));
    await proactiveDiscoverAndWarmImages();

    const finalSummary = getPageImageLoadSummary();
    console.log('[UMR] Final image load summary:', finalSummary, 'probe=', STATE.proactiveMetaMap.size);

    showLoader(tf('loader_analyzing_pages', {
      domCount: finalSummary.largeLoaded,
      probeCount: STATE.proactiveMetaMap.size
    }));
    STATE.pages = smartCollectMangaPages();

    if (!STATE.pages.length) {
      showLoader(tf('loader_no_pages'));
      showToast(tf('toast_not_found_pages'));
      return;
    }

    STATE.index = 0;
    loadReaderState();
    applyI18nToOverlay();
    bindShakeMotionIfNeeded();

    if (STATE.shakePagingEnabled) {
      resetShakeDetector();
    }

    hideLoader();
    renderPages();

    if (STATE.restoreUiHidden) {
      toggleUI(true);
    } else {
      toggleUI(false);
    }

    preloadAround(0);
    showToast(tf('toast_loaded_pages', { count: STATE.pages.length }));
  }

  async function closeReader() {
    saveReaderState();
    clearAllAsyncTimers();

    if (!STATE.overlay) return;

    STATE.isOpen = false;
    toggleMore(false);
    toggleUI(false);
    closeRuleDialog();
    STATE.overlay.classList.add('umr-hidden');
    unlockPageScroll();
    await releaseWakeLock();

    const help = STATE.overlay.querySelector('#umr-help');
    if (help) help.classList.remove('show');

    if (document.fullscreenElement) {
      document.exitFullscreen().catch(() => {});
    }
  }

  function createOverlay() {
    if (STATE.overlay) return STATE.overlay;

    const overlay = document.createElement('div');
    overlay.id = 'umr-overlay';
    overlay.className = 'umr-hidden';

    overlay.innerHTML = `
      <div id="umr-topbar">
        <div class="umr-title" id="umr-title">${escapeHtml(tf('app_title'))}</div>
      </div>

      <div id="umr-stage">
        <div class="umr-nav-zone umr-nav-left"></div>
        <div class="umr-nav-center"></div>
        <div class="umr-nav-zone umr-nav-right"></div>

        <button id="umr-ui-unlock" class="umr-icon-btn" title="${escapeHtml(tf('btn_ui_unlock_title'))}">✕</button>

        <div id="umr-book"></div>
        <div id="umr-toast"></div>

        <div id="umr-loader">
          <div class="umr-spinner"></div>
          <div id="umr-loader-text">${escapeHtml(tf('loader_analyzing_current_page'))}</div>
        </div>

        <div id="umr-rule-dialog-backdrop">
          <div id="umr-rule-dialog">
            <div id="umr-rule-toolbar">
              <div id="umr-rule-title">${escapeHtml(tf('rule_dialog_title'))}</div>
              <button class="umr-btn" id="umr-rule-add">+</button>
              <button class="umr-btn umr-icon-btn" id="umr-rule-close" title="${escapeHtml(tf('close'))}">✕</button>
            </div>

            <div id="umr-rule-body">
              <div class="umr-rule-tip">${tf('rule_tip_html')}</div>

              <div id="umr-rule-list"></div>

              <div id="umr-rule-editor">
                <textarea id="umr-rule-textarea" spellcheck="false" placeholder="${escapeHtml(tf('rule_input_placeholder'))}"></textarea>
                <div class="umr-rule-editor-actions">
                  <button class="umr-btn" id="umr-rule-save">${escapeHtml(tf('save'))}</button>
                  <button class="umr-btn" id="umr-rule-cancel">${escapeHtml(tf('cancel'))}</button>
                  <button class="umr-btn umr-danger" id="umr-rule-delete-current">${escapeHtml(tf('delete'))}</button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div id="umr-bottombar">
        <div id="umr-more-panel">
          <div id="umr-help">${tf('help_html')}</div>

          <div id="umr-more-grid">
            <button class="umr-btn umr-more-item" id="umr-mode">${escapeHtml(tf('mode_single'))}</button>
            <button class="umr-btn umr-more-item umr-active" id="umr-auto-next">${escapeHtml(tf('auto_next'))}</button>
            <button class="umr-btn umr-more-item" id="umr-shake-toggle">${escapeHtml(tf('shake_paging'))}</button>
            <button class="umr-btn umr-more-item" id="umr-shake-sensitivity">${escapeHtml(tf('shake_sensitivity', { level: 3 }))}</button>
            <button class="umr-btn umr-more-item" id="umr-auto-open-manager">${escapeHtml(tf('auto_open_manager'))}</button>
            <button class="umr-btn umr-more-item" id="umr-language-toggle">${escapeHtml(tf('language'))}: ${escapeHtml(tf('language_en'))}</button>
            <button class="umr-btn umr-more-item" id="umr-help-toggle">${escapeHtml(tf('help'))}</button>
          </div>
        </div>

        <div id="umr-control-row">
          <button class="umr-btn" id="umr-auto-play">${escapeHtml(tf('auto_play'))}</button>
          <button class="umr-btn" id="umr-auto-play-delay">${escapeHtml(tf('btn_seconds', { seconds: 3 }))}</button>
          <button class="umr-btn" id="umr-ui-toggle">${escapeHtml(tf('hide_ui'))}</button>
          <button class="umr-btn umr-icon-btn" id="umr-fullscreen" title="${escapeHtml(tf('btn_fullscreen_title'))}">⛶</button>
          <button class="umr-btn umr-icon-btn" id="umr-close" title="${escapeHtml(tf('btn_close_title'))}">✕</button>
          <button class="umr-btn" id="umr-more-toggle">${escapeHtml(tf('more'))}</button>
        </div>

        <div id="umr-progress-row">
          <div id="umr-progress">
            <div id="umr-progress-fill"></div>
            <div id="umr-progress-handle"></div>
          </div>
          <div id="umr-counter">0 / 0</div>
        </div>
      </div>
    `;

    document.body.appendChild(overlay);
    STATE.overlay = overlay;
    bindOverlayEvents();
    applyI18nToOverlay();
    return overlay;
  }

  function bindOverlayEvents() {
    const overlay = document.getElementById('umr-overlay');
    if (!overlay) return;

    const stage = overlay.querySelector('#umr-stage');

    overlay.querySelector('#umr-close').addEventListener('click', closeReader);
    overlay.querySelector('#umr-auto-play').addEventListener('click', toggleAutoPlay);
    overlay.querySelector('#umr-auto-play-delay').addEventListener('click', cycleAutoPlayDelay);
    overlay.querySelector('#umr-auto-next').addEventListener('click', toggleAutoNext);
    overlay.querySelector('#umr-mode').addEventListener('click', toggleMode);
    overlay.querySelector('#umr-shake-toggle').addEventListener('click', toggleShakePaging);
    overlay.querySelector('#umr-shake-sensitivity').addEventListener('click', cycleShakeSensitivity);
    overlay.querySelector('#umr-auto-open-manager').addEventListener('click', openRuleDialog);
    overlay.querySelector('#umr-language-toggle').addEventListener('click', toggleLanguage);
    overlay.querySelector('#umr-fullscreen').addEventListener('click', toggleFullscreen);
    overlay.querySelector('#umr-ui-toggle').addEventListener('click', toggleUI);
    overlay.querySelector('#umr-more-toggle').addEventListener('click', () => toggleMore());
    overlay.querySelector('#umr-help-toggle').addEventListener('click', () => {
      overlay.querySelector('#umr-help').classList.toggle('show');
    });

    overlay.querySelector('#umr-ui-unlock').addEventListener('click', (e) => {
      e.stopPropagation();
      if (overlay.classList.contains('umr-ui-hidden')) {
        toggleUI(false);
      }
    });

    overlay.querySelector('#umr-rule-close').addEventListener('click', closeRuleDialog);
    overlay.querySelector('#umr-rule-add').addEventListener('click', () => startRuleEdit(-1));
    overlay.querySelector('#umr-rule-save').addEventListener('click', saveRuleFromEditor);
    overlay.querySelector('#umr-rule-cancel').addEventListener('click', cancelRuleEdit);
    overlay.querySelector('#umr-rule-delete-current').addEventListener('click', deleteCurrentEditingRule);
    overlay.querySelector('#umr-rule-dialog-backdrop').addEventListener('click', (e) => {
      if (e.target === overlay.querySelector('#umr-rule-dialog-backdrop')) {
        closeRuleDialog();
      }
    });

    overlay.querySelector('.umr-nav-left').addEventListener('click', () => prevPage());
    overlay.querySelector('.umr-nav-right').addEventListener('click', () => nextPage());
    overlay.querySelector('.umr-nav-center').addEventListener('click', () => toggleUI());

    const progress = overlay.querySelector('#umr-progress');
    progress.addEventListener('click', (e) => {
      if (!STATE.pages.length) return;
      const rect = progress.getBoundingClientRect();
      const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
      const target = Math.round(ratio * Math.max(STATE.pages.length - 1, 0));
      goToPage(target);
    });

    stage.addEventListener('touchstart', (e) => {
      if (!e.touches[0]) return;
      STATE.touchStartX = e.touches[0].clientX;
      STATE.touchStartY = e.touches[0].clientY;
      STATE.touchMoved = false;
    }, { passive: true });

    stage.addEventListener('touchmove', (e) => {
      if (!e.touches[0]) return;
      const dx = e.touches[0].clientX - STATE.touchStartX;
      const dy = e.touches[0].clientY - STATE.touchStartY;
      if (Math.abs(dx) > 12 || Math.abs(dy) > 12) STATE.touchMoved = true;
    }, { passive: true });

    stage.addEventListener('touchend', (e) => {
      const touch = e.changedTouches[0];
      if (!touch) return;

      const dx = touch.clientX - STATE.touchStartX;
      const dy = touch.clientY - STATE.touchStartY;

      if (!STATE.touchMoved) {
        const width = window.innerWidth;
        if (touch.clientX < width * 0.30) prevPage();
        else if (touch.clientX > width * 0.70) nextPage();
        else toggleUI();
        return;
      }

      if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) {
        if (dx < 0) nextPage();
        else prevPage();
      }
    }, { passive: true });

    if (!STATE.keyHandlerBound) {
      document.addEventListener('keydown', onKeydown, true);
      STATE.keyHandlerBound = true;
    }

    if (!STATE.resizeHandlerBound) {
      window.addEventListener('resize', debounce(() => {
        if (STATE.isOpen) renderPages();
      }, 80));
      STATE.resizeHandlerBound = true;
    }

    if (!STATE.fullscreenHandlerBound) {
      document.addEventListener('fullscreenchange', syncFullscreenState, true);
      STATE.fullscreenHandlerBound = true;
    }

    if (!STATE.visibilityHandlerBound) {
      document.addEventListener('visibilitychange', refreshWakeLockOnVisibility, true);
      STATE.visibilityHandlerBound = true;
    }
  }

  function onKeydown(e) {
    if (!STATE.isOpen) return;
    const key = String(e.key || '').toLowerCase();

    if (key === 'arrowright' || key === 'd' || key === ' ') {
      e.preventDefault();
      nextPage();
    } else if (key === 'arrowleft' || key === 'a') {
      e.preventDefault();
      prevPage();
    } else if (key === 'escape') {
      if (STATE.ruleDialogOpen) {
        e.preventDefault();
        closeRuleDialog();
        return;
      }
      e.preventDefault();
      closeReader();
    } else if (key === 'f') {
      e.preventDefault();
      toggleFullscreen();
    } else if (key === 'm') {
      e.preventDefault();
      toggleMode();
    } else if (key === 'u') {
      e.preventDefault();
      toggleUI();
    } else if (key === 'n') {
      e.preventDefault();
      toggleAutoNext();
    } else if (key === 'p') {
      e.preventDefault();
      toggleAutoPlay();
    } else if (key === 't') {
      e.preventDefault();
      cycleAutoPlayDelay();
    } else if (key === 'l') {
      e.preventDefault();
      toggleLanguage();
    }
  }

  async function bootAutoOpenIfNeeded() {
    let fromNext = false;
    try {
      fromNext = sessionStorage.getItem(AUTO_NEXT_FLAG) === '1';
      if (fromNext) sessionStorage.removeItem(AUTO_NEXT_FLAG);
    } catch {}

    const fromRule = matchesAutoOpenRule(location.href);
    if (!fromNext && !fromRule) return;

    await sleep(700);
    await openReader();
  }

  bootAutoOpenIfNeeded();
})();