☰

Sleazy Fork is available in English.

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램을 먼저 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 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();
  });
})();