Coomer Explore

Unified: Smart Search, Parallel CDN, Mixer, Trending, Curated, Background Preloading

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name Coomer Explore
// @namespace http://tampermonkey.net/
// @version 3.2.1
// @description Unified: Smart Search, Parallel CDN, Mixer, Trending, Curated, Background Preloading
// @author Hky
// @match https://coomer.st/*
// @grant GM_xmlhttpRequest
// @license MIT (Non-commercial use only PLS)
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @connect coomer.st
// @connect bin.lysator.liu.se
// @connect *
// @grant GM_getResourceText
// @run-at document-idle
// ==/UserScript==

// =========================================================================
// DISCOVERY CONFIG & DATA
// =========================================================================
// ATresource     creators_v2 creators_v2.json <-- add this at the top
const DISCO_CFG = {
  baseUrl: window.location.origin || 'https://coomer.st',
  ratio: GM_getValue('disco_ratio', 70),
};

const TAG_CATEGORIES = [
  { id: 'bodyparts', label: 'Body Parts', tags: ['ass', 'asshole', 'bigass', 'fatass', 'bigbutt', 'booty', 'butthole', 'butt', 'doggystyle', 'bigbooty', 'perfectass', 'sexyass', 'bigasss', 'assshaking', 'assbig', 'breasts', 'tits', 'boobs', 'bigtits', 'bigboobs', 'smalltits', 'smallboobs', 'hugetits', 'hugeboobs', 'naturaltits', 'titties', 'boobies', 'nipples', 'bignipples', 'nipple', 'legs', 'longlegs', 'thighs', 'thickthighs', 'feet', 'foot', 'toes', 'soles', 'footfetish', 'feetfetish', 'footfetishnation', 'pussy', 'wetpussy', 'pinkpussy', 'tightpussy', 'hairypussy', 'shavedpussy', 'perfectpussy', 'creamypussy', 'juicypussy', 'pussylips', 'closeuppussy', 'openpussy', 'bigpussy', 'pussywet', 'pussyrubbing', 'pussyplay', 'pussyeating', 'ebonypussy', 'vagina', 'bubblebutt', 'oil', 'milk'] },
  { id: 'bodytypes', label: 'Body Types', tags: ['petite', 'tiny', 'slim', 'skinny', 'thick', 'thicc', 'curvy', 'bbw', 'chubby', 'fit', 'model', 'babe', 'petitegirl', 'cutegirl', 'curvygirl', 'smallgirl', 'whitegirl', 'pawg', 'curvybigass', 'natural', 'naturalbody', 'perfectbody'] },
  { id: 'agerole', label: 'Age / Role', tags: ['teen', 'young', 'barelylegal', 'college', 'student', 'girlnextdoor', 'schoolgirl', 'schoolgirls', 'milf', 'milfs', 'mommy', 'stepmom', 'stepmommy', 'stepsister', 'stepdaughter', 'cougar', 'mature', 'hotwife', 'hotgirl', 'hotmom', 'sexymilf', 'hotmilf', 'hornymilf', 'collegegirl', 'naughtygirl', 'daddysgirl', 'mom', 'wife', 'girl', 'virgin', 'sister', 'sisters'] },
  { id: 'hairethnicity', label: 'Hair / Ethnicity', tags: ['blonde', 'blond', 'brunette', 'redhead', 'ginger', 'redhair', 'latina', 'latinas', 'asian', 'asiangirl', 'ebony', 'japanese', 'indian', 'arab', 'colombiana', 'colombian', 'colombianas', 'colombia', 'blondegirl', 'lesbianas', 'anime', 'hentai', 'vtuber', 'waifu', 'catgirl', 'ahegao'] },
  { id: 'sexualacts', label: 'Sexual Acts', tags: ['oral', 'blowjob', 'bj', 'deepthroat', 'handjob', 'sloppyblowjob', 'sucking', 'anal', 'analplay', 'analsex', 'analfuck', 'analplug', 'buttplug', 'insertions', 'doggystyle', 'doggy', 'solo', 'masturbation', 'masturbating', 'masturbate', 'fingering', 'soloplay', 'solomasturbation', 'cum', 'cumshot', 'cumming', 'cumslut', 'cumsluts', 'creampie', 'squirt', 'squirting', 'squirter', 'squirts', 'squirtingorgasm', 'multisquirt', 'orgasm', 'femaleorgasm', 'sex', 'fuck', 'fucking', 'fuckme', 'fuckpussy', 'cowgirl', 'riding', 'ride', 'threesome', 'doublepenetration', 'roughsex', 'hardsex', 'hardcore', 'striptease', 'dildofucking', 'ridedildo', 'sensual', 'dirtytalk', 'moaning', 'tease', 'exhibitionism', 'publicsex', 'publicmasturbation', 'publicflash', 'publicshow', 'voyeur', 'realsex'] },
  { id: 'sextoys', label: 'Sex Toys & Objects', tags: ['toys', 'dildo', 'toy', 'sextoy', 'vibrator', 'strapon', 'machine', 'fuckmachine', 'fuckingmachine', 'sexmachine', 'lovense'] },
  { id: 'fetish', label: 'Fetish & Roleplay', tags: ['bdsm', 'femdom', 'dominatrix', 'submissive', 'bondage', 'roleplay', 'roleplays', 'fetish', 'fetishes', 'footfetish', 'feetfetish', 'footfetishnation', 'public', 'ddlg', 'findom', 'sph', 'bbc', 'bigcock', 'bigdick', 'cuckold', 'slave', 'whore'] },
  { id: 'style', label: 'Style & Aesthetics', tags: ['alt', 'goth', 'altgirl', 'gothgirl', 'emo', 'tattoo', 'tattoos', 'tattooed', 'inked', 'tatted', 'tattoogirl', 'cosplay', 'cosplayer', 'cosplaygirl', 'lingerie', 'stockings', 'fishnets', 'latex', 'pantyhose', 'thong', 'panties', 'bikini', 'glasses', 'upskirt'] },
  { id: 'platform', label: 'Platform & Creator Type', tags: ['amateur', 'homemade', 'natural', 'allnatural', 'pro', 'model', 'onlyfans', 'fansly', 'fanslymodel', 'tiktok', 'tiktok18', 'tiktokporn', 'tiktokxxx', 'tiktoksexy', 'pornhub', 'xvideos', 'camsoda', 'camsodagirl', 'camsodalive', 'stripchat', 'chaturbate', 'webcam', 'camgirl', 'pornstar', 'fastfap', 'instagram', 'snapchat', 'online', 'homevideo', 'latinascamsoda'] },
  { id: 'general', label: 'General Descriptors', tags: ['sexy', 'hot', 'cute', 'beautiful', 'pretty', 'nude', 'naked', 'topless', 'nakedgirl', 'slut', 'slutty', 'kinky', 'kink', 'naughty', 'lewd', 'erotic', 'public', 'horny', 'hornygirl', 'wet', 'creamy', 'dirty', 'dance', 'twerk', 'twerking', 'oil', 'couple', 'outdoor', 'pov', 'lesbian', 'lesbians', 'trans', 'transgirl', 'porn', 'shower', 'gym'] }
];

const MATCHED_CREATORS_RAW = `
fansly:399542742671175680
fansly:492216456952426496
fansly:582967323984404480
fansly:704552506134372353
fansly:725100158009749504
onlyfans:addison.riley
onlyfans:adeenacherry
onlyfans:akemyni
onlyfans:alexis_de_vip
onlyfans:alexis_deep
onlyfans:alexisdemie
onlyfans:alexisdeveaux
onlyfans:alina_cherry1
onlyfans:alina_cherry7
onlyfans:amalilith
onlyfans:amy_quinn
onlyfans:anaischerryl
onlyfans:andreaocean
onlyfans:angy.cherry
onlyfans:anika_cherry
onlyfans:animebecca
onlyfans:anna_cherry77
onlyfans:annacherry1
onlyfans:annachherry
onlyfans:anny_bunnyy
onlyfans:aria_brooks
onlyfans:ariahix
onlyfans:ariana.gomez
onlyfans:ariasix
onlyfans:ariaxbrooks
onlyfans:aritabrooks
onlyfans:ashleyswfree
onlyfans:ashleyve_free
onlyfans:asia.bby
onlyfans:asialynnbby
onlyfans:asianbarby
onlyfans:asianbbyari
onlyfans:asianbbyboi
onlyfans:asiann.babyy
onlyfans:asiannbabyy
onlyfans:avacherrrry
onlyfans:bella-th
onlyfans:bella_hotx
onlyfans:bella_tx
onlyfans:bellahale
onlyfans:bellahotx
onlyfans:bellashyx
onlyfans:cardi-i
onlyfans:carili
onlyfans:carlamrtz
onlyfans:carmii
onlyfans:carolmarthy
onlyfans:carolts
onlyfans:cassie_babe
onlyfans:cassie_bale
onlyfans:cassiemaee
onlyfans:celinecinnamon
onlyfans:celinecinnamon2
onlyfans:chloe_skin
onlyfans:christalsskin
onlyfans:cristal-kinky
onlyfans:crystal_conners
onlyfans:danner1.0
onlyfans:dannye18
onlyfans:dari_ki
onlyfans:di.foxy
onlyfans:djarii
onlyfans:dunnybunny
onlyfans:dvalentinaxo
onlyfans:ella.cherry
onlyfans:elliehopps
onlyfans:emily.miller
onlyfans:emilyhill
onlyfans:emilywillis18
onlyfans:emilywills
onlyfans:emma_fairy
onlyfans:emmaacd
onlyfans:emmalayne
onlyfans:eva_charming
onlyfans:eva_fay
onlyfans:eva_fiery
onlyfans:eva_rouge
onlyfans:everougex
onlyfans:fitrebecca
onlyfans:funnyy_bunnyy
onlyfans:gunbunnyy
onlyfans:hellokinky2
onlyfans:hina_wayne
onlyfans:hollysecret
onlyfans:hunny_bunny_u
onlyfans:hunnybunny5
onlyfans:hunnybunnyb
onlyfans:i.am.ellie
onlyfans:iam.becca
onlyfans:iamiel
onlyfans:iamthylilith
onlyfans:illnanaa
onlyfans:irinnnaa
onlyfans:islandjade
onlyfans:itsscinnamon
onlyfans:jasi.bae
onlyfans:juicy_katie
onlyfans:karilix
onlyfans:karina_ocean
onlyfans:katescutiies
onlyfans:keittyfree
onlyfans:kitty_dream
onlyfans:kittybread
onlyfans:lanahcherry03
onlyfans:liamei
onlyfans:lianna_na_na
onlyfans:lil_amelia
onlyfans:lil_kittyy
onlyfans:lila.cherry
onlyfans:lilvkitty
onlyfans:lily_lovely_girl
onlyfans:linacherryy
onlyfans:lisedevine
onlyfans:littlekaitt
onlyfans:littlekitti
onlyfans:loivelygirl
onlyfans:lovelyegirl
onlyfans:lovelygirl17
onlyfans:luhasianbby
onlyfans:maddie-grey
onlyfans:maddie.grey
onlyfans:maddiegrey
onlyfans:maddisonspage
onlyfans:madisonginley
onlyfans:madisongray7
onlyfans:madisongrey69
onlyfans:madisongreyts
onlyfans:miachan
onlyfans:mialilith
onlyfans:milky.mei
onlyfans:milky_mei
onlyfans:mimifoxy
onlyfans:misaki_meii
onlyfans:mmarii
onlyfans:moanaocean
onlyfans:moni_kaa
onlyfans:monika_clay
onlyfans:monikaclay
onlyfans:monikastray
onlyfans:moondays
onlyfans:mystic_kitty2.0
onlyfans:mystic_kitty90
onlyfans:nattcherry
onlyfans:nickoletta.n
onlyfans:nicole_kitten
onlyfans:nicolettten
onlyfans:nikole_gy
onlyfans:nikoleg
onlyfans:ninabrooks
onlyfans:raaeluna
onlyfans:raarii
onlyfans:rebeccaa
onlyfans:rythecutiepie
onlyfans:sabrina_nellie
onlyfans:sabrinakellis
onlyfans:sabrinasplits
onlyfans:sallysecret
onlyfans:samyquinvip
onlyfans:sariixo
onlyfans:secretjuliet
onlyfans:shine_lia
onlyfans:shinegal
onlyfans:sofiblossomm
onlyfans:sofisofi
onlyfans:sophia.baily
onlyfans:sophia_rey
onlyfans:sophiebabyx
onlyfans:sophieblaze
onlyfans:sophieily
onlyfans:spunbunny
onlyfans:stassiebabee
onlyfans:stella_charming
onlyfans:sub_bunnyy
onlyfans:sunny.bunny.xx
onlyfans:sunny_anny
onlyfans:sunnybanny1
onlyfans:sunnyy.hunnyy
onlyfans:sweetiejuliet
onlyfans:sweetyjuliaa
onlyfans:synnbunny
onlyfans:takemy9
onlyfans:tianabrook
onlyfans:tiffanyrousso
onlyfans:tina_cherry
onlyfans:tiny_may
onlyfans:va_ness_a
onlyfans:valentiinaxo
onlyfans:valentina-arg
onlyfans:valentina.cho
onlyfans:valentina.jones
onlyfans:valentina.sm
onlyfans:valentina.ts
onlyfans:valentina.xo
onlyfans:valentinaofswe
onlyfans:valentinaomi
onlyfans:valentinaricco
onlyfans:valentinaros06
onlyfans:valentinarosa89
onlyfans:valentinarose18
onlyfans:valentinarose4
onlyfans:valentinarouge
onlyfans:valentinasz
onlyfans:valentinayours
onlyfans:valentinisss
onlyfans:valeria_kross
onlyfans:valfairyx
onlyfans:vanesaonly
onlyfans:vanessa.snow
onlyfans:vanessa_gold
onlyfans:vanessadvl
onlyfans:vanessamorel
onlyfans:vanessascorp
onlyfans:vanessaxo
onlyfans:vanessssas
onlyfans:vera_fairy
onlyfans:victoria_skinny
onlyfans:victoriaprice
onlyfans:victoriashy
onlyfans:victoriasinz
onlyfans:victoriasvip
onlyfans:vnikolee
onlyfans:yourcutekami`;

const MATCHED_CREATORS = MATCHED_CREATORS_RAW.trim().split('\n').map(line => {
  const i = line.indexOf(':');
  if (i < 0) return null;
  return { service: line.slice(0, i).trim(), id: line.slice(i + 1).trim() };
}).filter(Boolean);

// =========================================================================
// RESILIENT API FETCH
// =========================================================================
async function apiFetch(path, opts = {}) {
  const origin = window.location.origin || 'https://coomer.st';
  if (origin && DISCO_CFG.baseUrl !== origin) DISCO_CFG.baseUrl = origin;
  let url = path.startsWith('http') ? path : DISCO_CFG.baseUrl + '/api/v1' + path;
  let retries = 0;
  const maxRetries = 3;
  while (retries < maxRetries) {
    try {
      return await new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: opts.method || 'GET',
          url,
          headers: Object.assign({ 'Accept': 'text/css' }, opts.headers),
          timeout: 10000,
          onload: (res) => {
            if (res.status >= 200 && res.status < 300) {
              try { resolve(JSON.parse(res.responseText)); } catch (e) { reject(e); }
            } else { reject(new Error(`HTTP ${res.status}`)); }
          },
          onerror: (e) => reject(new Error('Network error')),
          ontimeout: () => reject(new Error('Timeout'))
        });
      });
    } catch (e) {
      retries++;
      if (retries >= maxRetries) throw e;
      await new Promise(r => setTimeout(r, 1000 * retries));
    }
  }
}

(function () {
  'use strict';

  const COOMER_ORIGIN = window.location.origin || 'https://coomer.st';
  const EXPLORE_HASH = '#explore';

  const SITE_DOMAIN = window.location.hostname.replace(/^www\./, '') || 'coomer.st';
  const CDN_DOMAIN = SITE_DOMAIN.includes('kemono') ? 'kemono.su' : SITE_DOMAIN;
  const CDN_NODES = [`n1.${CDN_DOMAIN}`, `n2.${CDN_DOMAIN}`, `n3.${CDN_DOMAIN}`, `n4.${CDN_DOMAIN}`];
  let preferredNode = `n2.${CDN_DOMAIN}`;

  function cdnUrl(path, node, bustCache = false) {
    const base = `https://${node || preferredNode}/data${path}`;
    return bustCache ? `${base}?_=${Date.now()}` : base;
  }

  const CREATORS_URL = 'https://bin.lysator.liu.se/raw/YxScT91MSbm';
  const TAGS_URL = 'https://bin.lysator.liu.se/raw/I0WV5pil7ne';
  let cachedCreatorsList = null;
  let cachedTagsList = null;

  const FALLBACK_TAGS = [
    { tag: 'public', count: 8177, category: 'Location' },
    { tag: 'x', count: 27904, category: 'Content' },
  ];

  const MAX_WATCHED_HISTORY = 5000;
  let watchedSet = new Set(GM_getValue('watchedVideos', []));
  function markWatched(postId) {
    if (!postId) return;
    watchedSet.add(String(postId));
    if (watchedSet.size > MAX_WATCHED_HISTORY) {
      const arr = [...watchedSet];
      watchedSet = new Set(arr.slice(arr.length - MAX_WATCHED_HISTORY));
    }
    GM_setValue('watchedVideos', [...watchedSet]);
  }
  function isWatched(postId) { return watchedSet.has(String(postId)); }
  function getPostId(post) { const p = post.post || post; return p.id || p.hash || null; }

  let currentSlideIdx = 0;
  let currentPlaylistPosts = [];
  let userManuallyPlaying = false;
  let activeWatchTimers = new Map();

  function escapeHtml(str) {
    if (!str) return '';
    const p = document.createElement('p');
    p.textContent = str;
    return p.innerHTML;
  }
  function escapeAttr(str) {
    if (!str) return '';
    return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
  }
  function avatarUrl(s, id) { return `https://img.${CDN_DOMAIN}/icons/${s}/${encodeURIComponent(id)}`; }
  function bannerUrl(s, id) { return `https://img.${CDN_DOMAIN}/banners/${s}/${encodeURIComponent(id)}`; }

  const CFG = {
    nodes: ['n1', 'n2', 'n3', 'n4'],
    chunkSize: 1024 * 1024 * 5,
    maxConcurrency: 8,
    chunkTimeout: 30000,
    headTimeout: 10000,
    maxChunkRetries: 3,
    debug: false,
    ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
    mseBufferAhead: 30,
    mseBufferBehind: 15,
    mseAppendRetryDelay: 100,
    mseMaxQueuedAppends: 50,
    autoPlayOnData: true,
    minBufferToPlay: 0.5,
    showOverlay: true,
    useParallelCDN: true,
    parallelCDNNodes: 3,
    nodePerformanceTracking: true,
  };

  const log = (...a) => CFG.debug && console.log('%c[CDN-Stream]', 'color:#4ade80;font-weight:bold', ...a);
  const warn = (...a) => console.warn('%c[CDN-Stream]', 'color:#f59e0b;font-weight:bold', ...a);
  const err = (...a) => console.error('%c[CDN-Stream]', 'color:#ef4444;font-weight:bold', ...a);

  class FileMetadataCache {
    constructor() { this.cache = new Map(); this.maxAge = 5 * 60 * 1000; this.pendingRequests = new Map(); }
    extractHash(url) { const match = url.match(/\/([a-f0-9]{64})\.(mp4|webm|mov|m4v)/i); return match ? match[1] : null; }
    async get(hash) {
      const cached = this.cache.get(hash);
      if (cached && Date.now() - cached.timestamp < this.maxAge) return cached.data;
      if (this.pendingRequests.has(hash)) return await this.pendingRequests.get(hash);
      const fetchPromise = this._fetchFromApi(hash);
      this.pendingRequests.set(hash, fetchPromise);
      try { const data = await fetchPromise; this.cache.set(hash, { data, timestamp: Date.now() }); return data; }
      finally { this.pendingRequests.delete(hash); }
    }
    async _fetchFromApi(hash) {
      const response = await cdnFetch(`${COOMER_ORIGIN}/api/v2/file/${hash}`, { timeout: 5000 });
      if (response.status !== 200) throw new Error(`API returned ${response.status}`);
      const json = JSON.parse(response.responseText);
      if (!json.data || !json.data.file) throw new Error('Invalid API response structure');
      return json.data;
    }
    cleanup() { const now = Date.now(); for (const [hash, entry] of this.cache.entries()) { if (now - entry.timestamp > this.maxAge) this.cache.delete(hash); } }
  }

  class SmartNodeSelector {
    constructor(nodes = CFG.nodes) {
      this.nodes = nodes; this.latencyMap = new Map(); this.failureMap = new Map();
      this.sessionBestNode = GM_getValue('sessionBestNode', null);
      this.lastMeasurementTime = 0; this.measurementCooldown = 60000;
    }
    async measureLatency(node, testUrl) {
      const t0 = performance.now();
      try { await cdnFetch(testUrl, { method: 'HEAD', timeout: 3000 }); return performance.now() - t0; }
      catch (e) { return Infinity; }
    }
    async getBestNode(candidateUrls, fileHash = null, fileCache = null) {
      if (this.sessionBestNode) {
        const failures = this.failureMap.get(this.sessionBestNode) || 0;
        if (failures < 3) return this.sessionBestNode;
      }
      const now = Date.now();
      if (now - this.lastMeasurementTime < this.measurementCooldown && this.latencyMap.size > 0) {
        const sorted = [...this.latencyMap.entries()].filter(([n]) => (this.failureMap.get(n) || 0) < 3).sort((a, b) => a[1] - b[1]);
        if (sorted.length > 0) return sorted[0][0];
      }
      let availableNodes = this.nodes;
      if (fileHash && fileCache) {
        try { const metadata = await fileCache.get(fileHash); if (metadata.file_list?.length > 0) availableNodes = metadata.file_list; } catch (e) {}
      }
      const filteredCandidates = []; const nodeIndices = [];
      for (let i = 0; i < candidateUrls.length; i++) {
        const node = this.nodes[i];
        if (availableNodes.includes(node)) { filteredCandidates.push(candidateUrls[i]); nodeIndices.push(i); }
      }
      if (filteredCandidates.length === 0) { filteredCandidates.push(...candidateUrls); nodeIndices.push(...Array.from({ length: candidateUrls.length }, (_, i) => i)); }
      this.lastMeasurementTime = now;
      const measurements = await Promise.all(filteredCandidates.map(async (url, idx) => {
        const node = this.nodes[nodeIndices[idx]];
        const latency = await this.measureLatency(node, url);
        this.latencyMap.set(node, latency);
        return { node, latency };
      }));
      const healthy = measurements.filter(m => m.latency < Infinity && (this.failureMap.get(m.node) || 0) < 3).sort((a, b) => a.latency - b.latency);
      if (healthy.length === 0) return this.nodes[0];
      const bestNode = healthy[0].node;
      this.sessionBestNode = bestNode;
      GM_setValue('sessionBestNode', bestNode);
      return bestNode;
    }
    recordFailure(node) { const count = (this.failureMap.get(node) || 0) + 1; this.failureMap.set(node, count); if (count >= 3 && this.sessionBestNode === node) { this.sessionBestNode = null; GM_setValue('sessionBestNode', null); } }
    recordSuccess(node) { this.failureMap.set(node, 0); }
  }

  class URLResolver {
    constructor(selector, fileCache = null) { this.selector = selector; this.fileCache = fileCache; }
    extractHash(url) {
      const match = url.match(/\/([a-f0-9]{2})\/([a-f0-9]{2})\/([a-f0-9]{64})\.(mp4|webm|mov|m4v)/i);
      if (match) return { p1: match[1], p2: match[2], hash: match[3], ext: '.' + match[4].toLowerCase() };
      return null;
    }
    buildCandidateUrls(p1, p2, hash, ext) { return this.selector.nodes.map(node => `https://${node}.${CDN_DOMAIN}/data/${p1}/${p2}/${hash}${ext}`); }
    async resolve(originalUrl) {
      const info = this.extractHash(originalUrl);
      if (!info) return originalUrl;
      const candidates = this.buildCandidateUrls(info.p1, info.p2, info.hash, info.ext);
      const bestNode = await this.selector.getBestNode(candidates, info.hash, this.fileCache);
      return `https://${bestNode}.${CDN_DOMAIN}/data/${info.p1}/${info.p2}/${info.hash}${info.ext}`;
    }
  }

  const PAGE_ORIGIN = typeof window !== 'undefined' && window.location ? window.location.origin : 'https://coomer.st';
  let fetchQueue = Promise.resolve();
  const THROTTLE_MS = 250;
  let lastRequestTime = 0;

  function gmFetch(url, opts = {}) {
    const isApiCall = url.includes('/api/');
    const defaultHeaders = {
      'Referer': PAGE_ORIGIN + '/',
      'Origin': PAGE_ORIGIN,
      'User-Agent': CFG.ua,
      'Accept': opts.method === 'HEAD' ? '*/*' : (opts.headers?.['Accept']) || (isApiCall ? 'text/css' : '*/*'),
    };
    const headers = { ...defaultHeaders, ...(opts.headers || {}) };
    const requestPromise = fetchQueue.then(async () => {
      const now = Date.now();
      const timeSinceLast = now - lastRequestTime;
      if (timeSinceLast < THROTTLE_MS) await new Promise(r => setTimeout(r, THROTTLE_MS - timeSinceLast));
      lastRequestTime = Date.now();
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: opts.method || 'GET', url,
          responseType: opts.responseType || 'text',
          headers, timeout: opts.timeout || 30000, anonymous: false,
          onload(r) {
            if (r.status >= 200 && r.status < 400) {
              if (!opts.responseType || opts.responseType === 'text') {
                try { const text = r.responseText; if (text && (text.trim().startsWith('{') || text.trim().startsWith('['))) r.json = JSON.parse(text); } catch (_) {}
              }
              resolve(r);
            } else reject(new Error(`HTTP ${r.status}: ${url}`));
          },
          onerror() { reject(new Error(`Network error: ${url}`)); },
          ontimeout() { reject(new Error(`Timeout: ${url}`)); },
        });
      });
    });
    fetchQueue = requestPromise.catch(() => {});
    return requestPromise.then(r => {
      if (opts.responseType === 'arraybuffer') return r;
      return r.json || r.responseText;
    });
  }

  function cdnFetch(url, opts = {}) {
    const isApiCall = url.includes('/api/');
    const defaultHeaders = {
      'Referer': PAGE_ORIGIN + '/',
      'Origin': PAGE_ORIGIN,
      'User-Agent': CFG.ua,
      'Accept': opts.method === 'HEAD' ? '*/*' : (opts.headers?.['Accept']) || (isApiCall ? 'text/css' : '*/*'),
    };
    const headers = { ...defaultHeaders, ...(opts.headers || {}) };
    const requestPromise = fetchQueue.then(async () => {
      const now = Date.now();
      const timeSinceLast = now - lastRequestTime;
      if (timeSinceLast < THROTTLE_MS) await new Promise(r => setTimeout(r, THROTTLE_MS - timeSinceLast));
      lastRequestTime = Date.now();
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: opts.method || 'GET', url,
          responseType: opts.responseType || 'text',
          headers, timeout: opts.timeout || 30000, anonymous: false,
          onload(r) { if (r.status >= 200 && r.status < 400) resolve(r); else reject(new Error(`HTTP ${r.status}: ${url}`)); },
          onerror() { reject(new Error(`Network error: ${url}`)); },
          ontimeout() { reject(new Error(`Timeout: ${url}`)); },
        });
      });
    });
    fetchQueue = requestPromise.catch(() => {});
    return requestPromise;
  }

  class StreamOrchestrator {
    constructor(urlResolver) { this.resolver = urlResolver; this.videoMap = new Map(); this.accessOrder = []; this.maxPoolSize = /iPhone|iPad/i.test(navigator.userAgent) ? 2 : 3; }
    createVideo() {
      const v = document.createElement('video');
      v.preload = 'auto'; v.muted = true; v.playsInline = true;
      v.style.cssText = 'position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;';
      document.body.appendChild(v);
      return v;
    }
    async getStream(url, mode = 'playback') {
      const resolvedUrl = await this.resolver.resolve(url);
      if (this.videoMap.has(resolvedUrl)) { this._updateAccessOrder(resolvedUrl); return this.videoMap.get(resolvedUrl); }
      const video = this.createVideo();
      video.src = resolvedUrl;
      if (mode === 'metadata' && video.readyState < 1) await new Promise(r => video.addEventListener('loadedmetadata', r, { once: true }));
      this.videoMap.set(resolvedUrl, video);
      this.accessOrder.push(resolvedUrl);
      this._evictIfNeeded();
      return video;
    }
    async prepareNext(url) { try { const video = await this.getStream(url); const p = video.play(); if (p) await p.then(() => video.pause()).catch(() => {}); } catch (e) {} }
    _updateAccessOrder(url) { const idx = this.accessOrder.indexOf(url); if (idx !== -1) this.accessOrder.splice(idx, 1); this.accessOrder.push(url); }
    _evictIfNeeded() {
      while (this.videoMap.size > this.maxPoolSize) {
        const url = this.accessOrder.shift();
        const v = this.videoMap.get(url);
        if (v) { v.pause(); v.removeAttribute('src'); while (v.firstChild) v.removeChild(v.firstChild); v.load(); v.remove(); _activeConnections = Math.max(0, _activeConnections - 1); }
        this.videoMap.delete(url);
      }
    }
  }

  let urlResolver = null;
  let nodeSelector = null;
  let fileCache = null;
  let streamOrch = null;

  async function streamVideo(videoEl) {
    if (videoEl.dataset.cdnStreaming) return;
    if (!urlResolver) return;
    videoEl.dataset.cdnStreaming = 'true';
    let src = videoEl.src;
    if (!src) { const source = videoEl.querySelector('source'); if (source) src = source.src; }
    if (!src) return;
    const resolved = await urlResolver.resolve(src);
    if (resolved !== src) {
      const ct = videoEl.currentTime; const paused = videoEl.paused;
      videoEl.src = resolved;
      if (!paused) videoEl.play().catch(() => {});
      if (ct > 0) videoEl.currentTime = ct;
    }
  }

  function initCDN() {
    fileCache = new FileMetadataCache();
    nodeSelector = new SmartNodeSelector(CFG.nodes);
    urlResolver = new URLResolver(nodeSelector, fileCache);
    streamOrch = new StreamOrchestrator(urlResolver);
    window.cdnStream = streamVideo;
    window.coomerApi = window.coomerApi || {};
    window.coomerApi.urlResolver = urlResolver;
    log('CDN Logic Initialized');
    const s = document.createElement('style');
    s.textContent = '.gallery-page .gallery-creator-banner { display: none !important; }';
    document.head.appendChild(s);
  }
  initCDN();

  async function loadExternalCreatorsList() {
    if (cachedCreatorsList) return cachedCreatorsList;
    try {
      const text = await new Promise((resolve, reject) => {
        GM_xmlhttpRequest({ method: 'GET', url: CREATORS_URL, headers: { 'User-Agent': navigator.userAgent }, onload: (r) => { if (r.status >= 200 && r.status < 400) resolve(r.responseText); else reject(new Error(`HTTP ${r.status}`)); }, onerror: reject, ontimeout: () => reject(new Error('Timeout')) });
      });
      const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
      const creators = []; const seen = new Set();
      for (const line of lines) {
        const [service, id] = line.split(':');
        if (service && id) { const key = `${service}:${id}`; if (!seen.has(key)) { seen.add(key); creators.push({ service, id, name: id }); } }
      }
      cachedCreatorsList = creators;
      return creators;
    } catch (e) { return []; }
  }

  async function loadExternalTags() {
    if (cachedTagsList) return cachedTagsList;
    try {
      const text = await new Promise((resolve, reject) => {
        GM_xmlhttpRequest({ method: 'GET', url: TAGS_URL, headers: { 'User-Agent': navigator.userAgent }, onload: (r) => { if (r.status >= 200 && r.status < 400) resolve(r.responseText); else reject(new Error(`HTTP ${r.status}`)); }, onerror: reject, ontimeout: () => reject(new Error('Timeout')) });
      });
      const tags = [];
      const arrayMatches = text.matchAll(/\[\s*\{[^\]]+\}\s*\]/g);
      for (const match of arrayMatches) { try { const arr = JSON.parse(match[0]); if (Array.isArray(arr)) arr.forEach(item => { if (item.tag && item.post_count) tags.push({ tag: item.tag, count: parseInt(item.post_count, 10), category: item.category || 'General' }); }); } catch (e) {} }
      const jsonMatches = text.matchAll(/\{\s*"tag"\s*:\s*"([^"]+)"\s*,\s*"post_count"\s*:\s*(\d+)\s*(?:,\s*"category"\s*:\s*"([^"]*)")?\s*\}/g);
      for (const match of jsonMatches) { const tag = match[1]; const count = parseInt(match[2], 10); const category = match[3] || 'General'; if (!tags.find(t => t.tag === tag)) tags.push({ tag, count, category }); }
      tags.sort((a, b) => b.count - a.count);
      cachedTagsList = tags;
      return tags;
    } catch (e) { return FALLBACK_TAGS; }
  }

  async function getCreators() {
    const externalCreators = await loadExternalCreatorsList();
    if (externalCreators.length > 0) return externalCreators;
    return new Promise((resolve) => {
      const timer = setTimeout(() => { if (window.coomerApi?.getCreators) window.coomerApi.getCreators().then(resolve).catch(() => resolve([])); else resolve([]); }, 2000);
      document.addEventListener('CoomerApiCreatorsResponse', (e) => { clearTimeout(timer); resolve(e.detail.creators || []); }, { once: true });
      document.dispatchEvent(new CustomEvent('CoomerApiGetCreators'));
    });
  }

  async function getCreatorPosts(service, id, offset = 0) {
    const url = `/api/v1/${service}/user/${id}/posts?o=${offset}`;
    const data = await gmFetch(url);
    if (data.results && Array.isArray(data.results)) {
      const props = data.props || {};
      const limit = props.limit || 50;
      const posts = data.results;
      return { posts, count: props.count || 0, hasMore: posts.length >= limit, artist: props.artist };
    }
    const posts = Array.isArray(data) ? data : [];
    return { posts, count: posts.length, hasMore: posts.length >= 50, artist: null };
  }

  async function getPostsByTag(tag, offset = 0) {
    const data = await gmFetch(`/api/v1/posts?tag=${encodeURIComponent(tag)}&o=${offset}`);
    const posts = Array.isArray(data) ? data : (data.results || data.posts || []);
    return { posts, count: posts.length, hasMore: posts.length >= 50 };
  }

  async function getRecommendedCreators(service, id) {
    try {
      const data = await gmFetch(`/api/v1/${service}/user/${id}/recommended`);
      if (Array.isArray(data)) return data.map(c => ({ service: c.service || service, id: c.id || c.user || c.name, name: c.name || c.id || c.user, score: c.score })).filter(c => c.id);
      return [];
    } catch (e) { return []; }
  }

  function hasVideoAttachments(post) {
    const p = post.post || post;
    const file = p.file || {};
    const attachments = p.attachments || [];
    if (file.path && /\.(mp4|webm|mov|avi|mkv|m4v)$/i.test(file.path)) return true;
    return attachments.some(a => a.path && /\.(mp4|webm|mov|avi|mkv|m4v)$/i.test(a.path));
  }
  function isVideoPost(post) { return hasVideoAttachments(post); }
  function getFirstVideoPath(post) {
    const p = post.post || post;
    const file = p.file || {};
    const attachments = p.attachments || [];
    if (file.path && /\.(mp4|webm|mov|avi|mkv|m4v)$/i.test(file.path)) return file.path;
    for (const att of attachments) { if (att.path && /\.(mp4|webm|mov|avi|mkv|m4v)$/i.test(att.path)) return att.path; }
    return null;
  }
  function getFirstVideoUrl(post) { const path = getFirstVideoPath(post); return path ? cdnUrl(path) : null; }

  let currentPlaybackContext = null;
  let isLoadingMore = false;
  let lastActiveTab = 'creators';
  const visitedCreators = new Set();

  // =========================================================================
  // STYLES
  // =========================================================================
  GM_addStyle(`
    .hidden { display: none !important; }
    #explore-root { position: fixed; top: 0; left: 0; width: 100%; height: 100vh; z-index: 9999; background: #0a0a0a; overflow: hidden; }
    .video-player { width: 100%; height: calc(100vh - 60px); overflow-y: scroll; scroll-snap-type: y mandatory; scroll-behavior: smooth; scrollbar-width: none; }
    .video-player::-webkit-scrollbar { display: none; }
    .video-slide { width: 100%; height: 100%; scroll-snap-align: start; scroll-snap-stop: always; display: flex; align-items: center; justify-content: center; background: #000; position: relative; }
    .explore-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; background: #1a1a1a; border-bottom: 2px solid #4ade80; }
    .explore-tabs { display: flex; gap: 12px; }
    .explore-tab { padding: 8px 16px; background: #2a2a2a; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; transition: all 0.2s; }
    .explore-tab:hover { background: #3a3a3a; }
    .explore-tab.active { background: #4ade80; color: #000; }
    .explore-close { padding: 8px 16px; background: #f87171; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; }
    .explore-content { position: relative; height: calc(100vh - 60px); overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; }
    .explore-content::-webkit-scrollbar { display: none; }
    .explore-page { display: none; padding: 20px; }
    .explore-page.active { display: block; }
    .creator-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px; padding: 20px; }
    .creator-card { background: #1a1a1a; border-radius: 12px; overflow: hidden; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; }
    .creator-card:hover { transform: translateY(-4px); box-shadow: 0 8px 16px rgba(74, 222, 128, 0.3); }
    .creator-banner { width: 100%; height: 140px; object-fit: cover; background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%); }
    .creator-info { padding: 12px; }
    .creator-name { font-weight: 600; color: #fff; margin-bottom: 4px; }
    .creator-service { font-size: 14px; color: #4ade80; }
    .tag-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; padding: 20px; }
    .tag-card { background: #1a1a1a; padding: 16px; border-radius: 8px; cursor: pointer; transition: all 0.2s; border: 2px solid transparent; }
    .tag-card:hover { background: #2a2a2a; border-color: #4ade80; }
    .tag-name { font-weight: 600; color: #4ade80; font-size: 18px; margin-bottom: 4px; }
    .tag-count { color: #9ca3af; font-size: 14px; }
    .tag-category { color: #6b7280; font-size: 12px; margin-top: 4px; }
    .mixer-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; padding: 20px; }
    .mixer-card { background: #262626; border-radius: 8px; padding: 10px; display: flex; align-items: center; gap: 10px; cursor: pointer; border: 2px solid transparent; transition: all 0.2s; position: relative; }
    .mixer-card:hover { background: #333; transform: translateY(-2px); }
    .mixer-card.selected { border-color: #4ade80; background: #1a2e1a; }
    .mixer-avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; background: #333; }
    .mixer-info { flex: 1; min-width: 0; }
    .mixer-name { color: #e5e7eb; font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .mixer-stats { color: #9ca3af; font-size: 11px; }
    .mixer-checkbox { width: 16px; height: 16px; border: 2px solid #4b5563; border-radius: 4px; margin-left: auto; }
    .mixer-card.selected .mixer-checkbox { background: #4ade80; border-color: #4ade80; }
    .mixer-controls { padding: 15px 20px; display: flex; align-items: center; gap: 15px; border-bottom: 1px solid #333; background: #1a1a1a; position: sticky; top: 0; z-index: 10; }
    .mixer-play-btn { background: #4ade80; color: #000; padding: 8px 16px; border-radius: 6px; font-weight: 600; cursor: pointer; border: none; transition: transform 0.2s; }
    .mixer-play-btn:hover { transform: scale(1.05); }
    .fav-btn { position: absolute; top: 20px; right: 20px; background: rgba(0,0,0,0.6); color: #fff; border: 2px solid #fff; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: bold; cursor: pointer; z-index: 100; transition: all 0.2s; }
    .fav-btn:hover { background: rgba(255,255,255,0.2); transform: scale(1.1); }
    .fav-btn.active { background: #ef4444; border-color: #ef4444; }
    .trending-controls { display: flex; align-items: center; gap: 10px; padding: 16px 20px; flex-wrap: wrap; }
    .trending-controls label { color: #9ca3af; font-size: 14px; font-weight: 600; }
    .trending-period-btn { padding: 6px 16px; background: #2a2a2a; color: #fff; border: 2px solid transparent; border-radius: 20px; cursor: pointer; font-weight: 600; font-size: 13px; transition: all 0.2s; }
    .trending-period-btn:hover { background: #3a3a3a; }
    .trending-period-btn.active { background: #4ade80; color: #000; border-color: #4ade80; }
    .trending-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; padding: 0 20px 20px; }
    .trending-card { background: #1a1a1a; border-radius: 12px; overflow: hidden; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; }
    .trending-card:hover { transform: translateY(-4px); box-shadow: 0 8px 16px rgba(74, 222, 128, 0.3); }
    .trending-thumb { position: relative; width: 100%; height: 160px; background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%); overflow: hidden; }
    .trending-thumb img { width: 100%; height: 100%; object-fit: cover; }
    .trending-thumb .trending-play-icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 36px; opacity: 0.7; pointer-events: none; text-shadow: 0 2px 8px rgba(0,0,0,0.6); }
    .trending-card-info { padding: 12px; }
    .trending-card-title { font-weight: 600; color: #fff; font-size: 14px; margin-bottom: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .trending-card-meta { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #9ca3af; }
    .trending-card-creator { color: #4ade80; font-weight: 600; }
    .trending-card-service { color: #6b7280; text-transform: capitalize; }
    .trending-load-more { display: block; margin: 0 auto 24px; padding: 10px 32px; background: #2a2a2a; color: #4ade80; border: 2px solid #4ade80; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 14px; transition: all 0.2s; }
    .trending-load-more:hover { background: #4ade80; color: #000; }
    .trending-load-more:disabled { opacity: 0.5; cursor: not-allowed; }
    .video-player video { max-width: 100%; max-height: 100%; object-fit: contain; }
    .video-slide video { width: 100%; height: 100%; object-fit: contain; }
    .video-slide.watched-marker::after { content: '✓ Watched'; position: absolute; top: 40px; right: 15px; background: rgba(74, 222, 128, 0.8); color: #000; font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: 4px; z-index: 20; pointer-events: none; }
    .video-info { position: absolute; bottom: 40px; left: 20px; right: 80px; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.9); z-index: 20; pointer-events: none; }
    .video-creator { font-weight: 600; font-size: 18px; }
    .video-service { font-size: 14px; opacity: 0.9; }
    .video-title { font-size: 13px; opacity: 0.85; margin-top: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
    .video-meta { font-size: 11px; opacity: 0.6; margin-top: 3px; }
    .back-btn { position: fixed; top: 75px; left: 15px; z-index: 10000; background: rgba(0, 0, 0, 0.7); color: #fff; border: 1px solid rgba(255, 255, 255, 0.2); padding: 8px 16px; border-radius: 20px; cursor: pointer; backdrop-filter: blur(4px); font-size: 13px; transition: all 0.2s; }
    .back-btn:hover { background: rgba(0, 0, 0, 0.8); transform: translateY(1px); }
    .explore-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #4ade80; font-size: 18px; gap: 16px; }
    .explore-loading-spinner { width: 48px; height: 48px; border: 4px solid rgba(74, 222, 128, 0.2); border-top-color: #4ade80; border-radius: 50%; animation: spin 1s linear infinite; }
    @keyframes spin { to { transform: rotate(360deg); } }
    .seek-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.75); color: #4ade80; font-size: 28px; font-weight: 700; padding: 16px 28px; border-radius: 12px; pointer-events: none; z-index: 200; animation: seekFade 0.6s ease-out forwards; }
    @keyframes seekFade { 0% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.2); } }
    .slide-counter { position: fixed; top: 175px; right: 15px; background: rgba(0, 0, 0, 0.7); color: #fff; font-size: 13px; padding: 6px 12px; border-radius: 20px; z-index: 10000; backdrop-filter: blur(4px); border: 1px solid rgba(255, 255, 255, 0.2); }
    .play-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.4); z-index: 150; cursor: pointer; }
    .play-overlay-btn { width: 80px; height: 80px; background: rgba(74, 222, 128, 0.85); border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: transform 0.2s; }
    .play-overlay-btn:hover { transform: scale(1.1); }
    .play-overlay-btn::after { content: ''; display: block; width: 0; height: 0; border-style: solid; border-width: 18px 0 18px 30px; border-color: transparent transparent transparent #000; margin-left: 6px; }
    .video-error-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.7); color: #f87171; z-index: 150; cursor: pointer; gap: 12px; }
    .video-error-overlay span { font-size: 14px; color: #9ca3af; }
    .video-progress-container { position: absolute; top: 0; left: 0; width: 100%; height: 4px; background: rgba(255, 255, 255, 0.1); z-index: 10; pointer-events: none; }
    .video-progress-bar { height: 100%; background: #4ade80; width: 0%; transition: width 0.1s linear; }
    /* ── Smart Search styles ── */
    .smart-search-panel { background: #111; border-radius: 16px; padding: 28px; max-width: 720px; margin: 24px auto; border: 1px solid #2a2a2a; }
    .smart-search-title { font-size: 22px; font-weight: 700; color: #4ade80; margin-bottom: 24px; display: flex; align-items: center; gap: 10px; }
    .smart-search-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }
    .smart-search-field { display: flex; flex-direction: column; gap: 6px; }
    .smart-search-field label { font-size: 12px; font-weight: 600; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.05em; }
    .smart-search-field select, .smart-search-field input { background: #1e1e1e; border: 1px solid #333; color: #fff; border-radius: 8px; padding: 10px 12px; font-size: 14px; outline: none; transition: border-color 0.2s; width: 100%; box-sizing: border-box; }
    .smart-search-field select:focus, .smart-search-field input:focus { border-color: #4ade80; }
    .smart-search-keyword { margin-bottom: 20px; }
    .smart-search-keyword input { width: 100%; }
    .smart-search-btn { width: 100%; padding: 14px; background: #4ade80; color: #000; border: none; border-radius: 10px; font-size: 16px; font-weight: 700; cursor: pointer; transition: all 0.2s; letter-spacing: 0.03em; }
    .smart-search-btn:hover { background: #22c55e; transform: translateY(-1px); box-shadow: 0 4px 16px rgba(74,222,128,0.3); }
    .smart-search-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
    .smart-search-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px; gap: 16px; color: #4ade80; }
    .smart-search-loading-text { font-size: 15px; color: #9ca3af; }
    /* Search overlay that slides in front of the rest of the UI */
    #smart-search-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #0a0a0a; z-index: 10001; overflow-y: auto; display: flex; flex-direction: column; }
    #smart-search-overlay.hidden { display: none !important; }
    .sso-header { display: flex; align-items: center; gap: 12px; padding: 16px 20px; background: #111; border-bottom: 2px solid #4ade80; position: sticky; top: 0; z-index: 10; }
    .sso-back { background: #2a2a2a; color: #fff; border: none; border-radius: 8px; padding: 8px 16px; cursor: pointer; font-weight: 600; font-size: 14px; transition: background 0.2s; }
    .sso-back:hover { background: #3a3a3a; }
    .sso-title { font-size: 18px; font-weight: 700; color: #4ade80; }
    .sso-count { font-size: 13px; color: #9ca3af; margin-left: auto; }
    .sso-content { flex: 1; padding: 20px; }
    .sso-mode-tabs { display: flex; gap: 10px; margin-bottom: 20px; }
    .sso-mode-btn { padding: 7px 18px; background: #1e1e1e; color: #9ca3af; border: 1px solid #333; border-radius: 20px; cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.2s; }
    .sso-mode-btn.active { background: #4ade80; color: #000; border-color: #4ade80; }
    .sso-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
    .sso-video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
    .sso-play-all { display: inline-flex; align-items: center; gap: 8px; background: #4ade80; color: #000; border: none; border-radius: 8px; padding: 10px 22px; cursor: pointer; font-weight: 700; font-size: 15px; margin-bottom: 18px; transition: all 0.2s; }
    .sso-play-all:hover { background: #22c55e; transform: translateY(-1px); }
    .sso-empty { text-align: center; padding: 60px 20px; color: #6b7280; font-size: 16px; }
  `);

  // =========================================================================
  // SEEK OVERLAY
  // =========================================================================
  function showSeekOverlay(slide, text) {
    const existing = slide.querySelector('.seek-overlay');
    if (existing) existing.remove();
    const el = document.createElement('div');
    el.className = 'seek-overlay';
    el.textContent = text;
    slide.appendChild(el);
    setTimeout(() => el.remove(), 600);
  }

  // =========================================================================
  // VIDEO PLAYBACK HELPERS
  // =========================================================================
  function tryPlayVideo(video) {
    if (!video) return;
    if (video._playPending) return;
    if (!video.src || video.src === '' || video.readyState === 0) { video.addEventListener('loadedmetadata', () => tryPlayVideo(video), { once: true }); return; }
    if (video._disposed || !video.isConnected) return;
    video.muted = true;
    video._playPending = true;
    const p = video.play();
    if (p !== undefined) {
      p.then(() => { video._playPending = false; video.muted = false; video.volume = 1.0; removePlayOverlay(video); })
       .catch(e => {
         video._playPending = false;
         if (e.name === 'AbortError') return;
         if (e.name === 'NotSupportedError') { video.addEventListener('loadedmetadata', () => tryPlayVideo(video), { once: true }); return; }
         if (e.name === 'NotAllowedError') showPlayOverlay(video);
       });
    } else { video._playPending = false; }
  }

  function showPlayOverlay(video) {
    const slide = video?.closest('.video-slide');
    if (!slide || slide.querySelector('.play-overlay')) return;
    const overlay = document.createElement('div');
    overlay.className = 'play-overlay';
    overlay.innerHTML = '<div class="play-overlay-btn"></div>';
    overlay.addEventListener('click', (e) => { e.stopPropagation(); overlay.remove(); video.muted = false; video.volume = 1.0; video.play().catch(() => {}); });
    slide.appendChild(overlay);
  }

  function showErrorOverlay(video, videoPath) {
    const slide = video?.closest('.video-slide');
    if (!slide || slide.querySelector('.video-error-overlay')) return;
    video._failed = true;
    clearTimeout(video._stallTimer);
    const overlay = document.createElement('div');
    overlay.className = 'video-error-overlay';
    overlay.innerHTML = '<div>⚠️</div><span>Video failed to load</span><span>Skipping in 2s... (tap to retry)</span>';
    overlay.addEventListener('click', (e) => { e.stopPropagation(); overlay.remove(); clearTimeout(slide._autoSkipTimer); video._failed = false; video._cdnAttempts = 0; video._cdnNodeIndex = CDN_NODES.indexOf(preferredNode); video.src = cdnUrl(videoPath, preferredNode, true); video.load(); tryPlayVideo(video); });
    slide.appendChild(overlay);
    slide._autoSkipTimer = setTimeout(() => {
      const player = document.querySelector('.video-player');
      if (!player) return;
      const allSlides = player.querySelectorAll('.video-slide:not(.loading-placeholder)');
      const nextIdx = Math.min(currentSlideIdx + 1, allSlides.length - 1);
      if (nextIdx !== currentSlideIdx) allSlides[nextIdx]?.scrollIntoView({ behavior: 'smooth' });
    }, 2000);
  }

  function removePlayOverlay(video) {
    const slide = video?.closest('.video-slide');
    if (!slide) return;
    slide.querySelector('.play-overlay')?.remove();
    slide.querySelector('.video-error-overlay')?.remove();
  }

  function isCurrentSlideVideo(video) {
    const slide = video?.closest('.video-slide');
    if (!slide) return false;
    return parseInt(slide.dataset.index, 10) === currentSlideIdx;
  }

  function setupVideoErrorFallback(video, videoPath) {
    if (!videoPath || video._cdnFallbackAttached) return;
    video._cdnFallbackAttached = true;
    video._cdnNodeIndex = CDN_NODES.indexOf(preferredNode);
    if (video._cdnNodeIndex < 0) video._cdnNodeIndex = 0;
    video._cdnAttempts = 0;
    video._disposed = false;
    video.addEventListener('error', () => {
      if (video._disposed || !video.isConnected) return;
      if (!isCurrentSlideVideo(video)) return;
      video._cdnAttempts++;
      if (video._cdnAttempts >= CDN_NODES.length * 2) { showErrorOverlay(video, videoPath); return; }
      video._cdnNodeIndex = (video._cdnNodeIndex + 1) % CDN_NODES.length;
      const nextNode = CDN_NODES[video._cdnNodeIndex];
      const retryDelay = Math.min(300 * video._cdnAttempts, 2000);
      setTimeout(() => { if (video._disposed || !video.isConnected) return; if (!isCurrentSlideVideo(video)) return; video.src = cdnUrl(videoPath, nextNode, true); video.load(); }, retryDelay);
    });
    video.addEventListener('loadstart', () => {
      video._loadStart = Date.now();
      clearTimeout(video._stallTimer);
      video._stallTimer = setTimeout(() => {
        if (video._disposed || !video.isConnected) return;
        if (!isCurrentSlideVideo(video)) return;
        if (video._failed) return;
        if (video.readyState < 2 && !video.error) { video._cdnAttempts++; video._cdnNodeIndex = (video._cdnNodeIndex + 1) % CDN_NODES.length; const nextNode = CDN_NODES[video._cdnNodeIndex]; video.src = cdnUrl(videoPath, nextNode, true); video.load(); }
      }, 15000);
    });
    video.addEventListener('canplay', () => { clearTimeout(video._stallTimer); }, { once: true });
  }

  function startWatchTimer(postId, video) {
    if (!postId || isWatched(postId)) return;
    stopWatchTimer(postId);
    let watchTime = 0;
    const interval = setInterval(() => {
      if (video && !video.paused && !video.ended) {
        watchTime += 0.5;
        if (watchTime >= 3) { markWatched(postId); clearInterval(interval); activeWatchTimers.delete(postId); const slide = video.closest('.video-slide'); if (slide) slide.classList.add('watched-marker'); }
      }
    }, 500);
    activeWatchTimers.set(postId, interval);
  }

  function stopWatchTimer(postId) { if (activeWatchTimers.has(postId)) { clearInterval(activeWatchTimers.get(postId)); activeWatchTimers.delete(postId); } }

  // =========================================================================
  // CONNECTION BUDGET & TRICKLE PRELOAD
  // =========================================================================
  const MAX_CDN_CONNECTIONS = 30;
  const EVICT_COOLDOWN_MS = 450;
  const TRICKLE_INTERVAL_MIN = 50;
  const TRICKLE_INTERVAL_MAX = 150;
  const TRICKLE_RATIO = 0.20;
  let _activeConnections = 0;
  let _recentlyEvicted = [];
  let _trickleTimer = null;
  let _trickleQueue = [];

  function mountVideo(slide) {
    if (slide.querySelector('video')) return;
    const videoPath = slide.dataset.videoPath;
    const url = getPreloadedUrl(videoPath) || cdnUrl(videoPath);
    const video = document.createElement('video');
    video.src = url; video.loop = true; video.playsInline = true; video.preload = 'metadata';
    video.style.cssText = 'max-width:100%;max-height:100%;object-fit:contain;';
    _activeConnections++;
    if (!slide.querySelector('.video-progress-container')) {
      const progress = document.createElement('div');
      progress.className = 'video-progress-container';
      progress.innerHTML = `<div class="video-progress-bar"></div><div class="video-time-display" style="position:absolute;top:6px;right:8px;color:#fff;font-size:11px;background:rgba(0,0,0,0.5);padding:2px 6px;border-radius:4px;display:none;"></div>`;
      slide.appendChild(progress);
    }
    const bar = slide.querySelector('.video-progress-bar');
    const timeDisplay = slide.querySelector('.video-time-display');
    if (bar && timeDisplay) {
      video.addEventListener('timeupdate', () => {
        if (!video.duration) return;
        bar.style.width = `${(video.currentTime / video.duration) * 100}%`;
        const left = video.duration - video.currentTime;
        const m = Math.floor(left / 60); const s = Math.floor(left % 60).toString().padStart(2, '0');
        timeDisplay.textContent = `-${m}:${s}`; timeDisplay.style.display = 'block';
      });
    }
    const info = slide.querySelector('.video-info');
    if (info) slide.insertBefore(video, info); else slide.appendChild(video);
    setupVideoErrorFallback(video, videoPath);
  }

  function unmountVideo(slide) {
    const video = slide.querySelector('video');
    if (!video) return;
    video._disposed = true; clearTimeout(video._stallTimer);
    stopWatchTimer(slide.dataset.postId);
    video.pause(); video.removeAttribute('src');
    while (video.firstChild) video.removeChild(video.firstChild);
    video.load();
    if (video._objectUrl) { try { URL.revokeObjectURL(video._objectUrl); } catch (_) {} video._objectUrl = null; }
    video.remove();
    const videoPath = slide.dataset.videoPath;
    if (videoPath) { _recentlyEvicted.push(videoPath); if (_recentlyEvicted.length > 30) _recentlyEvicted.shift(); }
    slide._manualPlay = false;
    slide.querySelectorAll('.play-overlay, .video-error-overlay, .seek-overlay').forEach(el => el.remove());
    _activeConnections = Math.max(0, _activeConnections - 1);
  }

  function scheduleTricklePreload() {
    if (_trickleTimer) return;
    if (_recentlyEvicted.length === 0) return;
    _trickleTimer = setTimeout(() => {
      _trickleTimer = null;
      const evicted = _recentlyEvicted.splice(0);
      const count = Math.max(1, Math.ceil(evicted.length * TRICKLE_RATIO));
      const toPreload = evicted.slice(0, count);
      if (toPreload.length === 0) return;
      const budget = MAX_CDN_CONNECTIONS - _activeConnections;
      if (budget <= 2) return;
      const capped = toPreload.slice(0, Math.min(budget - 1, toPreload.length));
      _trickleQueue.push(...capped.map(path => getPreloadedUrl(path) || cdnUrl(path)));
      drainTrickleQueue();
    }, EVICT_COOLDOWN_MS);
  }

  function drainTrickleQueue() {
    if (_trickleQueue.length === 0) return;
    if (_activeConnections >= MAX_CDN_CONNECTIONS) return;
    const url = _trickleQueue.shift();
    if (!url) return;
    _activeConnections++;
    GM_xmlhttpRequest({
      method: 'GET', url,
      headers: { 'Range': 'bytes=0-262143', 'Referer': PAGE_ORIGIN + '/', 'User-Agent': CFG.ua },
      responseType: 'arraybuffer', timeout: 8000, anonymous: false,
      onload() { _activeConnections = Math.max(0, _activeConnections - 1); setTimeout(() => drainTrickleQueue(), TRICKLE_INTERVAL_MIN + Math.random() * (TRICKLE_INTERVAL_MAX - TRICKLE_INTERVAL_MIN)); },
      onerror() { _activeConnections = Math.max(0, _activeConnections - 1); setTimeout(() => drainTrickleQueue(), TRICKLE_INTERVAL_MIN + Math.random() * (TRICKLE_INTERVAL_MAX - TRICKLE_INTERVAL_MIN)); },
      ontimeout() { _activeConnections = Math.max(0, _activeConnections - 1); drainTrickleQueue(); }
    });
  }

  function manageVideoResources(player) {
    if (!player) return;
    const slides = player.querySelectorAll('.video-slide:not(.loading-placeholder)');
    let evictedCount = 0;
    slides.forEach((slide, i) => {
      const distance = Math.abs(i - currentSlideIdx);
      if (distance <= 2) {
        if (!slide.querySelector('video') && _activeConnections >= MAX_CDN_CONNECTIONS) return;
        mountVideo(slide);
        const video = slide.querySelector('video');
        if (video && !video.dataset.cdnStreaming && typeof window.cdnStream === 'function') {
          if (distance <= 1) { try { video._optimizationPromise = window.cdnStream(video); } catch (e) {} }
        }
        if (video) { video.preload = distance === 0 ? 'auto' : distance === 1 ? 'auto' : 'metadata'; }
      } else {
        if (slide.querySelector('video')) { unmountVideo(slide); evictedCount++; }
      }
    });
    if (evictedCount > 0) scheduleTricklePreload();
    backgroundPreloadAhead(slides);
  }

  let _bgPreloadCache = new Map();
  let _bgPreloadInFlight = new Set();

  async function backgroundPreloadAhead(slides) {
    if (!urlResolver) return;
    if (_activeConnections >= MAX_CDN_CONNECTIONS - 3) return;
    for (let offset = 3; offset <= 6; offset++) {
      const targetIdx = currentSlideIdx + offset;
      if (targetIdx >= slides.length) break;
      const slide = slides[targetIdx];
      if (!slide) continue;
      const videoPath = slide.dataset.videoPath;
      if (!videoPath || _bgPreloadCache.has(videoPath) || _bgPreloadInFlight.has(videoPath)) continue;
      _bgPreloadInFlight.add(videoPath);
      (async () => {
        try {
          const originalUrl = cdnUrl(videoPath);
          const resolved = await urlResolver.resolve(originalUrl);
          _bgPreloadCache.set(videoPath, resolved);
          if (_activeConnections < MAX_CDN_CONNECTIONS - 5) {
            _activeConnections++;
            GM_xmlhttpRequest({ method: 'HEAD', url: resolved, headers: { 'Referer': PAGE_ORIGIN + '/', 'User-Agent': CFG.ua }, timeout: 5000, anonymous: false, onload() { _activeConnections = Math.max(0, _activeConnections - 1); }, onerror() { _activeConnections = Math.max(0, _activeConnections - 1); }, ontimeout() { _activeConnections = Math.max(0, _activeConnections - 1); } });
          }
        } catch (e) {}
        finally { _bgPreloadInFlight.delete(videoPath); }
      })();
    }
    if (_bgPreloadCache.size > 50) { const entries = [..._bgPreloadCache.entries()]; _bgPreloadCache = new Map(entries.slice(entries.length - 30)); }
  }

  function getPreloadedUrl(videoPath) { return _bgPreloadCache.get(videoPath) || null; }

  // =========================================================================
  // RENDERING - CREATORS / TAGS / RECOMMENDED
  // =========================================================================
  async function renderCreators(creators) {
    const page = document.getElementById('explore-page-creators');
    if (!page) return;
    const shuffled = [...creators].sort(() => Math.random() - 0.5);
    page.innerHTML = `<div class="creator-grid">${shuffled.map(c => `<div class="creator-card" data-service="${escapeAttr(c.service)}" data-id="${escapeAttr(c.id)}"><img class="creator-banner" src="${escapeAttr(bannerUrl(c.service, c.id))}" onerror="this.style.display='none'"><div class="creator-info"><div class="creator-name">${escapeHtml(c.name || c.id)}</div><div class="creator-service">${escapeHtml(c.service)}</div></div></div>`).join('')}</div>`;
    page.querySelectorAll('.creator-card').forEach(card => { card.addEventListener('click', () => playCreator(card.dataset.service, card.dataset.id)); });
  }

  async function renderRecommended() {
    const page = document.getElementById('explore-page-recommended');
    if (!page) return;
    page.innerHTML = '<div class="explore-loading"><div class="explore-loading-spinner"></div>Loading recommended creators...</div>';
    let recommended = [];
    if (currentPlaybackContext?.type === 'creator') {
      const apiRecs = await getRecommendedCreators(currentPlaybackContext.service, currentPlaybackContext.id);
      if (apiRecs.length > 0) recommended = apiRecs;
    }
    if (recommended.length === 0) {
      const allCreators = await loadExternalCreatorsList();
      if (!allCreators?.length) { page.innerHTML = '<div class="explore-loading">No recommended creators found</div>'; return; }
      recommended = [...allCreators].sort(() => Math.random() - 0.5).slice(0, 30);
    }
    page.innerHTML = `<div class="creator-grid">${recommended.map(c => `<div class="creator-card" data-service="${escapeAttr(c.service)}" data-id="${escapeAttr(c.id)}"><img class="creator-banner" src="${escapeAttr(bannerUrl(c.service, c.id))}" onerror="this.style.display='none'"><div class="creator-info"><div class="creator-name">${escapeHtml(c.name || c.id)}</div><div class="creator-service">${escapeHtml(c.service)}</div></div></div>`).join('')}</div>`;
    page.querySelectorAll('.creator-card').forEach(card => { card.addEventListener('click', () => playCreator(card.dataset.service, card.dataset.id)); });
  }

  async function renderTags() {
    const page = document.getElementById('explore-page-tags');
    if (!page) return;
    page.innerHTML = '<div class="explore-loading"><div class="explore-loading-spinner"></div>Loading tags...</div>';
    const tags = await loadExternalTags();
    const shuffled = [...tags].sort(() => Math.random() - 0.5);
    page.innerHTML = `<div class="tag-grid">${shuffled.map(t => `<div class="tag-card" data-tag="${escapeAttr(t.tag)}"><div class="tag-name">#${escapeHtml(t.tag)}</div><div class="tag-count">${t.count.toLocaleString()} posts</div><div class="tag-category">${escapeHtml(t.category)}</div></div>`).join('')}</div>`;
    page.querySelectorAll('.tag-card').forEach(card => { card.addEventListener('click', () => playTag(card.dataset.tag)); });
  }

  // =========================================================================
  // SMART SEARCH — Opens a dedicated explorer overlay on top
  // =========================================================================
  function createSmartSearchTab() {
    let container = document.getElementById('smart-search-view');
    if (!container) {
      container = document.createElement('div');
      container.id = 'smart-search-view';
      container.innerHTML = `
        <div class="smart-search-panel">
          <div class="smart-search-title">🔍 Smart Search</div>
          <div class="smart-search-row">
            <div class="smart-search-field">
              <label>Tag 1</label>
              <select id="smart-tag1">
                <option value="">-- Select Category --</option>
                ${TAG_CATEGORIES.map(cat => `<optgroup label="${escapeHtml(cat.label)}">${cat.tags.map(t => `<option value="${escapeAttr(t)}">${escapeHtml(t)}</option>`).join('')}</optgroup>`).join('')}
              </select>
            </div>
            <div class="smart-search-field">
              <label>Tag 2 (Optional)</label>
              <select id="smart-tag2">
                <option value="">-- None --</option>
                ${TAG_CATEGORIES.map(cat => `<optgroup label="${escapeHtml(cat.label)}">${cat.tags.map(t => `<option value="${escapeAttr(t)}">${escapeHtml(t)}</option>`).join('')}</optgroup>`).join('')}
              </select>
            </div>
          </div>
          <div class="smart-search-field smart-search-keyword">
            <label>Keyword (Optional)</label>
            <input type="text" id="smart-query" placeholder="e.g. outdoor, amateur, lingerie..." />
          </div>
          <button class="smart-search-btn" id="btn-smart-search">▶ Search &amp; Explore</button>
        </div>
      `;
    }

    const btn = container.querySelector('#btn-smart-search');
    if (btn) {
      btn.onclick = async () => {
        const t1 = container.querySelector('#smart-tag1').value;
        const t2 = container.querySelector('#smart-tag2').value;
        const q = container.querySelector('#smart-query').value.trim();
        if (!t1 && !t2 && !q) { alert('Select at least one filter'); return; }

        const labelParts = [t1, t2, q].filter(Boolean);
        const searchLabel = labelParts.join(' + ');

        // Show loading state on button
        btn.disabled = true;
        btn.textContent = '⏳ Searching...';

        try {
          const params = new URLSearchParams();
          if (t1) params.append('tag', t1);
          if (t2) params.append('tag', t2);
          if (q) params.set('q', q);

          const raw = await gmFetch(`/api/v1/posts?${params.toString()}`);
          const data = Array.isArray(raw) ? raw : (raw?.results || raw?.posts || []);

          if (!data || data.length === 0) {
            alert('No results found for this search.');
            return;
          }

          // Open the full explorer overlay with results
          openSmartSearchExplorer(data, searchLabel, { t1, t2, q, offset: data.length, hasMore: data.length >= 50 });

        } catch (e) {
          alert(`Search error: ${e.message}`);
        } finally {
          btn.disabled = false;
          btn.textContent = '▶ Search & Explore';
        }
      };
    }

    return container;
  }

  // =========================================================================
  // SMART SEARCH EXPLORER OVERLAY
  // =========================================================================
  function openSmartSearchExplorer(posts, label, searchCtx) {
    // Remove any existing overlay
    document.getElementById('smart-search-overlay')?.remove();

    const videoPosts = posts.filter(p => isVideoPost(p));
    const allPosts = posts;

    const overlay = document.createElement('div');
    overlay.id = 'smart-search-overlay';
    overlay.innerHTML = `
      <div class="sso-header">
        <button class="sso-back" id="sso-back-btn">← Back</button>
        <div class="sso-title">🔍 ${escapeHtml(label)}</div>
        <div class="sso-count" id="sso-count">${allPosts.length} results</div>
      </div>
      <div class="sso-content">
        <div class="sso-mode-tabs">
          <button class="sso-mode-btn active" data-mode="videos">▶ Videos (${videoPosts.length})</button>
          <button class="sso-mode-btn" data-mode="all">All Posts (${allPosts.length})</button>
        </div>
        <div id="sso-body"></div>
      </div>
    `;

    document.body.appendChild(overlay);

    // Back button
    overlay.querySelector('#sso-back-btn').addEventListener('click', () => {
      overlay.remove();
    });

    // Mode tabs
    let currentMode = 'videos';
    overlay.querySelectorAll('.sso-mode-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        overlay.querySelectorAll('.sso-mode-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        currentMode = btn.dataset.mode;
        renderSSOBody(currentMode);
      });
    });

    // State for lazy-load
    let ssoAllPosts = [...allPosts];
    let ssoOffset = searchCtx.offset || allPosts.length;
    let ssoLoading = false;
    let ssoHasMore = searchCtx.hasMore;

    function renderSSOBody(mode) {
      const body = overlay.querySelector('#sso-body');
      if (!body) return;

      const displayPosts = mode === 'videos' ? ssoAllPosts.filter(p => isVideoPost(p)) : ssoAllPosts;

      if (displayPosts.length === 0) {
        body.innerHTML = '<div class="sso-empty">No ' + (mode === 'videos' ? 'video' : '') + ' posts found.</div>';
        return;
      }

      // Play All button (only for videos mode)
      const playAllBtn = mode === 'videos' ? `
        <button class="sso-play-all" id="sso-play-all">
          ▶ Play All Videos (${displayPosts.length})
        </button>
      ` : '';

      body.innerHTML = `
        ${playAllBtn}
        <div class="sso-grid" id="sso-cards"></div>
        ${ssoHasMore ? '<button class="trending-load-more" id="sso-load-more">Load More</button>' : ''}
      `;

      // Play All
      body.querySelector('#sso-play-all')?.addEventListener('click', () => {
        overlay.remove();
        launchSmartSearchPlayer(displayPosts, label, searchCtx);
      });

      // Load More
      body.querySelector('#sso-load-more')?.addEventListener('click', () => loadMoreSSO());

      // Cards
      renderSSOCards(body.querySelector('#sso-cards'), displayPosts, mode);
    }

    function renderSSOCards(container, posts, mode) {
      if (!container) return;
      posts.forEach(post => {
        const p = post.post || post;
        const service = p.service || post.service || 'unknown';
        const user = p.user || post.user || 'unknown';
        const postId = p.id || post.id || '';
        const title = p.title || p.substring || '';
        const hasVideo = isVideoPost(post);
        const watched = isWatched(postId);

        const file = p.file || {};
        const attachments = p.attachments || [];
        let thumbUrl = '';
        if (file.path && /\.(jpe?g|png|gif|webp)$/i.test(file.path)) thumbUrl = cdnUrl(file.path, preferredNode);
        else { for (const att of attachments) { if (att.path && /\.(jpe?g|png|gif|webp)$/i.test(att.path)) { thumbUrl = cdnUrl(att.path, preferredNode); break; } } }
        if (!thumbUrl) thumbUrl = avatarUrl(service, user);

        const card = document.createElement('div');
        card.className = 'trending-card';
        card.style.opacity = watched ? '0.5' : '1';
        card.innerHTML = `
          <div class="trending-thumb">
            <img src="${escapeAttr(thumbUrl)}" loading="lazy" onerror="this.style.display='none'">
            ${hasVideo ? '<div class="trending-play-icon">▶</div>' : ''}
          </div>
          <div class="trending-card-info">
            <div class="trending-card-title">${escapeHtml(title || 'Untitled post')}</div>
            <div class="trending-card-meta">
              <span class="trending-card-creator">${escapeHtml(user)}</span>
              <span class="trending-card-service">${escapeHtml(service)}</span>
            </div>
          </div>
        `;

        card.addEventListener('click', () => {
          if (hasVideo) {
            overlay.remove();
            // Play just this video, then chain into creator
            playCreatorFromPost(post, service, user, label);
          } else {
            window.open(`${COOMER_ORIGIN}/${service}/user/${user}/post/${postId}`, '_blank');
          }
        });

        container.appendChild(card);
      });
    }

    async function loadMoreSSO() {
      if (ssoLoading || !ssoHasMore) return;
      ssoLoading = true;
      const loadBtn = overlay.querySelector('#sso-load-more');
      if (loadBtn) { loadBtn.disabled = true; loadBtn.textContent = 'Loading...'; }

      try {
        const params = new URLSearchParams();
        if (searchCtx.t1) params.append('tag', searchCtx.t1);
        if (searchCtx.t2) params.append('tag', searchCtx.t2);
        if (searchCtx.q) params.set('q', searchCtx.q);
        params.set('o', ssoOffset);

        const raw = await gmFetch(`/api/v1/posts?${params.toString()}`);
        const newPosts = Array.isArray(raw) ? raw : (raw?.results || raw?.posts || []);

        if (newPosts.length > 0) {
          ssoAllPosts.push(...newPosts);
          ssoOffset += newPosts.length;
          ssoHasMore = newPosts.length >= 50;

          // Update count display
          const countEl = overlay.querySelector('#sso-count');
          if (countEl) countEl.textContent = `${ssoAllPosts.length} results`;

          // Append new cards to current grid
          const grid = overlay.querySelector('#sso-cards');
          if (grid) {
            const displayPosts = currentMode === 'videos' ? newPosts.filter(p => isVideoPost(p)) : newPosts;
            renderSSOCards(grid, displayPosts, currentMode);
          }
        } else {
          ssoHasMore = false;
        }
      } catch (e) {
        console.error('[SmartSearch] Load more error:', e);
      } finally {
        ssoLoading = false;
        const loadBtn = overlay.querySelector('#sso-load-more');
        if (loadBtn) {
          if (ssoHasMore) { loadBtn.disabled = false; loadBtn.textContent = 'Load More'; }
          else { loadBtn.textContent = 'No more results'; loadBtn.disabled = true; }
        }
      }
    }

    // Initial render
    renderSSOBody('videos');
  }

  // =========================================================================
  // SMART SEARCH → PLAYER LAUNCH
  // =========================================================================
  function launchSmartSearchPlayer(posts, label, searchCtx) {
    const videoPosts = posts.filter(p => isVideoPost(p) && !isWatched(getPostId(p)));
    if (videoPosts.length === 0) { alert('No unwatched videos to play.'); return; }

    const page = document.getElementById('explore-page-player');
    if (!page) return;

    page.classList.add('active');
    document.querySelectorAll('.explore-page').forEach(p => { if (p.id !== 'explore-page-player') p.classList.remove('active'); });
    // Switch active tab visual
    document.querySelectorAll('.explore-tab').forEach(t => t.classList.remove('active'));

    currentPlaybackContext = {
      type: 'smartsearch',
      label,
      searchCtx,
      offset: searchCtx.offset || posts.length,
      hasMore: searchCtx.hasMore ?? false,
      _allFetched: [...posts]
    };

    renderVideoPlayer(videoPosts, `Search: ${label}`);
  }

  async function playCreatorFromPost(post, service, user, label) {
    const page = document.getElementById('explore-page-player');
    if (!page) return;

    page.classList.add('active');
    document.querySelectorAll('.explore-page').forEach(p => { if (p.id !== 'explore-page-player') p.classList.remove('active'); });

    currentPlaybackContext = { type: 'creator', service, id: user, offset: 0, hasMore: true };
    renderVideoPlayer([post], label || `${user} (${service})`);

    try {
      const result = await getCreatorPosts(service, user, 0);
      const videoPosts = result.posts.filter(p => {
        const pid = getPostId(p); const trendingPid = getPostId(post);
        return isVideoPost(p) && !isWatched(pid) && pid !== trendingPid;
      });
      currentPlaybackContext = { type: 'creator', service, id: user, offset: result.posts.length, hasMore: result.hasMore };
      if (videoPosts.length > 0) appendVideosToPlayer(videoPosts);
    } catch (e) { console.warn('[Explore] Error loading creator after search pick:', e); }
  }

  // =========================================================================
  // MATCHED CREATORS TAB
  // =========================================================================
  function createMatchedCreatorsTab() {
    let container = document.getElementById('matched-creators-view');
    if (!container) {
      container = document.createElement('div');
      container.className = 'trending-container hidden';
      container.id = 'matched-creators-view';
      container.innerHTML = `<h2 style="color:#4ade80;padding:20px 20px 0;">Curated Creators</h2><div class="trending-grid" id="matched-grid" style="padding-top:16px;"></div>`;
      setTimeout(() => {
        const grid = container.querySelector('#matched-grid');
        if (!grid) return;
        MATCHED_CREATORS.forEach(c => {
          const card = document.createElement('div'); card.className = 'trending-card'; card.style.cursor = 'pointer';
          card.innerHTML = `<div class="trending-card-info"><div class="trending-card-title">${escapeHtml(c.id)}</div><div class="trending-card-meta"><span class="trending-card-service">${escapeHtml(c.service)}</span></div></div>`;
          card.onclick = () => window.open(`${COOMER_ORIGIN}/${c.service}/user/${c.id}`, '_blank');
          grid.appendChild(card);
        });
      }, 100);
    }
    return container;
  }

  // =========================================================================
  // TRENDING PAGE
  // =========================================================================
  let trendingState = { period: 'day', offset: 0, posts: [], loading: false, date: getTodayDate(), renderVersion: 0 };
  function getTodayDate() { return new Date().toISOString().split('T')[0]; }
  function abbreviateNumber(value) {
    if (value >= 1000) { const suffixes = ["", "k", "m", "b", "t"]; const suffixNum = Math.floor(("" + value).length / 3); let shortValue = parseFloat((suffixNum !== 0 ? value / Math.pow(1000, suffixNum) : value).toPrecision(2)); if (shortValue % 1 !== 0) shortValue = shortValue.toFixed(1); return shortValue + suffixes[suffixNum]; }
    return value;
  }

  async function fetchPopularPosts(period, offset = 0) {
    const date = trendingState.date || getTodayDate();
    const result = await gmFetch(`/api/v1/posts/popular?date=${date}&period=${period}&o=${offset}`);
    if (result && typeof result === 'object' && Array.isArray(result.posts)) return result;
    if (Array.isArray(result)) return { posts: result, info: {}, props: {} };
    return { posts: [], info: {}, props: {} };
  }

  function buildTrendingCardHTML(post) {
    const p = post.post || post;
    const service = p.service || post.service || 'unknown';
    const user = p.user || post.user || 'unknown';
    const postId = p.id || post.id || '';
    const title = p.title || p.substring || '';
    const hasVideo = isVideoPost(post);
    const favCount = p.fav_count || post.fav_count || 0;
    let thumbUrl = '';
    const file = p.file || {}; const attachments = p.attachments || [];
    if (file.path && /\.(jpe?g|png|gif|webp)$/i.test(file.path)) thumbUrl = cdnUrl(file.path, preferredNode);
    else { for (const att of attachments) { if (att.path && /\.(jpe?g|png|gif|webp)$/i.test(att.path)) { thumbUrl = cdnUrl(att.path, preferredNode); break; } } }
    if (!thumbUrl) thumbUrl = avatarUrl(service, user);
    let dateStr = '';
    const published = p.published || p.added || '';
    if (published) { try { dateStr = new Date(published).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } catch (e) {} }
    const watchedStyle = isWatched(postId) ? 'style="opacity:0.5"' : '';
    return `<div class="trending-card" data-service="${escapeAttr(service)}" data-user="${escapeAttr(user)}" data-post-id="${escapeAttr(postId)}" ${watchedStyle}><div class="trending-thumb">${thumbUrl ? `<img src="${escapeAttr(thumbUrl)}" loading="lazy">` : '<div style="width:100%;height:100%;background:#1a1a1a;"></div>'}${hasVideo ? '<div class="trending-play-icon">▶</div>' : ''}</div><div class="trending-card-info"><div class="trending-card-title">${escapeHtml(title || 'Untitled post')}</div><div class="trending-card-meta"><span class="trending-card-creator">${escapeHtml(user)}</span><span class="trending-card-service">${escapeHtml(service)}</span></div></div></div>`;
  }

  async function renderTrending(period, forceReload = false) {
    const page = document.getElementById('explore-page-trending');
    if (!page) return;
    if (!period && !forceReload && trendingState.posts.length > 0) return;
    if (period) { trendingState.period = period; trendingState.date = getTodayDate(); }
    trendingState.offset = 0; trendingState.posts = []; trendingState.loading = false;
    trendingState.renderVersion++;
    const currentVersion = trendingState.renderVersion;
    page.innerHTML = `<div class="trending-header" style="padding:16px 20px 0;"><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;"><label style="color:#9ca3af;font-weight:600;">Period:</label><button class="trending-period-btn ${trendingState.period==='day'?'active':''}" data-period="day">Today</button><button class="trending-period-btn ${trendingState.period==='week'?'active':''}" data-period="week">This Week</button><button class="trending-period-btn ${trendingState.period==='month'?'active':''}" data-period="month">This Month</button></div></div><div class="trending-grid" style="margin-top:20px;"><div class="explore-loading" style="grid-column:1/-1;padding:60px;"><div class="explore-loading-spinner"></div></div></div>`;
    page.querySelectorAll('.trending-period-btn').forEach(btn => { btn.addEventListener('click', () => renderTrending(btn.dataset.period)); });
    try {
      const result = await fetchPopularPosts(trendingState.period, 0);
      if (trendingState.renderVersion !== currentVersion) return;
      const posts = result.posts || [];
      trendingState.posts = posts; trendingState.offset = posts.length;
      const grid = page.querySelector('.trending-grid');
      if (!posts.length) { grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:60px;color:#6b7280;">No trending posts found.</div>'; return; }
      grid.innerHTML = posts.map(p => buildTrendingCardHTML(p)).join('');
      attachTrendingCardListeners(page);
      if (posts.length >= 50) { const btn = document.createElement('button'); btn.className = 'trending-load-more'; btn.textContent = 'Load More'; btn.addEventListener('click', () => loadMoreTrending()); page.appendChild(btn); }
    } catch (e) { page.querySelector('.trending-grid').innerHTML = `<div style="grid-column:1/-1;text-align:center;padding:60px;color:#f87171;">Error: ${escapeHtml(e.message)}</div>`; }
  }

  async function loadMoreTrending() {
    if (trendingState.loading) return;
    trendingState.loading = true;
    const currentVersion = trendingState.renderVersion;
    const page = document.getElementById('explore-page-trending');
    const loadMoreBtn = page?.querySelector('.trending-load-more');
    if (loadMoreBtn) { loadMoreBtn.disabled = true; loadMoreBtn.textContent = 'Loading...'; }
    try {
      const result = await fetchPopularPosts(trendingState.period, trendingState.offset);
      if (trendingState.renderVersion !== currentVersion) return;
      const posts = result.posts || [];
      if (!posts.length) { if (loadMoreBtn) { loadMoreBtn.textContent = 'No more posts'; loadMoreBtn.disabled = true; } trendingState.loading = false; return; }
      trendingState.posts.push(...posts); trendingState.offset += posts.length;
      const grid = page?.querySelector('.trending-grid');
      if (grid) { grid.insertAdjacentHTML('beforeend', posts.map(p => buildTrendingCardHTML(p)).join('')); attachTrendingCardListeners(page); }
      if (loadMoreBtn) { loadMoreBtn.disabled = false; loadMoreBtn.textContent = posts.length < 50 ? 'No more posts' : 'Load More'; if (posts.length < 50) loadMoreBtn.disabled = true; }
      trendingState.loading = false;
    } catch (e) { if (loadMoreBtn) { loadMoreBtn.disabled = false; loadMoreBtn.textContent = 'Retry'; } trendingState.loading = false; }
  }

  function attachTrendingCardListeners(page) {
    page.querySelectorAll('.trending-card').forEach(card => {
      if (card._listenerAttached) return; card._listenerAttached = true;
      card.addEventListener('click', () => {
        const service = card.dataset.service; const user = card.dataset.user; const postId = card.dataset.postId;
        const post = trendingState.posts.find(p => { const pp = p.post || p; return String(pp.id || p.id) === postId; });
        if (post && isVideoPost(post)) playTrendingVideo(post, service, user);
        else playCreator(service, user);
      });
    });
  }

  async function playTrendingVideo(post, service, user) {
    const page = document.getElementById('explore-page-player');
    if (!page) return;
    page.innerHTML = `<div class="explore-loading"><div class="explore-loading-spinner"></div>Loading video...</div>`;
    page.classList.add('active');
    document.querySelectorAll('.explore-page').forEach(p => { if (p.id !== 'explore-page-player') p.classList.remove('active'); });
    currentPlaybackContext = { type: 'creator', service, id: user, offset: 0, hasMore: true };
    renderVideoPlayer([post], `Trending – ${user} (${service})`);
    try {
      const result = await getCreatorPosts(service, user, 0);
      const videoPosts = result.posts.filter(p => { const pid = getPostId(p); const trendingPid = getPostId(post); return isVideoPost(p) && !isWatched(pid) && pid !== trendingPid; });
      currentPlaybackContext = { type: 'creator', service, id: user, offset: result.posts.length, hasMore: result.hasMore };
      if (videoPosts.length > 0) appendVideosToPlayer(videoPosts);
    } catch (e) { console.warn('[Explore] Error loading creator after trending:', e); }
  }

  async function playCreator(service, id, offset = 0) {
    const page = document.getElementById('explore-page-player');
    if (!page) return;
    if (offset === 0) {
      page.innerHTML = `<div class="explore-loading"><div class="explore-loading-spinner"></div>Loading videos from ${escapeHtml(id)}...</div>`;
      page.classList.add('active');
      document.querySelectorAll('.explore-page').forEach(p => { if (p.id !== 'explore-page-player') p.classList.remove('active'); });
    }
    try {
      const result = await getCreatorPosts(service, id, offset);
      const videoPosts = result.posts.filter(p => isVideoPost(p) && !isWatched(getPostId(p)));
      const artistName = result.artist?.name || id;
      if (videoPosts.length === 0 && offset === 0) {
        if (result.hasMore && result.posts.filter(isVideoPost).length > 0) { if (offset > 1000) { page.innerHTML = `<div class="explore-loading">Too many watched videos skipped.</div>`; return; } return playCreator(service, id, offset + result.posts.length); }
        page.innerHTML = `<div style="padding:40px;text-align:center;color:#9ca3af;"><p>No unwatched videos found for ${escapeHtml(artistName)}</p><button class="back-btn" style="position:static;display:inline-block;margin-top:16px;">← Back to Creators</button></div>`;
        page.querySelector('.back-btn')?.addEventListener('click', () => { document.querySelector('.explore-tab[data-tab="creators"]')?.click(); });
        return;
      }
      visitedCreators.add(`${service}/${id}`);
      currentPlaybackContext = { type: 'creator', service, id, offset: offset + result.posts.length, hasMore: result.hasMore };
      if (offset === 0) renderVideoPlayer(videoPosts, `${artistName} (${service})`);
      else appendVideosToPlayer(videoPosts);
    } catch (e) { if (offset === 0) page.innerHTML = `<div class="explore-loading" style="color:#f87171;">Error: ${escapeHtml(e.message)}</div>`; }
  }

  function filterDiverseAuthors(posts, maxPerAuthor = 3) {
    const authorCounts = {};
    return posts.filter(p => { const author = p.user || p.author || 'unknown'; authorCounts[author] = (authorCounts[author] || 0) + 1; return authorCounts[author] <= maxPerAuthor; });
  }

  async function playTag(tag, offset = 0) {
    const page = document.getElementById('explore-page-player');
    if (!page) return;
    if (offset === 0) {
      page.innerHTML = `<div class="explore-loading"><div class="explore-loading-spinner"></div>Loading videos for #${escapeHtml(tag)}...</div>`;
      page.classList.add('active');
      document.querySelectorAll('.explore-page').forEach(p => { if (p.id !== 'explore-page-player') p.classList.remove('active'); });
    }
    try {
      const result = await getPostsByTag(tag, offset);
      let videoPosts = result.posts.filter(p => isVideoPost(p) && !isWatched(getPostId(p)));
      videoPosts = filterDiverseAuthors(videoPosts, 2);
      if (videoPosts.length === 0 && offset === 0) {
        if (result.hasMore && result.posts.filter(isVideoPost).length > 0) { if (offset > 1000) { page.innerHTML = `<div class="explore-loading">Too many watched videos skipped.</div>`; return; } return playTag(tag, offset + result.posts.length); }
        page.innerHTML = '<div class="explore-loading">No unwatched videos found</div>'; return;
      }
      currentPlaybackContext = { type: 'tag', tag, offset: offset + result.posts.length, hasMore: result.hasMore };
      if (offset === 0) renderVideoPlayer(videoPosts, `#${tag}`);
      else appendVideosToPlayer(videoPosts);
    } catch (e) { if (offset === 0) page.innerHTML = `<div class="explore-loading" style="color:#f87171;">Error: ${escapeHtml(e.message)}</div>`; }
  }

  async function loadMoreVideos() {
    if (!currentPlaybackContext || isLoadingMore) return;
    // Smart search: load more from API
    if (currentPlaybackContext.type === 'smartsearch') {
      return loadMoreSmartSearch();
    }
    if (!currentPlaybackContext.hasMore) {
      if (currentPlaybackContext.type === 'creator' && !currentPlaybackContext._recommendedExhausted) return loadRecommendedChain();
      return;
    }
    isLoadingMore = true;
    const player = document.querySelector('.video-player');
    if (player) { player.insertAdjacentHTML('beforeend', Array(3).fill(`<div class="video-slide loading-placeholder"><div class="explore-loading"><div class="explore-loading-spinner"></div></div></div>`).join('')); }
    try {
      if (currentPlaybackContext.type === 'creator') await playCreator(currentPlaybackContext.service, currentPlaybackContext.id, currentPlaybackContext.offset);
      else if (currentPlaybackContext.type === 'tag') await playTag(currentPlaybackContext.tag, currentPlaybackContext.offset);
      else if (currentPlaybackContext.type === 'mixer') await loadNextMixBatch();
    } finally { if (player) player.querySelectorAll('.loading-placeholder').forEach(el => el.remove()); isLoadingMore = false; }
  }

  async function loadMoreSmartSearch() {
    if (isLoadingMore) return;
    const ctx = currentPlaybackContext;
    if (!ctx || !ctx.hasMore || !ctx.searchCtx) return;
    isLoadingMore = true;
    try {
      const params = new URLSearchParams();
      if (ctx.searchCtx.t1) params.append('tag', ctx.searchCtx.t1);
      if (ctx.searchCtx.t2) params.append('tag', ctx.searchCtx.t2);
      if (ctx.searchCtx.q) params.set('q', ctx.searchCtx.q);
      params.set('o', ctx.offset);
      const raw = await gmFetch(`/api/v1/posts?${params.toString()}`);
      const newPosts = Array.isArray(raw) ? raw : (raw?.results || raw?.posts || []);
      if (newPosts.length > 0) {
        ctx.offset += newPosts.length;
        ctx.hasMore = newPosts.length >= 50;
        const videoPosts = newPosts.filter(p => isVideoPost(p) && !isWatched(getPostId(p)));
        if (videoPosts.length > 0) appendVideosToPlayer(videoPosts);
      } else { ctx.hasMore = false; }
    } catch (e) { console.error('[SmartSearch] loadMore error:', e); }
    finally { isLoadingMore = false; }
  }

  async function loadRecommendedChain() {
    if (!currentPlaybackContext || currentPlaybackContext.type !== 'creator' || isLoadingMore) return;
    isLoadingMore = true;
    const player = document.querySelector('.video-player');
    try {
      const ctx = currentPlaybackContext;
      if (player && !player.querySelector('.loading-placeholder')) { player.insertAdjacentHTML('beforeend', `<div class="video-slide loading-placeholder"><div class="explore-loading"><div class="explore-loading-spinner"></div></div></div>`); }
      while (true) {
        if (!ctx._recommendedQueue?.length) {
          if (ctx._recommendedExhausted) break;
          const recs = await getRecommendedCreators(ctx.service, ctx.id);
          if (recs?.length > 0) ctx._recommendedQueue = recs.sort((a, b) => (b.score || 0) - (a.score || 0)).slice(0, 10);
          else { ctx._recommendedExhausted = true; break; }
        }
        const next = ctx._recommendedQueue.shift();
        if (!next) { ctx._recommendedExhausted = true; continue; }
        const nextService = next.service || ctx.service; const nextId = next.id || next.name;
        const creatorKey = `${nextService}/${nextId}`;
        if (visitedCreators.has(creatorKey)) continue;
        visitedCreators.add(creatorKey);
        const result = await getCreatorPosts(nextService, nextId, 0);
        const videoPosts = result.posts.filter(p => isVideoPost(p) && !isWatched(getPostId(p)));
        if (videoPosts.length > 0) {
          const savedQueue = ctx._recommendedQueue;
          currentPlaybackContext = { type: 'creator', service: nextService, id: nextId, offset: result.posts.length, hasMore: result.hasMore, _recommendedQueue: savedQueue, _parentCreator: ctx._parentCreator || { service: ctx.service, id: ctx.id } };
          appendVideosToPlayer(videoPosts);
          break;
        }
      }
    } catch (e) { console.error('[Explore] Error in recommended chain:', e); }
    finally { if (player) player.querySelectorAll('.loading-placeholder').forEach(el => el.remove()); isLoadingMore = false; }
  }

  // =========================================================================
  // SLIDE BUILDING
  // =========================================================================
  function buildSlideHTML(post, index) {
    const videoPath = getFirstVideoPath(post);
    if (!videoPath || !/\.(mp4|webm|mov|avi|mkv|m4v)$/i.test(videoPath)) return '';
    const p = post.post || post;
    const service = p.service || post.service || 'unknown';
    const user = p.user || post.user || 'unknown';
    const postId = getPostId(post) || '';
    const watchedClass = isWatched(postId) ? ' watched-marker' : '';
    const title = p.title || p.substring || '';
    const published = p.published || '';
    let dateStr = '';
    if (published) { try { dateStr = new Date(published).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } catch (e) {} }
    return `<div class="video-slide${watchedClass}" data-index="${index}" data-post-id="${escapeAttr(postId)}" data-service="${escapeAttr(service)}" data-user="${escapeAttr(user)}" data-video-path="${escapeAttr(videoPath)}"><div class="video-info"><div class="video-creator">${escapeHtml(user)}</div><div class="video-service">${escapeHtml(service)}</div>${title ? `<div class="video-title">${escapeHtml(title)}</div>` : ''}${dateStr ? `<div class="video-meta">${escapeHtml(dateStr)}</div>` : ''}</div></div>`;
  }

  // =========================================================================
  // SLIDE OBSERVER & PLAYER
  // =========================================================================
  function createSlideObserver(player) {
    return new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        const slide = entry.target;
        const video = slide.querySelector('video');
        if (!video) return;
        const idx = parseInt(slide.dataset.index, 10);
        const postId = slide.dataset.postId;
        const videoPath = slide.dataset.videoPath;
        if (entry.isIntersecting && entry.intersectionRatio > 0.6) {
          currentSlideIdx = idx;
          const favBtn = player.closest('#explore-page-player')?.querySelector('.fav-btn');
          if (favBtn && currentPlaylistPosts[idx]) { const p = currentPlaylistPosts[idx].post || currentPlaylistPosts[idx]; const key = `${p.service}:${p.user}:${p.id}`; favBtn.classList.toggle('active', !!(GM_getValue('favorites', {})[key])); }
          player.querySelectorAll('.video-slide').forEach(otherSlide => {
            const otherIdx = parseInt(otherSlide.dataset.index, 10);
            if (otherIdx !== idx) {
              const otherVideo = otherSlide.querySelector('video');
              if (otherVideo && !otherVideo.paused) {
                if (userManuallyPlaying && otherSlide._manualPlay) { const rect = otherSlide.getBoundingClientRect(); const vis = Math.max(0, Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0)) / rect.height; if (vis >= 0.3) return; }
                otherVideo.pause(); otherVideo.muted = true; otherSlide._manualPlay = false; stopWatchTimer(otherSlide.dataset.postId);
              }
            }
          });
          setupVideoErrorFallback(video, videoPath);
          tryPlayVideo(video);
          startWatchTimer(postId, video);
          manageVideoResources(player);
          updateSlideCounter(player);
        } else if (!entry.isIntersecting || entry.intersectionRatio < 0.3) {
          if (userManuallyPlaying && slide._manualPlay) { const rect = slide.getBoundingClientRect(); const vis = Math.max(0, Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0)) / rect.height; if (vis >= 0.3) return; }
          video.pause(); video.muted = true; slide._manualPlay = false; stopWatchTimer(postId);
        }
      });
    }, { threshold: [0.3, 0.6] });
  }

  function appendVideosToPlayer(posts) {
    const player = document.querySelector('.video-player');
    if (!player) return;
    const validPosts = posts.filter(p => getFirstVideoPath(p));
    currentPlaylistPosts.push(...validPosts);
    const currentSlideCount = player.querySelectorAll('.video-slide:not(.loading-placeholder)').length;
    const slides = validPosts.map((post, i) => buildSlideHTML(post, currentSlideCount + i)).join('');
    if (!slides?.trim()) return;
    const tempDiv = document.createElement('div');
    tempDiv.innerHTML = slides;
    const observer = player._slideObserver || createSlideObserver(player);
    player._slideObserver = observer;
    let appendedCount = 0;
    while (tempDiv.firstChild) {
      const slide = tempDiv.firstChild;
      if (slide?.nodeType === 1) { player.appendChild(slide); observer.observe(slide); appendedCount++; }
      else tempDiv.removeChild(slide);
    }
    updateSlideCounter(player);
    manageVideoResources(player);
  }

  function updateSlideCounter(player) {
    if (!player) return;
    const totalSlides = player.querySelectorAll('.video-slide:not(.loading-placeholder)').length;
    const counter = player.closest('#explore-page-player')?.querySelector('.slide-counter');
    if (counter) counter.textContent = `${currentSlideIdx + 1} / ${totalSlides}`;
  }

  // =========================================================================
  // MAIN VIDEO PLAYER RENDERER
  // =========================================================================
  function renderVideoPlayer(posts, title) {
    const page = document.getElementById('explore-page-player');
    if (!page) return;
    if (page._keyHandler) { document.removeEventListener('keydown', page._keyHandler); page._keyHandler = null; }
    if (page._favListener) { document.removeEventListener('keydown', page._favListener); page._favListener = null; }
    if (page._slideObserver) { page._slideObserver.disconnect(); page._slideObserver = null; }
    if (page._predictiveInterval) { clearInterval(page._predictiveInterval); page._predictiveInterval = null; }
    currentSlideIdx = 0;
    const validPosts = posts.filter(p => getFirstVideoPath(p));
    currentPlaylistPosts = [...validPosts];
    userManuallyPlaying = false;
    activeWatchTimers.forEach(t => clearInterval(t)); activeWatchTimers.clear();
    visitedCreators.clear();
    const slides = validPosts.map((post, i) => buildSlideHTML(post, i)).join('');
    page.innerHTML = `
      <button class="back-btn">← Back to ${escapeHtml(title)}</button>
      <div class="slide-counter">1 / ${validPosts.length}</div>
      <button class="fav-btn">V</button>
      <div class="video-player">${slides}</div>
    `;
    const favBtn = page.querySelector('.fav-btn');
    if (validPosts[0]) { const p = validPosts[0].post || validPosts[0]; const k = `${p.service}:${p.user}:${p.id}`; if (GM_getValue('favorites', {})[k]) favBtn.classList.add('active'); }
    favBtn.addEventListener('click', () => { if (currentPlaylistPosts[currentSlideIdx]) toggleFavorite(currentPlaylistPosts[currentSlideIdx], favBtn); });
    if (!page._favListener) {
      page._favListener = (e) => { if (e.key.toLowerCase() === 'v' && page.classList.contains('active') && currentPlaylistPosts[currentSlideIdx]) toggleFavorite(currentPlaylistPosts[currentSlideIdx], page.querySelector('.fav-btn')); };
      document.addEventListener('keydown', page._favListener);
    }
    page.querySelector('.back-btn').addEventListener('click', () => {
      if (page._slideObserver) { page._slideObserver.disconnect(); page._slideObserver = null; }
      page.querySelectorAll('video').forEach(v => { v._disposed = true; clearTimeout(v._stallTimer); v.pause(); v.removeAttribute('src'); while (v.firstChild) v.removeChild(v.firstChild); v.load(); v.remove(); });
      _activeConnections = 0; _trickleQueue = []; clearTimeout(_trickleTimer); _trickleTimer = null;
      activeWatchTimers.forEach(t => clearInterval(t)); activeWatchTimers.clear();
      const tabToClick = document.querySelector(`.explore-tab[data-tab="${lastActiveTab}"]`) || document.querySelector('.explore-tab[data-tab="creators"]');
      tabToClick?.click();
    });
    const player = page.querySelector('.video-player');
    if (page._slideObserver) { page._slideObserver.disconnect(); page._slideObserver = null; }
    const observer = createSlideObserver(player);
    page._slideObserver = observer;
    player.querySelectorAll('.video-slide').forEach(s => observer.observe(s));
    player.addEventListener('click', (e) => {
      const slide = e.target.closest('.video-slide');
      if (!slide) return;
      const video = slide.querySelector('video');
      if (video) { if (video.paused) { video.muted = false; video.play(); userManuallyPlaying = true; slide._manualPlay = true; } else { video.pause(); userManuallyPlaying = false; slide._manualPlay = false; } }
    });
    manageVideoResources(player);
    page._predictiveInterval = setInterval(() => {
      const allSlides = player.querySelectorAll('.video-slide:not(.loading-placeholder)');
      if (allSlides.length - currentSlideIdx - 1 <= 5 && allSlides.length < currentSlideIdx + 20) loadMoreVideos();
    }, 3000);
    const keyHandler = (e) => {
      if (!page.classList.contains('active')) return;
      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
      const allSlides = player.querySelectorAll('.video-slide:not(.loading-placeholder)');
      if (e.key === 'ArrowDown' || e.key === 'j' || e.key === 's') { e.preventDefault(); userManuallyPlaying = false; allSlides[Math.min(currentSlideIdx + 1, allSlides.length - 1)]?.scrollIntoView({ behavior: 'smooth' }); }
      else if (e.key === 'ArrowUp' || e.key === 'k' || e.key === 'w') { e.preventDefault(); userManuallyPlaying = false; allSlides[Math.max(currentSlideIdx - 1, 0)]?.scrollIntoView({ behavior: 'smooth' }); }
      else if (e.key === ' ') { e.preventDefault(); const slide = allSlides[currentSlideIdx]; const video = slide?.querySelector('video'); if (video) { if (video.paused) { video.muted = false; video.play(); userManuallyPlaying = true; slide._manualPlay = true; } else { video.pause(); userManuallyPlaying = false; slide._manualPlay = false; } } }
      else if (e.key === 'm') { const video = allSlides[currentSlideIdx]?.querySelector('video'); if (video) video.muted = !video.muted; }
      else if (e.key === 'd' || e.key === 'ArrowRight') { e.preventDefault(); const slide = allSlides[currentSlideIdx]; const video = slide?.querySelector('video'); if (video) { video.currentTime = Math.min(video.currentTime + 5, video.duration); showSeekOverlay(slide, '+5s'); } }
      else if (e.key === 'a' || e.key === 'ArrowLeft') { e.preventDefault(); const slide = allSlides[currentSlideIdx]; const video = slide?.querySelector('video'); if (video) { video.currentTime = Math.max(video.currentTime - 5, 0); showSeekOverlay(slide, '-5s'); } }
    };
    document.addEventListener('keydown', keyHandler);
    page._keyHandler = keyHandler;
  }

  // =========================================================================
  // MIXER
  // =========================================================================
  let topCreatorsCache = null;
  async function renderMixer() {
    const page = document.getElementById('explore-page-mixer');
    if (!page) return;
    page.innerHTML = '<div class="explore-loading"><div class="explore-loading-spinner"></div>Loading top creators...</div>';
    const creators = await fetchTopCreators();
    if (!creators.length) { page.innerHTML = '<div class="explore-loading">Failed to load creators.</div>'; return; }
    page.innerHTML = `<div class="mixer-controls"><button class="mixer-play-btn" id="mixer-start-btn">▶ Create Playlist</button><span style="color:#9ca3af;font-size:13px;">Select creators to mix</span></div><div class="mixer-grid">${creators.map(c => `<div class="mixer-card" data-service="${escapeAttr(c.service)}" data-id="${escapeAttr(c.id)}"><img class="mixer-avatar" src="${escapeAttr(avatarUrl(c.service, c.id))}" onerror="this.src=''" alt=""><div class="mixer-info"><div class="mixer-name">${escapeHtml(c.name || c.id)}</div><div class="mixer-stats">❤️ ${abbreviateNumber(c.favorited || 0)}</div></div><div class="mixer-checkbox"></div></div>`).join('')}</div>`;
    page.querySelectorAll('.mixer-card').forEach(card => { card.addEventListener('click', () => card.classList.toggle('selected')); });
    document.getElementById('mixer-start-btn').addEventListener('click', () => {
      const selected = Array.from(page.querySelectorAll('.mixer-card.selected')).map(el => ({ service: el.dataset.service, id: el.dataset.id }));
      if (!selected.length) { alert('Please select at least one creator!'); return; }
      playMix(selected);
    });
  }

  async function fetchTopCreators() {
    if (topCreatorsCache) return topCreatorsCache;
    try { const data = await gmFetch('/api/v1/creators'); if (Array.isArray(data)) { topCreatorsCache = data.sort((a, b) => (b.favorited || 0) - (a.favorited || 0)).slice(0, 100); return topCreatorsCache; } return []; }
    catch (e) { return []; }
  }

  async function loadNextMixBatch() {
    if (!currentPlaybackContext || currentPlaybackContext.type !== 'mixer') return;
    const ctx = currentPlaybackContext;
    const batch = ctx.creators.slice(ctx.nextIndex, ctx.nextIndex + 5);
    if (!batch.length) { ctx.hasMore = false; return; }
    ctx.nextIndex += 5;
    if (ctx.nextIndex >= ctx.creators.length) ctx.hasMore = false;
    try {
      const results = await Promise.all(batch.map(c => getCreatorPosts(c.service, c.id, 0)));
      let newPosts = [];
      for (const res of results) { if (res.posts?.length) newPosts.push(...res.posts.filter(p => isVideoPost(p) || p.file?.path || p.attachments?.length).slice(0, 5)); }
      newPosts.sort(() => Math.random() - 0.5);
      if (newPosts.length > 0) appendVideosToPlayer(newPosts);
      else if (ctx.hasMore) await loadNextMixBatch();
    } catch (e) { console.error(e); }
  }

  async function playMix(creators) {
    const page = document.getElementById('explore-page-player');
    if (!page) return;
    page.innerHTML = `<div class="explore-loading"><div class="explore-loading-spinner"></div>Starting playlist...</div>`;
    page.classList.add('active');
    document.querySelectorAll('.explore-page').forEach(p => { if (p.id !== 'explore-page-player') p.classList.remove('active'); });
    const shuffled = [...creators].sort(() => Math.random() - 0.5);
    try {
      const results = await Promise.all(shuffled.slice(0, 5).map(c => getCreatorPosts(c.service, c.id, 0)));
      let initialPosts = [];
      for (const res of results) { if (res.posts?.length) initialPosts.push(...res.posts.filter(p => isVideoPost(p) || p.file?.path || p.attachments?.length).slice(0, 5)); }
      initialPosts.sort(() => Math.random() - 0.5);
      if (!initialPosts.length) { page.innerHTML = '<div class="explore-loading">No content found.</div>'; return; }
      currentPlaybackContext = { type: 'mixer', creators: shuffled, nextIndex: 5, hasMore: shuffled.length > 5 };
      renderVideoPlayer(initialPosts, 'Custom Playlist');
    } catch (e) { page.innerHTML = `<div class="explore-loading" style="color:#f87171;">Error: ${escapeHtml(e.message)}</div>`; }
  }

  function toggleFavorite(post, btn) {
    const p = post.post || post;
    const favs = GM_getValue('favorites', {});
    const key = `${p.service}:${p.user}:${p.id}`;
    if (favs[key]) { delete favs[key]; if (btn) btn.classList.remove('active'); }
    else { favs[key] = { service: p.service, user: p.user, id: p.id, title: p.title, date: new Date().toISOString() }; if (btn) btn.classList.add('active'); }
    GM_setValue('favorites', favs);
  }

  // =========================================================================
  // MOUNT EXPLORE
  // =========================================================================
  function isExplorePage() { return window.location.hash === EXPLORE_HASH; }

  function unmountExplore() {
    const root = document.getElementById('explore-root');
    if (root) {
      const playerPage = document.getElementById('explore-page-player');
      if (playerPage) {
        if (playerPage._keyHandler) { document.removeEventListener('keydown', playerPage._keyHandler); playerPage._keyHandler = null; }
        if (playerPage._favListener) { document.removeEventListener('keydown', playerPage._favListener); playerPage._favListener = null; }
        if (playerPage._predictiveInterval) { clearInterval(playerPage._predictiveInterval); playerPage._predictiveInterval = null; }
        if (playerPage._slideObserver) { playerPage._slideObserver.disconnect(); playerPage._slideObserver = null; }
      }
      root.querySelectorAll('video').forEach(v => { v._disposed = true; clearTimeout(v._stallTimer); v.pause(); v.removeAttribute('src'); while (v.firstChild) v.removeChild(v.firstChild); v.load(); v.remove(); });
      _activeConnections = 0; _recentlyEvicted = []; _trickleQueue = []; clearTimeout(_trickleTimer); _trickleTimer = null;
      activeWatchTimers.forEach(t => clearInterval(t)); activeWatchTimers.clear();
      root.remove();
    }
    document.getElementById('smart-search-overlay')?.remove();
  }

  async function mountExplore() {
    if (document.getElementById('explore-root')) return;
    const root = document.createElement('div');
    root.id = 'explore-root';
    root.innerHTML = `
      <div class="explore-header">
        <div class="explore-tabs">
          <button class="explore-tab active" data-tab="creators">Creators</button>
          <button class="explore-tab" data-tab="recommended">Recommended</button>
          <button class="explore-tab" data-tab="tags">Tags</button>
          <button class="explore-tab" data-tab="trending">Trending</button>
          <button class="explore-tab" data-tab="smartsearch">Smart Search</button>
          <button class="explore-tab" data-tab="matched">Curated</button>
          <button class="explore-tab" data-tab="mixer">Mixer</button>
        </div>
        <button class="explore-close">✕ Close</button>
      </div>
      <div class="explore-content">
        <div id="explore-page-creators" class="explore-page active"></div>
        <div id="explore-page-recommended" class="explore-page"></div>
        <div id="explore-page-tags" class="explore-page"></div>
        <div id="explore-page-trending" class="explore-page"></div>
        <div id="explore-page-smartsearch" class="explore-page"></div>
        <div id="explore-page-matched" class="explore-page"></div>
        <div id="explore-page-mixer" class="explore-page"></div>
        <div id="explore-page-player" class="explore-page"></div>
      </div>
    `;
    document.body.appendChild(root);

    root.querySelectorAll('.explore-tab').forEach(tab => {
      tab.addEventListener('click', () => {
        root.querySelectorAll('video').forEach(v => { v.pause(); v.muted = true; });
        activeWatchTimers.forEach(t => clearInterval(t)); activeWatchTimers.clear();
        root.querySelectorAll('.explore-tab').forEach(t => t.classList.remove('active'));
        root.querySelectorAll('.explore-page').forEach(p => p.classList.remove('active'));
        tab.classList.add('active');
        if (tab.dataset.tab !== 'player') lastActiveTab = tab.dataset.tab;
        const page = document.getElementById(`explore-page-${tab.dataset.tab}`);
        if (page) page.classList.add('active');
        if (tab.dataset.tab === 'tags') renderTags();
        else if (tab.dataset.tab === 'recommended') renderRecommended();
        else if (tab.dataset.tab === 'trending') renderTrending();
        else if (tab.dataset.tab === 'mixer') renderMixer();
        else if (tab.dataset.tab === 'smartsearch') {
          const main = document.getElementById('explore-page-smartsearch');
          if (main) {
            const ssContainer = createSmartSearchTab();
            if (!main.contains(ssContainer)) main.appendChild(ssContainer);
          }
        } else if (tab.dataset.tab === 'matched') {
          const main = document.getElementById('explore-page-matched');
          if (main && !main.querySelector('#matched-creators-view')) {
            const matchedContainer = createMatchedCreatorsTab();
            main.appendChild(matchedContainer);
            matchedContainer.classList.remove('hidden');
          }
        }
      });
    });

    root.querySelector('.explore-close').addEventListener('click', () => { window.location.hash = ''; });

    const creators = await getCreators();
    if (creators.length > 0) renderCreators(creators);
    else document.getElementById('explore-page-creators').innerHTML = '<div class="explore-loading">No creators found.</div>';
  }

  function addExploreLink() {
    if (document.querySelector('[data-explore-link]')) return;
    const nav = document.querySelector('header nav') || document.querySelector('nav') || document.querySelector('.header');
    if (!nav) return;
    const link = document.createElement('a');
    link.href = '/' + EXPLORE_HASH; link.textContent = 'Explore'; link.setAttribute('data-explore-link', '1');
    link.style.cssText = 'margin-left:12px;color:#4ade80;text-decoration:none;font-weight:600;';
    link.addEventListener('click', e => { e.preventDefault(); window.location.hash = EXPLORE_HASH; });
    nav.firstChild ? nav.insertBefore(link, nav.firstChild) : nav.appendChild(link);
  }

  if (isExplorePage()) mountExplore();
  else { addExploreLink(); setTimeout(addExploreLink, 1500); }
  window.addEventListener('hashchange', () => { if (isExplorePage()) { if (!document.getElementById('explore-root')) mountExplore(); } else unmountExplore(); });
})();