Coomer Explore

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

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

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

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

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

이 스크립트를 설치하려면 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(); });
})();