☰

Hitomi Revamp

A redesigned hitomi.la: themeable gallery grid, gallery pages and reader, tag autocomplete, hover previews, a global tag blacklist and a per-tab search builder

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

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

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

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

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

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

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

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

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

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         Hitomi Revamp
// @namespace    hitomi-revamp
// @version      1.0.0
// @author       WRE
// @license      MIT
// @description  A redesigned hitomi.la: themeable gallery grid, gallery pages and reader, tag autocomplete, hover previews, a global tag blacklist and a per-tab search builder
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzhhOGZmMCIvPjxzdG9wIG9mZnNldD0iLjU1IiBzdG9wLWNvbG9yPSIjNGE0ZTlhIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMjMyNDNhIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9InBnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmZmZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZGNkZGY2Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTUiIGZpbGw9InVybCgjYmcpIi8+PHBhdGggZD0iTTExIDE3LjVjNi41LTIuMiAxMy41LTEuNiAxOSAyLjJWNDhjLTUuNS0zLjYtMTIuNS00LjItMTktMnoiIGZpbGw9InVybCgjcGcpIi8+PHBhdGggZD0iTTUzIDE3LjVjLTYuNS0yLjItMTMuNS0xLjYtMTkgMi4yVjQ4YzUuNS0zLjYgMTIuNS00LjIgMTktMnoiIGZpbGw9InVybCgjcGcpIi8+PHBhdGggZD0iTTE2LjUgMjYuNXYxM00yNC41IDI3djEzTTE2LjUgMzMuM2g4IiBzdHJva2U9IiM0YTRlOWEiIHN0cm9rZS13aWR0aD0iMy4yIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNNDAgMjdsNi41IDYuNUw0MCA0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjYzAzOTVhIiBzdHJva2Utd2lkdGg9IjMuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+Cg==
// @match        https://hitomi.la/*
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @grant        GM_addStyle
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// ==/UserScript==

(() => {
  'use strict';

  const LOGO_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#8a8ff0"/><stop offset=".55" stop-color="#4a4e9a"/><stop offset="1" stop-color="#23243a"/></linearGradient><linearGradient id="pg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#dcddf6"/></linearGradient></defs><rect width="64" height="64" rx="15" fill="url(#bg)"/><path d="M11 17.5c6.5-2.2 13.5-1.6 19 2.2V48c-5.5-3.6-12.5-4.2-19-2z" fill="url(#pg)"/><path d="M53 17.5c-6.5-2.2-13.5-1.6-19 2.2V48c5.5-3.6 12.5-4.2 19-2z" fill="url(#pg)"/><path d="M16.5 26.5v13M24.5 27v13M16.5 33.3h8" stroke="#4a4e9a" stroke-width="3.2" stroke-linecap="round"/><path d="M40 27l6.5 6.5L40 40" fill="none" stroke="#c0395a" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
  const LOGO_URL = 'data:image/svg+xml,' + encodeURIComponent(LOGO_SVG);
  const VERSION = (typeof GM_info !== 'undefined' && GM_info.script?.version) || '';

  const DEFAULTS = {
    newUi: true,
    theme: 'midnight',
    accent: 'indigo',
    cardSize: 'm',
    cardTags: 'compact',
    classicTheme: true,
    readerAutoHide: true,
    fit: 'height',
    rightArrowIsNext: true,
    escBack: true,
    zoom: 100,
    clickHalves: true,
    openInReader: false,
    newTab: false,
    newTabFocus: false,
    blMode: 'hide',
    stripPos: 'bottom',
    thumbSize: 110,
    thumbLoad: 'visible',
    maxParallel: 2,
    delayMs: 150,
  };
  const SKEY = 'hr-settings';
  const S = Object.assign({}, DEFAULTS, GM_getValue(SKEY, {}));
  const saveSettings = () => GM_setValue(SKEY, { ...S });
  if (S.ui === 'classic') S.newUi = false;
  delete S.ui; delete S.readerUi;
  if (S.cardTags === true) S.cardTags = 'compact'; else if (S.cardTags === false) S.cardTags = 'off';

  const THEMES = {
    midnight: { bg: '#141527', surface: '#1d1f35', surface2: '#272a46', border: '#34385f', text: '#e9eaf7', muted: '#9a9dc8', shadow: 'rgba(0,0,0,.5)', scheme: 'dark' },
    graphite: { bg: '#151517', surface: '#1e1e21', surface2: '#29292d', border: '#37373d', text: '#ededf0', muted: '#9c9ca6', shadow: 'rgba(0,0,0,.5)', scheme: 'dark' },
    oled:     { bg: '#000000', surface: '#0d0d0f', surface2: '#19191c', border: '#29292e', text: '#f3f3f6', muted: '#8d8d97', shadow: 'rgba(0,0,0,.7)', scheme: 'dark' },
    paper:    { bg: '#f3f3f6', surface: '#ffffff', surface2: '#e9e9f0', border: '#d6d6e0', text: '#1a1b26', muted: '#62647a', shadow: 'rgba(20,20,40,.14)', scheme: 'light' },
  };
  const THEME_NAMES = [['midnight', 'Midnight'], ['graphite', 'Graphite'], ['oled', 'Black'], ['paper', 'Light']];
  const ACCENTS = { indigo: '#6d72d8', violet: '#9a5fe0', rose: '#df5279', amber: '#d48d1c', teal: '#1fa392', sky: '#2e8ddf' };
  const CARD_W = { s: 160, m: 200, l: 250 };

  const W = unsafeWindow;
  const isReader = () => location.pathname.startsWith('/reader/');
  const readerGalleryId = () => (location.pathname.match(/\/reader\/(\d+)\.html/) || [])[1];
  const GALLERY_RE = /^\/(?:manga|doujinshi|gamecg|cg|imageset|anime|galleries)\/(?:.*-)?(\d+)\.html$/;
  const pageGalleryId = (location.pathname.match(GALLERY_RE) || [])[1];
  const readerUrl = id => `/reader/${id}.html#1`;
  const el = (tag, props = {}, ...kids) => { const n = Object.assign(document.createElement(tag), props); n.append(...kids); return n; };
  const ready = fn => document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', fn) : fn();
  const onRemote = (key, fn) => {
    if (typeof GM_addValueChangeListener === 'function') GM_addValueChangeListener(key, (k, o, n, remote) => { if (remote) fn(n); });
  };

  function openTab(url) {
    const href = new URL(url, location.href).href;
    handoff(href);
    GM_openInTab(href, { active: S.newTab && S.newTabFocus, insert: true });
  }

  const curPage = () => {
    const n = parseInt(location.hash.slice(1), 10);
    return Number.isFinite(n) && n > 0 ? n : 1;
  };

  function pageCount() {
    const n = W.galleryinfo?.files?.length;
    if (n) return n;
    let max = 0;
    document.querySelectorAll('select').forEach(s => {
      for (const o of s.options) {
        const v = parseInt((o.value || o.textContent).replace(/\D+/g, ''), 10);
        if (Number.isFinite(v)) max = Math.max(max, v);
      }
    });
    return max || Infinity;
  }

  function goToPage(n) {
    n = Math.max(1, Math.min(n, pageCount()));
    if (n === curPage()) return;
    location.replace('#' + n);
  }

  function goBack() {
    const ref = document.referrer;
    let sameSite = false;
    try { sameSite = ref && new URL(ref).hostname === location.hostname && !ref.includes('/reader/'); } catch {}
    if (sameSite && history.length > 1) history.back();
    else location.href = document.querySelector('.navbar a.brand')?.href || `/galleries/${readerGalleryId()}.html`;
  }

  window.addEventListener('keydown', e => {
    if (!isReader()) return;
    if (e.target instanceof Element && e.target.closest('input, textarea, select, [contenteditable], dialog, #hr-settings-host')) return;
    if (e.altKey || e.ctrlKey || e.metaKey) return;
    if (menu || dlg?.open || document.querySelector('dialog[open]')) return;

    let target = null;
    const k = e.key.toLowerCase();
    if (k === 'arrowright' || k === 'd') target = curPage() + (S.rightArrowIsNext ? 1 : -1);
    else if (k === 'arrowleft' || k === 'a') target = curPage() + (S.rightArrowIsNext ? -1 : 1);
    else if (k === 'home') target = 1;
    else if (k === 'end') target = pageCount();
    else if (k === 't') { stripHidden = !stripHidden; applyLayout(); }
    else if (k === '=' || k === '+') setZoom(S.zoom + 5);
    else if (k === '-') setZoom(S.zoom - 5);
    else if (k === '0') setZoom(100);
    else if (k === 'f') toggleFullscreen();
    else if (k === 'w' && wantReaderUi()) toggleFit();
    else if (k === 'escape' && S.escBack) goBack();
    else return;

    e.preventDefault(); e.stopImmediatePropagation();
    if (target !== null) goToPage(target);
  }, true);

  document.addEventListener('click', e => {
    if (!isReader() || e.button !== 0) return;
    const a = e.target instanceof Element && e.target.closest('a[href^="#"]');
    if (!a) return;
    const n = parseInt(a.getAttribute('href').slice(1), 10);
    if (!Number.isFinite(n)) return;
    e.preventDefault(); e.stopImmediatePropagation();
    goToPage(n);
  }, true);

  window.addEventListener('click', e => {
    if (!isReader() || !S.clickHalves || e.button !== 0) return;
    const box = e.target instanceof Element && e.target.closest('#comicImages');
    if (!box || getSelection().toString()) return;
    e.preventDefault(); e.stopImmediatePropagation();
    const r = box.getBoundingClientRect();
    const rightHalf = e.clientX >= r.left + r.width / 2;
    goToPage(curPage() + (rightHalf ? 1 : -1));
  }, true);

  const PAGE_PATCH = `(() => {
    const fix = (id, two) => {
      const sel = document.getElementById(id);
      if (!sel || typeof sel.onchange !== 'function') return;
      const orig = sel.onchange;
      sel.onchange = function (e) {
        try {
          const val = String(this.value);
          const parts = val.split('-');
          const first = parseInt(parts[0], 10);
          const last = two && parts[1] ? parseInt(parts[1], 10) : first;
          const total = our_galleryinfo.length;
          enable($('#prevPanel')); enable($('#nextPanel'));
          if (first <= 1) disable($('#prevPanel'));
          if (last >= total) disable($('#nextPanel'));
          if (!two) {
            enable($('#prevScene')); enable($('#nextScene'));
            if (at_first_scene()) disable($('#prevScene'));
            if (at_last_scene()) disable($('#nextScene'));
          }
          curPanel = two ? String(first) : val;
          location.replace('#' + val);
          $(this).trigger('blur');
        } catch (err) {
          console.warn('[hitomi-revamp] falling back to the site page handler', err);
          return orig.call(this, e);
        }
      };
    };
    fix('single-page-select', false);
    fix('two-page-select', true);
  })();`;

  function patchReader() {
    const s = document.createElement('script');
    s.textContent = PAGE_PATCH;
    document.documentElement.append(s);
    s.remove();

    if (!S.rightArrowIsNext) return;
    const swap = (a, b) => {
      const A = document.getElementById(a), B = document.getElementById(b);
      if (!A || !B) return;
      A.id = b; B.id = a;
      return [A, B];
    };
    const p = swap('nextPanel', 'prevPanel');
    if (p) {
      p[0].innerHTML = '<i class="icon-chevron-left icon-white"></i> Prev';
      p[0].title = 'Left arrow';
      p[1].innerHTML = 'Next <i class="icon-chevron-right icon-white"></i>';
      p[1].title = 'Right arrow';
    }
    swap('nextScene', 'prevScene');
  }

  function setZoom(v) {
    S.zoom = Math.max(10, Math.min(300, Math.round(+v || 100)));
    saveSettings();
    document.documentElement.style.setProperty('--hr-zoom', S.zoom / 100);
    syncZoomBox();
  }
  function syncZoomBox() {
    const box = document.getElementById('hr-zoom');
    if (box && document.activeElement !== box) box.value = S.zoom;
    const out = document.getElementById('hr-zoom-out');
    if (out) out.textContent = S.zoom + '%';
  }

  function addZoomBox(ul) {
    const input = el('input', { id: 'hr-zoom', type: 'number', min: 10, max: 300, step: 5, value: S.zoom, title: 'Image size, % of the screen (+ / - / 0)' });
    input.addEventListener('keydown', e => {
      e.stopPropagation();
      if (e.key === 'Enter' || e.key === 'Escape') input.blur();
    });
    input.addEventListener('input', () => { if (input.value) setZoom(input.value); });
    input.addEventListener('change', () => { setZoom(input.value); input.value = S.zoom; });
    ul.append(el('li', { className: 'hr-nav-li' }, el('label', { htmlFor: 'hr-zoom' }, 'Size ', input, ' %')));
  }

  function pageBoxEl(word) {
    const input = el('input', { id: 'hr-page', type: 'number', min: 1, step: 1, value: curPage(), title: 'Type a page number and press Enter' });
    input.setAttribute('aria-label', 'Page');
    const total = el('span', { id: 'hr-total', textContent: '…' });
    input.addEventListener('keydown', e => {
      e.stopPropagation();
      if (e.key === 'Enter') { goToPage(+input.value || 1); input.blur(); }
      else if (e.key === 'Escape') input.blur();
    });
    input.addEventListener('change', () => goToPage(+input.value || 1));
    input.addEventListener('blur', updatePageBox);
    window.addEventListener('hashchange', updatePageBox);
    waitFor(() => W.galleryinfo?.files?.length, 15000).then(updatePageBox);
    return word
      ? el('label', { htmlFor: 'hr-page' }, 'Page ', input, ' / ', total)
      : el('label', { htmlFor: 'hr-page', className: 'hr-pagebox' }, input, el('span', { textContent: '/' }), total);
  }

  function addPageBox(ul) {
    ul.append(el('li', { className: 'hr-nav-li' }, pageBoxEl(true)));
    updatePageBox();
  }

  function updatePageBox() {
    const input = document.getElementById('hr-page');
    const total = document.getElementById('hr-total');
    const n = pageCount();
    const bar = document.querySelector('#hr-progress i');
    if (bar) bar.style.width = Number.isFinite(n) ? Math.min(100, curPage() / n * 100) + '%' : '0';
    if (!input) return;
    if (Number.isFinite(n)) { total.textContent = n; input.max = n; }
    if (document.activeElement !== input) input.value = curPage();
  }

  function galleryIdFromLink(a) {
    try {
      const u = new URL(a.dataset.hrInfo || a.href);
      if (u.hostname !== location.hostname) return null;
      return (u.pathname.match(GALLERY_RE) || [])[1] || null;
    } catch { return null; }
  }

  function rewrite(a) {
    if (!S.openInReader || isReader() || a.dataset.hrInfo) return;
    const id = galleryIdFromLink(a);
    if (!id || id === pageGalleryId) return;
    a.dataset.hrInfo = a.href;
    a.href = readerUrl(id);
  }
  for (const ev of ['pointerover', 'focusin', 'touchstart']) {
    document.addEventListener(ev, e => {
      const a = e.target instanceof Element && e.target.closest('a[href]');
      if (a) rewrite(a);
    }, { capture: true, passive: true });
  }

  document.addEventListener('click', e => {
    if (isReader() || !S.newTab || e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
    const a = e.target instanceof Element && e.target.closest('a[href]');
    if (!a) return;
    const id = galleryIdFromLink(a);
    if (!id || id === pageGalleryId) return;
    rewrite(a);
    e.preventDefault(); e.stopImmediatePropagation();
    openTab(a.href);
  }, true);

  document.addEventListener('mousedown', e => {
    const newTabGesture = e.button === 1 || e.button === 2 || (e.button === 0 && (e.ctrlKey || e.metaKey || e.shiftKey));
    if (!newTabGesture) return;
    const a = e.target instanceof Element && e.target.closest('a[href]');
    if (!a) return;
    rewrite(a);
    handoff(a.href);
  }, true);

  const TERM_RE = /^\/(tag|artist|series|character|group|type)\/(.+?)-(all|[a-z]+)(?:-\d+)?\.html$/;
  function termFromUrl(u) {
    if (u.hostname !== location.hostname) return null;
    let m = u.pathname.match(TERM_RE);
    if (m) {
      const ns = m[1];
      let val;
      try { val = decodeURIComponent(m[2]); } catch { return null; }
      val = val.trim().replace(/\s+/g, '_');
      if (ns === 'tag') return /^(fe)?male:/.test(val) ? val : 'tag:' + val;
      return `${ns}:${val}`;
    }
    m = u.pathname.match(/^\/index-([a-z]+)(?:-\d+)?\.html$/);
    if (m && m[1] !== 'all') return 'language:' + m[1];
    return null;
  }
  function termFromLink(a) {
    try { return termFromUrl(new URL(a.href)); } catch { return null; }
  }

  function normTerm(s) {
    let t = String(s).trim().toLowerCase().replace(/^-/, '').replace(/\s+/g, '_');
    if (!t) return '';
    if (!/^(tag|female|male|artist|series|character|group|type|language):./.test(t)) t = 'tag:' + t;
    return t;
  }

  const QKEY = 'hr-query';
  const HKEY = 'hr-handoff';
  const HANDOFF_MS = 60000;
  const hasQuery = () => { try { return sessionStorage.getItem(QKEY) !== null; } catch { return false; } };
  const load = () => { try { return JSON.parse(sessionStorage.getItem(QKEY)) || []; } catch { return []; } };
  const store = t => { try { sessionStorage.setItem(QKEY, JSON.stringify(t)); } catch {} };
  const save = t => { store(t); renderPanel(); };
  const pathKey = u => u.pathname + u.search;

  function handoff(url) {
    const terms = load();
    if (!terms.length) return;
    let u;
    try { u = new URL(url, location.href); } catch { return; }
    if (u.hostname !== location.hostname) return;
    const now = Date.now();
    const map = {};
    for (const [k, v] of Object.entries(GM_getValue(HKEY, {}))) if (now - v.t < HANDOFF_MS) map[k] = v;
    map[pathKey(u)] = { terms, t: now };
    GM_setValue(HKEY, map);
  }

  if (!hasQuery()) {
    const h = GM_getValue(HKEY, {})[pathKey(location)];
    if (h && Date.now() - h.t < HANDOFF_MS) store(h.terms);
  }

  function pageQuery() {
    if (location.pathname === '/search.html') {
      if (location.search.length < 2) return null;
      try {
        const q = decodeURIComponent(location.search.slice(1)).split(/\s+/).filter(Boolean);
        return q.length ? q : null;
      } catch { return null; }
    }
    const m = location.pathname.match(TERM_RE);
    if (m) {
      const t = termFromUrl(new URL(location.href));
      if (!t) return null;
      return m[3] !== 'all' ? [t, 'language:' + m[3]] : [t];
    }
    const lm = location.pathname.match(/^\/index-([a-z]+)(?:-\d+)?\.html$/);
    if (lm && lm[1] !== 'all') return ['language:' + lm[1]];
    return null;
  }
  {
    const pq = pageQuery();
    if (pq) store(pq);
  }

  const bare = t => t.replace(/^-/, '');
  function addTerm(term, exclude) {
    const t = load().filter(x => bare(x) !== term);
    t.push(exclude ? '-' + term : term);
    save(t);
  }
  const searchUrl = terms => '/search.html?' + encodeURIComponent(terms.join(' '));

  const BL_KEY = 'hr-blacklist';
  let blacklist = GM_getValue(BL_KEY, []);
  function setBlacklist(list) {
    blacklist = [...new Set(list.map(normTerm).filter(Boolean))].sort();
    GM_setValue(BL_KEY, blacklist);
    blacklistChanged();
  }
  const blAdd = t => setBlacklist([...blacklist, t]);
  const blRemove = t => setBlacklist(blacklist.filter(x => x !== t));
  const blacklistChanged = () => { scanCards(); renderBlacklist(); };

  onRemote(BL_KEY, n => { blacklist = n || []; blacklistChanged(); });
  onRemote(SKEY, n => { Object.assign(S, DEFAULTS, n); applyTheme(); applyLayout(); syncUi(); scanCards(); });

  const CARD_SEL = '.gallery-content > div';
  function cardHit(card, bl) {
    if (!bl.size) return '';
    for (const a of card.querySelectorAll('a[href]')) {
      const t = termFromLink(a);
      if (t && bl.has(normTerm(t))) return normTerm(t);
    }
    return '';
  }
  let blCount = 0;
  function scanCards() {
    if (isReader() || !document.body) return;
    const bl = new Set(blacklist);
    let n = 0;
    for (const card of document.querySelectorAll(CARD_SEL)) {
      const hit = cardHit(card, bl);
      card.classList.toggle('hr-bl-hide', !!hit && S.blMode === 'hide');
      card.classList.toggle('hr-bl-dim', !!hit && S.blMode === 'dim');
      if (hit) { card.dataset.hrBl = hit; n++; }
      else if (card.dataset.hrBl) delete card.dataset.hrBl;
    }
    blCount = n;
    updateBadge();
    renderApp();
  }

  let scanTimer = 0;
  const scheduleScan = () => { if (!scanTimer) scanTimer = setTimeout(() => { scanTimer = 0; scanCards(); }, 60); };

  function updateBadge() {
    const fab = document.getElementById('hr-fab');
    const b = fab?.querySelector('.hr-badge');
    if (!b) return;
    const txt = String(blCount);
    if (b.textContent !== txt) b.textContent = txt;
    b.hidden = !blCount;
    fab.title = blCount
      ? `Hitomi Revamp settings (${blCount} blacklisted ${blCount === 1 ? 'gallery' : 'galleries'} ${S.blMode === 'hide' ? 'hidden' : 'dimmed'} here)`
      : 'Hitomi Revamp settings';
  }

  GM_addStyle(`
    :root {
      --hr-size: ${S.thumbSize}px;
      --hr-font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
      --hr-accent-soft: color-mix(in srgb, var(--hr-accent) 22%, transparent);
      --hr-neg: #d6476b;
      --hr-neg-soft: color-mix(in srgb, #d6476b 22%, transparent);
      --hr-hover: color-mix(in srgb, var(--hr-text) 10%, transparent);
    }

    #hr-menu { position: fixed; z-index: 2147483647; min-width: 250px; max-width: 320px; padding: 6px; background: var(--hr-surface);
      border: 1px solid var(--hr-border); border-radius: 10px; box-shadow: 0 14px 36px var(--hr-shadow);
      color: var(--hr-text); font: 14px/1.4 var(--hr-font); color-scheme: var(--hr-scheme); }
    #hr-menu .hr-mh { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; padding: 6px 8px 10px; }
    #hr-menu .hr-mh .hr-ns { min-width: 0; text-transform: capitalize; }
    #hr-menu .hr-mh strong { flex: 1; min-width: 0; font-size: 14px; font-weight: 650; overflow-wrap: anywhere;
      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
    #hr-menu .hr-mh small { flex-basis: 100%; color: var(--hr-muted); font-size: 12px; }
    #hr-menu .hr-mi { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; background: none; border: 0;
      color: inherit; padding: 7px 8px; border-radius: 8px; cursor: pointer; font: inherit; }
    #hr-menu .hr-mi:hover, #hr-menu .hr-mi:focus-visible { background: var(--hr-surface2); outline: none; }
    #hr-menu .hr-mi-ic { flex: none; display: grid; place-items: center; width: 18px; color: var(--hr-muted); }
    #hr-menu .hr-mi-txt { display: flex; flex-direction: column; min-width: 0; }
    #hr-menu .hr-mi small { color: var(--hr-muted); font-size: 12px; line-height: 1.3; }
    #hr-menu .hr-mi.danger, #hr-menu .hr-mi.danger .hr-mi-ic { color: var(--hr-neg); }
    #hr-menu hr { border: 0; border-top: 1px solid var(--hr-border); margin: 5px 4px; }

    .hr-chip { display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 3px 0 11px; border-radius: 999px;
      background: var(--hr-accent-soft); color: var(--hr-text); font: 13px/1 var(--hr-font); cursor: pointer;
      white-space: nowrap; user-select: none; }
    .hr-chip.neg { background: var(--hr-neg-soft); }
    .hr-chip-not { color: var(--hr-neg); font-weight: 600; font-size: 11px; }
    .hr-chip button { display: grid; place-items: center; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 50%;
      background: none; color: var(--hr-muted); cursor: pointer; font: 15px/1 var(--hr-font); }
    .hr-chip button:hover { background: var(--hr-hover); color: var(--hr-text); }
    .hr-btn { height: 32px; padding: 0 14px; border-radius: 9px; border: 1px solid var(--hr-border); background: transparent;
      color: var(--hr-text); font: 600 13px/1 var(--hr-font); cursor: pointer; white-space: nowrap; }
    .hr-btn:hover { background: var(--hr-hover); }
    .hr-btn.primary { background: var(--hr-accent); border-color: var(--hr-accent); color: #fff; }
    .hr-btn.primary:hover { background: color-mix(in srgb, var(--hr-accent) 85%, #fff); }
    .hr-chip button:focus-visible, .hr-btn:focus-visible, .hr-icon:focus-visible, .hr-pg:focus-visible,
    #hr-app a:focus-visible, .hr-select:focus-visible { outline: 2px solid var(--hr-accent); outline-offset: 2px; }

    #hr-panel { position: fixed; z-index: 2147483646; right: 16px; bottom: 16px; max-width: min(560px, calc(100vw - 96px));
      display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 8px;
      background: color-mix(in srgb, var(--hr-surface) 88%, transparent); backdrop-filter: blur(14px);
      border: 1px solid var(--hr-border); border-radius: 16px; box-shadow: 0 12px 32px var(--hr-shadow);
      color: var(--hr-text); font: 14px/1.4 var(--hr-font); color-scheme: var(--hr-scheme); }
    #hr-panel[hidden], html.hr-ui #hr-panel { display: none !important; }
    #hr-panel .hr-acts { display: flex; gap: 6px; margin-left: auto; }

    #hr-fab { position: fixed; z-index: 2147483646; left: 16px; bottom: 16px; width: 44px; height: 44px; padding: 0;
      border: 0; border-radius: 12px; background: none; cursor: pointer; opacity: .85;
      box-shadow: 0 6px 18px var(--hr-shadow); transition: opacity .15s; }
    #hr-fab:hover, #hr-fab:focus-visible { opacity: 1; }
    #hr-fab:focus-visible { outline: 2px solid var(--hr-accent); outline-offset: 2px; }
    #hr-fab img { display: block; width: 100%; height: 100%; border-radius: 12px; }
    #hr-fab .hr-badge { position: absolute; top: -6px; right: -6px; min-width: 18px; height: 18px; padding: 0 5px;
      box-sizing: border-box; border-radius: 9px; background: var(--hr-neg); color: #fff;
      font: 600 11px/18px system-ui, sans-serif; text-align: center; }
    #hr-fab .hr-badge[hidden], html.hr-ui #hr-fab { display: none; }

    .hr-bl-hide { display: none !important; }
    .hr-bl-dim { position: relative; opacity: .3; transition: opacity .15s; }
    .hr-bl-dim img { filter: blur(10px) grayscale(1); }
    .hr-bl-dim:hover { opacity: 1; }
    .hr-bl-dim:hover img { filter: none; }
    .hr-bl-dim::before { content: 'Blacklisted: ' attr(data-hr-bl); position: absolute; z-index: 2; left: 6px; top: 6px;
      background: var(--hr-neg); color: #fff; font: 12px/1.4 system-ui, sans-serif; padding: 2px 6px; border-radius: 4px; pointer-events: none; }

    html.hr-ui, html.hr-ui body { background: var(--hr-bg) !important; }
    html.hr-ui body { margin: 0 !important; padding: 0 !important; min-width: 0 !important; }
    html.hr-ui body > :not([id^="hr-"]) { display: none !important; }
    #hr-app { min-height: 100vh; color: var(--hr-text); font: 14px/1.45 var(--hr-font); color-scheme: var(--hr-scheme);
      -webkit-font-smoothing: antialiased; }
    #hr-app *, #hr-app *::before, #hr-app *::after { box-sizing: border-box; }
    #hr-app a { color: inherit; text-decoration: none; }

    .hr-top { position: sticky; top: 0; z-index: 100; display: flex; align-items: center; gap: 10px;
      padding: 10px clamp(12px, 3vw, 32px); background: color-mix(in srgb, var(--hr-bg) 82%, transparent);
      backdrop-filter: blur(16px); border-bottom: 1px solid var(--hr-border); }
    .hr-brand { display: flex; align-items: center; gap: 9px; flex: none; font-weight: 700; font-size: 15px; letter-spacing: -.01em; }
    .hr-brand img { width: 30px; height: 30px; border-radius: 8px; display: block; }
    .hr-sbox { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 5px; min-height: 42px;
      padding: 6px 8px 6px 12px; background: var(--hr-surface); border: 1px solid var(--hr-border); border-radius: 12px; cursor: text; }
    .hr-sbox:focus-within { border-color: var(--hr-accent); box-shadow: 0 0 0 3px var(--hr-accent-soft); }
    .hr-sbox > svg { flex: none; color: var(--hr-muted); margin-right: 2px; }
    .hr-chips { display: contents; }
    .hr-sbox input { flex: 1; min-width: 150px; height: 28px; border: 0; outline: none; background: none; color: inherit;
      font: inherit; padding: 0 4px; }
    .hr-sbox input::placeholder { color: var(--hr-muted); }
    .hr-select { height: 42px; padding: 0 32px 0 12px; border-radius: 12px; border: 1px solid var(--hr-border);
      background: var(--hr-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238e90b0' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 12px center;
      color: inherit; font: inherit; appearance: none; -webkit-appearance: none; cursor: pointer; }
    .hr-select option { background: var(--hr-surface); color: var(--hr-text); }
    .hr-top .hr-btn { height: 42px; border-radius: 12px; padding: 0 18px; }
    .hr-icon { flex: none; display: grid; place-items: center; width: 42px; height: 42px; padding: 0; border-radius: 12px;
      border: 1px solid var(--hr-border); background: var(--hr-surface); color: var(--hr-muted); cursor: pointer; }
    .hr-icon:hover { color: var(--hr-text); }

    .hr-main { max-width: 1680px; margin: 0 auto; padding: 28px clamp(12px, 3vw, 32px) 64px; }
    .hr-headline { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 12px; margin-bottom: 20px; }
    .hr-headline h1 { margin: 0; font-size: 28px; font-weight: 750; letter-spacing: -.02em; line-height: 1.15; overflow-wrap: anywhere; }
    .hr-kind { color: var(--hr-muted); font-size: 14px; }
    .hr-stat { margin-left: auto; color: var(--hr-muted); font-size: 13px; }

    .hr-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--hr-card), 1fr)); gap: 26px 18px; }
    .hr-card { position: relative; min-width: 0; display: flex; flex-direction: column; gap: 7px; }
    .hr-cover { position: relative; display: block; aspect-ratio: 5 / 7; overflow: hidden; border-radius: 10px;
      background: var(--hr-surface2); }
    .hr-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
    .hr-cover-link { position: absolute; inset: 0; display: block; border-radius: inherit; }
    .hr-cover:has(.hr-cover-link:hover) { box-shadow: 0 0 0 2px var(--hr-accent); }
    .hr-pill { position: absolute; bottom: 8px; padding: 3px 7px; border-radius: 6px; background: rgba(10,10,16,.68);
      color: #fff; font: 600 11px/1.2 var(--hr-font); backdrop-filter: blur(6px); text-transform: capitalize; max-width: 60%;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .hr-pill.type { left: 8px; }
    #hr-app a.hr-pill:hover { background: var(--hr-accent); color: #fff; }
    .hr-pill.lang { right: 8px; }
    .hr-title { font-weight: 600; font-size: 14px; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2;
      -webkit-box-orient: vertical; overflow: hidden; margin-top: 3px; }
    .hr-title:hover { color: var(--hr-accent); }
    .hr-sub { margin-top: -3px; color: var(--hr-muted); font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .hr-sub a:hover { color: var(--hr-text); }
    .hr-tags { display: flex; flex-wrap: wrap; gap: 4px; }
    .hr-tag { padding: 4px 7px; border-radius: 6px; background: var(--hr-surface2); color: var(--hr-muted);
      font-size: 11.5px; line-height: 1; white-space: nowrap; }
    .hr-tag.f { background: color-mix(in srgb, #df5279 15%, transparent); color: color-mix(in srgb, #df5279 65%, var(--hr-text)); }
    .hr-tag.m { background: color-mix(in srgb, #2e8ddf 15%, transparent); color: color-mix(in srgb, #2e8ddf 65%, var(--hr-text)); }
    .hr-tag:hover { background: var(--hr-hover); color: var(--hr-text); }
    .hr-card.is-dim { opacity: .5; }
    .hr-card.is-dim img { filter: blur(14px) grayscale(1); }
    .hr-card.is-dim:hover { opacity: 1; }
    .hr-card.is-dim:hover img { filter: none; }
    .hr-card.is-dim .hr-cover::after { content: 'Blacklisted: ' attr(data-bl); position: absolute; left: 8px; right: 8px; top: 8px;
      padding: 3px 7px; border-radius: 6px; background: var(--hr-neg); color: #fff; font-size: 11px; font-weight: 600;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .hr-skel .hr-cover { animation: hr-pulse 1.4s ease-in-out infinite; }
    .hr-skel i { display: block; height: 12px; border-radius: 6px; background: var(--hr-surface2); }
    .hr-skel i + i { width: 60%; }
    @keyframes hr-pulse { 50% { opacity: .55; } }
    .hr-empty { grid-column: 1 / -1; padding: 96px 0; text-align: center; color: var(--hr-muted); font-size: 15px; }

    .hr-pager { display: flex; justify-content: center; flex-wrap: wrap; gap: 6px; margin-top: 44px; }
    .hr-pg { display: grid; place-items: center; min-width: 40px; height: 40px; padding: 0 12px; border-radius: 10px;
      border: 1px solid var(--hr-border); background: var(--hr-surface); color: var(--hr-text); font: 600 13px/1 var(--hr-font); }
    a.hr-pg:hover { border-color: var(--hr-accent); }
    .hr-pg.cur { background: var(--hr-accent); border-color: var(--hr-accent); color: #fff; }
    .hr-pg.gap { border-color: transparent; background: none; color: var(--hr-muted); }

    @media (max-width: 900px) { .hr-brand span { display: none; } }
    @media (max-width: 720px) {
      .hr-top { flex-wrap: wrap; }
      .hr-sbox { order: 10; flex-basis: 100%; }
      .hr-brand { margin-right: auto; }
      .hr-headline h1 { font-size: 22px; }
      .hr-grid { grid-template-columns: repeat(auto-fill, minmax(min(var(--hr-card), 42vw), 1fr)); gap: 20px 12px; }
    }
    @media (prefers-reduced-motion: reduce) { .hr-skel .hr-cover { animation: none; } #hr-fab { transition: none; } }

    #hr-strip { position: fixed; z-index: 2147483645; display: flex; gap: 6px; padding: 8px; box-sizing: border-box;
      background: color-mix(in srgb, var(--hr-bg) 94%, transparent); scrollbar-width: thin;
      scrollbar-color: var(--hr-border) transparent; font: 14px/1.4 var(--hr-font); color: var(--hr-text); }
    #hr-strip[hidden] { display: none; }
    html.hr-pos-bottom #hr-strip { left: 0; right: 0; bottom: 0; height: var(--hr-striph);
      flex-direction: row; overflow-x: auto; overflow-y: hidden; align-items: center; border-top: 1px solid var(--hr-border); }
    html.hr-pos-left #hr-strip, html.hr-pos-right #hr-strip { top: var(--hr-nav); bottom: 0; width: var(--hr-stripw);
      flex-direction: column; overflow-y: auto; overflow-x: hidden; align-items: center; }
    html.hr-pos-left #hr-strip { left: 0; border-right: 1px solid var(--hr-border); }
    html.hr-pos-right #hr-strip { right: 0; border-left: 1px solid var(--hr-border); }

    html.hr-reader { --hr-nav: 41px; --hr-striph: 0px; --hr-stripw: 0px; }
    html.hr-pos-bottom { --hr-striph: calc(var(--hr-size) + 26px); }
    html.hr-pos-left, html.hr-pos-right { --hr-stripw: calc(var(--hr-size) + 26px); }
    html.hr-reader #comicImages { height: calc(100vh - var(--hr-nav) - var(--hr-striph)) !important; box-sizing: border-box; }
    html.hr-pos-left #comicImages { margin-left: var(--hr-stripw); }
    html.hr-pos-right #comicImages { margin-right: var(--hr-stripw); }
    html.hr-reader #comicImages.fitVertical img { max-height: calc(100% * var(--hr-zoom)) !important; }
    html.hr-reader .spread1 #comicImages.fitHorizontal img { width: calc(100% * var(--hr-zoom)) !important; }
    html.hr-reader .spread2 #comicImages.fitHorizontal img { width: calc(50% * var(--hr-zoom)) !important; }
    html.hr-reader #comicImages:fullscreen { height: 100vh !important; margin: 0 !important; }
    html.hr-reader #comicImages:fullscreen.fitVertical img { max-height: calc(100vh * var(--hr-zoom)) !important; }

    .hr-nav-li label { display: block; margin: 0; padding: 10px 12px; color: #999; font-size: 14px; line-height: 20px; font-weight: normal; }
    #hr-zoom, #hr-page { width: 52px; height: 20px; margin: 0 2px; padding: 0 4px; background: #2b2b2b; color: #ddd;
      border: 1px solid #444; border-radius: 3px; font-size: 14px; vertical-align: baseline; box-sizing: content-box; }
    #hr-total { color: #ddd; }
    #hr-navgear { display: block; background: none; border: 0; padding: 10px 12px; cursor: pointer; }
    #hr-navgear img { display: block; width: 20px; height: 20px; border-radius: 5px; }
    #hr-navgear:focus-visible { outline: 2px solid #fff; outline-offset: -2px; }

    .hr-tile { flex: 0 0 auto; position: relative; padding: 0; border: 2px solid transparent; border-radius: 6px;
      background: var(--hr-surface2); cursor: pointer; overflow: hidden; }
    html.hr-pos-bottom .hr-tile { height: var(--hr-size);
      width: clamp(calc(var(--hr-size) * .45), calc(var(--hr-size) * var(--r)), calc(var(--hr-size) * 2)); }
    html.hr-pos-left .hr-tile, html.hr-pos-right .hr-tile { width: var(--hr-size);
      height: clamp(calc(var(--hr-size) * .45), calc(var(--hr-size) / var(--r)), calc(var(--hr-size) * 2)); }
    .hr-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .hr-tile:not([data-state="done"]) img { visibility: hidden; }
    .hr-tile span { position: absolute; left: 3px; bottom: 3px; font-size: 12px; color: #fff;
      background: rgba(0,0,0,.65); padding: 0 5px; border-radius: 4px; }
    .hr-tile.cur { border-color: var(--hr-accent); }
    .hr-tile:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
  `);

  let menu = null;
  const closeMenu = () => { menu?.remove(); menu = null; };

  function openMenu(x, y, head, items) {
    closeMenu();
    menu = el('div', { id: 'hr-menu' }, head);
    menu.setAttribute('role', 'menu');
    for (const it of items) {
      if (it === '-') { menu.append(el('hr')); continue; }
      const b = el('button', { type: 'button', className: 'hr-mi' + (it.danger ? ' danger' : '') },
        el('span', { className: 'hr-mi-ic' }, icon(it.icon, 16)),
        el('span', { className: 'hr-mi-txt' }, el('span', { textContent: it.label }), ...(it.hint ? [el('small', { textContent: it.hint })] : [])));
      b.setAttribute('role', 'menuitem');
      b.onclick = () => { closeMenu(); it.fn(); };
      menu.append(b);
    }
    menu.addEventListener('keydown', e => {
      if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
      e.preventDefault();
      const bs = [...menu.querySelectorAll('button')];
      const i = bs.indexOf(document.activeElement);
      bs[(i + (e.key === 'ArrowDown' ? 1 : -1) + bs.length) % bs.length].focus();
    });
    document.body.append(menu);
    const r = menu.getBoundingClientRect();
    menu.style.left = Math.max(8, Math.min(x, innerWidth - r.width - 8)) + 'px';
    menu.style.top = Math.max(8, Math.min(y, innerHeight - r.height - 8)) + 'px';
    menu.querySelector('button').focus();
  }

  const menuHead = (ns, name, note) => el('div', { className: 'hr-mh' },
    el('span', { className: 'hr-ns ' + nsClass(ns), textContent: ns }),
    el('strong', { textContent: name, title: name }),
    ...(note ? [el('small', { textContent: note })] : []));

  document.addEventListener('contextmenu', e => {
    if (e.shiftKey) return;
    const a = e.target instanceof Element && e.target.closest('a[href]');
    if (!a || isReader()) return;

    const gid = galleryIdFromLink(a);
    if (gid && gid !== pageGalleryId) {
      e.preventDefault();
      const info = a.dataset.hrInfo || a.href;
      const name = a.closest('.hr-card')?.querySelector('.hr-title')?.textContent || a.textContent.trim() || `Gallery ${gid}`;
      openMenu(e.clientX, e.clientY, menuHead('gallery', name), [
        { label: 'Read', icon: 'book', fn: () => { location.href = readerUrl(gid); } },
        { label: 'Read in new tab', icon: 'external', fn: () => openTab(readerUrl(gid)) },
        '-',
        { label: 'Gallery page', icon: 'info', hint: 'Details, tags and every page', fn: () => { location.href = info; } },
        { label: 'Gallery page in new tab', icon: 'external', fn: () => openTab(info) },
      ]);
      return;
    }

    const term = termFromLink(a);
    if (!term) return;
    e.preventDefault();
    const key = normTerm(term);
    const listed = blacklist.includes(key);
    const cur = load().find(x => bare(x) === term);
    const ns = term.split(':')[0];
    const name = term.slice(ns.length + 1).replace(/_/g, ' ');
    const note = [cur ? (cur.startsWith('-') ? 'Excluded from this tab’s search' : 'In this tab’s search') : '', listed ? 'Blacklisted' : ''].filter(Boolean).join('. ');
    const removeTerm = () => save(load().filter(x => bare(x) !== term));

    const searchItems = !cur
      ? [{ label: 'Add to search', icon: 'plus', hint: 'Show galleries that have it', fn: () => addTerm(term, false) },
         { label: 'Exclude from search', icon: 'minus', hint: 'Hide galleries that have it', fn: () => addTerm(term, true) }]
      : [{ label: 'Remove from search', icon: 'x', fn: removeTerm },
         cur.startsWith('-')
           ? { label: 'Include instead', icon: 'plus', fn: () => addTerm(term, false) }
           : { label: 'Exclude instead', icon: 'minus', fn: () => addTerm(term, true) }];

    openMenu(e.clientX, e.clientY, menuHead(ns, name, note), [
      ...searchItems,
      { label: 'Search with it now', icon: 'search', hint: 'Adds it and runs the search', fn: () => { addTerm(term, false); location.href = searchUrl(load()); } },
      { label: 'Search only this', icon: 'search', hint: 'Starts a fresh search', fn: () => { location.href = searchUrl([term]); } },
      '-',
      { label: 'Open in new tab', icon: 'external', fn: () => openTab(a.href) },
      '-',
      listed
        ? { label: 'Remove from blacklist', icon: 'ban', hint: 'Show these galleries again', fn: () => blRemove(key) }
        : { label: 'Blacklist', icon: 'ban', hint: 'Hide galleries with it everywhere', danger: true, fn: () => blAdd(key) },
    ]);
  }, true);

  document.addEventListener('mousedown', e => { if (menu && !menu.contains(e.target)) closeMenu(); }, true);
  document.addEventListener('keydown', e => { if (e.key === 'Escape' && menu) { e.stopImmediatePropagation(); closeMenu(); } }, true);
  window.addEventListener('scroll', closeMenu, { passive: true });

  function applyTheme() {
    const root = document.documentElement;
    let t = THEMES[S.theme] || THEMES.midnight;
    if (isReader() && t.scheme === 'light') t = THEMES.graphite;
    for (const [k, v] of Object.entries(t)) root.style.setProperty('--hr-' + k, v);
    root.style.setProperty('--hr-accent', ACCENTS[S.accent] || ACCENTS.indigo);
    root.style.setProperty('--hr-card', (CARD_W[S.cardSize] || CARD_W.m) + 'px');
  }
  applyTheme();

  function chipEl(t, i) {
    const neg = t.startsWith('-');
    const chip = el('span', { className: 'hr-chip' + (neg ? ' neg' : ''), title: neg ? 'Excluded. Click to include' : 'Click to exclude' },
      ...(neg ? [el('span', { className: 'hr-chip-not', textContent: 'not' })] : []),
      el('span', { textContent: neg ? t.slice(1) : t }));
    chip.onclick = () => {
      const next = load();
      next[i] = next[i].startsWith('-') ? next[i].slice(1) : '-' + next[i];
      save(next);
    };
    const x = el('button', { type: 'button', textContent: '×' });
    x.setAttribute('aria-label', 'Remove ' + t);
    x.onclick = ev => { ev.stopPropagation(); const next = load(); next.splice(i, 1); save(next); };
    chip.append(x);
    return chip;
  }

  const runSearch = () => { const t = load(); location.href = t.length ? searchUrl(t) : '/'; };

  let panel = null;
  function renderPanel() {
    const terms = load();
    if (panel) {
      panel.hidden = terms.length === 0 || isReader();
      const go = el('button', { type: 'button', className: 'hr-btn primary', textContent: 'Search' });
      go.onclick = runSearch;
      const clr = el('button', { type: 'button', className: 'hr-btn', textContent: 'Clear' });
      clr.onclick = () => save([]);
      panel.replaceChildren(...terms.map(chipEl), el('span', { className: 'hr-acts' }, go, clr));
    }
    if (appRefs) {
      appRefs.chips.replaceChildren(...terms.map(chipEl));
      const flat = terms.filter(t => !t.startsWith('-'));
      const key = flat.find(t => t.startsWith('orderbykey:'));
      appRefs.sort.value = flat.includes('orderby:popular') && key ? key.slice(11) : '';
      const lg = (flat.find(t => t.startsWith('language:')) || '').slice(9);
      if (lg && ![...appRefs.lang.options].some(o => o.value === lg)) appRefs.lang.append(el('option', { value: lg, textContent: lg }));
      appRefs.lang.value = lg;
    }
  }

  GM_addStyle(`
    .hr-sbox { position: relative; }
    .hr-sugg { position: absolute; left: -1px; right: -1px; top: calc(100% + 6px); z-index: 10; padding: 5px;
      background: var(--hr-surface); border: 1px solid var(--hr-border); border-radius: 12px;
      box-shadow: 0 16px 40px var(--hr-shadow); max-height: 60vh; overflow: auto; cursor: default; }
    .hr-sugg[hidden] { display: none; }
    .hr-sugg-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; cursor: pointer; }
    .hr-sugg-item.on, .hr-sugg-item:hover { background: var(--hr-surface2); }
    .hr-ns { flex: none; min-width: 58px; text-align: center; padding: 3px 7px; border-radius: 6px; background: var(--hr-surface2);
      color: var(--hr-muted); font-size: 11px; font-weight: 600; }
    .hr-sugg-item.on .hr-ns, .hr-sugg-item:hover .hr-ns { background: var(--hr-bg); }
    .hr-ns.f, .hr-sugg-item .hr-ns.f { background: color-mix(in srgb, #df5279 18%, transparent); color: color-mix(in srgb, #df5279 70%, var(--hr-text)); }
    .hr-ns.m, .hr-sugg-item .hr-ns.m { background: color-mix(in srgb, #2e8ddf 18%, transparent); color: color-mix(in srgb, #2e8ddf 70%, var(--hr-text)); }
    .hr-sugg-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .hr-sugg-count { color: var(--hr-muted); font-size: 12px; font-variant-numeric: tabular-nums; }

    .hr-cover .hr-prev { position: absolute; inset: 0; opacity: 0; transition: opacity .15s; }
    .hr-cover .hr-prev.on { opacity: 1; }
    .hr-dots { position: absolute; left: 10px; right: 10px; top: 8px; display: flex; gap: 4px; opacity: 0; pointer-events: none; transition: opacity .15s; }
    .hr-cover.peek .hr-dots { opacity: 1; }
    .hr-dots i { flex: 1; max-width: 26px; height: 3px; border-radius: 2px; background: rgba(255,255,255,.4); box-shadow: 0 0 3px rgba(0,0,0,.4); }
    .hr-dots i.on { background: #fff; }
    @media (prefers-reduced-motion: reduce) { .hr-cover .hr-prev, .hr-dots { transition: none; } }

    .hr-tagbox { position: relative; }
    .hr-tags.open { position: absolute; z-index: 30; top: -8px; left: -8px; right: -8px; padding: 8px; max-height: 320px; overflow: auto;
      background: var(--hr-surface); border: 1px solid var(--hr-border); border-radius: 12px; box-shadow: 0 18px 44px var(--hr-shadow); }
    .hr-card.tags-open { z-index: 30; }
    .hr-tags { max-height: none; overflow: visible; }
    .hr-tag.hr-more { border: 0; cursor: pointer; font-family: inherit; font-weight: 600;
      background: var(--hr-accent-soft); color: var(--hr-text); }
    .hr-tag.hr-more:hover { background: var(--hr-accent); color: #fff; }
    #hr-app .hr-btn.primary { color: #fff; }

    .hr-hero { display: grid; grid-template-columns: minmax(200px, 300px) minmax(0, 1fr); gap: clamp(20px, 3vw, 44px); align-items: start; }
    .hr-hero-cover { position: sticky; top: 86px; display: block; aspect-ratio: 5 / 7; overflow: hidden; border-radius: 14px;
      background: var(--hr-surface2); box-shadow: 0 18px 44px var(--hr-shadow); }
    .hr-hero-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
    .hr-hero-info h1 { margin: 0; font-size: clamp(22px, 2.6vw, 32px); font-weight: 750; letter-spacing: -.02em; line-height: 1.15; overflow-wrap: anywhere; }
    .hr-jp { margin: 8px 0 0; color: var(--hr-muted); font-size: 15px; overflow-wrap: anywhere; }
    .hr-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 22px 0; }
    .hr-btn.hr-btn-lg { height: 44px; padding: 0 20px; border-radius: 12px; display: inline-flex; align-items: center; gap: 8px; font-size: 14px; }
    .hr-meta { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 10px 22px; margin: 0; padding: 18px 20px;
      background: var(--hr-surface); border: 1px solid var(--hr-border); border-radius: 14px; }
    .hr-meta dt { color: var(--hr-muted); font-size: 13px; padding-top: 1px; }
    .hr-meta dd { margin: 0; overflow-wrap: anywhere; }
    #hr-app .hr-meta a { border-bottom: 1px solid var(--hr-border); }
    #hr-app .hr-meta a:hover { color: var(--hr-accent); border-color: var(--hr-accent); }
    .hr-tagcloud { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 18px; }
    .hr-tagcloud .hr-tag { font-size: 13px; padding: 7px 10px; border-radius: 8px; }
    .hr-sec { margin-top: 52px; }
    .hr-sec[hidden] { display: none; }
    .hr-sec-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 16px; }
    .hr-sec h2 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: -.01em; }
    .hr-thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 12px; }
    .hr-thumb { position: relative; display: block; aspect-ratio: 5 / 7; overflow: hidden; border-radius: 8px; background: var(--hr-surface2); }
    .hr-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; transition: opacity .2s; }
    .hr-thumb img:not([data-state="done"]) { opacity: 0; }
    .hr-thumb span { position: absolute; left: 6px; bottom: 6px; padding: 2px 6px; border-radius: 5px; background: rgba(10,10,16,.68);
      color: #fff; font-size: 11px; font-weight: 600; }
    .hr-thumb:hover { box-shadow: 0 0 0 2px var(--hr-accent); }
    .hr-skl { display: block; height: 14px; margin: 12px 0; border-radius: 7px; background: var(--hr-surface2); animation: hr-pulse 1.4s ease-in-out infinite; }
    .hr-skl.big { height: 34px; width: 70%; }
    @media (max-width: 720px) {
      .hr-hero { grid-template-columns: 1fr; }
      .hr-hero-cover { position: static; width: min(260px, 70vw); }
    }
    @media (prefers-reduced-motion: reduce) { .hr-skl { animation: none; } .hr-thumb img { transition: none; } }

    html.hr-rv { --hr-nav: 0px; }
    html.hr-rv.hr-rv-pin { --hr-nav: 56px; }
    html.hr-rv .navbar { display: none !important; }
    html.hr-rv, html.hr-rv body { background: var(--hr-bg) !important; }
    html.hr-rv body { margin: 0 !important; padding: var(--hr-nav) 0 0 !important; }
    html.hr-rv #comicImages { background: var(--hr-bg) !important; }
    html.hr-rv.hr-idle, html.hr-rv.hr-idle * { cursor: none !important; }
    #hr-rbar { position: fixed; z-index: 2147483646; top: 0; left: 0; right: 0; height: 56px; display: flex; align-items: center;
      gap: 6px; padding: 0 10px; background: color-mix(in srgb, var(--hr-bg) 82%, transparent); backdrop-filter: blur(16px);
      border-bottom: 1px solid var(--hr-border); color: var(--hr-text); font: 14px/1.3 var(--hr-font); color-scheme: dark;
      transition: transform .2s ease, opacity .2s ease; box-sizing: border-box; }
    #hr-rbar * { box-sizing: border-box; }
    html.hr-idle #hr-rbar { transform: translateY(-100%); opacity: 0; pointer-events: none; }
    .hr-rb { flex: none; display: inline-grid; place-items: center; height: 38px; min-width: 38px; padding: 0 8px; border: 0;
      border-radius: 10px; background: none; color: var(--hr-muted); cursor: pointer; font: 600 13px/1 var(--hr-font); }
    .hr-rb:hover { background: var(--hr-hover); color: var(--hr-text); }
    .hr-rb[aria-pressed="true"] { color: var(--hr-accent); }
    .hr-rb:focus-visible, #hr-rbar input:focus-visible { outline: 2px solid var(--hr-accent); outline-offset: 1px; }
    .hr-rtitle { min-width: 0; display: flex; flex-direction: column; margin-left: 4px; }
    .hr-rtitle strong { font-size: 14px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .hr-rtitle span { color: var(--hr-muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .hr-rspacer { flex: 1; }
    .hr-rgroup { flex: none; display: flex; align-items: center; gap: 2px; padding: 2px; border-radius: 12px;
      background: var(--hr-surface); border: 1px solid var(--hr-border); }
    .hr-rgroup .hr-rb { height: 34px; min-width: 34px; }
    .hr-pagebox { display: flex; align-items: center; gap: 5px; padding: 0 4px; color: var(--hr-muted); font-variant-numeric: tabular-nums; cursor: text; }
    #hr-rbar #hr-page { width: 52px; height: 30px; margin: 0; padding: 0 4px; text-align: center; border-radius: 8px;
      border: 1px solid transparent; background: var(--hr-bg); color: var(--hr-text); font: 600 14px/1 var(--hr-font); -moz-appearance: textfield; }
    #hr-rbar #hr-page:focus { border-color: var(--hr-accent); outline: none; }
    #hr-rbar #hr-page::-webkit-inner-spin-button, #hr-rbar #hr-page::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
    #hr-rbar #hr-total { color: var(--hr-muted); }
    .hr-zoomval { min-width: 54px !important; font-variant-numeric: tabular-nums; }
    #hr-progress { position: fixed; z-index: 2147483646; left: 0; right: 0; top: 56px; height: 2px; pointer-events: none; transition: top .2s ease; }
    html.hr-idle #hr-progress { top: 0; }
    #hr-progress i { display: block; height: 100%; width: 0; background: var(--hr-accent); transition: width .2s ease; }
    @media (max-width: 760px) { .hr-rtitle span, .hr-rzoom, #hr-fullbtn { display: none !important; } }
    @media (prefers-reduced-motion: reduce) { #hr-rbar, #hr-progress, #hr-progress i { transition: none; } }

    :where(html.hr-ct:not(.hr-ui)), :where(html.hr-ct:not(.hr-ui)) body { background-color: var(--hr-bg) !important; color: var(--hr-text) !important; color-scheme: var(--hr-scheme); }
    :where(html.hr-ct:not(.hr-ui)) :where(div, table, thead, tbody, tr, td, th, ul, ol, li, section, article, header, footer, main, nav, p, h1, h2, h3):where(:not([id^="hr-"], [id^="hr-"] *)) {
      background-color: transparent !important; border-color: var(--hr-border) !important; }
    :where(html.hr-ct:not(.hr-ui)) :where(h1, h2, h3, h4, h5, h6, p, td, th, li, span, div, label, dt, dd, b, strong):where(:not([id^="hr-"], [id^="hr-"] *)) { color: var(--hr-text) !important; }
    :where(html.hr-ct:not(.hr-ui)) a:where(:not([id^="hr-"] *)) { color: color-mix(in srgb, var(--hr-accent) 45%, var(--hr-text)) !important; }
    :where(html.hr-ct:not(.hr-ui)) :where(input, select, textarea):where(:not([id^="hr-"] *)) {
      background-color: var(--hr-surface) !important; color: var(--hr-text) !important; border: 1px solid var(--hr-border) !important; border-radius: 6px; }
    :where(html.hr-ct:not(.hr-ui)) button:where(:not([id^="hr-"], [id^="hr-"] *)) { background-color: var(--hr-surface2) !important; color: var(--hr-text) !important; border-color: var(--hr-border) !important; }
    html.hr-ct:not(.hr-ui) .gallery-content > div { background-color: var(--hr-surface) !important; border: 1px solid var(--hr-border) !important; border-radius: 12px !important; }
    html.hr-ct:not(.hr-ui) .gallery-content > div h1 a { color: var(--hr-text) !important; }
    html.hr-ct:not(.hr-ui) :is(.relatedtags, .tags, #tags) a { background-color: var(--hr-surface2) !important; color: var(--hr-text) !important; border-color: transparent !important; }
    html.hr-ct:not(.hr-ui) .page-container a { color: var(--hr-text) !important; }
  `);

  let app = null, appRefs = null, gridSig = '', settled = false;
  const wantUi = () => S.newUi && !isReader();
  const wantReaderUi = () => S.newUi && isReader();
  {
    const c = document.documentElement.classList;
    if (wantUi()) c.add('hr-ui');
    if (wantReaderUi()) c.add('hr-rv');
    if (S.classicTheme && !isReader()) c.add('hr-ct');
  }

  const ICONS = {
    search: '<circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/>',
    sliders: '<path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/>',
    back: '<path d="M19 12H5M11 18l-6-6 6-6"/>',
    prev: '<path d="M15 18l-6-6 6-6"/>',
    next: '<path d="M9 18l6-6-6-6"/>',
    fitH: '<path d="M12 3v18M8 7l4-4 4 4M8 17l4 4 4-4"/>',
    fitW: '<path d="M3 12h18M7 8l-4 4 4 4M17 8l4 4-4 4"/>',
    full: '<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>',
    grid: '<rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/>',
    minus: '<path d="M6 12h12"/>',
    plus: '<path d="M12 6v12M6 12h12"/>',
    book: '<path d="M4 5.5C6.5 4 9.5 4 12 6c2.5-2 5.5-2 8-.5V19c-2.5-1.5-5.5-1.5-8 .5-2.5-2-5.5-2-8-.5z"/><path d="M12 6v13.5"/>',
    external: '<path d="M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/>',
    x: '<path d="M6 6l12 12M18 6L6 18"/>',
    ban: '<circle cx="12" cy="12" r="8"/><path d="M6.5 6.5l11 11"/>',
    info: '<circle cx="12" cy="12" r="8.5"/><path d="M12 11v5M12 8h.01"/>',
  };
  const svg = html => { const t = document.createElement('template'); t.innerHTML = html; return t.content.firstChild; };
  const icon = (name, size = 18) => svg(`<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[name]}</svg>`);
  const setText = (n, t) => { if (n.textContent !== t) n.textContent = t; };
  const nsClass = ns => ns === 'female' ? 'f' : ns === 'male' ? 'm' : '';

  const SORTS = [['', 'Newest'], ['today', 'Popular today'], ['week', 'Popular this week'], ['month', 'Popular this month'], ['year', 'Popular this year']];
  const LANGS = [['', 'Any language'], ['english', 'English'], ['japanese', 'Japanese'], ['chinese', 'Chinese'], ['korean', 'Korean'],
    ['spanish', 'Spanish'], ['french', 'French'], ['german', 'German'], ['russian', 'Russian'], ['portuguese', 'Portuguese']];
  const KIND = { tag: 'Tag', artist: 'Artist', series: 'Series', character: 'Character', group: 'Group', type: 'Type' };

  function replaceTerms(drop, add) {
    const t = load().filter(x => !drop(bare(x)));
    save([...t, ...add]);
    runSearch();
  }

  const suggestFn = () => typeof W.get_suggestions_for_query === 'function' ? W.get_suggestions_for_query : null;
  function attachSuggest(input, box) {
    const list = el('div', { className: 'hr-sugg', hidden: true });
    list.setAttribute('role', 'listbox');
    box.append(list);
    let items = [], idx = -1, seq = 0, timer = 0;
    const close = () => { list.hidden = true; items = []; idx = -1; };

    const collect = (x, out = []) => {
      if (!x || out.length > 50) return out;
      if (Array.isArray(x)) {
        if (typeof x[0] === 'string') out.push({ s: x[0], t: x[1], n: typeof x[3] === 'string' ? x[3] : x[2] });
        else for (const y of x) collect(y, out);
      } else if (typeof x === 'object') {
        if (typeof x.s === 'string') out.push(x);
        else for (const y of Object.values(x)) if (y && typeof y === 'object') collect(y, out);
      }
      return out;
    };
    const norm = x => {
      const name = String(x.s).trim();
      if (!name) return null;
      const n = typeof x.n === 'string' && /^[a-z]+$/.test(x.n) ? x.n : 'tag';
      return { name, count: +x.t || 0, ns: n, term: `${n}:${name.replace(/\s+/g, '_')}` };
    };
    async function run() {
      const raw = (input.value.match(/(\S*)$/) || ['', ''])[1];
      const neg = raw.startsWith('-');
      const q = raw.replace(/^-/, '');
      const fn = suggestFn();
      if (!q || !fn) return close();
      const my = ++seq;
      let res = null;
      try { res = await fn.call(W, q); } catch {}
      if (my !== seq || document.activeElement !== input) return;
      const seen = new Set();
      items = collect(res).map(norm).filter(x => x && !seen.has(x.term) && seen.add(x.term)).slice(0, 10).map(x => ({ ...x, neg }));
      idx = -1;
      draw();
    }
    function draw() {
      if (!items.length) return close();
      list.replaceChildren(...items.map((it, i) => {
        const row = el('div', { className: 'hr-sugg-item' + (i === idx ? ' on' : '') },
          el('span', { className: 'hr-ns ' + nsClass(it.ns), textContent: (it.neg ? 'not ' : '') + it.ns }),
          el('span', { className: 'hr-sugg-name', textContent: it.name }),
          el('span', { className: 'hr-sugg-count', textContent: it.count ? it.count.toLocaleString() : '' }));
        row.setAttribute('role', 'option');
        row.setAttribute('aria-selected', String(i === idx));
        row.addEventListener('mousedown', e => { e.preventDefault(); pick(it); });
        return row;
      }));
      list.hidden = false;
      list.querySelector('.on')?.scrollIntoView({ block: 'nearest' });
    }
    function pick(it) {
      input.value = input.value.replace(/\S*$/, '');
      const t = load().filter(x => bare(x) !== it.term);
      t.push((it.neg ? '-' : '') + it.term);
      save(t);
      close();
      input.focus();
    }
    input.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(run, 120); });
    input.addEventListener('blur', () => setTimeout(close, 120));
    return {
      handleKey(e) {
        if (list.hidden || !items.length) return false;
        const n = items.length;
        if (e.key === 'ArrowDown') idx = idx + 1 >= n ? -1 : idx + 1;
        else if (e.key === 'ArrowUp') idx = idx - 1 < -1 ? n - 1 : idx - 1;
        else if ((e.key === 'Enter' && idx >= 0) || (e.key === 'Tab' && !e.shiftKey)) { e.preventDefault(); pick(items[Math.max(idx, 0)]); return true; }
        else if (e.key === 'Escape') { e.preventDefault(); close(); return true; }
        else return false;
        e.preventDefault();
        draw();
        return true;
      },
    };
  }

  function buildTopBar() {
    const chips = el('span', { className: 'hr-chips' });
    const input = el('input', { type: 'text', placeholder: 'Search tags, artists or titles', spellcheck: false, autocomplete: 'off' });
    input.setAttribute('aria-label', 'Search');
    const sbox = el('div', { className: 'hr-sbox' }, icon('search', 16), chips, input);
    sbox.addEventListener('click', e => { if (e.target === sbox) input.focus(); });
    const sugg = attachSuggest(input, sbox);
    const commit = () => {
      const words = input.value.trim().split(/\s+/).filter(Boolean);
      if (words.length) { const t = load(); for (const w of words) if (!t.includes(w)) t.push(w); store(t); }
      input.value = '';
      runSearch();
    };
    input.addEventListener('keydown', e => {
      if (sugg.handleKey(e)) return;
      if (e.key === 'Enter') { e.preventDefault(); commit(); }
      else if (e.key === 'Backspace' && !input.value) { const t = load(); if (t.length) { t.pop(); save(t); } }
      else if (e.key === 'Escape') input.blur();
    });

    const select = (opts, label) => {
      const s = el('select', { className: 'hr-select' });
      s.setAttribute('aria-label', label);
      for (const [v, text] of opts) s.append(el('option', { value: v, textContent: text }));
      return s;
    };
    const sort = select(SORTS, 'Sort');
    sort.onchange = () => replaceTerms(x => /^orderby/.test(x), sort.value ? ['orderby:popular', 'orderbykey:' + sort.value] : []);
    const lang = select(LANGS, 'Language');
    lang.onchange = () => replaceTerms(x => x.startsWith('language:'), lang.value ? ['language:' + lang.value] : []);

    const go = el('button', { type: 'button', className: 'hr-btn primary', textContent: 'Search' });
    go.onclick = commit;
    const gear = el('button', { type: 'button', className: 'hr-icon', title: 'Settings' }, icon('sliders'));
    gear.setAttribute('aria-label', 'Hitomi Revamp settings');
    gear.onclick = openSettings;
    const brand = el('a', { className: 'hr-brand', href: '/', title: 'Latest galleries' },
      el('img', { src: LOGO_URL, alt: '' }), el('span', { textContent: 'Hitomi' }));
    return { header: el('header', { className: 'hr-top' }, brand, sbox, sort, lang, go, gear), refs: { chips, input, sort, lang } };
  }

  function buildApp() {
    if (app) return;
    const top = buildTopBar();
    appRefs = { ...top.refs, mode: pageGalleryId ? 'gallery' : 'list' };
    let main;
    if (appRefs.mode === 'list') {
      const h1 = el('h1'), kind = el('span', { className: 'hr-kind' }), stat = el('span', { className: 'hr-stat' });
      const grid = el('div', { className: 'hr-grid' });
      const pager = el('nav', { className: 'hr-pager' });
      pager.setAttribute('aria-label', 'Pages');
      Object.assign(appRefs, { h1, kind, stat, grid, pager });
      main = el('main', { className: 'hr-main' }, el('div', { className: 'hr-headline' }, h1, kind, stat), grid, pager);
    } else {
      main = buildGalleryMain();
    }
    app = el('div', { id: 'hr-app' }, top.header, main);
    document.body.append(app);
    setTimeout(() => { settled = true; renderApp(); }, 8000);
    renderPanel();
    renderApp();
  }

  function teardownApp() {
    app?.remove();
    app = appRefs = null;
    gridSig = '';
    document.documentElement.classList.remove('hr-ui');
  }

  function syncUi() {
    if (!document.body || isReader()) return;
    document.documentElement.classList.toggle('hr-ct', !!S.classicTheme);
    const ok = wantUi() && (pageGalleryId ? !!W.galleryinfo?.files : !!document.querySelector('.gallery-content'));
    if (ok) {
      document.documentElement.classList.add('hr-ui');
      if (!app) buildApp(); else { gridSig = ''; renderApp(); }
    } else teardownApp();
  }

  function parseCard(card, bl) {
    const links = [...card.querySelectorAll('a[href]')];
    const ga = links.find(a => galleryIdFromLink(a));
    if (!ga) return null;
    const c = { href: ga.dataset.hrInfo || ga.href, title: '', covers: [], artists: [], series: [], tags: [], type: null, lang: null };
    c.title = (card.querySelector('h1') || ga).textContent.trim() || 'Untitled';

    const push = u => {
      u = (u || '').trim().split(/\s+/)[0];
      if (!u || u.startsWith('data:')) return;
      try { u = new URL(u, location.href).href; } catch { return; }
      if (!c.covers.includes(u)) c.covers.push(u);
    };
    const sets = [];
    const pics = [...card.querySelectorAll('picture')];
    for (const node of pics.length ? pics : [...card.querySelectorAll('img')]) {
      c.covers = [];
      const img = node.tagName === 'IMG' ? node : node.querySelector('img');
      if (img) { push(img.getAttribute('data-src')); push(img.getAttribute('src')); (img.getAttribute('data-srcset') || '').split(',').forEach(push); }
      if (node.tagName !== 'IMG') node.querySelectorAll('source').forEach(s => (s.getAttribute('data-srcset') || s.getAttribute('srcset') || '').split(',').forEach(push));
      if (c.covers.length && !sets.some(x => x[0] === c.covers[0])) sets.push(c.covers);
    }
    c.covers = sets[0] || [];
    c.previews = sets.slice(1);

    for (const a of links) {
      const t = termFromLink(a);
      if (!t) continue;
      const item = { href: a.href, text: a.textContent.trim() || t.replace(/^[a-z]+:/, '').replace(/_/g, ' '), term: t };
      if (t.startsWith('artist:') || t.startsWith('group:')) c.artists.push(item);
      else if (t.startsWith('series:')) c.series.push(item);
      else if (t.startsWith('type:')) c.type = item;
      else if (t.startsWith('language:')) c.lang = item;
      else if (/^(tag|female|male):/.test(t)) c.tags.push(item);
    }
    c.hit = cardHit(card, bl);
    return c;
  }

  const tagLink = t => el('a', { className: 'hr-tag ' + nsClass(t.term.split(':')[0]), href: t.href, textContent: t.text, title: t.term });

  function cardEl(c) {
    const coverLink = el('a', { className: 'hr-cover-link', href: c.href });
    coverLink.setAttribute('aria-label', c.title);
    const cover = el('div', { className: 'hr-cover' }, coverLink);
    if (c.covers.length) {
      let k = 0;
      const img = el('img', { alt: '', loading: 'lazy', decoding: 'async' });
      img.onerror = () => { if (++k < c.covers.length) img.src = c.covers[k]; else img.remove(); };
      img.src = c.covers[0];
      coverLink.append(img);
    }
    if (c.previews.length) addPeek(cover, coverLink, c.previews);
    if (c.type) cover.append(el('a', { className: 'hr-pill type', href: c.type.href, textContent: c.type.text, title: c.type.term }));
    if (c.lang) cover.append(el('a', { className: 'hr-pill lang', href: c.lang.href, textContent: c.lang.text, title: c.lang.term }));

    const card = el('article', { className: 'hr-card' }, cover, el('a', { className: 'hr-title', href: c.href, textContent: c.title, title: c.title }));
    const people = [...c.artists, ...c.series];
    if (people.length) {
      const sub = el('div', { className: 'hr-sub' });
      people.forEach((p, i) => { if (i) sub.append(', '); sub.append(el('a', { href: p.href, textContent: p.text, title: p.term })); });
      card.append(sub);
    }
    if (S.cardTags !== 'off' && c.tags.length) {
      const LIMIT = 8;
      const wrap = el('div', { className: 'hr-tags' });
      const box = el('div', { className: 'hr-tagbox' }, wrap);
      let off = null;
      const fill = open => {
        off?.();
        off = null;
        if (open) box.style.height = box.offsetHeight + 'px';
        else box.style.height = '';
        wrap.classList.toggle('open', open);
        card.classList.toggle('tags-open', open);
        wrap.replaceChildren(...(open || S.cardTags === 'all' ? c.tags : c.tags.slice(0, LIMIT)).map(tagLink));
        if (S.cardTags === 'all' || c.tags.length <= LIMIT) return;
        const b = el('button', { type: 'button', className: 'hr-tag hr-more', textContent: open ? 'Less' : `+${c.tags.length - LIMIT}`,
          title: open ? 'Show fewer tags' : 'Show all tags' });
        b.setAttribute('aria-expanded', String(open));
        b.onclick = () => { fill(!open); wrap.querySelector('.hr-more')?.focus(); };
        wrap.append(b);
        if (open) {
          const outside = e => { if (!wrap.contains(e.target) && !menu?.contains(e.target)) fill(false); };
          const esc = e => { if (e.key === 'Escape' && !menu) fill(false); };
          document.addEventListener('pointerdown', outside, true);
          document.addEventListener('keydown', esc);
          off = () => { document.removeEventListener('pointerdown', outside, true); document.removeEventListener('keydown', esc); };
        }
      };
      fill(false);
      card.append(box);
    }
    if (c.hit) { card.classList.add('is-dim'); cover.dataset.bl = c.hit; }
    return card;
  }

  function addPeek(cover, link, previews) {
    const layers = previews.map(urls => { const im = el('img', { className: 'hr-prev', alt: '', decoding: 'async' }); im._hrUrls = urls; return im; });
    const dots = el('div', { className: 'hr-dots' }, ...[0, ...layers].map(() => el('i')));
    link.append(...layers);
    cover.append(dots);
    let cur = 0;
    const render = () => {
      layers.forEach((im, k) => im.classList.toggle('on', k + 1 === cur && !!im.dataset.ok));
      [...dots.children].forEach((d, k) => d.classList.toggle('on', k === cur));
    };
    const load = im => {
      if (im.getAttribute('src')) return;
      let k = 0;
      im.onload = () => { im.dataset.ok = '1'; render(); };
      im.onerror = () => { if (++k < im._hrUrls.length) im.src = im._hrUrls[k]; };
      im.src = im._hrUrls[0];
    };
    const total = layers.length + 1;
    cover.addEventListener('pointerenter', e => {
      if (e.pointerType === 'touch') return;
      layers.forEach(load);
      cover.classList.add('peek');
      cur = 1;
      render();
    });
    cover.addEventListener('pointermove', e => {
      if (total < 3 || e.pointerType === 'touch') return;
      const r = cover.getBoundingClientRect();
      const n = Math.min(total - 1, Math.max(0, Math.floor((e.clientX - r.left) / r.width * total)));
      if (n !== cur) { cur = n; render(); }
    });
    cover.addEventListener('pointerleave', () => { cover.classList.remove('peek'); cur = 0; render(); });
  }

  function cardEls(cards) {
    const bl = new Set(blacklist);
    const out = [];
    for (const card of cards) {
      const c = parseCard(card, bl);
      if (!c || (c.hit && S.blMode === 'hide')) continue;
      out.push(cardEl(c));
    }
    return out;
  }

  const skeleton = () => el('article', { className: 'hr-card hr-skel' }, el('div', { className: 'hr-cover' }), el('i'), el('i'));

  function parsePages() {
    const pc = document.querySelector('.page-container');
    if (!pc) return [];
    return [...pc.querySelectorAll('li')]
      .map(li => ({ label: li.textContent.trim(), href: li.querySelector('a')?.href || '' }))
      .filter(p => p.label);
  }

  function renderApp() {
    if (!appRefs) return;
    const R = appRefs;
    const cards = [...document.querySelectorAll(CARD_SEL)];
    const cardSig = [cards.map(c => c.querySelector('a[href]')?.getAttribute('href')).join(','), blacklist.join(','), S.blMode, S.cardTags].join('|');

    if (R.mode === 'gallery') {
      if (!R.relGrid || cardSig === gridSig) return;
      gridSig = cardSig;
      const out = cardEls(cards);
      R.relSection.hidden = !out.length;
      R.relGrid.replaceChildren(...out);
      return;
    }

    const title = (document.title || '').replace(/\s*[|\-]\s*hitomi\.la.*$/i, '').trim();
    const m = location.pathname.match(/^\/([a-z]+)\//);
    setText(R.h1, title && !/^hitomi\.la$/i.test(title) ? title : 'Latest galleries');
    setText(R.kind, (m && KIND[m[1]]) || (location.pathname === '/search.html' ? 'Search results' : ''));

    const pages = parsePages();
    const sig = cardSig + '|' + pages.map(p => p.label + (p.href ? '' : '*')).join(',') + '|' + settled;
    if (sig !== gridSig) {
      gridSig = sig;
      const out = cardEls(cards);
      R.shown = out.length;
      if (out.length) R.grid.replaceChildren(...out);
      else if (!cards.length && !settled) R.grid.replaceChildren(...Array.from({ length: 12 }, skeleton));
      else R.grid.replaceChildren(el('p', { className: 'hr-empty', textContent: cards.length ? 'Everything on this page is blacklisted.' : 'No galleries here.' }));

      R.pager.replaceChildren(...pages.map(p => {
        if (!p.href) return el('span', { className: 'hr-pg ' + (/^\d+$/.test(p.label) ? 'cur' : 'gap'), textContent: p.label });
        const a = el('a', { className: 'hr-pg', href: p.href, textContent: p.label });
        a.addEventListener('click', e => {
          if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey) return;
          e.preventDefault();
          location.href = p.href;
          scrollTo(0, 0);
        });
        return a;
      }));
    }

    const n = R.shown || 0;
    let s = cards.length ? `${n} ${n === 1 ? 'gallery' : 'galleries'} on this page` : '';
    if (blCount) s += `, ${blCount} ${S.blMode === 'hide' ? 'hidden' : 'dimmed'} by your blacklist`;
    setText(R.stat, s);
  }

  function makeThumbLoader() {
    const queue = [];
    let active = 0, last = 0, timer = 0;
    const io = new IntersectionObserver(entries => {
      for (const en of entries) {
        const img = en.target;
        if (en.isIntersecting) { if (!img.dataset.state && !queue.includes(img)) queue.push(img); }
        else { const i = queue.indexOf(img); if (i > -1) queue.splice(i, 1); }
      }
      pump();
    }, { rootMargin: '400px' });
    function pump() {
      while (active < S.maxParallel && queue.length) {
        const wait = last + S.delayMs - Date.now();
        if (wait > 0) { if (!timer) timer = setTimeout(() => { timer = 0; pump(); }, wait); return; }
        last = Date.now();
        const img = queue.shift();
        const urls = img._hrUrls;
        let k = 0;
        active++;
        img.dataset.state = 'loading';
        img.onload = () => { active--; img.dataset.state = 'done'; io.unobserve(img); pump(); };
        img.onerror = () => {
          if (++k < urls.length) { img.src = urls[k]; return; }
          active--; img.dataset.state = 'err'; io.unobserve(img); pump();
        };
        img.src = urls[0];
      }
    }
    return { add(img, urls) { img._hrUrls = urls; io.observe(img); } };
  }

  function fmtDate(s) {
    if (!s) return '';
    const d = new Date(String(s).replace(' ', 'T').replace(/([+-]\d\d)$/, '$1:00'));
    return isNaN(d) ? String(s).slice(0, 10) : d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
  }

  function buildGalleryMain() {
    const hero = el('section', { className: 'hr-hero' },
      el('div', { className: 'hr-hero-cover' }),
      el('div', { className: 'hr-hero-info' }, el('i', { className: 'hr-skl big' }), el('i', { className: 'hr-skl' }), el('i', { className: 'hr-skl' })));
    const pagesSec = el('section', { className: 'hr-sec', hidden: true });
    const relGrid = el('div', { className: 'hr-grid' });
    const relSection = el('section', { className: 'hr-sec', hidden: true },
      el('div', { className: 'hr-sec-head' }, el('h2', { textContent: 'Related galleries' })), relGrid);
    Object.assign(appRefs, { hero, pagesSec, relGrid, relSection });
    waitFor(() => W.galleryinfo?.files, 15000).then(ok => {
      if (!appRefs || appRefs.mode !== 'gallery') return;
      if (ok) fillGallery(W.galleryinfo);
      else teardownApp();
    });
    return el('main', { className: 'hr-main' }, hero, pagesSec, relSection);
  }

  function fillGallery(g) {
    const R = appRefs;
    const id = g.id || pageGalleryId;
    const files = g.files || [];
    const named = (list, key, path) => (list || []).map(x => ({
      text: x[key], href: x.url || `/${path}/${encodeURIComponent(x[key])}-all.html`, term: `${path}:${String(x[key]).replace(/\s+/g, '_')}`,
    }));
    const links = items => items.length
      ? el('span', {}, ...items.flatMap((it, i) => [...(i ? [', '] : []), el('a', { href: it.href, textContent: it.text, title: it.term })]))
      : null;
    const tags = (g.tags || []).map(t => {
      const f = +t.female === 1 || t.female === true, m = +t.male === 1 || t.male === true;
      const pre = f ? 'female:' : m ? 'male:' : '';
      return { text: t.tag + (f ? ' ♀' : m ? ' ♂' : ''), href: t.url || `/tag/${encodeURIComponent(pre + t.tag)}-all.html`,
        term: (pre || 'tag:') + String(t.tag).replace(/\s+/g, '_') };
    }).sort((a, b) => (a.term.startsWith('tag:') - b.term.startsWith('tag:')));

    const rows = [
      ['Type', g.type ? links([{ text: g.type, href: `/type/${encodeURIComponent(g.type)}-all.html`, term: 'type:' + g.type }]) : null],
      ['Language', g.language ? links([{ text: g.language_localname || g.language, href: `/index-${g.language}.html`, term: 'language:' + g.language }]) : null],
      ['Also in', links((g.languages || []).filter(l => String(l.galleryid) !== String(id))
        .map(l => ({ text: l.language_localname || l.name, href: `/galleries/${l.galleryid}.html`, term: 'language:' + l.name })))],
      ['Artists', links(named(g.artists, 'artist', 'artist'))],
      ['Groups', links(named(g.groups, 'group', 'group'))],
      ['Series', links(named(g.parodys, 'parody', 'series'))],
      ['Characters', links(named(g.characters, 'character', 'character'))],
      ['Pages', String(files.length)],
      ['Added', fmtDate(g.date)],
    ].filter(([, v]) => v);

    const coverImg = el('img', { alt: '' });
    const cover = el('a', { className: 'hr-hero-cover', href: readerUrl(id), title: 'Read' }, coverImg);
    const read = el('a', { className: 'hr-btn primary hr-btn-lg', href: readerUrl(id) }, icon('book', 17), el('span', { textContent: 'Read' }));
    const readTab = el('button', { type: 'button', className: 'hr-btn hr-btn-lg' }, icon('external', 16), el('span', { textContent: 'Read in new tab' }));
    readTab.onclick = () => openTab(readerUrl(id));

    R.hero.replaceChildren(cover, el('div', { className: 'hr-hero-info' },
      el('h1', { textContent: g.title || 'Untitled' }),
      ...(g.japanese_title && g.japanese_title !== g.title ? [el('p', { className: 'hr-jp', textContent: g.japanese_title })] : []),
      el('div', { className: 'hr-actions' }, read, readTab),
      el('dl', { className: 'hr-meta' }, ...rows.flatMap(([k, v]) => [el('dt', { textContent: k }), el('dd', {}, v)])),
      ...(tags.length ? [el('div', { className: 'hr-tagcloud' }, ...tags.map(tagLink))] : [])));

    const covers = [];
    const siteImg = document.querySelector('.cover img, .cover-column img');
    if (siteImg) for (const u of [siteImg.getAttribute('data-src'), siteImg.getAttribute('src')]) if (u && !u.startsWith('data:')) covers.push(new URL(u, location.href).href);
    const thumbs = el('div', { className: 'hr-thumbs' });
    R.pagesSec.replaceChildren(el('div', { className: 'hr-sec-head' }, el('h2', { textContent: 'Pages' }), el('span', { className: 'hr-kind', textContent: String(files.length) })), thumbs);
    R.pagesSec.hidden = !files.length;

    waitFor(() => typeof W.url_from_url_from_hash === 'function' && W.gg, 8000).then(ok => {
      const tn = (f, kind) => { try { return W.url_from_url_from_hash(id, f, kind, 'webp', 'tn'); } catch { return null; } };
      if (ok && files[0]) covers.push(...[tn(files[0], 'webpbigtn'), tn(files[0], 'webpsmalltn')].filter(Boolean));
      if (covers.length) {
        let k = 0;
        coverImg.onerror = () => { if (++k < covers.length) coverImg.src = covers[k]; };
        coverImg.src = covers[0];
      }
      const loader = makeThumbLoader();
      files.forEach((f, i) => {
        const img = el('img', { alt: '', decoding: 'async' });
        thumbs.append(el('a', { className: 'hr-thumb', href: `/reader/${id}.html#${i + 1}`, title: `Page ${i + 1}` }, img, el('span', { textContent: i + 1 })));
        if (!ok) return;
        const urls = [tn(f, 'webpsmalltn'), tn(f, 'webpsmallsmalltn')].filter(Boolean);
        if (urls.length) loader.add(img, urls);
      });
    });
    gridSig = '';
    renderApp();
  }

  document.addEventListener('keydown', e => {
    if (e.key !== '/' || !appRefs || e.ctrlKey || e.metaKey || e.altKey) return;
    if (e.target instanceof Element && e.target.closest('input, textarea, select, [contenteditable], #hr-settings-host')) return;
    e.preventDefault();
    appRefs.input.focus();
  });

  let rbar = null, idleTimer = 0;

  function applyFit() {
    if (!wantReaderUi()) return;
    const box = document.getElementById('comicImages');
    if (box) {
      box.classList.toggle('fitVertical', S.fit !== 'width');
      box.classList.toggle('fitHorizontal', S.fit === 'width');
    }
    const b = document.getElementById('hr-fit');
    if (b) {
      b.replaceChildren(icon(S.fit === 'width' ? 'fitW' : 'fitH'));
      b.title = (S.fit === 'width' ? 'Fitting width' : 'Fitting height') + ' (W to switch)';
    }
  }
  const toggleFit = () => { S.fit = S.fit === 'width' ? 'height' : 'width'; saveSettings(); applyFit(); };
  const toggleFullscreen = () => {
    if (document.fullscreenElement) document.exitFullscreen?.();
    else document.documentElement.requestFullscreen?.()?.catch?.(() => {});
  };

  function readerActivity() {
    if (!rbar) return;
    const root = document.documentElement;
    root.classList.remove('hr-idle');
    clearTimeout(idleTimer);
    if (!S.readerAutoHide) return;
    idleTimer = setTimeout(() => {
      if (rbar.matches(':hover') || rbar.contains(document.activeElement) || dlg?.open) return readerActivity();
      root.classList.add('hr-idle');
    }, 2200);
  }

  function buildReaderBar() {
    const btn = (name, title, fn, id) => {
      const b = el('button', { type: 'button', className: 'hr-rb', title }, icon(name));
      b.setAttribute('aria-label', title);
      if (id) b.id = id;
      b.onclick = fn;
      return b;
    };
    const dir = S.rightArrowIsNext ? 1 : -1;
    const titleEl = el('strong', { textContent: 'Loading…' }), subEl = el('span');
    const zoomVal = el('button', { type: 'button', className: 'hr-rb hr-zoomval', id: 'hr-zoom-out', title: 'Reset size (0)', textContent: S.zoom + '%' });
    zoomVal.onclick = () => setZoom(100);
    const stripBtn = btn('grid', 'Thumbnails (T)', () => {
      if (S.stripPos === 'off') { S.stripPos = 'bottom'; stripHidden = false; saveSettings(); }
      else stripHidden = !stripHidden;
      applyLayout();
    }, 'hr-stripbtn');

    rbar = el('div', { id: 'hr-rbar' },
      btn('back', 'Back (Esc)', goBack),
      el('div', { className: 'hr-rtitle' }, titleEl, subEl),
      el('span', { className: 'hr-rspacer' }),
      el('div', { className: 'hr-rgroup' },
        btn('prev', dir > 0 ? 'Previous page' : 'Next page', () => goToPage(curPage() - dir)),
        pageBoxEl(false),
        btn('next', dir > 0 ? 'Next page' : 'Previous page', () => goToPage(curPage() + dir))),
      el('div', { className: 'hr-rgroup hr-rzoom' },
        btn('minus', 'Smaller (-)', () => setZoom(S.zoom - 10)), zoomVal, btn('plus', 'Bigger (+)', () => setZoom(S.zoom + 10))),
      btn('fitH', 'Fit', toggleFit, 'hr-fit'),
      stripBtn,
      btn('full', 'Fullscreen (F)', toggleFullscreen, 'hr-fullbtn'),
      btn('sliders', 'Settings', openSettings));
    document.body.append(rbar, el('div', { id: 'hr-progress' }, el('i')));

    for (const ev of ['pointermove', 'pointerdown']) {
      window.addEventListener(ev, e => {
        if (e.pointerType === 'touch' && e.type === 'pointerdown' && e.clientY > 90) return;
        readerActivity();
      }, { passive: true });
    }
    rbar.addEventListener('focusin', readerActivity);
    window.addEventListener('hashchange', applyFit);

    waitFor(() => W.galleryinfo?.files?.length, 15000).then(ok => {
      if (!ok) { titleEl.textContent = document.title.replace(/\s*[|\-]\s*hitomi\.la.*$/i, '') || 'Reader'; return; }
      const g = W.galleryinfo;
      titleEl.textContent = g.title || 'Untitled';
      titleEl.title = g.title || '';
      document.title = (g.title || 'Reader') + ' | Hitomi.la';
      subEl.textContent = [(g.artists || []).map(a => a.artist).join(', '), g.language_localname || g.language, `${g.files.length} pages`].filter(Boolean).join(', ');
      updatePageBox();
      setTimeout(applyFit, 300);
    });
    updatePageBox();
    applyFit();
    readerActivity();
  }

  const TN = ['webpsmallsmalltn', 'webpsmalltn'];
  const tnPick = () => (S.thumbSize * (window.devicePixelRatio || 1) > 130 ? 1 : 0);

  let strip = null, stripInit = false, stripHidden = false, tiles = [], files = [], gid = null, io = null;
  const queue = [];
  let active = 0, lastStart = 0, pumpTimer = null;

  const waitFor = (fn, ms) => new Promise(res => {
    const t0 = Date.now();
    (function tick() {
      const v = fn();
      if (v || Date.now() - t0 > ms) return res(v);
      setTimeout(tick, 250);
    })();
  });

  async function initStrip() {
    stripInit = true;
    const ok = await waitFor(() => W.galleryinfo?.files?.length && typeof W.url_from_url_from_hash === 'function', 15000);
    if (!ok) { console.warn('[hitomi-revamp] reader data not found; thumbnail strip disabled'); return; }
    await waitFor(() => W.gg, 5000);
    files = W.galleryinfo.files;
    gid = W.galleryinfo.id || readerGalleryId();
    buildStrip();
  }

  function buildStrip() {
    strip = el('div', { id: 'hr-strip' });
    strip.setAttribute('aria-label', 'Pages');
    tiles = files.map((f, i) => {
      const r = f.width && f.height ? f.width / f.height : 0.7;
      const b = el('button', { className: 'hr-tile', title: `Page ${i + 1}` },
        el('img', { alt: '', decoding: 'async' }), el('span', { textContent: i + 1 }));
      b.style.setProperty('--r', r);
      b.dataset.i = i;
      b.onclick = () => goToPage(i + 1);
      strip.append(b);
      return b;
    });

    strip.addEventListener('wheel', e => {
      if (S.stripPos === 'bottom' && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
        strip.scrollLeft += e.deltaY; e.preventDefault();
      }
    }, { passive: false });

    io = new IntersectionObserver(entries => {
      for (const en of entries) {
        const i = +en.target.dataset.i;
        if (en.isIntersecting) enqueue(i);
        else if (S.thumbLoad === 'visible') { const q = queue.indexOf(i); if (q !== -1) queue.splice(q, 1); }
      }
      pump();
    }, { root: strip, rootMargin: '300px' });
    tiles.forEach(t => io.observe(t));

    document.body.append(strip);
    window.addEventListener('hashchange', () => markCurrent(true));
    applyLayout();
    markCurrent(false);
  }

  function enqueue(i) {
    const st = tiles[i].dataset.state;
    if (st || queue.includes(i)) return;
    queue.push(i);
  }

  function enqueueAll() {

    const c = curPage() - 1;
    [...tiles.keys()].sort((a, b) => Math.abs(a - c) - Math.abs(b - c)).forEach(enqueue);
    pump();
  }

  function pump() {
    while (active < S.maxParallel && queue.length) {
      const wait = lastStart + S.delayMs - Date.now();
      if (wait > 0) {
        if (!pumpTimer) pumpTimer = setTimeout(() => { pumpTimer = null; pump(); }, wait);
        return;
      }
      lastStart = Date.now();
      loadTile(queue.shift(), tnPick(), false);
    }
  }

  function loadTile(i, pick, retried) {
    const tile = tiles[i];
    const img = tile.firstChild;
    let url;
    try { url = W.url_from_url_from_hash(gid, files[i], TN[pick], 'webp', 'tn'); }
    catch { tile.dataset.state = 'err'; return; }
    active++;
    tile.dataset.state = 'loading';
    img.onload = () => { active--; tile.dataset.state = 'done'; pump(); };
    img.onerror = () => {
      active--;
      if (!retried) { loadTile(i, 1 - pick, true); return; }
      tile.dataset.state = 'err';
      pump();
    };
    img.src = url;
  }

  function markCurrent(smooth) {
    if (!strip) return;
    const t = tiles[curPage() - 1];
    strip.querySelector('.hr-tile.cur')?.classList.remove('cur');
    if (!t) return;
    t.classList.add('cur');
    const behavior = smooth && !matchMedia('(prefers-reduced-motion: reduce)').matches ? 'smooth' : 'auto';
    if (S.stripPos === 'bottom') strip.scrollTo({ left: t.offsetLeft - strip.clientWidth / 2 + t.offsetWidth / 2, behavior });
    else strip.scrollTo({ top: t.offsetTop - strip.clientHeight / 2 + t.offsetHeight / 2, behavior });
  }

  function applyLayout() {
    const root = document.documentElement;
    root.style.setProperty('--hr-size', S.thumbSize + 'px');
    root.style.setProperty('--hr-zoom', S.zoom / 100);
    syncZoomBox();
    if (!isReader()) return;
    root.classList.add('hr-reader');
    root.classList.toggle('hr-rv-pin', wantReaderUi() && !S.readerAutoHide);
    root.classList.remove('hr-pos-bottom', 'hr-pos-left', 'hr-pos-right');
    document.getElementById('hr-stripbtn')?.setAttribute('aria-pressed', String(S.stripPos !== 'off' && !stripHidden));
    if (!document.body) return;
    const show = S.stripPos !== 'off' && !stripHidden;
    if (!strip) { if (show && !stripInit) initStrip(); return; }
    strip.hidden = !show;
    if (show) root.classList.add('hr-pos-' + S.stripPos);
    if (S.thumbLoad === 'all') enqueueAll();
    markCurrent(false);
  }

  let dlg = null, blListEl = null, blHeadEl = null;

  const DLG_CSS = `
    :host { all: initial; }
    * { box-sizing: border-box; }
    dialog { padding: 0; border: 1px solid var(--hr-border); border-radius: 16px; background: var(--hr-bg); color: var(--hr-text);
      width: min(480px, calc(100vw - 24px)); max-height: min(760px, calc(100vh - 24px)); overflow: hidden;
      font: 14px/1.45 var(--hr-font); color-scheme: var(--hr-scheme); box-shadow: 0 24px 64px var(--hr-shadow); }
    dialog[open] { display: flex; flex-direction: column; }
    dialog::backdrop { background: rgba(6,6,14,.55); }

    header { display: flex; align-items: center; gap: 12px; padding: 14px 14px 14px 18px; border-bottom: 1px solid var(--hr-border); }
    header img { width: 36px; height: 36px; border-radius: 9px; flex: none; }
    header h2 { margin: 0; font-size: 16px; font-weight: 700; line-height: 1.2; }
    header .ver { display: block; font-size: 12px; color: var(--hr-muted); }
    header .x { margin-left: auto; width: 32px; height: 32px; border: 0; border-radius: 8px; background: none;
      color: var(--hr-muted); font: 20px/1 system-ui, sans-serif; cursor: pointer; }
    header .x:hover { background: var(--hr-hover); color: var(--hr-text); }

    .body { overflow: auto; padding: 4px 18px 18px; scrollbar-width: thin; scrollbar-color: var(--hr-border) transparent; }
    section { margin-top: 18px; }
    .sh { display: flex; align-items: baseline; justify-content: space-between; margin: 0 2px 8px; }
    h3 { margin: 0; font-size: 13px; font-weight: 600; color: var(--hr-muted); }
    .card { background: var(--hr-surface); border: 1px solid var(--hr-border); border-radius: 12px; }

    .row { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 16px; padding: 10px 14px; min-height: 50px; }
    .row + .row, .row + .bl { border-top: 1px solid var(--hr-border); }
    .row.off { opacity: .45; cursor: not-allowed; }
    .sw:disabled { cursor: not-allowed; }
    label.row { cursor: pointer; }
    .name { display: block; }
    .desc { display: block; font-size: 12px; color: var(--hr-muted); margin-top: 1px; }

    .sw { appearance: none; -webkit-appearance: none; width: 38px; height: 22px; margin: 0; border-radius: 11px;
      background: color-mix(in srgb, var(--hr-text) 22%, transparent); position: relative; cursor: pointer; transition: background .15s; }
    .sw::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%;
      background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.3); transition: transform .15s; }
    .sw:checked { background: var(--hr-accent); }
    .sw:checked::after { transform: translateX(16px); }

    .seg { display: inline-flex; gap: 2px; padding: 2px; background: var(--hr-bg); border: 1px solid var(--hr-border); border-radius: 9px; }
    .seg button { border: 0; background: none; color: var(--hr-muted); font: inherit; font-size: 13px; padding: 4px 10px;
      border-radius: 7px; cursor: pointer; min-width: 30px; }
    .seg button:hover { color: var(--hr-text); background: var(--hr-hover); }
    .seg button[aria-pressed="true"] { background: var(--hr-accent); color: #fff; }

    .swatches { display: flex; gap: 10px; padding: 0 4px; }
    .swatches button { width: 22px; height: 22px; padding: 0; border: 0; border-radius: 50%; background: var(--c); cursor: pointer;
      box-shadow: 0 0 0 2px var(--hr-surface); }
    .swatches button[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--hr-surface), 0 0 0 4px var(--c); }

    .num { display: inline-flex; align-items: center; background: var(--hr-bg); border: 1px solid var(--hr-border); border-radius: 9px; padding-right: 10px; }
    .num:focus-within { border-color: var(--hr-accent); }
    .num input { width: 62px; background: none; border: 0; outline: none; color: inherit; font: inherit;
      padding: 5px 4px 5px 10px; text-align: right; -moz-appearance: textfield; }
    .num input::-webkit-inner-spin-button, .num input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
    .num span { color: var(--hr-muted); font-size: 12px; }

    .range { display: flex; align-items: center; gap: 10px; }
    .range input { width: 130px; margin: 0; accent-color: var(--hr-accent); cursor: pointer; }
    .range output { min-width: 46px; text-align: right; color: var(--hr-muted); font-size: 13px; font-variant-numeric: tabular-nums; }

    .bl { padding: 12px 14px 14px; }
    .add { display: flex; gap: 8px; }
    .add input { flex: 1; min-width: 0; background: var(--hr-bg); border: 1px solid var(--hr-border); border-radius: 9px;
      color: inherit; font: inherit; padding: 7px 10px; outline: none; }
    .add input:focus { border-color: var(--hr-accent); }
    .add input::placeholder { color: var(--hr-muted); opacity: .8; }
    .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; max-height: 168px; overflow: auto; }
    .chip { display: inline-flex; align-items: center; gap: 2px; background: var(--hr-neg-soft); color: var(--hr-text);
      border-radius: 999px; padding: 2px 3px 2px 10px; font-size: 13px; }
    .chip button { border: 0; background: none; color: var(--hr-muted); cursor: pointer; width: 20px; height: 20px;
      border-radius: 50%; font: 14px/1 system-ui, sans-serif; }
    .chip button:hover { background: var(--hr-hover); color: var(--hr-text); }
    .empty { color: var(--hr-muted); font-size: 13px; margin: 10px 0 0; }
    .link { border: 0; background: none; color: var(--hr-neg); font: inherit; font-size: 12px; cursor: pointer; padding: 2px 4px; border-radius: 4px; }
    .link:hover { text-decoration: underline; }
    .link[hidden] { display: none; }

    dl.keys { display: grid; grid-template-columns: auto 1fr; gap: 9px 16px; margin: 0; padding: 12px 14px; font-size: 13px; align-items: center; }
    .keys dt { display: flex; gap: 4px; flex-wrap: wrap; }
    .keys dd { margin: 0; color: var(--hr-muted); }
    kbd { font: 12px/1 var(--hr-font); background: var(--hr-bg); border: 1px solid var(--hr-border); border-bottom-width: 2px;
      border-radius: 5px; padding: 3px 6px; color: var(--hr-text); }
    .note { font-size: 12px; color: var(--hr-muted); margin: 12px 2px 0; }

    footer { display: flex; justify-content: space-between; gap: 8px; padding: 12px 18px; border-top: 1px solid var(--hr-border); background: var(--hr-surface); }
    .btn { border: 1px solid var(--hr-border); background: transparent; color: var(--hr-text); font: 600 13px/1 var(--hr-font);
      padding: 9px 16px; border-radius: 9px; cursor: pointer; }
    .btn:hover { background: var(--hr-hover); }
    .btn.primary { background: var(--hr-accent); border-color: var(--hr-accent); color: #fff; }
    .btn.primary:hover { background: color-mix(in srgb, var(--hr-accent) 85%, #fff); }

    :focus-visible { outline: 2px solid var(--hr-accent); outline-offset: 2px; }
    @media (prefers-reduced-motion: reduce) { .sw, .sw::after { transition: none; } }
  `;

  function gearButton(id) {
    const b = el('button', { id, type: 'button', title: 'Hitomi Revamp settings' }, el('img', { src: LOGO_URL, alt: '' }));
    b.setAttribute('aria-label', 'Hitomi Revamp settings');
    b.onclick = openSettings;
    return b;
  }

  function renderBlacklist() {
    if (blHeadEl) {
      blHeadEl.firstChild.textContent = blacklist.length ? `Blacklist (${blacklist.length})` : 'Blacklist';
      blHeadEl.lastChild.hidden = !blacklist.length;
    }
    if (!blListEl) return;
    blListEl.textContent = '';
    if (!blacklist.length) {
      blListEl.append(el('p', { className: 'empty', textContent: 'Nothing blacklisted yet. Right-click any tag and pick "Blacklist everywhere", or type one above.' }));
      return;
    }
    const chips = el('div', { className: 'chips' });
    for (const t of blacklist) {
      const x = el('button', { type: 'button', textContent: '×', title: 'Remove' });
      x.setAttribute('aria-label', 'Remove ' + t);
      x.onclick = () => blRemove(t);
      chips.append(el('span', { className: 'chip' }, t, x));
    }
    blListEl.append(chips);
  }

  function openSettings() {
    document.getElementById('hr-settings-host')?.remove();
    const host = el('div', { id: 'hr-settings-host' });
    const root = host.attachShadow({ mode: 'open' });
    root.append(el('style', { textContent: DLG_CSS }));

    const sw = (key, after) => {
      const c = el('input', { type: 'checkbox', className: 'sw', checked: !!S[key] });
      c.setAttribute('role', 'switch');
      c.onchange = () => { S[key] = c.checked; changed(); after?.(); };
      return c;
    };
    const seg = (key, opts, label) => {
      const g = el('div', { className: 'seg' });
      g.setAttribute('role', 'group');
      g.setAttribute('aria-label', label);
      for (const [v, text] of opts) {
        const b = el('button', { type: 'button', textContent: text });
        b.setAttribute('aria-pressed', String(String(S[key]) === v));
        b.onclick = () => {
          S[key] = typeof DEFAULTS[key] === 'number' ? +v : v;
          g.querySelectorAll('button').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
          changed();
        };
        g.append(b);
      }
      return g;
    };
    const num = (key, min, max, step, unit) => {
      const n = el('input', { type: 'number', min, max, step, value: S[key] });
      n.onchange = () => {
        const v = Math.max(min, Math.min(max, Math.round(n.value === '' ? DEFAULTS[key] : +n.value)));
        n.value = v; S[key] = v; changed();
      };
      return el('span', { className: 'num' }, n, el('span', { textContent: unit }));
    };
    const swatches = () => {
      const g = el('div', { className: 'swatches' });
      g.setAttribute('role', 'group');
      g.setAttribute('aria-label', 'Accent color');
      for (const [k, c] of Object.entries(ACCENTS)) {
        const b = el('button', { type: 'button', title: k[0].toUpperCase() + k.slice(1) });
        b.style.setProperty('--c', c);
        b.setAttribute('aria-label', k);
        b.setAttribute('aria-pressed', String(S.accent === k));
        b.onclick = () => {
          S.accent = k;
          g.querySelectorAll('button').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
          changed();
        };
        g.append(b);
      }
      return g;
    };
    const focusSw = sw('newTabFocus');
    let focusRow = null;
    const syncFocusRow = () => {
      focusSw.disabled = !S.newTab;
      focusRow?.classList.toggle('off', !S.newTab);
      focusRow?.setAttribute('aria-disabled', String(!S.newTab));
    };
    const row = (name, ctrl, desc, tag = 'label') => el(tag, { className: 'row' },
      el('span', {}, el('span', { className: 'name', textContent: name }),
        ...(desc ? [el('span', { className: 'desc', textContent: desc })] : [])),
      ctrl);
    focusRow = row('Switch to the new tab', focusSw, 'Off opens it in the background');
    syncFocusRow();
    const section = (title, ...kids) => el('section', {},
      el('div', { className: 'sh' }, el('h3', { textContent: title })),
      el('div', { className: 'card' }, ...kids));

    const sizeOut = el('output', { textContent: S.thumbSize + ' px' });
    const size = el('input', { type: 'range', min: 60, max: 260, step: 10, value: S.thumbSize });
    size.setAttribute('aria-label', 'Thumbnail size');
    size.oninput = () => { S.thumbSize = +size.value; sizeOut.textContent = size.value + ' px'; changed(); };

    const blInput = el('input', { type: 'text', placeholder: 'female:big_breasts, artist:name, full_color' });
    blInput.setAttribute('aria-label', 'Tags to blacklist, comma separated');
    const addFromInput = () => {
      const v = blInput.value.split(',').map(normTerm).filter(Boolean);
      if (!v.length) return;
      setBlacklist([...blacklist, ...v]);
      blInput.value = '';
    };
    blInput.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); addFromInput(); } });
    const addBtn = el('button', { type: 'button', className: 'btn primary', textContent: 'Add' });
    addBtn.onclick = addFromInput;
    const clearBtn = el('button', { type: 'button', className: 'link', textContent: 'Clear all' });
    clearBtn.onclick = () => { if (confirm(`Remove all ${blacklist.length} blacklisted tags?`)) setBlacklist([]); };
    blHeadEl = el('div', { className: 'sh' }, el('h3'), clearBtn);
    blListEl = el('div');

    const keys = (combo, what) => [el('dt', {}, ...combo.map(k => el('kbd', { textContent: k }))), el('dd', { textContent: what })];

    const close = el('button', { type: 'button', className: 'x', textContent: '×', title: 'Close' });
    close.setAttribute('aria-label', 'Close settings');
    const reset = el('button', { type: 'button', className: 'btn', textContent: 'Reset settings' });
    reset.onclick = () => { Object.assign(S, DEFAULTS); changed(); openSettings(); };
    const done = el('button', { type: 'button', className: 'btn primary', textContent: 'Done' });

    dlg = el('dialog', {},
      el('header', {},
        el('img', { src: LOGO_URL, alt: '' }),
        el('div', {}, el('h2', { textContent: 'Hitomi Revamp' }), el('span', { className: 'ver', textContent: VERSION ? 'Version ' + VERSION : 'Settings' })),
        close),
      el('div', { className: 'body' },
        section('Appearance',
          row('New UI', sw('newUi'), 'Redesigned lists, gallery pages and reader. An open reader switches on its next load'),
          row('Theme', seg('theme', THEME_NAMES, 'Theme'), null, 'div'),
          row('Accent', swatches(), null, 'div'),
          row('Theme the classic pages', sw('classicTheme'), 'Recolors the site wherever New UI is off'),
          row('Card size', seg('cardSize', [['s', 'S'], ['m', 'M'], ['l', 'L']], 'Card size'), null, 'div'),
          row('Tags on cards', seg('cardTags', [['off', 'Off'], ['compact', 'Some'], ['all', 'All']], 'Tags on cards'), 'Some shows 8; the rest opens over the card', 'div')),

        section('Browsing',
          row('Open galleries in the reader', sw('openInReader'), 'Skip the details page'),
          row('Open galleries in a new tab', sw('newTab', syncFocusRow), 'When you click a result'),
          focusRow),

        section('Reader',
          row('Right arrow goes forward', sw('rightArrowIsNext'), 'Turn off for right-to-left reading'),
          row('Click image halves to turn pages', sw('clickHalves')),
          row('Esc leaves the reader', sw('escBack')),
          row('Auto-hide the reader bar', sw('readerAutoHide'), 'Move the mouse to bring it back'),
          row('Image size', num('zoom', 10, 300, 5, '%'), 'Share of the screen the page fills')),

        section('Thumbnail strip',
          row('Position', seg('stripPos', [['bottom', 'Bottom'], ['left', 'Left'], ['right', 'Right'], ['off', 'Off']], 'Strip position'), null, 'div'),
          row('Thumbnail size', el('span', { className: 'range' }, size, sizeOut), null, 'div'),
          row('Load thumbnails', seg('thumbLoad', [['visible', 'On screen'], ['all', 'All']], 'Load thumbnails'), null, 'div'),
          row('Downloads at once', seg('maxParallel', [['1', '1'], ['2', '2'], ['3', '3'], ['4', '4']], 'Downloads at once'), null, 'div'),
          row('Pause between downloads', num('delayMs', 0, 3000, 50, 'ms'))),

        el('section', {}, blHeadEl,
          el('div', { className: 'card' },
            row('Blacklisted galleries', seg('blMode', [['hide', 'Hide'], ['dim', 'Dim']], 'Blacklisted galleries'), 'Dim blurs them until you hover', 'div'),
            el('div', { className: 'bl' }, el('div', { className: 'add' }, blInput, addBtn), blListEl))),

        section('Shortcuts',
          el('dl', { className: 'keys' },
            ...keys(['←', '→'], 'Turn pages (A / D work too)'),
            ...keys(['Home', 'End'], 'First / last page'),
            ...keys(['T'], 'Hide the thumbnail strip'),
            ...keys(['W'], 'Fit width or height'),
            ...keys(['F'], 'Fullscreen'),
            ...keys(['+', '−', '0'], 'Image size'),
            ...keys(['/'], 'Jump to the search box'),
            ...keys(['Esc'], 'Leave the reader'),
            ...keys(['Shift', 'Right-click'], 'Normal browser menu'))),

        el('p', { className: 'note', textContent: 'Right-click a tag to search or blacklist it. The search list belongs to this tab; tabs opened from here start with a copy. Button swaps apply on the next page load.' })),
      el('footer', {}, reset, done));

    close.onclick = done.onclick = () => dlg.close();
    dlg.addEventListener('click', e => { if (e.target === dlg) dlg.close(); });
    dlg.addEventListener('close', () => { host.remove(); dlg = blListEl = blHeadEl = null; });

    root.append(dlg);
    document.body.append(host);
    renderBlacklist();
    dlg.showModal();
  }

  function changed() { saveSettings(); applyTheme(); applyLayout(); syncUi(); scanCards(); readerActivity(); }

  GM_registerMenuCommand('Settings', () => ready(openSettings));

  ready(() => {
    if (isReader()) {
      patchReader();
      const ul = document.querySelector('.navbar .nav.navbar-nav');
      if (wantReaderUi()) {
        buildReaderBar();
      } else if (ul) {
        addPageBox(ul);
        addZoomBox(ul);
        ul.append(el('li', { className: 'hr-nav-li' }, gearButton('hr-navgear')));
      } else {
        document.body.append(gearButton('hr-fab'));
      }
    } else {
      const fab = gearButton('hr-fab');
      fab.append(el('span', { className: 'hr-badge', hidden: true }));
      document.body.append(fab);
      if (wantUi()) {
        buildApp();

        if (!pageGalleryId) waitFor(() => document.querySelector('.gallery-content'), 4000).then(ok => { if (!ok) teardownApp(); });
      }
      new MutationObserver(scheduleScan).observe(document.body, { childList: true, subtree: true });
      scanCards();
    }
    panel = el('div', { id: 'hr-panel' });
    document.body.append(panel);
    renderPanel();
    applyLayout();
  });
})();