☰

Ex Fast Reader

With the goal of being light, discreet, and non-invasive, a QoL script for Exhentai and E-hentai. It adds features like horizontal and vertical reading, infinite scrolling, pre-fetching images, and more to make browsing extremely smooth.

スクリプトをインストールするには、Tampermonkey, Greasemonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==UserScript==
// @name         Ex Fast Reader
// @namespace    https://tampermonkey.net/
// @version      4.4.3
// @description  With the goal of being light, discreet, and non-invasive, a QoL script for Exhentai and E-hentai. It adds features like horizontal and vertical reading, infinite scrolling, pre-fetching images, and more to make browsing extremely smooth.
// @author       ethyr
// @match        *://e-hentai.org/*
// @match        *://exhentai.org/*
// @match        *://*.e-hentai.org/*
// @match        *://*.exhentai.org/*
// @icon         https://e-hentai.org/favicon.ico
// @exclude      *://forums.e-hentai.org/*
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  // =========================================================================
  // 0. CORE & SEAMLESS STRIP STYLES
  // =========================================================================
  const CORE_CSS = `
    /* Failure Toast */
    .eh-toast-fail {
      position: fixed;
      bottom: 24px;
      right: 24px;
      background: rgba(26, 18, 18, 0.96);
      color: #fca5a5;
      border: 1px solid #ef4444;
      padding: 9px 16px;
      border-radius: 6px;
      font-size: 12px;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      box-shadow: 0 4px 14px rgba(0,0,0,0.5);
      z-index: 999999;
      cursor: pointer;
    }

    /* Seamless Native-Themed Controls */
    .eh-strip-controls {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      margin: 3px 8px;
      vertical-align: middle;
    }

.eh-strip-controls.single-mode-bottom {
      display: flex !important;
      justify-content: center !important;
      align-items: center !important;
      margin: 1px auto 2px auto !important;
      padding: 0 !important;
      text-align: center !important;
      clear: both !important;
      width: 100% !important;
    }

    /* In single-page mode, strictly hide every control except the strip mode toggle button */
    .eh-strip-controls.single-mode-bottom > :not(#eh-strip-mode-toggle) {
      display: none !important;
    }

    .eh-strip-btn {
      display: inline-flex;
      align-items: center !important;
      justify-content: center !important;
      font-family: arial, helvetica, sans-serif !important;
      font-size: 9pt !important;
      font-weight: bold !important;
      min-height: 26px !important;
      line-height: 20px !important;
      padding: 1px 12px 2px !important;
      margin: 0 2px !important;
      border-radius: 3px !important;
      cursor: pointer !important;
      text-decoration: none !important;
      user-select: none !important;
      white-space: nowrap !important;
      box-sizing: border-box !important;
      -webkit-text-stroke: none !important;
      text-shadow: none !important;
      outline: none !important;
      vertical-align: middle !important;
      transition: background-color 0.1s ease, border-color 0.1s ease !important;
      border: 2px solid #B5A4A4 !important;
      background-color: #EDEADA !important;
      color: #5C0D12 !important;
    }
    .eh-strip-btn b,
    .eh-strip-btn span {
      font-weight: normal;
      -webkit-text-stroke: none !important;
      text-shadow: none !important;
    }
    .eh-strip-btn.active b {
      font-weight: bold;
    }

    .eh-strip-page-counter {
      font-family: arial, helvetica, sans-serif;
      font-size: 9pt;
      font-weight: normal;
      padding: 2px 6px;
      user-select: none;
      white-space: nowrap;
      line-height: normal;
    }

    /* E-Hentai Theme (Light) - Exact Match to Native Site Buttons */
    .eh-theme-light .eh-strip-btn,
    .eh-strip-btn {
      background-color: #EDEADA !important;
      color: #5C0D12 !important;
      border: 2px solid #B5A4A4 !important;
      border-radius: 3px !important;
      -webkit-text-stroke: none !important;
      text-shadow: none !important;
    }
    .eh-theme-light .eh-strip-btn:hover {
      background-color: #F3F0E0 !important;
      border-color: #977273 !important;
      color: #5C0D12 !important;
    }
    .eh-theme-light .eh-strip-btn.active {
      background-color: #D7D3C2 !important;
      border-color: #5C0D12 !important;
      color: #5C0D12 !important;
      box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.25) !important;
    }
    .eh-theme-light .eh-strip-page-counter,
    .eh-theme-light #eh-topbar-info {
      color: #5C0D12 !important;
      paint-order: stroke fill !important;
      -webkit-text-stroke: 0.6px #ffffff !important;
      text-shadow: none !important;
    }

    /* ExHentai Theme (Dark) - Exact Match to Native Site Buttons */
    .eh-theme-dark .eh-strip-btn {
      background-color: #34353b !important;
      color: #dddddd !important;
      border: 2px solid #8d8d8d !important;
      border-radius: 3px !important;
      -webkit-text-stroke: none !important;
      text-shadow: none !important;
    }
    .eh-theme-dark .eh-strip-btn:hover {
      background-color: #43464e !important;
      border-color: #aeaeae !important;
      color: #ffffff !important;
    }
    .eh-theme-dark .eh-strip-btn.active {
      background-color: #1a1a1a !important;
      border-color: #aeaeae !important;
      color: #ffffff !important;
      box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4) !important;
    }
    .eh-theme-dark .eh-strip-page-counter,
    .eh-theme-dark #eh-topbar-info {
      color: #dddddd !important;
      paint-order: stroke fill !important;
      -webkit-text-stroke: 0.6px #000000 !important;
      text-shadow: none !important;
    }

    /* Hide title bar (h1, #nb, #gn), original switchers, bottom bars, footers, and redundant links cleanly via CSS */
    html.eh-strip-mode-active h1,
    html.eh-strip-mode-active #nb,
    html.eh-strip-mode-active #gn,
    html.eh-strip-mode-active #gj,
    html.eh-strip-mode-active .sb,
    html.eh-strip-mode-active .sn,
    html.eh-strip-mode-active #i3,
    html.eh-strip-mode-active #i4,
    html.eh-strip-mode-active #i5,
    html.eh-strip-mode-active #i6,
    html.eh-strip-mode-active #i7,
    html.eh-strip-mode-active .c,
    html.eh-strip-mode-active .c1,
    html.eh-strip-mode-active .c2,
    html.eh-strip-mode-active .c3,
    html.eh-strip-mode-active #footer,
    html.eh-strip-mode-active .dp,
    html.eh-strip-mode-active #dp,
    html.eh-strip-mode-active .ip,
    html.eh-strip-mode-active #ip,
    html.eh-strip-mode-active .ft,
    html.eh-strip-mode-active #ft,
    html.eh-strip-mode-active .gpc,
    html.eh-strip-mode-active #gpc,
    html.eh-strip-mode-active div.d,
    html.eh-strip-mode-active #i2 > div:not(.sn):not(#eh-strip-controls):not(#eh-topbar-info),
    html.eh-strip-mode-active #i2 > p,
    html.eh-strip-mode-active #i2 > span:not(.sn),
    html.eh-strip-mode-active #i2 > a:not(#eh-strip-controls a),
    html.eh-strip-mode-active .eh-strip-hidden-bottom,
    body.eh-strip-mode-active h1,
    body.eh-strip-mode-active #nb,
    body.eh-strip-mode-active #gn,
    body.eh-strip-mode-active #gj,
    body.eh-strip-mode-active .sb,
    body.eh-strip-mode-active .sn,
    body.eh-strip-mode-active #i3,
    body.eh-strip-mode-active #i4,
    body.eh-strip-mode-active #i5,
    body.eh-strip-mode-active #i6,
    body.eh-strip-mode-active #i7,
    body.eh-strip-mode-active .c,
    body.eh-strip-mode-active .c1,
    body.eh-strip-mode-active .c2,
    body.eh-strip-mode-active .c3,
    body.eh-strip-mode-active #footer,
    body.eh-strip-mode-active .dp,
    body.eh-strip-mode-active #dp,
    body.eh-strip-mode-active .ip,
    body.eh-strip-mode-active #ip,
    body.eh-strip-mode-active .ft,
    body.eh-strip-mode-active #ft,
    body.eh-strip-mode-active .gpc,
    body.eh-strip-mode-active #gpc,
    body.eh-strip-mode-active div.d,
    body.eh-strip-mode-active #i2 > div:not(.sn):not(#eh-strip-controls):not(#eh-topbar-info),
    body.eh-strip-mode-active #i2 > p,
    body.eh-strip-mode-active #i2 > span:not(.sn),
    body.eh-strip-mode-active #i2 > a:not(#eh-strip-controls a),
    body.eh-strip-mode-active .eh-strip-hidden-bottom {
      display: none !important;
    }

    /* Hide strip topbar info when strip mode is inactive */
    body:not(.eh-strip-mode-active) #eh-topbar-info {
      display: none !important;
    }

    /* TOP BAR: Fully extends to both edges; auto-hides unless hovered or focused */
    html.eh-strip-mode-active #i2,
    body.eh-strip-mode-active #i2 {
      position: fixed !important;
      top: 0 !important;
      left: 0 !important;
      right: 0 !important;
      width: 100vw !important;
      width: 100% !important;
      max-width: 100vw !important;
      margin: 0 !important;
      padding: 6px 12px !important;
      box-sizing: border-box !important;
      z-index: 100000 !important;
      display: flex !important;
      justify-content: center !important;
      align-items: center !important;
      transform: translateY(-100%) !important;
      opacity: 0 !important;
      visibility: hidden !important;
      box-shadow: none !important;
      border: none !important;
      pointer-events: none !important;
      transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.22s ease, visibility 0.22s !important;
    }

    body.eh-theme-light #i2 {
      background-color: #edeada !important;
    }

    body.eh-theme-dark #i2 {
      background-color: rgba(52, 53, 59, 0.97) !important;
      backdrop-filter: blur(8px) !important;
    }

    body.eh-theme-light.eh-topbar-visible #i2 {
      border-bottom: 1px solid #5c0d12 !important;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important;
    }

    body.eh-theme-dark.eh-topbar-visible #i2 {
      border-bottom: 1px solid #202020 !important;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4) !important;
    }

    /* High contrast solid stroke on top-bar text without fade/glow */
    body.eh-theme-light.eh-strip-mode-active #i2,
    body.eh-theme-light.eh-strip-mode-active #i2 a:not(.eh-strip-btn),
    body.eh-theme-light.eh-strip-mode-active #i2 span:not(.eh-strip-btn):not(#eh-strip-controls *),
    body.eh-theme-light.eh-strip-mode-active #i2 b:not(#eh-strip-controls *) {
      color: #5c0d12;
      paint-order: stroke fill;
      -webkit-text-stroke: 0.6px #ffffff;
      text-shadow: none !important;
    }
    body.eh-theme-dark.eh-strip-mode-active #i2,
    body.eh-theme-dark.eh-strip-mode-active #i2 a:not(.eh-strip-btn),
    body.eh-theme-dark.eh-strip-mode-active #i2 span:not(.eh-strip-btn):not(#eh-strip-controls *),
    body.eh-theme-dark.eh-strip-mode-active #i2 b:not(#eh-strip-controls *) {
      color: #dddddd;
      paint-order: stroke fill;
      -webkit-text-stroke: 0.6px #000000;
      text-shadow: none !important;
    }

    body.eh-strip-mode-active.eh-topbar-visible #i2 {
      transform: translateY(0) !important;
      opacity: 1 !important;
      visibility: visible !important;
      pointer-events: auto !important;
    }

    /* Container constraints in horizontal mode: maximize vertical space */
    html.eh-strip-mode-active.strip-horizontal,
    body.eh-strip-mode-active.strip-horizontal {
      overflow: hidden !important;
      height: 100vh !important;
      max-height: 100vh !important;
      margin: 0 !important;
      padding: 0 !important;
    }

    html.eh-strip-mode-active.strip-horizontal #i1,
    body.eh-strip-mode-active.strip-horizontal #i1 {
      width: 100% !important;
      max-width: 100vw !important;
      height: 100vh !important;
      max-height: 100vh !important;
      margin: 0 !important;
      padding: 0 !important;
      border: none !important;
      outline: none !important;
      box-sizing: border-box !important;
      overflow: hidden !important;
      position: relative !important;
    }

    /* Vertical mode: Clean layout with no excessive bottom padding */
    html.eh-strip-mode-active.strip-vertical #i1,
    body.eh-strip-mode-active.strip-vertical #i1 {
      padding-bottom: 0 !important;
      margin-bottom: 0 !important;
    }

    /* COMPLETELY SEAMLESS RIGHT-TO-LEFT (RTL) HORIZONTAL STRIP */
    #eh-strip-container.layout-horizontal {
      display: flex;
      flex-direction: row;
      direction: rtl; /* RIGHT TO LEFT (Manga standard) */
      align-items: center;
      overflow-x: auto;
      overflow-y: hidden;
      white-space: nowrap;
      height: 100vh !important;
      max-height: 100vh !important;
      width: 100vw;
      box-sizing: border-box;
      padding: 0;
      margin: 0;
      gap: 0;
      border: none;
      scroll-behavior: auto;
      overscroll-behavior-x: contain;
      overflow-anchor: none !important;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: thin;
      scrollbar-color: #4f535b rgba(0, 0, 0, 0.22);
    }

    .eh-theme-light #eh-strip-container.layout-horizontal {
      scrollbar-color: #5c0d12 rgba(0, 0, 0, 0.22);
    }

    /* Items are sized via aspect-ratio and width calculated to match rendered 100vh height */
    #eh-strip-container.layout-horizontal .eh-strip-item {
      display: inline-flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      height: 100vh !important;
      max-height: 100vh !important;
      width: calc(100vh * var(--eh-ar, 0.707));
      aspect-ratio: var(--eh-ar, 0.707);
      margin: 0;
      padding: 0;
      border: none;
      position: relative;
      box-sizing: border-box;
      overflow: hidden;
      overflow-anchor: none !important;
    }

    #eh-strip-container.layout-horizontal .eh-strip-item img.eh-main-img {
      height: 100vh !important;
      max-height: 100vh !important;
      width: auto;
      max-width: none;
      object-fit: contain;
      display: block;
      margin: 0;
      padding: 0;
      border: none;
      box-shadow: none;
      vertical-align: middle;
      position: relative;
      z-index: 2;
    }

    /* End Card at the end of horizontal strip: generous leeway allows the last image to be centered */
    .eh-strip-item.eh-strip-end-card {
      display: inline-flex !important;
      align-items: center !important;
      justify-content: center !important;
      height: 100vh !important;
      width: auto !important;
      min-width: max(200px, calc(50vw - (100vh * var(--eh-ar, 0.707)) / 2 + 60px)) !important;
      aspect-ratio: auto !important;
      padding: 0 40px !important;
      flex: 0 0 auto;
      z-index: 3;
      box-sizing: border-box !important;
    }

    /* Start Spacer for Horizontal RTL mode: provides generous leeway on the right so Page 1 can be centered */
    .eh-strip-start-spacer {
      display: inline-flex !important;
      align-items: center !important;
      justify-content: center !important;
      height: 100vh !important;
      max-height: 100vh !important;
      width: max(40px, calc(50vw - (100vh * var(--eh-ar, 0.707)) / 2)) !important;
      margin: 0 !important;
      padding: 0 !important;
      border: none !important;
      box-sizing: border-box !important;
      flex: 0 0 auto !important;
      pointer-events: none !important;
      overflow-anchor: none !important;
      background: transparent !important;
    }

    #eh-strip-container.layout-vertical .eh-strip-start-spacer {
      display: none !important;
    }

    /* COMPLETELY SEAMLESS VERTICAL STRIP */
    #eh-strip-container.layout-vertical {
      display: flex;
      flex-direction: column;
      align-items: center;
      overflow-y: visible;
      overflow-x: hidden;
      width: 100%;
      margin: 0 auto;
      padding: 0;
      gap: 0;
      border: none;
      overflow-anchor: none !important;
    }

    #eh-strip-container.layout-vertical .eh-strip-item {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      width: 100%;
      max-width: var(--eh-max-w, 100%);
      aspect-ratio: var(--eh-ar, 0.707);
      margin: 0 auto;
      padding: 0;
      border: none;
      position: relative;
      box-sizing: border-box;
      overflow: hidden;
      overflow-anchor: none !important;
    }

    #eh-strip-container.layout-vertical .eh-strip-item img.eh-main-img {
      max-width: 100%;
      width: 100%;
      height: auto;
      aspect-ratio: var(--eh-ar, 0.707);
      display: block;
      margin: 0 auto;
      padding: 0;
      border: none;
      box-shadow: none;
      position: relative;
      z-index: 2;
    }

    /* Top Bar Image Info */
    #eh-topbar-info {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      font-family: arial, helvetica, sans-serif;
      font-size: 9pt;
      font-weight: normal;
      margin: 0 10px;
      white-space: nowrap;
      user-select: none;
      -webkit-text-stroke: none !important;
      text-shadow: none !important;
    }
    #eh-topbar-info a {
      text-decoration: underline;
    }

    /* Stretched Thumbnail Placeholders (Unzoomed 1:1) */
    .eh-placeholder-thumb-wrap {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
      pointer-events: none;
      z-index: 1;
      transform: none;
    }

    .eh-placeholder-thumb-img {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      object-fit: fill;
      filter: blur(2px);
      opacity: 1;
      pointer-events: none;
      z-index: 1;
      transform: none;
    }

    .eh-placeholder-thumb-sprite {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-repeat: no-repeat;
      filter: blur(2px);
      opacity: 1;
      transform: none;
      pointer-events: none;
    }

    /* Clean, Theme-Consistent Solid Neutral Placeholder */
    .eh-strip-item.eh-placeholder {
      box-sizing: border-box;
      position: relative;
    }

    /* Only show & animate the spinner when the page is actively loading */
    .eh-placeholder:not(.eh-loading) .eh-spinner {
      display: none !important;
    }

    .eh-theme-light .eh-strip-item.eh-placeholder {
      background-color: #e8e4d6;
      outline: 1px dashed rgba(92, 13, 18, 0.22);
      outline-offset: -1px;
      border: none;
    }

    .eh-theme-dark .eh-strip-item.eh-placeholder {
      background-color: #27282c;
      outline: 1px dashed rgba(255, 255, 255, 0.12);
      outline-offset: -1px;
      border: none;
    }

    /* Discreet, elegant frosted loading capsule */
    .eh-placeholder-content {
      display: inline-flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      gap: 7px;
      user-select: none;
      position: relative;
      z-index: 2;
      background: rgba(0, 0, 0, 0.42);
      padding: 5px 12px;
      border-radius: 14px;
      backdrop-filter: blur(3px);
      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
      pointer-events: none;
    }

    .eh-placeholder-content.eh-placeholder-retry {
      flex-direction: column;
      gap: 8px;
      padding: 12px 18px;
      border-radius: 8px;
      pointer-events: auto;
    }

    .eh-spinner {
      width: 14px;
      height: 14px;
      border: 2px solid rgba(255, 255, 255, 0.22);
      border-top-color: #ffffff;
      border-radius: 50%;
      animation: eh-spin 0.7s linear infinite;
      flex-shrink: 0;
    }

    .eh-theme-light .eh-spinner {
      border-top-color: #5c0d12;
      border-color: rgba(92, 13, 18, 0.2);
    }
    .eh-theme-dark .eh-spinner {
      border-top-color: #ffffff;
      border-color: rgba(255, 255, 255, 0.22);
    }

    @keyframes eh-spin {
      to { transform: rotate(360deg); }
    }

    .eh-placeholder-text {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, arial, sans-serif;
      font-size: 8.5pt;
      font-weight: 600;
      color: #ffffff;
      opacity: 0.9;
      letter-spacing: 0.3px;
      line-height: 1;
      white-space: nowrap;
    }

    .eh-theme-light .eh-placeholder-text {
      color: #5c0d12;
    }
    .eh-theme-dark .eh-placeholder-text {
      color: #eeeeee;
    }

    /* Scrollbar styling with comfortable leeway for horizontal sliding */
    #eh-strip-container.layout-horizontal::-webkit-scrollbar {
      height: 12px;
    }
    #eh-strip-container.layout-horizontal::-webkit-scrollbar-track {
      background: rgba(0, 0, 0, 0.22);
    }
    .eh-theme-light #eh-strip-container.layout-horizontal::-webkit-scrollbar-thumb {
      background: #5c0d12;
      border-radius: 6px;
    }
    .eh-theme-light #eh-strip-container.layout-horizontal::-webkit-scrollbar-thumb:hover {
      background: #87141b;
    }
    .eh-theme-dark #eh-strip-container.layout-horizontal::-webkit-scrollbar-thumb {
      background: #4f535b;
      border-radius: 6px;
    }
    .eh-theme-dark #eh-strip-container.layout-horizontal::-webkit-scrollbar-thumb:hover {
      background: #6a6f7b;
    }

    /* Native-Themed Single Bottom Return to Gallery Button (Compact, No Extra Space) */
    .eh-bottom-gallery-bar {
      display: flex !important;
      justify-content: center !important;
      align-items: center !important;
      margin: 15px auto 18px auto !important;
      padding: 6px 0 !important;
      text-align: center !important;
      clear: both !important;
      width: 100% !important;
    }

    .eh-bottom-gallery-bar a {
      display: inline-block;
      padding: 6px 20px;
      font-family: arial, helvetica, sans-serif;
      font-size: 10pt;
      font-weight: bold;
      text-decoration: none;
      border-radius: 3px;
      cursor: pointer;
      transition: all 0.15s ease-in-out;
    }

    .eh-theme-light .eh-bottom-gallery-bar a {
      background-color: #edeada;
      color: #5c0d12;
      border: 1px solid #5c0d12;
    }

    .eh-theme-light .eh-bottom-gallery-bar a:hover {
      background-color: #f2efdf;
      color: #9b4e03;
      border-color: #9b4e03;
    }

    .eh-theme-dark .eh-bottom-gallery-bar a {
      background-color: #34353b;
      color: #dddddd;
      border: 1px solid #4f535b;
    }

    .eh-theme-dark .eh-bottom-gallery-bar a:hover {
      background-color: #43464e;
      color: #ffffff;
      border-color: #727783;
    }

/* Infinite Scroll Button */
#eh-infinite-scroll-wrap {
  display: inline-flex !important;
  align-items: center !important;
  margin-left: 8px !important;
  vertical-align: middle !important;
}

#eh-infinite-scroll-toggle {
  font-family: inherit;
  font-size: 9pt;
  line-height: normal;
  box-sizing: border-box;
  vertical-align: middle;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 10px;
  min-height: 22px !important;
  height: 22px;
}

/* Light Theme: keep the hover background (#F3F0E0) and border (#977273) when active */
.eh-theme-light #eh-infinite-scroll-toggle.active {
  background-color: #F3F0E0 !important;
  border-color: #977273 !important;
  color: #5C0D12 !important;
  box-shadow: none !important;
}

/* Dark Theme: keep the hover background (#43464e) and border (#aeaeae) when active */
.eh-theme-dark #eh-infinite-scroll-toggle.active {
  background-color: #43464e !important;
  border-color: #aeaeae !important;
  color: #ffffff !important;
  box-shadow: none !important;
}

    /* Single page thumbnail placeholder */
    #i3 {
      position: relative !important;
      text-align: center !important;
    }
    #i3 a {
      position: relative !important;
      display: inline-block !important;
      max-width: 100% !important;
    }
    #eh-single-thumb-placeholder {
      position: relative !important;
      margin: 0 auto !important;
      overflow: hidden !important;
      pointer-events: none !important;
      z-index: 1 !important;
      display: flex !important;
      align-items: center !important;
      justify-content: center !important;
      transition: opacity 0.15s ease-out !important;
      box-sizing: border-box !important;
    }
    .eh-theme-light #eh-single-thumb-placeholder {
      background-color: #e8e4d6;
    }
    .eh-theme-dark #eh-single-thumb-placeholder {
      background-color: #27282c;
    }
    #eh-single-thumb-placeholder .eh-placeholder-thumb-wrap {
      position: absolute !important;
      top: 0 !important;
      left: 0 !important;
      width: 100% !important;
      height: 100% !important;
      overflow: hidden !important;
      pointer-events: none !important;
      z-index: 1 !important;
    }
    #eh-single-thumb-placeholder .eh-placeholder-thumb-img {
      position: absolute !important;
      top: 0 !important;
      left: 0 !important;
      width: 100% !important;
      height: 100% !important;
      object-fit: fill !important;
      filter: blur(2px) !important;
      opacity: 1 !important;
      pointer-events: none !important;
    }
    #i3 img#img {
      position: relative !important;
      z-index: 2 !important;
      display: block !important;
      margin: 0 auto !important;
    }


    /* Inline Favorites Menu (Replaces #taglist in #gd4) */
    #gd4.eh-fav-active {
      height: auto !important;
      max-height: 100% !important;
      overflow: visible !important;
    }

    #gd4.eh-fav-active > :not(#eh-inline-favorites-container) {
      display: none !important;
    }

    #eh-inline-favorites-container {
      width: 100% !important;
      max-width: 100% !important;
      box-sizing: border-box !important;
      padding: 0 4px 4px 4px !important;
      margin: 0 !important;
      font-family: arial, helvetica, sans-serif !important;
      font-size: 9pt !important;
      line-height: normal !important;
      color: inherit !important;
      overflow: visible !important;
    }

    #eh-inline-favorites-container p.eh-fav-header {
      margin: 0 0 6px 0 !important;
      padding: 0 !important;
      text-align: center !important;
      font-weight: normal !important;
      font-size: 9pt !important;
      line-height: 1.25 !important;
    }

    #eh-inline-favorites-container .eh-fav-body {
      display: flex !important;
      flex-direction: row !important;
      justify-content: space-between !important;
      align-items: flex-start !important;
      gap: 12px !important;
      width: 100% !important;
      max-width: 100% !important;
      box-sizing: border-box !important;
    }

    #eh-inline-favorites-container .eh-fav-cats {
      display: flex !important;
      flex-direction: column !important;
      flex: 0 0 auto !important;
      width: auto !important;
      min-width: 140px !important;
      max-width: 165px !important;
      box-sizing: border-box !important;
      margin: 0 !important;
      padding: 0 !important;
      gap: 2px !important;
    }

    #eh-inline-favorites-container .eh-fav-cat-item {
      display: flex !important;
      align-items: center !important;
      cursor: pointer !important;
      user-select: none !important;
      padding: 1px 4px !important;
      border-radius: 3px !important;
      gap: 6px !important;
      height: 20px !important;
      box-sizing: border-box !important;
    }

    #eh-inline-favorites-container .eh-fav-cat-item:hover {
      background: rgba(255, 255, 255, 0.08) !important;
    }
    .eh-theme-light #eh-inline-favorites-container .eh-fav-cat-item:hover {
      background: rgba(0, 0, 0, 0.06) !important;
    }

    #eh-inline-favorites-container .eh-fav-cat-item input[type="radio"] {
      margin: 0 !important;
      cursor: pointer !important;
      flex-shrink: 0 !important;
    }

    #eh-inline-favorites-container .eh-fav-cat-diamond,
    #eh-inline-favorites-container .i {
      width: 19px !important;
      height: 19px !important;
      min-width: 19px !important;
      min-height: 19px !important;
      background-image: url('https://ehgt.org/g/fav.png') !important;
      background-repeat: no-repeat !important;
      display: inline-block !important;
      vertical-align: middle !important;
      flex-shrink: 0 !important;
      box-sizing: border-box !important;
    }

    #eh-inline-favorites-container .eh-fav-cat-label {
      font-size: 9pt !important;
      font-weight: normal !important;
      white-space: nowrap !important;
      overflow: hidden !important;
      text-overflow: ellipsis !important;
    }

    #eh-inline-favorites-container .eh-fav-note-pane {
      display: flex !important;
      flex-direction: column !important;
      flex: 1 1 0% !important;
      min-width: 0 !important;
      max-width: 480px !important;
      margin-right: 35px !important; /* Space on the right so it doesn't touch the panel edge */
      box-sizing: border-box !important;
    }

    #eh-inline-favorites-container .eh-fav-note-title {
      font-weight: bold !important;
      text-align: center !important;
      margin-bottom: 4px !important;
      font-size: 9pt !important;
    }

    #eh-inline-favorites-container textarea.eh-fav-note-textarea {
      width: 100% !important;
      height: 125px !important;
      min-height: 110px !important;
      max-height: 135px !important;
      box-sizing: border-box !important;
      resize: none !important;
      font-family: inherit !important;
      font-size: 9pt !important;
      padding: 6px 8px !important;
      border-radius: 3px !important;
      outline: none !important;
    }

    .eh-theme-dark #eh-inline-favorites-container textarea.eh-fav-note-textarea {
      background-color: #27282c !important;
      color: #eeeeee !important;
      border: 1px solid #4f535b !important;
    }

    .eh-theme-light #eh-inline-favorites-container textarea.eh-fav-note-textarea {
      background-color: #fcfbfa !important;
      color: #333333 !important;
      border: 1px solid #b5a4a4 !important;
    }

    #eh-inline-favorites-container .eh-fav-slots {
      margin: 4px 0 0 0 !important;
      text-align: right !important;
      font-size: 8pt !important;
      opacity: 0.9 !important;
    }

    #eh-inline-favorites-container .eh-fav-actions {
      display: flex !important;
      align-items: center !important;
      justify-content: center !important;
      gap: 10px !important;
      margin-top: 8px !important;
      width: 100% !important;
    }

    #eh-inline-favorites-container input[type="submit"],
    #eh-inline-favorites-container input[type="button"],
    #eh-inline-favorites-container .eh-fav-submit-btn,
    #eh-inline-favorites-container .eh-fav-remove-btn,
    #eh-inline-favorites-container .eh-fav-cancel-btn {
      font-family: arial, helvetica, sans-serif !important;
      font-size: 9pt !important;
      font-weight: bold !important;
      min-height: 23px !important;
      line-height: 17px !important;
      padding: 2px 14px !important;
      border-radius: 3px !important;
      cursor: pointer !important;
      box-sizing: border-box !important;
      outline: none !important;
      transition: background-color 0.1s ease, border-color 0.1s ease !important;
    }

    .eh-theme-light #eh-inline-favorites-container input[type="submit"],
    .eh-theme-light #eh-inline-favorites-container input[type="button"],
    .eh-theme-light #eh-inline-favorites-container .eh-fav-submit-btn,
    .eh-theme-light #eh-inline-favorites-container .eh-fav-remove-btn,
    .eh-theme-light #eh-inline-favorites-container .eh-fav-cancel-btn {
      background-color: #EDEADA !important;
      color: #5C0D12 !important;
      border: 2px solid #B5A4A4 !important;
    }
    .eh-theme-light #eh-inline-favorites-container input[type="submit"]:hover,
    .eh-theme-light #eh-inline-favorites-container input[type="button"]:hover,
    .eh-theme-light #eh-inline-favorites-container .eh-fav-submit-btn:hover,
    .eh-theme-light #eh-inline-favorites-container .eh-fav-remove-btn:hover,
    .eh-theme-light #eh-inline-favorites-container .eh-fav-cancel-btn:hover {
      background-color: #F3F0E0 !important;
      border-color: #977273 !important;
      color: #5C0D12 !important;
    }

    .eh-theme-dark #eh-inline-favorites-container input[type="submit"],
    .eh-theme-dark #eh-inline-favorites-container input[type="button"],
    .eh-theme-dark #eh-inline-favorites-container .eh-fav-submit-btn,
    .eh-theme-dark #eh-inline-favorites-container .eh-fav-remove-btn,
    .eh-theme-dark #eh-inline-favorites-container .eh-fav-cancel-btn {
      background-color: #34353b !important;
      color: #dddddd !important;
      border: 2px solid #8d8d8d !important;
    }
    .eh-theme-dark #eh-inline-favorites-container input[type="submit"]:hover,
    .eh-theme-dark #eh-inline-favorites-container input[type="button"]:hover,
    .eh-theme-dark #eh-inline-favorites-container .eh-fav-submit-btn:hover,
    .eh-theme-dark #eh-inline-favorites-container .eh-fav-remove-btn:hover,
    .eh-theme-dark #eh-inline-favorites-container .eh-fav-cancel-btn:hover {
      background-color: #43464e !important;
      border-color: #aeaeae !important;
      color: #ffffff !important;
    }

    /* Suppress foreign/ComicRead quick-favorite popup injected into #gd3 */
    #gd3 > :not(#gdc):not(#gdn):not(#gdd):not(#gdr):not(#gdf):not(.c) {
      display: none !important;
      visibility: hidden !important;
      pointer-events: none !important;
      opacity: 0 !important;
      height: 0 !important;
      width: 0 !important;
      overflow: hidden !important;
    }

        .ido,
    div.ido {
      position: relative !important;
    }

    /* Relocated #favact to the top right of .ido (light gray box) */
    #eh-favact-top,
    #favact.eh-favact-top,
    .eh-favact-top {
      display: inline-flex !important;
      justify-content: flex-end !important;
      gap: 6px !important;
      position: absolute !important;
      top: 6px !important;
      right: 0px !important;
      float: none !important;
      clear: none !important;
      margin: 0 !important;
      padding: 0 !important;
      text-align: right !important;
      box-sizing: border-box !important;
      white-space: nowrap !important;
      width: auto !important;
      height: auto !important;
      line-height: normal !important;
      z-index: 100 !important;
    }

    #favact.eh-favact-top select,
    .eh-favact-top select {
      font-family: inherit !important;
      font-size: 9pt !important;
      box-sizing: border-box !important;
      margin: 0 !important;
      vertical-align: middle !important;
      height: 26px !important;
      min-height: 26px !important;
      line-height: 20px !important;
      border-radius: 3px !important;
    }

    #favact.eh-favact-top input[type="submit"],
    #favact.eh-favact-top input[type="button"],
    #favact.eh-favact-top input[value="Apply"],
    #favact.eh-favact-top input[value="Confirm"],
    #favact.eh-favact-top button,
    .eh-favact-top input[type="submit"],
    .eh-favact-top input[type="button"],
    .eh-favact-top input[value="Apply"],
    .eh-favact-top input[value="Confirm"],
    .eh-favact-top button {
      display: inline-flex !important;
      align-items: center !important;
      justify-content: center !important;
      font-family: arial, helvetica, sans-serif !important;
      font-size: 9pt !important;
      font-weight: bold !important;
      padding: 1px 12px 2px !important;
      margin: 0 0 0 2px !important;
      border-radius: 3px !important;
      cursor: pointer !important;
      box-sizing: border-box !important;
      vertical-align: middle !important;
      height: 26px !important;
      min-height: 26px !important;
      line-height: 20px !important;
      white-space: nowrap !important;
      transition: background-color 0.1s ease, border-color 0.1s ease !important;
    }

    /* Checkbox aligned horizontally with the dropdown and button */
    #alltoggle,
    [name="alltoggle"],
    .alltoggle,
    #favact input[type="checkbox"],
    .eh-favact-top input[type="checkbox"],
    #favact.eh-favact-top input[type="checkbox"],
    #eh-favact-top input[type="checkbox"] {
      margin: 0 5px 0 0 !important;
      padding: 0 !important;
      vertical-align: middle !important;
      position: static !important;
      top: 0 !important;
      cursor: pointer !important;
      align-self: center !important;
    }

    #favact.eh-favact-top label,
    #favact.eh-favact-top span,
    .eh-favact-top label,
    .eh-favact-top span,
    #eh-favact-top label,
    #eh-favact-top span {
      display: inline-flex !important;
      align-items: center !important;
      vertical-align: middle !important;
      font-size: 9pt !important;
      line-height: normal !important;
    }

    #favact.eh-favact-top div,
    .eh-favact-top div,
    #eh-favact-top div {
      display: inline-flex !important;
      vertical-align: middle !important;
      font-size: 9pt !important;
      line-height: normal !important;
    }

    /* Light Theme (E-Hentai) */
    .eh-theme-light #favact.eh-favact-top select,
    .eh-theme-light .eh-favact-top select {
      background-color: #EDEADA !important;
      color: #5C0D12 !important;
      border: 2px solid #B5A4A4 !important;
    }

    .eh-theme-light #favact.eh-favact-top input[type="submit"],
    .eh-theme-light #favact.eh-favact-top input[type="button"],
    .eh-theme-light #favact.eh-favact-top input[value="Apply"],
    .eh-theme-light #favact.eh-favact-top input[value="Confirm"],
    .eh-theme-light #favact.eh-favact-top button,
    .eh-theme-light .eh-favact-top input[type="submit"],
    .eh-theme-light .eh-favact-top input[type="button"],
    .eh-theme-light .eh-favact-top input[value="Apply"],
    .eh-theme-light .eh-favact-top input[value="Confirm"],
    .eh-theme-light .eh-favact-top button {
      background-color: #EDEADA !important;
      color: #5C0D12 !important;
      border: 2px solid #B5A4A4 !important;
    }
    .eh-theme-light #favact.eh-favact-top input[type="submit"]:hover,
    .eh-theme-light #favact.eh-favact-top input[type="button"]:hover,
    .eh-theme-light #favact.eh-favact-top input[value="Apply"]:hover,
    .eh-theme-light #favact.eh-favact-top input[value="Confirm"]:hover,
    .eh-theme-light #favact.eh-favact-top button:hover,
    .eh-theme-light .eh-favact-top input[type="submit"]:hover,
    .eh-theme-light .eh-favact-top input[type="button"]:hover,
    .eh-theme-light .eh-favact-top input[value="Apply"]:hover,
    .eh-theme-light .eh-favact-top input[value="Confirm"]:hover,
    .eh-theme-light .eh-favact-top button:hover {
      background-color: #F3F0E0 !important;
      border-color: #977273 !important;
      color: #5C0D12 !important;
    }

    /* Dark Theme (ExHentai) */
    .eh-theme-dark #favact.eh-favact-top select,
    .eh-theme-dark .eh-favact-top select {
      background-color: #34353b !important;
      color: #dddddd !important;
      border: 2px solid #8d8d8d !important;
    }

    .eh-theme-dark #favact.eh-favact-top input[type="submit"],
    .eh-theme-dark #favact.eh-favact-top input[type="button"],
    .eh-theme-dark #favact.eh-favact-top input[value="Apply"],
    .eh-theme-dark #favact.eh-favact-top input[value="Confirm"],
    .eh-theme-dark #favact.eh-favact-top button,
    .eh-theme-dark .eh-favact-top input[type="submit"],
    .eh-theme-dark .eh-favact-top input[type="button"],
    .eh-theme-dark .eh-favact-top input[value="Apply"],
    .eh-theme-dark .eh-favact-top input[value="Confirm"],
    .eh-theme-dark .eh-favact-top button {
      background-color: #34353b !important;
      color: #dddddd !important;
      border: 2px solid #8d8d8d !important;
    }
    .eh-theme-dark #favact.eh-favact-top input[type="submit"]:hover,
    .eh-theme-dark #favact.eh-favact-top input[type="button"]:hover,
    .eh-theme-dark #favact.eh-favact-top input[value="Apply"]:hover,
    .eh-theme-dark #favact.eh-favact-top input[value="Confirm"]:hover,
    .eh-theme-dark #favact.eh-favact-top button:hover,
    .eh-theme-dark .eh-favact-top input[type="submit"]:hover,
    .eh-theme-dark .eh-favact-top input[type="button"]:hover,
    .eh-theme-dark .eh-favact-top input[value="Apply"]:hover,
    .eh-theme-dark .eh-favact-top input[value="Confirm"]:hover,
    .eh-theme-dark .eh-favact-top button:hover {
      background-color: #43464e !important;
      border-color: #aeaeae !important;
      color: #ffffff !important;
    }
  `;

function injectStyles() {
    const style = document.createElement('style');
    style.id = 'eh-fast-reader-core-css';
    style.textContent = CORE_CSS;

    const root = document.head || document.documentElement;
    if (root) {
      root.appendChild(style);
    } else {
      const observer = new MutationObserver(() => {
        const el = document.head || document.documentElement;
        if (el) {
          el.appendChild(style);
          observer.disconnect();
        }
      });
      observer.observe(document, { childList: true });
    }
  }

  injectStyles();

  const isReaderUrl = location.pathname.startsWith('/s/');
  let earlyStripActive = true;
  try {
    earlyStripActive = localStorage.getItem('eh_strip_mode_enabled') !== '0';
  } catch {}
  let earlyLayout = 'horizontal';
  try {
    earlyLayout = localStorage.getItem('eh_strip_layout') || 'horizontal';
  } catch {}
  const isEarlyDark = location.hostname.includes('exhentai');
  if (document.documentElement) {
    document.documentElement.classList.add(isEarlyDark ? 'eh-theme-dark' : 'eh-theme-light');
    if (isReaderUrl && earlyStripActive) {
      document.documentElement.classList.add('eh-strip-mode-active', 'strip-' + earlyLayout);
    }
  }

  // =========================================================================
  // 1. MAIN-WORLD INJECTION
  // =========================================================================
  function injectMainWorldEngine() {
    const run = () => {
      let injected = false;
      try {
        const script = document.createElement('script');
        script.textContent = `(${mainWorldCode.toString()})();`;
        (document.head || document.documentElement).appendChild(script);
        script.remove();
        injected = true;
      } catch (e) {
        console.warn('ExFastReader: Script injection failed, falling back to direct execution:', e);
      }
      if (!injected && typeof mainWorldCode === 'function') {
        mainWorldCode();
      }
    };

    if (document.head || document.documentElement) {
      run();
    } else {
      const observer = new MutationObserver(() => {
        if (document.head || document.documentElement) {
          observer.disconnect();
          run();
        }
      });
      observer.observe(document, { childList: true });
    }
  }

  injectMainWorldEngine();

// =========================================================================
  // 2. INCOGNITO SETTINGS AUTO-SYNC (FAST-LANE)
  // =========================================================================
  function getCookie(name) {
    const matches = document.cookie.match(
      new RegExp('(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\/\+^])/g, '\\$1') + '=([^;]*)')
    );
    return matches ? decodeURIComponent(matches[1]) : undefined;
  }

  function getSavedUconfig() {
    try {
      if (typeof GM_getValue === 'function') {
        const v = GM_getValue('eh_saved_uconfig');
        if (v && typeof v === 'string') return v;
      }
    } catch {}
    try {
      const v = localStorage.getItem('eh_saved_uconfig');
      if (v && typeof v === 'string') return v;
    } catch {}
    return null;
  }

  function setSavedUconfig(val) {
    if (!val || typeof val !== 'string') return;
    try {
      if (typeof GM_setValue === 'function') {
        GM_setValue('eh_saved_uconfig', val);
      }
    } catch {}
    try {
      localStorage.setItem('eh_saved_uconfig', val);
    } catch {}
  }

async function checkAndSyncSettings() {
    // 1. AUTO-ADVANCE FROM PONI TOKEN:
    // When landing on exhentai.org/?poni=..., cookies have already been set by the response headers.
    // Strip ?poni= and immediately replace with the clean URL so you don't get stuck waiting for F5.
    if (location.search.includes('poni=')) {
      const params = new URLSearchParams(location.search);
      const poni = params.get('poni');
      if (poni) {
        const key = 'eh_poni_' + poni.slice(0, 24);
        if (!sessionStorage.getItem(key)) {
          sessionStorage.setItem(key, '1');
          const clean = new URL(location.href);
          clean.searchParams.delete('poni');
          location.replace(clean.href);
          return;
        }
      }
      return;
    }

    // 2. If stuck on an interstitial bounce page, wait for the DOM and auto-click the redirect link
    if (location.pathname.includes('bounce') || location.pathname.includes('login')) {
      const followBounce = () => {
        const link = document.querySelector('a[href*="poni="], a[href*="exhentai.org"]');
        if (link && link.href) {
          location.replace(link.href);
        }
      };
      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', followBounce, { once: true });
      } else {
        followBounce();
      }
      return;
    }

    // If currently on the settings page, capture and save the latest uconfig cookie
    if (location.pathname.startsWith('/uconfig')) {
      const cur = getCookie('uconfig');
      if (cur) setSavedUconfig(cur);
      return;
    }

    const isReader = location.pathname.startsWith('/s/');
    const syncCookieName = 'eh_uconfig_synced';
    const syncKey = 'eh_uconfig_synced_' + location.hostname;

    let memberId, uconfig, syncAttempted;
    try {
      memberId = getCookie('ipb_member_id');
      uconfig = getCookie('uconfig');
      syncAttempted = getCookie(syncCookieName) || sessionStorage.getItem(syncKey);
    } catch {
      return;
    }

    // Always update saved cache if uconfig is present
    if (uconfig) {
      setSavedUconfig(uconfig);
      return;
    }

    if (!memberId || memberId === '0' || syncAttempted) {
      return;
    }

    // FAST LANE: If we have a cached uconfig string in storage, inject it instantly without fetching!
    const saved = getSavedUconfig();
    const cookieDomain = '.' + location.hostname.replace(/^.*?(e-hentai\.org|exhentai\.org)$/, '$1');
    if (saved) {
      document.cookie = `uconfig=${saved}; path=/; domain=${cookieDomain}; SameSite=Lax; max-age=31536000`;
      document.cookie = `${syncCookieName}=1; path=/; SameSite=Lax`;
      try { sessionStorage.setItem(syncKey, 'done'); } catch {}
      if (!isReader) {
        location.replace(location.href);
      }
      return;
    }

    // On reader pages, silently sync in background without reloading
    if (isReader) {
      try {
        sessionStorage.setItem(syncKey, 'attempted');
        document.cookie = `${syncCookieName}=1; path=/; SameSite=Lax`;
        fetch(location.origin + '/uconfig.php', {
          method: 'GET',
          credentials: 'include',
          priority: 'high'
        }).then((res) => {
          if (res.ok) {
            sessionStorage.setItem(syncKey, 'done');
            document.cookie = `${syncCookieName}=1; path=/; SameSite=Lax`;
            const newU = getCookie('uconfig');
            if (newU) setSavedUconfig(newU);
          }
        }).catch(() => {});
      } catch {}
      return;
    }

    // COLD SYNC (First time with no cached uconfig):
    // Prevent unstyled flash while fetching
    let blocker = null;
    try {
      blocker = document.createElement('style');
      blocker.id = 'eh-fast-sync-blocker';
      blocker.textContent = 'html { opacity: 0 !important; }';
      (document.head || document.documentElement).appendChild(blocker);
      setTimeout(() => { if (blocker && blocker.parentNode) blocker.remove(); }, 1800);
    } catch {}

    try {
      sessionStorage.setItem(syncKey, 'attempted');
      document.cookie = `${syncCookieName}=1; path=/; SameSite=Lax`;

      const res = await fetch(location.origin + '/uconfig.php', {
        method: 'GET',
        credentials: 'include',
        priority: 'high'
      });

      if (res.ok) {
        sessionStorage.setItem(syncKey, 'done');
        document.cookie = `${syncCookieName}=1; path=/; SameSite=Lax`;
        const newU = getCookie('uconfig');
        if (newU) setSavedUconfig(newU);
        location.replace(location.href);
      } else if (blocker) {
        blocker.remove();
      }
    } catch {
      if (blocker) blocker.remove();
    }
  }

  checkAndSyncSettings();

  // =========================================================================
  // 3. CORE BACKGROUND ENGINE & BIDIRECTIONAL STRIP READER
  // =========================================================================
function mainWorldCode() {
    if (window.__exFastReaderLoaded) return;
    window.__exFastReaderLoaded = true;

    // Skip all reader/index logic entirely on auth, bounce, and handshake URLs
    if (
      location.search.includes('poni=') ||
      location.pathname.includes('bounce') ||
      location.pathname.includes('login')
    ) {
      return;
    }

    const isReaderPage = location.pathname.startsWith('/s/');
    const isGalleryPage = location.pathname.startsWith('/g/');
    const isIndexPage = !isReaderPage && !isGalleryPage && (
      location.pathname === '/' ||
      location.pathname === '' ||
      location.pathname.startsWith('/watched') ||
      location.pathname.startsWith('/favorites') ||
      location.pathname.startsWith('/popular') ||
      location.pathname.startsWith('/tag/')
    );

    const PREFETCH_DEPTH = 3;
    const IMAGE_TIMEOUT_MS = 10000;
    const MAX_RETRIES = 3;
    const MAX_CONCURRENT_LOADS = 2;

    function safeStorageGet(storage, key, fallback = null) {
      try {
        const val = storage.getItem(key);
        return val !== null ? val : fallback;
      } catch {
        return fallback;
      }
    }

    function safeStorageSet(storage, key, val) {
      try {
        storage.setItem(key, val);
      } catch {}
    }

    function safeStorageRemove(storage, key) {
      try {
        storage.removeItem(key);
      } catch {}
    }

    const pageRespCache = new Map();
    const pageImgKeyCache = new Map();
    const pageImgSrcCache = new Map();
    const pageNlCodes = new Map();
    const preloadedImages = new Map();
    const inFlightFetches = new Map();
    const inFlightGalleryFetches = new Map();
    const fetchedGalleryPages = new Map();

    const pageDimensions = new Map();
    const pageItemMap = new Map();

    const loadedImages = new Set();
    const loadingPages = new Map(); // pageNo -> startTime
    const failedPages = new Map();

    const pageTopBarInfoCache = new Map();
    const pageFileSizeCache = new Map();
    const downloadSpeedSamples = [];
    const MAX_SPEED_SAMPLES = 8;
    const pageThumbnailCache = new Map();
    const spriteImagePromiseCache = new Map();
    const spriteEntriesMap = new Map();
    const spriteDimensionsCache = new Map();

    let cachedGid = null;
    let cachedShowkey = null;
    let cachedApiUrl = null;
    let lastSyncedIndicatorPage = -1;

    let thumbsPerPage = parseInt(safeStorageGet(localStorage, 'eh_thumbs_per_page', '0'), 10) || 40;
    let defaultAspectRatio = 0.707;
    let cachedTotalPages = 0;
    let stallTimer = null;
    let isReloading = false;
    let currentPageNum = 1;
    let lastSingleTurnTime = 0;

    function parseFileSizeInBytes(rawStr) {
      if (!rawStr || typeof rawStr !== 'string') return 0;
      const parts = rawStr.split('::');
      const targetStr = parts.length >= 3 ? parts[2] : rawStr;
      const m = targetStr.match(/(\d+(?:\.\d+)?)\s*([kKMmGg]?[iI]?[bB]+)/i) ||
                rawStr.match(/(\d+(?:\.\d+)?)\s*([kKMmGg][iI]?[bB]+)/i);
      if (!m) return 0;
      const num = parseFloat(m[1]);
      if (isNaN(num) || num <= 0) return 0;
      const unit = m[2].toUpperCase();
      if (unit.startsWith('G')) return Math.round(num * 1024 * 1024 * 1024);
      if (unit.startsWith('M')) return Math.round(num * 1024 * 1024);
      if (unit.startsWith('K')) return Math.round(num * 1024);
      if (unit.startsWith('B')) return Math.round(num);
      return 0;
    }

    function getPageFileSize(pageNo) {
      if (!pageNo) return 1.5 * 1024 * 1024;
      if (pageFileSizeCache.has(pageNo)) {
        const cached = pageFileSizeCache.get(pageNo);
        if (cached > 0) return cached;
      }

      const topInfo = pageTopBarInfoCache.get(pageNo);
      if (topInfo) {
        const sz = parseFileSizeInBytes(topInfo);
        if (sz > 0) {
          pageFileSizeCache.set(pageNo, sz);
          return sz;
        }
      }

      if (pageRespCache.has(pageNo)) {
        const resp = pageRespCache.get(pageNo);
        const candidates = [resp.i, resp.d, resp.i5, resp.i2, resp.i4, resp.i1, resp.i3];
        for (const c of candidates) {
          if (!c) continue;
          const sz = parseFileSizeInBytes(c);
          if (sz > 0) {
            pageFileSizeCache.set(pageNo, sz);
            return sz;
          }
        }
      }

      if (pageNo === currentPageNum) {
        const raw = (document.getElementById('i2')?.textContent || '') + ' ' +
                    (document.getElementById('i4')?.textContent || '') + ' ' +
                    (document.getElementById('i1')?.textContent || '');
        const sz = parseFileSizeInBytes(raw);
        if (sz > 0) {
          pageFileSizeCache.set(pageNo, sz);
          return sz;
        }
      }

      const dim = pageDimensions.get(pageNo);
      if (dim && dim.width > 0 && dim.height > 0) {
        const estimated = Math.round(dim.width * dim.height * 0.35);
        if (estimated > 100 * 1024) {
          return estimated;
        }
      }

      return 1.5 * 1024 * 1024;
    }

    function recordDownloadSpeedSample(pageNo, durationSec) {
      if (!durationSec || durationSec <= 0.05) return;
      const sizeBytes = getPageFileSize(pageNo);
      if (!sizeBytes || sizeBytes <= 0) return;
      const speed = sizeBytes / durationSec;
      if (speed <= 0 || !isFinite(speed)) return;

      downloadSpeedSamples.push({ speed, timestamp: Date.now() });
      while (downloadSpeedSamples.length > MAX_SPEED_SAMPLES) {
        downloadSpeedSamples.shift();
      }

      const currentSpeed = getCurrentDownloadSpeed();
      safeStorageSet(sessionStorage, 'eh_last_speed', String(Math.round(currentSpeed)));
    }

    function getCurrentDownloadSpeed() {
      const now = Date.now();
      while (downloadSpeedSamples.length > 0 && now - downloadSpeedSamples[0].timestamp > 300000) {
        downloadSpeedSamples.shift();
      }

      if (downloadSpeedSamples.length > 0) {
        const sorted = downloadSpeedSamples.map(s => s.speed).sort((a, b) => a - b);
        const mid = Math.floor(sorted.length / 2);
        const medianSpeed = sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
        return Math.max(100 * 1024, medianSpeed);
      }

      const saved = parseInt(safeStorageGet(sessionStorage, 'eh_last_speed', '0'), 10);
      if (saved > 50 * 1024 && saved < 500 * 1024 * 1024) {
        return saved;
      }

      if (typeof navigator !== 'undefined' && navigator.connection && typeof navigator.connection.downlink === 'number' && navigator.connection.downlink > 0) {
        const bps = (navigator.connection.downlink * 1000 * 1000) / 8;
        return Math.max(200 * 1024, bps);
      }

      return 1.5 * 1024 * 1024;
    }

    function calculateImageTimeout(pageNo) {
      const fileSize = getPageFileSize(pageNo);
      const speed = getCurrentDownloadSpeed();

      const expectedTransferSec = fileSize / speed;
      const baseLatencySec = 4.0;

      const slowTimeoutMs = Math.max(6000, Math.round((baseLatencySec + expectedTransferSec * 2.2) * 1000));
      const hardTimeoutMs = Math.max(12000, Math.round((baseLatencySec * 1.5 + expectedTransferSec * 4.0) * 1000));
      const watchdogTimeoutMs = hardTimeoutMs + 5000;

      return {
        fileSize,
        speed,
        expectedTransferSec,
        slowTimeoutMs,
        hardTimeoutMs,
        watchdogTimeoutMs
      };
    }

      function normalizeApiUrl(url) {
          const defaultApi = location.origin + '/api.php';

          if (!url) return defaultApi;
          try {
              const parsed = new URL(url, location.origin);
              const isAllowed =
                    parsed.origin === location.origin ||
                    parsed.hostname === 'api.e-hentai.org' ||
                    parsed.hostname === 'e-hentai.org' ||
                    parsed.hostname === 'exhentai.org' ||
                    parsed.hostname.endsWith('.e-hentai.org') ||
                    parsed.hostname.endsWith('.exhentai.org');

              if (!isAllowed) {
                  return defaultApi;
              }
              return parsed.href;
          } catch {
              return defaultApi;
          }
      }

    function safeParseJson(str) {
      if (!str || typeof str !== 'string') return null;
      const trimmed = str.trim();
      if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null;
      try {
        return JSON.parse(trimmed);
      } catch {
        return null;
      }
    }

      let _currentApiUrl = location.origin + '/api.php';

    if (isReaderPage) {
      try {
        if (window.api_url) {
          _currentApiUrl = normalizeApiUrl(window.api_url);
        }
        Object.defineProperty(window, 'api_url', {
          configurable: true,
          enumerable: true,
          get() {
            return _currentApiUrl;
          },
          set(val) {
            _currentApiUrl = normalizeApiUrl(val);
          }
        });
      } catch (e) {
        window.api_url = _currentApiUrl;
      }

      if (typeof window.api_response === 'function' && !window.api_response.__eh_safe) {
        const origApiResponse = window.api_response;
        window.api_response = function (xhr) {
          try {
            if (xhr && typeof xhr.responseText === 'string') {
              const parsed = safeParseJson(xhr.responseText);
              if (parsed) return parsed;
              return { error: xhr.responseText.trim() || 'Invalid response' };
            }
            return origApiResponse.apply(this, arguments);
          } catch {
            return { error: 'Invalid JSON response' };
          }
        };
        window.api_response.__eh_safe = true;
      }
    }

    function pruneGalleryLocalStorage(newGid) {
      try {
        let list = JSON.parse(safeStorageGet(localStorage, 'eh_gdata_recent', '[]') || '[]');
        list = list.filter(id => id !== newGid);
        list.unshift(newGid);
        while (list.length > 8) {
          const oldGid = list.pop();
          safeStorageRemove(localStorage, 'eh_gdata_' + oldGid);
          safeStorageRemove(localStorage, 'eh_gallery_' + oldGid);
          safeStorageRemove(sessionStorage, 'eh_gdata_' + oldGid);
          safeStorageRemove(sessionStorage, 'eh_gallery_' + oldGid);
        }
        safeStorageSet(localStorage, 'eh_gdata_recent', JSON.stringify(list));
      } catch {}
    }

    function saveGalleryDataToSession(gid) {
      if (!gid) return;
      try {
        const prevStr = safeStorageGet(sessionStorage, 'eh_gdata_' + gid) || safeStorageGet(localStorage, 'eh_gdata_' + gid);
        const prev = prevStr ? JSON.parse(prevStr) : {
          gid,
          totalPages: cachedTotalPages || 0,
          thumbsPerPage: thumbsPerPage || 40,
          keys: {},
          dims: {},
          info: {},
          thumbs: {},
          spriteDims: {}
        };

        if (cachedTotalPages) prev.totalPages = cachedTotalPages;
        if (thumbsPerPage) prev.thumbsPerPage = thumbsPerPage;

        for (const [p, k] of pageImgKeyCache.entries()) {
          prev.keys[p] = k;
        }
        for (const [p, d] of pageDimensions.entries()) {
          prev.dims[p] = { w: d.width, h: d.height, ar: d.aspectRatio };
        }
        for (const [p, inf] of pageTopBarInfoCache.entries()) {
          prev.info[p] = inf;
        }
        if (!prev.fileSizes) prev.fileSizes = {};
        for (const [p, sz] of pageFileSizeCache.entries()) {
          prev.fileSizes[p] = sz;
        }
        for (const [p, th] of pageThumbnailCache.entries()) {
          prev.thumbs[p] = th;
        }
        for (const [url, sdim] of spriteDimensionsCache.entries()) {
          prev.spriteDims[url] = sdim;
        }

        const serialized = JSON.stringify(prev);
        safeStorageSet(sessionStorage, 'eh_gdata_' + gid, serialized);
        safeStorageSet(localStorage, 'eh_gdata_' + gid, serialized);
        pruneGalleryLocalStorage(gid);
      } catch (err) {
        console.warn('ExFastReader: Failed to save gallery session data', err);
      }
    }

    function loadGalleryDataFromSession(gid) {
      if (!gid) return false;
      try {
        let raw = safeStorageGet(sessionStorage, 'eh_gdata_' + gid);
        if (!raw) {
          raw = safeStorageGet(localStorage, 'eh_gdata_' + gid);
        }
        if (!raw) return false;
        const data = JSON.parse(raw);
        if (!data) return false;

        if (data.totalPages && data.totalPages > cachedTotalPages) {
          cachedTotalPages = data.totalPages;
        }
        if (data.thumbsPerPage) {
          thumbsPerPage = data.thumbsPerPage;
        }
        if (data.keys) {
          for (const [p, k] of Object.entries(data.keys)) {
            pageImgKeyCache.set(parseInt(p, 10), k);
          }
        }
        if (data.dims) {
          for (const [p, d] of Object.entries(data.dims)) {
            const page = parseInt(p, 10);
            if (!pageDimensions.has(page) || pageDimensions.get(page).width === 0) {
              pageDimensions.set(page, { width: d.w, height: d.h, aspectRatio: d.ar });
            }
          }
        }
        if (data.info) {
          for (const [p, inf] of Object.entries(data.info)) {
            pageTopBarInfoCache.set(parseInt(p, 10), inf);
          }
        }
        if (data.fileSizes) {
          for (const [p, sz] of Object.entries(data.fileSizes)) {
            pageFileSizeCache.set(parseInt(p, 10), sz);
          }
        }
        if (data.spriteDims) {
          for (const [url, sdim] of Object.entries(data.spriteDims)) {
            spriteDimensionsCache.set(url, sdim);
          }
        }
        if (data.thumbs) {
          for (const [p, th] of Object.entries(data.thumbs)) {
            const page = parseInt(p, 10);
            pageThumbnailCache.set(page, th);
          }
        }

        const spriteUrls = new Set();
        for (const th of pageThumbnailCache.values()) {
          if (th && th.type === 'sprite' && th.url) {
            spriteUrls.add(th.url);
          }
        }
        for (const sUrl of spriteUrls) {
          loadAndSliceSprite(sUrl);
        }
        return true;
      } catch (err) {
        console.warn('ExFastReader: Failed to load gallery session data', err);
        return false;
      }
    }

    let stripActive = safeStorageGet(localStorage, 'eh_strip_mode_enabled', '1') !== '0';
    let currentLayout = safeStorageGet(localStorage, 'eh_strip_layout', 'horizontal');

    function setStripClasses(active, layout) {
      const root = document.documentElement;
      const body = document.body;
      const targets = body ? [root, body] : [root];

      for (const el of targets) {
        if (!el) continue;
        if (active) {
          el.classList.add('eh-strip-mode-active');
          el.classList.remove('strip-horizontal', 'strip-vertical');
          el.classList.add('strip-' + layout);
        } else {
          el.classList.remove('eh-strip-mode-active', 'strip-horizontal', 'strip-vertical', 'eh-topbar-visible');
        }
      }
    }

    if (isReaderPage) {
      setStripClasses(stripActive, currentLayout);
      if (!document.body && stripActive) {
        let bodyObsTimer = null;
        const bodyObs = new MutationObserver(() => {
          if (document.body) {
            if (bodyObsTimer) clearTimeout(bodyObsTimer);
            setStripClasses(stripActive, currentLayout);
            bodyObs.disconnect();
          }
        });
        bodyObs.observe(document.documentElement, { childList: true });
        bodyObsTimer = setTimeout(() => {
          bodyObs.disconnect();
        }, 10000);
      }
    }

    const mInitialGidMatch = location.pathname.match(/\/s\/[0-9a-zA-Z]+\/(\d+)-/i) || location.pathname.match(/\/g\/(\d+)\//i);
    if (mInitialGidMatch) {
      cachedGid = parseInt(mInitialGidMatch[1], 10);
      loadGalleryDataFromSession(cachedGid);
      if (isReaderPage && stripActive) {
        const initMeta = getCurrentPageMeta();
        const initP = initMeta.page || 1;
        const pDim = pageDimensions.get(initP) || pageDimensions.get(1) || (pageDimensions.size > 0 ? pageDimensions.values().next().value : null);
        if (pDim && pDim.aspectRatio) {
          defaultAspectRatio = pDim.aspectRatio;
          applyGlobalAspectRatio(defaultAspectRatio);
        }
      }
    }
    let lastStripTurnTime = 0;

    let scrollRafId = null;
    let lastHistoryPage = -1;
    let historyDebounceTimer = null;
    let lastUpdatePage = -1;
    let isPumpingQueue = false;
    let isTransitioningLayout = false;
    let isAnchoring = false;

    let targetScrollLeft = null;
    let animScrollRaf = null;
    let targetScrollTop = null;
    let animScrollTopRaf = null;

    let cachedFrontUrl = location.origin + '/';
    let cachedOnionUrl = '';
    let cachedGalleryUrl = '';

    let scrollDirection = 'forward';
    let lastScrollY = typeof window !== 'undefined' ? window.scrollY : 0;
    let lastScrollLeft = 0;
    let stripItemObserver = null;

    function getStripItemObserver() {
      if (!stripItemObserver && typeof IntersectionObserver !== 'undefined') {
        stripItemObserver = new IntersectionObserver((entries) => {
          let shouldPump = false;
          for (const entry of entries) {
            if (entry.isIntersecting) {
              const p = parseInt(entry.target.dataset.page, 10);
              if (p) {
                if (entry.target.classList.contains('eh-placeholder') && !entry.target.querySelector('.eh-placeholder-thumb-wrap')) {
                  applyThumbnailToPlaceholder(entry.target, p);
                }
                if (isPageEligibleToLoad(p)) {
                  shouldPump = true;
                }
              }
            }
          }
          if (shouldPump) {
            checkAndLoadNearbyPages();
          }
        }, {
          root: currentLayout === 'horizontal' ? document.getElementById('eh-strip-container') : null,
          rootMargin: '350px'
        });
      }
      return stripItemObserver;
    }

    function cancelKeyboardAnimation() {
      if (animScrollRaf) {
        cancelAnimationFrame(animScrollRaf);
        animScrollRaf = null;
      }
      if (animScrollTopRaf) {
        cancelAnimationFrame(animScrollTopRaf);
        animScrollTopRaf = null;
      }
      targetScrollLeft = null;
      targetScrollTop = null;
    }

    function withScrollAnchor(fn, targetPageNo = null) {
      if (!stripActive || isAnchoring || isTransitioningLayout) {
        fn();
        return;
      }

      const container = document.getElementById('eh-strip-container');
      if (!container) {
        fn();
        return;
      }

      // If the page being updated is at or after the current visible page,
      // it cannot shift earlier elements in RTL row or vertical column flex.
      if (targetPageNo !== null && targetPageNo >= currentPageNum) {
        fn();
        return;
      }

      isAnchoring = true;
      try {
        let anchorEl = null;
        let anchorRightBefore = 0;
        let anchorTopBefore = 0;

        const vpX = window.innerWidth / 2;
        const vpY = window.innerHeight / 2;

        let closestDist = Infinity;
        for (const [p, item] of pageItemMap.entries()) {
          if (Math.abs(p - currentPageNum) > 3) continue;
          if (!item || !item.isConnected) continue;
          const r = item.getBoundingClientRect();
          if (currentLayout === 'horizontal') {
            if (r.right > 0 && r.left < window.innerWidth) {
              const center = (r.left + r.right) / 2;
              const dist = Math.abs(center - vpX);
              if (dist < closestDist) {
                closestDist = dist;
                anchorEl = item;
              }
            }
          } else {
            if (r.bottom > 0 && r.top < window.innerHeight) {
              const center = (r.top + r.bottom) / 2;
              const dist = Math.abs(center - vpY);
              if (dist < closestDist) {
                closestDist = dist;
                anchorEl = item;
              }
            }
          }
        }

        if (!anchorEl) {
          anchorEl = pageItemMap.get(currentPageNum);
        }

        if (anchorEl && anchorEl.isConnected) {
          const r = anchorEl.getBoundingClientRect();
          anchorRightBefore = r.right;
          anchorTopBefore = r.top;
        }

        fn();

        if (anchorEl && anchorEl.isConnected && (anchorRightBefore || anchorTopBefore)) {
          const rAfter = anchorEl.getBoundingClientRect();
          if (currentLayout === 'horizontal') {
            const deltaX = rAfter.right - anchorRightBefore;
            if (Math.abs(deltaX) >= 4.0) {
              const roundX = Math.round(deltaX);
              container.scrollLeft += roundX;
              lastScrollLeft = container.scrollLeft;
              if (targetScrollLeft !== null) {
                targetScrollLeft += roundX;
                const maxScroll = 0;
                const minScroll = -(container.scrollWidth - container.clientWidth);
                targetScrollLeft = Math.max(minScroll, Math.min(maxScroll, targetScrollLeft));
              }
            }
          } else {
            const deltaY = rAfter.top - anchorTopBefore;
            if (Math.abs(deltaY) >= 4.0) {
              const roundY = Math.round(deltaY);
              window.scrollBy(0, roundY);
              lastScrollY = window.scrollY;
              if (targetScrollTop !== null) {
                targetScrollTop += roundY;
                const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
                targetScrollTop = Math.max(0, Math.min(maxScroll, targetScrollTop));
              }
            }
          }
        }
      } finally {
        isAnchoring = false;
      }
    }

    function scrollByEvenIncrement(delta) {
      if (delta < 0) {
        scrollDirection = 'backward';
      } else if (delta > 0) {
        scrollDirection = 'forward';
      }

      if (stripActive && currentLayout === 'horizontal') {
        const container = document.getElementById('eh-strip-container');
        if (!container) return;

        const maxScroll = 0;
        const minScroll = -(container.scrollWidth - container.clientWidth);

        if (targetScrollLeft === null || isNaN(targetScrollLeft)) {
          targetScrollLeft = container.scrollLeft;
        }

        targetScrollLeft -= delta;
        if (targetScrollLeft > maxScroll) targetScrollLeft = maxScroll;
        if (targetScrollLeft < minScroll) targetScrollLeft = minScroll;

        animateHorizontalScroll(container);
      } else {
        const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
        let targetTop = (targetScrollTop !== null && !isNaN(targetScrollTop)) ? targetScrollTop : window.scrollY;
        targetTop += delta;
        if (targetTop < 0) targetTop = 0;
        if (targetTop > maxScroll) targetTop = maxScroll;
        targetScrollTop = targetTop;

        animateVerticalScroll();
      }
    }

    function animateHorizontalScroll(container) {
      if (animScrollRaf) return;

      function step() {
        if (targetScrollLeft === null) {
          animScrollRaf = null;
          return;
        }

        const current = container.scrollLeft;
        const diff = targetScrollLeft - current;

        if (Math.abs(diff) <= 1) {
          container.scrollLeft = targetScrollLeft;
          targetScrollLeft = null;
          animScrollRaf = null;
          scheduleScrollUpdate();
          return;
        }

        const move = diff * 0.28;
        const nextVal = current + (Math.abs(move) < 1 ? Math.sign(diff) : move);
        container.scrollLeft = nextVal;
        if (container.scrollLeft === current) {
          targetScrollLeft = null;
          animScrollRaf = null;
          scheduleScrollUpdate();
          return;
        }
        scheduleScrollUpdate();
        animScrollRaf = requestAnimationFrame(step);
      }

      animScrollRaf = requestAnimationFrame(step);
    }

    function animateVerticalScroll() {
      if (animScrollTopRaf) return;

      function step() {
        if (targetScrollTop === null) {
          animScrollTopRaf = null;
          return;
        }

        const current = window.scrollY;
        const diff = targetScrollTop - current;

        if (Math.abs(diff) <= 1) {
          window.scrollTo(0, targetScrollTop);
          targetScrollTop = null;
          animScrollTopRaf = null;
          scheduleScrollUpdate();
          return;
        }

        const move = diff * 0.28;
        const nextVal = current + (Math.abs(move) < 1 ? Math.sign(diff) : move);
        window.scrollTo(0, nextVal);
        if (window.scrollY === current) {
          targetScrollTop = null;
          animScrollTopRaf = null;
          scheduleScrollUpdate();
          return;
        }
        scheduleScrollUpdate();
        animScrollTopRaf = requestAnimationFrame(step);
      }

      animScrollTopRaf = requestAnimationFrame(step);
    }

    function extractAndHideBottomLinks() {
      let frontUrl = location.origin + '/';
      let onionUrl = '';

      if (location.hostname.includes('exhentai')) {
        onionUrl = 'http://exhentai55ld2wyap5juskbm67czulomrouspvlunwfuhqack263eccd.onion';
      } else {
        onionUrl = 'http://e-hentai.org';
      }

      const footerLinks = document.querySelectorAll('#footer a, .ft a, .dp a, #dp a');
      for (const a of footerLinks) {
        const href = a.getAttribute('href') || a.href || '';
        const txt = a.textContent.trim().toLowerCase();
        if (href.includes('wiki') || href.includes('forum')) continue;
        if (txt === 'front page' || txt === 'front' || txt.startsWith('front page')) {
          try {
            const u = new URL(href, location.origin);
            if (u.origin === location.origin) frontUrl = u.href;
          } catch {}
        }
        if (txt.includes('onion') || txt.includes('tor') || href.includes('.onion')) {
          if (href) onionUrl = href;
        }
      }

      cachedFrontUrl = frontUrl;
      cachedOnionUrl = onionUrl;
      return { frontUrl, onionUrl };
    }

    function syncThemeClass() {
      const isDark =
        location.hostname.includes('exhentai') ||
        (document.body &&
          window.getComputedStyle(document.body).backgroundColor.includes('52') &&
          window.getComputedStyle(document.body).backgroundColor.includes('53'));
      document.documentElement.classList.remove('eh-theme-dark', 'eh-theme-light');
      document.documentElement.classList.add(isDark ? 'eh-theme-dark' : 'eh-theme-light');
    }

    function showFailureToast(msg) {
      let toast = document.getElementById('eh-toast-fail');
      if (!toast) {
        toast = document.createElement('div');
        toast.id = 'eh-toast-fail';
        toast.className = 'eh-toast-fail';
        document.body.appendChild(toast);
        toast.addEventListener('click', () => {
          safeStorageRemove(sessionStorage, 'eh_retry_' + location.pathname);
          toast.remove();
          executeBrokenReload();
        });
      }
      toast.textContent = msg;
    }

    function parseTemplate(html) {
      const t = document.createElement('template');
      t.innerHTML = html;
      return t.content;
    }

    function getBaseGalleryUrl() {
      if (cachedGalleryUrl && /\/g\/\d+\/[0-9a-fA-F]{10}/i.test(cachedGalleryUrl)) {
        return cachedGalleryUrl.replace(/\?.*$/, '').replace(/\/?$/, '/');
      }
      const mPath = location.pathname.match(/\/s\/[0-9a-zA-Z]+\/(\d+)-/i);
      const curGid = mPath ? mPath[1] : (getPageParams()?.gid || '');

      if (curGid) {
        const stored = safeStorageGet(sessionStorage, 'eh_gallery_' + curGid) || safeStorageGet(localStorage, 'eh_gallery_' + curGid);
        if (stored && /\/g\/\d+\/[0-9a-fA-F]{10}/i.test(stored)) {
          cachedGalleryUrl = stored.replace(/\?.*$/, '').replace(/\/?$/, '/');
          return cachedGalleryUrl;
        }
      }

      // 1. Check all <a> tags for exact /g/{gid}/{token}/ link
      const links = document.querySelectorAll('a[href*="/g/"]');
      for (const a of links) {
        const href = a.getAttribute('href') || a.href || '';
        const m = href.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
        if (m) {
          if (!curGid || m[1] === String(curGid)) {
            cachedGalleryUrl = `${location.origin}/g/${m[1]}/${m[2]}/`;
            if (curGid) { safeStorageSet(sessionStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); safeStorageSet(localStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); }
            return cachedGalleryUrl;
          }
        }
      }

      // 2. Check document.referrer
      if (document.referrer) {
        const mRef = document.referrer.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
        if (mRef && (!curGid || mRef[1] === String(curGid))) {
          cachedGalleryUrl = `${location.origin}/g/${mRef[1]}/${mRef[2]}/`;
          if (curGid) { safeStorageSet(sessionStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); safeStorageSet(localStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); }
          return cachedGalleryUrl;
        }
      }

      // 3. Scan scripts without whole-DOM serialization
      for (const s of document.scripts) {
        const txt = s.textContent || '';
        if (curGid) {
          const rGid = new RegExp('/g/' + curGid + '/([0-9a-fA-F]{10})', 'i');
          const mGid = txt.match(rGid);
          if (mGid) {
            cachedGalleryUrl = `${location.origin}/g/${curGid}/${mGid[1]}/`;
            safeStorageSet(sessionStorage, 'eh_gallery_' + curGid, cachedGalleryUrl);
            return cachedGalleryUrl;
          }
        }
        const mAny = txt.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
        if (mAny) {
          cachedGalleryUrl = `${location.origin}/g/${mAny[1]}/${mAny[2]}/`;
          if (curGid) { safeStorageSet(sessionStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); safeStorageSet(localStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); }
          return cachedGalleryUrl;
        }
      }

      return '';
    }

    function detectInitialThumbsPerPage() {
      const curMeta = getCurrentPageMeta();
      const page = curMeta.page;
      const refUrl = document.referrer || '';
      let foundP = null;

      const returnLinks = document.querySelectorAll('a[href*="/g/"]');
      for (const a of returnLinks) {
        const href = a.getAttribute('href') || a.href || '';
        const m = href.match(/[?&]p=(\d+)/);
        if (m) {
          foundP = parseInt(m[1], 10);
          break;
        }
      }
      if (foundP === null && refUrl) {
        const m = refUrl.match(/[?&]p=(\d+)/);
        if (m) foundP = parseInt(m[1], 10);
      }

      if (foundP !== null && page > 1) {
        for (const count of [20, 40, 100]) {
          if (Math.floor((page - 1) / count) === foundP) {
            thumbsPerPage = count;
            safeStorageSet(localStorage, 'eh_thumbs_per_page', String(count));
            break;
          }
        }
      }
    }

    function getGalleryUrl(pageNo = null) {
      const base = getBaseGalleryUrl();
      if (!base) return '';
      const targetPage = pageNo || currentPageNum || 1;
      const count = (typeof thumbsPerPage === 'number' && thumbsPerPage > 0) ? thumbsPerPage : 40;
      const pageIndex = Math.max(0, Math.floor((targetPage - 1) / count));
      try {
        const u = new URL(base, location.origin);
        if (pageIndex > 0) {
          u.searchParams.set('p', String(pageIndex));
        } else {
          u.searchParams.delete('p');
        }
        return u.href;
      } catch {
        const cleanBase = base.replace(/[?&]p=\d+/g, '').replace(/\?$/, '');
        return pageIndex > 0 ? (cleanBase + (cleanBase.includes('?') ? '&' : '?') + 'p=' + pageIndex) : cleanBase;
      }
    }

    function getCurrentPageMeta() {
      const m = location.pathname.match(/\/s\/([0-9a-zA-Z]+)\/\d+-(\d+)/i);
      let page = m ? parseInt(m[2], 10) : 0;
      let key = m ? m[1] : '';

      if (!page && typeof window.currentpage === 'number' && window.currentpage > 0) {
        page = window.currentpage;
      }
      if (!page && typeof window.startpage === 'number' && window.startpage > 0) {
        page = window.startpage;
      }
      if (!key && typeof window.currentkey === 'string' && window.currentkey) {
        key = window.currentkey;
      }
      if (!key && typeof window.startkey === 'string' && window.startkey) {
        key = window.startkey;
      }

      if (!page || isNaN(page)) {
        const curSpan = document.querySelector('#i2 .sn span:first-child, #i4 .sn span:first-child');
        if (curSpan) {
          const p = parseInt(curSpan.textContent.trim(), 10);
          if (!isNaN(p)) page = p;
        }
      }
      return { page: Math.max(1, page || 1), imgKey: key };
    }

    function applyGlobalAspectRatio(ar) {
      if (!ar || isNaN(ar) || ar <= 0) return;
      document.documentElement.style.setProperty('--eh-ar', String(ar));
      const container = document.getElementById('eh-strip-container');
      if (container) {
        container.style.setProperty('--eh-ar', String(ar));
      }
    }

    function extractNlCode(source = document) {
      if (!source) return null;
      if (typeof source === 'object' && !(source instanceof Node)) {
        if (typeof source.nl === 'string' && source.nl.trim()) return source.nl.trim();
        const searchSources = [source.i6, source.i5, source.i7, source.i3, source.i2, source.i4, source.i1, source.n];
        for (const s of searchSources) {
          if (!s || typeof s !== 'string') continue;
          const m = s.match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i) ||
                    s.match(/id=["']loadfail["'][^>]*onclick=["']return\s*nl\(['"]([^'"]+)['"]\)/i) ||
                    s.match(/[?&]nl=([0-9a-zA-Z\-_]+)/i);
          if (m) return m[1].trim();
        }
        try {
          const raw = JSON.stringify(source);
          const mRaw = raw.match(/nl\s*(?:\\(|\()\s*['"\ ]*([0-9a-zA-Z\-_]+)['"\ ]*\s*(?:\\)|\))/i) ||
                       raw.match(/[?&]nl=([0-9a-zA-Z\-_]+)/i);
          if (mRaw) return mRaw[1].trim();
        } catch {}
        return null;
      }

      const doc = source;
      const loadfail = doc.getElementById ? doc.getElementById('loadfail') : null;
      if (loadfail) {
        const m = (loadfail.getAttribute('onclick') || '').match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i);
        if (m) return m[1].trim();
        const mHref = (loadfail.getAttribute('href') || '').match(/[?&]nl=([0-9a-zA-Z\-_]+)/i);
        if (mHref) return mHref[1].trim();
      }
      const img = doc.getElementById ? doc.getElementById('img') : null;
      if (img) {
        const m = (img.getAttribute('onerror') || '').match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i);
        if (m) return m[1].trim();
      }
      const allNlLinks = doc.querySelectorAll ? doc.querySelectorAll('a[onclick*="nl("]') : [];
      for (const a of allNlLinks) {
        const m = (a.getAttribute('onclick') || '').match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i);
        if (m) return m[1].trim();
      }
      const i3 = doc.getElementById ? doc.getElementById('i3') : null;
      if (i3) {
        const m = i3.innerHTML.match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i);
        if (m) return m[1].trim();
      }
      const i6 = doc.getElementById ? doc.getElementById('i6') : null;
      if (i6) {
        const m = i6.innerHTML.match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i);
        if (m) return m[1].trim();
      }
      return null;
    }
    const extractInitialNlCode = extractNlCode;

    function detectInitialPageDimensions() {
      if (pageDimensions.has(currentPageNum) && pageDimensions.get(currentPageNum).aspectRatio) {
        defaultAspectRatio = pageDimensions.get(currentPageNum).aspectRatio;
        applyGlobalAspectRatio(defaultAspectRatio);
        return;
      }
      const curImg = document.getElementById('img');
      if (curImg) {
        const sw = parseInt(curImg.style.width, 10);
        const sh = parseInt(curImg.style.height, 10);
        if (sw > 0 && sh > 0) {
          const ar = sw / sh;
          if (ar >= 0.3 && ar <= 2.5) {
            defaultAspectRatio = ar > 1.05 ? (ar / 2) : ar;
            pageDimensions.set(currentPageNum, { width: sw, height: sh, aspectRatio: ar });
            applyGlobalAspectRatio(defaultAspectRatio);
            return;
          }
        }
        if (curImg.naturalWidth > 0 && curImg.naturalHeight > 0) {
          const ar = curImg.naturalWidth / curImg.naturalHeight;
          if (ar >= 0.3 && ar <= 2.5) {
            defaultAspectRatio = ar > 1.05 ? (ar / 2) : ar;
            pageDimensions.set(currentPageNum, { width: curImg.naturalWidth, height: curImg.naturalHeight, aspectRatio: ar });
            applyGlobalAspectRatio(defaultAspectRatio);
            return;
          }
        }
      }

      const infoText = (document.getElementById('i2')?.textContent || '') + ' ' + (document.getElementById('i4')?.textContent || '') + ' ' + (document.getElementById('i1')?.textContent || '');
      const mDim = infoText.match(/(\d{3,5})\s*x\s*(\d{3,5})/);
      if (mDim) {
        const w = parseInt(mDim[1], 10);
        const h = parseInt(mDim[2], 10);
        if (w > 0 && h > 0) {
          const ar = w / h;
          if (ar >= 0.3 && ar <= 2.5) {
            defaultAspectRatio = ar > 1.05 ? (ar / 2) : ar;
            pageDimensions.set(currentPageNum, { width: w, height: h, aspectRatio: ar });
            applyGlobalAspectRatio(defaultAspectRatio);
          }
        }
      }
    }

    function updatePageDimensions(pageNo, width, height) {
      if (!width || !height) return;
      const ar = width / height;
      const prevDim = pageDimensions.get(pageNo);
      if (prevDim && prevDim.aspectRatio && Math.abs(prevDim.aspectRatio - ar) < 0.008) {
        if (!prevDim.width && width) {
          prevDim.width = width;
          prevDim.height = height;
        }
        return;
      }
      withScrollAnchor(() => {
        pageDimensions.set(pageNo, { width, height, aspectRatio: ar });
        if (Math.abs(defaultAspectRatio - 0.707) < 0.001 && ar >= 0.4 && ar <= 1.0) {
          defaultAspectRatio = ar;
        }
        const item = pageItemMap.get(pageNo);
        if (item) {
          applyItemDimensions(item, pageNo);
        }
      }, pageNo);

      if (currentPageNum === pageNo) {
        updateTopBarInfo(pageNo);
      }
    }

    function extractKeysFromText(text) {
      if (!text) return [];
      const found = new Set();
      for (const m of text.matchAll(/load_image\((\d+),\s*['"]([^'"]+)['"]\)/g)) {
        const p = parseInt(m[1], 10);
        pageImgKeyCache.set(p, m[2]);
        found.add(p);
      }
      for (const m of text.matchAll(/\/s\/([0-9a-zA-Z]+)\/\d+-(\d+)/g)) {
        const p = parseInt(m[2], 10);
        pageImgKeyCache.set(p, m[1]);
        found.add(p);
      }
      return Array.from(found);
    }

    function extractKeysFromHtml(html) {
      if (!html) return;
      try {
        const template = document.createElement('template');
        template.innerHTML = html;
        const links = template.content.querySelectorAll('a');
        for (const a of links) {
          const oc = a.getAttribute('onclick') || '';
          const mClick = oc.match(/load_image\((\d+),\s*['"]([^'"]+)['"]\)/i);
          if (mClick) {
            pageImgKeyCache.set(parseInt(mClick[1], 10), mClick[2]);
          }
          const href = a.getAttribute('href') || a.href || '';
          const mHref = href.match(/\/s\/([0-9a-zA-Z]+)\/\d+-(\d+)/i);
          if (mHref) {
            pageImgKeyCache.set(parseInt(mHref[2], 10), mHref[1]);
          }
        }
      } catch {}
    }

    function extractKeysFromResp(resp, pageNo = null) {
      if (!resp) return;
      const htmlParts = [];
      if (resp.i2) htmlParts.push(resp.i2);
      if (resp.i4) htmlParts.push(resp.i4);
      if (resp.n) htmlParts.push(resp.n);
      const combined = htmlParts.join(' ');
      extractKeysFromText(combined);
      extractKeysFromHtml(combined);

      const targetP = pageNo || (resp.p ? parseInt(resp.p, 10) : null);
      if (targetP) {
        const nlCode = extractNlCode(resp);
        if (nlCode) {
          pageNlCodes.set(targetP, nlCode);
        }
        let w = resp.x ? parseInt(resp.x, 10) : 0;
        let h = resp.y ? parseInt(resp.y, 10) : 0;
        if ((!w || !h) && resp.i3) {
          const mDim = resp.i3.match(/style=["'][^"']*width:\s*(\d+)px;\s*height:\s*(\d+)px/i) ||
                       resp.i3.match(/style=["'][^"']*height:\s*(\d+)px;\s*width:\s*(\d+)px/i);
          if (mDim) {
            w = parseInt(mDim[1], 10);
            h = parseInt(mDim[2], 10);
          }
        }
        if (w > 0 && h > 0) {
          updatePageDimensions(targetP, w, h);
        }
      }

      if (!cachedGalleryUrl) {
        const combinedAll = (resp.i1 || '') + ' ' + (resp.i2 || '');
        const mG = combinedAll.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
        if (mG) {
          cachedGalleryUrl = `${location.origin}/g/${mG[1]}/${mG[2]}/`;
          discoverAndFetchGallery(currentPageNum);
      prefetchNearbyThumbnails(currentPageNum);
        }
      }
    }

    function extractKeysFromDoc(doc) {
      const html =
        (doc.getElementById('i2')?.innerHTML || '') + ' ' +
        (doc.getElementById('i4')?.innerHTML || '') + ' ' +
        (doc.getElementById('i1')?.innerHTML || '');
      extractKeysFromText(html);
      extractKeysFromHtml(html);
      const nl = extractInitialNlCode(doc);
      if (nl) {
        pageNlCodes.set(currentPageNum, nl);
      }
    }

function cleanImageInfo(rawStr) {
      if (!rawStr) return '';
      const text = rawStr.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
      if (text.includes('::')) {
        const parts = text.split('::').map(s => s.trim());
        if (parts.length >= 2) {
          let fn = parts[0].replace(/^Page\s+\d+[:_\-]\s*/i, '').trim();
          fn = fn.replace(/.*?(?:Download|source)\s+(?:original\s+)?(?:\d+\s*x\s*\d+\s+)?(?:[\d\.]+\s*[kKMmGg]?[iI]?[bB]+\s+)?/i, '').trim();
          const dims = parts[1].trim();
          const sz = parts[2] ? ` :: ${parts[2].split(/\s+(?:Download|source)/i)[0].trim()}` : '';
          return fn ? `${fn} :: ${dims}${sz}` : `${dims}${sz}`;
        }
      }
      const m = text.match(/([^\s<>]+\.[a-zA-Z0-9]+)\s*::\s*(\d+\s*x\s*\d+)(?:\s*::\s*([\d\.]+\s*[kKMmGg]?[iI]?[bB]?))?/i);
      if (m) {
        const fn = m[1].trim();
        const dims = m[2].trim();
        const sz = m[3] ? ` :: ${m[3].trim()}` : '';
        return fn ? `${fn} :: ${dims}${sz}` : `${dims}${sz}`;
      }
      return text.replace(/^Page\s+\d+[:_\-]\s*/i, '').trim();
    }

function extractInfoFromResp(resp, pageNo = null) {
      if (!resp) return '';
      // E-Hentai returns the info string in resp.i
      const candidates = [resp.i, resp.d, resp.i5, resp.i2, resp.i4, resp.i1, resp.i3];
      for (const html of candidates) {
        if (!html) continue;
        const cleaned = cleanImageInfo(html);
        if (cleaned && cleaned.includes('::') && cleaned.includes('x')) {
          if (pageNo) {
            const sz = parseFileSizeInBytes(cleaned);
            if (sz > 0) pageFileSizeCache.set(pageNo, sz);
          }
          return cleaned;
        }
      }
      let filename = '';
      let dimStr = '';
      if (resp.i3) {
        const mImg = resp.i3.match(/<img[^>]+src=["']([^"']+)["']/i);
        const imgSrc = mImg ? mImg[1] : resp.i3;
        const mSrc = imgSrc.match(/\/([^\s"'/]+\.[a-zA-Z0-9]+)(?:["']|\?)/i);
        if (mSrc && !mSrc[1].includes('blank') && !mSrc[1].includes('loadfail') && !mSrc[1].includes('fullimg')) {
          filename = mSrc[1];
        }
        const mDim = resp.i3.match(/style=["'][^"']*width:\s*(\d+)px;\s*height:\s*(\d+)px/i) ||
                     resp.i3.match(/style=["'][^"']*height:\s*(\d+)px;\s*width:\s*(\d+)px/i) ||
                     resp.i3.match(/(\d{3,5})\s*x\s*(\d{3,5})/);
        if (mDim) {
          dimStr = `${mDim[1]} x ${mDim[2]}`;
        }
      }
      if (resp.x && resp.y) {
        dimStr = `${resp.x} x ${resp.y}`;
      }
      if (!dimStr && pageNo && pageDimensions.has(pageNo)) {
        const d = pageDimensions.get(pageNo);
        if (d.width && d.height) {
          dimStr = `${d.width} x ${d.height}`;
        }
      }
      if (filename && dimStr) return `${filename} :: ${dimStr}`;
      if (dimStr) return dimStr;
      return filename;
    }

    function getOrCreateTopBarInfo() {
      if (!stripActive) return null;
      const i2 = document.getElementById('i2');
      if (!i2) return null;
      let infoContainer = document.getElementById('eh-topbar-info');
      if (!infoContainer) {
        infoContainer = document.createElement('div');
        infoContainer.id = 'eh-topbar-info';
        infoContainer.className = 'eh-topbar-info';

        let initialRaw = '';
        const dEl = i2.querySelector('.d');
        if (dEl) {
          initialRaw = dEl.textContent;
          dEl.remove();
        }

        for (const child of Array.from(i2.childNodes)) {
          if (child.nodeType === Node.ELEMENT_NODE) {
            if (child.classList.contains('sn') || child.id === 'eh-strip-controls' || child.id === 'eh-topbar-info') continue;
            if (!initialRaw) initialRaw += ' ' + child.textContent;
            child.remove();
          } else if (child.nodeType === Node.TEXT_NODE && child.textContent.trim()) {
            if (!initialRaw) initialRaw += ' ' + child.textContent;
            child.remove();
          }
        }

        const controls = document.getElementById('eh-strip-controls');
        if (controls) {
          i2.insertBefore(infoContainer, controls);
        } else {
          i2.appendChild(infoContainer);
        }

        const cleaned = cleanImageInfo(initialRaw);
        if (cleaned) {
          pageTopBarInfoCache.set(currentPageNum, cleaned);
          infoContainer.innerHTML = `<div>${cleaned}</div>`;
        }
      }
      return infoContainer;
    }

    function updateTopBarInfo(pageNo) {
      const infoContainer = getOrCreateTopBarInfo();
      if (!infoContainer) return;

      // 1. Inspect live image in DOM
      const item = pageItemMap.get(pageNo);
      const img = item?.querySelector('img.eh-main-img');
      let currentW = 0, currentH = 0;
      if (img && img.naturalWidth > 0 && img.naturalHeight > 0) {
        currentW = img.naturalWidth;
        currentH = img.naturalHeight;
        if (!pageDimensions.has(pageNo) || pageDimensions.get(pageNo).width === 0) {
          updatePageDimensions(pageNo, currentW, currentH);
        }
      }

      // 2. Inspect pageDimensions cache
      if (!currentW || !currentH) {
        const dim = pageDimensions.get(pageNo);
        if (dim && dim.width > 0 && dim.height > 0) {
          currentW = dim.width;
          currentH = dim.height;
        }
      }

      // 3. Inspect pageRespCache
      let respInfo = '';
      if (pageRespCache.has(pageNo)) {
        const resp = pageRespCache.get(pageNo);
        respInfo = extractInfoFromResp(resp, pageNo);
        if (respInfo && respInfo.includes('x') && !currentW) {
          const m = respInfo.match(/(\d{2,5})\s*x\s*(\d{2,5})/);
          if (m) {
            currentW = parseInt(m[1], 10);
            currentH = parseInt(m[2], 10);
          }
        }
      }

      // 4. Inspect cached info string
      let cachedStr = pageTopBarInfoCache.get(pageNo) || '';
      if (cachedStr && cachedStr.includes('x') && !currentW) {
        const m = cachedStr.match(/(\d{2,5})\s*x\s*(\d{2,5})/);
        if (m) {
          currentW = parseInt(m[1], 10);
          currentH = parseInt(m[2], 10);
        }
      }

      // 5. Render (throttled to avoid layout thrashing during scroll)
      let finalStr = '';
      if (respInfo && respInfo.includes('::') && respInfo.includes('x')) {
        pageTopBarInfoCache.set(pageNo, respInfo);
        finalStr = respInfo;
      } else if (cachedStr && cachedStr.includes('::') && cachedStr.includes('x')) {
        finalStr = cachedStr;
      } else {
        const baseName = (respInfo && !respInfo.includes('x')) ? respInfo :
                         (cachedStr && !cachedStr.includes('x')) ? cachedStr : '';

        if (baseName && currentW > 0 && currentH > 0) {
          finalStr = `${baseName} :: ${currentW} x ${currentH}`;
          pageTopBarInfoCache.set(pageNo, finalStr);
        } else if (currentW > 0 && currentH > 0) {
          finalStr = `Page ${pageNo} :: ${currentW} x ${currentH}`;
        } else if (baseName) {
          finalStr = baseName;
        }
      }

      if (infoContainer._lastText !== finalStr) {
        infoContainer._lastText = finalStr;
        infoContainer.innerHTML = finalStr ? `<div>${finalStr}</div>` : '';
      }
    }

    async function loadAndSliceSprite(spriteUrl, entries) {
      if (!spriteUrl) return;

      if (!spriteEntriesMap.has(spriteUrl)) {
        spriteEntriesMap.set(spriteUrl, []);
      }
      const allEntries = spriteEntriesMap.get(spriteUrl);
      if (entries) {
        for (const e of entries) {
          if (!allEntries.some(existing => existing.page === e.page)) {
            allEntries.push(e);
          }
        }
      }

      if (spriteDimensionsCache.has(spriteUrl)) {
        const { sheetW, sheetH } = spriteDimensionsCache.get(spriteUrl);
        for (const e of (entries || allEntries)) {
          e.sheetW = sheetW;
          e.sheetH = sheetH;
          pageThumbnailCache.set(e.page, { type: 'sprite', ...e, sheetW, sheetH });
          const item = pageItemMap.get(e.page);
          if (item && item.classList.contains('eh-placeholder') && !item.querySelector('img.eh-main-img')) {
            applyThumbnailToPlaceholder(item, e.page);
          }
          if (!stripActive) {
            const singleWrap = document.getElementById('eh-single-thumb-placeholder');
            if (singleWrap && singleWrap.dataset.page === String(e.page)) {
              applyThumbnailToSinglePlaceholder(singleWrap, e.page);
            }
          }
        }
        return;
      }

      if (spriteImagePromiseCache.has(spriteUrl)) {
        return spriteImagePromiseCache.get(spriteUrl);
      }

      const p = (async () => {
        try {
          const plainImg = new Image();
          await new Promise((resolve, reject) => {
            plainImg.onload = () => {
              plainImg.onload = null;
              plainImg.onerror = null;
              resolve();
            };
            plainImg.onerror = (err) => {
              plainImg.onload = null;
              plainImg.onerror = null;
              reject(err);
            };
            plainImg.src = spriteUrl;
          });
          if (plainImg.naturalWidth > 0) {
            const sheetW = plainImg.naturalWidth;
            const sheetH = plainImg.naturalHeight;
            plainImg.removeAttribute('src');
            plainImg.src = '';
            spriteDimensionsCache.set(spriteUrl, { sheetW, sheetH });
            for (const e of allEntries) {
              e.sheetW = sheetW;
              e.sheetH = sheetH;
              pageThumbnailCache.set(e.page, { type: 'sprite', ...e, sheetW, sheetH });
              const item = pageItemMap.get(e.page);
              if (item && item.classList.contains('eh-placeholder') && !item.querySelector('img.eh-main-img')) {
                applyThumbnailToPlaceholder(item, e.page);
              }
              if (!stripActive) {
                const singleWrap = document.getElementById('eh-single-thumb-placeholder');
                if (singleWrap && singleWrap.dataset.page === String(e.page)) {
                  applyThumbnailToSinglePlaceholder(singleWrap, e.page);
                }
              }
            }
          } else {
            plainImg.removeAttribute('src');
            plainImg.src = '';
          }
        } catch (err) {
          console.warn('ExFastReader: Failed to load sprite sheet ' + spriteUrl, err);
        } finally {
          spriteImagePromiseCache.delete(spriteUrl);
        }
      })();

      spriteImagePromiseCache.set(spriteUrl, p);
      return p;
    }

    function createThumbnailElement(info) {
      if (!info) return null;
      if (info.dataUrl || info.type === 'dataUrl' || info.type === 'url') {
        const src = info.url || info.dataUrl;
        const img = document.createElement('img');
        img.className = 'eh-placeholder-thumb-img';
        img.alt = '';
        img.src = src;
        img.style.cssText = `
          position: absolute !important;
          top: 0 !important;
          left: 0 !important;
          width: 100% !important;
          height: 100% !important;
          object-fit: fill !important;
          filter: blur(2px) !important;
          opacity: 1 !important;
          transform: none !important;
          pointer-events: none !important;
        `;
        return img;
      } else if (info.type === 'sprite') {
        const img = document.createElement('img');
        img.className = 'eh-placeholder-thumb-img';
        img.alt = '';

        const renderSpritePosition = (sw, sh) => {
          const estimatedW = (info.h > 150 ? 4000 : 2000);
          const sheetW = sw || info.sheetW || Math.max(estimatedW, (info.x || 0) + (info.w || 200));
          const sheetH = sh || info.sheetH || (info.h > 150 ? 300 : 140);
          const thumbW = info.w || 200;
          const thumbH = info.h || 300;

          const wRatio = (sheetW / thumbW) * 100;
          const hRatio = (sheetH / thumbH) * 100;
          const xOffset = ((info.x || 0) / thumbW) * 100;
          const yOffset = ((info.y || 0) / thumbH) * 100;

          img.style.cssText = `
            position: absolute !important;
            top: -${yOffset}% !important;
            left: -${xOffset}% !important;
            width: ${wRatio}% !important;
            height: ${hRatio}% !important;
            max-width: none !important;
            max-height: none !important;
            object-fit: fill !important;
            filter: blur(2px) !important;
            opacity: 1 !important;
            transform: none !important;
            pointer-events: none !important;
            display: block !important;
          `;
        };

        renderSpritePosition();

        img.onload = () => {
          img.onload = null;
          img.onerror = null;
          if (img.naturalWidth > 0 && img.naturalHeight > 0) {
            info.sheetW = img.naturalWidth;
            info.sheetH = img.naturalHeight;
            renderSpritePosition(img.naturalWidth, img.naturalHeight);
          }
        };
        img.onerror = () => {
          img.onload = null;
          img.onerror = null;
        };

        img.src = info.url;

        if (img.complete && img.naturalWidth > 0) {
          img.onload = null;
          img.onerror = null;
          info.sheetW = img.naturalWidth;
          info.sheetH = img.naturalHeight;
          renderSpritePosition(img.naturalWidth, img.naturalHeight);
        }
        return img;
      }
      return null;
    }

function applyThumbnailToSinglePlaceholder(wrap, pageNo) {
      if (!wrap || wrap.dataset.page !== String(pageNo)) return;

      const info = pageThumbnailCache.get(pageNo);
      if (info) {
        const oldImgs = wrap.querySelectorAll('img');
        oldImgs.forEach(i => {
          i.onload = null;
          i.onerror = null;
          i.removeAttribute('src');
          i.src = '';
        });
        wrap.innerHTML = '';

        const thumbWrap = document.createElement('div');
        thumbWrap.className = 'eh-placeholder-thumb-wrap';
        const thumbImg = createThumbnailElement(info);
        if (thumbImg) {
          thumbWrap.appendChild(thumbImg);
          wrap.appendChild(thumbWrap);
        }
      } else {
        if (!wrap.querySelector('.eh-placeholder-content')) {
          const oldImgs = wrap.querySelectorAll('img');
          oldImgs.forEach(i => {
            i.onload = null;
            i.onerror = null;
            i.removeAttribute('src');
            i.src = '';
          });
          wrap.innerHTML = '';

          const content = document.createElement('div');
          content.className = 'eh-placeholder-content';
          content.innerHTML = `<div class="eh-spinner"></div><div class="eh-placeholder-text">Page ${pageNo}...</div>`;
          wrap.appendChild(content);
        }
        prefetchNearbyThumbnails(pageNo);
      }

      const pageKey = 'eh_retry_' + location.pathname + '_' + pageNo;
      const retryCount = parseInt(safeStorageGet(sessionStorage, pageKey, '0'), 10);
      if (retryCount > 0) {
        const oldContent = wrap.querySelector('.eh-placeholder-content');
        if (oldContent) oldContent.remove();

        const content = document.createElement('div');
        content.className = 'eh-placeholder-content';
        content.innerHTML = `<div class="eh-spinner"></div><div class="eh-placeholder-text">Retrying Page ${pageNo}...</div>`;
        wrap.appendChild(content);
      }
    }

    function showSinglePageThumbnailPlaceholder(pageNo) {
      if (stripActive) return;
      const i3 = document.getElementById('i3');
      if (!i3) return;

      const imgEl = i3.querySelector('img#img') || i3.querySelector('img');
      if (imgEl && imgEl._eh_page === pageNo && !imgEl._eh_obsolete && imgEl.complete && imgEl.naturalWidth > 0) {
        imgEl.style.display = 'block';
        hideSinglePageThumbnailPlaceholder();
        return;
      }

      if (imgEl && imgEl._eh_page !== pageNo) {
        imgEl.style.display = 'none';
        imgEl._eh_obsolete = true;
        imgEl.__eh_monitored = false;
      } else if (imgEl) {
        imgEl.style.display = 'none';
      }

      let wrap = document.getElementById('eh-single-thumb-placeholder');
      if (!wrap) {
        wrap = document.createElement('div');
        wrap.id = 'eh-single-thumb-placeholder';
        if (imgEl && imgEl.parentNode) {
          imgEl.parentNode.insertBefore(wrap, imgEl);
        } else {
          i3.appendChild(wrap);
        }
      }

      wrap.dataset.page = String(pageNo);

      const dim = pageDimensions.get(pageNo);
      let ar = (dim && dim.aspectRatio) ? dim.aspectRatio : defaultAspectRatio;
      let targetW = (dim && dim.width > 0) ? dim.width : 0;
      let targetH = (dim && dim.height > 0) ? dim.height : 0;

      if (!targetW && pageRespCache.has(pageNo)) {
        const resp = pageRespCache.get(pageNo);
        if (resp && resp.i3) {
          const mW = resp.i3.match(/style=["'][^"']*width:\s*(\d+)px/i);
          const mH = resp.i3.match(/style=["'][^"']*height:\s*(\d+)px/i);
          if (mW && mH) {
            targetW = parseInt(mW[1], 10);
            targetH = parseInt(mH[1], 10);
            ar = targetW / targetH;
          }
        }
        if (!targetW && resp && resp.x && resp.y) {
          targetW = parseInt(resp.x, 10);
          targetH = parseInt(resp.y, 10);
          ar = targetW / targetH;
        }
      }

      if (!targetW) {
        const prevImg = document.getElementById('img');
        const prevW = prevImg ? (parseInt(prevImg.style.width, 10) || prevImg.naturalWidth) : 0;
        targetW = (prevW > 400 && prevW < 3000) ? prevW : Math.min(1280, window.innerWidth - 24);
        targetH = Math.round(targetW / ar);
      } else if (!targetH) {
        targetH = Math.round(targetW / ar);
      }

      const maxW = Math.min(targetW, window.innerWidth - 24);
      const maxH = Math.round(maxW / ar);

      wrap.style.cssText = `
        position: relative !important;
        display: flex !important;
        align-items: center !important;
        justify-content: center !important;
        margin: 0 auto !important;
        width: ${maxW}px !important;
        max-width: 100% !important;
        height: ${maxH}px !important;
        aspect-ratio: ${ar} !important;
        box-sizing: border-box !important;
        overflow: hidden !important;
      `;

      applyThumbnailToSinglePlaceholder(wrap, pageNo);
    }

    function hideSinglePageThumbnailPlaceholder() {
      const i3 = document.getElementById('i3');
      const imgEl = i3 ? (i3.querySelector('img#img') || i3.querySelector('img')) : null;
      if (imgEl && imgEl._eh_page === currentPageNum && !imgEl._eh_obsolete && imgEl.complete && imgEl.naturalWidth > 0) {
        imgEl.style.display = 'block';
      }
      const wrap = document.getElementById('eh-single-thumb-placeholder');
      if (wrap) {
        const oldImgs = wrap.querySelectorAll('img');
        oldImgs.forEach(i => {
          i.onload = null;
          i.onerror = null;
          i.removeAttribute('src');
          i.src = '';
        });
        wrap.remove();
      }
    }

    function applyThumbnailToPlaceholder(item, pageNo) {
      if (!item || !item.classList.contains('eh-placeholder')) return;
      if (item.querySelector('img.eh-main-img')) return;

      const info = pageThumbnailCache.get(pageNo);
      if (!info) return;

      const existingThumbs = item.querySelectorAll('.eh-placeholder-thumb-img, .eh-placeholder-thumb-wrap');
      existingThumbs.forEach(el => {
        if (el.tagName === 'IMG') {
          el.onload = null;
          el.onerror = null;
          el.removeAttribute('src');
          el.src = '';
        } else {
          const imgs = el.querySelectorAll('img');
          imgs.forEach(i => { i.onload = null; i.onerror = null; i.removeAttribute('src'); i.src = ''; });
        }
        el.remove();
      });

      const content = item.querySelector('.eh-placeholder-content');
      const wrap = document.createElement('div');
      wrap.className = 'eh-placeholder-thumb-wrap';

      if (info.dataUrl || info.type === 'dataUrl' || info.type === 'url') {
        const src = info.url || info.dataUrl;
        const img = document.createElement('img');
        img.className = 'eh-placeholder-thumb-img';
        img.alt = '';
        img.src = src;
        img.style.cssText = `
          position: absolute !important;
          top: 0 !important;
          left: 0 !important;
          width: 100% !important;
          height: 100% !important;
          object-fit: fill !important;
          filter: blur(2px) !important;
          opacity: 1 !important;
          transform: none !important;
          pointer-events: none !important;
        `;
        wrap.appendChild(img);
      } else if (info.type === 'sprite') {
        const img = document.createElement('img');
        img.className = 'eh-placeholder-thumb-img';
        img.alt = '';

        const renderSpritePosition = (sw, sh) => {
          const estimatedW = (info.h > 150 ? 4000 : 2000);
          const sheetW = sw || info.sheetW || Math.max(estimatedW, (info.x || 0) + (info.w || 200));
          const sheetH = sh || info.sheetH || (info.h > 150 ? 300 : 140);
          const thumbW = info.w || 200;
          const thumbH = info.h || 300;

          const wRatio = (sheetW / thumbW) * 100;
          const hRatio = (sheetH / thumbH) * 100;
          const xOffset = ((info.x || 0) / thumbW) * 100;
          const yOffset = ((info.y || 0) / thumbH) * 100;

          img.style.cssText = `
            position: absolute !important;
            top: -${yOffset}% !important;
            left: -${xOffset}% !important;
            width: ${wRatio}% !important;
            height: ${hRatio}% !important;
            max-width: none !important;
            max-height: none !important;
            object-fit: fill !important;
            filter: blur(2px) !important;
            opacity: 1 !important;
            transform: none !important;
            pointer-events: none !important;
            display: block !important;
          `;
        };

        renderSpritePosition();

        img.onload = () => {
          img.onload = null;
          img.onerror = null;
          if (img.naturalWidth > 0 && img.naturalHeight > 0) {
            info.sheetW = img.naturalWidth;
            info.sheetH = img.naturalHeight;
            renderSpritePosition(img.naturalWidth, img.naturalHeight);
          }
        };
        img.onerror = () => {
          img.onload = null;
          img.onerror = null;
        };

        img.src = info.url;

        if (img.complete && img.naturalWidth > 0) {
          img.onload = null;
          img.onerror = null;
          info.sheetW = img.naturalWidth;
          info.sheetH = img.naturalHeight;
          renderSpritePosition(img.naturalWidth, img.naturalHeight);
        }

        wrap.appendChild(img);
      }

      if (content) {
        item.insertBefore(wrap, content);
      } else {
        item.appendChild(wrap);
      }
    }

    async function drainGalleryQueue(queue) {
      if (!queue || queue._draining) return;
      queue._draining = true;
      const worker = async () => {
        while (queue.length > 0) {
          const pIndex = queue.shift();
          await fetchGalleryPage(pIndex);
          if (queue.length > 0) {
            await new Promise(r => setTimeout(r, 300));
          }
        }
        queue._draining = false;
      };
      await worker();
    }

    function prefetchNearbyThumbnails(curPage) {
      if (!isReaderPage) return;
      const total = getTotalPages();
      const THUMB_WINDOW = 20;
      const startP = Math.max(1, curPage - 10);
      const endP = total > 0 ? Math.min(total, curPage + THUMB_WINDOW) : (curPage + THUMB_WINDOW);

      const galleryPagesToFetch = [];
      for (let p = startP; p <= endP; p++) {
        if (!pageThumbnailCache.has(p) || !pageImgKeyCache.has(p)) {
          const gIndex = Math.max(0, Math.floor((p - 1) / thumbsPerPage));
          if (!fetchedGalleryPages.has(gIndex) && !inFlightGalleryFetches.has(gIndex)) {
            if (!galleryPagesToFetch.includes(gIndex)) {
              galleryPagesToFetch.push(gIndex);
            }
          }
        }
      }

      for (const gIndex of galleryPagesToFetch) {
        fetchGalleryPage(gIndex);
      }
    }

    async function discoverAndFetchGallery(targetPage = 1) {
      let galleryUrl = getGalleryUrl();
      if (!galleryUrl) {
        for (let attempt = 0; attempt < 12; attempt++) {
          await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
          galleryUrl = getGalleryUrl();
          if (galleryUrl) break;
        }
        if (!galleryUrl) {
          if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => discoverAndFetchGallery(targetPage), { once: true });
          }
          return;
        }
      }

      const targetIndex = Math.max(0, Math.floor((targetPage - 1) / thumbsPerPage));
      if (pageThumbnailCache.has(targetPage) && pageImgKeyCache.has(targetPage)) {
        fetchedGalleryPages.set(targetIndex, Date.now());
      } else {
        await fetchGalleryPage(targetIndex);
      }

      const total = getTotalPages();
      if (total > 0) {
        const totalGalleryPages = Math.ceil(total / thumbsPerPage);
        const queue = [];
        // Window of 20 gallery pages for deep early thumbnail prefetching
        const maxOffset = Math.min(2, totalGalleryPages);
        for (let offset = 1; offset <= maxOffset; offset++) {
          const f = targetIndex + offset;
          const b = targetIndex - offset;
          if (f < totalGalleryPages && !fetchedGalleryPages.has(f)) queue.push(f);
          if (b >= 0 && !fetchedGalleryPages.has(b)) queue.push(b);
        }
        drainGalleryQueue(queue);
      }
    }

    function extractDimensionsFromGallery(source) {
      if (!source) return 0;
      let count = 0;
      try {
        let doc;
        let htmlStr = '';
        if (typeof source === 'string') {
          htmlStr = source;
          doc = new DOMParser().parseFromString(source, 'text/html');
        } else if (source && (source.nodeType || source.querySelector)) {
          doc = source;
          htmlStr = (source.documentElement ? source.documentElement.innerHTML : source.innerHTML) || '';
        }
        if (!doc) return 0;

        const mGpc = htmlStr.match(/Showing\s+(\d+)\s*-\s*(\d+)\s+of\s+([\d,]+)\s+images/i) ||
                     (doc.querySelector('.gpc, #gpc')?.textContent || '').match(/Showing\s+(\d+)\s*-\s*(\d+)\s+of\s+([\d,]+)\s+images/i);
        if (mGpc) {
          const sNum = parseInt(mGpc[1], 10);
          const eNum = parseInt(mGpc[2], 10);
          const tot = parseInt(mGpc[3].replace(/,/g, ''), 10);
          if (eNum < tot) {
            const countPerPg = eNum - sNum + 1;
            if ([20, 40, 100].includes(countPerPg)) {
              thumbsPerPage = countPerPg;
              safeStorageSet(localStorage, 'eh_thumbs_per_page', String(countPerPg));
            }
          }
          if (tot > 0 && (!cachedTotalPages || cachedTotalPages < tot)) {
            cachedTotalPages = tot;
            if (isReaderPage && stripActive) {
              ensureAllPlaceholders(tot);
            }
          }
        }
        const links = doc.querySelectorAll('a[href*="/s/"]');
        const spritesToSlice = new Map();

        withScrollAnchor(() => {
          for (const a of links) {
            const href = a.getAttribute('href') || a.href || '';
            const m = href.match(/\/s\/([0-9a-zA-Z]+)\/\d+-(\d+)/i);
            if (!m) continue;
            count++;
            const key = m[1];
            const page = parseInt(m[2], 10);
            pageImgKeyCache.set(page, key);

            const img = a.querySelector('img');
            const thumbDiv = a.querySelector('[style*="url("]') ||
                             a.querySelector('[style*="background"]') ||
                             a.closest('[style*="url("]') ||
                             a.closest('[style*="background"]') ||
                             a.querySelector('div[title]') ||
                             a.querySelector('div');

            const parent = a.closest('.gdtm > div') || a.closest('.gdtm') || a.closest('.gdtl') || a.parentElement;

            const titleEl = a.querySelector('[title*="Page"]') ||
                            a.querySelector('[title]') ||
                            thumbDiv ||
                            img ||
                            a ||
                            parent;

            const title = (
              titleEl?.getAttribute('title') ||
              img?.getAttribute('title') ||
              a.getAttribute('title') ||
              parent?.getAttribute('title') ||
              img?.getAttribute('alt') ||
              ''
            ).trim();

            const cleanInfo = cleanImageInfo(title);
            if (cleanInfo) {
              const existingInfo = pageTopBarInfoCache.get(page) || '';
              if (!existingInfo || !existingInfo.includes('x') || cleanInfo.includes('x')) {
                pageTopBarInfoCache.set(page, cleanInfo);
              }
              const sz = parseFileSizeInBytes(cleanInfo);
              if (sz > 0) pageFileSizeCache.set(page, sz);
              if (currentPageNum === page) {
                updateTopBarInfo(page);
              }
            }

            const styleStr = [
              thumbDiv?.getAttribute('style') || '',
              a.getAttribute('style') || '',
              parent?.getAttribute('style') || '',
              img?.getAttribute('style') || ''
            ].join(' ');

            let w = 0, h = 0;
            const mW = styleStr.match(/(?:^|[;\s])width:\s*(\d+)px/i);
            const mH = styleStr.match(/(?:^|[;\s])height:\s*(\d+)px/i);
            if (mW) w = parseInt(mW[1], 10);
            if (mH) h = parseInt(mH[1], 10);

            if (!w && img) {
              w = parseInt(img.style.width || img.getAttribute('width') || '0', 10);
            }
            if (!h && img) {
              h = parseInt(img.style.height || img.getAttribute('height') || '0', 10);
            }
            if (!w && thumbDiv) {
              w = parseInt(thumbDiv.style.width || thumbDiv.getAttribute('width') || '0', 10);
            }
            if (!h && thumbDiv) {
              h = parseInt(thumbDiv.style.height || thumbDiv.getAttribute('height') || '0', 10);
            }

            const dimMatch = (cleanInfo || title).match(/(\d{3,5})\s*x\s*(\d{3,5})/);
            let hasRealDim = false;
            if (dimMatch) {
              const realW = parseInt(dimMatch[1], 10);
              const realH = parseInt(dimMatch[2], 10);
              if (realW > 0 && realH > 0) {
                const ar = realW / realH;
                pageDimensions.set(page, { width: realW, height: realH, aspectRatio: ar });
                const item = pageItemMap.get(page);
                if (item) applyItemDimensions(item, page);
                hasRealDim = true;
              }
            }

            if (!hasRealDim && w > 0 && h > 0) {
              const ar = w / h;
              if (ar >= 0.3 && ar <= 2.8) {
                if (!pageDimensions.has(page) || pageDimensions.get(page).width === 0) {
                  pageDimensions.set(page, { width: 0, height: 0, aspectRatio: ar });
                  const item = pageItemMap.get(page);
                  if (item) applyItemDimensions(item, page);
                }
              }
            }

            const imgSrc = img ? (img.getAttribute('src') || img.src || '') : '';
            const isBlank = !imgSrc || imgSrc.includes('blank.gif');

            const mUrl = styleStr.match(/url\(["']?([^"')]+)["']?\)/i);
            const bgUrl = mUrl ? mUrl[1].trim() : '';

            if (img && !isBlank) {
              pageThumbnailCache.set(page, { type: 'url', url: imgSrc, w, h });
              const preImg = new Image();
              preImg.decoding = 'async';
              preImg.src = imgSrc;
              const item = pageItemMap.get(page);
              if (item && item.classList.contains('eh-placeholder') && !item.querySelector('img.eh-main-img')) {
                applyThumbnailToPlaceholder(item, page);
              }
              if (!stripActive) {
                const singleWrap = document.getElementById('eh-single-thumb-placeholder');
                if (singleWrap && singleWrap.dataset.page === String(page)) {
                  applyThumbnailToSinglePlaceholder(singleWrap, page);
                }
              }
            } else if (bgUrl) {
              let posX = 0, posY = 0;
              const mPos = styleStr.match(/background-position:\s*(-?\d+(?:px)?)\s+(-?\d+(?:px)?)/i) ||
                           styleStr.match(/url\([^)]+\)\s+(-?\d+(?:px)?)\s+(-?\d+(?:px)?)/i) ||
                           styleStr.match(/(-?\d+(?:px)?)\s+(-?\d+(?:px)?)\s+no-repeat/i) ||
                           styleStr.match(/no-repeat\s+(-?\d+(?:px)?)\s+(-?\d+(?:px)?)/i) ||
                           styleStr.match(/(-?\d+px)\s+(-?\d+px)/i);
              if (mPos) {
                posX = Math.abs(parseInt(mPos[1].replace(/[^\d-]/g, ''), 10) || 0);
                posY = Math.abs(parseInt(mPos[2].replace(/[^\d-]/g, ''), 10) || 0);
              }
              const thumbW = w || 200;
              const thumbH = h || 300;

              let sheetW = 0, sheetH = 0;
              if (spriteDimensionsCache.has(bgUrl)) {
                const sDim = spriteDimensionsCache.get(bgUrl);
                sheetW = sDim.sheetW;
                sheetH = sDim.sheetH;
              }
              const entry = { page, x: posX, y: posY, w: thumbW, h: thumbH, url: bgUrl, sheetW, sheetH };
              pageThumbnailCache.set(page, { type: 'sprite', ...entry });
              const preSprite = new Image();
              preSprite.decoding = 'async';
              preSprite.src = bgUrl;

              if (sheetW > 0 && sheetH > 0) {
                const item = pageItemMap.get(page);
                if (item && item.classList.contains('eh-placeholder') && !item.querySelector('img.eh-main-img')) {
                  applyThumbnailToPlaceholder(item, page);
                }
                if (!stripActive && currentPageNum === page) {
                  showSinglePageThumbnailPlaceholder(page);
                }
              }

              if (!spritesToSlice.has(bgUrl)) {
                spritesToSlice.set(bgUrl, []);
              }
              spritesToSlice.get(bgUrl).push(entry);
            }
          }
        });

        for (const [bgUrl, entries] of spritesToSlice.entries()) {
          const maxW = Math.max(...entries.map(e => e.x + (e.w || 200)));
          const maxH = Math.max(...entries.map(e => e.y + (e.h || 300)));
          const estimatedSheetW = Math.max(2000, Math.ceil(maxW / 100) * 100);
          const estimatedSheetH = Math.max(maxH > 150 ? 300 : 140, maxH);
          for (const e of entries) {
            if (!e.sheetW) {
              e.sheetW = estimatedSheetW;
              e.sheetH = estimatedSheetH;
              pageThumbnailCache.set(e.page, { type: 'sprite', ...e, sheetW: estimatedSheetW, sheetH: estimatedSheetH });
              const item = pageItemMap.get(e.page);
              if (item && item.classList.contains('eh-placeholder') && !item.querySelector('img.eh-main-img')) {
                applyThumbnailToPlaceholder(item, e.page);
              }
              if (!stripActive && currentPageNum === e.page) {
                showSinglePageThumbnailPlaceholder(e.page);
              }
            }
          }
          loadAndSliceSprite(bgUrl, entries);
        }
        if (cachedGid) {
          saveGalleryDataToSession(cachedGid);
        }
      } catch (err) {
        console.warn('ExFastReader: Failed to extract gallery dimensions/thumbnails', err);
      }
      return count;
    }

    async function fetchGalleryPage(pageIndex) {
      if (pageIndex < 0) return;
      const lastFetch = fetchedGalleryPages.get(pageIndex);
      if (lastFetch && Date.now() - lastFetch < 30000) {
        return;
      }
      if (inFlightGalleryFetches.has(pageIndex)) {
        return inFlightGalleryFetches.get(pageIndex);
      }

      const baseGalleryUrl = getBaseGalleryUrl();
      if (!baseGalleryUrl) return;

      const promise = (async () => {
        try {
          let fetchUrl;
          try {
            const u = new URL(baseGalleryUrl, location.origin);
            if (pageIndex > 0) {
              u.searchParams.set('p', String(pageIndex));
            } else {
              u.searchParams.delete('p');
            }
            fetchUrl = u.href;
          } catch {
            const cleanBase = baseGalleryUrl.replace(/[?&]p=\d+/g, '').replace(/\?$/, '');
            fetchUrl = pageIndex > 0 ? (cleanBase + (cleanBase.includes('?') ? '&' : '?') + 'p=' + pageIndex) : cleanBase;
          }

          const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
          const timer = setTimeout(() => { if (controller) controller.abort(); }, 12000);
          const res = await fetch(fetchUrl, {
            credentials: 'include',
            signal: controller ? controller.signal : undefined
          }).finally(() => clearTimeout(timer));

          if (res.ok) {
            fetchedGalleryPages.set(pageIndex, Date.now());
            const html = await res.text();
            extractKeysFromText(html);
            extractDimensionsFromGallery(html);
          } else {
            fetchedGalleryPages.set(pageIndex, Date.now() - 25000);
          }
        } catch (err) {
          console.warn('ExFastReader: Gallery fetch failed for p=' + pageIndex, err);
          fetchedGalleryPages.set(pageIndex, Date.now() - 25000);
        } finally {
          inFlightGalleryFetches.delete(pageIndex);
        }
      })();

      inFlightGalleryFetches.set(pageIndex, promise);
      return promise;
    }

    async function ensureKeysForPage(targetPageNo) {
      if (pageImgKeyCache.has(targetPageNo)) {
        return pageImgKeyCache.get(targetPageNo);
      }
      const total = getTotalPages();
      if (total > 0 && targetPageNo > total) {
        return null;
      }

      const primaryIndex = Math.max(0, Math.floor((targetPageNo - 1) / thumbsPerPage));
      await fetchGalleryPage(primaryIndex);
      if (pageImgKeyCache.has(targetPageNo)) {
        return pageImgKeyCache.get(targetPageNo);
      }

      for (const altCount of [20, 40, 100]) {
        if (altCount === thumbsPerPage) continue;
        const altIndex = Math.max(0, Math.floor((targetPageNo - 1) / altCount));
        if (altIndex !== primaryIndex) {
          await fetchGalleryPage(altIndex);
          if (pageImgKeyCache.has(targetPageNo)) {
            thumbsPerPage = altCount;
            return pageImgKeyCache.get(targetPageNo);
          }
        }
      }

      return pageImgKeyCache.get(targetPageNo) || null;
    }

    function getNextMeta(doc) {
      const nextEl = doc.querySelector('a#next');
      if (!nextEl) return null;
      const oc = nextEl.getAttribute('onclick') || '';
      const mClick = oc.match(/load_image\((\d+),\s*['"]([^'"]+)['"]\)/i);
      if (mClick) return { page: parseInt(mClick[1], 10), imgKey: mClick[2] };
      const mHref = (nextEl.getAttribute('href') || nextEl.href || '').match(/\/s\/([0-9a-zA-Z]+)\/\d+-(\d+)/i);
      if (mHref) return { page: parseInt(mHref[2], 10), imgKey: mHref[1] };
      return null;
    }

    function getTotalPages() {
      if (cachedTotalPages > 0) return cachedTotalPages;
      const spans = document.querySelectorAll('#i2 .sn span, #i4 .sn span');
      for (let i = 0; i < spans.length; i++) {
        const t = parseInt(spans[i].textContent.trim(), 10);
        if (!isNaN(t) && t > 1) {
          if (i + 1 < spans.length) {
            const nextT = parseInt(spans[i + 1].textContent.trim(), 10);
            if (!isNaN(nextT) && nextT >= t) {
              cachedTotalPages = nextT;
              return nextT;
            }
          }
          cachedTotalPages = t;
          return t;
        }
      }
      const snEl = document.querySelector('#i2 .sn, #i4 .sn, .sn');
      if (snEl) {
        const match = snEl.textContent.match(/(\d+)\s*\/\s*(\d+)/);
        if (match) {
          const total = parseInt(match[2], 10);
          if (!isNaN(total) && total > 0) {
            cachedTotalPages = total;
            return total;
          }
        }
      }
      return 0;
    }

    function getPageParams() {
      if (cachedGid && cachedShowkey) {
        return { gid: cachedGid, showkey: cachedShowkey, api_url: cachedApiUrl };
      }
      let gid = window.gid || cachedGid;
      let showkey = window.showkey || cachedShowkey;
      let api_url = window.api_url || cachedApiUrl;

      if (!gid || !showkey) {
        for (const s of document.scripts) {
          const txt = s.textContent || '';
          if (!gid) {
            const mG = txt.match(/var\s+gid\s*=\s*(\d+)/);
            if (mG) gid = parseInt(mG[1], 10);
          }
          if (!showkey) {
            const mS = txt.match(/var\s+showkey\s*=\s*['"]([^'"]+)['"]/);
            if (mS) showkey = mS[1];
          }
          if (!api_url) {
            const mA = txt.match(/var\s+api_url\s*=\s*['"]([^'"]+)['"]/);
            if (mA) api_url = mA[1];
          }
          if (gid && showkey && api_url) break;
        }
      }
      if (gid) cachedGid = parseInt(gid, 10);
      if (showkey) cachedShowkey = showkey;
      if (api_url) cachedApiUrl = normalizeApiUrl(api_url);
      else cachedApiUrl = location.origin + '/api.php';

      return { gid: cachedGid || 0, showkey, api_url: cachedApiUrl };
    }

    function pruneCaches(activePage) {
      if (stripActive) {
        withScrollAnchor(() => {
          const UNLOAD_DISTANCE = 15;
          for (const p of Array.from(loadedImages)) {
            if (Math.abs(p - activePage) > UNLOAD_DISTANCE) {
              const item = pageItemMap.get(p);
              if (item) {
                if (typeof item._abortImg === 'function') {
                  item._abortImg();
                  item._abortImg = null;
                }
                const img = item.querySelector('img.eh-main-img');
                if (img) {
                  img.onload = null;
                  img.onerror = null;
                  img.removeAttribute('src');
                  img.src = '';
                  img.remove();
                }
                item._currentImgToken = null;
                loadedImages.delete(p);
                renderPlaceholderContent(item, p);
              }
            }
          }

          // Also abort distant in-flight image loads to free connection pool
          for (const [p, startTime] of loadingPages.entries()) {
            if (Math.abs(p - activePage) > UNLOAD_DISTANCE) {
              loadingPages.delete(p);
              const item = pageItemMap.get(p);
              if (item) {
                item.classList.remove('eh-loading');
                if (typeof item._abortImg === 'function') {
                  item._abortImg();
                  item._abortImg = null;
                } else if (item._currentImgToken) {
                  const img = item._currentImgToken;
                  img.onload = null;
                  img.onerror = null;
                  img.removeAttribute('src');
                  img.src = '';
                }
                item._currentImgToken = null;
              }
            }
          }

          // Unload distant placeholder thumbnail images (> 30 pages away) to free memory
          const THUMB_UNLOAD_DISTANCE = 30;
          for (const [p, item] of pageItemMap.entries()) {
            if (Math.abs(p - activePage) > THUMB_UNLOAD_DISTANCE && item.classList.contains('eh-placeholder')) {
              const oldThumbs = item.querySelectorAll('.eh-placeholder-thumb-img, .eh-placeholder-thumb-wrap');
              if (oldThumbs.length > 0) {
                oldThumbs.forEach(t => {
                  if (t.tagName === 'IMG') {
                    t.onload = null;
                    t.onerror = null;
                    t.removeAttribute('src');
                    t.src = '';
                  } else {
                    t.querySelectorAll('img').forEach(i => {
                      i.onload = null;
                      i.onerror = null;
                      i.removeAttribute('src');
                      i.src = '';
                    });
                  }
                  t.remove();
                });
              }
            }
          }
        });
      }

      for (const [page, record] of preloadedImages.entries()) {
        if (stripActive || page < activePage - 2 || page > activePage + 3) {
          if (record && record.img) {
            record.img.onload = null;
            record.img.onerror = null;
            record.img.removeAttribute('src');
            record.img.src = '';
          }
          preloadedImages.delete(page);
        }
      }

      const respLimit = stripActive ? 80 : 10;
      if (pageRespCache.size > respLimit) {
        for (const page of pageRespCache.keys()) {
          if (Math.abs(page - activePage) > (stripActive ? 40 : 3)) {
            pageRespCache.delete(page);
            if (pageRespCache.size <= respLimit) break;
          }
        }
      }

      if (pageImgSrcCache.size > 100) {
        for (const page of pageImgSrcCache.keys()) {
          if (Math.abs(page - activePage) > 50) {
            pageImgSrcCache.delete(page);
            if (pageImgSrcCache.size <= 100) break;
          }
        }
      }

      for (const key of inFlightFetches.keys()) {
        const p = parseInt(key, 10);
        if (!isNaN(p) && Math.abs(p - activePage) > 20) {
          inFlightFetches.delete(key);
        }
      }

      if (pageTopBarInfoCache.size > 150) {
        for (const page of pageTopBarInfoCache.keys()) {
          if (Math.abs(page - activePage) > 75) {
            pageTopBarInfoCache.delete(page);
            if (pageTopBarInfoCache.size <= 150) break;
          }
        }
      }

      if (pageFileSizeCache.size > 150) {
        for (const page of pageFileSizeCache.keys()) {
          if (Math.abs(page - activePage) > 75) {
            pageFileSizeCache.delete(page);
            if (pageFileSizeCache.size <= 150) break;
          }
        }
      }

      if (pageNlCodes.size > 80) {
        for (const page of pageNlCodes.keys()) {
          if (Math.abs(page - activePage) > 40) {
            pageNlCodes.delete(page);
            if (pageNlCodes.size <= 80) break;
          }
        }
      }

      if (failedPages.size > 50) {
        for (const page of failedPages.keys()) {
          if (Math.abs(page - activePage) > 40) {
            failedPages.delete(page);
            if (failedPages.size <= 50) break;
          }
        }
      }

      const curGalleryIdx = Math.max(0, Math.floor((activePage - 1) / thumbsPerPage));
      if (fetchedGalleryPages.size > 40) {
        for (const gIdx of fetchedGalleryPages.keys()) {
          if (Math.abs(gIdx - curGalleryIdx) > 15) {
            fetchedGalleryPages.delete(gIdx);
            if (fetchedGalleryPages.size <= 40) break;
          }
        }
      }

      if (pageImgKeyCache.size > 500) {
        for (const page of pageImgKeyCache.keys()) {
          if (Math.abs(page - activePage) > 250) {
            pageImgKeyCache.delete(page);
            if (pageImgKeyCache.size <= 500) break;
          }
        }
      }
    }

function fetchApi(pageNo, imgKey, nlCode = null, forceFresh = false, isPriority = false) {
      const parsedPage = parseInt(pageNo, 10);
      const tot = getTotalPages();
      if (!parsedPage || isNaN(parsedPage) || parsedPage < 1 || (tot > 0 && parsedPage > tot)) {
        return Promise.resolve(null);
      }
      if (!imgKey || typeof imgKey !== 'string' || !/^[0-9a-zA-Z]+$/.test(imgKey)) {
        return Promise.resolve(null);
      }

      const cacheKey = parsedPage + (nlCode ? '_' + nlCode : '');
      if (!forceFresh && inFlightFetches.has(cacheKey)) {
        return inFlightFetches.get(cacheKey);
      }

      const { gid, showkey } = getPageParams();
      if (!gid || !showkey || !imgKey) return Promise.resolve(null);

      const requestPayload = {
        method: 'showpage',
        gid: parseInt(gid, 10),
        page: parsedPage,
        imgkey: String(imgKey).trim(),
        showkey: showkey
      };
      if (nlCode) {
        requestPayload.nl = nlCode;
      }

      const fetchPromise = (async () => {
        const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
        // Increased from 10s to 18s to allow queued background requests to complete
        const timer = setTimeout(() => {
          if (controller) controller.abort();
        }, 18000);

        try {
          // ALWAYS send to same-origin /api.php
          const res = await fetch(location.origin + '/api.php', {
            method: 'POST',
            credentials: 'include',
            headers: {
              'Content-Type': 'application/json',
              'Accept': 'application/json'
            },
            body: JSON.stringify(requestPayload),
            signal: controller ? controller.signal : undefined,
            priority: isPriority ? 'high' : 'low'
          });

          clearTimeout(timer);

          if (!res.ok) {
            console.warn(`[ExFastReader] API returned HTTP ${res.status} on page ${parsedPage}`);
            return null;
          }

          const text = await res.text();
          const data = safeParseJson(text);

          if (data && !data.error) {
            return data;
          } else {
            if (data && data.error) {
              console.warn(`[ExFastReader] API returned error on page ${parsedPage}:`, data.error);
            }
            return null;
          }
        } catch (err) {
          clearTimeout(timer);
          console.warn(`[ExFastReader] Request failed on page ${parsedPage}:`, err.name === 'AbortError' ? 'Timed out (18s)' : err.message);
          return null;
        } finally {
          inFlightFetches.delete(cacheKey);
        }
      })();

      inFlightFetches.set(cacheKey, fetchPromise);
      return fetchPromise;
    }

    function prefetchPage(pageNo, depthRemaining) {
      if (stripActive) return;

const onResponse = (resp) => {
        pageRespCache.set(pageNo, resp);

        const extracted = extractInfoFromResp(resp, pageNo);
        if (extracted) pageTopBarInfoCache.set(pageNo, extracted);

        const nextDoc = parseTemplate(resp.n);
        const nextMeta = getNextMeta(nextDoc);
        if (nextMeta) {
          pageImgKeyCache.set(nextMeta.page, nextMeta.imgKey);
        }

        const i3Doc = parseTemplate(resp.i3);
        const imgEl = i3Doc.querySelector('img#img');
        const imgSrc = imgEl ? (imgEl.getAttribute('src') || imgEl.src) : null;

        if (depthRemaining > 0 && nextMeta) {
          prefetchPage(nextMeta.page, depthRemaining - 1);
        }

        if (imgSrc && !preloadedImages.has(pageNo)) {
          const img = new Image();
          img.fetchPriority = 'low';
          img.decoding = 'async';

          const record = { img, isReady: false, promise: null };

          record.promise = new Promise((resolve) => {
            img.onload = async () => {
              img.onload = null;
              img.onerror = null;
              try {
                if ('decode' in img) await img.decode();
              } catch {}
              record.isReady = true;
              resolve(img);
            };

            img.onerror = () => {
              img.onload = null;
              img.onerror = null;
              record.isReady = false;
              resolve(null);
            };

            img.src = imgSrc;
          });

          preloadedImages.set(pageNo, record);
        }
      };

      if (pageRespCache.has(pageNo)) {
        onResponse(pageRespCache.get(pageNo));
        return;
      }

      const imgKey = pageImgKeyCache.get(pageNo);
      if (!imgKey) return;

      fetchApi(pageNo, imgKey).then((resp) => {
        if (resp) onResponse(resp);
      });
    }

    function prefetchSingleMode(cur) {
      if (stripActive) return;
      const total = getTotalPages();
      prefetchNearbyThumbnails(cur);

      const targets = [cur + 1, cur + 2, cur + 3, cur - 1].filter(p => p >= 1 && (!total || p <= total));
      for (const p of targets) {
        if (pageRespCache.has(p)) {
          const resp = pageRespCache.get(p);
          const i3Doc = parseTemplate(resp.i3);
          const imgEl = i3Doc.querySelector('img#img') || i3Doc.querySelector('img');
          const imgSrc = imgEl ? (imgEl.getAttribute('src') || imgEl.src) : null;
          if (imgSrc && !preloadedImages.has(p)) {
            preloadSingleImage(p, imgSrc);
          }
          continue;
        }

        const imgKey = pageImgKeyCache.get(p);
        if (imgKey) {
          fetchApi(p, imgKey).then((resp) => {
            if (resp) {
              pageRespCache.set(p, resp);
              extractKeysFromResp(resp, p);
              const extracted = extractInfoFromResp(resp, p);
              if (extracted) pageTopBarInfoCache.set(p, extracted);

              const i3Doc = parseTemplate(resp.i3);
              const imgEl = i3Doc.querySelector('img#img') || i3Doc.querySelector('img');
              const imgSrc = imgEl ? (imgEl.getAttribute('src') || imgEl.src) : null;
              if (imgSrc) {
                pageImgSrcCache.set(p, imgSrc);
                preloadSingleImage(p, imgSrc);
              }
            }
          });
        }
      }
    }

    function preloadSingleImage(pageNo, src) {
      if (preloadedImages.has(pageNo)) return;
      const img = new Image();
      img.fetchPriority = 'low';
      img.decoding = 'async';
      const record = { img, isReady: false, promise: null };
      const startTime = Date.now();
      record.promise = new Promise((resolve) => {
        img.onload = async () => {
          img.onload = null;
          img.onerror = null;
          try {
            if ('decode' in img) await img.decode();
          } catch {}
          record.isReady = true;
          const durationSec = Math.max(0.1, (Date.now() - startTime) / 1000);
          recordDownloadSpeedSample(pageNo, durationSec);
          resolve(img);
        };
        img.onerror = () => {
          img.onload = null;
          img.onerror = null;
          record.isReady = false;
          resolve(null);
        };
        img.src = src;
      });
      preloadedImages.set(pageNo, record);
    }
    function startPrefetch() {
      const nextMeta = getNextMeta(document);
      if (nextMeta) {
        pageImgKeyCache.set(nextMeta.page, nextMeta.imgKey);
      }
      prefetchSingleMode(currentPageNum);
    }

    // =========================================================================
    // 4. BIDIRECTIONAL STRIP MODE ENGINE (RTL & VERTICAL WITH PLACEHOLDERS)
    // =========================================================================
    function getOrCreateStripContainer() {
      if (!isReaderPage) return null;
      let container = document.getElementById('eh-strip-container');
      if (!container) {
        container = document.createElement('div');
        container.id = 'eh-strip-container';
        container.className = `eh-strip-container layout-${currentLayout}`;

        const i3 = document.getElementById('i3');
        if (i3 && i3.parentNode) {
          i3.parentNode.insertBefore(container, i3.nextSibling);
        } else {
          const i1 = document.getElementById('i1') || document.body;
          i1.appendChild(container);
        }

        container.addEventListener('scroll', scheduleScrollUpdate, { passive: true });

        container.addEventListener('pointerover', (e) => {
          const item = e.target.closest('.eh-strip-item');
          if (item && item.dataset.page) {
            const page = parseInt(item.dataset.page, 10);
            if (page && isPageEligibleToLoad(page)) {
              checkAndLoadNearbyPages(page);
            }
          }
        }, { passive: true });
      }
      return container;
    }

    function ensureStartSpacer() {
      const container = getOrCreateStripContainer();
      let spacer = document.getElementById('eh-strip-start-spacer');
      if (!spacer) {
        spacer = document.createElement('div');
        spacer.id = 'eh-strip-start-spacer';
        spacer.className = 'eh-strip-start-spacer';
        if (container.firstChild) {
          container.insertBefore(spacer, container.firstChild);
        } else {
          container.appendChild(spacer);
        }
      } else if (spacer !== container.firstChild) {
        container.insertBefore(spacer, container.firstChild);
      }
      return spacer;
    }

    function ensureBottomGalleryButton() {
      if (!stripActive) return;
      const url = getGalleryUrl();

      if (currentLayout === 'vertical') {
        const existingEndCard = document.getElementById('eh-strip-end-card');
        if (existingEndCard) existingEndCard.remove();

        let bar = document.getElementById('eh-bottom-gallery-bar');
        if (!bar) {
          bar = document.createElement('div');
          bar.id = 'eh-bottom-gallery-bar';
          bar.className = 'eh-bottom-gallery-bar';
          bar.innerHTML = `<a href="${url}">← Return to Gallery</a>`;
          const container = getOrCreateStripContainer();
          if (container.parentNode) {
            container.parentNode.insertBefore(bar, container.nextSibling);
          }
        } else {
          const link = bar.querySelector('a');
          if (link) link.href = url;
        }
      } else {
        const existingBar = document.getElementById('eh-bottom-gallery-bar');
        if (existingBar) existingBar.remove();

        const container = getOrCreateStripContainer();
        let endCard = document.getElementById('eh-strip-end-card');
        if (!endCard) {
          endCard = document.createElement('div');
          endCard.id = 'eh-strip-end-card';
          endCard.className = 'eh-strip-item eh-strip-end-card';
          endCard.innerHTML = `<div class="eh-bottom-gallery-bar"><a href="${url}">← Return to Gallery</a></div>`;
          container.appendChild(endCard);
        } else {
          const link = endCard.querySelector('a');
          if (link) link.href = url;
          if (endCard.nextSibling) {
            container.appendChild(endCard);
          }
        }
      }
    }

    function applyItemDimensions(item, pageNo) {
      const dim = pageDimensions.get(pageNo);
      const ar = dim ? dim.aspectRatio : defaultAspectRatio;
      item.style.setProperty('--eh-ar', String(ar));
      item.style.aspectRatio = String(ar);

      if (currentLayout === 'horizontal') {
        item.style.width = `calc(100vh * ${ar})`;
        item.style.maxWidth = '';
      } else {
        item.style.width = '100%';
        if (dim && dim.width && dim.width > 500) {
          item.style.setProperty('--eh-max-w', dim.width + 'px');
        } else {
          item.style.setProperty('--eh-max-w', '100%');
        }
      }
    }

function renderPlaceholderContent(item, pageNo, retryCount = 0) {
      item.classList.add('eh-placeholder');
      if (loadingPages.has(pageNo)) {
        item.classList.add('eh-loading');
      } else {
        item.classList.remove('eh-loading');
      }

      const oldContent = item.querySelector('.eh-placeholder-content');
      if (oldContent) oldContent.remove();

      applyThumbnailToPlaceholder(item, pageNo);

      // Only show the spinner & status text if the image is having trouble loading
      const failInfo = failedPages.get(pageNo);
      const isTroubled = (retryCount > 0) || (failInfo && failInfo.count > 0);

      if (isTroubled) {
        if (failInfo && failInfo.count >= MAX_RETRIES) {
          showRetryUI(item, pageNo);
        } else {
          const content = document.createElement('div');
          content.className = 'eh-placeholder-content';

          const spinner = document.createElement('div');
          spinner.className = 'eh-spinner';

          const text = document.createElement('div');
          text.className = 'eh-placeholder-text';
          text.textContent = `Retrying Page ${pageNo}...`;

          content.appendChild(spinner);
          content.appendChild(text);
          item.appendChild(content);
        }
      }
    }

    function ensureAllPlaceholders(tot) {
      if (!isReaderPage || !stripActive || !tot || tot <= 0) return;
      const container = getOrCreateStripContainer();
      let added = false;

      withScrollAnchor(() => {
        ensureStartSpacer();
        const obs = getStripItemObserver();
        const endCard = document.getElementById('eh-strip-end-card');

        let batchFrag = null;
        let batchNextEl = null;

        const flushBatch = () => {
          if (batchFrag && batchFrag.childNodes.length > 0) {
            if (batchNextEl && batchNextEl.isConnected) {
              container.insertBefore(batchFrag, batchNextEl);
            } else if (endCard && endCard.isConnected) {
              container.insertBefore(batchFrag, endCard);
            } else {
              container.appendChild(batchFrag);
            }
            batchFrag = null;
            batchNextEl = null;
          }
        };

        for (let p = 1; p <= tot; p++) {
          if (pageItemMap.has(p)) {
            flushBatch();
            continue;
          }

          const item = document.createElement('div');
          item.className = 'eh-strip-item eh-placeholder';
          item.dataset.page = String(p);
          applyItemDimensions(item, p);
          renderPlaceholderContent(item, p);
          pageItemMap.set(p, item);

          if (obs) obs.observe(item);

          if (!batchFrag) {
            batchFrag = document.createDocumentFragment();
            let nextEl = null;
            for (let nextP = p + 1; nextP <= tot; nextP++) {
              if (pageItemMap.has(nextP)) {
                const el = pageItemMap.get(nextP);
                if (el && el.isConnected) {
                  nextEl = el;
                  break;
                }
              }
            }
            batchNextEl = nextEl;
          }

          batchFrag.appendChild(item);
          added = true;
        }

        flushBatch();
      });

      if (added) {
        syncPageIndicator(currentPageNum);
      }
    }

    function createOrGetPlaceholder(pageNo) {
      if (!isReaderPage) return null;
      const container = getOrCreateStripContainer();
      if (!container) return null;
      let item = pageItemMap.get(pageNo);
      if (item) return item;

      item = document.createElement('div');
      item.className = 'eh-strip-item eh-placeholder';
      item.dataset.page = String(pageNo);

      applyItemDimensions(item, pageNo);
      renderPlaceholderContent(item, pageNo);
      pageItemMap.set(pageNo, item);

      const obs = getStripItemObserver();
      if (obs) obs.observe(item);

      withScrollAnchor(() => {
        ensureStartSpacer();
        const existing = container.querySelectorAll('.eh-strip-item:not(.eh-strip-end-card)');
        let inserted = false;
        for (const el of existing) {
          const p = parseInt(el.dataset.page, 10);
          if (p > pageNo) {
            container.insertBefore(item, el);
            inserted = true;
            break;
          }
        }
        if (!inserted) {
          const endCard = document.getElementById('eh-strip-end-card');
          if (endCard) {
            container.insertBefore(item, endCard);
          } else {
            container.appendChild(item);
          }
        }
      });

      return item;
    }

    function renderImageInItem(pageNo, imgSrc, retryCount = 0) {
      let item = pageItemMap.get(pageNo);
      if (!item) {
        item = createOrGetPlaceholder(pageNo);
      }

      const existingImg = item.querySelector('img.eh-main-img');
      if (existingImg && existingImg.src === imgSrc && loadedImages.has(pageNo)) {
        loadingPages.delete(pageNo);
        setTimeout(() => { if (stripActive) checkAndLoadNearbyPages(); }, 10);
        return;
      }

      const img = document.createElement('img');
      img.alt = `Page ${pageNo}`;
      img.className = 'eh-main-img';
      img.decoding = 'async';
      const startTime = Date.now();

      const dim = pageDimensions.get(pageNo);
      if (dim && dim.width && dim.height && dim.width > 500) {
        img.width = dim.width;
        img.height = dim.height;
      }

      item._currentImgToken = img;

      let slowTimerId = null;
      let hardTimerId = null;
      let isSettled = false;

      const cleanupTimers = () => {
        if (slowTimerId) { clearTimeout(slowTimerId); slowTimerId = null; }
        if (hardTimerId) { clearTimeout(hardTimerId); hardTimerId = null; }
      };

      const cleanupImg = () => {
        cleanupTimers();
        img.onload = null;
        img.onerror = null;
      };

      const abortImg = () => {
        isSettled = true;
        cleanupImg();
        img.removeAttribute('src');
        img.src = '';
      };
      item._abortImg = abortImg;

      const handleFailure = (reason, isSlowStall = false) => {
        if (isSettled) return;
        isSettled = true;
        item._abortImg = null;
        cleanupImg();

        // Release socket connection immediately
        img.removeAttribute('src');
        img.src = '';

        loadingPages.delete(pageNo);
        if (item) item.classList.remove('eh-loading');
        setTimeout(() => { if (stripActive) checkAndLoadNearbyPages(); }, 10);

        if (item._currentImgToken !== img) return;
        console.warn(`ExFastReader: Image for page ${pageNo} ${reason}, attempt ${retryCount + 1}/${MAX_RETRIES}`);

        if (retryCount < MAX_RETRIES) {
          loadingPages.set(pageNo, Date.now());
          const delay = isSlowStall ? 100 : Math.min(2000, 400 * Math.pow(1.5, retryCount));
          setTimeout(() => {
            if (!stripActive) {
              loadingPages.delete(pageNo);
              return;
            }
            reloadPageImage(pageNo, retryCount + 1);
          }, delay);
        } else {
          loadingPages.delete(pageNo);
          loadedImages.delete(pageNo);
          recordPageFailure(pageNo, reason);
          showRetryUI(item, pageNo);
          setTimeout(() => { if (stripActive) checkAndLoadNearbyPages(); }, 10);
        }
      };

      img.onload = async () => {
        if (!stripActive || isSettled) return;
        isSettled = true;
        item._abortImg = null;
        cleanupImg();

        loadingPages.delete(pageNo);
        if (item) item.classList.remove('eh-loading');
        setTimeout(() => { if (stripActive) checkAndLoadNearbyPages(); }, 10);

        if (item._currentImgToken !== img) return;

        try {
          if ('decode' in img) await img.decode();
        } catch {}

        if (item._currentImgToken !== img) return;

        if (img.naturalWidth > 0 && img.naturalHeight > 0) {
          updatePageDimensions(pageNo, img.naturalWidth, img.naturalHeight);
        }

        withScrollAnchor(() => {
          item.classList.remove('eh-placeholder');

          const content = item.querySelector('.eh-placeholder-content');
          if (content) content.remove();

          const thumbs = item.querySelectorAll('.eh-placeholder-thumb-img, .eh-placeholder-thumb-wrap');
          thumbs.forEach(t => {
            if (t.tagName === 'IMG') {
              t.onload = null;
              t.onerror = null;
              t.removeAttribute('src');
              t.src = '';
            } else {
              t.querySelectorAll('img').forEach(i => { i.onload = null; i.onerror = null; i.removeAttribute('src'); i.src = ''; });
            }
            t.remove();
          });

          const oldImg = item.querySelector('img.eh-main-img');
          if (oldImg && oldImg !== img) {
            oldImg.onload = null;
            oldImg.onerror = null;
            oldImg.removeAttribute('src');
            oldImg.src = '';
            oldImg.remove();
          }

          if (!item.contains(img)) {
            item.appendChild(img);
          }
        }, pageNo);

        loadedImages.add(pageNo);
        failedPages.delete(pageNo);

        const durationSec = Math.max(0.1, (Date.now() - startTime) / 1000);
        recordDownloadSpeedSample(pageNo, durationSec);

        if (currentPageNum === pageNo) {
          updateTopBarInfo(pageNo);
        }
      };

      img.onerror = () => {
        handleFailure('network load error', false);
      };

      // Dynamic stall timeouts based on image file size and measured download speed
      const { fileSize, speed, slowTimeoutMs, hardTimeoutMs } = calculateImageTimeout(pageNo);

      slowTimerId = setTimeout(() => {
        if (!img.complete || img.naturalWidth === 0) {
          console.warn(`ExFastReader: Page ${pageNo} (${(fileSize / 1024).toFixed(0)} KB) is exceptionally slow (> ${(slowTimeoutMs / 1000).toFixed(1)}s at ${(speed / (1024 * 1024)).toFixed(2)} MB/s). Triggering failover bounce.`);
          if (retryCount < MAX_RETRIES) {
            handleFailure(`exceptionally slow (> ${(slowTimeoutMs / 1000).toFixed(1)}s)`, true);
          }
        }
      }, slowTimeoutMs);

      // Hard stall timeout safeguard based on file size and download speed
      hardTimerId = setTimeout(() => {
        if (!img.complete || img.naturalWidth === 0) {
          handleFailure(`hard stalled (> ${(hardTimeoutMs / 1000).toFixed(1)}s)`, false);
        }
      }, hardTimeoutMs);

      img.src = imgSrc;
    }

    function showRetryUI(item, pageNo) {
      item.classList.add('eh-placeholder');
      applyThumbnailToPlaceholder(item, pageNo);

      const oldContent = item.querySelector('.eh-placeholder-content');
      if (oldContent) oldContent.remove();

      const content = document.createElement('div');
      content.className = 'eh-placeholder-content eh-placeholder-retry';
      content.style.cursor = 'pointer';
      content.innerHTML = `
        <div style="font-size: 20px;">⚠️</div>
        <div class="eh-placeholder-text">Page ${pageNo} failed to load.</div>
        <button type="button" class="eh-strip-btn active" style="margin-top: 4px;">Click to switch server & retry</button>
      `;
      content.onclick = (e) => {
        e.stopPropagation();
        content.onclick = null;
        failedPages.delete(pageNo);
        reloadPageImage(pageNo, 0);
      };
      item.appendChild(content);
    }

    async function reloadPageImage(pageNo, retryCount = 0) {
      const item = pageItemMap.get(pageNo);
      if (item && !item.querySelector('img.eh-main-img')) {
        renderPlaceholderContent(item, pageNo);
      }
      loadedImages.delete(pageNo);
      loadingPages.set(pageNo, Date.now()); // Mark loading to prevent duplicate concurrent load
      failedPages.delete(pageNo);
      pageImgSrcCache.delete(pageNo);
      pageRespCache.delete(pageNo);

      const nl = pageNlCodes.get(pageNo);
      const imgKey = pageImgKeyCache.get(pageNo);
      if (imgKey) {
        try {
          const freshResp = await fetchApi(pageNo, imgKey, nl || null, true);
          if (freshResp) {
            pageRespCache.set(pageNo, freshResp);
            extractKeysFromResp(freshResp, pageNo);
            const extracted = extractInfoFromResp(freshResp, pageNo);
            if (extracted) pageTopBarInfoCache.set(pageNo, extracted);
            if (currentPageNum === pageNo) updateTopBarInfo(pageNo);
            const newNl = extractNlCode(freshResp);
            if (newNl) {
              pageNlCodes.set(pageNo, newNl);
            }
            if (freshResp.i3) {
              const i3Doc = parseTemplate(freshResp.i3);
              const imgEl = i3Doc.querySelector('img#img') || i3Doc.querySelector('img');
              const freshSrc = imgEl ? (imgEl.getAttribute('src') || imgEl.src) : null;
              if (freshSrc) {
                pageImgSrcCache.set(pageNo, freshSrc);
                renderImageInItem(pageNo, freshSrc, retryCount);
                return;
              }
            }
          }
        } catch (err) {
          console.warn(`ExFastReader: Failed to reload page ${pageNo}:`, err);
        }
      }
      loadPage(pageNo, retryCount);
    }

    function isPageEligibleToLoad(pageNo) {
      if (!pageNo || isNaN(pageNo) || pageNo < 1) return false;
      const tot = getTotalPages();
      if (tot > 0 && pageNo > tot) return false;
      if (loadedImages.has(pageNo) || loadingPages.has(pageNo)) return false;
      const failInfo = failedPages.get(pageNo);
      if (failInfo) {
        if (failInfo.count >= 3) return false;
        const cooldown = Math.min(10000, 1000 * Math.pow(2, failInfo.count - 1));
        if (Date.now() - failInfo.lastAttempt < cooldown) {
          return false;
        }
      }
      return true;
    }

    function recordPageFailure(pageNo, reason) {
      const current = failedPages.get(pageNo) || { count: 0, lastAttempt: 0 };
      current.count += 1;
      current.lastAttempt = Date.now();
      failedPages.set(pageNo, current);
      console.warn(`ExFastReader: Page ${pageNo} failed (attempt ${current.count}): ${reason}`);

      if (current.count >= 3) {
        const item = pageItemMap.get(pageNo);
        if (item && !item.querySelector('img.eh-main-img')) {
          showRetryUI(item, pageNo);
        }
      }
    }

    function getVisiblePagesAndCurrent() {
      const vpW = window.innerWidth;
      const vpH = window.innerHeight;
      const vpCenterX = vpW / 2;
      const vpCenterY = vpH / 2;
      const isHoriz = currentLayout === 'horizontal';

      if (pageItemMap.size === 0) {
        return { visible: [currentPageNum || 1], current: currentPageNum || 1 };
      }

      // 1. Instant O(1) probe at viewport center
      let seedPage = null;
      const centerEl = document.elementFromPoint(vpCenterX, vpCenterY)?.closest('.eh-strip-item');
      if (centerEl && centerEl.dataset.page) {
        const p = parseInt(centerEl.dataset.page, 10);
        if (p && pageItemMap.has(p)) seedPage = p;
      }

      // 2. Fast check around currentPageNum or immediate neighbors
      if (!seedPage && currentPageNum && pageItemMap.has(currentPageNum)) {
        for (const testP of [currentPageNum, currentPageNum - 1, currentPageNum + 1]) {
          const item = pageItemMap.get(testP);
          if (item && item.isConnected) {
            const r = item.getBoundingClientRect();
            const inView = isHoriz ? (r.right > 0 && r.left < vpW) : (r.bottom > 0 && r.top < vpH);
            if (inView) {
              seedPage = testP;
              break;
            }
          }
        }
      }

      // 3. Fallback: monotonic binary search over pageItemMap keys (log2(N) <= 9 probes for 500 items)
      if (!seedPage) {
        const pages = Array.from(pageItemMap.keys()).sort((a, b) => a - b);
        let low = 0, high = pages.length - 1;
        while (low <= high) {
          const mid = Math.floor((low + high) / 2);
          const p = pages[mid];
          const item = pageItemMap.get(p);
          if (!item || !item.isConnected) {
            low = mid + 1;
            continue;
          }
          const r = item.getBoundingClientRect();
          if (isHoriz) {
            // In RTL flex-row: earlier pages (page 1) are on the right (higher X).
            if (r.left >= vpW) {
              low = mid + 1;
            } else if (r.right <= 0) {
              high = mid - 1;
            } else {
              seedPage = p;
              break;
            }
          } else {
            if (r.bottom <= 0) {
              low = mid + 1;
            } else if (r.top >= vpH) {
              high = mid - 1;
            } else {
              seedPage = p;
              break;
            }
          }
        }
      }

      if (!seedPage) {
        seedPage = currentPageNum || 1;
      }

      const visible = [];
      let bestPage = seedPage;
      let minCenterDist = Infinity;
      let centerCoveredPage = null;

      function testPage(p) {
        const item = pageItemMap.get(p);
        if (!item || !item.isConnected) return false;
        const r = item.getBoundingClientRect();
        if (isHoriz) {
          if (r.right > 0 && r.left < vpW) {
            visible.push(p);
            if (r.left <= vpCenterX && r.right >= vpCenterX) centerCoveredPage = p;
            const dist = Math.abs((r.left + r.right) / 2 - vpCenterX);
            if (dist < minCenterDist) {
              minCenterDist = dist;
              bestPage = p;
            }
            return true;
          }
        } else {
          if (r.bottom > 0 && r.top < vpH) {
            visible.push(p);
            if (r.top <= vpCenterY && r.bottom >= vpCenterY) centerCoveredPage = p;
            const dist = Math.abs((r.top + r.bottom) / 2 - vpCenterY);
            if (dist < minCenterDist) {
              minCenterDist = dist;
              bestPage = p;
            }
            return true;
          }
        }
        return false;
      }

      testPage(seedPage);

      // Expand outward backwards
      for (let p = seedPage - 1; p >= 1; p--) {
        if (!testPage(p)) break;
      }
      // Expand outward forwards
      const total = getTotalPages() || (seedPage + 100);
      for (let p = seedPage + 1; p <= total; p++) {
        if (!testPage(p)) break;
      }

      if (visible.length === 0) {
        visible.push(bestPage);
      }

      visible.sort((a, b) => a - b);
      const current = centerCoveredPage !== null ? centerCoveredPage : bestPage;
      return { visible, current };
    }

    function getVisiblePageNums() {
      return getVisiblePagesAndCurrent().visible;
    }

    function getCurrentlyVisiblePageNum() {
      return getVisiblePagesAndCurrent().current;
    }

    function checkAndLoadNearbyPages(targetPage, precomputedVisible = null) {
      if (!isReaderPage || !stripActive || isPumpingQueue) return;
      isPumpingQueue = true;

      try {
        const cur = targetPage || currentPageNum;
        const total = getTotalPages();
        prefetchNearbyThumbnails(cur);

        const candidates = [];
        const visiblePages = precomputedVisible || getVisiblePageNums();

        const sortedVisible = scrollDirection === 'backward' ? [...visiblePages].reverse() : visiblePages;
        for (const vp of sortedVisible) {
          if (!candidates.includes(vp)) candidates.push(vp);
        }
        if (!candidates.includes(cur)) candidates.push(cur);

        if (scrollDirection === 'backward') {
          const backDepth = PREFETCH_DEPTH + 1;
          for (let b = 1; b <= backDepth; b++) {
            const p = cur - b;
            if (p >= 1 && (!total || p <= total) && !candidates.includes(p)) {
              candidates.push(p);
            }
          }
          for (let f = 1; f <= 2; f++) {
            const p = cur + f;
            if (p >= 1 && (!total || p <= total) && !candidates.includes(p)) {
              candidates.push(p);
            }
          }
        } else {
          const forwardDepth = currentLayout === 'horizontal' ? (PREFETCH_DEPTH + 1) : PREFETCH_DEPTH;
          for (let f = 1; f <= forwardDepth; f++) {
            const p = cur + f;
            if (p >= 1 && (!total || p <= total) && !candidates.includes(p)) {
              candidates.push(p);
            }
          }
          for (let b = 1; b <= 2; b++) {
            const p = cur - b;
            if (p >= 1 && (!total || p <= total) && !candidates.includes(p)) {
              candidates.push(p);
            }
          }
        }

        for (const pageNo of candidates) {
          const isVisible = visiblePages.includes(pageNo) || pageNo === cur;
          const limit = isVisible ? (MAX_CONCURRENT_LOADS + 2) : MAX_CONCURRENT_LOADS;
          if (loadingPages.size >= limit) {
            if (!isVisible) break;
          }
          if (isPageEligibleToLoad(pageNo)) {
            loadPage(pageNo, 0);
          }
        }
      } finally {
        isPumpingQueue = false;
      }
    }

    async function loadPage(pageNo, retryCount = 0) {
      if (!isReaderPage || !isPageEligibleToLoad(pageNo)) return;
      loadingPages.set(pageNo, Date.now());

      let renderStarted = false;
      const item = pageItemMap.get(pageNo);
      if (item) item.classList.add('eh-loading');
      try {
        let imgKey = pageImgKeyCache.get(pageNo);
        if (!imgKey) {
          imgKey = await ensureKeysForPage(pageNo);
        }
        if (!imgKey) {
          recordPageFailure(pageNo, 'Missing image key');
          return;
        }
        if (!stripActive) {
          return;
        }

        let imgSrc = pageImgSrcCache.get(pageNo);
        if (!imgSrc) {
            let resp = pageRespCache.get(pageNo);
            if (!resp) {
                resp = await fetchApi(pageNo, imgKey);
                if (resp && !resp.error) {
                    pageRespCache.set(pageNo, resp);
                }
            }

            if (!resp) {
                recordPageFailure(pageNo, 'API request timed out or network error');
                return;
            }
            if (resp.error) {
                recordPageFailure(pageNo, `API error: ${resp.error}`);
                return;
            }
            if (!resp.i3) {
                recordPageFailure(pageNo, 'API response missing i3 image container');
                return;
            }
            if (!resp || resp.error) {
                // If throttled or errored, retry after a short delay
                if (retryCount < MAX_RETRIES) {
                    const backoff = 1000 * (retryCount + 1);
                    setTimeout(() => {
                        if (stripActive) reloadPageImage(pageNo, retryCount + 1);
                    }, backoff);
                    return;
                }
            }
          if (!stripActive) {
            return;
          }

          const newNl = extractNlCode(resp);
          if (newNl) {
            pageNlCodes.set(pageNo, newNl);
          }

          const i3Doc = parseTemplate(resp.i3);
          const imgEl = i3Doc.querySelector('img#img');
          imgSrc = imgEl ? (imgEl.getAttribute('src') || imgEl.src) : null;
          if (imgSrc) {
            pageImgSrcCache.set(pageNo, imgSrc);
          }
        }

        if (imgSrc) {
          renderStarted = true;
          renderImageInItem(pageNo, imgSrc, retryCount);
        } else {
          recordPageFailure(pageNo, 'Image element missing src');
        }
      } catch (err) {
        recordPageFailure(pageNo, err.message || 'Exception during load');
      } finally {
        if (!renderStarted) {
          loadingPages.delete(pageNo);
          const it = pageItemMap.get(pageNo);
          if (it) it.classList.remove('eh-loading');
        }
        setTimeout(() => {
          if (stripActive) checkAndLoadNearbyPages();
        }, 50);
      }
    }

    function scheduleScrollUpdate() {
      if (!isReaderPage || !stripActive || scrollRafId || isTransitioningLayout) return;
      scrollRafId = requestAnimationFrame(() => {
        scrollRafId = null;
        onScrollThrottled();
      });
    }

    function onScrollThrottled() {
      if (!isReaderPage || !stripActive || isTransitioningLayout) return;

      if (!animScrollRaf) targetScrollLeft = null;
      if (!animScrollTopRaf) targetScrollTop = null;

      const container = document.getElementById('eh-strip-container');
      if (currentLayout === 'horizontal' && container) {
        const curLeft = container.scrollLeft;
        if (curLeft > lastScrollLeft + 2) {
          scrollDirection = 'backward';
        } else if (curLeft < lastScrollLeft - 2) {
          scrollDirection = 'forward';
        }
        lastScrollLeft = curLeft;
      } else {
        const curY = window.scrollY;
        if (curY < lastScrollY - 2) {
          scrollDirection = 'backward';
        } else if (curY > lastScrollY + 2) {
          scrollDirection = 'forward';
        }
        lastScrollY = curY;
      }

      const { visible: visiblePages, current: activePage } = getVisiblePagesAndCurrent();
      if (activePage < currentPageNum) {
        scrollDirection = 'backward';
      } else if (activePage > currentPageNum) {
        scrollDirection = 'forward';
      }

      currentPageNum = activePage;
      syncPageIndicator(activePage);
      updateTopBarInfo(activePage);

      if (activePage !== lastUpdatePage) {
        lastUpdatePage = activePage;
        pruneCaches(activePage);
      }

      checkAndLoadNearbyPages(activePage, visiblePages);
      prefetchNearbyThumbnails(activePage);

      if (activePage !== lastHistoryPage) {
        lastHistoryPage = activePage;
        if (historyDebounceTimer) clearTimeout(historyDebounceTimer);
        historyDebounceTimer = setTimeout(() => {
          if (pageRespCache.has(activePage)) {
            const resp = pageRespCache.get(activePage);
            const targetUrl = (window.base_url || '/') + resp.s;
            if (location.pathname !== '/' + resp.s) {
              window.history.replaceState({ page: activePage }, document.title, targetUrl);
            }
          }
        }, 300);
      }
    }

    function syncPageIndicator(activePage, force = false) {
      if (!force && activePage === lastSyncedIndicatorPage) return;
      lastSyncedIndicatorPage = activePage;

      const total = getTotalPages();
      const pageDisplay = document.getElementById('eh-strip-page-display');
      if (pageDisplay) {
        pageDisplay.textContent = total ? `Page ${activePage} / ${total}` : `Page ${activePage}`;
      }

      const curSpan = document.querySelector('#i2 .sn span:first-child');
      if (curSpan && curSpan.textContent !== String(activePage)) {
        curSpan.textContent = activePage;
      }

      const galUrl = getGalleryUrl(activePage);
      const galleryBtn = document.getElementById('eh-strip-gallery-btn');
      if (galleryBtn) {
        galleryBtn.href = galUrl;
      }

      const bottomLinks = document.querySelectorAll('#eh-bottom-gallery-bar a, #eh-strip-end-card a');
      for (const a of bottomLinks) {
        a.href = galUrl;
      }
    }

    function enableStripMode() {
      stripActive = true;
      safeStorageSet(localStorage, 'eh_strip_mode_enabled', '1');

      setStripClasses(true, currentLayout);

      updateToggleButtonUI();

      const container = getOrCreateStripContainer();
      container.className = `eh-strip-container layout-${currentLayout}`;

      const initialMeta = getCurrentPageMeta();
      currentPageNum = initialMeta.page;
      if (initialMeta.imgKey) {
        pageImgKeyCache.set(currentPageNum, initialMeta.imgKey);
      }

      detectInitialPageDimensions();
      applyGlobalAspectRatio(defaultAspectRatio);
      extractKeysFromDoc(document);
      const initNl = extractInitialNlCode(document);
      if (initNl) {
        pageNlCodes.set(currentPageNum, initNl);
      }
      extractAndHideBottomLinks();

      ensureStartSpacer();

      const totalPages = getTotalPages();
      if (totalPages > 0) {
        const frag = document.createDocumentFragment();
        for (let p = 1; p <= totalPages; p++) {
          if (!pageItemMap.has(p)) {
            const item = document.createElement('div');
            item.className = 'eh-strip-item eh-placeholder';
            item.dataset.page = String(p);
            applyItemDimensions(item, p);
            renderPlaceholderContent(item, p);
            pageItemMap.set(p, item);

            frag.appendChild(item);
          }
        }
        const endCard = document.getElementById('eh-strip-end-card');
        if (endCard) {
          container.insertBefore(frag, endCard);
        } else {
          container.appendChild(frag);
        }
      } else {
        createOrGetPlaceholder(currentPageNum);
      }

      const currentImg = document.getElementById('img');
      if (currentImg && currentImg.src) {
        pageImgSrcCache.set(currentPageNum, currentImg.src);
        if (!loadedImages.has(currentPageNum)) {
          renderImageInItem(currentPageNum, currentImg.src, 0);
        }
      }

      ensureBottomGalleryButton();

      discoverAndFetchGallery(currentPageNum);

      isTransitioningLayout = true;
      instantScrollToPage(currentPageNum);

      const obs = getStripItemObserver();
      if (obs) {
        for (const it of pageItemMap.values()) {
          obs.observe(it);
        }
      }

      isTransitioningLayout = false;
      scheduleScrollUpdate();
      checkAndLoadNearbyPages(currentPageNum);

      lastUpdatePage = currentPageNum;
      syncPageIndicator(currentPageNum);
      updateTopBarInfo(currentPageNum);
    }

    function disableStripMode() {
      stripActive = false;
      cancelKeyboardAnimation();
      safeStorageSet(localStorage, 'eh_strip_mode_enabled', '0');

      if (historyDebounceTimer) {
        clearTimeout(historyDebounceTimer);
        historyDebounceTimer = null;
      }
      if (scrollRafId) {
        cancelAnimationFrame(scrollRafId);
        scrollRafId = null;
      }

      if (stripItemObserver) {
        stripItemObserver.disconnect();
        stripItemObserver = null;
      }

      setStripClasses(false, currentLayout);

      updateToggleButtonUI();

      for (const item of pageItemMap.values()) {
        if (typeof item._abortImg === 'function') {
          item._abortImg();
          item._abortImg = null;
        }
        item._currentImgToken = null;
        const allImgs = item.querySelectorAll('img');
        allImgs.forEach(img => {
          img.onload = null;
          img.onerror = null;
          img.removeAttribute('src');
          img.src = '';
          img.remove();
        });
        const retryContents = item.querySelectorAll('.eh-placeholder-retry, .eh-strip-btn');
        retryContents.forEach(el => { el.onclick = null; });
      }

      for (const [page, record] of preloadedImages.entries()) {
        if (record && record.img) {
          record.img.onload = null;
          record.img.onerror = null;
          record.img.removeAttribute('src');
          record.img.src = '';
        }
      }
      preloadedImages.clear();

      const container = document.getElementById('eh-strip-container');
      if (container) {
        container.removeEventListener('scroll', scheduleScrollUpdate);
        container.remove();
      }

      const bottomBar = document.getElementById('eh-bottom-gallery-bar');
      if (bottomBar) bottomBar.remove();

      const startSpacer = document.getElementById('eh-strip-start-spacer');
      if (startSpacer) startSpacer.remove();

      pageItemMap.clear();
      loadedImages.clear();
      loadingPages.clear();
      failedPages.clear();
      inFlightFetches.clear();
      inFlightGalleryFetches.clear();

      if (pageRespCache.has(currentPageNum)) {
        const resp = pageRespCache.get(currentPageNum);
        if (typeof window.apply_json_state === 'function') {
          window.apply_json_state(resp);
        }
      } else if (pageImgKeyCache.has(currentPageNum) && typeof window.load_image === 'function') {
        window.load_image(currentPageNum, pageImgKeyCache.get(currentPageNum));
      }

      initImageMonitor();
      startPrefetch();
    }

    function toggleStripMode() {
      if (stripActive) {
        disableStripMode();
      } else {
        enableStripMode();
      }
    }

    function toggleStripLayout() {
      const targetPage = currentPageNum || getCurrentlyVisiblePageNum() || 1;
      cancelKeyboardAnimation();
      isTransitioningLayout = true;

      currentLayout = currentLayout === 'horizontal' ? 'vertical' : 'horizontal';
      safeStorageSet(localStorage, 'eh_strip_layout', currentLayout);

      setStripClasses(true, currentLayout);

      const container = document.getElementById('eh-strip-container');
      if (container) {
        container.className = `eh-strip-container layout-${currentLayout}`;
        if (currentLayout === 'vertical') {
          container.scrollLeft = 0;
        }
        for (const [p, item] of pageItemMap.entries()) {
          applyItemDimensions(item, p);
        }
      }

      if (stripItemObserver) {
        stripItemObserver.disconnect();
        stripItemObserver = null;
      }
      const obs = getStripItemObserver();
      if (obs) {
        for (const item of pageItemMap.values()) {
          obs.observe(item);
        }
      }

      ensureBottomGalleryButton();
      updateToggleButtonUI();

      currentPageNum = targetPage;
      lastUpdatePage = targetPage;
      syncPageIndicator(targetPage);
      updateTopBarInfo(targetPage);

      if (container) void container.offsetHeight;
      instantScrollToPage(targetPage);

      requestAnimationFrame(() => {
        instantScrollToPage(targetPage);
        setTimeout(() => {
          instantScrollToPage(targetPage);
          currentPageNum = targetPage;
          lastUpdatePage = targetPage;
          syncPageIndicator(targetPage);
          isTransitioningLayout = false;
          scheduleScrollUpdate();
          checkAndLoadNearbyPages(targetPage);
        }, 60);
      });
    }

    function getSingleModeLowerArea() {
      const i5 = document.getElementById('i5');
      const i6 = document.getElementById('i6');
      const i7 = document.getElementById('i7');
      const loadfail = document.getElementById('loadfail');
      const loadfailParent = loadfail ? loadfail.closest('.idi, div, p') : null;

      let textMatch = null;
      const candidates = document.querySelectorAll('#i1 > div, #i1 > p, div.idi');
      for (const el of candidates) {
        const txt = (el.textContent || '').toLowerCase();
        if (txt.includes('download original') || txt.includes('reload broken image') || txt.includes('show galleries')) {
          textMatch = el;
          break;
        }
      }
      return i5 || i6 || i7 || loadfailParent || textMatch;
    }

function placeStripControls() {
      let controls = document.getElementById('eh-strip-controls');
      if (!controls) return;

      if (stripActive) {
        controls.classList.remove('single-mode-bottom');
        const i2 = document.getElementById('i2');
        if (i2 && controls.parentNode !== i2) {
          const topbarInfo = document.getElementById('eh-topbar-info');
          if (topbarInfo) {
            i2.insertBefore(controls, topbarInfo.nextSibling);
          } else {
            i2.appendChild(controls);
          }
        }
      } else {
        controls.classList.add('single-mode-bottom');

        // Locate the 30x30px icon container that links back to the gallery (#i5 / div.sb)
        const galleryIcon = document.getElementById('i5') ||
                            document.querySelector('div.sb') ||
                            document.querySelector('#i1 a[href*="/g/"] img')?.closest('div, p');

        if (galleryIcon && galleryIcon.parentNode) {
          // Insert directly underneath (after) the 30x30px gallery icon
          if (galleryIcon.nextElementSibling !== controls || controls.parentNode !== galleryIcon.parentNode) {
            galleryIcon.insertAdjacentElement('afterend', controls);
          }
        } else {
          // Fallback placement if the gallery icon container isn't found
          const lowerArea = getSingleModeLowerArea();
          if (lowerArea && lowerArea.parentNode) {
            if (controls.nextSibling !== lowerArea || controls.parentNode !== lowerArea.parentNode) {
              lowerArea.parentNode.insertBefore(controls, lowerArea);
            }
          } else {
            const i4 = document.getElementById('i4');
            if (i4 && i4.parentNode) {
              if (i4.nextElementSibling !== controls || controls.parentNode !== i4.parentNode) {
                i4.insertAdjacentElement('afterend', controls);
              }
            } else {
              const i1 = document.getElementById('i1') || document.body;
              if (controls.parentNode !== i1) {
                i1.appendChild(controls);
              }
            }
          }
        }
      }
    }

    function updateToggleButtonUI() {
      const modeBtn = document.getElementById('eh-strip-mode-toggle');
      const statusText = document.getElementById('eh-strip-status');
      const dirBtn = document.getElementById('eh-strip-dir-toggle');
      const dirText = document.getElementById('eh-strip-dir-text');
      const pageDisplay = document.getElementById('eh-strip-page-display');
      const galleryBtn = document.getElementById('eh-strip-gallery-btn');

      placeStripControls();

      if (modeBtn && statusText) {
        if (stripActive) {
          modeBtn.classList.add('active');
          statusText.textContent = 'ON';
        } else {
          modeBtn.classList.remove('active');
          statusText.textContent = 'OFF';
        }
      }

      if (dirBtn && dirText) {
        dirBtn.style.display = stripActive ? 'inline-flex' : 'none';
        dirText.textContent = currentLayout === 'horizontal' ? 'Horizontal (RTL)' : 'Vertical';
      }

      if (pageDisplay) {
        pageDisplay.style.display = stripActive ? 'inline-block' : 'none';
      }

      if (galleryBtn) {
        galleryBtn.style.display = stripActive ? 'inline-flex' : 'none';
        galleryBtn.href = getGalleryUrl();
      }

      const frontBtn = document.getElementById('eh-strip-front-btn');
      const onionBtn = document.getElementById('eh-strip-onion-btn');

      if (frontBtn) {
        frontBtn.style.display = stripActive ? 'inline-flex' : 'none';
        frontBtn.href = cachedFrontUrl || (location.origin + '/');
      }

      if (onionBtn) {
        onionBtn.style.display = stripActive && cachedOnionUrl ? 'inline-flex' : 'none';
        if (cachedOnionUrl) onionBtn.href = cachedOnionUrl;
      }
    }

    function injectReaderToggle() {
      let controls = document.getElementById('eh-strip-controls');
      if (!controls) {
        controls = document.createElement('div');
        controls.id = 'eh-strip-controls';
        controls.className = 'eh-strip-controls';

        const modeBtn = document.createElement('button');
        modeBtn.type = 'button';
        modeBtn.id = 'eh-strip-mode-toggle';
        modeBtn.className = 'eh-strip-btn' + (stripActive ? ' active' : '');
        modeBtn.title = 'Toggle Seamless Strip Reading Mode';
        modeBtn.innerHTML = `Strip Mode: <span id="eh-strip-status">${stripActive ? 'ON' : 'OFF'}</span>`;
        modeBtn.addEventListener('click', toggleStripMode);

        const dirBtn = document.createElement('button');
        dirBtn.type = 'button';
        dirBtn.id = 'eh-strip-dir-toggle';
        dirBtn.className = 'eh-strip-btn';
        dirBtn.title = 'Switch Orientation (Horizontal RTL / Vertical)';
        dirBtn.style.display = stripActive ? 'inline-flex' : 'none';
        dirBtn.innerHTML = `<span id="eh-strip-dir-text">${currentLayout === 'horizontal' ? 'Horizontal (RTL)' : 'Vertical'}</span>`;
        dirBtn.addEventListener('click', toggleStripLayout);

        const galleryBtn = document.createElement('a');
        galleryBtn.id = 'eh-strip-gallery-btn';
        galleryBtn.className = 'eh-strip-btn';
        galleryBtn.title = 'Return to Gallery';
        galleryBtn.href = getGalleryUrl();
        galleryBtn.textContent = 'Gallery';
        galleryBtn.style.display = stripActive ? 'inline-flex' : 'none';

        const frontBtn = document.createElement('a');
        frontBtn.id = 'eh-strip-front-btn';
        frontBtn.className = 'eh-strip-btn';
        frontBtn.title = 'Front Page';
        frontBtn.href = cachedFrontUrl || (location.origin + '/');
        frontBtn.textContent = 'Front Page';
        frontBtn.style.display = stripActive ? 'inline-flex' : 'none';

        const onionBtn = document.createElement('a');
        onionBtn.id = 'eh-strip-onion-btn';
        onionBtn.className = 'eh-strip-btn';
        onionBtn.title = 'Tor / Onion Mirror';
        onionBtn.href = cachedOnionUrl || '#';
        onionBtn.textContent = 'Tor / Onion';
        onionBtn.target = '_blank';
        onionBtn.rel = 'noopener noreferrer';
        onionBtn.style.display = stripActive && cachedOnionUrl ? 'inline-flex' : 'none';

        const pageDisplay = document.createElement('span');
        pageDisplay.id = 'eh-strip-page-display';
        pageDisplay.className = 'eh-strip-page-counter';
        pageDisplay.style.display = stripActive ? 'inline-block' : 'none';

        controls.appendChild(modeBtn);
        controls.appendChild(dirBtn);
        controls.appendChild(galleryBtn);
        controls.appendChild(frontBtn);
        controls.appendChild(onionBtn);
        controls.appendChild(pageDisplay);

        const i2 = document.getElementById('i2');
        if (i2) {
          i2.appendChild(controls);
        } else {
          document.body.appendChild(controls);
        }
      }

      placeStripControls();
      updateToggleButtonUI();
      syncPageIndicator(getCurrentlyVisiblePageNum());
    }

    window.addEventListener(
      'mousemove',
      (e) => {
        if (!isReaderPage || !stripActive) return;

        const i2 = document.getElementById('i2');
        const bottom = i2 ? Math.max(50, i2.getBoundingClientRect().bottom) : 50;

        if (e.clientY <= bottom) {
          document.body.classList.add('eh-topbar-visible');
        } else {
          document.body.classList.remove('eh-topbar-visible');
        }
      },
      { passive: true }
    );

    document.addEventListener('mouseleave', () => {
      if (!isReaderPage || !stripActive) return;
      document.body.classList.remove('eh-topbar-visible');
      if (document.activeElement && document.activeElement.blur) {
        document.activeElement.blur();
      }
    });

    window.addEventListener('blur', () => {
      if (!isReaderPage || !stripActive) return;
      document.body.classList.remove('eh-topbar-visible');
      if (document.activeElement && document.activeElement.blur) {
        document.activeElement.blur();
      }
    });

    document.addEventListener('click', (e) => {
      if (!isReaderPage || !stripActive) return;
      const i2 = document.getElementById('i2');
      if (i2 && i2.contains(e.target)) {
        const btn = e.target.closest('button, a');
        if (btn) {
          setTimeout(() => { if (btn.blur) btn.blur(); }, 50);
        }
      } else if (i2 && e.clientY > 50) {
        document.body.classList.remove('eh-topbar-visible');
      }
    }, { passive: true });

    window.addEventListener('scroll', (e) => {
      if (!isReaderPage || !stripActive) return;
      scheduleScrollUpdate();
    }, { passive: true });

    window.addEventListener(
      'wheel',
      (e) => {
        cancelKeyboardAnimation();
        if (!isReaderPage || !stripActive || isTransitioningLayout) return;
        const container = document.getElementById('eh-strip-container');
        if (!container) return;
        if (e.target && e.target.closest('#i2')) return;

        if (currentLayout === 'horizontal') {
          if (Math.abs(e.deltaY) >= Math.abs(e.deltaX)) {
            e.preventDefault();
            const rawDelta = e.deltaMode === 1 ? e.deltaY * 33 : (e.deltaMode === 2 ? e.deltaY * 100 : e.deltaY);
            const clampedDelta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 100);
            container.scrollLeft -= clampedDelta;
            scheduleScrollUpdate();
          }
        }
      },
      { passive: false }
    );

    let resizeTimer = null;
    function handleViewportResize() {
      if (!isReaderPage) return;
      isTransitioningLayout = true;
      cancelKeyboardAnimation();
      if (resizeTimer) clearTimeout(resizeTimer);
      resizeTimer = setTimeout(() => {
        instantScrollToPage(currentPageNum);
        isTransitioningLayout = false;
        scheduleScrollUpdate();
      }, 70);
    }

    window.addEventListener('resize', handleViewportResize, { passive: true });
    document.addEventListener('fullscreenchange', handleViewportResize, { passive: true });
    document.addEventListener('webkitfullscreenchange', handleViewportResize, { passive: true });

    function instantScrollToPage(targetPage) {
      if (!isReaderPage) return;
      const container = document.getElementById('eh-strip-container');
      if (!container) return;
      let item = pageItemMap.get(targetPage);
      if (!item) item = createOrGetPlaceholder(targetPage);
      if (!item) return;

      cancelKeyboardAnimation();

      if (currentLayout === 'horizontal') {
        window.scrollTo(0, 0);
        const itemRect = item.getBoundingClientRect();
        const contRect = container.getBoundingClientRect();
        const deltaX = ((itemRect.left + itemRect.right) / 2) - ((contRect.left + contRect.right) / 2);
        const maxScroll = 0;
        const minScroll = -(container.scrollWidth - container.clientWidth);

        let targetLeft = container.scrollLeft + deltaX;
        if (targetLeft > maxScroll) targetLeft = maxScroll;
        if (targetLeft < minScroll) targetLeft = minScroll;

        container.scrollLeft = targetLeft;
        lastScrollLeft = targetLeft;
      } else {
        if (container) container.scrollLeft = 0;
        const itemRect = item.getBoundingClientRect();
        const deltaY = ((itemRect.top + itemRect.bottom) / 2) - (window.innerHeight / 2);
        const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);

        let targetTop = window.scrollY + deltaY;
        if (targetTop < 0) targetTop = 0;
        if (targetTop > maxScroll) targetTop = maxScroll;

        window.scrollTo(0, targetTop);
        lastScrollY = targetTop;
      }
    }

    function scrollToPage(targetPage) {
      const total = getTotalPages();
      if (total > 0) {
        if (targetPage < 1) targetPage = 1;
        if (targetPage > total) targetPage = total;
      } else if (targetPage < 1) {
        targetPage = 1;
      }

      currentPageNum = targetPage;
      syncPageIndicator(targetPage);
      updateTopBarInfo(targetPage);
      checkAndLoadNearbyPages(targetPage);

      let item = pageItemMap.get(targetPage);
      if (!item) item = createOrGetPlaceholder(targetPage);
      if (!item) return;

      const container = document.getElementById('eh-strip-container');
      if (!container) return;

      cancelKeyboardAnimation();

      if (currentLayout === 'horizontal') {
        const itemRect = item.getBoundingClientRect();
        const contRect = container.getBoundingClientRect();
        const deltaX = ((itemRect.left + itemRect.right) / 2) - ((contRect.left + contRect.right) / 2);
        const maxScroll = 0;
        const minScroll = -(container.scrollWidth - container.clientWidth);

        let targetLeft = container.scrollLeft + deltaX;
        if (targetLeft > maxScroll) targetLeft = maxScroll;
        if (targetLeft < minScroll) targetLeft = minScroll;

        targetScrollLeft = targetLeft;
        animateHorizontalScroll(container);
      } else {
        const itemRect = item.getBoundingClientRect();
        const deltaY = ((itemRect.top + itemRect.bottom) / 2) - (window.innerHeight / 2);
        const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);

        let targetTop = window.scrollY + deltaY;
        if (targetTop < 0) targetTop = 0;
        if (targetTop > maxScroll) targetTop = maxScroll;

        targetScrollTop = targetTop;
        animateVerticalScroll();
      }
    }

    window.addEventListener(
      'keydown',
      (e) => {
        const tag = document.activeElement?.tagName?.toLowerCase();
        if (tag === 'input' || tag === 'textarea' || tag === 'select' || document.activeElement?.isContentEditable) {
          return;
        }

        if (e.ctrlKey || e.altKey || e.metaKey) {
          return;
        }

        if (isReaderPage) {
          if (stripActive) {
            const container = document.getElementById('eh-strip-container');
            if (!container) return;

            const isHoriz = currentLayout === 'horizontal';
            const step = 140;
            const pageStep = Math.round((isHoriz ? window.innerWidth : window.innerHeight) * 0.85);

            if (e.key === '[' || e.key === '{') {
              e.preventDefault();
              e.stopPropagation();
              e.stopImmediatePropagation();
              const now = Date.now();
              if (now - lastStripTurnTime < 180) return;
              lastStripTurnTime = now;
              scrollDirection = 'backward';
              scrollToPage(getCurrentlyVisiblePageNum() - 1);
              return;
            }
            if (e.key === ']' || e.key === '}') {
              e.preventDefault();
              e.stopPropagation();
              e.stopImmediatePropagation();
              const now = Date.now();
              if (now - lastStripTurnTime < 180) return;
              lastStripTurnTime = now;
              scrollDirection = 'forward';
              scrollToPage(getCurrentlyVisiblePageNum() + 1);
              return;
            }

            if (e.key === 'Home') {
              e.preventDefault();
              e.stopPropagation();
              e.stopImmediatePropagation();
              cancelKeyboardAnimation();
              if (isHoriz) {
                targetScrollLeft = 0;
                animateHorizontalScroll(container);
              } else {
                targetScrollTop = 0;
                animateVerticalScroll();
              }
              return;
            }
            if (e.key === 'End') {
              e.preventDefault();
              e.stopPropagation();
              e.stopImmediatePropagation();
              cancelKeyboardAnimation();
              if (isHoriz) {
                targetScrollLeft = -(container.scrollWidth - container.clientWidth);
                animateHorizontalScroll(container);
              } else {
                targetScrollTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
                animateVerticalScroll();
              }
              return;
            }

            if (e.key === 'r' || e.key === 'R') {
              e.preventDefault();
              const visPage = getCurrentlyVisiblePageNum();
              reloadPageImage(visPage, 0);
              return;
            }

            let delta = 0;

            if (
              e.key === 'ArrowDown' || e.key === 's' || e.key === 'S' ||
              e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A'
            ) {
              delta = step;
            } else if (
              e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W' ||
              e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D'
            ) {
              delta = -step;
            } else if (e.key === 'PageDown' || (e.key === ' ' && !e.shiftKey)) {
              delta = pageStep;
            } else if (e.key === 'PageUp' || (e.key === ' ' && e.shiftKey)) {
              delta = -pageStep;
            }

            if (delta !== 0) {
              e.preventDefault();
              e.stopPropagation();
              e.stopImmediatePropagation();
              scrollByEvenIncrement(delta);
            }
            return;
          }

          if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();

            const isRepeat = e.repeat;
            const minInterval = isRepeat ? 300 : 100;
            const now = Date.now();
            if (now - lastSingleTurnTime < minInterval) return;
            lastSingleTurnTime = now;

            const targetP = currentPageNum + 1;
            turnSinglePage(targetP, isRepeat);
          } else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();

            const isRepeat = e.repeat;
            const minInterval = isRepeat ? 300 : 100;
            const now = Date.now();
            if (now - lastSingleTurnTime < minInterval) return;
            lastSingleTurnTime = now;

            const targetP = currentPageNum - 1;
            turnSinglePage(targetP, isRepeat);
          } else if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            scrollByEvenIncrement(140);
          } else if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            scrollByEvenIncrement(-140);
          } else if (e.key === 'PageDown' || (e.key === ' ' && !e.shiftKey)) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            scrollByEvenIncrement(Math.round(window.innerHeight * 0.85));
          } else if (e.key === 'PageUp' || (e.key === ' ' && e.shiftKey)) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            scrollByEvenIncrement(-Math.round(window.innerHeight * 0.85));
          } else if (e.key === 'Home') {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            targetScrollTop = 0;
            animateVerticalScroll();
          } else if (e.key === 'End') {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            targetScrollTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
            animateVerticalScroll();
          } else if (e.key === 'r' || e.key === 'R') {
            e.preventDefault();
            executeBrokenReload();
          }
        } else if (isGalleryPage) {
          if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
            const nextThumb = document.querySelector('table.ptb td.ptds + td a, table.ptt td.ptds + td a') || document.querySelector('table.ptb td:last-child a');
            if (nextThumb && nextThumb.href && nextThumb.textContent.trim() !== '<') {
              e.preventDefault();
              e.stopPropagation();
              e.stopImmediatePropagation();
              if (typeof window.__eh_navigateGalleryPage === 'function') {
                window.__eh_navigateGalleryPage(nextThumb.href, true);
              } else {
                nextThumb.click();
              }
            }
          } else if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
            const curThumb = document.querySelector('table.ptb td.ptds, table.ptt td.ptds');
            const prevThumb = curThumb?.previousElementSibling?.querySelector('a');
            if (prevThumb && prevThumb.href) {
              e.preventDefault();
              e.stopPropagation();
              e.stopImmediatePropagation();
              if (typeof window.__eh_navigateGalleryPage === 'function') {
                window.__eh_navigateGalleryPage(prevThumb.href, true);
              } else {
                prevThumb.click();
              }
            }
          }
        }
      },
      true
    );

    window.addEventListener('popstate', (e) => {
      if (isGalleryPage) {
        if (typeof window.__eh_navigateGalleryPage === 'function') {
          window.__eh_navigateGalleryPage(location.href, false);
        }
        return;
      }
      const targetMeta = getCurrentPageMeta();
      const page = (e.state && e.state.page) || targetMeta.page;
      if (page && page !== currentPageNum) {
        if (stripActive) {
          scrollToPage(page);
        } else {
          onPageTurn(page);
        }
      }
    });

    let isSinglePageTurning = false;
    let pendingSinglePage = null;

    function applySinglePageResponse(resp, targetPage) {
      if (pendingSinglePage !== targetPage && currentPageNum !== targetPage) return;

      const extracted = extractInfoFromResp(resp, targetPage);
      if (extracted) pageTopBarInfoCache.set(targetPage, extracted);

      if (window.history && window.history.pushState && resp && resp.s) {
        const targetUrl = (window.base_url || '/') + resp.s;
        window.history.pushState({ page: resp.p }, document.title, targetUrl);
      }

      window.apply_json_state(resp);
      injectReaderToggle();
      onPageTurn(targetPage);
      isSinglePageTurning = false;
      pendingSinglePage = null;
    }

    async function turnSinglePage(targetPage, isKeyRepeat = false, passedKey = null) {
      if (stripActive) return;
      const total = getTotalPages();
      if (targetPage < 1 || (total > 0 && targetPage > total)) return;

      if (isKeyRepeat && isSinglePageTurning) {
        return;
      }

      isSinglePageTurning = true;
      pendingSinglePage = targetPage;
      currentPageNum = targetPage;
      window.currentpage = targetPage;
      if (passedKey) pageImgKeyCache.set(targetPage, passedKey);

      // Hide previous image immediately and mark obsolete so monitors don't resurrect it
      const oldImg = document.getElementById('img');
      if (oldImg && oldImg._eh_page !== targetPage) {
        oldImg.style.display = 'none';
        oldImg._eh_obsolete = true;
        oldImg.__eh_monitored = false;
      }

      showSinglePageThumbnailPlaceholder(targetPage);
      syncPageIndicator(targetPage);
      updateTopBarInfo(targetPage);

      if (pageRespCache.has(targetPage)) {
        const resp = pageRespCache.get(targetPage);
        applySinglePageResponse(resp, targetPage);
        return;
      }

      let key = passedKey || pageImgKeyCache.get(targetPage);
      if (!key) {
        key = await ensureKeysForPage(targetPage);
      }

      if (pendingSinglePage !== targetPage) {
        return;
      }

      if (key) {
        try {
          const resp = await fetchApi(targetPage, key, null, false, true);
          if (pendingSinglePage !== targetPage) return;

          if (resp && !resp.error) {
            pageRespCache.set(targetPage, resp);
            extractKeysFromResp(resp, targetPage);
            applySinglePageResponse(resp, targetPage);
          } else if (typeof window.origLoadImage === 'function') {
            window.origLoadImage(targetPage, key);
          }
        } finally {
          if (pendingSinglePage === targetPage) {
            isSinglePageTurning = false;
            pendingSinglePage = null;
          }
        }
      } else {
        const nextOrPrev = targetPage > (window.currentpage || 1) ? document.getElementById('next') : document.getElementById('prev');
        if (nextOrPrev) nextOrPrev.click();
        isSinglePageTurning = false;
        pendingSinglePage = null;
      }
    }

    function hookReader() {
      if (typeof window.load_image === 'function' && !window.load_image.__eh_hooked) {
        const origLoadImage = window.load_image;
        window.origLoadImage = origLoadImage;

        window.load_image = function (page, imgKey) {
          currentPageNum = page;
          window.currentpage = page;
          if (imgKey) window.currentkey = imgKey;

          if (!stripActive) {
            turnSinglePage(page, false, imgKey);
            return false;
          }

          if (pageRespCache.has(page)) {
            const resp = pageRespCache.get(page);

            if (window.history && window.history.pushState) {
              const targetUrl = (window.base_url || '/') + resp.s;
              window.history.pushState({ page: resp.p }, document.title, targetUrl);
            }

            window.apply_json_state(resp);
            injectReaderToggle();
            onPageTurn(page);
            return false;
          }

          return origLoadImage.apply(this, arguments);
        };
        window.load_image.__eh_hooked = true;
      }

      if (typeof window.apply_json_state === 'function' && !window.apply_json_state.__eh_hooked) {
        const origApply = window.apply_json_state;
        window.apply_json_state = function () {
          const ret = origApply.apply(this, arguments);
          const activeP = (arguments[0] && arguments[0].p) || currentPageNum;
          currentPageNum = activeP;
          window.currentpage = activeP;
          const resp = arguments[0];
          if (resp) {
            extractKeysFromResp(resp, activeP);
            const newNl = extractNlCode(resp);
            if (newNl) pageNlCodes.set(activeP, newNl);
          }

          if (!stripActive) {
            const newImg = document.getElementById('img');
            if (newImg) {
              newImg._eh_page = activeP;
              newImg._eh_obsolete = false;
              newImg.__eh_monitored = false;

              const isPreloaded = preloadedImages.has(activeP) && preloadedImages.get(activeP)?.isReady;
              const isImgComplete = newImg.complete && newImg.naturalWidth > 0;

              if (isPreloaded || isImgComplete) {
                newImg.style.display = 'block';
                hideSinglePageThumbnailPlaceholder();
              } else {
                newImg.style.display = 'none';
                showSinglePageThumbnailPlaceholder(activeP);

                const onImgLoaded = async () => {
                  newImg.removeEventListener('load', onImgLoaded);
                  newImg.removeEventListener('error', onImgError);
                  if (newImg._eh_page !== currentPageNum || newImg._eh_obsolete) return;
                  try {
                    if ('decode' in newImg) await newImg.decode();
                  } catch {}
                  if (newImg._eh_page !== currentPageNum || newImg._eh_obsolete) return;
                  newImg.style.display = 'block';
                  hideSinglePageThumbnailPlaceholder();
                };

                const onImgError = () => {
                  newImg.removeEventListener('load', onImgLoaded);
                  newImg.removeEventListener('error', onImgError);
                  if (newImg._eh_page !== currentPageNum || newImg._eh_obsolete) return;
                  handleBrokenImage('load error');
                };

                newImg.addEventListener('load', onImgLoaded, { once: true });
                newImg.addEventListener('error', onImgError, { once: true });
              }
            }
          }

          injectReaderToggle();
          onPageTurn(activeP);
          return ret;
        };
        window.apply_json_state.__eh_hooked = true;
      }

      if (typeof window.api_response === 'function' && !window.api_response.__eh_safe) {
        const origApiResponse = window.api_response;
        window.api_response = function (xhr) {
          try {
            if (xhr && typeof xhr.responseText === 'string') {
              const parsed = safeParseJson(xhr.responseText);
              if (parsed) return parsed;
              return { error: xhr.responseText.trim() || 'Invalid response' };
            }
            return origApiResponse.apply(this, arguments);
          } catch {
            return { error: 'Invalid JSON response' };
          }
        };
        window.api_response.__eh_safe = true;
      }
    }

    let turnDebounce = null;
    function onPageTurn(activePage) {
      if (turnDebounce) clearTimeout(turnDebounce);
      turnDebounce = setTimeout(() => {
        injectReaderToggle();
        syncThemeClass();
        pruneCaches(activePage || currentPageNum);
        initImageMonitor();
        startPrefetch();

        if (!stripActive) {
          const img = document.getElementById('img');
          const targetP = activePage || currentPageNum;
          if (img && img._eh_page === targetP && !img._eh_obsolete && img.complete && img.naturalWidth > 0) {
            img.style.display = 'block';
            hideSinglePageThumbnailPlaceholder();
          } else {
            showSinglePageThumbnailPlaceholder(targetP);
          }
          const anchor = document.getElementById('i1') || document.getElementById('i3');
          if (anchor) {
            const y = anchor.getBoundingClientRect().top + window.scrollY - 10;
            if (window.scrollY > y) {
              window.scrollTo(0, Math.max(0, y));
            }
          }
        }
      }, 25);
    }

    function clearStallTimer() {
      if (stallTimer) {
        clearTimeout(stallTimer);
        stallTimer = null;
      }
    }

    async function bounceSinglePageImage(pageNo) {
      if (isReloading) return;
      isReloading = true;
      clearStallTimer();

      const pageKey = 'eh_retry_' + location.pathname + '_' + pageNo;
      const count = parseInt(safeStorageGet(sessionStorage, pageKey, '0'), 10);
      if (count >= MAX_RETRIES) {
        showFailureToast(`⚠️ Image for page ${pageNo} failed after ${MAX_RETRIES} attempts. Click to retry.`);
        isReloading = false;
        return;
      }
      safeStorageSet(sessionStorage, pageKey, String(count + 1));

      showSinglePageThumbnailPlaceholder(pageNo);

      const nl = pageNlCodes.get(pageNo) || extractNlCode(document);
      let imgKey = pageImgKeyCache.get(pageNo);
      if (!imgKey) {
        imgKey = await ensureKeysForPage(pageNo);
      }

      if (imgKey) {
        try {
          const freshResp = await fetchApi(pageNo, imgKey, nl || null, true);
          if (freshResp) {
            pageRespCache.set(pageNo, freshResp);
            extractKeysFromResp(freshResp, pageNo);
            const newNl = extractNlCode(freshResp);
            if (newNl) {
              pageNlCodes.set(pageNo, newNl);
            }
            const extracted = extractInfoFromResp(freshResp, pageNo);
            if (extracted) pageTopBarInfoCache.set(pageNo, extracted);
            updateTopBarInfo(pageNo);

            if (typeof window.apply_json_state === 'function') {
              window.apply_json_state(freshResp);
              injectReaderToggle();
              const newImg = document.getElementById('img');
              if (newImg) {
                newImg.__eh_monitored = false;
              }
              initImageMonitor();
              isReloading = false;
              return;
            }
          }
        } catch (err) {
          console.warn('ExFastReader: In-place bounce failed, trying fallback:', err);
        }
      }

      isReloading = false;
      const { gid } = getPageParams();
      if (imgKey && gid && nl) {
        location.href = `${location.origin}/s/${imgKey}/${gid}-${pageNo}?nl=${encodeURIComponent(nl)}`;
      } else {
        const loadfail = document.getElementById('loadfail');
        if (loadfail) loadfail.click();
        else location.reload();
      }
    }

    function handleBrokenImage(reason) {
      bounceSinglePageImage(currentPageNum);
    }

    function executeBrokenReload() {
      bounceSinglePageImage(currentPageNum);
    }

    function initImageMonitor() {
      if (stripActive) return;
      isReloading = false;
      clearStallTimer();

      const img = document.getElementById('img');
      if (!img) return;
      if (img._eh_page !== undefined && img._eh_page !== currentPageNum) return;
      if (img._eh_obsolete) return;
      if (img.__eh_monitored) return;
      img.__eh_monitored = true;

      const pageNo = currentPageNum;
      if (img._eh_page === undefined) {
        img._eh_page = pageNo;
      }
      let bounceTriggered = false;
      const startTime = Date.now();

      const triggerBounce = (reason) => {
        if (bounceTriggered || isReloading) return;
        bounceTriggered = true;
        clearStallTimer();
        img.removeEventListener('error', onError);
        img.removeEventListener('load', onLoad);
        console.warn(`ExFastReader: Single page ${pageNo} image ${reason}. Triggering in-place failover bounce.`);
        bounceSinglePageImage(pageNo);
      };

      const onError = () => {
        if (img._eh_page !== currentPageNum || img._eh_obsolete) return;
        triggerBounce('network load error');
      };

      const onLoad = () => {
        clearStallTimer();
        img.removeEventListener('error', onError);
        img.removeEventListener('load', onLoad);
        if (img._eh_page !== currentPageNum || img._eh_obsolete) return;
        if (img.naturalWidth === 0 || img.naturalHeight === 0) {
          triggerBounce('0x0 dimension after load');
        } else {
          img.style.display = 'block';
          hideSinglePageThumbnailPlaceholder();
          safeStorageRemove(sessionStorage, 'eh_retry_' + location.pathname + '_' + pageNo);
          const durationSec = Math.max(0.1, (Date.now() - startTime) / 1000);
          recordDownloadSpeedSample(pageNo, durationSec);
        }
      };

      if (img.complete) {
        if (img.naturalWidth === 0 || img.naturalHeight === 0) {
          triggerBounce('0x0 dimension on arrival');
          return;
        }
        img.style.display = 'block';
        hideSinglePageThumbnailPlaceholder();
        safeStorageRemove(sessionStorage, 'eh_retry_' + location.pathname + '_' + pageNo);
      } else {
        img.addEventListener('error', onError, { once: true });
        img.addEventListener('load', onLoad, { once: true });

        const { slowTimeoutMs } = calculateImageTimeout(pageNo);
        stallTimer = setTimeout(() => {
          if (img._eh_page !== currentPageNum || img._eh_obsolete) return;
          if (!img.complete || img.naturalWidth === 0) {
            triggerBounce(`slow / stalled (> ${(slowTimeoutMs / 1000).toFixed(1)}s)`);
          }
        }, slowTimeoutMs);
      }
    }

    function initObservers() {
      const i2 = document.getElementById('i2');
      if (i2 && !i2.__eh_observed) {
        i2.__eh_observed = true;
        const obs = new MutationObserver(() => {
          if (!document.getElementById('eh-strip-controls')) {
            injectReaderToggle();
          }
        });
        obs.observe(i2, { childList: true });
      }

      const i1 = document.getElementById('i1') || document.body;
      if (i1 && !i1.__eh_observed) {
        i1.__eh_observed = true;
        const obs = new MutationObserver(() => {
          if (!stripActive) {
            placeStripControls();
          }
        });
        obs.observe(i1, { childList: true });
      }

      const container = document.getElementById('i3');
      if (container && !container.__eh_observed) {
        container.__eh_observed = true;
        let obsDebounce = null;
        const obs = new MutationObserver(() => {
          if (obsDebounce) clearTimeout(obsDebounce);
          obsDebounce = setTimeout(() => {
            if (!stripActive) initImageMonitor();
          }, 20);
        });
        obs.observe(container, { childList: true });
      }
    }

    // Watchdog to ensure no page stays stuck in loadingPages permanently
    setInterval(() => {
      if (!isReaderPage || loadingPages.size === 0) return;
      const now = Date.now();
      let hadStuck = false;
      for (const [p, startTime] of loadingPages.entries()) {
        const { watchdogTimeoutMs } = calculateImageTimeout(p);
        if (now - startTime > watchdogTimeoutMs) {
          console.warn(`ExFastReader Watchdog: Page ${p} exceeded ${(watchdogTimeoutMs / 1000).toFixed(1)}s load limit. Force releasing concurrency slot.`);
          loadingPages.delete(p);
          hadStuck = true;
          const item = pageItemMap.get(p);
          if (item) {
            item.classList.remove('eh-loading');
            if (typeof item._abortImg === 'function') {
              item._abortImg();
              item._abortImg = null;
            } else if (item._currentImgToken) {
              const stuckImg = item._currentImgToken;
              stuckImg.onload = null;
              stuckImg.onerror = null;
              stuckImg.removeAttribute('src');
              stuckImg.src = '';
            }
            item._currentImgToken = null;
            if (!item.querySelector('img.eh-main-img')) {
              recordPageFailure(p, `Watchdog load timeout > ${(watchdogTimeoutMs / 1000).toFixed(1)}s`);
              const failInfo = failedPages.get(p);
              if (failInfo && failInfo.count >= MAX_RETRIES) {
                showRetryUI(item, p);
              } else {
                reloadPageImage(p, failInfo ? failInfo.count : 1);
              }
            }
          }
        }
      }
      if (hadStuck) {
        checkAndLoadNearbyPages();
      }
    }, 3000);

    function initIndexInfiniteScroll() {
      if (!isIndexPage) return;

      let infiniteScrollActive = safeStorageGet(localStorage, 'eh_infinite_scroll_enabled', '1') !== '0';
      let isLoadingNext = false;
      let nextPageUrl = getNextPageUrl(document);
      const loadedUrls = new Set([location.href]);
      let sentinelObserver = null;

      function findThumbnailDropdown() {
        let sel = document.querySelector("select[onchange*='inline_set=dm_']");
        if (sel && !sel.closest('#favact, .favact, [id*="favact"]')) return sel;
        sel = document.querySelector("select[onchange*='inline_set'], select[name*='inline_set']");
        if (sel && !sel.closest('#favact, .favact, [id*="favact"]')) return sel;
        const allSelects = document.querySelectorAll('select');
        for (const s of allSelects) {
          if (s.closest('#favact, .favact, [id*="favact"]') || s.name === 'favact' || s.id === 'favsel' || s.name === 'ddact') continue;
          const oc = s.getAttribute('onchange') || '';
          if (oc.includes('inline_set') || oc.includes('dm_')) return s;
          for (const opt of s.options) {
            const txt = (opt.textContent || '').trim().toLowerCase();
            if (txt === 'thumbnail' || txt === 'thumbnails' || opt.value === 't' || opt.value === 'dm_t') {
              return s;
            }
          }
        }
        return null;
      }

            function getFavoritesForm() {
        const sampleCb = document.querySelector('.itg input[type="checkbox"], input[name^="modifygals"]');
        if (sampleCb) {
          const f = sampleCb.closest('form');
          if (f) return f;
        }
        const itg = document.querySelector('.itg');
        if (itg) {
          const f = itg.closest('form');
          if (f) return f;
        }
        const postForm = document.querySelector('form[method="post"][action*="favorites"], form[method="POST"][action*="favorites"], form[method="post"]');
        if (postForm) return postForm;
        const favform = document.getElementById('favform') || document.querySelector('form[name="favform"]');
        if (favform && (!favform.method || favform.method.toLowerCase() === 'post')) return favform;
        return null;
      }

      function getFavactElement() {
        const byId = document.getElementById('favact');
        if (byId && byId.id !== 'eh-favact-top' && byId.tagName.toLowerCase() !== 'select' && byId.tagName.toLowerCase() !== 'input') {
          return byId;
        }
        const byClass = document.querySelector('div.favact:not(.eh-favact-top), p.favact:not(.eh-favact-top)');
        if (byClass) return byClass;
        const inputEl = document.querySelector('select[name="favact"], select[name="ddact"], input[name="apply"], select#favsel');
        if (inputEl) {
          const parent = inputEl.closest('#favact, .favact, div, p');
          if (parent && parent !== document.body && parent !== document.documentElement && !parent.classList.contains('eh-favact-top') && parent.id !== 'eh-favact-top') {
            return parent;
          }
        }
        return byId || null;
      }

      function toggleAllGalleries(checked) {
        if (typeof window.toggle_all === 'function') {
          try {
            window.toggle_all(checked);
          } catch (err) {
            console.warn('ExFastReader: native toggle_all error', err);
          }
        }

        const galleryCheckboxes = document.querySelectorAll(
          '.itg input[type="checkbox"], input[name^="modifygals"], input[name*="modifygals"]'
        );
        galleryCheckboxes.forEach((cb) => {
          cb.checked = checked;
        });

        document.querySelectorAll(
          '#alltoggle, [name="alltoggle"], .alltoggle, #eh-alltoggle-top, input[type="checkbox"][onclick*="toggle_all"]'
        ).forEach((cb) => {
          cb.checked = checked;
        });
      }

      function executeFavAction(selectedAction) {
        const form = getFavoritesForm();
        if (!form) return;
        if (!form.id) form.id = 'favform';

        let ddactInput = form.querySelector('select[name="ddact"], select[name="favact"]');
        if (ddactInput) {
          ddactInput.value = selectedAction;
        } else {
          let hiddenAct = form.querySelector('input[name="ddact"].eh-sync-input');
          if (!hiddenAct) {
            hiddenAct = document.createElement('input');
            hiddenAct.type = 'hidden';
            hiddenAct.name = 'ddact';
            hiddenAct.className = 'eh-sync-input';
            form.appendChild(hiddenAct);
          }
          hiddenAct.value = selectedAction;
        }

        let hiddenApply = form.querySelector('input[name="apply"].eh-sync-input');
        if (!hiddenApply) {
          hiddenApply = document.createElement('input');
          hiddenApply.type = 'hidden';
          hiddenApply.name = 'apply';
          hiddenApply.value = 'Apply';
          hiddenApply.className = 'eh-sync-input';
          form.appendChild(hiddenApply);
        }

        const checkedBoxes = document.querySelectorAll(
          '.itg input[type="checkbox"]:checked, input[name^="modifygals"]:checked'
        );
        checkedBoxes.forEach((cb) => {
          if (cb.form !== form) {
            cb.setAttribute('form', form.id);
          }
        });

        if (typeof form.requestSubmit === 'function') {
          form.requestSubmit();
        } else {
          form.submit();
        }
      }

      function getNextPageUrl(doc = document) {
        const ptdsNext = doc.querySelector('table.ptb td.ptds + td a, table.ptt td.ptds + td a');
        if (ptdsNext && ptdsNext.href && ptdsNext.textContent.trim() !== '<') {
          return ptdsNext.href;
        }
        const searchnavNext = doc.querySelector('.searchnav a[href*="next="], .searchnav a#next');
        if (searchnavNext && searchnavNext.href) {
          return searchnavNext.href;
        }
        const lastA = doc.querySelector('table.ptb td:last-child a, table.ptt td:last-child a');
        if (lastA && lastA.href && (lastA.textContent.trim() === '>' || lastA.textContent.trim() === '»')) {
          return lastA.href;
        }
        const relNext = doc.querySelector('a[rel="next"]');
        if (relNext && relNext.href) {
          return relNext.href;
        }
        return null;
      }

      function getOrCreateSentinel() {
        let sentinel = document.getElementById('eh-infinite-sentinel');
        if (!sentinel) {
          sentinel = document.createElement('div');
          sentinel.id = 'eh-infinite-sentinel';
          sentinel.className = 'eh-infinite-sentinel';
          sentinel.style.cssText = 'text-align: center; padding: 18px 0; margin: 12px auto; font-family: arial, helvetica, sans-serif; font-size: 10pt; font-weight: bold; width: 100%; clear: both; user-select: none;';

          const itg = document.querySelector('.itg');
          const ptb = document.querySelector('table.ptb, .itg + .searchnav, #favform + .searchnav');
          if (itg && itg.parentNode) {
            if (ptb && ptb.parentNode === itg.parentNode) {
              itg.parentNode.insertBefore(sentinel, ptb);
            } else if (itg.nextSibling) {
              itg.parentNode.insertBefore(sentinel, itg.nextSibling);
            } else {
              itg.parentNode.appendChild(sentinel);
            }
          }
        }
        return sentinel;
      }

      function updateSentinelState(state) {
        const sentinel = getOrCreateSentinel();
        if (!sentinel) return;

        if (!infiniteScrollActive) {
          sentinel.style.display = 'none';
          return;
        }

        sentinel.style.display = 'block';
        if (state === 'loading') {
          sentinel.innerHTML = `
            <div style="display: inline-flex; align-items: center; justify-content: center; gap: 8px;">
              <span class="eh-spinner" style="display: inline-block;"></span>
              <span>Loading next page...</span>
            </div>
          `;
        } else if (state === 'end') {
          sentinel.innerHTML = `<span style="opacity: 0.7;">— All pages loaded —</span>`;
        } else if (state === 'error') {
          sentinel.innerHTML = `
            <span style="color: #ef4444; margin-right: 8px;">Failed to load next page.</span>
            <button type="button" class="eh-strip-btn active" id="eh-infinite-retry-btn">Retry</button>
          `;
          const btn = document.getElementById('eh-infinite-retry-btn');
          if (btn) {
            btn.onclick = () => loadNextPage();
          }
        } else {
          sentinel.innerHTML = '';
        }
      }

      async function loadNextPage() {
        if (!infiniteScrollActive || isLoadingNext || !nextPageUrl || loadedUrls.has(nextPageUrl)) return;
        isLoadingNext = true;
        updateSentinelState('loading');

        const fetchUrl = nextPageUrl;
        loadedUrls.add(fetchUrl);

        try {
          const res = await fetch(fetchUrl, { credentials: 'include' });
          if (!res.ok) throw new Error(`HTTP ${res.status}`);
          const html = await res.text();
          const doc = new DOMParser().parseFromString(html, 'text/html');

          const currentItg = document.querySelector('.itg');
          const newItg = doc.querySelector('.itg');

          if (currentItg && newItg) {
            if (currentItg.tagName.toLowerCase() === 'table') {
              const currentTbody = currentItg.querySelector(':scope > tbody') || currentItg;
              const newTbody = newItg.querySelector(':scope > tbody') || newItg;
              const newRows = Array.from(newTbody.children).filter(
                el => el.tagName && el.tagName.toLowerCase() === 'tr' && !el.querySelector('th')
              );
              for (const row of newRows) {
                currentTbody.appendChild(document.importNode(row, true));
              }
            } else {
              const newItems = Array.from(newItg.children);
              for (const item of newItems) {
                currentItg.appendChild(document.importNode(item, true));
              }
            }
          }

          const curPtb = document.querySelector('table.ptb');
          const newPtb = doc.querySelector('table.ptb');
          if (curPtb && newPtb) {
            curPtb.innerHTML = newPtb.innerHTML;
          }

          nextPageUrl = getNextPageUrl(doc);
          syncFavactPosition();
          if (nextPageUrl && !loadedUrls.has(nextPageUrl)) {
            updateSentinelState('idle');
          } else {
            nextPageUrl = null;
            updateSentinelState('end');
          }
        } catch (err) {
          console.warn('ExFastReader: Infinite scroll page load error', err);
          loadedUrls.delete(fetchUrl);
          updateSentinelState('error');
        } finally {
          isLoadingNext = false;
        }
      }

      function setupObserver() {
        if (sentinelObserver) {
          sentinelObserver.disconnect();
          sentinelObserver = null;
        }
        if (!infiniteScrollActive) return;

        const sentinel = getOrCreateSentinel();
        if (!sentinel) return;

        if (typeof IntersectionObserver !== 'undefined') {
          sentinelObserver = new IntersectionObserver((entries) => {
            for (const entry of entries) {
              if (entry.isIntersecting) {
                loadNextPage();
              }
            }
          }, { rootMargin: '600px' });
          sentinelObserver.observe(sentinel);
        }
      }

      let isSyncingFavact = false;
      function syncFavactPosition() {
        if (!location.pathname.startsWith('/favorites')) return;
        if (isSyncingFavact) return;
        isSyncingFavact = true;

        try {
          const bottomFavact = getFavactElement();
          const ido = document.querySelector('.ido') || document.querySelector('div.ido');
          if (!ido) return;

          if (ido.style.position !== 'relative') {
            ido.style.setProperty('position', 'relative', 'important');
          }

          let topFavact = document.getElementById('eh-favact-top');

          if (infiniteScrollActive) {
            if (!topFavact && bottomFavact) {
              topFavact = bottomFavact.cloneNode(true);
              topFavact.id = 'eh-favact-top';
              topFavact.classList.add('eh-favact-top');

              const cloneCheckbox = topFavact.querySelector('#alltoggle, [name="alltoggle"]');
              if (cloneCheckbox) cloneCheckbox.id = 'eh-alltoggle-top';

              ido.insertBefore(topFavact, ido.firstChild);
            } else if (topFavact) {
              topFavact.style.display = 'inline-flex';
            }

            const topSelect = topFavact ? topFavact.querySelector('select') : null;
            const bottomSelect = bottomFavact ? bottomFavact.querySelector('select') : null;
            const topApply = topFavact ? topFavact.querySelector('input[type="submit"], input[type="button"], button') : null;
            const bottomApply = bottomFavact ? bottomFavact.querySelector('input[type="submit"], input[type="button"], button') : null;

            // Two-way dropdown sync
            if (topSelect && !topSelect.__eh_bound) {
              topSelect.__eh_bound = true;
              topSelect.addEventListener('change', () => {
                if (bottomSelect) bottomSelect.value = topSelect.value;
              });
            }
            if (bottomSelect && !bottomSelect.__eh_bound) {
              bottomSelect.__eh_bound = true;
              bottomSelect.addEventListener('change', () => {
                if (topSelect) topSelect.value = bottomSelect.value;
              });
            }

            // Apply button handling
            if (topApply && !topApply.__eh_bound) {
              topApply.__eh_bound = true;
              topApply.addEventListener('click', (e) => {
                e.preventDefault();
                executeFavAction(topSelect ? topSelect.value : (bottomSelect ? bottomSelect.value : ''));
              });
            }
            if (bottomApply && !bottomApply.__eh_bound) {
              bottomApply.__eh_bound = true;
              bottomApply.addEventListener('click', (e) => {
                e.preventDefault();
                executeFavAction(bottomSelect ? bottomSelect.value : (topSelect ? topSelect.value : ''));
              });
            }
          } else {
            if (topFavact) {
              topFavact.style.display = 'none';
            }
          }
        } catch (err) {
          console.warn('ExFastReader: syncFavactPosition error', err);
        } finally {
          isSyncingFavact = false;
        }
      }      // Delegated listener for alltoggle checkboxes (both top and bottom)
      document.addEventListener('click', (e) => {
        const toggleCb = e.target.closest('#alltoggle, [name="alltoggle"], .alltoggle, #eh-alltoggle-top, input[type="checkbox"][onclick*="toggle_all"]');
        if (toggleCb && toggleCb.type === 'checkbox') {
          setTimeout(() => {
            toggleAllGalleries(toggleCb.checked);
          }, 0);
        }
      });

      // Delegated listener for individual gallery checkboxes to keep master checkboxes synced
      document.addEventListener('change', (e) => {
        if (e.target && e.target.type === 'checkbox') {
          if (e.target.matches('.itg input[type="checkbox"], input[name^="modifygals"], input[name*="modifygals"]')) {
            const all = document.querySelectorAll('.itg input[type="checkbox"], input[name^="modifygals"], input[name*="modifygals"]');
            const checked = document.querySelectorAll('.itg input[type="checkbox"]:checked, input[name^="modifygals"]:checked, input[name*="modifygals"]:checked');
            const isAll = all.length > 0 && all.length === checked.length;
            document.querySelectorAll('#alltoggle, [name="alltoggle"], .alltoggle, #eh-alltoggle-top').forEach((cb) => {
              cb.checked = isAll;
            });
          }
        }
      }, { passive: true });

      function toggleInfiniteScroll() {
        infiniteScrollActive = !infiniteScrollActive;
        safeStorageSet(localStorage, 'eh_infinite_scroll_enabled', infiniteScrollActive ? '1' : '0');
        syncFavactPosition();

        const btn = document.getElementById('eh-infinite-scroll-toggle');
        if (btn) {
          btn.classList.toggle('active', infiniteScrollActive);
        }

        if (infiniteScrollActive) {
          updateSentinelState('idle');
          setupObserver();
          const scrollDist = document.documentElement.scrollHeight - (window.scrollY + window.innerHeight);
          if (scrollDist < 800) {
            loadNextPage();
          }
        } else {
          updateSentinelState('idle');
          if (sentinelObserver) {
            sentinelObserver.disconnect();
            sentinelObserver = null;
          }
        }
      }

      function injectToggle() {
        const dropdown = findThumbnailDropdown();
        if (!dropdown || document.getElementById('eh-infinite-scroll-toggle')) return Boolean(dropdown);

        const wrap = document.createElement('span');
        wrap.id = 'eh-infinite-scroll-wrap';
        wrap.style.cssText = 'display: inline-flex; align-items: center; margin-left: 8px; vertical-align: middle; height: 100%;';

        const btn = document.createElement('button');
        btn.type = 'button';
        btn.id = 'eh-infinite-scroll-toggle';
        btn.className = 'eh-strip-btn' + (infiniteScrollActive ? ' active' : '');
        btn.title = 'Toggle Infinite Scrolling';
        btn.textContent = 'Infinite Scroll';

        const syncDropdownStyle = () => {
          if (!dropdown.isConnected || !btn.isConnected) return;
          const cs = window.getComputedStyle(dropdown);
          if (cs.fontFamily) btn.style.fontFamily = cs.fontFamily;
          if (cs.fontSize) btn.style.fontSize = cs.fontSize;
          if (cs.fontWeight) btn.style.fontWeight = cs.fontWeight;
          const h = dropdown.offsetHeight;
          if (h > 0) {
            btn.style.height = h + 'px';
            btn.style.boxSizing = 'border-box';
          }
        };

        btn.addEventListener('click', (e) => {
          e.preventDefault();
          toggleInfiniteScroll();
          btn.blur();
        });

        wrap.appendChild(btn);
        dropdown.insertAdjacentElement('afterend', wrap);

        syncDropdownStyle();
        requestAnimationFrame(syncDropdownStyle);
        setTimeout(syncDropdownStyle, 100);

        if (infiniteScrollActive) {
          setupObserver();
        }
        syncFavactPosition();
        return true;
      }

      if (location.pathname.startsWith('/favorites')) {
        syncFavactPosition();
        let favDebounce = null;
        const favObs = new MutationObserver(() => {
          if (favDebounce) return;
          favDebounce = setTimeout(() => {
            favDebounce = null;
            syncFavactPosition();
          }, 80);
        });
        favObs.observe(document.body || document.documentElement, { childList: true, subtree: true });
        setTimeout(() => favObs.disconnect(), 15000);
      }

      window.addEventListener('scroll', () => {
        if (!infiniteScrollActive || isLoadingNext || !nextPageUrl) return;
        const scrollDist = document.documentElement.scrollHeight - (window.scrollY + window.innerHeight);
        if (scrollDist < 800) {
          loadNextPage();
        }
      }, { passive: true });

      let injected = injectToggle();
      if (!injected) {
        let obsTimeout = null;
        const obs = new MutationObserver(() => {
          if (injectToggle()) {
            if (obsTimeout) clearTimeout(obsTimeout);
            obs.disconnect();
          }
        });
        obs.observe(document.documentElement, { childList: true, subtree: true });
        obsTimeout = setTimeout(() => obs.disconnect(), 10000);
      }

      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
          if (!nextPageUrl) nextPageUrl = getNextPageUrl(document);
          syncFavactPosition();
          if (infiniteScrollActive) setupObserver();
          if (!document.getElementById('eh-infinite-scroll-toggle')) injectToggle();
        }, { once: true });
      }
    }

    syncThemeClass();

    const initialMeta = getCurrentPageMeta();
    currentPageNum = initialMeta.page;
    if (initialMeta.imgKey) {
      pageImgKeyCache.set(currentPageNum, initialMeta.imgKey);
    }
    const initialImg = document.getElementById('img');
    if (initialImg) {
      initialImg._eh_page = currentPageNum;
      initialImg._eh_obsolete = false;
    }

    if (isReaderPage) {
      let pollCount = 0;
      const hookTimer = setInterval(() => {
        pollCount++;
        const { gid, showkey } = getPageParams();
        const i2 = document.getElementById('i2');
        if (i2) {
          injectReaderToggle();
          syncThemeClass();
          initObservers();
        }

        const canStart = typeof window.load_image === 'function' &&
                         typeof window.apply_json_state === 'function' &&
                         gid && showkey &&
                         (i2 || document.readyState !== 'loading' || pollCount > 60);

        if (canStart) {
          hookReader();
          detectInitialThumbsPerPage();
          discoverAndFetchGallery(currentPageNum);
          prefetchNearbyThumbnails(currentPageNum);
          if (stripActive) {
            enableStripMode();
          } else {
            initImageMonitor();
            startPrefetch();
            injectReaderToggle();
          }
          clearInterval(hookTimer);
        } else if (pollCount > 400) {
          clearInterval(hookTimer);
        }
      }, 25);
    } else if (isGalleryPage) {
      let lastFavToggleTime = 0;
      let cachedPopupData = null; // { note: '', slots: '', names: [], popupItems: [], isFavorited: bool, favcat: num }
      let savedFavLinkInitialHtml = null;

      const DEFAULT_FAV_NAMES = [
        'Favorites 0', 'Favorites 1', 'Favorites 2', 'Favorites 3', 'Favorites 4',
        'Favorites 5', 'Favorites 6', 'Favorites 7', 'Favorites 8', 'Favorites 9'
      ];

      function getSavedFavNames() {
        try {
          const raw = safeStorageGet(localStorage, 'eh_fav_cat_names', null);
          if (raw) {
            const parsed = JSON.parse(raw);
            if (Array.isArray(parsed) && parsed.length >= 10) return parsed;
          }
        } catch {}
        return [...DEFAULT_FAV_NAMES];
      }

      function detectCurrentCategory() {
        if (cachedPopupData && typeof cachedPopupData.favcat === 'number' && cachedPopupData.isFavorited) {
          return cachedPopupData.favcat;
        }
        const favDiv = document.getElementById('fav');
        if (favDiv) {
          const style = (favDiv.getAttribute('style') || '') + ' ' + (favDiv.style.cssText || '') + ' ' + (favDiv.style.backgroundPosition || '');
          const m = style.match(/(?:0(?:px|%)?|left)\s+(-?\d+)px/i) ||
                    style.match(/background-position:\s*[^;]*?(-?\d+)px/i) ||
                    style.match(/(-?\d+)px\s+no-repeat/i) ||
                    style.match(/0px\s+(-?\d+)px/i);
          if (m) {
            const posY = parseInt(m[1], 10);
            const idx = Math.round((-2 - posY) / 19);
            if (idx >= 0 && idx <= 9) return idx;
          }
          const title = favDiv.getAttribute('title');
          if (title && title.trim()) {
            const names = getSavedFavNames();
            const idx = names.findIndex(n => n.toLowerCase() === title.trim().toLowerCase());
            if (idx !== -1) return idx;
            const mNum = title.match(/Favorites\s+(\d)/i);
            if (mNum) {
              const catNum = parseInt(mNum[1], 10);
              if (catNum >= 0 && catNum <= 9) return catNum;
            }
          }
        }
        const favLink = document.getElementById('favoritelink');
        if (favLink) {
          const txt = favLink.textContent.trim().toLowerCase();
          if (txt && !txt.includes('add to')) {
            const names = getSavedFavNames();
            const idx = names.findIndex(n => n.toLowerCase() === txt);
            if (idx !== -1) return idx;
            const mNum = txt.match(/favorites\s+(\d)/i);
            if (mNum) {
              const catNum = parseInt(mNum[1], 10);
              if (catNum >= 0 && catNum <= 9) return catNum;
            }
          }
        }
        return null;
      }

      function isGalleryFavorited() {
        if (cachedPopupData && typeof cachedPopupData.isFavorited === 'boolean') {
          return cachedPopupData.isFavorited;
        }
        const favLink = document.getElementById('favoritelink');
        if (favLink) {
          const txt = favLink.textContent.trim().toLowerCase();
          if (txt.includes('edit')) return true;
          if (txt && !txt.includes('add to')) return true;
        }
        const favDiv = document.getElementById('fav');
        if (favDiv) {
          if (favDiv.getAttribute('title')) return true;
          const style = (favDiv.getAttribute('style') || '') + ' ' + (favDiv.style.cssText || '');
          if (style.includes('fav.png') || /background(-image)?:\s*url/i.test(style)) return true;
        }
        return detectCurrentCategory() !== null;
      }

      // Intercept mousedown, pointerdown, and click on window to prevent foreign scripts (ComicRead, etc.) or native popup
      ['mousedown', 'pointerdown', 'click'].forEach((evtType) => {
        window.addEventListener(evtType, (e) => {
          const favTarget = e.target.closest('#gdf, #favoritelink, [onclick*="pop_fav"], [onclick*="addfav"]');
          if (favTarget) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            if (evtType === 'click') {
              openInlineFavorites();
            }
            return false;
          }
        }, true);
      });

      // Override pop_fav, popUp, window.open
      try {
        const _popFavOverride = function () {
          openInlineFavorites();
          return false;
        };
        Object.defineProperty(window, 'pop_fav', {
          configurable: true,
          enumerable: true,
          get() { return _popFavOverride; },
          set() {}
        });
      } catch {
        window.pop_fav = function () { openInlineFavorites(); return false; };
      }

      const _origPopUp = window.popUp;
      window.popUp = function (url, w, h) {
        if (typeof url === 'string' && (url.includes('addfav') || url.includes('act=addfav'))) {
          openInlineFavorites();
          return false;
        }
        if (typeof _origPopUp === 'function') {
          return _origPopUp.apply(this, arguments);
        }
        return false;
      };

      const _origWindowOpen = window.open;
      window.open = function (url, name, specs) {
        if (typeof url === 'string' && (url.includes('addfav') || url.includes('act=addfav'))) {
          openInlineFavorites();
          return null;
        }
        if (typeof _origWindowOpen === 'function') {
          return _origWindowOpen.apply(this, arguments);
        }
        return null;
      };

      function purgeForeignPopups() {
        const gd3 = document.getElementById('gd3');
        if (gd3) {
          Array.from(gd3.children).forEach((child) => {
            const id = child.id;
            const cls = child.className;
            if (!['gdc', 'gdn', 'gdd', 'gdr', 'gdf'].includes(id) && cls !== 'c') {
              child.remove();
            }
          });
        }
      }

      function sanitizeFavElements() {
        const oldGdf = document.getElementById('gdf');
        if (oldGdf && !oldGdf.__eh_sanitized) {
          const favLink = oldGdf.querySelector('#favoritelink');
          if (favLink && !savedFavLinkInitialHtml && favLink.querySelector('img')) {
            savedFavLinkInitialHtml = favLink.innerHTML;
          }
          const newGdf = oldGdf.cloneNode(true);
          newGdf.__eh_sanitized = true;
          newGdf.removeAttribute('onclick');
          const newFavLink = newGdf.querySelector('#favoritelink');
          if (newFavLink) newFavLink.removeAttribute('onclick');

          newGdf.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            openInlineFavorites();
          });

          oldGdf.parentNode.replaceChild(newGdf, oldGdf);
        }
        purgeForeignPopups();
      }

      function attachRemoveHandler(btn, form, container) {
        if (!btn || btn.__eh_bound) return;
        btn.__eh_bound = true;
        btn.addEventListener('click', async (e) => {
          e.preventDefault();
          btn.disabled = true;
          btn.value = 'Removing...';
          const submitBtn = container.querySelector('.eh-fav-submit-btn');
          const cancelBtn = container.querySelector('.eh-fav-cancel-btn');
          if (submitBtn) submitBtn.disabled = true;
          if (cancelBtn) cancelBtn.disabled = true;

          const m = location.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
          const gid = m ? m[1] : (window.gid || '');
          const token = m ? m[2] : (window.token || '');
          const postUrl = `${location.origin}/gallerypopups.php?gid=${gid}&t=${token}&act=addfav`;

          const formData = new FormData();
          formData.append('favcat', 'favdel');
          formData.append('favnote', '');
          formData.append('apply', 'Apply Changes');
          formData.append('update', '1');

          try {
            const res = await fetch(postUrl, {
              method: 'POST',
              body: formData,
              credentials: 'include'
            });

            if (res.ok) {
              cachedPopupData = {
                note: '',
                slots: (cachedPopupData ? cachedPopupData.slots : ''),
                names: (cachedPopupData ? cachedPopupData.names : getSavedFavNames()),
                popupItems: (cachedPopupData ? cachedPopupData.popupItems : []),
                isFavorited: false,
                favcat: null
              };
              updateGalleryFavoriteUI('favdel', 'Remove from Favorites');
              closeInlineFavorites();
            } else {
              throw new Error('HTTP ' + res.status);
            }
          } catch (err) {
            console.warn('ExFastReader: Failed to remove favorite', err);
            alert('Failed to remove favorite. Please try again.');
            btn.disabled = false;
            btn.value = 'Remove from Favorites';
            if (submitBtn) submitBtn.disabled = false;
            if (cancelBtn) cancelBtn.disabled = false;
          }
        });
      }

      // Silently prefetch popup info (exact diamonds, note, slots, custom names) straight from the popup in background
      async function silentPrefetchPopupInfo() {
        if (cachedPopupData && typeof cachedPopupData.isFavorited === 'boolean') return cachedPopupData;
        const m = location.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
        const gid = m ? m[1] : (window.gid || '');
        const token = m ? m[2] : (window.token || '');
        if (!gid || !token) return null;

        const popupUrl = `${location.origin}/gallerypopups.php?gid=${gid}&t=${token}&act=addfav`;
        try {
          const res = await fetch(popupUrl, { credentials: 'include' });
          if (!res.ok) return null;
          const html = await res.text();
          const doc = new DOMParser().parseFromString(html, 'text/html');

          let note = '';
          const noteEl = doc.querySelector('textarea[name="favnote"]');
          if (noteEl) note = noteEl.value || noteEl.textContent || '';

          let slots = '';
          const mSlots = html.match(/(\d+\s*\/\s*\d+\s+favorite note slots used[^\n<\[]*(?:<a[^>]*>\[\?\]<\/a>)?)/i);
          if (mSlots) {
            slots = mSlots[1];
          } else {
            const allElements = doc.querySelectorAll('p, div');
            for (const p of allElements) {
              if (p.children.length <= 1 && p.textContent.includes('favorite note slots used')) {
                slots = p.innerHTML.trim();
                break;
              }
            }
          }
            if (slots && !slots.includes('href=')) {
                slots += ` <a href="https://ehwiki.org/wiki/Favorite" target="_blank">[?]</a>`;
            }

          const catDivs = doc.querySelectorAll('.nosel > div, div.nosel > div');
          const names = getSavedFavNames();
          const popupItems = [];

          if (catDivs.length >= 10) {
            catDivs.forEach((div, i) => {
              if (i < 10) {
                const txt = div.textContent.trim();
                if (txt && !txt.toLowerCase().includes('remove')) {
                  names[i] = txt;
                }
                const diamondDiv = div.querySelector('div.i, div[style*="background"]');
                const diamondStyle = diamondDiv ? diamondDiv.getAttribute('style') : `background-image:url('https://ehgt.org/g/fav.png'); background-position:0px ${-2 - 19 * i}px;`;
                popupItems.push({ idx: i, name: txt || names[i], style: diamondStyle });
              }
            });
            safeStorageSet(localStorage, 'eh_fav_cat_names', JSON.stringify(names));
          }

          const hasApply = Boolean(doc.querySelector('input[value*="Apply Changes"], input[value="Apply Changes"]'));
          const hasRemove = Boolean(doc.querySelector('input[value*="Remove"], [onclick*="favdel"]'));
          const checkedRadio = doc.querySelector('input[name="favcat"]:checked');
          const checkedCat = checkedRadio ? parseInt(checkedRadio.value, 10) : null;

          const isFavorited = hasApply || hasRemove || (checkedCat !== null && isGalleryFavorited());
          const favcat = checkedCat !== null ? checkedCat : (detectCurrentCategory() ?? 0);

          cachedPopupData = { note, slots, names, popupItems, isFavorited, favcat };

          // If the menu is currently open, seamlessly sync the extracted fields
          const container = document.getElementById('eh-inline-favorites-container');
          if (container && container.style.display !== 'none') {
            const textarea = container.querySelector('#eh-fav-note-textarea');
            if (textarea && !textarea.value && note) {
              textarea.value = note;
            }
            if (slots) {
              const slotsEl = container.querySelector('.eh-fav-slots');
              if (slotsEl) slotsEl.innerHTML = slots;
            }
            if (popupItems.length > 0) {
              popupItems.forEach((item) => {
                const label = container.querySelector(`.eh-fav-cat-label[data-cat-idx="${item.idx}"]`);
                if (label && label.textContent !== item.name) {
                  label.textContent = item.name;
                  label.title = item.name;
                }
                const diamond = container.querySelector(`.eh-fav-cat-diamond[data-cat-idx="${item.idx}"]`);
                if (diamond && item.style) {
                  diamond.setAttribute('style', item.style);
                }
              });
            }

            const submitBtn = container.querySelector('.eh-fav-submit-btn');
            if (submitBtn) {
              submitBtn.value = isFavorited ? 'Apply Changes' : 'Add to Favorites';
            }

            const targetRadio = container.querySelector(`input[name="favcat"][value="${favcat}"]`);
            if (targetRadio) {
              targetRadio.checked = true;
            }

            let removeBtn = container.querySelector('#eh-fav-remove-btn');
            if (isFavorited) {
              if (!removeBtn) {
                removeBtn = document.createElement('input');
                removeBtn.type = 'button';
                removeBtn.className = 'eh-fav-remove-btn';
                removeBtn.id = 'eh-fav-remove-btn';
                removeBtn.value = 'Remove from Favorites';
                const actionsEl = container.querySelector('.eh-fav-actions');
                const cancelBtn = container.querySelector('.eh-fav-cancel-btn');
                if (actionsEl && cancelBtn) {
                  actionsEl.insertBefore(removeBtn, cancelBtn);
                }
                const formEl = container.querySelector('#eh-fav-form');
                attachRemoveHandler(removeBtn, formEl, container);
              }
            } else if (removeBtn) {
              removeBtn.remove();
            }
          }
          return cachedPopupData;
        } catch {
          return null;
        }
      }

      function openInlineFavorites() {
        const now = Date.now();
        if (now - lastFavToggleTime < 300) return;
        lastFavToggleTime = now;

        purgeForeignPopups();

        const gd4 = document.getElementById('gd4');
        if (!gd4) return;

        let container = document.getElementById('eh-inline-favorites-container');
        if (container && container.style.display !== 'none') {
          closeInlineFavorites();
          return;
        }

        // Clean up any stray containers
        const allContainers = document.querySelectorAll('#eh-inline-favorites-container');
        if (allContainers.length > 1) {
          for (let i = 1; i < allContainers.length; i++) allContainers[i].remove();
          container = allContainers[0];
        }

        // Hide ALL other elements inside #gd4 (tag list, voting, and enter new tags boxes)
        gd4.classList.add('eh-fav-active');
        Array.from(gd4.children).forEach((child) => {
          if (child !== container) {
            if (child._eh_prev_display === undefined) child._eh_prev_display = child.style.display;
            child.style.setProperty('display', 'none', 'important');
          }
        });

        document.querySelectorAll('#tagmenu_new, #tagmenu_act, #tagmenu, #tagform').forEach((el) => {
          if (el.closest('#gd4')) {
            el.style.setProperty('display', 'none', 'important');
          }
        });

        if (!container) {
          container = document.createElement('div');
          container.id = 'eh-inline-favorites-container';
          gd4.appendChild(container);
        }
        container.style.display = 'block';

        // INSTANT SYNCHRONOUS BUILD: 0ms delay, no loading spinner
        renderBuiltInFavoriteForm(container);

        // Silently sync note/slots/folder names in the background
        silentPrefetchPopupInfo();
      }

      function closeInlineFavorites() {
        const gd4 = document.getElementById('gd4');
        if (gd4) {
          gd4.classList.remove('eh-fav-active');
          Array.from(gd4.children).forEach((child) => {
            if (child.id !== 'eh-inline-favorites-container') {
              child.style.display = child._eh_prev_display !== undefined ? child._eh_prev_display : '';
            }
          });
        }
        const container = document.getElementById('eh-inline-favorites-container');
        if (container) {
          container.style.display = 'none';
        }
      }

      function updateGalleryFavoriteUI(catVal, submitVal) {
        const favDiv = document.getElementById('fav');
        const favLink = document.getElementById('favoritelink');
        const gdf = document.getElementById('gdf');

        if (catVal === 'favdel' || (submitVal && submitVal.toLowerCase().includes('remove'))) {
          if (favDiv) {
            favDiv.style.background = '';
            favDiv.style.width = '';
            favDiv.style.height = '';
            favDiv.style.display = '';
            favDiv.style.marginRight = '';
            favDiv.removeAttribute('title');
            favDiv.innerHTML = '';
          }
          if (favLink) {
            favLink.innerHTML = savedFavLinkInitialHtml || '<img src="https://ehgt.org/g/mr.gif" /> Add to Favorites';
          }
          if (gdf) {
            gdf.querySelectorAll('.i, img[src*="fav.png"], div[style*="fav.png"]').forEach((el) => {
              if (el !== favDiv) el.remove();
            });
          }
          cachedPopupData = {
            note: '',
            slots: (cachedPopupData ? cachedPopupData.slots : ''),
            names: (cachedPopupData ? cachedPopupData.names : getSavedFavNames()),
            popupItems: (cachedPopupData ? cachedPopupData.popupItems : []),
            isFavorited: false,
            favcat: null
          };
        } else if (catVal !== null && catVal !== undefined) {
          const catIdx = parseInt(catVal, 10);
          if (!isNaN(catIdx)) {
            const names = getSavedFavNames();
            const chosenName = names[catIdx] || ('Favorites ' + catIdx);
            if (favDiv) {
              favDiv.innerHTML = '';
              favDiv.style.background = `transparent url('https://ehgt.org/g/fav.png') 0px ${-2 - 19 * catIdx}px no-repeat`;
              favDiv.style.width = '19px';
              favDiv.style.height = '19px';
              favDiv.style.display = 'inline-block';
              favDiv.style.marginRight = '4px';
              favDiv.title = chosenName;
            }
            if (favLink) {
              favLink.textContent = chosenName;
            }
            if (gdf) {
              gdf.querySelectorAll('.i, img[src*="fav.png"], div[style*="fav.png"]').forEach((el) => {
                if (el !== favDiv) el.remove();
              });
            }
            cachedPopupData = {
              note: (cachedPopupData ? cachedPopupData.note : ''),
              slots: (cachedPopupData ? cachedPopupData.slots : ''),
              names,
              popupItems: (cachedPopupData ? cachedPopupData.popupItems : []),
              isFavorited: true,
              favcat: catIdx
            };
          }
        }
      }

      function renderBuiltInFavoriteForm(container) {
        const currentCat = detectCurrentCategory();
        const isAlreadyFav = isGalleryFavorited();

        const submitText = isAlreadyFav ? 'Apply Changes' : 'Add to Favorites';
        const checkedCat = currentCat !== null ? currentCat : 0;
        const favNames = (cachedPopupData && cachedPopupData.names) ? cachedPopupData.names : getSavedFavNames();
        const noteVal = (cachedPopupData && cachedPopupData.note) ? cachedPopupData.note : '';
        const slotsHtml = (cachedPopupData && cachedPopupData.slots) ? cachedPopupData.slots : `0 / 1000 favorite note slots used. <a href="https://ehwiki.org/wiki/Favorite" target="_blank">[?]</a>`;
        const popupItems = cachedPopupData && cachedPopupData.popupItems ? cachedPopupData.popupItems : [];

        let catsHtml = '';
        for (let i = 0; i < 10; i++) {
          const name = favNames[i] || `Favorites ${i}`;
          const isChecked = (i === checkedCat);
          const customStyle = popupItems[i]?.style || `background-image:url('https://ehgt.org/g/fav.png'); background-position:0px ${-2 - 19 * i}px;`;
          catsHtml += `
            <label class="eh-fav-cat-item">
              <input type="radio" name="favcat" value="${i}" ${isChecked ? 'checked' : ''} />
              <div class="eh-fav-cat-diamond i" data-cat-idx="${i}" style="${customStyle}"></div>
              <span class="eh-fav-cat-label" data-cat-idx="${i}" title="${name}">${name}</span>
            </label>
          `;
        }

        const removeBtnHtml = isAlreadyFav
          ? `<input type="button" class="eh-fav-remove-btn" id="eh-fav-remove-btn" value="Remove from Favorites" />`
          : '';

        const headerText = 'Choose a favorite category to save this gallery under. You can also add a note to it if you wish.';

        container.innerHTML = `
          <form class="eh-fav-form" id="eh-fav-form" method="post">
            <input type="hidden" name="update" value="1" />
            <p class="eh-fav-header">${headerText}</p>
            <div class="eh-fav-body">
              <div class="eh-fav-cats">
                ${catsHtml}
              </div>
              <div class="eh-fav-note-pane">
                <div class="eh-fav-note-title">Favorite Note (Max 200 Characters)</div>
                <textarea name="favnote" id="eh-fav-note-textarea" maxlength="200" class="eh-fav-note-textarea" placeholder="">${noteVal}</textarea>
                <div class="eh-fav-slots">${slotsHtml}</div>
                <div class="eh-fav-actions">
                  <input type="submit" name="apply" class="eh-fav-submit-btn" value="${submitText}" />
                  ${removeBtnHtml}
                  <input type="button" class="eh-fav-cancel-btn" value="Cancel" />
                </div>
              </div>
            </div>
          </form>
        `;

        const form = container.querySelector('#eh-fav-form');
        const cancelBtn = container.querySelector('.eh-fav-cancel-btn');
        const submitBtn = container.querySelector('.eh-fav-submit-btn');
        const removeBtn = container.querySelector('#eh-fav-remove-btn');

        if (cancelBtn) {
          cancelBtn.addEventListener('click', (e) => {
            e.preventDefault();
            closeInlineFavorites();
          });
        }

        if (removeBtn) {
          attachRemoveHandler(removeBtn, form, container);
        }

        if (form) {
          form.addEventListener('submit', async (e) => {
            e.preventDefault();

            const curSubmitVal = submitBtn ? submitBtn.value : 'Add to Favorites';
            const selectedRadio = form.querySelector('input[name="favcat"]:checked');
            const chosenCat = selectedRadio ? selectedRadio.value : '0';
            const chosenNote = form.querySelector('#eh-fav-note-textarea')?.value || '';

            const m = location.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
            const gid = m ? m[1] : (window.gid || '');
            const token = m ? m[2] : (window.token || '');
            const postUrl = `${location.origin}/gallerypopups.php?gid=${gid}&t=${token}&act=addfav`;

            const formData = new FormData();
            formData.append('favcat', chosenCat);
            formData.append('favnote', chosenNote);
            formData.append('apply', curSubmitVal);
            formData.append('update', '1');

            if (submitBtn) {
              submitBtn.disabled = true;
              submitBtn.value = 'Saving...';
            }
            if (removeBtn) removeBtn.disabled = true;
            if (cancelBtn) cancelBtn.disabled = true;

            try {
              const res = await fetch(postUrl, {
                method: 'POST',
                body: formData,
                credentials: 'include'
              });

              if (res.ok) {
                cachedPopupData = {
                  note: chosenNote,
                  slots: (cachedPopupData ? cachedPopupData.slots : ''),
                  names: (cachedPopupData ? cachedPopupData.names : getSavedFavNames()),
                  popupItems: (cachedPopupData ? cachedPopupData.popupItems : []),
                  isFavorited: true,
                  favcat: parseInt(chosenCat, 10)
                };
                updateGalleryFavoriteUI(chosenCat, curSubmitVal);
                closeInlineFavorites();
              } else {
                throw new Error('HTTP ' + res.status);
              }
            } catch (err) {
              console.warn('ExFastReader: Failed to save favorite', err);
              alert('Failed to save favorite. Please try again.');
              if (submitBtn) {
                submitBtn.disabled = false;
                submitBtn.value = curSubmitVal;
              }
              if (removeBtn) removeBtn.disabled = false;
              if (cancelBtn) cancelBtn.disabled = false;
            }
          });
        }
      }

      function setupInlineFavorites() {
        const favLink = document.getElementById('favoritelink');
        if (favLink && favLink.querySelector('img')) {
          savedFavLinkInitialHtml = favLink.innerHTML;
        }
        sanitizeFavElements();
        silentPrefetchPopupInfo();

        window.addEventListener('keydown', (e) => {
          if (e.key === 'Escape') {
            const container = document.getElementById('eh-inline-favorites-container');
            if (container && container.style.display !== 'none') {
              closeInlineFavorites();
            }
          }
        });
      }

      function initGalleryPage() {
        setupInlineFavorites();
        const m = location.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
        const curGid = m ? parseInt(m[1], 10) : 0;
        if (m) {
          const gUrl = location.origin + `/g/${m[1]}/${m[2]}/`;
          safeStorageSet(sessionStorage, 'eh_gallery_' + m[1], gUrl);
          safeStorageSet(localStorage, 'eh_gallery_' + m[1], gUrl);
          cachedGalleryUrl = gUrl;
        }
        if (curGid) {
          cachedGid = curGid;
          loadGalleryDataFromSession(curGid);
        }
        const gpcEl = document.querySelector('.gpc, #gpc');
        if (gpcEl) {
          const mGpc = gpcEl.textContent.match(/Showing\s+(\d+)\s*-\s*(\d+)\s+of\s+([\d,]+)\s+images/i);
          if (mGpc) {
            const sNum = parseInt(mGpc[1], 10);
            const eNum = parseInt(mGpc[2], 10);
            const tot = parseInt(mGpc[3].replace(/,/g, ''), 10);
            if (eNum < tot) {
              const count = eNum - sNum + 1;
              if ([20, 40, 100].includes(count)) {
                thumbsPerPage = count;
                safeStorageSet(localStorage, 'eh_thumbs_per_page', String(count));
              }
            }
            if (tot > 0) cachedTotalPages = tot;
          }
        }
        const thumbCount = document.querySelectorAll('#gdt .gdtm, #gdt .gdtl').length;
        if ([20, 40, 100].includes(thumbCount)) {
          thumbsPerPage = thumbCount;
          safeStorageSet(localStorage, 'eh_thumbs_per_page', String(thumbCount));
        }

        // Immediately parse all thumbnails, dimensions, keys, and sprites directly from the gallery DOM
        extractDimensionsFromGallery(document);
        if (curGid) {
          saveGalleryDataToSession(curGid);
        }

        if ('scrollRestoration' in history) {
          try { history.scrollRestoration = 'manual'; } catch {}
        }

        const MAX_PREFETCH_LINKS = 12;
        const prefetchLinks = [];
        function safePrefetchUrl(url) {
          if (!url) return;
          try {
            if (document.querySelector(`link[rel="prefetch"][href="${CSS.escape(url)}"]`)) return;
            while (prefetchLinks.length >= MAX_PREFETCH_LINKS) {
              const oldLink = prefetchLinks.shift();
              if (oldLink && oldLink.parentNode) {
                oldLink.remove();
              }
            }
            const link = document.createElement('link');
            link.rel = 'prefetch';
            link.href = url;
            document.head.appendChild(link);
            prefetchLinks.push(link);
          } catch {}
        }

let isGalleryNavigating = false;
        async function navigateGalleryPage(targetUrl, pushState = true) {
          if (isGalleryNavigating) return;
          isGalleryNavigating = true;

          const gdt = document.getElementById('gdt');
          const ptt = document.querySelector('table.ptt');
          const ptb = document.querySelector('table.ptb');
          const gpc = document.querySelector('.gpc, #gpc');

          try {
            const res = await fetch(targetUrl, { credentials: 'include' });
            if (!res.ok) {
              location.href = targetUrl;
              return;
            }

            const html = await res.text();
            const doc = new DOMParser().parseFromString(html, 'text/html');

            const newPtt = doc.querySelector('table.ptt');
            const newGpc = doc.querySelector('.gpc, #gpc');
            const newGdt = doc.getElementById('gdt');
            const newPtb = doc.querySelector('table.ptb');

            // 1. Update in natural top-to-bottom DOM order to eliminate reflow jumping
            if (newPtt && ptt) {
              ptt.innerHTML = newPtt.innerHTML;
            }
            if (newGpc && gpc) {
              gpc.innerHTML = newGpc.innerHTML;
            }
            if (newGdt && gdt) {
              gdt.innerHTML = newGdt.innerHTML;
            }
            if (newPtb && ptb) {
              ptb.innerHTML = newPtb.innerHTML;
            }

            if (doc.title) {
              document.title = doc.title;
            }

            if (pushState) {
              window.history.pushState({ galleryUrl: targetUrl }, doc.title || document.title, targetUrl);
            }

            // 2. Parse and cache the newly loaded thumbnails and dimensions
            extractDimensionsFromGallery(doc);
            if (curGid) {
              saveGalleryDataToSession(curGid);
            }

            // 3. Prefetch next page thumbnail if present
            const nextP = document.querySelector('table.ptb td.ptds + td a') || document.querySelector('table.ptb td:last-child a');
            if (nextP && nextP.href && nextP.textContent.trim() !== '<') {
              safePrefetchUrl(nextP.href);
            }
          } catch (err) {
            console.warn('ExFastReader: Gallery AJAX navigation failed, falling back to full navigation', err);
            location.href = targetUrl;
          } finally {
            isGalleryNavigating = false;
          }
        }

        window.__eh_navigateGalleryPage = navigateGalleryPage;

        document.addEventListener('click', (e) => {
          const cell = e.target.closest('table.ptt td, table.ptb td, table.ptt a, table.ptb a, .searchnav td, .searchnav a');
          if (!cell) return;

          const link = cell.tagName === 'A' ? cell : cell.querySelector('a');
          if (!link) return;

          const rawHref = link.getAttribute('href') || link.href;
          if (!rawHref || rawHref === '#' || rawHref.startsWith('javascript:')) return;

          let targetUrl;
          try {
            targetUrl = new URL(rawHref, location.href).href;
          } catch {
            targetUrl = link.href;
          }
          if (!targetUrl) return;

          try {
            const parsedTarget = new URL(targetUrl, location.origin);
            if (parsedTarget.origin !== location.origin) return;

            const mCur = location.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
            const mTar = parsedTarget.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
            const isSameGallery = (mCur && mTar && mCur[1] === mTar[1]) ||
                                  (parsedTarget.pathname.replace(/\/+$/, '') === location.pathname.replace(/\/+$/, ''));
            if (!isSameGallery) return;

            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();

            navigateGalleryPage(targetUrl, true);
          } catch (err) {
            console.warn('ExFastReader: Gallery link check error', err);
          }
        }, true);

        // Listen for reader navigation clicks to guarantee session storage synchronization
        document.addEventListener('click', (e) => {
          const a = e.target.closest('a[href*="/s/"]');
          if (a && curGid) {
            saveGalleryDataToSession(curGid);
          }
        }, { capture: true, passive: true });

        // Prefetch reader page on hover for instant transition
        document.addEventListener('pointerover', (e) => {
          const a = e.target.closest('a[href*="/s/"]');
          if (a && a.href && !a.__eh_prefetched) {
            a.__eh_prefetched = true;
            safePrefetchUrl(a.href);
          }
        }, { capture: true, passive: true });

        const nextThumb = document.querySelector('table.ptb td.ptds + td a') || document.querySelector('table.ptb td:last-child a');
        if (nextThumb && nextThumb.href && nextThumb.textContent.trim() !== '<') {
          safePrefetchUrl(nextThumb.href);
        }
      }

      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initGalleryPage, { once: true });
      } else {
        initGalleryPage();
      }
    } else if (isIndexPage) {
      initIndexInfiniteScroll();
    }
  }
})();