Chaturbate Enhancer (compat build)

Lag fixes, safer observers, ES2018 syntax (no #private fields)

// ==UserScript==
// @name         Chaturbate Enhancer (compat build)
// @namespace    http://tampermonkey.net/
// @version      5.2.0
// @description  Lag fixes, safer observers, ES2018 syntax (no #private fields)
// @match        https://chaturbate.com/*
// @match        https://*.chaturbate.com/*
// @run-at       document-end
// @author       shadow
// @grant        none
// ==/UserScript==
console.log("✅ Enhancer userscript loaded");
(function () {
  'use strict';

  /***********************
   * Configuration & Constants
   ***********************/
  const CONFIG = Object.freeze({
    STORAGE: Object.freeze({
      PREFIX: 'chaturbateEnhancer:',
      HIDDEN_KEY: 'hiddenModels',
      TOKENS_KEY: 'tokensSpent',
      SETTINGS_KEY: 'settings',
      BACKUP_KEY: 'backup'
    }),
    TIMERS: Object.freeze({
      TOKEN_MONITOR_RETRY_DELAY: 500,
      TOKEN_MONITOR_MAX_RETRIES: 20,
      CHAT_TIMESTAMP_INTERVAL: 1000,
      PATH_CHECK_INTERVAL: 900,
      MUTATION_DEBOUNCE: 150
    }),
    SELECTORS: Object.freeze({
      ROOM_CARDS: 'li.roomCard.camBgColor',
      TOKEN_BALANCE_SELECTORS: ['span.balance', 'span.token_balance', 'span.tokencount'],
      SCAN_CAMS: '[data-testid="scan-cams"]',
      CHAT_MESSAGES: '[data-testid="chat-message"]',
      CHAT_USERNAME: '[data-testid="chat-message-username"]',
      ROOM_NOTICE: '[data-testid="room-notice-viewport"]'
    }),
    EXTERNAL_LINKS: Object.freeze({
      RECU_ME: 'https://recu.me/performer/',
      CAMWHORES_TV: 'https://www.camwhores.tv/search/'
    })
  });

  /* ======================
     Logger (robust & safe)
     ====================== */
  class Logger {
    constructor(maxErrors = 200) {
      if (Logger._inst) return Logger._inst;
      Logger._inst = this;
      this.errors = [];
      this.maxErrors = maxErrors;
    }
    static getInstance() { return new Logger(); }
    _fmt(msg) { return `[ChaturbateEnhancer ${new Date().toISOString()}] ${msg}`; }
    log(level, message, obj = null) {
      try {
        const formatted = this._fmt(message);
        const extra = obj || '';
        if (level === 'error') console.error(formatted, extra);
        else if (level === 'warn') console.warn(formatted, extra);
        else if (level === 'info') console.info(formatted, extra);
        else console.debug(formatted, extra);

        if (level === 'error' && this.errors.length < this.maxErrors) {
          this.errors.push({ ts: Date.now(), message, extra: obj ? (obj.stack || String(obj)) : null });
        }
      } catch (e) { /* don't break app on logging failure */ }
    }
    error(m, o) { this.log('error', m, o); }
    warn(m, o) { this.log('warn', m, o); }
    info(m, o) { this.log('info', m, o); }
    debug(m, o) { this.log('debug', m, o); }
    getErrorReport() {
      return { errors: this.errors.slice(), userAgent: navigator.userAgent, url: location.href, ts: Date.now() };
    }
  }
  Logger._inst = null;
  const logger = Logger.getInstance();

  /* ======================
     Utils (stable helpers)
     ====================== */
const Utils = {
    getCurrentModelFromPath() {
        try {
            const parts = window.location.pathname.split('/').filter(Boolean);

            // Only consider it a model page if there's exactly one path segment
            // and it doesn't contain common non-model patterns
            if (parts.length !== 1) return null;

            const segment = parts[0];

            // Exclude known non-model pages
            if (segment.includes('-') ||
                segment === 'female' ||
                segment === 'male' ||
                segment === 'couple' ||
                segment === 'trans') {
                return null;
            }

            return segment;
        } catch (e) {
            logger.error('getCurrentModelFromPath failed', e);
            return null;
        }
    },
    safeParseInt(str) {
      try { const s = String(str || '').replace(/[^\d\-]/g, ''); const n = parseInt(s, 10); return Number.isNaN(n) ? 0 : n; }
      catch (e) { logger.error('safeParseInt', e); return 0; }
    },
    safeString(val, fallback = '') {
      try { return String(val || '').trim(); } catch { return fallback; }
    },
    isObject(val) { return val && typeof val === 'object' && !Array.isArray(val); },
    createElement(tag, className = '', attrs = {}) {
      const el = document.createElement(tag);
      if (className) el.className = className;
      for (const k in (attrs || {})) {
        const v = attrs[k];
        if (k === 'textContent' || k === 'innerHTML') el[k] = v;
        else if (k.startsWith('data-') || ['id','href','title','aria-label','role','type','value','style'].includes(k)) el.setAttribute(k, v);
        else el[k] = v;
      }
      return el;
    },
    findElement(selectors = [], ctx = document) {
      for (let i=0;i<selectors.length;i++) {
        try {
          const el = ctx.querySelector(selectors[i]);
          if (el) return el;
        } catch {}
      }
      return null;
    },
    debounce(fn, wait = 100) {
      let t = null;
      return function() {
        const args = arguments, self = this;
        clearTimeout(t);
        t = setTimeout(() => { try { fn.apply(self, args); } catch (err) { logger.error('debounce handler error', err); } }, wait);
      };
    },
    throttle(fn, limit = 200) {
      let last = 0;
      return function() {
        const now = Date.now();
        if (now - last >= limit) {
          last = now;
          try { fn.apply(this, arguments); } catch (err) { logger.error('throttle handler error', err); }
        }
      };
    },
    safeJSONParse(raw, fallback = null) { try { return JSON.parse(raw); } catch { return fallback; } },
    setCookie(name, value, options = {}) {
      try {
        let cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
        if (options.expires) cookie += '; expires=' + new Date(options.expires).toUTCString();
        cookie += '; path=' + (options.path || '/');
        if (options.domain) cookie += '; domain=' + options.domain;
        document.cookie = cookie;
        return true;
      } catch (e) { logger.error('setCookie failed', e); return false; }
    },
    downloadAsFile(content, filename = 'export.json') {
      const blob = new Blob([content], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = Utils.createElement('a', '', { href: url });
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(url);
    }
  };

  /* =========================
     StorageManager (localStorage) — no private fields
     ========================= */
  class StorageManager {
    static _prefix(key) { return CONFIG.STORAGE.PREFIX + key; }

    static migrateOldKeys() {
      try {
        const map = {
          hiddenModels: CONFIG.STORAGE.HIDDEN_KEY,
          tokensSpent: CONFIG.STORAGE.TOKENS_KEY,
          chaturbateHiderSettings: CONFIG.STORAGE.SETTINGS_KEY,
          chaturbate_backup: CONFIG.STORAGE.BACKUP_KEY
        };
        for (const oldKey in map) {
          const newKey = map[oldKey];
          const oldVal = localStorage.getItem(oldKey);
          if (oldVal && !localStorage.getItem(this._prefix(newKey))) {
            localStorage.setItem(this._prefix(newKey), oldVal);
            logger.info('Migrated ' + oldKey + ' → ' + this._prefix(newKey));
          }
          if (oldVal) localStorage.removeItem(oldKey);
        }
      } catch (e) { logger.error('migrateOldKeys failed', e); }
    }

    static _safeGet(key, fallback = null) {
      try {
        const raw = localStorage.getItem(this._prefix(key));
        if (!raw) return fallback;
        return JSON.parse(raw);
      } catch (e) { logger.error('StorageManager.get ' + key + ' parse failed', e); return fallback; }
    }
    static _safeSet(key, value) {
      try { localStorage.setItem(this._prefix(key), JSON.stringify(value)); return true; }
      catch (e) { logger.error('StorageManager.set ' + key + ' failed', e); return false; }
    }

    static getHiddenModels() { return this._safeGet(CONFIG.STORAGE.HIDDEN_KEY, []); }
    static saveHiddenModels(arr) { if (!Array.isArray(arr)) return false; return this._safeSet(CONFIG.STORAGE.HIDDEN_KEY, arr); }

    static getTokensSpent() { return this._safeGet(CONFIG.STORAGE.TOKENS_KEY, {}); }
    static saveTokensSpent(obj) { if (!Utils.isObject(obj)) return false; return this._safeSet(CONFIG.STORAGE.TOKENS_KEY, obj); }

    static getSettings() {
      return this._safeGet(CONFIG.STORAGE.SETTINGS_KEY, {
        hideGenderTabs: true,
        showTimestamps: true,
        autoBackup: false,
        performanceMode: false,
        animateThumbnails: true
      });
    }
    static saveSettings(obj) { return this._safeSet(CONFIG.STORAGE.SETTINGS_KEY, obj); }

    static createBackupObject() {
      try {
        return {
          hiddenModels: this.getHiddenModels(),
          tokensSpent: this.getTokensSpent(),
          settings: this.getSettings(),
          timestamp: Date.now(),
          version: 'patched-compat'
        };
      } catch (e) { logger.error('createBackupObject error', e); return null; }
    }

    static saveBackupToLocal(backupObj) {
      try {
        if (!backupObj) backupObj = this.createBackupObject();
        if (!backupObj) return false;
        return this._safeSet(CONFIG.STORAGE.BACKUP_KEY, backupObj);
      } catch (e) { logger.error('saveBackupToLocal error', e); return false; }
    }

    static loadBackupFromLocal() { return this._safeGet(CONFIG.STORAGE.BACKUP_KEY, null); }

    static resetAll() {
      try {
        const vals = CONFIG.STORAGE;
        for (const k in vals) localStorage.removeItem(this._prefix(vals[k]));
        return true;
      } catch (e) { logger.error('resetAll error', e); return false; }
    }
  }

  /* =========================
     DataManager — Backup / Import
     ========================= */
  class DataManager {
    static exportData(filename = null) {
      try {
        const backup = StorageManager.createBackupObject();
        if (!backup) { NotificationManager.show('Nothing to export', 'error'); return; }
        Utils.downloadAsFile(JSON.stringify(backup, null, 2), filename || `chaturbate_backup_${new Date().toISOString().slice(0,10)}.json`);
        NotificationManager.show('Backup exported', 'success');
      } catch (e) { logger.error('exportData failed', e); NotificationManager.show('Export failed: ' + e.message, 'error'); }
    }

    static async importData(fileOrString, options = { mergeMode: 'replace' }) {
      try {
        let payload = null;
        if (typeof fileOrString === 'string') {
          payload = Utils.safeJSONParse(fileOrString, null);
          if (!payload) throw new Error('Invalid JSON string');
        } else if (fileOrString && fileOrString.text) {
          const t = await fileOrString.text();
          payload = Utils.safeJSONParse(t, null);
          if (!payload) throw new Error('Invalid JSON file');
        } else throw new Error('Unsupported input for importData');

        if (!Utils.isObject(payload)) throw new Error('Payload not object');

        const incomingHidden = Array.isArray(payload.hiddenModels) ? payload.hiddenModels : [];
        const incomingTokens = Utils.isObject(payload.tokensSpent) ? payload.tokensSpent : {};
        const incomingSettings = Utils.isObject(payload.settings) ? payload.settings : {};

        if (options.mergeMode === 'merge') {
          const existingHidden = new Set(StorageManager.getHiddenModels());
          for (let i=0;i<incomingHidden.length;i++) existingHidden.add(incomingHidden[i]);
          StorageManager.saveHiddenModels(Array.from(existingHidden));

          const currentTokens = StorageManager.getTokensSpent();
          for (const k in incomingTokens) {
            const v = Utils.safeParseInt(incomingTokens[k]);
            currentTokens[k] = (currentTokens[k] || 0) + v;
          }
          StorageManager.saveTokensSpent(currentTokens);

          const curSettings = StorageManager.getSettings();
          StorageManager.saveSettings(Object.assign({}, curSettings, incomingSettings));
        } else {
          StorageManager.saveHiddenModels(incomingHidden);
          StorageManager.saveTokensSpent(incomingTokens);
          StorageManager.saveSettings(Object.assign(StorageManager.getSettings(), incomingSettings));
        }

        StorageManager.saveBackupToLocal();
        NotificationManager.show('Data imported successfully', 'success');
        try { localStorage.setItem('chaturbate_enhancer_data_imported', JSON.stringify({ ts: Date.now() })); localStorage.removeItem('chaturbate_enhancer_data_imported'); } catch {}
        return true;
      } catch (e) {
        logger.error('importData error', e);
        NotificationManager.show('Import failed: ' + e.message, 'error');
        return false;
      }
    }
  }

  /* =========================
     NotificationManager (stacked toasts)
     ========================= */
  class NotificationManager {
    static show(message, type = 'info', duration = 3000) {
      try {
        NotificationManager._toasts = NotificationManager._toasts || new Set();
        const el = Utils.createElement('div', 'toast-notification ' + type, { textContent: message });
        el.style.position = 'fixed';
        el.style.top = (20 + NotificationManager._toasts.size * 50) + 'px';
        el.style.right = '20px';
        el.style.zIndex = 10001;
        el.style.padding = '8px 12px';
        el.style.background = 'rgba(0,0,0,0.7)';
        el.style.color = '#fff';
        el.style.borderRadius = '8px';
        el.style.boxShadow = '0 8px 24px rgba(0,0,0,0.35)';
        el.style.transition = 'opacity .25s';
        document.body.appendChild(el);
        NotificationManager._toasts.add(el);
        setTimeout(() => NotificationManager.remove(el), duration);
        return el;
      } catch (e) { logger.error('Notification show failed', e); return null; }
    }
    static remove(el) {
      try {
        if (el && el.parentNode) el.parentNode.removeChild(el);
        if (NotificationManager._toasts) NotificationManager._toasts.delete(el);
        NotificationManager.reposition();
      } catch {}
    }
    static clear() { if (!NotificationManager._toasts) return; for (const t of Array.from(NotificationManager._toasts)) NotificationManager.remove(t); }
    static reposition() {
      if (!NotificationManager._toasts) return;
      Array.from(NotificationManager._toasts).forEach((el, idx) => { el.style.top = (20 + idx * 50) + 'px'; });
    }
  }

  // expose some helpers for debugging
  window.__ChaturbateEnhancer = window.__ChaturbateEnhancer || {};
  Object.assign(window.__ChaturbateEnhancer, { Logger, Utils, StorageManager, DataManager });
  /* =========================
     PerformanceMonitor
     ========================= */
  class PerformanceMonitor {
    static start(label) { PerformanceMonitor.timers = PerformanceMonitor.timers || new Map(); PerformanceMonitor.timers.set(label, performance.now()); }
    static end(label) {
      PerformanceMonitor.timers = PerformanceMonitor.timers || new Map();
      const start = PerformanceMonitor.timers.get(label);
      if (!start) return 0;
      const dur = performance.now() - start;
      PerformanceMonitor.timers.delete(label);
      if (dur > 12) logger.debug(label + ' took ' + dur.toFixed(2) + 'ms');
      return dur;
    }
  }

  /* =========================
     ThumbnailManager (animated previews) — no private fields
     ========================= */
/* =========================
   ThumbnailManager (animated previews) — fixed
   ========================= */
class ThumbnailManager {
  static init() {
    try {
      const settings = StorageManager.getSettings();
      if (!('animateThumbnails' in settings)) {
        settings.animateThumbnails = true;
        StorageManager.saveSettings(settings);
      }
      ThumbnailManager.setupListeners();
      ThumbnailManager.observeDomCleanup();
    } catch (e) { logger.error('ThumbnailManager.init failed', e); }
  }

  static setupListeners() {
    const onEnter = Utils.debounce(function(ev) {
      try {
        const img = ev.target;
        if (!(img instanceof Element)) return;
        if (!img.matches('.room_list_room img, .roomElement img, .roomCard img')) return;
        if (!StorageManager.getSettings().animateThumbnails) return;
        ThumbnailManager.clearIntervalFor(img);
        ThumbnailManager.updateRoomThumb(img);
        const perf = StorageManager.getSettings().performanceMode;
        const interval = perf ? 400 : 150;
        const id = setInterval(() => ThumbnailManager.updateRoomThumb(img), interval);
        ThumbnailManager._hoverIntervals.set(img, id);
      } catch (e) { logger.error('thumb mouseenter', e); }
    }, 60);

    const onLeave = function(ev) {
      try {
        const img = ev.target;
        if (!(img instanceof Element)) return;
        if (!img.matches('.room_list_room img, .roomElement img, .roomCard img')) return;
        ThumbnailManager.clearIntervalFor(img);
      } catch (e) { logger.error('thumb mouseleave', e); }
    };

    document.addEventListener('mouseenter', onEnter, true);
    document.addEventListener('mouseleave', onLeave, true);
  }

  static observeDomCleanup() {
    if (ThumbnailManager._cleanupObserver) ThumbnailManager._cleanupObserver.disconnect();
    ThumbnailManager._cleanupObserver = new MutationObserver(function(muts) {
      for (let i=0;i<muts.length;i++) {
        const removed = muts[i].removedNodes;
        if (!removed) continue;
        for (let j=0;j<removed.length;j++) {
          const n = removed[j];
          if (n && n.querySelectorAll) {
            const imgs = n.querySelectorAll('img');
            for (let k=0;k<imgs.length;k++) ThumbnailManager.clearIntervalFor(imgs[k]);
          }
        }
      }
    });
    ThumbnailManager._cleanupObserver.observe(document.body, { childList: true, subtree: true });
  }

  static clearIntervalFor(img) {
    const id = ThumbnailManager._hoverIntervals.get(img);
    if (id) {
      clearInterval(id);
      ThumbnailManager._hoverIntervals.delete(img);
    }
  }

  static async updateRoomThumb(img) {
    try {
      const parent = img.parentElement;
      let username = null;
      if (parent) {
        username = (parent.dataset && parent.dataset.room) || null;
        if (!username) {
          const card = parent.closest ? parent.closest('li.roomCard') : null;
          username = ModelManager.extractUsername(card);
        }
      }
      if (!username) return;

      const now = Date.now();
      const perf = StorageManager.getSettings().performanceMode;
      const minGap = perf ? 260 : 120;
      const last = ThumbnailManager._lastReqTime.get(username) || 0;
      if (now - last < minGap) return;
      ThumbnailManager._lastReqTime.set(username, now);

      const url = 'https://thumb.live.mmcdn.com/minifwap/' + encodeURIComponent(username) + '.jpg?' + Math.random();
      const controller = new AbortController();
      const timeout = setTimeout(() => { try { controller.abort(); } catch {} }, perf ? 1500 : 2500);
      const resp = await fetch(url, { cache: 'no-cache', signal: controller.signal });
      clearTimeout(timeout);
      if (!resp || !resp.ok) return;
      const blob = await resp.blob();
      const prev = img.getAttribute('data-__thumb-obj-url');
      if (prev) { try { URL.revokeObjectURL(prev); } catch {} }
      const objUrl = URL.createObjectURL(blob);
      img.src = objUrl;
      img.setAttribute('data-__thumb-obj-url', objUrl);
    } catch (e) { if (!e || e.name !== 'AbortError') logger.error('updateRoomThumb error', e); }
  }

  static stopAll() {
    for (const [img, id] of ThumbnailManager._hoverIntervals.entries()) {
      clearInterval(id);
    }
    ThumbnailManager._hoverIntervals.clear();

    const imgs = document.querySelectorAll('img[data-__thumb-obj-url]');
    for (let i=0;i<imgs.length;i++) {
      const img = imgs[i];
      const u = img.getAttribute('data-__thumb-obj-url');
      if (u) { try { URL.revokeObjectURL(u); } catch {} }
      img.removeAttribute('data-__thumb-obj-url');
    }
    if (ThumbnailManager._cleanupObserver) {
      ThumbnailManager._cleanupObserver.disconnect();
      ThumbnailManager._cleanupObserver = null;
    }
  }
}
ThumbnailManager._hoverIntervals = new Map();   // ✅ use Map, not WeakMap
ThumbnailManager._lastReqTime = new Map();
ThumbnailManager._cleanupObserver = null;


  /* =========================
     ModelManager
     ========================= */
  class ModelManager {
    static extractUsername(card) {
      if (!card) return null;
      try {
        const slug = card.querySelector('[data-slug]');
        if (slug) return slug.getAttribute('data-slug') || null;
        const a = card.querySelector('a[href*="/"]');
        if (a) {
          const href = a.getAttribute('href') || '';
          const parts = href.split('/').filter(Boolean);
          return parts[0] || null;
        }
      } catch (e) { logger.error('extractUsername failed', e); }
      return null;
    }
  }

  /* =========================
     ModalManager (no private)
     ========================= */
  class ModalManager {
    static createModal(title, bodyElement, buttons = []) {
      if (ModalManager._active) ModalManager.closeModal();

      const overlay = Utils.createElement('div', 'chaturbate-hider-overlay');
      overlay.setAttribute('role', 'dialog');
      overlay.setAttribute('aria-modal', 'true');
      const modal = Utils.createElement('div', 'chaturbate-hider-modal', { tabindex: '-1' });

      const header = Utils.createElement('div', 'chaturbate-hider-header');
      const h3 = Utils.createElement('h3', '', { textContent: title, id: 'ce-modal-title' });
      const closeBtn = Utils.createElement('button', 'chaturbate-hider-close', { type: 'button', 'aria-label': 'Close' });
      closeBtn.innerHTML = '&times;';
      header.appendChild(h3); header.appendChild(closeBtn);

      const body = Utils.createElement('div', 'chaturbate-hider-body');
      body.appendChild(bodyElement);

      const footer = Utils.createElement('div', 'chaturbate-hider-footer');
      for (let i=0;i<buttons.length;i++) {
        const cfg = buttons[i] || {};
        const b = Utils.createElement('button', 'chaturbate-hider-btn ' + (cfg.class || ''), { type: 'button', textContent: cfg.text || 'OK' });
        if (cfg.ariaLabel) b.setAttribute('aria-label', cfg.ariaLabel);
        if (cfg.onClick) b.addEventListener('click', function(e){ e.preventDefault(); try { cfg.onClick(); } catch (err) { logger.error('modal btn cb', err); } });
        footer.appendChild(b);
      }

      modal.appendChild(header);
      modal.appendChild(body);
      modal.appendChild(footer);
      overlay.appendChild(modal);
      document.body.appendChild(overlay);

      const escHandler = function(e) { if (e.key === 'Escape') ModalManager.closeModal(); };
      const overlayClick = function(e) { if (e.target === overlay) ModalManager.closeModal(); };
      closeBtn.addEventListener('click', function(){ ModalManager.closeModal(); });
      document.addEventListener('keydown', escHandler);
      overlay.addEventListener('click', overlayClick);

      const focusable = function() { return overlay.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); };
      const keydownTrap = function(e) {
        if (e.key !== 'Tab') return;
        const f = Array.from(focusable());
        if (!f.length) return;
        const first = f[0], last = f[f.length - 1];
        if (e.shiftKey && document.activeElement === first) { last.focus(); e.preventDefault(); }
        else if (!e.shiftKey && document.activeElement === last) { first.focus(); e.preventDefault(); }
      };
      document.addEventListener('keydown', keydownTrap);

      ModalManager._active = { overlay, modal, escHandler, overlayClick, keydownTrap };
      requestAnimationFrame(function(){ overlay.classList.add('visible'); });
      setTimeout(function(){ try { modal.focus(); } catch {} }, 80);
      return overlay;
    }

    static closeModal() {
      if (!ModalManager._active) return;
      const st = ModalManager._active;
      try {
        document.removeEventListener('keydown', st.escHandler);
        document.removeEventListener('keydown', st.keydownTrap);
        st.overlay.removeEventListener('click', st.overlayClick);
        st.overlay.classList.remove('visible');
        setTimeout(function(){ if (st.overlay.parentNode) st.overlay.parentNode.removeChild(st.overlay); }, 200);
      } catch (e) { logger.error('closeModal error', e); }
      ModalManager._active = null;
    }
  }
  ModalManager._active = null;

  /* =========================
     SettingsModal
     ========================= */
  class SettingsModal {
    static _makeToggle(settings, key, label) {
      const wrap = Utils.createElement('label', 'enhancer-toggle');
      const span = Utils.createElement('span', '', { textContent: label });
      const input = Utils.createElement('input', '', { type: 'checkbox' });
      input.checked = !!settings[key];
      input.addEventListener('change', function() {
        settings[key] = input.checked;
        StorageManager.saveSettings(settings);
        NotificationManager.show(label + ' ' + (input.checked ? 'enabled' : 'disabled'), 'info');
      });
      wrap.appendChild(span); wrap.appendChild(input);
      return wrap;
    }

    static _makeActionsRow() {
      const wrap = Utils.createElement('div', 'settings-actions');
      const exportBtn = Utils.createElement('button', 'primary', { textContent: 'Export Backup', type: 'button' });
      const importBtn = Utils.createElement('button', 'secondary', { textContent: 'Import Backup', type: 'button' });

      exportBtn.addEventListener('click', function(){ DataManager.exportData(); });
      importBtn.addEventListener('click', function(){ SettingsModal.showImportDialog(); });

      wrap.appendChild(exportBtn); wrap.appendChild(importBtn);
      return wrap;
    }

    static showImportDialog() {
      try {
        const container = Utils.createElement('div', '', { style: 'display:flex;flex-direction:column;gap:12px;padding:6px 0' });

        const fileInput = Utils.createElement('input', '', { type: 'file', accept: 'application/json' });
        const radioWrap = Utils.createElement('div', '', { style: 'display:flex;gap:8px;align-items:center' });
        radioWrap.appendChild(Utils.createElement('label', '', { innerHTML: '<input type="radio" name="importMode" value="replace" checked /> Replace existing' }));
        radioWrap.appendChild(Utils.createElement('label', '', { innerHTML: '<input type="radio" name="importMode" value="merge" /> Merge (recommended)' }));

        const hint = Utils.createElement('div', '', {
          textContent: 'Choose a backup file (.json). Merge adds hidden models and sums token counts; Replace overwrites.',
          style:'font-size:12px;color:var(--ch-text-muted)'
        });

        const btnRow = Utils.createElement('div', '', { style: 'display:flex;gap:8px;justify-content:flex-end' });
        const importBtn = Utils.createElement('button', 'chaturbate-hider-btn primary', { textContent: 'Import', type: 'button' });
        const cancelBtn = Utils.createElement('button', 'chaturbate-hider-btn secondary', { textContent: 'Cancel', type: 'button' });
        btnRow.appendChild(cancelBtn); btnRow.appendChild(importBtn);

        container.appendChild(fileInput);
        container.appendChild(radioWrap);
        container.appendChild(hint);
        container.appendChild(btnRow);

        ModalManager.createModal('Import Backup', container, []);

        cancelBtn.addEventListener('click', function(){ ModalManager.closeModal(); });

        importBtn.addEventListener('click', async function() {
          const f = fileInput.files && fileInput.files[0];
          if (!f) { NotificationManager.show('Please select a file', 'error'); return; }
          const modeEl = document.querySelector('input[name="importMode"]:checked');
          const mode = (modeEl && modeEl.value) || 'replace';
          importBtn.disabled = true;
          const ok = await DataManager.importData(f, { mergeMode: mode });
          importBtn.disabled = false;
          if (ok) {
            ModalManager.closeModal();
            setTimeout(function(){ window.location.reload(); }, 600);
          }
        });
      } catch (e) { logger.error('showImportDialog error', e); NotificationManager.show('Failed to open import dialog', 'error'); }
    }

    static show() {
      try {
        const settings = StorageManager.getSettings();
        const container = Utils.createElement('div', 'enhancer-settings-group');

        const toggles = [
          ['hideGenderTabs', 'Hide gender tabs'],
          ['showTimestamps', 'Show chat timestamps'],
          ['animateThumbnails', 'Animate thumbnails on hover'],
          ['autoBackup', 'Auto-backup data daily'],
          ['performanceMode', 'Performance mode (less animations/requests)']
        ];

        for (let i=0;i<toggles.length;i++) {
          const [key, label] = toggles[i];
          container.appendChild(SettingsModal._makeToggle(settings, key, label));
        }

        container.appendChild(SettingsModal._makeActionsRow());

        const tokenStatsBtn = Utils.createElement('button', 'chaturbate-hider-btn', {
          textContent: 'Show Token Stats',
          type: 'button',
          style: 'margin-top:10px;'
        });
        tokenStatsBtn.addEventListener('click', function(){ StatsManager.showTokenStats(); });
        container.appendChild(tokenStatsBtn);

        ModalManager.createModal('Enhancer Settings', container, [
          { text: 'Close', class: 'secondary', onClick: function(){ ModalManager.closeModal(); } }
        ]);
      } catch (e) { logger.error('SettingsModal.show failed', e); }
    }
  }

  /* =========================
     ButtonManager (hide models)
     ========================= */
  class ButtonManager {
    static addHideButtons() {
      try {
        ButtonManager._processed = ButtonManager._processed || new WeakSet();
        const cards = document.querySelectorAll(CONFIG.SELECTORS.ROOM_CARDS);
        if (!cards.length) return;

        const hidden = new Set(StorageManager.getHiddenModels());
        let added = 0, hiddenCount = 0;

        for (let i=0;i<cards.length;i++) {
          const card = cards[i];
          if (ButtonManager._processed.has(card)) continue;
          ButtonManager._processed.add(card);

          const username = ModelManager.extractUsername(card);
          if (!username) continue;

          if (hidden.has(username)) {
            card.style.display = 'none';
            card.setAttribute('data-hidden','true');
            hiddenCount++;
            continue;
          }

          const btn = ButtonManager.createHideButton(card, username);
          if (getComputedStyle(card).position === 'static') card.style.position = 'relative';
          card.appendChild(btn);
          added++;
        }

        if (added > 0 || hiddenCount > 0) StatsManager.updateHiddenModelsStat();
      } catch (e) { logger.error('addHideButtons failed', e); }
    }

    static createHideButton(card, username) {
      const button = Utils.createElement('button', 'hide-model-button', {
        'aria-label': 'Hide ' + username,
        title: 'Hide ' + username,
        textContent: '✕',
        type: 'button'
      });

      button.addEventListener('click', function(ev) {
        ev.stopPropagation();
        ev.preventDefault();
        card.style.display = 'none';
        card.setAttribute('data-hidden','true');
        const hidden = StorageManager.getHiddenModels();
        if (hidden.indexOf(username) === -1) {
          hidden.push(username);
          StorageManager.saveHiddenModels(hidden);
          StatsManager.updateHiddenModelsStat();
          NotificationManager.show('Hidden: ' + username, 'success');
        }
      });

      return button;
    }

    static clearProcessedCards() { ButtonManager._processed = new WeakSet(); }
  }
  ButtonManager._processed = new WeakSet();

/* =========================
   ChatTimestampManager — contrast-aware + notices
   ========================= */
class ChatTimestampManager {
  constructor() {
    this.observer = null;
    this.processed = new WeakSet();
    this.settings = StorageManager.getSettings();
    this.maxProcessed = 5000;
    this.counter = 0;
  }

  start() {
    if (!this.settings.showTimestamps) return;
    this.addTimestamps();
    this.setupMonitoring();
  }

  /* ---------- core ---------- */
  addTimestamps() {
    try {
      this.processChatMessages();
      this.processStandaloneNotices();
    } catch (e) { logger.error('addTimestamps failed', e); }
  }

processChatMessages() {
  const messages = document.querySelectorAll(CONFIG.SELECTORS.CHAT_MESSAGES);
  for (let i = 0; i < messages.length; i++) {
    const msg = messages[i];
    if (this.processed.has(msg)) continue;

    // ✅ Skip notices (handled separately)
    if (msg.querySelector('div[data-testid="room-notice"]')) {
      this.markProcessed(msg);
      continue;
    }

    try {
      const tsAttr = msg.getAttribute('data-ts');
      if (!tsAttr) { this.markProcessed(msg); continue; }
      const tsNum = parseInt(tsAttr, 10);
      if (isNaN(tsNum)) { this.markProcessed(msg); continue; }

      const opts = this.settings.performanceMode
        ? { hour: '2-digit', minute: '2-digit' }
        : { hour: '2-digit', minute: '2-digit', second: '2-digit' };

      const timeString = new Date(tsNum).toLocaleTimeString([], opts);
      this.addTimestampToMessage(msg, timeString);
      this.markProcessed(msg);
    } catch (e) {
      logger.error('processChatMessages error', e);
      this.markProcessed(msg);
    }
  }
}


processStandaloneNotices() {
  const notices = document.querySelectorAll('div[data-testid="room-notice"]');
  for (let i = 0; i < notices.length; i++) {
    const notice = notices[i];
    if (this.processed.has(notice)) continue;

    // Find the innermost span/text container
    let target = notice.querySelector('[data-testid="room-notice-viewport"] span') ||
                 notice.querySelector('span') ||
                 notice;

    if (target.querySelector('.chat-timestamp')) {
      this.markProcessed(notice);
      continue;
    }

    const opts = this.settings.performanceMode
      ? { hour: '2-digit', minute: '2-digit' }
      : { hour: '2-digit', minute: '2-digit', second: '2-digit' };

    const ts = new Date().toLocaleTimeString([], opts);
    const color = this.getTimestampColor(notice);

    const sp = Utils.createElement('span', 'chat-timestamp', {
      textContent: `[${ts}] `,
      style: `color:${color}; font-size:11px; margin-right:4px; font-weight:normal; display:inline;`
    });

    // ✅ Insert timestamp at the start of the notice bubble text
    target.insertBefore(sp, target.firstChild);

    this.markProcessed(notice);
  }
}


  addTimestampToMessage(message, timeString) {
    if (message.querySelector('.chat-timestamp')) return;

    // Prefer username label (works for normal rows and tip bubbles);
    // fall back to legacy selector or the message itself
    const usernameEl =
      message.querySelector('[data-testid="username-label"]') ||
      message.querySelector(CONFIG.SELECTORS.CHAT_USERNAME) ||
      message;

    // Use the colored bubble inside the message (if any) to decide contrast
    const colorBase = this.getColorBaseFor(message, null);
    const color = this.getTimestampColor(colorBase);

const sp = Utils.createElement('span', 'chat-timestamp', {
  textContent: `[${timeString}] `,
  style: `color:${color}; font-size:11px; margin-right:4px; font-weight:normal; display:inline;`
});

    usernameEl.insertBefore(sp, usernameEl.firstChild);
  }

  /* ---------- color helpers ---------- */

  // Choose the element whose styles define contrast: prefer the colored notice bubble
  getColorBaseFor(message, notice) {
    return (message && message.querySelector('div[data-testid="room-notice"]')) ||
           notice ||
           message ||
           document.body;
  }

getTimestampColor(el) {
  try {
    const cs = getComputedStyle(el);
    const bg = cs.backgroundColor || '';
    const fg = cs.color || '';

    // Only override color for notice/tip bubbles
    if (el.classList.contains('roomNotice') || el.closest('.roomNotice')) {
      if (bg.includes('255, 255, 51') || bg.includes('255, 139, 69') || fg === 'rgb(0, 0, 0)') {
        return 'rgba(0,0,0,0.75)'; // dark for yellow/orange
      }
    }

    return 'rgba(255,255,255,0.85)'; // default light
  } catch {
    return 'rgba(255,255,255,0.85)';
  }
}

  /* ---------- observer & book-keeping ---------- */
  setupMonitoring() {
    const self = this;
    setTimeout(function () {
      const chatContainer = Utils.findElement(['#chat-messages', '.chat-messages']);
      if (chatContainer) {
        self.observer = new MutationObserver(
          Utils.debounce(function () { self.addTimestamps(); }, CONFIG.TIMERS.MUTATION_DEBOUNCE)
        );
        self.observer.observe(chatContainer, { childList: true, subtree: true });
      }
    }, 500);
  }

  markProcessed(node) {
    this.processed.add(node);
    this.counter++;
    if (this.counter > this.maxProcessed) {
      this.processed = new WeakSet();
      this.counter = 0;
    }
  }

  stop() {
    if (this.observer) this.observer.disconnect();
    this.observer = null;
    this.processed = new WeakSet();
  }
}


  /* =========================
     TabManager (hide gender tabs)
     ========================= */
  class TabManager {
    static hideGenderTabs() {
      try {
        if (!StorageManager.getSettings().hideGenderTabs) return;
        const selectors = [
          'a.gender-tab[href*="/trans-cams/"]',
          'a[href*="/male-cams"]',
          'a[href*="/trans-cams"]'
        ];
        const els = document.querySelectorAll(selectors.join(','));
        for (let i=0;i<els.length;i++) els[i].style.display = 'none';
      } catch (e) { logger.error('hideGenderTabs failed', e); }
    }
  }

  /* =========================
     StatsManager
     ========================= */
  class StatsManager {
    static updateHiddenModelsStat() {
      try {
        const merch = Utils.findElement(['li a#merch', 'a#merch']);
        const merchLi = merch && merch.closest ? merch.closest('li') : null;
        let statLi = document.querySelector('#hidden-models-stat-li');
        if (!statLi) {
          statLi = Utils.createElement('li', '', { id: 'hidden-models-stat-li' });
          const parent = (merchLi && merchLi.parentNode) || Utils.findElement(['ul.top-nav', 'ul.main-nav', 'nav ul']);
          if (parent) {
            if (merchLi) merchLi.insertAdjacentElement('afterend', statLi);
            else parent.appendChild(statLi);
          }
        }

        let statA = document.querySelector('#hidden-models-stat');
        if (!statA) {
          statA = Utils.createElement('a', '', {
            id: 'hidden-models-stat',
            href: 'javascript:void(0);',
            style: 'color:#fff;margin-right:12px;'
          });
          statA.addEventListener('click', function(e) {
            e.preventDefault();
            ModalManager.createModal('Hidden Models', StatsManager.createHiddenList(), [
              { text: 'Close', class: 'secondary', onClick: function(){ ModalManager.closeModal(); } }
            ]);
          });
          statLi.appendChild(statA);

          const settingsBtn = Utils.createElement('a', '', {
            id: 'enhancer-settings-btn',
            href: 'javascript:void(0);',
            style: 'color:#fff;font-weight:600;margin-left:10px;'
          });
          settingsBtn.textContent = '⚙ Settings';
          settingsBtn.addEventListener('click', function(e){ e.preventDefault(); SettingsModal.show(); });
          statLi.appendChild(settingsBtn);
        }

        const stats = StatsManager.calculateHiddenStats();
        statA.textContent = 'Hidden Models: ' + stats.currentHidden + '/' + stats.totalHidden;
      } catch (e) { logger.error('updateHiddenModelsStat failed', e); }
    }

    static calculateHiddenStats() {
      const hidden = StorageManager.getHiddenModels();
      const total = hidden.length;
      const set = new Set(hidden);
      const cards = document.querySelectorAll(CONFIG.SELECTORS.ROOM_CARDS);
      let currentHidden = 0;
      for (let i=0;i<cards.length;i++) {
        const c = cards[i];
        const u = ModelManager.extractUsername(c);
        if (u && set.has(u) && (c.style.display === 'none' || c.getAttribute('data-hidden') === 'true')) currentHidden++;
      }
      return { totalHidden: total, currentHidden };
    }

    static createHiddenList() {
      const container = Utils.createElement('div', '', { style: 'padding:10px;' });
      const ul = Utils.createElement('ul', 'hidden-models-list');
      const list = StorageManager.getHiddenModels();
      for (let i=0;i<list.length;i++) {
        const name = list[i];
        const li = Utils.createElement('li', 'hidden-models-item');
        li.appendChild(Utils.createElement('span', '', { textContent: name }));
        const btn = Utils.createElement('button', 'chaturbate-hider-btn danger', { textContent: 'Unhide', type: 'button' });
        btn.addEventListener('click', function() {
          const filtered = StorageManager.getHiddenModels().filter(function(n){ return n !== name; });
          StorageManager.saveHiddenModels(filtered);
          NotificationManager.show(name + ' unhidden', 'success');
          setTimeout(function(){ window.location.reload(); }, 500);
        });
        li.appendChild(btn);
        ul.appendChild(li);
      }
      container.appendChild(ul);
      return container;
    }

    static showTokenStats() {
      try {
        const tokens = StorageManager.getTokensSpent();
        const entries = [];
        for (const k in tokens) if (tokens[k] > 0) entries.push([k, tokens[k]]);
        if (!entries.length) { NotificationManager.show('No token data available', 'info'); return; }
        entries.sort(function(a,b){ return b[1]-a[1]; });
        const total = entries.reduce(function(sum, e){ return sum + e[1]; }, 0);

        const container = Utils.createElement('div', 'token-stats-container');

        const table = Utils.createElement('table', 'token-stats-table');
        const thead = Utils.createElement('thead');
        thead.innerHTML = '<tr><th>Model</th><th>Tokens Spent</th></tr>';
        table.appendChild(thead);

        const tbody = Utils.createElement('tbody');
        for (let i=0;i<entries.length;i++) {
          const tr = Utils.createElement('tr');
          tr.innerHTML = '<td>' + entries[i][0] + '</td><td>' + entries[i][1].toLocaleString() + '</td>';
          tbody.appendChild(tr);
        }
        const totalTr = Utils.createElement('tr', 'total-row');
        totalTr.innerHTML = '<td><strong>Total</strong></td><td><strong>' + total.toLocaleString() + '</strong></td>';
        tbody.appendChild(totalTr);

        table.appendChild(tbody);
        container.appendChild(table);

        ModalManager.createModal('Token Statistics', container, [
          { text: 'Close', class: 'secondary', onClick: function(){ ModalManager.closeModal(); } }
        ]);
      } catch (e) { logger.error('showTokenStats failed', e); }
    }
  }
  /* =========================
     TokenMonitor (track tokens spent)
     ========================= */
  class TokenMonitor {
    constructor() {
      this.observer = null;
      this.lastTokenCount = null;
      this.isActive = false;
      this.tokenCountSpan = null;
      this.currentModel = null;
    }

    async start() {
      if (this.isActive) return;
      this.isActive = true;

      const tokenCountSpan = Utils.findElement(CONFIG.SELECTORS.TOKEN_BALANCE_SELECTORS);
      const scanCamsSpan = document.querySelector(CONFIG.SELECTORS.SCAN_CAMS);
      if (!tokenCountSpan || !scanCamsSpan) return;

      const currentModel = Utils.getCurrentModelFromPath();
      if (!currentModel) return;

      this.tokenCountSpan = tokenCountSpan;
      this.currentModel = currentModel;

      this.setupTokenTracking(currentModel, tokenCountSpan);
      this.createTokenInterface(currentModel, scanCamsSpan);
    }

    setupTokenTracking(currentModel, tokenCountSpan) {
      const tokens = StorageManager.getTokensSpent();
      if (!(currentModel in tokens)) { tokens[currentModel] = 0; StorageManager.saveTokensSpent(tokens); }
      this.lastTokenCount = Utils.safeParseInt(tokenCountSpan.textContent);

      if (this.observer) this.observer.disconnect();

      const handleChange = (currentBalance) => {
        if (currentBalance < this.lastTokenCount) {
          const spent = this.lastTokenCount - currentBalance;
          const data = StorageManager.getTokensSpent();
          data[currentModel] = (data[currentModel] || 0) + spent;
          StorageManager.saveTokensSpent(data);
          this.updateTokenDisplay(currentModel, data[currentModel]);
        }
        this.lastTokenCount = currentBalance;
      };

      this.observer = new MutationObserver(() => {
        if (!this.isActive) return;
        const currentTokenCount = Utils.safeParseInt(tokenCountSpan.textContent);
        handleChange(currentTokenCount);
      });

      this.observer.observe(tokenCountSpan, { childList: true, characterData: true, subtree: true });
    }

    createTokenInterface(currentModel, scanCamsSpan) {
      let container = document.querySelector('#tokens-bar-container');
      if (container) { this.updateTokenDisplay(currentModel); return; }

      container = Utils.createElement('div', 'tokens-bar-container', { id: 'tokens-bar-container' });

      const left = Utils.createElement('div', 'tokens-bar-left');
      const spentDiv = Utils.createElement('span', 'tokens-spent-stat', { id: 'tokens-spent-stat' });
      left.appendChild(spentDiv);

      const right = Utils.createElement('div', 'tokens-bar-right');
      const recu = this.makeLinkBtn('RecuMe', 'R', CONFIG.EXTERNAL_LINKS.RECU_ME + encodeURIComponent(currentModel));
      const cw = this.makeLinkBtn('CamWhoresTV', 'CW', CONFIG.EXTERNAL_LINKS.CAMWHORES_TV + encodeURIComponent(currentModel) + '/');
      right.appendChild(recu); right.appendChild(cw);

      container.appendChild(left); container.appendChild(right);

      const scanLink = scanCamsSpan.closest ? scanCamsSpan.closest('a') : null;
      if (scanLink && scanLink.parentElement) {
        scanLink.parentElement.insertBefore(container, scanLink);
        container.style.display = 'inline-flex';
        container.style.margin = '0 8px 0 0';
        container.style.verticalAlign = 'middle';
      } else {
        document.body.prepend(container);
      }

      this.updateTokenDisplay(currentModel);
    }

    makeLinkBtn(title, text, url) {
      const btn = Utils.createElement('button', 'token-action-btn', {
        type: 'button',
        title: title,
        'aria-label': title,
        textContent: text
      });
      btn.addEventListener('click', function() {
        try { window.open(url, '_blank', 'noopener,noreferrer'); }
        catch (e) { logger.error('open ' + title + ' fail', e); NotificationManager.show('Failed to open ' + title, 'error'); }
      });
      return btn;
    }

    updateTokenDisplay(currentModel, tokenCount = null) {
      const el = document.querySelector('#tokens-spent-stat');
      if (!el) return;
      if (tokenCount === null) {
        const tokens = StorageManager.getTokensSpent();
        tokenCount = tokens[currentModel] || 0;
      }
      el.textContent = 'Tokens spent on ' + currentModel + ': ' + tokenCount.toLocaleString();
    }

    stop() {
      this.isActive = false;
      this.currentModel = null;
      if (this.observer) { this.observer.disconnect(); this.observer = null; }
      this.tokenCountSpan = null;
    }
  }

  /* =========================
     Main Controller
     ========================= */
  class ChaturbateEnhancer {
    constructor() {
      this.tokenMonitor = null;
      this.chatTimestamps = null;
      this.lastPath = location.pathname;
      this.mutationObserver = null;
      this.pathCheckInterval = null;
      this.isInitialized = false;
    }

    async init() {
      if (this.isInitialized) return;
      try {
        StorageManager.migrateOldKeys();
        this.injectStyles();
        await this.runInitialSetup();
        this.observeDynamicChanges();
        this.setupPathMonitoring();
        this.exposeGlobals();
        this.isInitialized = true;
      } catch (e) { logger.error('Enhancer init failed', e); }
    }

async runInitialSetup() {
  try {
    if (this.tokenMonitor) this.tokenMonitor.stop();
    this.tokenMonitor = new TokenMonitor();
    await this.tokenMonitor.start();

    if (this.chatTimestamps) this.chatTimestamps.stop();
    this.chatTimestamps = new ChatTimestampManager();
    this.chatTimestamps.start();

    const isModelPage = !!Utils.getCurrentModelFromPath();
    if (!isModelPage) {
      ThumbnailManager.init();
    } else {
      ThumbnailManager.stopAll();   // ✅ disable previews inside model pages
    }

    ButtonManager.addHideButtons();
    TabManager.hideGenderTabs();
    StatsManager.updateHiddenModelsStat();
  } catch (e) { logger.error('runInitialSetup error', e); }
}

    observeDynamicChanges() {
      const debounced = Utils.debounce(() => this.runInitialSetup(), CONFIG.TIMERS.MUTATION_DEBOUNCE);

      const targets = [
        document.querySelector('#room_list, .room_list'),
        document.querySelector('#main, .main-content')
      ].filter(function(n){ return !!n; });

      this.mutationObserver = new MutationObserver(function(mutations) {
        for (let i=0;i<mutations.length;i++) {
          const mut = mutations[i];
          if ((mut.addedNodes && mut.addedNodes.length) || (mut.removedNodes && mut.removedNodes.length)) {
            debounced();
            break;
          }
        }
      });

      for (let i=0;i<targets.length;i++) this.mutationObserver.observe(targets[i], { childList: true, subtree: true });
    }

    setupPathMonitoring() {
      const self = this;
      this.pathCheckInterval = setInterval(function() {
        if (location.pathname !== self.lastPath) self.handlePathChange();
      }, CONFIG.TIMERS.PATH_CHECK_INTERVAL);
    }

    async handlePathChange() {
      this.lastPath = location.pathname;

      if (this.tokenMonitor) this.tokenMonitor.stop();
      if (this.chatTimestamps) this.chatTimestamps.stop();
      ThumbnailManager.stopAll();
      ButtonManager.clearProcessedCards();

      setTimeout(() => this.runInitialSetup(), 500);
    }

    exposeGlobals() {
      window.showChaturbateStats = function(){ SettingsModal.show(); };
      window.exportChaturbateData = function(){ DataManager.exportData(); };
      window.importChaturbateData = function(){ SettingsModal.showImportDialog(); };
      window.resetChaturbateData = function(){
        if (confirm('Reset all hidden models and token data?')) {
          StorageManager.resetAll();
          NotificationManager.show('Data reset', 'success');
          setTimeout(function(){ location.reload(); }, 600);
        }
      };
    }

    injectStyles() {
      const style = document.createElement('style');
      style.textContent = `
      /* Modal + buttons + lists */
      .chaturbate-hider-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display:flex; align-items:flex-start; justify-content:center; padding:20px; overflow:auto; z-index:10000; opacity:0; transition:opacity .25s; }
      .chaturbate-hider-overlay.visible { opacity:1; }
      .chaturbate-hider-modal { background:#1f2937; color:#e5e7eb; border-radius:12px; width:min(95vw,760px); max-height:calc(100vh - 40px); overflow:hidden; display:flex; flex-direction:column; transform:translateY(8px); transition:transform .2s,opacity .2s; }
      .chaturbate-hider-header { display:flex; justify-content:space-between; align-items:center; padding:8px 12px; }
      .chaturbate-hider-body { flex:1 1 auto; overflow-y:auto; padding:14px 18px; font-family: system-ui, sans-serif; font-size:14px; line-height:1.5; }
      .chaturbate-hider-footer { padding:10px 16px; border-top:1px solid rgba(255,255,255,0.08); position:sticky; bottom:0; background:rgba(0,0,0,0.25); }
      #enhancer-settings-btn { transition: color .2s; } #enhancer-settings-btn:hover { color: #3b82f6; }

      .hidden-models-list { list-style:none; padding:0; margin:0; }
      .hidden-models-item { display:flex; justify-content:space-between; align-items:center; padding:6px 10px; border-bottom:1px solid rgba(255,255,255,0.1); }
      .hidden-models-item:nth-child(even) { background: rgba(255,255,255,0.03); }
      .hidden-models-item:hover { background: rgba(59,130,246,0.15); }

      .chaturbate-hider-btn { padding:6px 12px; border:none; border-radius:6px; font-weight:600; cursor:pointer; transition: background .2s; background:#4b5563; color:#fff; }
      .chaturbate-hider-btn.primary { background:#3b82f6; color:#fff; }
      .chaturbate-hider-btn.primary:hover { background:#2563eb; }
      .chaturbate-hider-btn.secondary { background:#6b7280; color:#fff; }
      .chaturbate-hider-btn.secondary:hover { background:#4b5563; }
      .chaturbate-hider-btn.danger { background:#dc2626; color:#fff; }
      .chaturbate-hider-btn.danger:hover { background:#b91c1c; }

      .settings-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
      .settings-actions button { padding:6px 12px; border:none; border-radius:6px; background:#3b82f6; color:#fff; font-weight:600; cursor:pointer; transition:background .2s; }
      .settings-actions button:hover { background:#2563eb; }

      .enhancer-settings-group { display:flex; flex-direction:column; gap:10px; }
      .enhancer-toggle { display:flex; align-items:center; justify-content:space-between; background:rgba(255,255,255,0.05); padding:8px 12px; border-radius:6px; cursor:pointer; transition:background .2s; }
      .enhancer-toggle:hover { background:rgba(255,255,255,0.1); }
      .enhancer-toggle span { font-size:14px; font-weight:500; color:#fff; }
      .enhancer-toggle input[type="checkbox"] { appearance:none; width:18px; height:18px; border:2px solid #3b82f6; border-radius:4px; background:#1f2937; cursor:pointer; display:grid; place-items:center; transition:all .2s; }
      .enhancer-toggle input[type="checkbox"]:checked { background:#3b82f6; border-color:#2563eb; }
      .enhancer-toggle input[type="checkbox"]:checked::after { content:"✔"; font-size:12px; color:#fff; }

      .tokens-bar-container { display:flex; justify-content:space-between; align-items:center; background:rgba(0,0,0,0.4); padding:8px 12px; border-radius:8px; margin:8px 0; gap:12px; }
      .tokens-bar-left { font-size:13px; font-weight:600; color:#fff; }
      .tokens-bar-right { display:flex; gap:8px; }
      .token-action-btn { padding:6px 10px; border:none; border-radius:6px; font-weight:600; background:#3b82f6; color:#fff; cursor:pointer; transition:background .2s; }
      .token-action-btn:hover, .token-action-btn:focus { background:#2563eb; outline:none; }

      .hide-model-button { position:absolute; top:8px; left:8px; background:rgba(0,0,0,0.6); color:#fff; border:none; border-radius:50%; width:22px; height:22px; cursor:pointer; font-size:13px; font-weight:bold; line-height:1; transition:background .2s; }
      .hide-model-button:hover, .hide-model-button:focus { background:rgba(220,38,38,0.85); }

      .toast-notification { transition:opacity .25s; }
      `;
      document.head.appendChild(style);
    }
  }

  /* =========================
     Bootstrap
     ========================= */
  let enhancer = null;
  function initEnhancer() {
    if (enhancer) return;
    enhancer = new ChaturbateEnhancer();
    enhancer.init();
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initEnhancer);
  } else {
    setTimeout(initEnhancer, 80);
  }
})();