☰

ReelsMunkey Enhanced

Explore grid and context-preserving Reels queue for ReelsMunkey

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         ReelsMunkey Enhanced
// @namespace    https://reelsmunkey.com/
// @version      0.3.1
// @description  Explore grid and context-preserving Reels queue for ReelsMunkey
// @match        https://reelsmunkey.com/*
// @match        https://www.reelsmunkey.com/*
// @run-at       document-idle
// @grant        none
// @noframes
// @license MIT
// ==/UserScript==

(() => {
  'use strict';

  const SITE = 'https://reelsmunkey.com';
  const IMAGE_SITE = 'https://imgs.reelsmunkey.com/';
  const PAGE_SIZE = 20;
  const REQUEST_TIMEOUT = 15000;
  const PREF_KEY = 'rm-enhanced-prefs-v1';
  const TEST = typeof globalThis !== 'undefined' && globalThis.__RM_TEST__;

  function videoKey(item) {
    return String(item?._id || item?.url || '').trim();
  }

  function normalizeVideo(raw) {
    if (!raw || typeof raw !== 'object') return null;
    const source = raw.video && typeof raw.video === 'object' ? raw.video : raw;
    let slug = source.url || source.slug || source.permalink || '';
    if (typeof slug === 'string' && slug.startsWith('/video/')) slug = slug.slice(7).split(/[?#]/)[0];
    if (typeof slug !== 'string' || !/^[\p{L}\p{N}_-]+$/u.test(slug)) return null;
    const tags = Array.isArray(source.tags) ? source.tags.map(tag => ({
      tag: String(tag?.tag || tag?.name || ''),
      url: String(tag?.url || tag?.slug || '')
    })) : [];
    return {
      _id: source._id ? String(source._id) : '',
      url: slug,
      title: String(source.title || 'Untitled video'),
      description: String(source.description || ''),
      thumbnail: typeof source.thumbnail === 'string' ? source.thumbnail : '',
      created_at: typeof source.created_at === 'string' ? source.created_at : '',
      views: Number(source.views?.count ?? source.views ?? NaN),
      likes: Number(source.likes?.count ?? source.likes ?? NaN),
      comments: Number(source.comments?.count ?? source.comments ?? NaN),
      tags
    };
  }

  function dedupe(items) {
    const output = [];
    const seen = new Set();
    for (const raw of items) {
      const item = normalizeVideo(raw);
      if (!item) continue;
      const key = videoKey(item);
      if (seen.has(key) || seen.has(`slug:${item.url}`)) continue;
      seen.add(key);
      seen.add(`slug:${item.url}`);
      output.push(item);
    }
    return output;
  }

  function listingUrl(source, page) {
    const p = Math.max(1, Number(page) || 1);
    if (source.kind === 'category') return `/collections/${encodeURIComponent(source.category)}${p > 1 ? `/${p}` : ''}`;
    if (source.kind === 'popular') return `/sortby/${source.sort}/${source.period}${p > 1 ? `/${p}` : ''}`;
    return p > 1 ? `/page/${p}` : '/';
  }

  function sourceFromControls(controls) {
    const query = String(controls.query || '').trim();
    const category = String(controls.category || '');
    const sort = String(controls.sort || 'site');
    const period = String(controls.period || 'allTime');
    if (query) return { kind: 'search', query, category, sort, period, fullScan: !!category || sort !== 'site' || period !== 'allTime' };
    if (category) return { kind: 'category', category, sort, period, fullScan: sort !== 'site' || period !== 'allTime' };
    if (sort !== 'site') return { kind: 'popular', sort, period, fullScan: false };
    return { kind: 'home', sort, period, fullScan: period !== 'allTime' };
  }

  function sourceLabel(source) {
    const base = source.kind === 'search' ? `Search: ${source.query}` :
      source.kind === 'category' ? `Category: ${source.category}` :
      source.kind === 'popular' ? `Most ${source.sort}` : 'Latest videos';
    const category = source.kind === 'search' && source.category ? ` · ${source.category}` : '';
    const ranking = source.fullScan && source.sort !== 'site' ? ` · Most ${source.sort}` : '';
    const period = source.period !== 'allTime' ? ` · ${source.period}` : '';
    return base + category + ranking + period;
  }

  function extractNextData(doc) {
    const node = doc.querySelector('script#__NEXT_DATA__');
    if (!node?.textContent) throw new Error('Page data was not found.');
    const json = JSON.parse(node.textContent);
    return json?.props?.pageProps || {};
  }

  function mergeVideo(brief, detail) {
    if (!detail) return brief;
    return {
      ...brief,
      _id: brief._id || detail._id,
      title: brief.title !== 'Untitled video' ? brief.title : detail.title,
      description: brief.description || detail.description,
      thumbnail: brief.thumbnail || detail.thumbnail,
      created_at: Number.isFinite(Date.parse(brief.created_at)) ? brief.created_at : detail.created_at,
      views: Number.isFinite(brief.views) ? brief.views : detail.views,
      likes: Number.isFinite(brief.likes) ? brief.likes : detail.likes,
      comments: Number.isFinite(brief.comments) ? brief.comments : detail.comments,
      tags: brief.tags.length ? brief.tags : detail.tags
    };
  }

  function rankComplete(items, source, now = Date.now()) {
    let result = items.filter(item => source.kind !== 'search' || !source.category || item.tags.some(tag => tag.url === source.category));
    const cutoff = periodCutoff(source.period, now);
    if (cutoff !== null) {
      result = result.filter(item => Number.isFinite(Date.parse(item.created_at)) && Date.parse(item.created_at) >= cutoff);
    }
    if (source.sort !== 'site') {
      result = result.map((item, index) => ({ item, index }))
        .sort((a, b) => (b.item[source.sort] - a.item[source.sort]) || a.index - b.index)
        .map(entry => entry.item);
    }
    return result;
  }

  class Queue {
    constructor(fetchPage, onChange = () => {}) {
      this.fetchPage = fetchPage;
      this.onChange = onChange;
      this.source = null;
      this.items = [];
      this.index = -1;
      this.page = 0;
      this.hasMore = false;
      this.loading = false;
      this.error = '';
      this.generation = 0;
      this.controller = null;
      this.pending = null;
      this.isolated = false;
    }
    notify() { this.onChange(this); }
    cancel() {
      this.generation++;
      this.controller?.abort();
      this.controller = null;
      this.pending = null;
      this.loading = false;
    }
    async replace(source) {
      this.cancel();
      const generation = this.generation;
      const controller = new AbortController();
      this.controller = controller;
      this.loading = true;
      this.error = '';
      this.notify();
      try {
        const result = await this.fetchPage(source, 1, controller.signal);
        if (generation !== this.generation) return false;
        this.source = source;
        this.items = dedupe(result.items);
        this.index = -1;
        this.page = 1;
        this.hasMore = !!result.hasMore;
        this.isolated = false;
        this.error = '';
        return true;
      } catch (error) {
        if (generation === this.generation && error?.name !== 'AbortError') this.error = error.message || 'Results could not be loaded.';
        return false;
      } finally {
        if (generation === this.generation) { this.loading = false; this.notify(); }
      }
    }
    installComplete(source, items) {
      this.cancel();
      this.source = source;
      this.items = dedupe(items);
      this.index = -1;
      this.page = 1;
      this.hasMore = false;
      this.isolated = false;
      this.error = '';
      this.notify();
    }
    installDirect(item) {
      this.cancel();
      this.source = { kind: 'direct' };
      this.items = dedupe([item]);
      this.index = this.items.length ? 0 : -1;
      this.page = 1;
      this.hasMore = false;
      this.isolated = true;
      this.error = '';
      this.notify();
    }
    async loadMore() {
      if (!this.hasMore || !this.source) return false;
      if (this.pending) return this.pending;
      const generation = this.generation;
      const page = this.page + 1;
      const controller = this.controller || new AbortController();
      this.controller = controller;
      this.loading = true;
      this.error = '';
      this.notify();
      this.pending = (async () => {
        try {
          const result = await this.fetchPage(this.source, page, controller.signal);
          if (generation !== this.generation) return false;
          const before = this.items.length;
          const seen = new Set(this.items.flatMap(item => [videoKey(item), `slug:${item.url}`]));
          for (const item of dedupe(result.items)) {
            if (seen.has(videoKey(item)) || seen.has(`slug:${item.url}`)) continue;
            seen.add(videoKey(item)); seen.add(`slug:${item.url}`);
            this.items.push(item);
          }
          if (result.hasMore && this.items.length === before) {
            // The source can legitimately repeat entries, but a whole repeated page
            // cannot advance the visible queue safely.
            throw new Error('The next page contained no new results.');
          }
          this.page = page;
          this.hasMore = !!result.hasMore;
          return true;
        } catch (error) {
          if (generation === this.generation && error?.name !== 'AbortError') this.error = error.message || 'More results could not be loaded.';
          return false;
        } finally {
          if (generation === this.generation) { this.loading = false; this.pending = null; this.notify(); }
        }
      })();
      return this.pending;
    }
    select(index) {
      if (index < 0 || index >= this.items.length) return false;
      this.index = index;
      this.notify();
      return true;
    }
    async move(delta) {
      const target = this.index + delta;
      if (target < 0) return 'start';
      while (target >= this.items.length && this.hasMore) {
        if (!await this.loadMore()) return this.error ? 'error' : 'end';
      }
      if (target >= this.items.length) return 'end';
      this.select(target);
      return 'selected';
    }
  }

  function periodCutoff(period, now) {
    const days = { today: 1, week: 7, month: 30 };
    return days[period] ? now - days[period] * 86400000 : null;
  }

  function knownOutsideFilters(item, source, cutoff) {
    if (cutoff !== null) {
      const date = Date.parse(item.created_at);
      if (Number.isFinite(date) && date < cutoff) return true;
    }
    return source.kind === 'search' && !!source.category && item.tags.length > 0 &&
      !item.tags.some(tag => tag.url === source.category);
  }

  function needsDetail(item, source) {
    if (source.kind === 'search' && source.category && !item.tags.length) return true;
    if (source.period !== 'allTime' && !Number.isFinite(Date.parse(item.created_at))) return true;
    if (source.sort !== 'site' && !Number.isFinite(item[source.sort])) return true;
    return false;
  }

  async function collectComplete(source, fetchPageFn, enrichFn, signal, onProgress = () => {}, now = Date.now()) {
    const all = [];
    const pageController = new AbortController();
    const abortPages = () => pageController.abort();
    signal?.addEventListener('abort', abortPages, { once: true });
    if (signal?.aborted) abortPages();
    const pending = new Map();
    const schedule = page => {
      if (!pending.has(page)) {
        // Resolve errors into values so speculative pages can be cancelled at the end
        // without leaving an unhandled rejection.
        pending.set(page, Promise.resolve().then(() => fetchPageFn(source, page, pageController.signal))
          .then(result => ({ result }), error => ({ error })));
      }
    };
    let page = 1;
    let hasMore = true;
    schedule(page);
    try {
      while (hasMore) {
        if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
        const { result, error } = await pending.get(page);
        pending.delete(page);
        if (error) throw error;
        if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
        if (!Array.isArray(result?.items)) throw new Error('Source returned an unexpected page.');
        all.push(...result.items);
        hasMore = !!result.hasMore;
        onProgress({ phase: 'pages', page, records: all.length });
        if (hasMore && !result.items.length) throw new Error('Source reported more results but returned an empty page.');
        if (hasMore) for (let next = page + 1; next <= page + 4; next++) schedule(next);
        page++;
      }
    } finally {
      pageController.abort();
      signal?.removeEventListener('abort', abortPages);
    }

    const unique = dedupe(all);
    const cutoff = periodCutoff(source.period, now);
    const candidates = unique.filter(item => !knownOutsideFilters(item, source, cutoff));
    const checked = new Array(candidates.length);
    let done = 0;
    onProgress({ phase: 'metadata', done, total: candidates.length, skipped: unique.length - candidates.length });
    for (let start = 0; start < candidates.length; start += 2) {
      if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
      await Promise.all(candidates.slice(start, start + 2).map(async (item, offset) => {
        const full = needsDetail(item, source) ? await enrichFn(item, signal) : item;
        if (needsDetail(full, source)) throw new Error(`Required metadata is missing for ${item.url}.`);
        checked[start + offset] = full;
        done++;
        onProgress({ phase: 'metadata', done, total: candidates.length, skipped: unique.length - candidates.length });
      }));
    }
    if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
    return { items: rankComplete(checked, source, now), scanned: unique.length, skipped: unique.length - candidates.length };
  }

  if (TEST) {
    globalThis.__RM_CORE__ = { Queue, normalizeVideo, dedupe, listingUrl, sourceFromControls, sourceLabel, extractNextData, mergeVideo, rankComplete, collectComplete };
    return;
  }

  if (!/^https?:\/\/(?:www\.)?reelsmunkey\.com$/.test(location.origin) || document.getElementById('rm-enhanced-host')) return;

  const cachedPages = new Map();
  const details = new Map();
  const detailPending = new Map();
  let detailActive = 0;
  const detailWaiters = [];
  const originalUrl = location.href;
  const initialPath = location.pathname;
  const initialProps = (() => { try { return extractNextData(document); } catch { return null; } })();
  const tags = Array.isArray(initialProps?.tags) ? initialProps.tags : Array.isArray(initialProps?.allTags) ? initialProps.allTags : [];
  const prefs = (() => {
    const defaults = { muted: true, volume: 0.7, loop: true, autoAdvance: false, cardSize: 230 };
    try { return { ...defaults, ...JSON.parse(localStorage.getItem(PREF_KEY) || '{}') }; }
    catch { return defaults; }
  })();
  for (const raw of Array.isArray(initialProps?.data) ? initialProps.data : []) {
    const item = normalizeVideo(raw);
    if (item) details.set(item.url, item);
  }
  const savePrefs = () => { try { localStorage.setItem(PREF_KEY, JSON.stringify(prefs)); } catch {} };

  async function request(url, signal, json = false) {
    if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
    const timeout = new AbortController();
    const timer = setTimeout(() => timeout.abort(), REQUEST_TIMEOUT);
    const onAbort = () => timeout.abort();
    signal?.addEventListener('abort', onAbort, { once: true });
    try {
      const response = await fetch(url, { credentials: 'same-origin', signal: timeout.signal, headers: { Accept: json ? 'application/json' : 'text/html' } });
      if (!response.ok) throw new Error(`Request failed (${response.status}).`);
      return json ? response.json() : response.text();
    } catch (error) {
      if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
      if (timeout.signal.aborted) throw new Error('Request timed out.');
      throw error;
    } finally {
      clearTimeout(timer);
      signal?.removeEventListener('abort', onAbort);
    }
  }

  async function pageProps(url, signal) {
    if (cachedPages.has(url)) return cachedPages.get(url);
    const html = await request(url, signal);
    const doc = new DOMParser().parseFromString(html, 'text/html');
    const props = extractNextData(doc);
    cachedPages.set(url, props);
    if (cachedPages.size > 40) cachedPages.delete(cachedPages.keys().next().value);
    return props;
  }

  async function fetchPage(source, page, signal) {
    if (source.kind === 'search') {
      const params = new URLSearchParams({ search: source.query, page: String(page) });
      const response = await request(`/api/search/query?${params}`, signal, true);
      if (!Array.isArray(response?.videos) || typeof response?.hasMore !== 'boolean') throw new Error('Search returned an unexpected response.');
      return { items: response.videos, hasMore: response.hasMore };
    }
    const path = listingUrl(source, page);
    const props = page === 1 && path === initialPath && Array.isArray(initialProps?.data) ? initialProps : await pageProps(path, signal);
    if (!Array.isArray(props.data)) throw new Error('Listing data was not found.');
    const count = Number(props.count);
    const currentPage = Number(props.currentPage) || page;
    return { items: props.data, hasMore: Number.isFinite(count) ? currentPage * PAGE_SIZE < count : props.data.length === PAGE_SIZE };
  }

  async function withDetailSlot(work, signal) {
    if (detailActive >= 2) await new Promise((resolve, reject) => {
      const waiter = { resolve, reject };
      detailWaiters.push(waiter);
      signal?.addEventListener('abort', () => {
        const index = detailWaiters.indexOf(waiter);
        if (index >= 0) detailWaiters.splice(index, 1);
        reject(new DOMException('Cancelled', 'AbortError'));
      }, { once: true });
    });
    if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
    detailActive++;
    try { return await work(); }
    finally { detailActive--; detailWaiters.shift()?.resolve(); }
  }

  function findDetail(props, slug) {
    const candidates = [props.video, props.data, props.videoData, props.item, props.post];
    for (const candidate of candidates) {
      if (Array.isArray(candidate)) {
        const found = candidate.find(item => item?.url === slug);
        if (found) return normalizeVideo(found);
      } else if (candidate && typeof candidate === 'object') {
        const found = normalizeVideo(candidate);
        if (found?.url === slug) return found;
      }
    }
    return null;
  }

  async function enrich(item, signal) {
    if (details.has(item.url)) return mergeVideo(item, details.get(item.url));
    if (!detailPending.has(item.url)) {
      const promise = withDetailSlot(async () => {
        const props = await pageProps(`/video/${encodeURIComponent(item.url)}`, signal);
        const detail = findDetail(props, item.url);
        if (!detail) throw new Error(`Metadata unavailable for ${item.url}.`);
        details.set(item.url, detail);
        return detail;
      }, signal).finally(() => detailPending.delete(item.url));
      detailPending.set(item.url, promise);
    }
    return mergeVideo(item, await detailPending.get(item.url));
  }

  const host = document.createElement('div');
  host.id = 'rm-enhanced-host';
  document.body.append(host);
  const root = host.attachShadow({ mode: 'open' });
  root.innerHTML = `
    <style>
      :host{all:initial;color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;color:#f5f5f5}
      *{box-sizing:border-box}button,input,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.4;cursor:default}
      #app{position:fixed;inset:0;z-index:2147483647;display:flex;background:#000;color:#f5f5f5;pointer-events:auto}
      #app.original{background:transparent;pointer-events:none}#app.original>*:not(#return){display:none}#return{display:none}
      #app.original #return{display:block;position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom));pointer-events:auto;background:#fff;color:#000;border:1px solid #fff;border-radius:999px;padding:13px 18px;box-shadow:0 8px 32px #000a;font-weight:700}
      aside{width:220px;flex:none;border-right:1px solid #242424;padding:28px 16px;display:flex;flex-direction:column;gap:24px;background:#000}
      .brand{font-size:22px;font-weight:800;letter-spacing:-.06em}.brand b{color:#fff}.nav{display:flex;flex-direction:column;gap:6px}
      .nav button{background:transparent;border:1px solid transparent;border-radius:11px;color:#c8c8c8;text-align:left;padding:13px;font-weight:650;min-height:46px}
      .nav button:hover,.nav button.active{background:#171717;border-color:#282828;color:#fff}.spacer{flex:1}.native{border-color:#333!important}
      main{min-width:0;flex:1;overflow:auto;scrollbar-color:#444 #000}#explore{max-width:1440px;margin:auto;padding:34px clamp(18px,3.5vw,56px) 90px}
      h1{font-size:clamp(30px,4vw,48px);letter-spacing:-.055em;margin:0 0 7px;line-height:1.1}p{margin:0}.lead{color:#aaa;font-size:14px;margin-bottom:25px}
      .controls{background:#0a0a0a;border:1px solid #292929;border-radius:16px;padding:17px;display:grid;grid-template-columns:minmax(180px,2fr) repeat(3,minmax(120px,1fr)) auto;gap:12px;align-items:end;position:sticky;top:10px;z-index:2}
      label{display:flex;flex-direction:column;gap:6px;color:#aaa;font-size:12px;font-weight:700;min-width:0}input,select{height:44px;width:100%;min-width:0;background:#000;color:#fff;border:1px solid #3a3a3a;border-radius:9px;padding:0 12px;outline:none}
      input:focus-visible,select:focus-visible,button:focus-visible{outline:2px solid #fff;outline-offset:2px}input[type=range]{accent-color:#fff}
      .primary{height:44px;background:#fff;border:1px solid #fff;color:#000;border-radius:9px;padding:0 18px;font-weight:800;white-space:nowrap}.primary:hover{background:#ddd}
      .meta{display:flex;justify-content:space-between;align-items:center;gap:16px;margin:24px 0 17px;flex-wrap:wrap}.meta h2{margin:0;font-size:20px;letter-spacing:-.03em}.sub{color:#999;font-size:13px}
      .grid-tools{display:flex;align-items:center;gap:9px;color:#aaa;font-size:12px;font-weight:700}.grid-tools button{width:44px;height:44px;border:1px solid #333;background:#111;border-radius:9px;color:#fff;font-size:22px;line-height:1}.grid-tools button:hover{background:#222}
      .grid-tools input{width:112px;height:36px;padding:0;border:0;background:transparent}.grid-tools output{min-width:54px;text-align:right;color:#ddd;font-variant-numeric:tabular-nums}
      #notice{min-height:0;color:#ccc;font-size:13px;margin:0 0 12px}#notice:empty{display:none}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--rm-card-size,230px)),1fr));gap:14px}
      .card{display:block;text-align:left;color:#fff;background:#080808;border:1px solid #252525;border-radius:12px;overflow:hidden;padding:0;min-width:0;transition:transform .15s,border-color .15s}.card:hover{transform:translateY(-3px);border-color:#777}
      .thumb{aspect-ratio:9/13;background:#111;position:relative;display:grid;place-items:center;color:#888}.thumb img{width:100%;height:100%;object-fit:cover;display:block}.thumb .play{position:absolute;right:10px;bottom:10px;background:#000b;border:1px solid #aaa;border-radius:50%;width:34px;height:34px;display:grid;place-items:center;color:#fff}
      .caption{padding:12px;min-height:66px;font-size:13px;font-weight:650;line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
      .state{padding:32px 14px;text-align:center;color:#aaa}.secondary{background:#111;border:1px solid #444;border-radius:9px;color:#fff;padding:11px 17px;font-weight:700;min-height:44px}#load{display:none;margin:25px auto}
      #viewer{display:none;position:fixed;inset:0;background:#000;z-index:3;min-width:0}.viewing #viewer{display:block}.viewing aside,.viewing main{display:none}
      .viewer-head{position:absolute;top:0;left:0;right:0;min-height:60px;display:flex;align-items:flex-start;justify-content:space-between;padding:14px 18px;z-index:5;pointer-events:none}
      .viewer-head button{pointer-events:auto;background:#000c;border:1px solid #555;border-radius:999px;color:#fff;padding:10px 15px;min-height:42px;font-weight:700}.viewer-head button:hover{background:#222}
      .viewer-body{width:min(100%,1600px);height:100dvh;margin:auto;padding:10px 18px;display:grid;grid-template-columns:minmax(0,1fr) minmax(320px,min(640px,calc((100dvh - 20px)*.62))) minmax(0,1fr);gap:18px;align-items:end}
      .stage{grid-column:2;grid-row:1;height:calc(100dvh - 20px);width:100%;position:relative;background:#050505;border:1px solid #222;border-radius:9px;overflow:hidden;box-shadow:0 16px 50px #000}
      .gesture-layer{position:absolute;inset:0;z-index:1;pointer-events:none;touch-action:pan-x pinch-zoom;cursor:pointer}.gesture-layer.active{pointer-events:auto}
      #frameHolder{width:100%;height:100%}iframe{width:100%;height:100%;border:0;background:#000}.overlay{position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:#000b;text-align:center;padding:28px;z-index:2}.overlay.show{display:flex}.overlay .primary{height:auto;min-height:48px}
      .player-controls{position:absolute;bottom:0;left:0;right:0;padding:28px 12px 12px;background:linear-gradient(transparent,#000f);display:flex;align-items:center;gap:8px;z-index:3}
      .player-controls button{border:1px solid #ffffff44;background:#0009;color:#fff;border-radius:50%;min-width:42px;height:42px;padding:0 8px;font-weight:800}.player-controls button:hover{background:#333}
      .player-controls input[type=range]{height:28px;padding:0;border:0;box-shadow:none;background:transparent;accent-color:#fff;min-width:0}.seek{flex:1}.volume{width:62px!important;flex:none}.time{font-size:11px;white-space:nowrap;color:#eee;font-variant-numeric:tabular-nums}
      .info{grid-column:1;grid-row:1;align-self:end;justify-self:end;width:min(100%,300px);padding-bottom:22px;min-width:0}.info-inner{padding:12px}.eyebrow{font-size:10px;font-weight:800;letter-spacing:.12em;color:#aaa;margin-bottom:12px}.eyebrow span{display:block;letter-spacing:normal;text-transform:none;font-size:12px;font-weight:500;margin-top:5px;line-height:1.4;overflow-wrap:anywhere}
      .info h2{font-size:20px;line-height:1.28;letter-spacing:-.03em;margin:0 0 10px;overflow-wrap:anywhere}.info p{color:#c9c9c9;font-size:13px;line-height:1.5;display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden}
      .tags{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.tags:empty{display:none}.tags span{border:1px solid #333;border-radius:999px;padding:5px 8px;color:#bdbdbd;font-size:11px}
      .queue-state{min-height:18px;color:#ddd;font-size:12px;line-height:1.35;margin-top:12px}.options{display:flex;flex-wrap:wrap;gap:14px;color:#bbb;font-size:12px;margin-top:12px}.options label{display:flex;flex-direction:row;align-items:center;gap:7px;font-weight:500;font-size:12px}.options input{width:18px;height:18px;accent-color:#fff}
      .actions{grid-column:3;grid-row:1;align-self:end;justify-self:start;width:70px;display:flex;flex-direction:column;align-items:center;gap:12px;padding-bottom:30px}
      .rail-button{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;background:transparent;border:0;color:#fff;min-width:62px;min-height:62px;border-radius:11px}.rail-button:hover{background:#171717}.rail-button span{font-size:25px;line-height:1}.rail-button small{font-size:10px;color:#ccc}.rail-divider{height:1px;width:36px;background:#333;margin:3px 0}
      .metric{display:flex;flex-direction:column;align-items:center;gap:2px;color:#ddd;font-variant-numeric:tabular-nums}.metric>span{font-size:24px;line-height:1.1}.metric strong{font-size:12px;color:#fff}.metric small{font-size:10px;color:#999}
      @media(max-width:1100px){aside{width:76px;padding:18px 8px;align-items:center}.brand{font-size:15px}.brand span,.nav button span,.native span{display:none}.nav button{font-size:20px;text-align:center;width:52px;padding:9px}.controls{grid-template-columns:repeat(2,minmax(0,1fr));position:static}.controls label:first-child,.controls .primary{grid-column:1/-1}.viewer-body{gap:8px}.info h2{font-size:17px}}
      @media(max-width:900px){
        #viewer{--rm-sheet:calc(178px + env(safe-area-inset-bottom))}.viewer-head{top:env(safe-area-inset-top);padding:10px 12px}.viewer-head button{min-height:44px}
        .viewer-body{display:block;width:100%;height:100dvh;padding:0}.stage{position:absolute;top:0;left:0;right:0;width:100%;height:calc(100dvh - var(--rm-sheet));border:0;border-radius:0;box-shadow:none}
        .info{position:absolute;left:0;right:0;bottom:0;width:100%;height:var(--rm-sheet);padding:0;overflow:auto;background:#000;border-top:1px solid #252525}.info-inner{padding:12px 16px calc(10px + env(safe-area-inset-bottom))}.eyebrow{margin-bottom:5px}.eyebrow span{display:inline;margin-left:7px}.info h2{font-size:15px;margin-bottom:5px}.info p{font-size:12px;-webkit-line-clamp:2}.tags{margin-top:7px;flex-wrap:nowrap;overflow:hidden}.tags span{white-space:nowrap}.queue-state{margin-top:6px}.options{margin-top:7px}
        .actions{position:absolute;right:10px;bottom:calc(var(--rm-sheet) + 72px);width:54px;gap:5px;padding:0;z-index:4}.rail-button{min-width:50px;min-height:50px;background:#000b;border:1px solid #ffffff33;border-radius:50%}.rail-button span{font-size:22px}.rail-button small{font-size:8px}.rail-divider{margin:2px 0}.metric{background:#000a;border-radius:9px;width:50px;padding:4px 2px}.metric>span{font-size:19px}.metric strong{font-size:10px}.metric small{font-size:8px}
        .player-controls{padding:22px 8px 8px;gap:5px}.player-controls button{min-width:42px;height:42px}.volume{display:none}
      }
      @media(max-width:620px){
        #app{display:block}aside{position:fixed;bottom:0;left:0;right:0;width:100%;height:calc(60px + env(safe-area-inset-bottom));z-index:3;flex-direction:row;padding:5px 8px calc(5px + env(safe-area-inset-bottom));border-right:0;border-top:1px solid #242424;background:#000}
        .brand,.spacer{display:none}.nav{flex-direction:row;width:100%;justify-content:space-around}.nav button{width:auto;min-width:78px;font-size:13px}.nav button span,.native span{display:inline}.native{font-size:12px!important}
        main{height:calc(100dvh - 60px - env(safe-area-inset-bottom))}#explore{padding:22px 12px 75px}.controls{grid-template-columns:repeat(2,minmax(0,1fr));position:static;padding:12px;gap:9px}.controls label:first-child{grid-column:1/-1}.controls .primary{grid-column:1/-1;min-height:48px}
        .meta{align-items:flex-start}.grid-tools{width:100%;justify-content:flex-end}.grid-tools input{flex:1;max-width:160px}.grid{grid-template-columns:repeat(var(--rm-mobile-cols,2),minmax(0,1fr));gap:9px}.caption{font-size:12px;min-height:57px}.card{border-radius:9px}
        .viewer-head button{font-size:12px;padding:9px 11px}.time{font-size:10px}.player-controls button{min-width:38px;height:38px}.actions{right:8px}
      }
      @media(max-height:620px) and (max-width:900px){#viewer{--rm-sheet:calc(132px + env(safe-area-inset-bottom))}.info p,.tags{display:none}.actions{bottom:calc(var(--rm-sheet) + 55px)}}
      @media(max-height:500px) and (orientation:landscape) and (max-width:900px){
        .viewer-body{display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,min(420px,calc((100dvh - 12px)*.9))) minmax(0,1fr);gap:8px;padding:6px;height:100dvh;align-items:end}
        .stage{grid-column:2;grid-row:1;position:relative;inset:auto;width:100%;height:calc(100dvh - 12px);border:1px solid #222;border-radius:7px}
        .info{grid-column:1;grid-row:1;position:static;width:100%;height:auto;max-height:calc(100dvh - 80px);padding:0;overflow:auto;border:0;align-self:end;justify-self:end}.info-inner{padding:8px}.info h2{font-size:13px}.info p,.tags{display:none}.options{gap:8px}
        .actions{grid-column:3;grid-row:1;position:static;width:52px;gap:2px;padding:0 0 8px;align-self:end;justify-self:start}.rail-button{min-width:42px;min-height:42px}.rail-button span{font-size:18px}.metric{width:42px;padding:2px}.metric>span{font-size:15px}.metric strong{font-size:9px}.metric small{display:none}
        .player-controls{gap:3px;padding:16px 4px 5px}.player-controls button{min-width:33px;height:33px}.time{font-size:9px}
      }
      @media(prefers-reduced-motion:reduce){.card{transition:none}}
    </style>
    <div id="app">
      <aside><div class="brand">reels<b>munkey</b><span> / enhanced</span></div><div class="nav"><button id="navExplore" class="active">⌗ <span>Explore</span></button><button id="navTop">↗ <span>Top viewed</span></button></div><div class="spacer"></div><div class="nav"><button id="native" class="native">↙ <span>Original site</span></button></div></aside>
      <main id="main"><section id="explore"><h1>Explore videos</h1><p class="lead">Find a video, then keep its exact result order as you watch.</p>
        <form class="controls" id="filterForm"><label>Search<input id="query" type="search" placeholder="Search videos" autocomplete="off"></label><label>Category<select id="category"><option value="">All categories</option></select></label><label>Order<select id="sort"><option value="site">Site order</option><option value="views">Most viewed</option><option value="likes">Most liked</option><option value="comments">Most commented</option></select></label><label>Period<select id="period"><option value="allTime">All time</option><option value="today">Today / past 24h</option><option value="week">This week / past 7d</option><option value="month">This month / past 30d</option></select></label><button class="primary" id="apply" type="submit">Show results</button></form>
        <div class="meta"><div><h2 id="heading">Latest videos</h2><div class="sub" id="summary"></div></div><div class="grid-tools"><span>Grid size</span><button id="smaller" type="button" aria-label="Smaller thumbnails">−</button><input id="cardSize" type="range" min="150" max="360" step="10" aria-label="Thumbnail size"><button id="larger" type="button" aria-label="Larger thumbnails">+</button><output id="cardSizeValue" for="cardSize"></output></div></div><div id="notice" role="status"></div><div id="grid" class="grid"></div><div id="empty" class="state" hidden>No results found.</div><button id="load" class="secondary">Load more</button>
      </section></main>
      <section id="viewer" aria-label="Video viewer"><div class="viewer-head"><button id="back">← Explore</button><button id="viewOriginal">Original video ↗</button></div><div class="viewer-body"><div class="info"><div class="info-inner"><div class="eyebrow">NOW PLAYING <span id="position"></span></div><h2 id="videoTitle"></h2><p id="videoDescription"></p><div id="videoTags" class="tags"></div><div class="queue-state" id="queueState" role="status"></div><div class="options"><label><input id="loop" type="checkbox">Loop</label><label><input id="advance" type="checkbox">Auto advance</label></div></div></div><div class="stage" id="stage"><div id="frameHolder"></div><div id="gestureLayer" class="gesture-layer" aria-hidden="true"></div><div class="overlay" id="playOverlay"><button class="primary" id="tapPlay">Tap to play</button></div><div class="player-controls"><button id="play" aria-label="Play or pause">▶</button><span class="time" id="time">0:00</span><input class="seek" id="seek" type="range" min="0" max="1000" value="0" aria-label="Seek"><button id="mute" aria-label="Mute or unmute">◖</button><input class="volume" id="volume" type="range" min="0" max="1" step="0.05" aria-label="Volume"><button id="fullscreen" aria-label="Fullscreen">⛶</button></div></div><div class="actions"><button id="previous" class="rail-button" aria-label="Previous result"><span aria-hidden="true">↑</span><small>Previous</small></button><button id="next" class="rail-button" aria-label="Next result"><span aria-hidden="true">↓</span><small>Next</small></button><div class="rail-divider"></div><div class="metric"><span aria-hidden="true">◉</span><strong id="viewCount">—</strong><small>views</small></div><div class="metric"><span aria-hidden="true">♡</span><strong id="likeCount">—</strong><small>likes</small></div><div class="metric"><span aria-hidden="true">◌</span><strong id="commentCount">—</strong><small>comments</small></div></div></div></section>
      <button id="return">Return to Enhanced</button>
    </div>`;

  const $ = id => root.getElementById(id);
  const ui = Object.fromEntries(['app','main','grid','heading','summary','notice','empty','load','query','category','sort','period','apply','cardSize','cardSizeValue','smaller','larger','viewer','frameHolder','stage','gestureLayer','playOverlay','tapPlay','play','seek','time','mute','volume','fullscreen','videoTitle','videoDescription','videoTags','position','viewCount','likeCount','commentCount','queueState','previous','next','loop','advance'].map(id => [id, $(id)]));
  function populateTags(list) {
    for (const tag of list) {
      if (!tag?.url || !tag?.tag || Array.from(ui.category.options).some(option => option.value === tag.url)) continue;
      const option = document.createElement('option');
      option.value = tag.url;
      option.textContent = tag.tag;
      ui.category.append(option);
    }
  }
  populateTags(tags);
  ui.volume.value = String(prefs.volume);
  ui.loop.checked = !!prefs.loop;
  ui.advance.checked = !!prefs.autoAdvance;

  function cardSizeValue() {
    const size = Number(prefs.cardSize);
    return Number.isFinite(size) ? Math.min(360, Math.max(150, Math.round(size / 10) * 10)) : 230;
  }
  function updateCardSize(size = cardSizeValue()) {
    prefs.cardSize = Math.min(360, Math.max(150, Math.round(Number(size) / 10) * 10));
    const columns = prefs.cardSize < 180 ? 3 : prefs.cardSize < 280 ? 2 : 1;
    ui.app.style.setProperty('--rm-card-size', `${prefs.cardSize}px`);
    ui.app.style.setProperty('--rm-mobile-cols', String(columns));
    ui.cardSize.value = String(prefs.cardSize);
    ui.cardSizeValue.textContent = window.matchMedia('(max-width:620px)').matches ? `${columns} col` : `${prefs.cardSize}px`;
    savePrefs();
  }
  updateCardSize();
  ui.cardSize.addEventListener('input', () => updateCardSize(ui.cardSize.value));
  ui.smaller.addEventListener('click', () => updateCardSize(cardSizeValue() - 10));
  ui.larger.addEventListener('click', () => updateCardSize(cardSizeValue() + 10));
  window.addEventListener('resize', () => updateCardSize());

  let mode = 'explore';
  let scrollPosition = 0;
  let renderedCount = 0;
  let player = null;
  let frame = null;
  let frameGeneration = 0;
  let frameObserver = null;
  let wheelUntil = 0;
  let wheelDistance = 0;
  let wheelLastAt = 0;
  let lastSwipeAt = 0;
  let navigationPending = false;
  let navigationToken = 0;
  let scanController = null;
  let scanGeneration = 0;
  let nativeStartRoute = '';
  const queue = new Queue(fetchPage, () => refresh());

  function thumbUrl(filename) {
    if (!filename || !/^[\w.-]+$/.test(filename)) return '';
    return IMAGE_SITE + encodeURIComponent(filename);
  }

  function addCard(item, index) {
    const button = document.createElement('button');
    button.type = 'button'; button.className = 'card';
    button.setAttribute('aria-label', `Open ${item.title}`);
    const thumb = document.createElement('div'); thumb.className = 'thumb';
    const image = thumbUrl(item.thumbnail);
    if (image) {
      const img = document.createElement('img'); img.src = image; img.alt = ''; img.loading = 'lazy';
      thumb.append(img);
    } else {
      const placeholder = document.createElement('span'); placeholder.textContent = 'Preview unavailable'; thumb.append(placeholder);
      enrich(item, queue.controller?.signal).then(full => {
        if (full.thumbnail && queue.items[index]?.url === item.url) {
          queue.items[index] = full;
          const img = document.createElement('img'); img.src = thumbUrl(full.thumbnail); img.alt = ''; img.loading = 'lazy';
          if (img.src) thumb.replaceChildren(img);
        }
      }).catch(() => {});
    }
    const play = document.createElement('span'); play.className = 'play'; play.textContent = '▶'; thumb.append(play);
    const caption = document.createElement('div'); caption.className = 'caption'; caption.textContent = item.title;
    button.append(thumb, caption);
    button.addEventListener('click', () => openIndex(index));
    ui.grid.append(button);
  }

  function renderGrid() {
    if (renderedCount > queue.items.length) { ui.grid.replaceChildren(); renderedCount = 0; }
    while (renderedCount < queue.items.length) { addCard(queue.items[renderedCount], renderedCount); renderedCount++; }
    ui.empty.hidden = queue.loading || queue.items.length > 0;
    ui.load.style.display = queue.hasMore && mode === 'explore' ? 'block' : 'none';
    ui.load.disabled = queue.loading;
    ui.load.textContent = queue.loading ? 'Loading…' : 'Load more';
  }

  function refresh() {
    if (queue.source) ui.heading.textContent = queue.source.kind === 'direct' ? 'Direct video' : sourceLabel(queue.source);
    ui.summary.textContent = `${queue.items.length} result${queue.items.length === 1 ? '' : 's'} loaded${queue.hasMore ? ' · More available' : ''}`;
    ui.notice.textContent = queue.error || (queue.loading && mode === 'explore' ? 'Loading results…' : '');
    renderGrid();
    if (mode === 'viewer') updateViewerInfo();
  }

  function draft() { return { query: ui.query.value, category: ui.category.value, sort: ui.sort.value, period: ui.period.value }; }
  function updateDraftNotice() {
    if (scanController) { scanGeneration++; scanController.abort(); scanController = null; }
    const source = sourceFromControls(draft());
    ui.apply.textContent = source.fullScan ? 'Build complete list' : 'Show results';
    if (source.fullScan) ui.notice.textContent = 'This combination requires every source page before filtering or ranking. Build complete list starts a cancellable scan.';
  }

  async function applyControls() {
    const source = sourceFromControls(draft());
    const generation = ++scanGeneration;
    scanController?.abort();
    scanController = null;
    if (source.fullScan) {
      queue.cancel();
      scanController = new AbortController();
      const signal = scanController.signal;
      ui.apply.textContent = 'Cancel scan';
      ui.notice.textContent = 'Building the complete matching list. This can take time; click Cancel scan to stop.';
      try {
        const result = await collectComplete(source, fetchPage, enrich, signal, progress => {
          if (generation !== scanGeneration) return;
          ui.notice.textContent = progress.phase === 'pages' ?
            `Scanning source page ${progress.page} · ${progress.records} records. Click Cancel scan to stop.` :
            `Checking metadata ${progress.done} / ${progress.total} · ${progress.skipped} already outside the selected filters. Click Cancel scan to stop.`;
        });
        if (generation !== scanGeneration || signal.aborted) return;
        queue.installComplete(source, result.items);
        resetGrid();
        ui.notice.textContent = `Complete list built from ${result.scanned} source records; ${result.skipped} excluded before metadata checks.`;
      } catch (error) {
        if (generation === scanGeneration) ui.notice.textContent = error?.name === 'AbortError' ? 'Scan cancelled. Previous results are intact.' : `Scan failed: ${error.message} Previous results are intact.`;
      } finally {
        if (generation === scanGeneration) { scanController = null; ui.apply.textContent = source.fullScan ? 'Build complete list' : 'Show results'; }
      }
      return;
    }
    ui.apply.disabled = true;
    const ok = await queue.replace(source);
    if (generation === scanGeneration) {
      ui.apply.disabled = false;
      if (ok) { resetGrid(); ui.main.scrollTop = 0; }
    }
  }

  function resetGrid() { ui.grid.replaceChildren(); renderedCount = 0; refresh(); }

  function pauseOtherVideos() { for (const video of document.querySelectorAll('video')) { try { video.pause(); } catch {} } }

  function disposePlayer() {
    frameGeneration++;
    frameObserver?.disconnect(); frameObserver = null;
    if (player) { try { player.pause(); } catch {} }
    player = null;
    if (frame) { frame.src = 'about:blank'; frame.remove(); frame = null; }
    ui.frameHolder.replaceChildren();
    ui.gestureLayer.classList.remove('active');
    ui.playOverlay.classList.remove('show');
    ui.play.disabled = true; ui.seek.disabled = true;
  }

  function formatTime(seconds) {
    if (!Number.isFinite(seconds)) return '0:00';
    const n = Math.floor(seconds);
    return `${Math.floor(n / 60)}:${String(n % 60).padStart(2, '0')}`;
  }

  function syncPlayerControls() {
    if (!player) return;
    ui.play.textContent = player.paused ? '▶' : 'Ⅱ';
    ui.mute.textContent = player.muted ? '◖' : '◗';
    ui.time.textContent = `${formatTime(player.currentTime)} / ${formatTime(player.duration)}`;
    ui.seek.value = Number.isFinite(player.duration) && player.duration > 0 ? String(Math.round(player.currentTime / player.duration * 1000)) : '0';
  }

  function attachPlayer(video, generation) {
    if (generation !== frameGeneration || player) return;
    player = video;
    ui.gestureLayer.classList.add('active');
    for (const other of video.ownerDocument.querySelectorAll('video')) if (other !== video) { try { other.pause(); } catch {} }
    video.muted = !!prefs.muted;
    video.volume = Math.max(0, Math.min(1, Number(prefs.volume)));
    video.loop = !!prefs.loop;
    video.playsInline = true;
    video.controls = false;
    video.preload = 'metadata';
    ui.play.disabled = false; ui.seek.disabled = false;
    for (const event of ['timeupdate','durationchange','play','pause','volumechange']) video.addEventListener(event, syncPlayerControls);
    video.addEventListener('ended', () => { if (!prefs.loop && prefs.autoAdvance && generation === frameGeneration) navigate(1); });
    syncPlayerControls();
    updateViewerInfo();
    Promise.resolve(video.play()).then(() => ui.playOverlay.classList.remove('show')).catch(() => ui.playOverlay.classList.add('show'));
  }

  function prepareFrame(doc, generation) {
    frameObserver?.disconnect(); frameObserver = null;
    doc.addEventListener('keydown', onKeydown, true);
    doc.addEventListener('wheel', onWheel, { passive: false, capture: true });
    doc.addEventListener('touchstart', onTouchStart, { passive: true, capture: true });
    doc.addEventListener('touchend', onTouchEnd, { passive: true, capture: true });
    doc.addEventListener('touchcancel', onTouchCancel, { passive: true, capture: true });
    doc.addEventListener('play', event => { if (event.target?.tagName === 'VIDEO' && event.target !== player) event.target.pause(); }, true);
    const tryAttach = () => {
      if (generation !== frameGeneration) return;
      if (player?.isConnected) return;
      if (player) { try { player.pause(); } catch {} player = null; ui.gestureLayer.classList.remove('active'); }
      const video = doc.querySelector('main video') || doc.querySelector('article video') || doc.querySelector('video');
      if (!video) return;
      const style = doc.createElement('style');
      style.textContent = 'html,body{margin:0!important;width:100%!important;height:100%!important;overflow:hidden!important;background:#000!important}body>*{visibility:hidden!important}video{visibility:visible!important;position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;object-fit:contain!important;touch-action:pan-x!important;z-index:2147483647!important;background:#000!important}';
      doc.head.append(style);
      attachPlayer(video, generation);
    };
    tryAttach();
    frameObserver = new MutationObserver(tryAttach);
    frameObserver.observe(doc.documentElement, { childList: true, subtree: true });
    setTimeout(() => {
      if (generation === frameGeneration && !player) ui.queueState.textContent = 'Site player controls are shown in the frame. Playback controls here need a video element.';
    }, 10000);
  }

  function loadPlayer(item) {
    disposePlayer();
    const generation = frameGeneration;
    pauseOtherVideos();
    ui.queueState.textContent = 'Loading the site player…';
    frame = document.createElement('iframe');
    frame.title = item.title;
    frame.referrerPolicy = 'same-origin';
    frame.allow = 'autoplay; fullscreen; picture-in-picture';
    frame.src = `/video/${encodeURIComponent(item.url)}`;
    frame.addEventListener('load', () => {
      if (generation !== frameGeneration) return;
      try { prepareFrame(frame.contentDocument, generation); }
      catch { ui.queueState.textContent = 'The site player cannot be controlled here. Use “Open original video”.'; }
    });
    ui.frameHolder.append(frame);
    setTimeout(() => {
      if (generation === frameGeneration && !player) ui.queueState.textContent = 'If playback is unavailable here, open the original video.';
    }, 13000);
  }

  function updateViewerInfo() {
    const item = queue.items[queue.index];
    if (!item) return;
    ui.videoTitle.textContent = item.title;
    ui.videoDescription.textContent = item.description;
    ui.videoTags.replaceChildren();
    for (const tag of item.tags.slice(0, 6)) {
      if (!tag.tag) continue;
      const chip = document.createElement('span');
      chip.textContent = `#${tag.tag}`;
      ui.videoTags.append(chip);
    }
    const countText = count => Number.isFinite(count) ? new Intl.NumberFormat().format(count) : '—';
    ui.viewCount.textContent = countText(item.views);
    ui.likeCount.textContent = countText(item.likes);
    ui.commentCount.textContent = countText(item.comments);
    ui.position.textContent = queue.isolated ? 'Direct link · No results queue' : `${queue.index + 1} of ${queue.items.length}${queue.hasMore ? '+' : ''} · ${sourceLabel(queue.source)}`;
    ui.previous.disabled = queue.index <= 0;
    ui.next.disabled = queue.isolated || (queue.index >= queue.items.length - 1 && !queue.hasMore);
    if (queue.error) ui.queueState.textContent = `${queue.error} Try Next again to retry.`;
    else if (queue.isolated) ui.queueState.textContent = 'Open Explore to create a browsing queue.';
    else if (queue.index >= queue.items.length - 1 && !queue.hasMore) ui.queueState.textContent = 'End of results.';
    else if (!queue.loading && player) ui.queueState.textContent = '';
  }

  function enrichCurrentItem() {
    const index = queue.index;
    const item = queue.items[index];
    if (!item || (item.description && item.tags.length && Number.isFinite(item.views) && Number.isFinite(item.likes) && Number.isFinite(item.comments))) return;
    enrich(item, queue.controller?.signal).then(full => {
      if (queue.items[index]?.url !== item.url) return;
      queue.items[index] = full;
      if (mode === 'viewer' && queue.index === index) updateViewerInfo();
    }).catch(() => {});
  }

  function enterViewer(push = true) {
    const item = queue.items[queue.index];
    if (!item) return;
    scrollPosition = ui.main.scrollTop;
    mode = 'viewer';
    ui.app.classList.add('viewing');
    if (push) history.pushState({ rmEnhanced: true, view: 'viewer', slug: item.url }, '', `${location.pathname}${location.search}#rm-reel=${encodeURIComponent(item.url)}`);
    updateViewerInfo();
    loadPlayer(item);
    enrichCurrentItem();
  }

  function openIndex(index) { if (queue.select(index)) enterViewer(); }

  function closeViewer(push = true) {
    if (mode !== 'viewer') return;
    navigationToken++; navigationPending = false;
    if (queue.loading) queue.cancel();
    disposePlayer();
    mode = 'explore';
    ui.app.classList.remove('viewing');
    if (push) history.pushState({ rmEnhanced: true, view: 'explore' }, '', queue.isolated ? '/' : originalUrl);
    ui.main.scrollTop = scrollPosition;
    refresh();
    if (queue.isolated) queue.replace({ kind: 'home', sort: 'site', period: 'allTime' }).then(ok => { if (ok) resetGrid(); });
  }

  async function navigate(delta) {
    if (mode !== 'viewer' || queue.isolated || navigationPending) return;
    navigationPending = true;
    const token = ++navigationToken;
    try {
      const status = await queue.move(delta);
      if (mode !== 'viewer' || token !== navigationToken) return;
      if (status === 'selected') {
        const item = queue.items[queue.index];
        history.replaceState({ rmEnhanced: true, view: 'viewer', slug: item.url }, '', `${originalUrl.split('#')[0]}#rm-reel=${encodeURIComponent(item.url)}`);
        updateViewerInfo();
        loadPlayer(item);
        enrichCurrentItem();
      } else if (status === 'end') ui.queueState.textContent = 'End of results.';
      else if (status === 'start') ui.queueState.textContent = 'Start of results.';
      else if (status === 'error') ui.queueState.textContent = `${queue.error} Press Next to retry.`;
    } finally { if (token === navigationToken) navigationPending = false; }
  }

  $('filterForm').addEventListener('submit', event => {
    event.preventDefault();
    if (scanController) { scanGeneration++; scanController.abort(); scanController = null; updateDraftNotice(); ui.notice.textContent = 'Scan cancelled. Previous results are intact.'; return; }
    applyControls();
  });
  for (const control of [ui.query, ui.category, ui.sort, ui.period]) control.addEventListener(control === ui.query ? 'input' : 'change', updateDraftNotice);
  ui.load.addEventListener('click', () => queue.loadMore());
  ui.main.addEventListener('scroll', () => {
    if (mode === 'explore' && !scanController && queue.hasMore && !queue.loading && ui.main.scrollHeight - ui.main.scrollTop - ui.main.clientHeight < 500) queue.loadMore();
  }, { passive: true });
  $('navExplore').addEventListener('click', () => { closeViewer(); ui.main.scrollTop = 0; });
  $('navTop').addEventListener('click', () => { closeViewer(); ui.query.value = ''; ui.category.value = ''; ui.sort.value = 'views'; ui.period.value = 'today'; applyControls(); });
  $('native').addEventListener('click', () => { nativeStartRoute = location.pathname + location.search; disposePlayer(); ui.app.classList.add('original'); });
  $('return').addEventListener('click', () => {
    if (location.pathname + location.search !== nativeStartRoute) { location.reload(); return; }
    ui.app.classList.remove('original');
    if (mode === 'viewer') loadPlayer(queue.items[queue.index]);
  });
  $('back').addEventListener('click', () => closeViewer());
  $('viewOriginal').addEventListener('click', () => { const item = queue.items[queue.index]; if (item) window.open(`${SITE}/video/${encodeURIComponent(item.url)}`, '_blank', 'noopener'); });
  ui.previous.addEventListener('click', () => navigate(-1));
  ui.next.addEventListener('click', () => navigate(1));
  ui.play.addEventListener('click', () => { if (!player) return; if (player.paused) player.play().then(() => ui.playOverlay.classList.remove('show')).catch(() => ui.playOverlay.classList.add('show')); else player.pause(); });
  ui.tapPlay.addEventListener('click', () => player?.play().then(() => ui.playOverlay.classList.remove('show')).catch(() => {}));
  ui.mute.addEventListener('click', () => { prefs.muted = !prefs.muted; if (player) player.muted = prefs.muted; savePrefs(); syncPlayerControls(); });
  ui.volume.addEventListener('input', () => { prefs.volume = Number(ui.volume.value); prefs.muted = false; if (player) { player.volume = prefs.volume; player.muted = false; } savePrefs(); syncPlayerControls(); });
  ui.seek.addEventListener('input', () => { if (player && Number.isFinite(player.duration)) player.currentTime = player.duration * Number(ui.seek.value) / 1000; });
  ui.fullscreen.addEventListener('click', () => { if (document.fullscreenElement) document.exitFullscreen(); else ui.stage.requestFullscreen?.(); });
  ui.loop.addEventListener('change', () => { prefs.loop = ui.loop.checked; if (player) player.loop = prefs.loop; savePrefs(); });
  ui.advance.addEventListener('change', () => { prefs.autoAdvance = ui.advance.checked; savePrefs(); });

  function onKeydown(event) {
    if (mode !== 'viewer' || ui.app.classList.contains('original') || event.altKey || event.ctrlKey || event.metaKey) return;
    const target = event.composedPath()[0];
    if (target?.closest?.('input,textarea,select,button,[contenteditable="true"],[role="slider"]') || target?.isContentEditable) return;
    const action = { ArrowDown: () => navigate(1), ArrowUp: () => navigate(-1), ' ': () => ui.play.click(), m: () => ui.mute.click(), M: () => ui.mute.click(), Escape: () => closeViewer() }[event.key];
    if (action) { event.preventDefault(); action(); }
  }
  document.addEventListener('keydown', onKeydown, true);
  function onWheel(event) {
    if (mode !== 'viewer' || ui.app.classList.contains('original') || event.ctrlKey || event.target?.closest?.('.player-controls,input,textarea,select,button,[contenteditable="true"],[role="slider"]')) return;
    const distance = event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? window.innerHeight : 1);
    if (!distance) return;
    event.preventDefault();
    const now = Date.now();
    if (now < wheelUntil) { wheelDistance = 0; return; }
    if (now - wheelLastAt > 240 || Math.sign(distance) !== Math.sign(wheelDistance)) wheelDistance = 0;
    wheelLastAt = now;
    wheelDistance += distance;
    if (Math.abs(wheelDistance) < 80) return;
    const direction = Math.sign(wheelDistance);
    wheelDistance = 0;
    wheelUntil = now + 850;
    navigate(direction);
  }
  ui.viewer.addEventListener('wheel', onWheel, { passive: false });
  let touchStart = null;
  function onTouchStart(event) {
    if (mode !== 'viewer' || ui.app.classList.contains('original') || event.touches?.length > 1 || event.target?.closest?.('.player-controls,input,textarea,select,button,[role="slider"]')) { touchStart = null; return; }
    const touch = event.changedTouches[0];
    if (touch) touchStart = { id: touch.identifier, x: touch.clientX, y: touch.clientY };
  }
  ui.stage.addEventListener('touchstart', onTouchStart, { passive: true });
  function onTouchEnd(event) {
    if (!touchStart) return;
    const touch = Array.from(event.changedTouches).find(item => item.identifier === touchStart.id) || event.changedTouches[0];
    if (!touch) { touchStart = null; return; }
    const dx = touch.clientX - touchStart.x, dy = touch.clientY - touchStart.y;
    touchStart = null;
    if (Math.abs(dy) > 65 && Math.abs(dy) > Math.abs(dx) * 1.25) { lastSwipeAt = Date.now(); navigate(dy < 0 ? 1 : -1); }
  }
  ui.stage.addEventListener('touchend', onTouchEnd, { passive: true });
  function onTouchCancel() { touchStart = null; }
  ui.stage.addEventListener('touchcancel', onTouchCancel, { passive: true });
  ui.gestureLayer.addEventListener('click', () => { if (Date.now() - lastSwipeAt > 450 && player) ui.play.click(); });

  window.addEventListener('popstate', event => {
    if (event.state?.rmEnhanced && event.state.view === 'viewer') {
      const index = queue.items.findIndex(item => item.url === event.state.slug);
      if (index >= 0) { queue.select(index); enterViewer(false); }
    } else if (mode === 'viewer') closeViewer(false);
  });

  async function initialize() {
    try {
      if (!tags.length) pageProps('/').then(props => populateTags(props.tags || props.allTags || [])).catch(() => {});
      const direct = /^\/video\/([^/]+)/.exec(initialPath);
      if (direct) {
        const slug = decodeURIComponent(direct[1]);
        const candidate = findDetail(initialProps || {}, slug) || normalizeVideo({ url: slug, title: document.title || 'Video' });
        queue.installDirect(candidate);
        enterViewer(false);
        return;
      }
      let source = { kind: 'home', sort: 'site', period: 'allTime' };
      const category = /^\/collections\/([^/]+)(?:\/\d+)?\/?$/.exec(initialPath);
      const popular = /^\/sortby\/(views|likes|comments)\/(today|week|month|allTime)(?:\/\d+)?\/?$/.exec(initialPath);
      if (category) {
        source = { kind: 'category', category: decodeURIComponent(category[1]), sort: 'site', period: 'allTime' };
        if (!Array.from(ui.category.options).some(option => option.value === source.category)) populateTags([{ tag: source.category, url: source.category }]);
        ui.category.value = source.category;
      }
      if (popular) { source = { kind: 'popular', sort: popular[1], period: popular[2] }; ui.sort.value = source.sort; ui.period.value = source.period; }
      const success = await queue.replace(source);
      if (success) resetGrid();
    } catch (error) {
      ui.notice.textContent = `Enhanced view could not start: ${error.message}. Use Original site below.`;
    }
  }
  initialize();
})();