☰

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.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Userscripts installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey installieren.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

(Ich habe bereits einen Benutzerstil Verwaltung, ich möchte ihn installieren!)

// ==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();
    }
  }
})();